From ef6494f4c3c5d4eebea6ba90bfa5581c4c7da21f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 10:02:23 -0400 Subject: [PATCH 001/762] add override to SettingsVariable::Commit. Fixes error for clangd. --- scwx-qt/source/scwx/qt/settings/settings_variable.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index 581ebcbe..4141f96b 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -91,7 +91,7 @@ public: * @return true if the staged value was committed, false if no staged value * is present. */ - bool Commit(); + bool Commit() override; /** * Clears the staged value of the settings variable. From 3ab18392b39ec073dead1bac305c385b8ff2bf29 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 10:04:06 -0400 Subject: [PATCH 002/762] save alert dock's visibility on exit --- scwx-qt/source/scwx/qt/main/main_window.cpp | 3 +-- scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 14 ++++++++++++-- scwx-qt/source/scwx/qt/settings/ui_settings.hpp | 1 + scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp | 10 ++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 4046e3bd..f3534338 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -279,7 +279,6 @@ MainWindow::MainWindow(QWidget* parent) : // Configure Alert Dock p->alertDockWidget_ = new ui::AlertDockWidget(this); - p->alertDockWidget_->setVisible(false); addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_); // GPS Info Dialog @@ -294,7 +293,6 @@ MainWindow::MainWindow(QWidget* parent) : ui->menuView->insertAction(ui->actionAlerts, p->alertDockWidget_->toggleViewAction()); p->alertDockWidget_->toggleViewAction()->setText(tr("&Alerts")); - ui->actionAlerts->setVisible(false); ui->menuDebug->menuAction()->setVisible( settings::GeneralSettings::Instance().debug_enabled().GetValue()); @@ -803,6 +801,7 @@ void MainWindowImpl::ConfigureUiSettings() mapSettingsGroup_->SetExpanded( uiSettings.map_settings_expanded().GetValue()); timelineGroup_->SetExpanded(uiSettings.timeline_expanded().GetValue()); + alertDockWidget_->setVisible(uiSettings.alert_dock_visible().GetValue()); connect(level2ProductsGroup_, &ui::CollapsibleGroup::StateChanged, diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index dc131d96..647732fa 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -19,6 +19,7 @@ public: level3ProductsExpanded_.SetDefault(true); mapSettingsExpanded_.SetDefault(true); timelineExpanded_.SetDefault(true); + alertDockVisible_.SetDefault(false); } ~UiSettingsImpl() {} @@ -28,6 +29,7 @@ public: SettingsVariable level3ProductsExpanded_ {"level3_products_expanded"}; SettingsVariable mapSettingsExpanded_ {"map_settings_expanded"}; SettingsVariable timelineExpanded_ {"timeline_expanded"}; + SettingsVariable alertDockVisible_ {"alert_dock_visible"}; }; UiSettings::UiSettings() : @@ -37,7 +39,8 @@ UiSettings::UiSettings() : &p->level2SettingsExpanded_, &p->level3ProductsExpanded_, &p->mapSettingsExpanded_, - &p->timelineExpanded_}); + &p->timelineExpanded_, + &p->alertDockVisible_}); SetDefaults(); } UiSettings::~UiSettings() = default; @@ -70,6 +73,11 @@ SettingsVariable& UiSettings::timeline_expanded() const return p->timelineExpanded_; } +SettingsVariable& UiSettings::alert_dock_visible() const +{ + return p->alertDockVisible_; +} + bool UiSettings::Shutdown() { bool dataChanged = false; @@ -80,6 +88,7 @@ bool UiSettings::Shutdown() dataChanged |= p->level3ProductsExpanded_.Commit(); dataChanged |= p->mapSettingsExpanded_.Commit(); dataChanged |= p->timelineExpanded_.Commit(); + dataChanged |= p->alertDockVisible_.Commit(); return dataChanged; } @@ -96,7 +105,8 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->level2SettingsExpanded_ == rhs.p->level2SettingsExpanded_ && lhs.p->level3ProductsExpanded_ == rhs.p->level3ProductsExpanded_ && lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && - lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_); + lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && + lhs.p->alertDockVisible_ == rhs.p->alertDockVisible_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index e3045bcb..d03812a6 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -32,6 +32,7 @@ public: SettingsVariable& level3_products_expanded() const; SettingsVariable& map_settings_expanded() const; SettingsVariable& timeline_expanded() const; + SettingsVariable& alert_dock_visible() const; bool Shutdown(); diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index 61fd160a..242c847e 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,15 @@ AlertDockWidget::AlertDockWidget(QWidget* parent) : // Check Active Alerts and trigger signal ui->actionActiveAlerts->setChecked(true); + + // Update setting on visiblity change. + connect(toggleViewAction(), + &QAction::triggered, + this, + [](bool checked) { + settings::UiSettings::Instance().alert_dock_visible().StageValue( + checked); + }); } AlertDockWidget::~AlertDockWidget() From 3789845a366b34fe79f4aed99f524d1e7b5abb3a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 10:43:55 -0400 Subject: [PATCH 003/762] save radar toolbox dock's visibility on exit --- scwx-qt/source/scwx/qt/main/main_window.cpp | 18 +++++++++++++++++- .../source/scwx/qt/settings/ui_settings.cpp | 15 +++++++++++++-- .../source/scwx/qt/settings/ui_settings.hpp | 1 + 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index f3534338..02506176 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -277,9 +277,12 @@ MainWindow::MainWindow(QWidget* parent) : ui->radarSitePresetsButton->setVisible(!radarSitePresets.empty()); + auto& uiSettings = settings::UiSettings::Instance(); // Configure Alert Dock + bool alertDockVisible_ = uiSettings.alert_dock_visible().GetValue(); p->alertDockWidget_ = new ui::AlertDockWidget(this); addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_); + p->alertDockWidget_->setVisible(alertDockVisible_); // GPS Info Dialog p->gpsInfoDialog_ = new ui::GpsInfoDialog(this); @@ -289,10 +292,24 @@ MainWindow::MainWindow(QWidget* parent) : ui->radarToolboxDock->toggleViewAction()); ui->radarToolboxDock->toggleViewAction()->setText(tr("Radar &Toolbox")); ui->actionRadarToolbox->setVisible(false); + ui->radarToolboxDock->setVisible( + uiSettings.radar_toolbox_dock_visible().GetValue()); + + // Update dock setting on visiblity change. + connect(ui->radarToolboxDock->toggleViewAction(), + &QAction::triggered, + this, + [](bool checked) + { + settings::UiSettings::Instance() + .radar_toolbox_dock_visible() + .StageValue(checked); + }); ui->menuView->insertAction(ui->actionAlerts, p->alertDockWidget_->toggleViewAction()); p->alertDockWidget_->toggleViewAction()->setText(tr("&Alerts")); + ui->actionAlerts->setVisible(false); ui->menuDebug->menuAction()->setVisible( settings::GeneralSettings::Instance().debug_enabled().GetValue()); @@ -801,7 +818,6 @@ void MainWindowImpl::ConfigureUiSettings() mapSettingsGroup_->SetExpanded( uiSettings.map_settings_expanded().GetValue()); timelineGroup_->SetExpanded(uiSettings.timeline_expanded().GetValue()); - alertDockWidget_->setVisible(uiSettings.alert_dock_visible().GetValue()); connect(level2ProductsGroup_, &ui::CollapsibleGroup::StateChanged, diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 647732fa..865b5c4a 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -20,6 +20,7 @@ public: mapSettingsExpanded_.SetDefault(true); timelineExpanded_.SetDefault(true); alertDockVisible_.SetDefault(false); + radarToolboxDockVisible_.SetDefault(true); } ~UiSettingsImpl() {} @@ -30,6 +31,7 @@ public: SettingsVariable mapSettingsExpanded_ {"map_settings_expanded"}; SettingsVariable timelineExpanded_ {"timeline_expanded"}; SettingsVariable alertDockVisible_ {"alert_dock_visible"}; + SettingsVariable radarToolboxDockVisible_ {"radar_toolbox_dock_visible"}; }; UiSettings::UiSettings() : @@ -40,7 +42,8 @@ UiSettings::UiSettings() : &p->level3ProductsExpanded_, &p->mapSettingsExpanded_, &p->timelineExpanded_, - &p->alertDockVisible_}); + &p->alertDockVisible_, + &p->radarToolboxDockVisible_}); SetDefaults(); } UiSettings::~UiSettings() = default; @@ -78,6 +81,12 @@ SettingsVariable& UiSettings::alert_dock_visible() const return p->alertDockVisible_; } +SettingsVariable& UiSettings::radar_toolbox_dock_visible() const +{ + return p->radarToolboxDockVisible_; +} + + bool UiSettings::Shutdown() { bool dataChanged = false; @@ -89,6 +98,7 @@ bool UiSettings::Shutdown() dataChanged |= p->mapSettingsExpanded_.Commit(); dataChanged |= p->timelineExpanded_.Commit(); dataChanged |= p->alertDockVisible_.Commit(); + dataChanged |= p->radarToolboxDockVisible_.Commit(); return dataChanged; } @@ -106,7 +116,8 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->level3ProductsExpanded_ == rhs.p->level3ProductsExpanded_ && lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && - lhs.p->alertDockVisible_ == rhs.p->alertDockVisible_); + lhs.p->alertDockVisible_ == rhs.p->alertDockVisible_ && + lhs.p->radarToolboxDockVisible_ == rhs.p->radarToolboxDockVisible_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index d03812a6..f05ce5d3 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -33,6 +33,7 @@ public: SettingsVariable& map_settings_expanded() const; SettingsVariable& timeline_expanded() const; SettingsVariable& alert_dock_visible() const; + SettingsVariable& radar_toolbox_dock_visible() const; bool Shutdown(); From bc622363519c0703a50ae67e409a533bcdbf286c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 11:31:53 -0400 Subject: [PATCH 004/762] Save the general state of the docks (position, size, corner, etc) --- scwx-qt/source/scwx/qt/main/main_window.cpp | 20 ++++++++++++++++++- scwx-qt/source/scwx/qt/main/main_window.hpp | 1 + .../source/scwx/qt/settings/ui_settings.cpp | 14 +++++++++++-- .../source/scwx/qt/settings/ui_settings.hpp | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 02506176..acf05368 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -460,7 +460,25 @@ void MainWindow::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); - resizeDocks({ui->radarToolboxDock}, {194}, Qt::Horizontal); + // restore the UI state + std::string uiState = + settings::UiSettings::Instance().main_ui_state().GetValue(); + + bool restored = + restoreState(QByteArray::fromBase64(QByteArray::fromStdString(uiState))); + if (!restored) + { + resizeDocks({ui->radarToolboxDock}, {194}, Qt::Horizontal); + } +} + +void MainWindow::closeEvent(QCloseEvent* event) +{ + // save the UI state + QByteArray uiState = saveState().toBase64(); + settings::UiSettings::Instance().main_ui_state().StageValue(uiState.data()); + + QMainWindow::closeEvent(event); } void MainWindow::on_actionOpenNexrad_triggered() diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 33043308..c6ea3a5f 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -29,6 +29,7 @@ public: void keyPressEvent(QKeyEvent* ev) override final; void keyReleaseEvent(QKeyEvent* ev) override final; void showEvent(QShowEvent* event) override; + void closeEvent(QCloseEvent *event) override; signals: void ActiveMapMoved(double latitude, double longitude); diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 865b5c4a..c5536230 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -21,6 +21,7 @@ public: timelineExpanded_.SetDefault(true); alertDockVisible_.SetDefault(false); radarToolboxDockVisible_.SetDefault(true); + mainUIState_.SetDefault(""); } ~UiSettingsImpl() {} @@ -32,6 +33,7 @@ public: SettingsVariable timelineExpanded_ {"timeline_expanded"}; SettingsVariable alertDockVisible_ {"alert_dock_visible"}; SettingsVariable radarToolboxDockVisible_ {"radar_toolbox_dock_visible"}; + SettingsVariable mainUIState_ {"main_ui_state"}; }; UiSettings::UiSettings() : @@ -43,7 +45,8 @@ UiSettings::UiSettings() : &p->mapSettingsExpanded_, &p->timelineExpanded_, &p->alertDockVisible_, - &p->radarToolboxDockVisible_}); + &p->radarToolboxDockVisible_, + &p->mainUIState_}); SetDefaults(); } UiSettings::~UiSettings() = default; @@ -86,6 +89,11 @@ SettingsVariable& UiSettings::radar_toolbox_dock_visible() const return p->radarToolboxDockVisible_; } +SettingsVariable& UiSettings::main_ui_state() const +{ + return p->mainUIState_; +} + bool UiSettings::Shutdown() { @@ -99,6 +107,7 @@ bool UiSettings::Shutdown() dataChanged |= p->timelineExpanded_.Commit(); dataChanged |= p->alertDockVisible_.Commit(); dataChanged |= p->radarToolboxDockVisible_.Commit(); + dataChanged |= p->mainUIState_.Commit(); return dataChanged; } @@ -117,7 +126,8 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && lhs.p->alertDockVisible_ == rhs.p->alertDockVisible_ && - lhs.p->radarToolboxDockVisible_ == rhs.p->radarToolboxDockVisible_); + lhs.p->radarToolboxDockVisible_ == rhs.p->radarToolboxDockVisible_ && + lhs.p->mainUIState_ == rhs.p->mainUIState_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index f05ce5d3..9fe9880b 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -34,6 +34,7 @@ public: SettingsVariable& timeline_expanded() const; SettingsVariable& alert_dock_visible() const; SettingsVariable& radar_toolbox_dock_visible() const; + SettingsVariable& main_ui_state() const; bool Shutdown(); From a94dc82c1f68d94a13744a8fcb478956390cf3cf Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 11:46:39 -0400 Subject: [PATCH 005/762] Revert "save alert dock's visibility on exit" This reverts commit 3ab18392b39ec073dead1bac305c385b8ff2bf29. --- scwx-qt/source/scwx/qt/main/main_window.cpp | 1 + scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 9 --------- scwx-qt/source/scwx/qt/settings/ui_settings.hpp | 1 - scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp | 10 ---------- 4 files changed, 1 insertion(+), 20 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index acf05368..913dd5e3 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -281,6 +281,7 @@ MainWindow::MainWindow(QWidget* parent) : // Configure Alert Dock bool alertDockVisible_ = uiSettings.alert_dock_visible().GetValue(); p->alertDockWidget_ = new ui::AlertDockWidget(this); + p->alertDockWidget_->setVisible(false); addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_); p->alertDockWidget_->setVisible(alertDockVisible_); diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index c5536230..316f31eb 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -19,7 +19,6 @@ public: level3ProductsExpanded_.SetDefault(true); mapSettingsExpanded_.SetDefault(true); timelineExpanded_.SetDefault(true); - alertDockVisible_.SetDefault(false); radarToolboxDockVisible_.SetDefault(true); mainUIState_.SetDefault(""); } @@ -31,7 +30,6 @@ public: SettingsVariable level3ProductsExpanded_ {"level3_products_expanded"}; SettingsVariable mapSettingsExpanded_ {"map_settings_expanded"}; SettingsVariable timelineExpanded_ {"timeline_expanded"}; - SettingsVariable alertDockVisible_ {"alert_dock_visible"}; SettingsVariable radarToolboxDockVisible_ {"radar_toolbox_dock_visible"}; SettingsVariable mainUIState_ {"main_ui_state"}; }; @@ -44,7 +42,6 @@ UiSettings::UiSettings() : &p->level3ProductsExpanded_, &p->mapSettingsExpanded_, &p->timelineExpanded_, - &p->alertDockVisible_, &p->radarToolboxDockVisible_, &p->mainUIState_}); SetDefaults(); @@ -79,10 +76,6 @@ SettingsVariable& UiSettings::timeline_expanded() const return p->timelineExpanded_; } -SettingsVariable& UiSettings::alert_dock_visible() const -{ - return p->alertDockVisible_; -} SettingsVariable& UiSettings::radar_toolbox_dock_visible() const { @@ -105,7 +98,6 @@ bool UiSettings::Shutdown() dataChanged |= p->level3ProductsExpanded_.Commit(); dataChanged |= p->mapSettingsExpanded_.Commit(); dataChanged |= p->timelineExpanded_.Commit(); - dataChanged |= p->alertDockVisible_.Commit(); dataChanged |= p->radarToolboxDockVisible_.Commit(); dataChanged |= p->mainUIState_.Commit(); @@ -125,7 +117,6 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->level3ProductsExpanded_ == rhs.p->level3ProductsExpanded_ && lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && - lhs.p->alertDockVisible_ == rhs.p->alertDockVisible_ && lhs.p->radarToolboxDockVisible_ == rhs.p->radarToolboxDockVisible_ && lhs.p->mainUIState_ == rhs.p->mainUIState_); } diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index 9fe9880b..6be8d48a 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -32,7 +32,6 @@ public: SettingsVariable& level3_products_expanded() const; SettingsVariable& map_settings_expanded() const; SettingsVariable& timeline_expanded() const; - SettingsVariable& alert_dock_visible() const; SettingsVariable& radar_toolbox_dock_visible() const; SettingsVariable& main_ui_state() const; diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index 242c847e..61fd160a 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -82,15 +81,6 @@ AlertDockWidget::AlertDockWidget(QWidget* parent) : // Check Active Alerts and trigger signal ui->actionActiveAlerts->setChecked(true); - - // Update setting on visiblity change. - connect(toggleViewAction(), - &QAction::triggered, - this, - [](bool checked) { - settings::UiSettings::Instance().alert_dock_visible().StageValue( - checked); - }); } AlertDockWidget::~AlertDockWidget() From f1bc8d2b13d88ea7f7cfbf0a3629e04f733b62d7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 11:49:11 -0400 Subject: [PATCH 006/762] Revert "save radar toolbox dock's visibility on exit" This reverts commit 3789845a366b34fe79f4aed99f524d1e7b5abb3a. --- scwx-qt/source/scwx/qt/main/main_window.cpp | 18 +----------------- .../source/scwx/qt/settings/ui_settings.cpp | 12 ------------ .../source/scwx/qt/settings/ui_settings.hpp | 1 - 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 913dd5e3..5c31c1e0 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -277,13 +277,10 @@ MainWindow::MainWindow(QWidget* parent) : ui->radarSitePresetsButton->setVisible(!radarSitePresets.empty()); - auto& uiSettings = settings::UiSettings::Instance(); // Configure Alert Dock - bool alertDockVisible_ = uiSettings.alert_dock_visible().GetValue(); p->alertDockWidget_ = new ui::AlertDockWidget(this); p->alertDockWidget_->setVisible(false); addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_); - p->alertDockWidget_->setVisible(alertDockVisible_); // GPS Info Dialog p->gpsInfoDialog_ = new ui::GpsInfoDialog(this); @@ -293,24 +290,10 @@ MainWindow::MainWindow(QWidget* parent) : ui->radarToolboxDock->toggleViewAction()); ui->radarToolboxDock->toggleViewAction()->setText(tr("Radar &Toolbox")); ui->actionRadarToolbox->setVisible(false); - ui->radarToolboxDock->setVisible( - uiSettings.radar_toolbox_dock_visible().GetValue()); - - // Update dock setting on visiblity change. - connect(ui->radarToolboxDock->toggleViewAction(), - &QAction::triggered, - this, - [](bool checked) - { - settings::UiSettings::Instance() - .radar_toolbox_dock_visible() - .StageValue(checked); - }); ui->menuView->insertAction(ui->actionAlerts, p->alertDockWidget_->toggleViewAction()); p->alertDockWidget_->toggleViewAction()->setText(tr("&Alerts")); - ui->actionAlerts->setVisible(false); ui->menuDebug->menuAction()->setVisible( settings::GeneralSettings::Instance().debug_enabled().GetValue()); @@ -837,6 +820,7 @@ void MainWindowImpl::ConfigureUiSettings() mapSettingsGroup_->SetExpanded( uiSettings.map_settings_expanded().GetValue()); timelineGroup_->SetExpanded(uiSettings.timeline_expanded().GetValue()); + alertDockWidget_->setVisible(uiSettings.alert_dock_visible().GetValue()); connect(level2ProductsGroup_, &ui::CollapsibleGroup::StateChanged, diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 316f31eb..3ef1a23d 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -19,7 +19,6 @@ public: level3ProductsExpanded_.SetDefault(true); mapSettingsExpanded_.SetDefault(true); timelineExpanded_.SetDefault(true); - radarToolboxDockVisible_.SetDefault(true); mainUIState_.SetDefault(""); } @@ -30,7 +29,6 @@ public: SettingsVariable level3ProductsExpanded_ {"level3_products_expanded"}; SettingsVariable mapSettingsExpanded_ {"map_settings_expanded"}; SettingsVariable timelineExpanded_ {"timeline_expanded"}; - SettingsVariable radarToolboxDockVisible_ {"radar_toolbox_dock_visible"}; SettingsVariable mainUIState_ {"main_ui_state"}; }; @@ -42,7 +40,6 @@ UiSettings::UiSettings() : &p->level3ProductsExpanded_, &p->mapSettingsExpanded_, &p->timelineExpanded_, - &p->radarToolboxDockVisible_, &p->mainUIState_}); SetDefaults(); } @@ -76,18 +73,11 @@ SettingsVariable& UiSettings::timeline_expanded() const return p->timelineExpanded_; } - -SettingsVariable& UiSettings::radar_toolbox_dock_visible() const -{ - return p->radarToolboxDockVisible_; -} - SettingsVariable& UiSettings::main_ui_state() const { return p->mainUIState_; } - bool UiSettings::Shutdown() { bool dataChanged = false; @@ -98,7 +88,6 @@ bool UiSettings::Shutdown() dataChanged |= p->level3ProductsExpanded_.Commit(); dataChanged |= p->mapSettingsExpanded_.Commit(); dataChanged |= p->timelineExpanded_.Commit(); - dataChanged |= p->radarToolboxDockVisible_.Commit(); dataChanged |= p->mainUIState_.Commit(); return dataChanged; @@ -117,7 +106,6 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->level3ProductsExpanded_ == rhs.p->level3ProductsExpanded_ && lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && - lhs.p->radarToolboxDockVisible_ == rhs.p->radarToolboxDockVisible_ && lhs.p->mainUIState_ == rhs.p->mainUIState_); } diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index 6be8d48a..6227a93a 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -32,7 +32,6 @@ public: SettingsVariable& level3_products_expanded() const; SettingsVariable& map_settings_expanded() const; SettingsVariable& timeline_expanded() const; - SettingsVariable& radar_toolbox_dock_visible() const; SettingsVariable& main_ui_state() const; bool Shutdown(); From 6f2a087ef8a92164e535a89f05fdd4bc9e3065f9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 11:54:01 -0400 Subject: [PATCH 007/762] revert code re-added by last revert (should revert commits in revers order) --- scwx-qt/source/scwx/qt/main/main_window.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 5c31c1e0..64c87c3c 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -820,7 +820,6 @@ void MainWindowImpl::ConfigureUiSettings() mapSettingsGroup_->SetExpanded( uiSettings.map_settings_expanded().GetValue()); timelineGroup_->SetExpanded(uiSettings.timeline_expanded().GetValue()); - alertDockWidget_->setVisible(uiSettings.alert_dock_visible().GetValue()); connect(level2ProductsGroup_, &ui::CollapsibleGroup::StateChanged, From 83c833e89c7ffbc8bfe998ee37eb176c4b6a4aa1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 12:04:09 -0400 Subject: [PATCH 008/762] updated test data with needed settings --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 5a91ded6..14be7d78 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 5a91ded677d4032b0de9370ed767a16708c0ecff +Subproject commit 14be7d7836e1bd2a9038a2ad6002bfe31e9cd0f7 From 06efa2da58b82a5612fadcb11b1ac9445b48f1fe Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 12 Sep 2024 13:18:22 -0400 Subject: [PATCH 009/762] Reverted change which added extra item to menu bar --- scwx-qt/source/scwx/qt/main/main_window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 64c87c3c..e8217290 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -279,7 +279,6 @@ MainWindow::MainWindow(QWidget* parent) : // Configure Alert Dock p->alertDockWidget_ = new ui::AlertDockWidget(this); - p->alertDockWidget_->setVisible(false); addDockWidget(Qt::BottomDockWidgetArea, p->alertDockWidget_); // GPS Info Dialog @@ -294,6 +293,7 @@ MainWindow::MainWindow(QWidget* parent) : ui->menuView->insertAction(ui->actionAlerts, p->alertDockWidget_->toggleViewAction()); p->alertDockWidget_->toggleViewAction()->setText(tr("&Alerts")); + ui->actionAlerts->setVisible(false); ui->menuDebug->menuAction()->setVisible( settings::GeneralSettings::Instance().debug_enabled().GetValue()); From 449912e655687b94f4428cf630aa74e6fca1133f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 17 Sep 2024 10:47:26 -0400 Subject: [PATCH 010/762] update test/data for saving geomotry --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 14be7d78..20a1ca17 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 14be7d7836e1bd2a9038a2ad6002bfe31e9cd0f7 +Subproject commit 20a1ca1752499222d33869e37148321936ca6354 From 1b16b903a0d90703c291468baf61b8ef1d3e30b6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 17 Sep 2024 10:48:59 -0400 Subject: [PATCH 011/762] added saving geometry --- scwx-qt/source/scwx/qt/main/main_window.cpp | 17 ++++++++++++++--- scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 14 ++++++++++++-- scwx-qt/source/scwx/qt/settings/ui_settings.hpp | 1 + 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index e8217290..d511241a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -443,10 +443,15 @@ void MainWindow::keyReleaseEvent(QKeyEvent* ev) void MainWindow::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); + auto& uiSettings = settings::UiSettings::Instance(); + + // restore the geometry state + std::string uiGeometry = uiSettings.main_ui_geometry().GetValue(); + restoreGeometry( + QByteArray::fromBase64(QByteArray::fromStdString(uiGeometry))); // restore the UI state - std::string uiState = - settings::UiSettings::Instance().main_ui_state().GetValue(); + std::string uiState = uiSettings.main_ui_state().GetValue(); bool restored = restoreState(QByteArray::fromBase64(QByteArray::fromStdString(uiState))); @@ -458,9 +463,15 @@ void MainWindow::showEvent(QShowEvent* event) void MainWindow::closeEvent(QCloseEvent* event) { + auto& uiSettings = settings::UiSettings::Instance(); + + // save the UI geometry + QByteArray uiGeometry = saveGeometry().toBase64(); + uiSettings.main_ui_geometry().StageValue(uiGeometry.data()); + // save the UI state QByteArray uiState = saveState().toBase64(); - settings::UiSettings::Instance().main_ui_state().StageValue(uiState.data()); + uiSettings.main_ui_state().StageValue(uiState.data()); QMainWindow::closeEvent(event); } diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 3ef1a23d..eca2c8e1 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -20,6 +20,7 @@ public: mapSettingsExpanded_.SetDefault(true); timelineExpanded_.SetDefault(true); mainUIState_.SetDefault(""); + mainUIGeometry_.SetDefault(""); } ~UiSettingsImpl() {} @@ -30,6 +31,7 @@ public: SettingsVariable mapSettingsExpanded_ {"map_settings_expanded"}; SettingsVariable timelineExpanded_ {"timeline_expanded"}; SettingsVariable mainUIState_ {"main_ui_state"}; + SettingsVariable mainUIGeometry_ {"main_ui_geometry"}; }; UiSettings::UiSettings() : @@ -40,7 +42,8 @@ UiSettings::UiSettings() : &p->level3ProductsExpanded_, &p->mapSettingsExpanded_, &p->timelineExpanded_, - &p->mainUIState_}); + &p->mainUIState_, + &p->mainUIGeometry_}); SetDefaults(); } UiSettings::~UiSettings() = default; @@ -78,6 +81,11 @@ SettingsVariable& UiSettings::main_ui_state() const return p->mainUIState_; } +SettingsVariable& UiSettings::main_ui_geometry() const +{ + return p->mainUIGeometry_; +} + bool UiSettings::Shutdown() { bool dataChanged = false; @@ -89,6 +97,7 @@ bool UiSettings::Shutdown() dataChanged |= p->mapSettingsExpanded_.Commit(); dataChanged |= p->timelineExpanded_.Commit(); dataChanged |= p->mainUIState_.Commit(); + dataChanged |= p->mainUIGeometry_.Commit(); return dataChanged; } @@ -106,7 +115,8 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->level3ProductsExpanded_ == rhs.p->level3ProductsExpanded_ && lhs.p->mapSettingsExpanded_ == rhs.p->mapSettingsExpanded_ && lhs.p->timelineExpanded_ == rhs.p->timelineExpanded_ && - lhs.p->mainUIState_ == rhs.p->mainUIState_); + lhs.p->mainUIState_ == rhs.p->mainUIState_ && + lhs.p->mainUIGeometry_ == rhs.p->mainUIGeometry_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index 6227a93a..0a9f95ef 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -33,6 +33,7 @@ public: SettingsVariable& map_settings_expanded() const; SettingsVariable& timeline_expanded() const; SettingsVariable& main_ui_state() const; + SettingsVariable& main_ui_geometry() const; bool Shutdown(); From 5d051e127f04dcfe93942fc479115e7aeac642d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 02:52:56 +0000 Subject: [PATCH 012/762] Update dependency boost to v1.86.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index b18f2d31..f993d4d4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -2,7 +2,7 @@ from conans import ConanFile class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") - requires = ("boost/1.85.0", + requires = ("boost/1.86.0", "cpr/1.10.5", "fontconfig/2.15.0", "freetype/2.13.2", From be5dad0195085e43c3229928d0b7e5889c261db6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 04:19:16 +0000 Subject: [PATCH 013/762] Update dependency openssl to v3.3.2 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index f993d4d4..0345ecf2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -13,7 +13,7 @@ class SupercellWxConan(ConanFile): "gtest/1.15.0", "libcurl/8.9.1", "libxml2/2.12.7", - "openssl/3.3.1", + "openssl/3.3.2", "re2/20240702", "spdlog/1.14.1", "sqlite3/3.46.0", From c363f5a1b7962e5d02aed3e0985936c30e72b7bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:16:35 +0000 Subject: [PATCH 014/762] Update dependency libcurl to v8.10.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 0345ecf2..6fed7906 100644 --- a/conanfile.py +++ b/conanfile.py @@ -11,7 +11,7 @@ class SupercellWxConan(ConanFile): "glew/2.2.0", "glm/cci.20230113", "gtest/1.15.0", - "libcurl/8.9.1", + "libcurl/8.10.0", "libxml2/2.12.7", "openssl/3.3.2", "re2/20240702", From 9a1b95e63e679589c05109be4b26892daee654ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:29:46 +0000 Subject: [PATCH 015/762] Update dependency sqlite3 to v3.46.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 6fed7906..3d781661 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ class SupercellWxConan(ConanFile): "openssl/3.3.2", "re2/20240702", "spdlog/1.14.1", - "sqlite3/3.46.0", + "sqlite3/3.46.1", "vulkan-loader/1.3.243.0", "zlib/1.3.1") generators = ("cmake", From a7b6a28c4217e16ed8acca2098571d7e1ee5f60a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 00:08:41 +0000 Subject: [PATCH 016/762] Update dependency geographiclib to v2.4 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 3d781661..49cb37ed 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,7 @@ class SupercellWxConan(ConanFile): "cpr/1.10.5", "fontconfig/2.15.0", "freetype/2.13.2", - "geographiclib/2.3", + "geographiclib/2.4", "geos/3.12.2", "glew/2.2.0", "glm/cci.20230113", From d545656176a3bbc62aa88061f477a58d77fd8224 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 02:40:00 +0000 Subject: [PATCH 017/762] Update dependency geos to v3.13.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 49cb37ed..a559e800 100644 --- a/conanfile.py +++ b/conanfile.py @@ -7,7 +7,7 @@ class SupercellWxConan(ConanFile): "fontconfig/2.15.0", "freetype/2.13.2", "geographiclib/2.4", - "geos/3.12.2", + "geos/3.13.0", "glew/2.2.0", "glm/cci.20230113", "gtest/1.15.0", From 7f672f709b740c7e11209731844b7b841214732d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:25:48 -0400 Subject: [PATCH 018/762] add needed overrides for clang --- .../include/scwx/awips/text_product_message.hpp | 2 +- .../rpg/digital_radial_data_array_packet.hpp | 16 ++++++++-------- .../wsr88d/rpg/graphic_alphanumeric_block.hpp | 2 +- .../scwx/wsr88d/rpg/graphic_product_message.hpp | 2 +- .../scwx/wsr88d/rpg/product_symbology_block.hpp | 2 +- .../scwx/wsr88d/rpg/radar_coded_message.hpp | 2 +- .../scwx/wsr88d/rpg/radial_data_packet.hpp | 16 ++++++++-------- .../wsr88d/rpg/tabular_alphanumeric_block.hpp | 2 +- .../scwx/wsr88d/rpg/tabular_product_message.hpp | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index d402cfd5..dec4af09 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -105,7 +105,7 @@ public: std::chrono::system_clock::time_point segment_event_begin(std::size_t s) const; - std::size_t data_size() const; + std::size_t data_size() const override; bool Parse(std::istream& is) override; diff --git a/wxdata/include/scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp index 9b94ba34..135b81ec 100644 --- a/wxdata/include/scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp @@ -29,16 +29,16 @@ public: operator=(DigitalRadialDataArrayPacket&&) noexcept; uint16_t packet_code() const override; - uint16_t index_of_first_range_bin() const; - uint16_t number_of_range_bins() const; - int16_t i_center_of_sweep() const; - int16_t j_center_of_sweep() const; + uint16_t index_of_first_range_bin() const override; + uint16_t number_of_range_bins() const override; + int16_t i_center_of_sweep() const override; + int16_t j_center_of_sweep() const override; float range_scale_factor() const; - uint16_t number_of_radials() const; + uint16_t number_of_radials() const override; - float start_angle(uint16_t r) const; - float delta_angle(uint16_t r) const; - const std::vector& level(uint16_t r) const; + float start_angle(uint16_t r) const override; + float delta_angle(uint16_t r) const override; + const std::vector& level(uint16_t r) const override; size_t data_size() const override; diff --git a/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp b/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp index 9b1f2019..5823d66d 100644 --- a/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp @@ -35,7 +35,7 @@ public: const std::vector>>& page_list() const; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static constexpr size_t SIZE = 102u; diff --git a/wxdata/include/scwx/wsr88d/rpg/graphic_product_message.hpp b/wxdata/include/scwx/wsr88d/rpg/graphic_product_message.hpp index a14df940..cf52badd 100644 --- a/wxdata/include/scwx/wsr88d/rpg/graphic_product_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/graphic_product_message.hpp @@ -30,7 +30,7 @@ public: GraphicProductMessage(GraphicProductMessage&&) noexcept; GraphicProductMessage& operator=(GraphicProductMessage&&) noexcept; - std::shared_ptr description_block() const; + std::shared_ptr description_block() const override; std::shared_ptr symbology_block() const; std::shared_ptr graphic_block() const; std::shared_ptr tabular_block() const; diff --git a/wxdata/include/scwx/wsr88d/rpg/product_symbology_block.hpp b/wxdata/include/scwx/wsr88d/rpg/product_symbology_block.hpp index d597532c..69eac4c6 100644 --- a/wxdata/include/scwx/wsr88d/rpg/product_symbology_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/product_symbology_block.hpp @@ -34,7 +34,7 @@ public: size_t data_size() const override; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static constexpr size_t SIZE = 102u; diff --git a/wxdata/include/scwx/wsr88d/rpg/radar_coded_message.hpp b/wxdata/include/scwx/wsr88d/rpg/radar_coded_message.hpp index f1a22b5e..7d7846f0 100644 --- a/wxdata/include/scwx/wsr88d/rpg/radar_coded_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/radar_coded_message.hpp @@ -27,7 +27,7 @@ public: RadarCodedMessage(RadarCodedMessage&&) noexcept; RadarCodedMessage& operator=(RadarCodedMessage&&) noexcept; - std::shared_ptr description_block() const; + std::shared_ptr description_block() const override; bool Parse(std::istream& is) override; diff --git a/wxdata/include/scwx/wsr88d/rpg/radial_data_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/radial_data_packet.hpp index dd6e61ec..34540dc8 100644 --- a/wxdata/include/scwx/wsr88d/rpg/radial_data_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/radial_data_packet.hpp @@ -27,16 +27,16 @@ public: RadialDataPacket& operator=(RadialDataPacket&&) noexcept; uint16_t packet_code() const override; - uint16_t index_of_first_range_bin() const; - uint16_t number_of_range_bins() const; - int16_t i_center_of_sweep() const; - int16_t j_center_of_sweep() const; + uint16_t index_of_first_range_bin() const override; + uint16_t number_of_range_bins() const override; + int16_t i_center_of_sweep() const override; + int16_t j_center_of_sweep() const override; float scale_factor() const; - uint16_t number_of_radials() const; + uint16_t number_of_radials() const override; - float start_angle(uint16_t r) const; - float delta_angle(uint16_t r) const; - const std::vector& level(uint16_t r) const; + float start_angle(uint16_t r) const override; + float delta_angle(uint16_t r) const override; + const std::vector& level(uint16_t r) const override; size_t data_size() const override; diff --git a/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp b/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp index 0d41fe62..267bd0e5 100644 --- a/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp @@ -33,7 +33,7 @@ public: const std::vector>& page_list() const; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; bool Parse(std::istream& is, bool skipHeader); static constexpr size_t SIZE = 102u; diff --git a/wxdata/include/scwx/wsr88d/rpg/tabular_product_message.hpp b/wxdata/include/scwx/wsr88d/rpg/tabular_product_message.hpp index 006d5e82..ff9231cc 100644 --- a/wxdata/include/scwx/wsr88d/rpg/tabular_product_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/tabular_product_message.hpp @@ -28,7 +28,7 @@ public: TabularProductMessage(TabularProductMessage&&) noexcept; TabularProductMessage& operator=(TabularProductMessage&&) noexcept; - std::shared_ptr description_block() const; + std::shared_ptr description_block() const override; std::shared_ptr tabular_block() const; bool Parse(std::istream& is) override; From 838032c8d6164a5e28272b3f7edd28d301b371d1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:28:37 -0400 Subject: [PATCH 019/762] updated files to use chronos for time with clang --- wxdata/source/scwx/awips/coded_time_motion_location.cpp | 2 +- wxdata/source/scwx/awips/pvtec.cpp | 2 +- wxdata/source/scwx/gr/placefile.cpp | 2 +- wxdata/source/scwx/network/dir_list.cpp | 2 +- wxdata/source/scwx/provider/aws_level2_data_provider.cpp | 2 +- wxdata/source/scwx/provider/aws_level3_data_provider.cpp | 2 +- wxdata/source/scwx/provider/warnings_provider.cpp | 2 +- wxdata/source/scwx/util/time.cpp | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/wxdata/source/scwx/awips/coded_time_motion_location.cpp b/wxdata/source/scwx/awips/coded_time_motion_location.cpp index 61e5ba27..ebf8cdf7 100644 --- a/wxdata/source/scwx/awips/coded_time_motion_location.cpp +++ b/wxdata/source/scwx/awips/coded_time_motion_location.cpp @@ -107,7 +107,7 @@ bool CodedTimeMotionLocation::Parse(const StringRange& lines, { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/awips/pvtec.cpp b/wxdata/source/scwx/awips/pvtec.cpp index a661a037..8152500e 100644 --- a/wxdata/source/scwx/awips/pvtec.cpp +++ b/wxdata/source/scwx/awips/pvtec.cpp @@ -143,7 +143,7 @@ bool PVtec::Parse(const std::string& s) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 1806cdfe..87167189 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -284,7 +284,7 @@ void Placefile::Impl::ProcessLine(const std::string& line) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/network/dir_list.cpp b/wxdata/source/scwx/network/dir_list.cpp index 3c6dabd4..24a61c6e 100644 --- a/wxdata/source/scwx/network/dir_list.cpp +++ b/wxdata/source/scwx/network/dir_list.cpp @@ -200,7 +200,7 @@ void DirListSAXHandler::Characters(void* userData, const xmlChar* ch, int len) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index 6ac939c0..ed83a590 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -82,7 +82,7 @@ AwsLevel2DataProvider::GetTimePointFromKey(const std::string& key) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp index 901eb41d..7d159703 100644 --- a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp @@ -110,7 +110,7 @@ AwsLevel3DataProvider::GetTimePointFromKey(const std::string& key) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index d187763a..e8a8c3ad 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -73,7 +73,7 @@ WarningsProvider::ListFiles(std::chrono::system_clock::time_point newerThan) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 5cf091fe..52ff076f 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -59,7 +59,7 @@ std::string TimeString(std::chrono::system_clock::time_point time, { using namespace std::chrono; -#if defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) # define FORMAT_STRING_24_HOUR "{:%Y-%m-%d %H:%M:%S %Z}" # define FORMAT_STRING_12_HOUR "{:%Y-%m-%d %I:%M:%S %p %Z}" namespace date = std::chrono; @@ -128,7 +128,7 @@ TryParseDateTime(const std::string& dateTimeFormat, const std::string& str) { using namespace std::chrono; -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clang__)) using namespace date; #endif From c92b881d4aa47ee1112ed0a7c6449a858933fdc0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:29:42 -0400 Subject: [PATCH 020/762] remove std::move for temps (This needs checking) --- .../source/scwx/wsr88d/rda/digital_radar_data_generic.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp index 4870c1c3..54c0ba2c 100644 --- a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp +++ b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp @@ -721,15 +721,15 @@ bool DigitalRadarDataGeneric::Parse(std::istream& is) { case DataBlockType::Volume: p->volumeDataBlock_ = - std::move(VolumeDataBlock::Create(dataBlockType, dataName, is)); + VolumeDataBlock::Create(dataBlockType, dataName, is); break; case DataBlockType::Elevation: p->elevationDataBlock_ = - std::move(ElevationDataBlock::Create(dataBlockType, dataName, is)); + ElevationDataBlock::Create(dataBlockType, dataName, is); break; case DataBlockType::Radial: p->radialDataBlock_ = - std::move(RadialDataBlock::Create(dataBlockType, dataName, is)); + RadialDataBlock::Create(dataBlockType, dataName, is); break; case DataBlockType::MomentRef: case DataBlockType::MomentVel: @@ -739,7 +739,7 @@ bool DigitalRadarDataGeneric::Parse(std::istream& is) case DataBlockType::MomentRho: case DataBlockType::MomentCfp: p->momentDataBlock_[dataBlock] = - std::move(MomentDataBlock::Create(dataBlockType, dataName, is)); + MomentDataBlock::Create(dataBlockType, dataName, is); break; default: logger_->warn("Unknown data name: {}", dataName); From 97b5b6f4cac20acf2f722694776d011bb30bb98d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:34:41 -0400 Subject: [PATCH 021/762] moved settings initialization into cpp files --- scwx-qt/source/scwx/qt/settings/settings_container.cpp | 4 +++- scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 10 +++++++++- scwx-qt/source/scwx/qt/settings/settings_variable.cpp | 10 +++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.cpp b/scwx-qt/source/scwx/qt/settings/settings_container.cpp index 1eb5b06a..f16af980 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.cpp @@ -1,4 +1,4 @@ -#define SETTINGS_CONTAINER_IMPLEMENTATION +//#define SETTINGS_CONTAINER_IMPLEMENTATION #include #include @@ -172,6 +172,8 @@ bool SettingsContainer::Equals(const SettingsVariableBase& o) const p->elementMaximum_ == v.p->elementMaximum_; } +template class SettingsContainer>; + } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 9a24c5ae..fc6d46ce 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -1,4 +1,4 @@ -#define SETTINGS_INTERFACE_IMPLEMENTATION +//#define SETTINGS_INTERFACE_IMPLEMENTATION #include #include @@ -616,6 +616,14 @@ void SettingsInterface::Impl::UpdateResetButton() } } +template class SettingsInterface; +template class SettingsInterface; +template class SettingsInterface; +template class SettingsInterface; + +// Containers are not to be used directly +template class SettingsInterface>; + } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 1a7160f5..195dbd01 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -1,4 +1,4 @@ -#define SETTINGS_VARIABLE_IMPLEMENTATION +//#define SETTINGS_VARIABLE_IMPLEMENTATION #include #include @@ -402,6 +402,14 @@ bool SettingsVariable::Equals(const SettingsVariableBase& o) const p->maximum_ == v.p->maximum_; } +template class SettingsVariable; +template class SettingsVariable; +template class SettingsVariable; +template class SettingsVariable; + +// Containers are not to be used directly +template class SettingsVariable>; + } // namespace settings } // namespace qt } // namespace scwx From e683154f020db73347202800f889ba6ef52a084a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:35:41 -0400 Subject: [PATCH 022/762] Added default value for clang (has unreachable enum value) --- scwx-qt/source/scwx/qt/util/q_file_buffer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/util/q_file_buffer.cpp b/scwx-qt/source/scwx/qt/util/q_file_buffer.cpp index 5d55361c..1808d283 100644 --- a/scwx-qt/source/scwx/qt/util/q_file_buffer.cpp +++ b/scwx-qt/source/scwx/qt/util/q_file_buffer.cpp @@ -224,6 +224,9 @@ QFileBuffer::pos_type QFileBuffer::seekoff(off_type off, break; } + default: + logger_->error("Got invalid seekdir value"); + break; } if (newPos != static_cast(-1)) From 80070109dbdaa95fe055899e4baebe69696e8be7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:38:34 -0400 Subject: [PATCH 023/762] removed brackets from basic initilizations --- scwx-qt/source/scwx/qt/map/map_provider.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index 586d012c..4b9964f9 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -24,10 +24,10 @@ static const std::vector mapboxDrawBelow_ { static const std::unordered_map mapProviderInfo_ { {MapProvider::Mapbox, MapProviderInfo { - .mapProvider_ {MapProvider::Mapbox}, + .mapProvider_ = MapProvider::Mapbox, .cacheDbName_ {"mbgl-cache.db"}, - .providerTemplate_ { - QMapLibre::Settings::ProviderTemplate::MapboxProvider}, + .providerTemplate_ = + QMapLibre::Settings::ProviderTemplate::MapboxProvider, .mapStyles_ { {.name_ {"Streets"}, .url_ {"mapbox://styles/mapbox/streets-v11"}, @@ -117,10 +117,10 @@ static const std::unordered_map mapProviderInfo_ { .drawBelow_ {mapboxDrawBelow_}}}}}, {MapProvider::MapTiler, MapProviderInfo { - .mapProvider_ {MapProvider::MapTiler}, + .mapProvider_ = MapProvider::MapTiler, .cacheDbName_ {"maptiler-cache.db"}, - .providerTemplate_ { - QMapLibre::Settings::ProviderTemplate::MapTilerProvider}, + .providerTemplate_ = + QMapLibre::Settings::ProviderTemplate::MapTilerProvider, .mapStyles_ { {.name_ {"Satellite"}, .url_ {"https://api.maptiler.com/maps/hybrid/style.json"}, From 3be82e1d1f0a6c36456d53c107fd788027422d74 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:40:02 -0400 Subject: [PATCH 024/762] added algorithm header to test file for find --- test/source/scwx/common/products.test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/source/scwx/common/products.test.cpp b/test/source/scwx/common/products.test.cpp index 5b945bf3..bec8a29b 100644 --- a/test/source/scwx/common/products.test.cpp +++ b/test/source/scwx/common/products.test.cpp @@ -1,6 +1,7 @@ #include #include +#include namespace scwx { From f2c9de4b36b0b2fd5120311dc979c832f558f794 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:42:28 -0400 Subject: [PATCH 025/762] updated functions to avoid unused arguments/captures --- scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp | 4 ++-- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index ae2c8866..98f091ac 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -420,7 +420,7 @@ void Level3ProductsWidgetImpl::UpdateCategorySelection( std::for_each(categoryButtons_.cbegin(), categoryButtons_.cend(), - [&, this](auto& toolButton) + [&](auto& toolButton) { if (toolButton->text().toStdString() == categoryName) { @@ -444,7 +444,7 @@ void Level3ProductsWidgetImpl::UpdateProductSelection( std::for_each(awipsProductMap_.cbegin(), awipsProductMap_.cend(), - [&, this](const auto& pair) + [&](const auto& pair) { if (pair.second == awipsId) { diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 8159fb69..6b5e6071 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -784,7 +784,7 @@ void SettingsDialogImpl::SetupPalettesColorTablesTab() QObject::connect(dialog, &QFileDialog::fileSelected, self_, - [this, lineEdit](const QString& file) + [lineEdit](const QString& file) { QString path = QDir::toNativeSeparators(file); @@ -1386,6 +1386,7 @@ void SettingsDialogImpl::LoadColorTablePreview(const std::string& key, void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit, QFrame* frame) { + (void)frame; QColorDialog* dialog = new QColorDialog(self_); dialog->setAttribute(Qt::WA_DeleteOnClose); @@ -1401,7 +1402,7 @@ void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit, QFrame* frame) dialog, &QColorDialog::colorSelected, self_, - [this, lineEdit, frame](const QColor& color) + [lineEdit](const QColor& color) { QString colorName = color.name(QColor::NameFormat::HexArgb); From a2efe1a92860d0c92fead0fc0db9b2cb4fd8e157 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:44:24 -0400 Subject: [PATCH 026/762] add arguments to ignore certian clang errors --- scwx-qt/scwx-qt.cmake | 4 ++-- wxdata/wxdata.cmake | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 78d45217..8c50f01e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -599,11 +599,11 @@ target_include_directories(supercell-wx PUBLIC ${scwx-qt_SOURCE_DIR}/source) target_compile_options(scwx-qt PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror> + $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=delete-non-abstract-non-virtual-dtor> ) target_compile_options(supercell-wx PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror> + $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=delete-non-abstract-non-virtual-dtor> ) if (MSVC) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 47ada181..ce2cb3a8 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -260,7 +260,7 @@ target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include) target_compile_options(wxdata PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror> + $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=unused-parameter> ) if (MSVC) From f5a5d3172f053427ca46cc2ecc7fd40e80b8400f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 11:45:18 -0400 Subject: [PATCH 027/762] comment out some unused variables/constants --- scwx-qt/source/scwx/qt/gl/draw/icons.cpp | 2 +- scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp | 8 ++++---- scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp | 2 +- scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 4 ++-- scwx-qt/source/scwx/qt/model/layer_model.cpp | 4 ++-- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 4 ++-- scwx-qt/source/scwx/qt/util/time.cpp | 4 ++-- wxdata/source/scwx/provider/warnings_provider.cpp | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp index 473b8a79..408f703f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp @@ -24,7 +24,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr std::size_t kNumRectangles = 1; static constexpr std::size_t kNumTriangles = kNumRectangles * 2; static constexpr std::size_t kVerticesPerTriangle = 3; -static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; +//static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; static constexpr std::size_t kPointsPerVertex = 10; static constexpr std::size_t kPointsPerTexCoord = 3; static constexpr std::size_t kIconBufferLength = diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index 8fdce9f1..e51164df 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -18,13 +18,13 @@ namespace draw static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_lines"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static constexpr std::size_t kNumRectangles = 1; -static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +//static constexpr std::size_t kNumRectangles = 1; +//static constexpr std::size_t kNumTriangles = kNumRectangles * 2; static constexpr std::size_t kVerticesPerTriangle = 3; static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; static constexpr std::size_t kPointsPerVertex = 9; -static constexpr std::size_t kBufferLength = - kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; +//static constexpr std::size_t kBufferLength = +// kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; // Threshold, start time, end time static constexpr std::size_t kIntegersPerVertex_ = 3; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index 2944fa20..2dee9f62 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -31,7 +31,7 @@ static constexpr std::size_t kIntegersPerVertex_ = 3; static constexpr std::size_t kTessVertexScreenX_ = 0; static constexpr std::size_t kTessVertexScreenY_ = 1; -static constexpr std::size_t kTessVertexScreenZ_ = 2; +//static constexpr std::size_t kTessVertexScreenZ_ = 2; static constexpr std::size_t kTessVertexXOffset_ = 3; static constexpr std::size_t kTessVertexYOffset_ = 4; static constexpr std::size_t kTessVertexR_ = 5; diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 79d03ce6..9f6d5e4d 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -31,8 +31,8 @@ namespace qt namespace map { -static constexpr uint32_t MAX_RADIALS = 720; -static constexpr uint32_t MAX_DATA_MOMENT_GATES = 1840; +//static constexpr uint32_t MAX_RADIALS = 720; +//static constexpr uint32_t MAX_DATA_MOMENT_GATES = 1840; static const std::string logPrefix_ = "scwx::qt::map::radar_product_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 35f188a9..3df8c138 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -66,12 +66,12 @@ static const std::vector kImmovableLayers_ { {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, }; -static const std::array kAlertPhenomena_ { +/*static const std::array kAlertPhenomena_ { awips::Phenomenon::Tornado, awips::Phenomenon::SnowSquall, awips::Phenomenon::SevereThunderstorm, awips::Phenomenon::FlashFlood, - awips::Phenomenon::Marine}; + awips::Phenomenon::Marine};*/ class LayerModel::Impl { diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 2d8e93b0..366effa6 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -29,8 +29,8 @@ static const std::string kRadarProductGroupName_ {"radar_product_group"}; static const std::string kRadarProductName_ {"radar_product"}; static const std::string kDefaultMapStyle_ {"?"}; -static constexpr common::RadarProductGroup kDefaultRadarProductGroup_ = - common::RadarProductGroup::Level3; +//static constexpr common::RadarProductGroup kDefaultRadarProductGroup_ = +// common::RadarProductGroup::Level3; static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; diff --git a/scwx-qt/source/scwx/qt/util/time.cpp b/scwx-qt/source/scwx/qt/util/time.cpp index f34c6ea5..46dbd3b5 100644 --- a/scwx-qt/source/scwx/qt/util/time.cpp +++ b/scwx-qt/source/scwx/qt/util/time.cpp @@ -12,8 +12,8 @@ std::chrono::sys_days SysDays(const QDate& date) using namespace std::chrono; using sys_days = time_point; constexpr auto julianEpoch = sys_days {-4713y / November / 24d}; - constexpr auto unixEpoch = sys_days {1970y / January / 1d}; - constexpr auto offset = std::chrono::days(julianEpoch - unixEpoch); + //constexpr auto unixEpoch = sys_days {1970y / January / 1d}; + //constexpr auto offset = std::chrono::days(julianEpoch - unixEpoch); return std::chrono::sys_days(std::chrono::days(date.toJulianDay()) + julianEpoch); diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index e8a8c3ad..3c030189 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -30,7 +30,7 @@ namespace provider static const std::string logPrefix_ = "scwx::provider::warnings_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); -static constexpr std::chrono::seconds kUpdatePeriod_ {15}; +//static constexpr std::chrono::seconds kUpdatePeriod_ {15}; class WarningsProvider::Impl { From a4c945803cd653643820e265a92e4db7b8f8397a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 12:06:24 -0400 Subject: [PATCH 028/762] added first pass clang build on github --- .github/workflows/ci.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba82d35a..eda0ee8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,23 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 + - name: linux64_gcc + os: ubuntu-22.04 + build_type: Release + env_cc: clang-17 + env_cxx: clang++-17 + compiler: clang + qt_version: 6.7.2 + qt_arch_aqt: linux_gcc_64 + qt_arch_dir: gcc_64 + qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport + qt_tools: '' + conan_arch: x86_64 + conan_compiler: clang + conan_compiler_version: 17 + conan_compiler_runtime: '' + conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + artifact_suffix: linux-clang-x64 name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} @@ -94,7 +111,8 @@ jobs: run: | sudo apt-get install doxygen \ libfuse2 \ - ninja-build + ninja-build \ + clang-17 - name: Setup Python Environment shell: pwsh From dfab86a4ad6334cf963dede33d5843cfd2c60a69 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 12:09:09 -0400 Subject: [PATCH 029/762] updated name of linux64_clang --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eda0ee8d..658131f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 - - name: linux64_gcc + - name: linux64_clang os: ubuntu-22.04 build_type: Release env_cc: clang-17 From 5f27d1e4844de9e94bb344042311cb0fa3d2236e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 12:16:34 -0400 Subject: [PATCH 030/762] updated ubuntu to newest version for clang-17 --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 658131f9..5d82a9ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: conan_package_manager: '' artifact_suffix: windows-x64 - name: linux64_gcc - os: ubuntu-22.04 + os: ubuntu-24.04 build_type: Release env_cc: gcc-11 env_cxx: g++-11 @@ -57,7 +57,7 @@ jobs: conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 - name: linux64_clang - os: ubuntu-22.04 + os: ubuntu-24.04 build_type: Release env_cc: clang-17 env_cxx: clang++-17 @@ -106,7 +106,7 @@ jobs: vsversion: ${{ matrix.msvc_version }} - name: Setup Ubuntu Environment - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' shell: bash run: | sudo apt-get install doxygen \ @@ -147,7 +147,7 @@ jobs: ninja supercell-wx wxtest - name: Separate Debug Symbols (Linux) - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' shell: bash run: | cd build/ @@ -166,7 +166,7 @@ jobs: cmake --install . --component supercell-wx - name: Collect Artifacts - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' shell: bash run: | pushd supercell-wx/ @@ -198,14 +198,14 @@ jobs: path: ${{ github.workspace }}/build/bin/*.pdb - name: Upload Artifacts (Linux) - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' uses: actions/upload-artifact@v4 with: name: supercell-wx-${{ matrix.artifact_suffix }} path: ${{ github.workspace }}/supercell-wx-${{ matrix.artifact_suffix }}.tar.gz - name: Upload Debug Artifacts (Linux) - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' uses: actions/upload-artifact@v4 with: name: supercell-wx-debug-${{ matrix.artifact_suffix }} @@ -228,7 +228,7 @@ jobs: path: ${{ github.workspace }}/build/supercell-wx-*.msi* - name: Build AppImage (Linux) - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' env: APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx/ LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*x86_64.AppImage.zsync @@ -252,7 +252,7 @@ jobs: rm -f linuxdeploy-x86_64.AppImage - name: Upload AppImage (Linux) - if: matrix.os == 'ubuntu-22.04' + if: matrix.os == 'ubuntu-24.04' uses: actions/upload-artifact@v4 with: name: supercell-wx-appimage-x64 From b16a299b16d8a44e90f8c930dded76462be845dc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 12:37:32 -0400 Subject: [PATCH 031/762] add install of gcc-11 for newer ubuntu version --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d82a9ad..86a4f6e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,7 +112,8 @@ jobs: sudo apt-get install doxygen \ libfuse2 \ ninja-build \ - clang-17 + clang-17 \ + gcc-11 - name: Setup Python Environment shell: pwsh From 5c3bcaa0ceb83bd496da274da2b226a75a7d0da2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 13:14:41 -0400 Subject: [PATCH 032/762] fix time.cpp using incorrect date time library --- wxdata/source/scwx/util/time.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 52ff076f..ea664e2e 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -59,7 +59,7 @@ std::string TimeString(std::chrono::system_clock::time_point time, { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (defined(_MSC_VER) || defined(__clang__)) # define FORMAT_STRING_24_HOUR "{:%Y-%m-%d %H:%M:%S %Z}" # define FORMAT_STRING_12_HOUR "{:%Y-%m-%d %I:%M:%S %p %Z}" namespace date = std::chrono; From 0585ab828cc16f8d4984485f40db685b64cd38bb Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 13:33:05 -0400 Subject: [PATCH 033/762] modified more time based files to work on clang --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 2 +- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- wxdata/include/scwx/util/time.hpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index c0cb4636..27b678f8 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -271,7 +271,7 @@ size_t RadarSite::ReadConfig(const std::string& path) try { -#if defined(_MSC_VER) +#if (defined(_MSC_VER) || defined(__clang__)) using namespace std::chrono; #else using namespace date; diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 595cda6d..377c7aff 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -413,7 +413,7 @@ const scwx::util::time_zone* RadarProductManager::default_time_zone() const } case types::DefaultTimeZone::Local: -#if defined(_MSC_VER) +#if (defined(_MSC_VER) || defined(__clang__)) return std::chrono::current_zone(); #else return date::current_zone(); diff --git a/wxdata/include/scwx/util/time.hpp b/wxdata/include/scwx/util/time.hpp index 62242589..780a31c3 100644 --- a/wxdata/include/scwx/util/time.hpp +++ b/wxdata/include/scwx/util/time.hpp @@ -15,7 +15,7 @@ namespace scwx namespace util { -#if defined(_MSC_VER) +#if (defined(_MSC_VER) || defined(__clang__)) typedef std::chrono::time_zone time_zone; #else typedef date::time_zone time_zone; From 2cd0a16028bd8ef53c7f3f338ad5cf3917518d26 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 13:41:00 -0400 Subject: [PATCH 034/762] added g++ to needed downloads --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86a4f6e5..1fe05221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,8 @@ jobs: libfuse2 \ ninja-build \ clang-17 \ - gcc-11 + gcc-11 \ + g++-11 - name: Setup Python Environment shell: pwsh From 797f26e4610af1e8a9900c058221d600ff5ec300 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 14:37:35 -0400 Subject: [PATCH 035/762] moved warning avoidance over to c files --- scwx-qt/scwx-qt.cmake | 4 ++-- scwx-qt/source/scwx/qt/gl/gl.hpp | 9 +++++++++ scwx-qt/source/scwx/qt/map/alert_layer.hpp | 10 ++++++++++ scwx-qt/source/scwx/qt/settings/text_settings.cpp | 9 +++++++++ wxdata/source/scwx/common/color_table.cpp | 10 ++++++++++ wxdata/wxdata.cmake | 2 +- 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 8c50f01e..78d45217 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -599,11 +599,11 @@ target_include_directories(supercell-wx PUBLIC ${scwx-qt_SOURCE_DIR}/source) target_compile_options(scwx-qt PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=delete-non-abstract-non-virtual-dtor> + $<$>:-Wall -Wextra -Wpedantic -Werror> ) target_compile_options(supercell-wx PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=delete-non-abstract-non-virtual-dtor> + $<$>:-Wall -Wextra -Wpedantic -Werror> ) if (MSVC) diff --git a/scwx-qt/source/scwx/qt/gl/gl.hpp b/scwx-qt/source/scwx/qt/gl/gl.hpp index e87454c8..55601b36 100644 --- a/scwx-qt/source/scwx/qt/gl/gl.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl.hpp @@ -1,7 +1,16 @@ #pragma once +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" +#endif + #include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + #define SCWX_GL_CHECK_ERROR() \ { \ GLenum err; \ diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index 99609210..b55c06e8 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -1,6 +1,16 @@ #pragma once +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" +#endif + #include + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + #include #include diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index 942ad4f8..e17031d6 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -1,8 +1,17 @@ +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" +#endif + #include #include #include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + namespace scwx { namespace qt diff --git a/wxdata/source/scwx/common/color_table.cpp b/wxdata/source/scwx/common/color_table.cpp index aff48f3d..32a4684f 100644 --- a/wxdata/source/scwx/common/color_table.cpp +++ b/wxdata/source/scwx/common/color_table.cpp @@ -10,8 +10,18 @@ #include #include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-parameter" +#endif + #include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + #include namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index ce2cb3a8..47ada181 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -260,7 +260,7 @@ target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include) target_compile_options(wxdata PRIVATE $<$:/W4 /WX> - $<$>:-Wall -Wextra -Wpedantic -Werror -Wno-error=unused-parameter> + $<$>:-Wall -Wextra -Wpedantic -Werror> ) if (MSVC) From d9f9e89b19af206312219f374060878bd3a532b4 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Sep 2024 14:38:53 -0400 Subject: [PATCH 036/762] do not include date/date.h for clang --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 2 +- scwx-qt/source/scwx/qt/main/main_window.cpp | 2 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 +- scwx-qt/source/scwx/qt/view/level3_product_view.cpp | 2 +- wxdata/source/scwx/awips/coded_time_motion_location.cpp | 2 +- wxdata/source/scwx/awips/pvtec.cpp | 2 +- wxdata/source/scwx/gr/placefile.cpp | 2 +- wxdata/source/scwx/network/dir_list.cpp | 2 +- wxdata/source/scwx/provider/aws_level2_data_provider.cpp | 2 +- wxdata/source/scwx/provider/aws_level3_data_provider.cpp | 2 +- wxdata/source/scwx/provider/warnings_provider.cpp | 2 +- wxdata/source/scwx/util/time.cpp | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index 27b678f8..e06f91e0 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -10,7 +10,7 @@ #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d511241a..b242b41b 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -52,7 +52,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index b2fada95..95e8fc62 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -22,7 +22,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index 551d04e4..83a8210f 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -17,7 +17,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/awips/coded_time_motion_location.cpp b/wxdata/source/scwx/awips/coded_time_motion_location.cpp index ebf8cdf7..ebe162f1 100644 --- a/wxdata/source/scwx/awips/coded_time_motion_location.cpp +++ b/wxdata/source/scwx/awips/coded_time_motion_location.cpp @@ -13,7 +13,7 @@ #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/awips/pvtec.cpp b/wxdata/source/scwx/awips/pvtec.cpp index 8152500e..485fecb8 100644 --- a/wxdata/source/scwx/awips/pvtec.cpp +++ b/wxdata/source/scwx/awips/pvtec.cpp @@ -17,7 +17,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 87167189..529f5f9b 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -20,7 +20,7 @@ #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/network/dir_list.cpp b/wxdata/source/scwx/network/dir_list.cpp index 24a61c6e..5134cb57 100644 --- a/wxdata/source/scwx/network/dir_list.cpp +++ b/wxdata/source/scwx/network/dir_list.cpp @@ -11,7 +11,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index ed83a590..4538dc53 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -5,7 +5,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp index 7d159703..2e020886 100644 --- a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp @@ -12,7 +12,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 3c030189..1b8b109f 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -14,7 +14,7 @@ #include #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index ea664e2e..665f2f1a 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -17,7 +17,7 @@ #include -#if !defined(_MSC_VER) +#if !(defined(_MSC_VER) || defined(__clange__)) # include #endif From fb94934d1b8f83e148e2942a719cd9d2ccec6cc9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 08:51:31 -0400 Subject: [PATCH 037/762] removed more warnings from header files --- wxdata/include/scwx/awips/coded_location.hpp | 9 +++++++++ wxdata/include/scwx/awips/coded_time_motion_location.hpp | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/wxdata/include/scwx/awips/coded_location.hpp b/wxdata/include/scwx/awips/coded_location.hpp index 0ac03d95..0b147444 100644 --- a/wxdata/include/scwx/awips/coded_location.hpp +++ b/wxdata/include/scwx/awips/coded_location.hpp @@ -7,8 +7,17 @@ #include #include +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-parameter" +#endif + #include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + namespace scwx { namespace awips diff --git a/wxdata/include/scwx/awips/coded_time_motion_location.hpp b/wxdata/include/scwx/awips/coded_time_motion_location.hpp index 6b7b9a19..123ea6e3 100644 --- a/wxdata/include/scwx/awips/coded_time_motion_location.hpp +++ b/wxdata/include/scwx/awips/coded_time_motion_location.hpp @@ -8,8 +8,17 @@ #include #include +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-parameter" +#endif + #include +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + namespace scwx { namespace awips From 403d7fdc2a6dde087d17b5603a5c75f00c452736 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 09:58:11 -0400 Subject: [PATCH 038/762] use a diffrent name for the AppImage artifact for gcc and clang --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fe05221..cacad99b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,7 +257,7 @@ jobs: if: matrix.os == 'ubuntu-24.04' uses: actions/upload-artifact@v4 with: - name: supercell-wx-appimage-x64 + name: supercell-wx-appimage-${{ matrix.artifact_suffix }} path: ${{ github.workspace }}/*-x86_64.AppImage* - name: Test Supercell Wx From ba7a518b4d1b30723d9f0072b2c5e301c7d8c4d1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 11:55:14 -0400 Subject: [PATCH 039/762] Add comiler.libcxx setting for conan build to avoid clang build from rebuilding some libraries while building supercell-wx --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cacad99b..dcaa8736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,7 @@ jobs: conan_arch: x86_64 conan_compiler: gcc conan_compiler_version: 11 + conan_compiler_libcxx: libstdc++ conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 @@ -69,6 +70,7 @@ jobs: qt_tools: '' conan_arch: x86_64 conan_compiler: clang + conan_compiler_libcxx: libstdc++ conan_compiler_version: 17 conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True @@ -134,6 +136,7 @@ jobs: --settings build_type=${{ matrix.build_type }} ` --settings compiler="${{ matrix.conan_compiler }}" ` --settings compiler.version=${{ matrix.conan_compiler_version }} ` + --settings compiler.libcxx=${{ matrix.conan_compiler_libcxx }} ` ${{ matrix.conan_compiler_runtime }} ` ${{ matrix.conan_package_manager }} From 4a7bff0d37ea5a9e191b7cfa6178b6531eb84b42 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 12:13:27 -0400 Subject: [PATCH 040/762] Modified stdcxx to work with windows --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcaa8736..36e2ccc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: conan_compiler: Visual Studio conan_compiler_version: 17 conan_compiler_runtime: --settings compiler.runtime=MD + conan_compiler_libcxx: '' conan_package_manager: '' artifact_suffix: windows-x64 - name: linux64_gcc @@ -53,7 +54,7 @@ jobs: conan_arch: x86_64 conan_compiler: gcc conan_compiler_version: 11 - conan_compiler_libcxx: libstdc++ + conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 @@ -70,8 +71,8 @@ jobs: qt_tools: '' conan_arch: x86_64 conan_compiler: clang - conan_compiler_libcxx: libstdc++ conan_compiler_version: 17 + conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-clang-x64 @@ -136,7 +137,7 @@ jobs: --settings build_type=${{ matrix.build_type }} ` --settings compiler="${{ matrix.conan_compiler }}" ` --settings compiler.version=${{ matrix.conan_compiler_version }} ` - --settings compiler.libcxx=${{ matrix.conan_compiler_libcxx }} ` + ${{ matrix.conan_compiler_libcxx }} ` ${{ matrix.conan_compiler_runtime }} ` ${{ matrix.conan_package_manager }} From 46e0adee153751973f0bfd8d92a5494206640a71 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 12:38:13 -0400 Subject: [PATCH 041/762] updated clang build to use stdc++11 so it is consistant and does not rebuild --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36e2ccc5..24f0dc56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: conan_arch: x86_64 conan_compiler: clang conan_compiler_version: 17 - conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ + conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-clang-x64 From 02bf6731054d8f8168562e75bdbc1dd82140d312 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 13:27:27 -0400 Subject: [PATCH 042/762] remove commented out unused variables --- scwx-qt/source/scwx/qt/gl/draw/icons.cpp | 1 - scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp | 4 ---- scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp | 1 - scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 3 --- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 2 -- scwx-qt/source/scwx/qt/settings/settings_container.cpp | 2 -- scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 2 -- scwx-qt/source/scwx/qt/settings/settings_variable.cpp | 2 -- scwx-qt/source/scwx/qt/util/time.cpp | 2 -- wxdata/source/scwx/provider/warnings_provider.cpp | 2 -- 10 files changed, 21 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp index 408f703f..d7f6b64c 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp @@ -24,7 +24,6 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr std::size_t kNumRectangles = 1; static constexpr std::size_t kNumTriangles = kNumRectangles * 2; static constexpr std::size_t kVerticesPerTriangle = 3; -//static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; static constexpr std::size_t kPointsPerVertex = 10; static constexpr std::size_t kPointsPerTexCoord = 3; static constexpr std::size_t kIconBufferLength = diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index e51164df..58b86c37 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -18,13 +18,9 @@ namespace draw static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_lines"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -//static constexpr std::size_t kNumRectangles = 1; -//static constexpr std::size_t kNumTriangles = kNumRectangles * 2; static constexpr std::size_t kVerticesPerTriangle = 3; static constexpr std::size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; static constexpr std::size_t kPointsPerVertex = 9; -//static constexpr std::size_t kBufferLength = -// kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; // Threshold, start time, end time static constexpr std::size_t kIntegersPerVertex_ = 3; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index 2dee9f62..4f7f2c81 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -31,7 +31,6 @@ static constexpr std::size_t kIntegersPerVertex_ = 3; static constexpr std::size_t kTessVertexScreenX_ = 0; static constexpr std::size_t kTessVertexScreenY_ = 1; -//static constexpr std::size_t kTessVertexScreenZ_ = 2; static constexpr std::size_t kTessVertexXOffset_ = 3; static constexpr std::size_t kTessVertexYOffset_ = 4; static constexpr std::size_t kTessVertexR_ = 5; diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 9f6d5e4d..be564926 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -31,9 +31,6 @@ namespace qt namespace map { -//static constexpr uint32_t MAX_RADIALS = 720; -//static constexpr uint32_t MAX_DATA_MOMENT_GATES = 1840; - static const std::string logPrefix_ = "scwx::qt::map::radar_product_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 366effa6..f0efea78 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -29,8 +29,6 @@ static const std::string kRadarProductGroupName_ {"radar_product_group"}; static const std::string kRadarProductName_ {"radar_product"}; static const std::string kDefaultMapStyle_ {"?"}; -//static constexpr common::RadarProductGroup kDefaultRadarProductGroup_ = -// common::RadarProductGroup::Level3; static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.cpp b/scwx-qt/source/scwx/qt/settings/settings_container.cpp index f16af980..cf8f9c18 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.cpp @@ -1,5 +1,3 @@ -//#define SETTINGS_CONTAINER_IMPLEMENTATION - #include #include diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index fc6d46ce..61a793e8 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -1,5 +1,3 @@ -//#define SETTINGS_INTERFACE_IMPLEMENTATION - #include #include #include diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 195dbd01..71db421e 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -1,5 +1,3 @@ -//#define SETTINGS_VARIABLE_IMPLEMENTATION - #include #include diff --git a/scwx-qt/source/scwx/qt/util/time.cpp b/scwx-qt/source/scwx/qt/util/time.cpp index 46dbd3b5..ea3afa1d 100644 --- a/scwx-qt/source/scwx/qt/util/time.cpp +++ b/scwx-qt/source/scwx/qt/util/time.cpp @@ -12,8 +12,6 @@ std::chrono::sys_days SysDays(const QDate& date) using namespace std::chrono; using sys_days = time_point; constexpr auto julianEpoch = sys_days {-4713y / November / 24d}; - //constexpr auto unixEpoch = sys_days {1970y / January / 1d}; - //constexpr auto offset = std::chrono::days(julianEpoch - unixEpoch); return std::chrono::sys_days(std::chrono::days(date.toJulianDay()) + julianEpoch); diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 1b8b109f..ecc3714e 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -30,8 +30,6 @@ namespace provider static const std::string logPrefix_ = "scwx::provider::warnings_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); -//static constexpr std::chrono::seconds kUpdatePeriod_ {15}; - class WarningsProvider::Impl { public: From d48baab44cc86be87d1d2cc996212c86600f749d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 20 Sep 2024 13:29:59 -0400 Subject: [PATCH 043/762] remove now unused (moved) template initialization --- scwx-qt/source/scwx/qt/settings/settings_container.hpp | 4 ---- scwx-qt/source/scwx/qt/settings/settings_interface.hpp | 10 ---------- scwx-qt/source/scwx/qt/settings/settings_variable.hpp | 10 ---------- 3 files changed, 24 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.hpp b/scwx-qt/source/scwx/qt/settings/settings_container.hpp index b35201c6..9c2ea487 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.hpp @@ -99,10 +99,6 @@ private: std::unique_ptr p; }; -#ifdef SETTINGS_CONTAINER_IMPLEMENTATION -template class SettingsContainer>; -#endif - } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index b049dcc1..2092b1b4 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -131,16 +131,6 @@ private: std::unique_ptr p; }; -#ifdef SETTINGS_INTERFACE_IMPLEMENTATION -template class SettingsInterface; -template class SettingsInterface; -template class SettingsInterface; -template class SettingsInterface; - -// Containers are not to be used directly -template class SettingsInterface>; -#endif - } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index 4141f96b..72d61dde 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -239,16 +239,6 @@ private: std::unique_ptr p; }; -#ifdef SETTINGS_VARIABLE_IMPLEMENTATION -template class SettingsVariable; -template class SettingsVariable; -template class SettingsVariable; -template class SettingsVariable; - -// Containers are not to be used directly -template class SettingsVariable>; -#endif - } // namespace settings } // namespace qt } // namespace scwx From c5d004aa4819072bff5b806678a7b431ba511ad2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 21 Sep 2024 12:44:24 -0400 Subject: [PATCH 044/762] fixed __clange__ spelling mistake --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 2 +- scwx-qt/source/scwx/qt/main/main_window.cpp | 2 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 +- scwx-qt/source/scwx/qt/view/level3_product_view.cpp | 2 +- wxdata/source/scwx/awips/coded_time_motion_location.cpp | 2 +- wxdata/source/scwx/awips/pvtec.cpp | 2 +- wxdata/source/scwx/gr/placefile.cpp | 2 +- wxdata/source/scwx/network/dir_list.cpp | 2 +- wxdata/source/scwx/provider/aws_level2_data_provider.cpp | 2 +- wxdata/source/scwx/provider/aws_level3_data_provider.cpp | 2 +- wxdata/source/scwx/provider/warnings_provider.cpp | 2 +- wxdata/source/scwx/util/time.cpp | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index e06f91e0..782436df 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -10,7 +10,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index b242b41b..711aed80 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -52,7 +52,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 95e8fc62..0151b6cb 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -22,7 +22,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index 83a8210f..a700b206 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -17,7 +17,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/awips/coded_time_motion_location.cpp b/wxdata/source/scwx/awips/coded_time_motion_location.cpp index ebe162f1..40358eef 100644 --- a/wxdata/source/scwx/awips/coded_time_motion_location.cpp +++ b/wxdata/source/scwx/awips/coded_time_motion_location.cpp @@ -13,7 +13,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/awips/pvtec.cpp b/wxdata/source/scwx/awips/pvtec.cpp index 485fecb8..426a58d3 100644 --- a/wxdata/source/scwx/awips/pvtec.cpp +++ b/wxdata/source/scwx/awips/pvtec.cpp @@ -17,7 +17,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 529f5f9b..70881299 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -20,7 +20,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/network/dir_list.cpp b/wxdata/source/scwx/network/dir_list.cpp index 5134cb57..e4a2c4c3 100644 --- a/wxdata/source/scwx/network/dir_list.cpp +++ b/wxdata/source/scwx/network/dir_list.cpp @@ -11,7 +11,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index 4538dc53..87f0d2a2 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -5,7 +5,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp index 2e020886..e052b907 100644 --- a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp @@ -12,7 +12,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index ecc3714e..443582c7 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -14,7 +14,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 665f2f1a..dcf763d2 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -17,7 +17,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clange__)) +#if !(defined(_MSC_VER) || defined(__clang__)) # include #endif From 3b5323cea0cd749a5793495355ae3bbcac8a2089 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 21 Sep 2024 13:01:56 -0400 Subject: [PATCH 045/762] remove warning supression and fix missing virtual destructor --- scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp | 2 +- scwx-qt/source/scwx/qt/gl/gl.hpp | 9 --------- scwx-qt/source/scwx/qt/map/alert_layer.hpp | 9 --------- scwx-qt/source/scwx/qt/settings/settings_category.hpp | 2 +- scwx-qt/source/scwx/qt/settings/text_settings.cpp | 9 --------- 5 files changed, 2 insertions(+), 29 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp index f7df44c4..281b189a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp @@ -22,7 +22,7 @@ class DrawItem { public: explicit DrawItem(OpenGLFunctions& gl); - ~DrawItem(); + virtual ~DrawItem(); DrawItem(const DrawItem&) = delete; DrawItem& operator=(const DrawItem&) = delete; diff --git a/scwx-qt/source/scwx/qt/gl/gl.hpp b/scwx-qt/source/scwx/qt/gl/gl.hpp index 55601b36..e87454c8 100644 --- a/scwx-qt/source/scwx/qt/gl/gl.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl.hpp @@ -1,16 +1,7 @@ #pragma once -#if defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" -#endif - #include -#if defined(__clang__) -# pragma clang diagnostic pop -#endif - #define SCWX_GL_CHECK_ERROR() \ { \ GLenum err; \ diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index b55c06e8..d51391e3 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -1,16 +1,7 @@ #pragma once -#if defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" -#endif - #include -#if defined(__clang__) -# pragma clang diagnostic pop -#endif - #include #include diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index d7c86abd..2da7b9ab 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -18,7 +18,7 @@ class SettingsCategory { public: explicit SettingsCategory(const std::string& name); - ~SettingsCategory(); + virtual ~SettingsCategory(); SettingsCategory(const SettingsCategory&) = delete; SettingsCategory& operator=(const SettingsCategory&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index e17031d6..942ad4f8 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -1,17 +1,8 @@ -#if defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdelete-non-abstract-non-virtual-dtor" -#endif - #include #include #include -#if defined(__clang__) -# pragma clang diagnostic pop -#endif - namespace scwx { namespace qt From d5703fe522efb91978fb7fb62a9d144617a09140 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 21 Sep 2024 13:08:38 -0400 Subject: [PATCH 046/762] removed unsued paramater instead of ignoring it --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 6b5e6071..cd72b4ee 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -181,7 +181,7 @@ public: void SetupTextTab(); void SetupHotkeysTab(); - void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); + void ShowColorDialog(QLineEdit* lineEdit); void UpdateRadarDialogLocation(const std::string& id); void UpdateAlertRadarDialogLocation(const std::string& id); @@ -913,12 +913,12 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() &QAbstractButton::clicked, self_, [=, this]() - { ShowColorDialog(activeEdit, activeFrame); }); + { ShowColorDialog(activeEdit); }); QObject::connect(inactiveButton, &QAbstractButton::clicked, self_, [=, this]() - { ShowColorDialog(inactiveEdit, inactiveFrame); }); + { ShowColorDialog(inactiveEdit); }); } } @@ -1384,9 +1384,8 @@ void SettingsDialogImpl::LoadColorTablePreview(const std::string& key, }); } -void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit, QFrame* frame) +void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit) { - (void)frame; QColorDialog* dialog = new QColorDialog(self_); dialog->setAttribute(Qt::WA_DeleteOnClose); From 4b4cc3b50061ea335b92d6914b08b6b6653b162b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 21 Sep 2024 13:12:38 -0400 Subject: [PATCH 047/762] removed commented out unused variables --- scwx-qt/source/scwx/qt/model/layer_model.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 3df8c138..999a2de9 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -66,13 +66,6 @@ static const std::vector kImmovableLayers_ { {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, }; -/*static const std::array kAlertPhenomena_ { - awips::Phenomenon::Tornado, - awips::Phenomenon::SnowSquall, - awips::Phenomenon::SevereThunderstorm, - awips::Phenomenon::FlashFlood, - awips::Phenomenon::Marine};*/ - class LayerModel::Impl { public: From 79e39021a6f79b6fb733e6e58cc5c246922f7e00 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 22 Sep 2024 12:02:38 -0400 Subject: [PATCH 048/762] switch to using __cpp_lib_chrono to determine if chrono or date/date.h should be used --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 4 ++-- scwx-qt/source/scwx/qt/main/main_window.cpp | 2 +- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 +- scwx-qt/source/scwx/qt/view/level3_product_view.cpp | 2 +- wxdata/include/scwx/util/time.hpp | 4 ++-- wxdata/source/scwx/awips/coded_time_motion_location.cpp | 4 ++-- wxdata/source/scwx/awips/pvtec.cpp | 4 ++-- wxdata/source/scwx/gr/placefile.cpp | 4 ++-- wxdata/source/scwx/network/dir_list.cpp | 4 ++-- wxdata/source/scwx/provider/aws_level2_data_provider.cpp | 4 ++-- wxdata/source/scwx/provider/aws_level3_data_provider.cpp | 4 ++-- wxdata/source/scwx/provider/warnings_provider.cpp | 4 ++-- wxdata/source/scwx/util/time.cpp | 6 +++--- 14 files changed, 25 insertions(+), 25 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index 782436df..5e49847a 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -10,7 +10,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -271,7 +271,7 @@ size_t RadarSite::ReadConfig(const std::string& path) try { -#if (defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono >= 201907L) using namespace std::chrono; #else using namespace date; diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 711aed80..6b865703 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -52,7 +52,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 377c7aff..1c093e9b 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -413,7 +413,7 @@ const scwx::util::time_zone* RadarProductManager::default_time_zone() const } case types::DefaultTimeZone::Local: -#if (defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono >= 201907L) return std::chrono::current_zone(); #else return date::current_zone(); diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 0151b6cb..a42bc2b3 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -22,7 +22,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index a700b206..df65c693 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -17,7 +17,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif diff --git a/wxdata/include/scwx/util/time.hpp b/wxdata/include/scwx/util/time.hpp index 780a31c3..e31e8ca9 100644 --- a/wxdata/include/scwx/util/time.hpp +++ b/wxdata/include/scwx/util/time.hpp @@ -6,7 +6,7 @@ #include #include -#if !defined(_MSC_VER) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -15,7 +15,7 @@ namespace scwx namespace util { -#if (defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono >= 201907L) typedef std::chrono::time_zone time_zone; #else typedef date::time_zone time_zone; diff --git a/wxdata/source/scwx/awips/coded_time_motion_location.cpp b/wxdata/source/scwx/awips/coded_time_motion_location.cpp index 40358eef..569cc513 100644 --- a/wxdata/source/scwx/awips/coded_time_motion_location.cpp +++ b/wxdata/source/scwx/awips/coded_time_motion_location.cpp @@ -13,7 +13,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -107,7 +107,7 @@ bool CodedTimeMotionLocation::Parse(const StringRange& lines, { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/awips/pvtec.cpp b/wxdata/source/scwx/awips/pvtec.cpp index 426a58d3..b93d2be0 100644 --- a/wxdata/source/scwx/awips/pvtec.cpp +++ b/wxdata/source/scwx/awips/pvtec.cpp @@ -17,7 +17,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -143,7 +143,7 @@ bool PVtec::Parse(const std::string& s) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 70881299..02bb3527 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -20,7 +20,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -284,7 +284,7 @@ void Placefile::Impl::ProcessLine(const std::string& line) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/network/dir_list.cpp b/wxdata/source/scwx/network/dir_list.cpp index e4a2c4c3..f1be20b9 100644 --- a/wxdata/source/scwx/network/dir_list.cpp +++ b/wxdata/source/scwx/network/dir_list.cpp @@ -11,7 +11,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -200,7 +200,7 @@ void DirListSAXHandler::Characters(void* userData, const xmlChar* ch, int len) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index 87f0d2a2..c2910773 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -5,7 +5,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -82,7 +82,7 @@ AwsLevel2DataProvider::GetTimePointFromKey(const std::string& key) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp index e052b907..5bae6843 100644 --- a/wxdata/source/scwx/provider/aws_level3_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level3_data_provider.cpp @@ -12,7 +12,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -110,7 +110,7 @@ AwsLevel3DataProvider::GetTimePointFromKey(const std::string& key) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 443582c7..8cfe9b77 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -14,7 +14,7 @@ #include #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -71,7 +71,7 @@ WarningsProvider::ListFiles(std::chrono::system_clock::time_point newerThan) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index dcf763d2..7a706224 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -17,7 +17,7 @@ #include -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) # include #endif @@ -59,7 +59,7 @@ std::string TimeString(std::chrono::system_clock::time_point time, { using namespace std::chrono; -#if (defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono >= 201907L) # define FORMAT_STRING_24_HOUR "{:%Y-%m-%d %H:%M:%S %Z}" # define FORMAT_STRING_12_HOUR "{:%Y-%m-%d %I:%M:%S %p %Z}" namespace date = std::chrono; @@ -128,7 +128,7 @@ TryParseDateTime(const std::string& dateTimeFormat, const std::string& str) { using namespace std::chrono; -#if !(defined(_MSC_VER) || defined(__clang__)) +#if (__cpp_lib_chrono < 201907L) using namespace date; #endif From 264ed5a943b85434659d582ef8dbe21e78f91c21 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 22 Sep 2024 12:33:43 -0400 Subject: [PATCH 049/762] Update cmake file and add test cpp file for not compiling the date library if not needed --- wxdata/wxdata.cmake | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 47ada181..42e07cce 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -252,6 +252,11 @@ source_group("Source Files\\wsr88d\\rda" FILES ${SRC_WSR88D_RDA}) source_group("Header Files\\wsr88d\\rpg" FILES ${HDR_WSR88D_RPG}) source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) + +try_compile(HAS_FULL_CHRONO + ${CMAKE_BINARY_DIR} + ${PROJECT_SOURCE_DIR}/source/cpp-feature-tests/chrono_feature_test.cpp) + target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} ${HSLUV_C_INCLUDE_DIR} ${scwx-data_SOURCE_DIR}/include @@ -293,9 +298,12 @@ if (WIN32) target_link_libraries(wxdata INTERFACE Ws2_32) endif() +if (NOT HAS_FULL_CHRONO) + target_link_libraries(wxdata PUBLIC date::date-tz) +endif() + if (NOT MSVC) - target_link_libraries(wxdata PUBLIC date::date-tz - TBB::tbb) + target_link_libraries(wxdata PUBLIC TBB::tbb) endif() set_target_properties(wxdata PROPERTIES CXX_STANDARD 20 From ccacc7f7db3ad148e17ca5b9296cb93607221f2f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 22 Sep 2024 12:58:44 -0400 Subject: [PATCH 050/762] add cpp-feature-tests folder and cpp test --- wxdata/source/cpp-feature-tests/chrono_feature_test.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 wxdata/source/cpp-feature-tests/chrono_feature_test.cpp diff --git a/wxdata/source/cpp-feature-tests/chrono_feature_test.cpp b/wxdata/source/cpp-feature-tests/chrono_feature_test.cpp new file mode 100644 index 00000000..c23acffb --- /dev/null +++ b/wxdata/source/cpp-feature-tests/chrono_feature_test.cpp @@ -0,0 +1,8 @@ +#include + +int main() +{ +#if (__cpp_lib_chrono < 201907L) +# error("Old chrono version") +#endif +} From 7c15a8272bd245dd328e9f46d01807d9adcd3857 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 22 Sep 2024 16:13:36 -0400 Subject: [PATCH 051/762] add c++ standard to try_compile --- wxdata/wxdata.cmake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 42e07cce..54c5574b 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -255,7 +255,10 @@ source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) try_compile(HAS_FULL_CHRONO ${CMAKE_BINARY_DIR} - ${PROJECT_SOURCE_DIR}/source/cpp-feature-tests/chrono_feature_test.cpp) + ${PROJECT_SOURCE_DIR}/source/cpp-feature-tests/chrono_feature_test.cpp + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} ${HSLUV_C_INCLUDE_DIR} From 1b79c37fd2fa8ab696eb251df3543db2c7cfc8dd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 11:55:20 -0400 Subject: [PATCH 052/762] Removed unneeded date/chrono header includes --- scwx-qt/source/scwx/qt/main/main_window.cpp | 4 ---- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 6 ------ scwx-qt/source/scwx/qt/view/level3_product_view.cpp | 4 ---- 3 files changed, 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 6b865703..cadd9c90 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -52,10 +52,6 @@ #include #include -#if (__cpp_lib_chrono < 201907L) -# include -#endif - namespace scwx { namespace qt diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index a42bc2b3..9640a705 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -11,8 +11,6 @@ #include #include -#include - #if defined(_MSC_VER) # pragma warning(push, 0) #endif @@ -22,10 +20,6 @@ #include #include -#if (__cpp_lib_chrono < 201907L) -# include -#endif - #if defined(_MSC_VER) # pragma warning(pop) #endif diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index df65c693..d0bb0d47 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -17,10 +17,6 @@ #include #include -#if (__cpp_lib_chrono < 201907L) -# include -#endif - namespace scwx { namespace qt From 6953eddea6042221951758987e481867466da7a2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 12:05:14 -0400 Subject: [PATCH 053/762] Moved cpp-feature-tests out of source --- wxdata/{source => }/cpp-feature-tests/chrono_feature_test.cpp | 0 wxdata/wxdata.cmake | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename wxdata/{source => }/cpp-feature-tests/chrono_feature_test.cpp (100%) diff --git a/wxdata/source/cpp-feature-tests/chrono_feature_test.cpp b/wxdata/cpp-feature-tests/chrono_feature_test.cpp similarity index 100% rename from wxdata/source/cpp-feature-tests/chrono_feature_test.cpp rename to wxdata/cpp-feature-tests/chrono_feature_test.cpp diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 54c5574b..03793291 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -255,7 +255,7 @@ source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) try_compile(HAS_FULL_CHRONO ${CMAKE_BINARY_DIR} - ${PROJECT_SOURCE_DIR}/source/cpp-feature-tests/chrono_feature_test.cpp + ${PROJECT_SOURCE_DIR}/cpp-feature-tests/chrono_feature_test.cpp CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) From 421cae671662ba8289075d12dd4ce059c60a98db Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 12:06:10 -0400 Subject: [PATCH 054/762] Renamed HAS_FULL_CHRONO to CHRONO_HAS_TIMEZONES_AND_CALANDERS --- wxdata/wxdata.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 03793291..fd05be20 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -253,7 +253,7 @@ source_group("Header Files\\wsr88d\\rpg" FILES ${HDR_WSR88D_RPG}) source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) -try_compile(HAS_FULL_CHRONO +try_compile(CHRONO_HAS_TIMEZONES_AND_CALANDERS ${CMAKE_BINARY_DIR} ${PROJECT_SOURCE_DIR}/cpp-feature-tests/chrono_feature_test.cpp CXX_STANDARD 20 @@ -301,7 +301,7 @@ if (WIN32) target_link_libraries(wxdata INTERFACE Ws2_32) endif() -if (NOT HAS_FULL_CHRONO) +if (NOT CHRONO_HAS_TIMEZONES_AND_CALANDERS) target_link_libraries(wxdata PUBLIC date::date-tz) endif() From 6c7f7f952fc02941dc167368f26e846065f6d52b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 12:12:51 -0400 Subject: [PATCH 055/762] bumped minimum CMake version to 3.26 so it logs try_compile statements --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c06a46e6..f3c4d157 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.26) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} VERSION 0.4.5 From 2d9d240a5a89ca91459daba895b35f6583e19f1d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 13:11:35 -0400 Subject: [PATCH 056/762] Fixed CALANDERS spelling mistake --- wxdata/wxdata.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index fd05be20..e1ed8ddd 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -253,7 +253,7 @@ source_group("Header Files\\wsr88d\\rpg" FILES ${HDR_WSR88D_RPG}) source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) -try_compile(CHRONO_HAS_TIMEZONES_AND_CALANDERS +try_compile(CHRONO_HAS_TIMEZONES_AND_CALENDERS ${CMAKE_BINARY_DIR} ${PROJECT_SOURCE_DIR}/cpp-feature-tests/chrono_feature_test.cpp CXX_STANDARD 20 @@ -301,7 +301,7 @@ if (WIN32) target_link_libraries(wxdata INTERFACE Ws2_32) endif() -if (NOT CHRONO_HAS_TIMEZONES_AND_CALANDERS) +if (NOT CHRONO_HAS_TIMEZONES_AND_CALENDERS) target_link_libraries(wxdata PUBLIC date::date-tz) endif() From d1e583d252e5145184df2cb3d84d279f17a165f0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Sep 2024 13:44:45 -0400 Subject: [PATCH 057/762] revert back to cmake version 3.21 and add manual logging of CHRONO_HAS_TIMEZONES_AND_CALENDERS --- CMakeLists.txt | 2 +- wxdata/wxdata.cmake | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f3c4d157..c06a46e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.26) +cmake_minimum_required(VERSION 3.21) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} VERSION 0.4.5 diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index e1ed8ddd..434fc415 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -259,6 +259,7 @@ try_compile(CHRONO_HAS_TIMEZONES_AND_CALENDERS CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) +message("CHRONO_HAS_TIMEZONES_AND_CALENDERS: ${CHRONO_HAS_TIMEZONES_AND_CALENDERS}") target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} ${HSLUV_C_INCLUDE_DIR} From 7254fc71fb6dcd3ed49a6d195ecc80479690d881 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 14 Aug 2024 23:18:50 -0500 Subject: [PATCH 058/762] Custom LineLabel widget to display line preview --- scwx-qt/scwx-qt.cmake | 2 + scwx-qt/source/scwx/qt/ui/line_label.cpp | 176 +++++++++++++++++++++++ scwx-qt/source/scwx/qt/ui/line_label.hpp | 43 ++++++ 3 files changed, 221 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/ui/line_label.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/line_label.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 78d45217..b300cd2a 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -252,6 +252,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/level2_products_widget.hpp source/scwx/qt/ui/level2_settings_widget.hpp source/scwx/qt/ui/level3_products_widget.hpp + source/scwx/qt/ui/line_label.hpp source/scwx/qt/ui/open_url_dialog.hpp source/scwx/qt/ui/placefile_dialog.hpp source/scwx/qt/ui/placefile_settings_widget.hpp @@ -278,6 +279,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/level2_products_widget.cpp source/scwx/qt/ui/level2_settings_widget.cpp source/scwx/qt/ui/level3_products_widget.cpp + source/scwx/qt/ui/line_label.cpp source/scwx/qt/ui/open_url_dialog.cpp source/scwx/qt/ui/placefile_dialog.cpp source/scwx/qt/ui/placefile_settings_widget.cpp diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp new file mode 100644 index 00000000..6fd6c854 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -0,0 +1,176 @@ +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::line_label"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class LineLabel::Impl +{ +public: + explicit Impl() {}; + ~Impl() = default; + + QImage GenerateImage() const; + + std::size_t borderWidth_ {1}; + std::size_t highlightWidth_ {1}; + std::size_t lineWidth_ {3}; + + boost::gil::rgba8_pixel_t borderColor_ {0, 0, 0, 255}; + boost::gil::rgba8_pixel_t highlightColor_ {255, 255, 0, 255}; + boost::gil::rgba8_pixel_t lineColor_ {0, 0, 255, 255}; + + QPixmap pixmap_ {}; + bool pixmapDirty_ {true}; +}; + +LineLabel::LineLabel(QWidget* parent) : + QFrame(parent), p {std::make_unique()} +{ +} + +LineLabel::~LineLabel() {} + +void LineLabel::set_border_width(std::size_t width) +{ + p->borderWidth_ = width; + p->pixmapDirty_ = true; + update(); +} + +void LineLabel::set_highlight_width(std::size_t width) +{ + p->highlightWidth_ = width; + p->pixmapDirty_ = true; + update(); +} + +void LineLabel::set_line_width(std::size_t width) +{ + p->lineWidth_ = width; + p->pixmapDirty_ = true; + update(); +} + +void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) +{ + p->borderColor_ = color; + p->pixmapDirty_ = true; + update(); +} + +void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) +{ + p->highlightColor_ = color; + p->pixmapDirty_ = true; + update(); +} + +void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) +{ + p->lineColor_ = color; + p->pixmapDirty_ = true; + update(); +} + +QSize LineLabel::minimumSizeHint() const +{ + return sizeHint(); +} + +QSize LineLabel::sizeHint() const +{ + QMargins margins = contentsMargins(); + + const std::size_t width = 1; + const std::size_t height = + (p->borderWidth_ + p->highlightWidth_) * 2 + p->lineWidth_; + + return QSize(static_cast(width) + margins.left() + margins.right(), + static_cast(height) + margins.top() + margins.bottom()); +} + +void LineLabel::paintEvent(QPaintEvent* e) +{ + logger_->trace("paintEvent"); + + QFrame::paintEvent(e); + + if (p->pixmapDirty_) + { + QImage image = p->GenerateImage(); + p->pixmap_ = QPixmap::fromImage(image); + p->pixmapDirty_ = false; + } + + // Don't stretch the line pixmap vertically + QRect rect = contentsRect(); + if (rect.height() > p->pixmap_.height()) + { + int dy = rect.height() - p->pixmap_.height(); + int dy1 = dy / 2; + int dy2 = dy - dy1; + rect.adjust(0, dy1, 0, -dy2); + } + + QPainter painter(this); + painter.drawPixmap(rect, p->pixmap_); +} + +QImage LineLabel::Impl::GenerateImage() const +{ + const QRgb borderRgba = qRgba(static_cast(borderColor_[0]), + static_cast(borderColor_[1]), + static_cast(borderColor_[2]), + static_cast(borderColor_[3])); + const QRgb highlightRgba = qRgba(static_cast(highlightColor_[0]), + static_cast(highlightColor_[1]), + static_cast(highlightColor_[2]), + static_cast(highlightColor_[3])); + const QRgb lineRgba = qRgba(static_cast(lineColor_[0]), + static_cast(lineColor_[1]), + static_cast(lineColor_[2]), + static_cast(lineColor_[3])); + + const std::size_t width = 1; + const std::size_t height = (borderWidth_ + highlightWidth_) * 2 + lineWidth_; + + QImage image(static_cast(width), + static_cast(height), + QImage::Format::Format_ARGB32); + + std::size_t y = 0; + for (std::size_t i = 0; i < borderWidth_; ++i, ++y) + { + image.setPixel(0, static_cast(y), borderRgba); + image.setPixel(0, static_cast(height - 1 - y), borderRgba); + } + + for (std::size_t i = 0; i < highlightWidth_; ++i, ++y) + { + image.setPixel(0, static_cast(y), highlightRgba); + image.setPixel(0, static_cast(height - 1 - y), highlightRgba); + } + + for (std::size_t i = 0; i < lineWidth_; ++i, ++y) + { + image.setPixel(0, static_cast(y), lineRgba); + image.setPixel(0, static_cast(height - 1 - y), lineRgba); + } + + return image; +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/line_label.hpp b/scwx-qt/source/scwx/qt/ui/line_label.hpp new file mode 100644 index 00000000..6036e92f --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/line_label.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class LineLabel : public QFrame +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(LineLabel) + +public: + explicit LineLabel(QWidget* parent = nullptr); + ~LineLabel(); + + void set_border_color(boost::gil::rgba8_pixel_t color); + void set_highlight_color(boost::gil::rgba8_pixel_t color); + void set_line_color(boost::gil::rgba8_pixel_t color); + + void set_border_width(std::size_t width); + void set_highlight_width(std::size_t width); + void set_line_width(std::size_t width); + +protected: + QSize minimumSizeHint() const override; + QSize sizeHint() const override; + void paintEvent(QPaintEvent* e) override; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx From eda7751eb907be7269b94a1b4d1ba793bf644502 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 20 Aug 2024 23:58:14 -0500 Subject: [PATCH 059/762] Edit line dialog in-work --- scwx-qt/scwx-qt.cmake | 15 +- .../source/scwx/qt/ui/edit_line_dialog.cpp | 309 ++++++++++++++++++ .../source/scwx/qt/ui/edit_line_dialog.hpp | 59 ++++ scwx-qt/source/scwx/qt/ui/edit_line_dialog.ui | 306 +++++++++++++++++ 4 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/edit_line_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/edit_line_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b300cd2a..a548b1f4 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -240,8 +240,8 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.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/wfo_dialog.hpp source/scwx/qt/ui/download_dialog.hpp + source/scwx/qt/ui/edit_line_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/gps_info_dialog.hpp source/scwx/qt/ui/hotkey_edit.hpp @@ -260,15 +260,16 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/radar_site_dialog.hpp source/scwx/qt/ui/serial_port_dialog.hpp source/scwx/qt/ui/settings_dialog.hpp - source/scwx/qt/ui/update_dialog.hpp) + source/scwx/qt/ui/update_dialog.hpp + source/scwx/qt/ui/wfo_dialog.hpp) set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/alert_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/wfo_dialog.cpp source/scwx/qt/ui/download_dialog.cpp + source/scwx/qt/ui/edit_line_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/gps_info_dialog.cpp source/scwx/qt/ui/hotkey_edit.cpp @@ -287,14 +288,15 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/radar_site_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp source/scwx/qt/ui/serial_port_dialog.cpp - source/scwx/qt/ui/update_dialog.cpp) + source/scwx/qt/ui/update_dialog.cpp + source/scwx/qt/ui/wfo_dialog.cpp) set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/alert_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/wfo_dialog.ui + source/scwx/qt/ui/edit_line_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui @@ -305,7 +307,8 @@ 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/serial_port_dialog.ui - source/scwx/qt/ui/update_dialog.ui) + source/scwx/qt/ui/update_dialog.ui + source/scwx/qt/ui/wfo_dialog.ui) set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.hpp source/scwx/qt/ui/settings/settings_page_widget.hpp source/scwx/qt/ui/settings/unit_settings_widget.hpp) diff --git a/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp new file mode 100644 index 00000000..1aafffeb --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp @@ -0,0 +1,309 @@ +#include "edit_line_dialog.hpp" +#include "ui_edit_line_dialog.h" + +#include +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::edit_line_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class EditLineDialog::Impl +{ +public: + struct EditComponent + { + void ConnectSignals(EditLineDialog* self) + { + QObject::connect(colorLineEdit_, + &QLineEdit::textEdited, + self, + [=, this](const QString& text) + { + boost::gil::rgba8_pixel_t color = + util::color::ToRgba8PixelT(text.toStdString()); + self->p->set_color(*this, color); + }); + + QObject::connect(colorButton_, + &QAbstractButton::clicked, + self, + [=, this]() { self->p->ShowColorDialog(*this); }); + + QObject::connect(widthSpinBox_, + &QSpinBox::valueChanged, + self, + [=, this](int width) + { self->p->set_width(*this, width); }); + } + + boost::gil::rgba8_pixel_t color_; + std::size_t width_; + QFrame* colorFrame_ {nullptr}; + QLineEdit* colorLineEdit_ {nullptr}; + QToolButton* colorButton_ {nullptr}; + QSpinBox* widthSpinBox_ {nullptr}; + }; + + explicit Impl(EditLineDialog* self) : + self_ {self}, lineLabel_ {new LineLabel(self)} + { + } + ~Impl() = default; + + void SetDefaults(); + void ShowColorDialog(EditComponent& component); + void UpdateLineLabel(); + + void set_color(EditComponent& component, boost::gil::rgba8_pixel_t color); + void set_width(EditComponent& component, std::size_t width); + + static void SetBackgroundColor(const std::string& value, QFrame* frame); + + EditLineDialog* self_; + + LineLabel* lineLabel_; + + boost::gil::rgba8_pixel_t defaultBorderColor_ {0, 0, 0, 255}; + boost::gil::rgba8_pixel_t defaultHighlightColor_ {0, 0, 0, 0}; + boost::gil::rgba8_pixel_t defaultLineColor_ {255, 255, 255, 255}; + + std::size_t defaultBorderWidth_ {1u}; + std::size_t defaultHighlightWidth_ {0u}; + std::size_t defaultLineWidth_ {3u}; + + EditComponent borderComponent_ {}; + EditComponent highlightComponent_ {}; + EditComponent lineComponent_ {}; +}; + +EditLineDialog::EditLineDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::EditLineDialog) +{ + ui->setupUi(this); + + p->borderComponent_.colorFrame_ = ui->borderColorFrame; + p->borderComponent_.colorLineEdit_ = ui->borderColorLineEdit; + p->borderComponent_.colorButton_ = ui->borderColorButton; + p->borderComponent_.widthSpinBox_ = ui->borderWidthSpinBox; + + p->highlightComponent_.colorFrame_ = ui->highlightColorFrame; + p->highlightComponent_.colorLineEdit_ = ui->highlightColorLineEdit; + p->highlightComponent_.colorButton_ = ui->highlightColorButton; + p->highlightComponent_.widthSpinBox_ = ui->highlightWidthSpinBox; + + p->lineComponent_.colorFrame_ = ui->lineColorFrame; + p->lineComponent_.colorLineEdit_ = ui->lineColorLineEdit; + p->lineComponent_.colorButton_ = ui->lineColorButton; + p->lineComponent_.widthSpinBox_ = ui->lineWidthSpinBox; + + p->SetDefaults(); + + p->lineLabel_->setMinimumWidth(72); + + QHBoxLayout* lineLabelContainerLayout = + static_cast(ui->lineLabelContainer->layout()); + lineLabelContainerLayout->insertWidget(1, p->lineLabel_); + + p->borderComponent_.ConnectSignals(this); + p->highlightComponent_.ConnectSignals(this); + p->lineComponent_.ConnectSignals(this); + + QObject::connect(ui->buttonBox, + &QDialogButtonBox::clicked, + this, + [this](QAbstractButton* button) + { + QDialogButtonBox::ButtonRole role = + ui->buttonBox->buttonRole(button); + + switch (role) + { + case QDialogButtonBox::ButtonRole::ResetRole: // Reset + p->SetDefaults(); + break; + + default: + break; + } + }); +} + +EditLineDialog::~EditLineDialog() +{ + delete ui; +} + +boost::gil::rgba8_pixel_t EditLineDialog::border_color() const +{ + return p->borderComponent_.color_; +} + +boost::gil::rgba8_pixel_t EditLineDialog::highlight_color() const +{ + return p->highlightComponent_.color_; +} + +boost::gil::rgba8_pixel_t EditLineDialog::line_color() const +{ + return p->lineComponent_.color_; +} + +std::size_t EditLineDialog::border_width() const +{ + return p->borderComponent_.width_; +} + +std::size_t EditLineDialog::highlight_width() const +{ + return p->highlightComponent_.width_; +} + +std::size_t EditLineDialog::line_width() const +{ + return p->lineComponent_.width_; +} + +void EditLineDialog::set_border_color(boost::gil::rgba8_pixel_t color) +{ + p->set_color(p->borderComponent_, color); +} + +void EditLineDialog::set_highlight_color(boost::gil::rgba8_pixel_t color) +{ + p->set_color(p->highlightComponent_, color); +} + +void EditLineDialog::set_line_color(boost::gil::rgba8_pixel_t color) +{ + p->set_color(p->lineComponent_, color); +} + +void EditLineDialog::set_border_width(std::size_t width) +{ + p->set_width(p->borderComponent_, width); +} + +void EditLineDialog::set_highlight_width(std::size_t width) +{ + p->set_width(p->highlightComponent_, width); +} + +void EditLineDialog::set_line_width(std::size_t width) +{ + p->set_width(p->lineComponent_, width); +} + +void EditLineDialog::Impl::set_color(EditComponent& component, + boost::gil::rgba8_pixel_t color) +{ + const std::string argbString {util::color::ToArgbString(color)}; + + component.color_ = color; + component.colorLineEdit_->setText(QString::fromStdString(argbString)); + SetBackgroundColor(argbString, component.colorFrame_); + + UpdateLineLabel(); +} + +void EditLineDialog::Impl::set_width(EditComponent& component, + std::size_t width) +{ + component.width_ = width; + component.widthSpinBox_->setValue(static_cast(width)); + + UpdateLineLabel(); +} + +void EditLineDialog::Impl::UpdateLineLabel() +{ + lineLabel_->set_border_color(borderComponent_.color_); + lineLabel_->set_highlight_color(highlightComponent_.color_); + lineLabel_->set_line_color(lineComponent_.color_); + + lineLabel_->set_border_width(borderComponent_.width_); + lineLabel_->set_highlight_width(highlightComponent_.width_); + lineLabel_->set_line_width(lineComponent_.width_); +} + +void EditLineDialog::Initialize(boost::gil::rgba8_pixel_t borderColor, + boost::gil::rgba8_pixel_t highlightColor, + boost::gil::rgba8_pixel_t lineColor, + std::size_t borderWidth, + std::size_t highlightWidth, + std::size_t lineWidth) +{ + p->defaultBorderColor_ = borderColor; + p->defaultHighlightColor_ = highlightColor; + p->defaultLineColor_ = lineColor; + + p->defaultBorderWidth_ = borderWidth; + p->defaultHighlightWidth_ = highlightWidth; + p->defaultLineWidth_ = lineWidth; + + p->SetDefaults(); +} + +void EditLineDialog::Impl::SetDefaults() +{ + self_->set_border_color(defaultBorderColor_); + self_->set_highlight_color(defaultHighlightColor_); + self_->set_line_color(defaultLineColor_); + + self_->set_border_width(defaultBorderWidth_); + self_->set_highlight_width(defaultHighlightWidth_); + self_->set_line_width(defaultLineWidth_); +} + +void EditLineDialog::Impl::ShowColorDialog(EditComponent& component) +{ + QColorDialog* dialog = new QColorDialog(self_); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); + + QColor initialColor(component.colorLineEdit_->text()); + if (initialColor.isValid()) + { + dialog->setCurrentColor(initialColor); + } + + QObject::connect( + dialog, + &QColorDialog::colorSelected, + self_, + [this, &component](const QColor& qColor) + { + QString colorName = qColor.name(QColor::NameFormat::HexArgb); + boost::gil::rgba8_pixel_t color = + util::color::ToRgba8PixelT(colorName.toStdString()); + + logger_->info("Selected color: {}", colorName.toStdString()); + set_color(component, color); + }); + + dialog->open(); +} + +void EditLineDialog::Impl::SetBackgroundColor(const std::string& value, + QFrame* frame) +{ + frame->setStyleSheet( + QString::fromStdString(fmt::format("background-color: {}", value))); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/edit_line_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.hpp new file mode 100644 index 00000000..2f8ea3ce --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include + +namespace Ui +{ +class EditLineDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class EditLineDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(EditLineDialog) + +public: + explicit EditLineDialog(QWidget* parent = nullptr); + ~EditLineDialog(); + + boost::gil::rgba8_pixel_t border_color() const; + boost::gil::rgba8_pixel_t highlight_color() const; + boost::gil::rgba8_pixel_t line_color() const; + + std::size_t border_width() const; + std::size_t highlight_width() const; + std::size_t line_width() const; + + void set_border_color(boost::gil::rgba8_pixel_t color); + void set_highlight_color(boost::gil::rgba8_pixel_t color); + void set_line_color(boost::gil::rgba8_pixel_t color); + + void set_border_width(std::size_t width); + void set_highlight_width(std::size_t width); + void set_line_width(std::size_t width); + + void Initialize(boost::gil::rgba8_pixel_t borderColor, + boost::gil::rgba8_pixel_t highlightColor, + boost::gil::rgba8_pixel_t lineColor, + std::size_t borderWidth, + std::size_t highlightWidth, + std::size_t lineWidth); + +private: + class Impl; + std::unique_ptr p; + Ui::EditLineDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/edit_line_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.ui new file mode 100644 index 00000000..bc133bf9 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.ui @@ -0,0 +1,306 @@ + + + EditLineDialog + + + + 0 + 0 + 350 + 225 + + + + Edit Line + + + + + + + true + + + + Component + + + + + + + #ff000000 + + + + + + + 0 + + + 9 + + + + + + + ... + + + + :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + + + + + Border + + + + + + + #ff000000 + + + + + + + Line + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + + + + + 24 + 24 + + + + QFrame::Shape::Box + + + QFrame::Shadow::Plain + + + + + + + 0 + + + 9 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + 24 + 24 + + + + QFrame::Shape::Box + + + QFrame::Shadow::Plain + + + + + + + + true + + + + Color + + + + + + + #ff000000 + + + + + + + + 24 + 24 + + + + QFrame::Shape::Box + + + QFrame::Shadow::Plain + + + + + + + + true + + + + Width + + + + + + + 1 + + + 9 + + + + + + + Highlight + + + + + + + ... + + + + :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + + + + + + 0 + 45 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Horizontal + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + + + + buttonBox + accepted() + EditLineDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditLineDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 88b8a8001f425aa595eb10fb68bba95d57896698 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 21 Aug 2024 22:20:22 -0500 Subject: [PATCH 060/762] Line label geometry needs updated whenever a component width changes --- scwx-qt/source/scwx/qt/ui/line_label.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp index 6fd6c854..f5c9c3e3 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.cpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -45,6 +45,7 @@ void LineLabel::set_border_width(std::size_t width) { p->borderWidth_ = width; p->pixmapDirty_ = true; + updateGeometry(); update(); } @@ -52,6 +53,7 @@ void LineLabel::set_highlight_width(std::size_t width) { p->highlightWidth_ = width; p->pixmapDirty_ = true; + updateGeometry(); update(); } @@ -59,6 +61,7 @@ void LineLabel::set_line_width(std::size_t width) { p->lineWidth_ = width; p->pixmapDirty_ = true; + updateGeometry(); update(); } From da79f47416c09806706d5d77e6a095448262391d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 21 Aug 2024 22:24:07 -0500 Subject: [PATCH 061/762] Don't overwite the edit line color text boxes during text editing --- scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp index 1aafffeb..5202e732 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_line_dialog.cpp @@ -32,7 +32,7 @@ public: { boost::gil::rgba8_pixel_t color = util::color::ToRgba8PixelT(text.toStdString()); - self->p->set_color(*this, color); + self->p->set_color(*this, color, false); }); QObject::connect(colorButton_, @@ -65,7 +65,9 @@ public: void ShowColorDialog(EditComponent& component); void UpdateLineLabel(); - void set_color(EditComponent& component, boost::gil::rgba8_pixel_t color); + void set_color(EditComponent& component, + boost::gil::rgba8_pixel_t color, + bool updateLineEdit = true); void set_width(EditComponent& component, std::size_t width); static void SetBackgroundColor(const std::string& value, QFrame* frame); @@ -207,14 +209,19 @@ void EditLineDialog::set_line_width(std::size_t width) } void EditLineDialog::Impl::set_color(EditComponent& component, - boost::gil::rgba8_pixel_t color) + boost::gil::rgba8_pixel_t color, + bool updateLineEdit) { const std::string argbString {util::color::ToArgbString(color)}; component.color_ = color; - component.colorLineEdit_->setText(QString::fromStdString(argbString)); SetBackgroundColor(argbString, component.colorFrame_); + if (updateLineEdit) + { + component.colorLineEdit_->setText(QString::fromStdString(argbString)); + } + UpdateLineLabel(); } From f8e0ab5b564fd61db99f8cdbdab09a9271134128 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 30 Aug 2024 23:03:13 -0500 Subject: [PATCH 062/762] Add alert palette settings widget --- scwx-qt/scwx-qt.cmake | 6 +- .../alert_palette_settings_widget.cpp | 239 ++++++++++++++++++ .../alert_palette_settings_widget.hpp | 29 +++ wxdata/include/scwx/awips/phenomenon.hpp | 1 + wxdata/source/scwx/awips/phenomenon.cpp | 137 +++++----- 5 files changed, 351 insertions(+), 61 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index a548b1f4..52d26ccc 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -309,10 +309,12 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/serial_port_dialog.ui source/scwx/qt/ui/update_dialog.ui source/scwx/qt/ui/wfo_dialog.ui) -set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.hpp +set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp + source/scwx/qt/ui/settings/hotkey_settings_widget.hpp source/scwx/qt/ui/settings/settings_page_widget.hpp source/scwx/qt/ui/settings/unit_settings_widget.hpp) -set(SRC_UI_SETTINGS source/scwx/qt/ui/settings/hotkey_settings_widget.cpp +set(SRC_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp + source/scwx/qt/ui/settings/hotkey_settings_widget.cpp source/scwx/qt/ui/settings/settings_page_widget.cpp source/scwx/qt/ui/settings/unit_settings_widget.cpp) set(HDR_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.hpp diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp new file mode 100644 index 00000000..f433156d --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = + "scwx::qt::ui::settings::alert_palette_settings_widget"; + +struct PhenomenonInfo +{ + bool hasObservedTag_ {false}; + bool hasTornadoPossibleTag_ {false}; + std::vector threatCategories_ { + awips::ThreatCategory::Base}; +}; + +static const boost::unordered_flat_map + phenomenaInfo_ {{awips::Phenomenon::Marine, + PhenomenonInfo {.hasTornadoPossibleTag_ {true}}}, + {awips::Phenomenon::FlashFlood, + PhenomenonInfo {.threatCategories_ { + awips::ThreatCategory::Base, + awips::ThreatCategory::Considerable, + awips::ThreatCategory::Catastrophic}}}, + {awips::Phenomenon::SevereThunderstorm, + PhenomenonInfo {.hasTornadoPossibleTag_ {true}, + .threatCategories_ { + awips::ThreatCategory::Base, + awips::ThreatCategory::Considerable, + awips::ThreatCategory::Destructive}}}, + {awips::Phenomenon::SnowSquall, PhenomenonInfo {}}, + {awips::Phenomenon::Tornado, + PhenomenonInfo {.hasObservedTag_ {true}, + .threatCategories_ { + awips::ThreatCategory::Base, + awips::ThreatCategory::Considerable, + awips::ThreatCategory::Catastrophic}}}}; + +class AlertPaletteSettingsWidget::Impl +{ +public: + explicit Impl(AlertPaletteSettingsWidget* self) : + self_ {self}, + phenomenonPagesWidget_ {new QStackedWidget(self)}, + phenomenonListView_ {new QListWidget(self)}, + editLineDialog_ {new EditLineDialog(self)} + { + SetupUi(); + ConnectSignals(); + } + ~Impl() = default; + + void + AddPhenomenonLine(const std::string& name, QGridLayout* layout, int row); + QWidget* CreateStackedWidgetPage(awips::Phenomenon phenomenon); + void ConnectSignals(); + void SelectPhenomenon(awips::Phenomenon phenomenon); + void SetupUi(); + + AlertPaletteSettingsWidget* self_; + + QStackedWidget* phenomenonPagesWidget_; + QListWidget* phenomenonListView_; + + EditLineDialog* editLineDialog_; + + boost::unordered_flat_map phenomenonPages_ {}; +}; + +AlertPaletteSettingsWidget::AlertPaletteSettingsWidget(QWidget* parent) : + SettingsPageWidget(parent), p {std::make_shared(this)} +{ +} + +AlertPaletteSettingsWidget::~AlertPaletteSettingsWidget() = default; + +void AlertPaletteSettingsWidget::Impl::SetupUi() +{ + // Setup primary widget layout + QGridLayout* gridLayout = new QGridLayout(self_); + gridLayout->setContentsMargins(0, 0, 0, 0); + self_->setLayout(gridLayout); + + QWidget* phenomenonIndexPane = new QWidget(self_); + phenomenonPagesWidget_->setSizePolicy(QSizePolicy::Policy::Expanding, + QSizePolicy::Policy::Preferred); + + gridLayout->addWidget(phenomenonIndexPane, 0, 0); + gridLayout->addWidget(phenomenonPagesWidget_, 0, 1); + + QSpacerItem* spacer = + new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); + gridLayout->addItem(spacer, 1, 0); + + // Setup phenomenon index pane + QVBoxLayout* phenomenonIndexLayout = new QVBoxLayout(self_); + phenomenonIndexPane->setLayout(phenomenonIndexLayout); + + QLabel* phenomenonLabel = new QLabel(tr("Phenomenon:"), self_); + phenomenonListView_->setSizePolicy(QSizePolicy::Policy::Minimum, + QSizePolicy::Policy::Expanding); + + phenomenonIndexLayout->addWidget(phenomenonLabel); + phenomenonIndexLayout->addWidget(phenomenonListView_); + + // Setup stacked widget + auto& paletteSettings = settings::PaletteSettings::Instance(); + Q_UNUSED(paletteSettings); + + for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) + { + QWidget* phenomenonWidget = CreateStackedWidgetPage(phenomenon); + phenomenonPagesWidget_->addWidget(phenomenonWidget); + + phenomenonPages_.insert_or_assign(phenomenon, phenomenonWidget); + + phenomenonListView_->addItem( + QString::fromStdString(awips::GetPhenomenonText(phenomenon))); + } + + phenomenonListView_->setCurrentRow(0); +} + +void AlertPaletteSettingsWidget::Impl::ConnectSignals() +{ + QObject::connect( + phenomenonListView_->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [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; + } + + if (selected.size() > 0) + { + QModelIndex selectedIndex = selected[0].indexes()[0]; + QVariant variantData = + phenomenonListView_->model()->data(selectedIndex); + if (variantData.typeId() == QMetaType::QString) + { + awips::Phenomenon phenomenon = awips::GetPhenomenonFromText( + variantData.toString().toStdString()); + SelectPhenomenon(phenomenon); + } + } + }); +} + +void AlertPaletteSettingsWidget::Impl::SelectPhenomenon( + awips::Phenomenon phenomenon) +{ + auto it = phenomenonPages_.find(phenomenon); + if (it != phenomenonPages_.cend()) + { + phenomenonPagesWidget_->setCurrentWidget(it->second); + } +} + +QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( + awips::Phenomenon phenomenon) +{ + QWidget* page = new QWidget(self_); + QGridLayout* gridLayout = new QGridLayout(self_); + page->setLayout(gridLayout); + + const auto& phenomenonInfo = phenomenaInfo_.at(phenomenon); + + int row = 0; + + // Add a blank label to align left and right widgets + gridLayout->addWidget(new QLabel(self_), row++, 0); + + AddPhenomenonLine("Active", gridLayout, row++); + + if (phenomenonInfo.hasObservedTag_) + { + AddPhenomenonLine("Observed", gridLayout, row++); + } + + if (phenomenonInfo.hasTornadoPossibleTag_) + { + AddPhenomenonLine("Tornado Possible", gridLayout, row++); + } + + for (auto& category : phenomenonInfo.threatCategories_) + { + if (category == awips::ThreatCategory::Base) + { + continue; + } + + AddPhenomenonLine( + awips::GetThreatCategoryName(category), gridLayout, row++); + } + + AddPhenomenonLine("Inactive", gridLayout, row++); + + QSpacerItem* spacer = + new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); + gridLayout->addItem(spacer, row, 0); + + return page; +} + +void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( + const std::string& name, QGridLayout* layout, int row) +{ + layout->addWidget(new QLabel(tr(name.c_str()), self_), row, 0); + layout->addWidget(new LineLabel(self_), row, 1); + layout->addWidget(new QToolButton(self_), row, 2); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp new file mode 100644 index 00000000..45f03e36 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class AlertPaletteSettingsWidget : public SettingsPageWidget +{ + Q_OBJECT + +public: + explicit AlertPaletteSettingsWidget(QWidget* parent = nullptr); + ~AlertPaletteSettingsWidget(); + +private: + class Impl; + std::shared_ptr p; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/wxdata/include/scwx/awips/phenomenon.hpp b/wxdata/include/scwx/awips/phenomenon.hpp index 013a9947..234e473a 100644 --- a/wxdata/include/scwx/awips/phenomenon.hpp +++ b/wxdata/include/scwx/awips/phenomenon.hpp @@ -69,6 +69,7 @@ enum class Phenomenon }; Phenomenon GetPhenomenon(const std::string& code); +Phenomenon GetPhenomenonFromText(const std::string& text); const std::string& GetPhenomenonCode(Phenomenon phenomenon); const std::string& GetPhenomenonText(Phenomenon phenomenon); diff --git a/wxdata/source/scwx/awips/phenomenon.cpp b/wxdata/source/scwx/awips/phenomenon.cpp index ee8e549d..c8154509 100644 --- a/wxdata/source/scwx/awips/phenomenon.cpp +++ b/wxdata/source/scwx/awips/phenomenon.cpp @@ -77,64 +77,65 @@ static const PhenomenonCodesBimap phenomenonCodes_ = (Phenomenon::FreezingSpray, "ZY") // (Phenomenon::Unknown, "??"); -static const std::unordered_map phenomenonText_ { - {Phenomenon::AshfallLand, "Ashfall (land)"}, // - {Phenomenon::AirStagnation, "Air Stagnation"}, // - {Phenomenon::BeachHazard, "Beach Hazard"}, // - {Phenomenon::BriskWind, "Brisk Wind"}, // - {Phenomenon::Blizzard, "Blizzard"}, // - {Phenomenon::CoastalFlood, "Coastal Flood"}, // - {Phenomenon::DebrisFlow, "Debris Flow"}, // - {Phenomenon::DustStorm, "Dust Storm"}, // - {Phenomenon::BlowingDust, "Blowing Dust"}, // - {Phenomenon::ExtremeCold, "Extreme Cold"}, // - {Phenomenon::ExcessiveHeat, "Excessive Heat"}, // - {Phenomenon::ExtremeWind, "Extreme Wind"}, // - {Phenomenon::Flood, "Flood"}, // - {Phenomenon::FlashFlood, "Flash Flood"}, // - {Phenomenon::DenseFogLand, "Dense Fog (land)"}, // - {Phenomenon::Flood, "Flood (Forecast Points)"}, // - {Phenomenon::Frost, "Frost"}, // - {Phenomenon::FireWeather, "Fire Weather"}, // - {Phenomenon::Freeze, "Freeze"}, // - {Phenomenon::Gale, "Gale"}, // - {Phenomenon::HurricaneForceWind, "Hurricane Force Wind"}, // - {Phenomenon::Heat, "Heat"}, // - {Phenomenon::Hurricane, "Hurricane"}, // - {Phenomenon::HighWind, "High Wind"}, // - {Phenomenon::Hydrologic, "Hydrologic"}, // - {Phenomenon::HardFreeze, "Hard Freeze"}, // - {Phenomenon::IceStorm, "Ice Storm"}, // - {Phenomenon::LakeEffectSnow, "Lake Effect Snow"}, // - {Phenomenon::LowWater, "Low Water"}, // - {Phenomenon::LakeshoreFlood, "Lakeshore Flood"}, // - {Phenomenon::LakeWind, "Lake Wind"}, // - {Phenomenon::Marine, "Marine"}, // - {Phenomenon::DenseFogMarine, "Dense Fog (marine)"}, // - {Phenomenon::AshfallMarine, "Ashfall (marine)"}, // - {Phenomenon::DenseSmokeMarine, "Dense Smoke (marine)"}, // - {Phenomenon::RipCurrentRisk, "Rip Current Risk"}, // - {Phenomenon::SmallCraft, "Small Craft"}, // - {Phenomenon::HazardousSeas, "Hazardous Seas"}, // - {Phenomenon::DenseSmokeLand, "Dense Smoke (land)"}, // - {Phenomenon::Storm, "Storm"}, // - {Phenomenon::StormSurge, "Storm Surge"}, // - {Phenomenon::SnowSquall, "Snow Squall"}, // - {Phenomenon::HighSurf, "High Surf"}, // - {Phenomenon::SevereThunderstorm, "Severe Thunderstorm"}, // - {Phenomenon::Tornado, "Tornado"}, // - {Phenomenon::TropicalStorm, "Tropical Storm"}, // - {Phenomenon::Tsunami, "Tsunami"}, // - {Phenomenon::Typhoon, "Typhoon"}, // - {Phenomenon::HeavyFreezingSpray, "Heavy Freezing Spray"}, // - {Phenomenon::WindChill, "Wind Chill"}, // - {Phenomenon::Wind, "Wind"}, // - {Phenomenon::WinterStorm, "Winter Storm"}, // - {Phenomenon::WinterWeather, "Winter Weather"}, // - {Phenomenon::FreezingFog, "Freezing Fog"}, // - {Phenomenon::FreezingRain, "Freezing Rain"}, // - {Phenomenon::FreezingSpray, "Freezing Spray"}, // - {Phenomenon::Unknown, "Unknown"}}; +static const PhenomenonCodesBimap phenomenonText_ = + boost::assign::list_of // + (Phenomenon::AshfallLand, "Ashfall (land)") // + (Phenomenon::AirStagnation, "Air Stagnation") // + (Phenomenon::BeachHazard, "Beach Hazard") // + (Phenomenon::BriskWind, "Brisk Wind") // + (Phenomenon::Blizzard, "Blizzard") // + (Phenomenon::CoastalFlood, "Coastal Flood") // + (Phenomenon::DebrisFlow, "Debris Flow") // + (Phenomenon::DustStorm, "Dust Storm") // + (Phenomenon::BlowingDust, "Blowing Dust") // + (Phenomenon::ExtremeCold, "Extreme Cold") // + (Phenomenon::ExcessiveHeat, "Excessive Heat") // + (Phenomenon::ExtremeWind, "Extreme Wind") // + (Phenomenon::Flood, "Flood") // + (Phenomenon::FlashFlood, "Flash Flood") // + (Phenomenon::DenseFogLand, "Dense Fog (land)") // + (Phenomenon::Flood, "Flood (Forecast Points)") // + (Phenomenon::Frost, "Frost") // + (Phenomenon::FireWeather, "Fire Weather") // + (Phenomenon::Freeze, "Freeze") // + (Phenomenon::Gale, "Gale") // + (Phenomenon::HurricaneForceWind, "Hurricane Force Wind") // + (Phenomenon::Heat, "Heat") // + (Phenomenon::Hurricane, "Hurricane") // + (Phenomenon::HighWind, "High Wind") // + (Phenomenon::Hydrologic, "Hydrologic") // + (Phenomenon::HardFreeze, "Hard Freeze") // + (Phenomenon::IceStorm, "Ice Storm") // + (Phenomenon::LakeEffectSnow, "Lake Effect Snow") // + (Phenomenon::LowWater, "Low Water") // + (Phenomenon::LakeshoreFlood, "Lakeshore Flood") // + (Phenomenon::LakeWind, "Lake Wind") // + (Phenomenon::Marine, "Marine") // + (Phenomenon::DenseFogMarine, "Dense Fog (marine)") // + (Phenomenon::AshfallMarine, "Ashfall (marine)") // + (Phenomenon::DenseSmokeMarine, "Dense Smoke (marine)") // + (Phenomenon::RipCurrentRisk, "Rip Current Risk") // + (Phenomenon::SmallCraft, "Small Craft") // + (Phenomenon::HazardousSeas, "Hazardous Seas") // + (Phenomenon::DenseSmokeLand, "Dense Smoke (land)") // + (Phenomenon::Storm, "Storm") // + (Phenomenon::StormSurge, "Storm Surge") // + (Phenomenon::SnowSquall, "Snow Squall") // + (Phenomenon::HighSurf, "High Surf") // + (Phenomenon::SevereThunderstorm, "Severe Thunderstorm") // + (Phenomenon::Tornado, "Tornado") // + (Phenomenon::TropicalStorm, "Tropical Storm") // + (Phenomenon::Tsunami, "Tsunami") // + (Phenomenon::Typhoon, "Typhoon") // + (Phenomenon::HeavyFreezingSpray, "Heavy Freezing Spray") // + (Phenomenon::WindChill, "Wind Chill") // + (Phenomenon::Wind, "Wind") // + (Phenomenon::WinterStorm, "Winter Storm") // + (Phenomenon::WinterWeather, "Winter Weather") // + (Phenomenon::FreezingFog, "Freezing Fog") // + (Phenomenon::FreezingRain, "Freezing Rain") // + (Phenomenon::FreezingSpray, "Freezing Spray") // + (Phenomenon::Unknown, "Unknown"); Phenomenon GetPhenomenon(const std::string& code) { @@ -154,6 +155,24 @@ Phenomenon GetPhenomenon(const std::string& code) return phenomenon; } +Phenomenon GetPhenomenonFromText(const std::string& text) +{ + Phenomenon phenomenon; + + if (phenomenonText_.right.find(text) != phenomenonText_.right.end()) + { + phenomenon = phenomenonText_.right.at(text); + } + else + { + phenomenon = Phenomenon::Unknown; + + logger_->debug("Unrecognized code: \"{}\"", text); + } + + return phenomenon; +} + const std::string& GetPhenomenonCode(Phenomenon phenomenon) { return phenomenonCodes_.left.at(phenomenon); @@ -161,7 +180,7 @@ const std::string& GetPhenomenonCode(Phenomenon phenomenon) const std::string& GetPhenomenonText(Phenomenon phenomenon) { - return phenomenonText_.at(phenomenon); + return phenomenonText_.left.at(phenomenon); } } // namespace awips From 0481281680a06243cad15a14c8f365029816d6ed Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 30 Aug 2024 23:04:16 -0500 Subject: [PATCH 063/762] Threat category of significant should be treated as considerable per latest NWSI --- wxdata/source/scwx/awips/text_product_message.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 5128aee8..541a1a1a 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -390,9 +390,21 @@ void ParseCodedInformation(std::shared_ptr segment, it->substr(threatTagIt->length()); ThreatCategory threatCategory = GetThreatCategory(threatCategoryName); - if (threatCategory == ThreatCategory::Unknown) + + switch (threatCategory) { + case ThreatCategory::Significant: + // "Significant" is no longer an official tag, and has largely been + // replaced with "Considerable". + threatCategory = ThreatCategory::Considerable; + break; + + case ThreatCategory::Unknown: threatCategory = ThreatCategory::Base; + break; + + default: + break; } segment->threatCategory_ = threatCategory; From 010879971851bfe533fdf28b4dea524e59e950d9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 31 Aug 2024 22:58:41 -0500 Subject: [PATCH 064/762] Add line label accessors --- scwx-qt/source/scwx/qt/ui/line_label.cpp | 30 ++++++++++++++++++++++++ scwx-qt/source/scwx/qt/ui/line_label.hpp | 8 +++++++ 2 files changed, 38 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp index f5c9c3e3..5248057c 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.cpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -41,6 +41,36 @@ LineLabel::LineLabel(QWidget* parent) : LineLabel::~LineLabel() {} +boost::gil::rgba8_pixel_t LineLabel::border_color() const +{ + return p->borderColor_; +} + +boost::gil::rgba8_pixel_t LineLabel::highlight_color() const +{ + return p->highlightColor_; +} + +boost::gil::rgba8_pixel_t LineLabel::line_color() const +{ + return p->lineColor_; +} + +std::size_t LineLabel::border_width() const +{ + return p->borderWidth_; +} + +std::size_t LineLabel::highlight_width() const +{ + return p->highlightWidth_; +} + +std::size_t LineLabel::line_width() const +{ + return p->lineWidth_; +} + void LineLabel::set_border_width(std::size_t width) { p->borderWidth_ = width; diff --git a/scwx-qt/source/scwx/qt/ui/line_label.hpp b/scwx-qt/source/scwx/qt/ui/line_label.hpp index 6036e92f..2c2d516b 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.hpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.hpp @@ -20,6 +20,14 @@ public: explicit LineLabel(QWidget* parent = nullptr); ~LineLabel(); + boost::gil::rgba8_pixel_t border_color() const; + boost::gil::rgba8_pixel_t highlight_color() const; + boost::gil::rgba8_pixel_t line_color() const; + + std::size_t border_width() const; + std::size_t highlight_width() const; + std::size_t line_width() const; + void set_border_color(boost::gil::rgba8_pixel_t color); void set_highlight_color(boost::gil::rgba8_pixel_t color); void set_line_color(boost::gil::rgba8_pixel_t color); From 9182d170de582ac5a0ea509e6b80d35736cd630a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 31 Aug 2024 22:59:36 -0500 Subject: [PATCH 065/762] Update alert line labels with the edit line dialog --- .../alert_palette_settings_widget.cpp | 110 +++++++++++++----- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index f433156d..bf0d5970 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -80,6 +80,7 @@ public: QListWidget* phenomenonListView_; EditLineDialog* editLineDialog_; + LineLabel* activeLineLabel_ {nullptr}; boost::unordered_flat_map phenomenonPages_ {}; }; @@ -93,37 +94,12 @@ AlertPaletteSettingsWidget::~AlertPaletteSettingsWidget() = default; void AlertPaletteSettingsWidget::Impl::SetupUi() { - // Setup primary widget layout - QGridLayout* gridLayout = new QGridLayout(self_); - gridLayout->setContentsMargins(0, 0, 0, 0); - self_->setLayout(gridLayout); - - QWidget* phenomenonIndexPane = new QWidget(self_); - phenomenonPagesWidget_->setSizePolicy(QSizePolicy::Policy::Expanding, + // Setup phenomenon index pane + QLabel* phenomenonLabel = new QLabel(tr("Phenomenon:"), self_); + phenomenonPagesWidget_->setSizePolicy(QSizePolicy::Policy::MinimumExpanding, QSizePolicy::Policy::Preferred); - gridLayout->addWidget(phenomenonIndexPane, 0, 0); - gridLayout->addWidget(phenomenonPagesWidget_, 0, 1); - - QSpacerItem* spacer = - new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); - gridLayout->addItem(spacer, 1, 0); - - // Setup phenomenon index pane - QVBoxLayout* phenomenonIndexLayout = new QVBoxLayout(self_); - phenomenonIndexPane->setLayout(phenomenonIndexLayout); - - QLabel* phenomenonLabel = new QLabel(tr("Phenomenon:"), self_); - phenomenonListView_->setSizePolicy(QSizePolicy::Policy::Minimum, - QSizePolicy::Policy::Expanding); - - phenomenonIndexLayout->addWidget(phenomenonLabel); - phenomenonIndexLayout->addWidget(phenomenonListView_); - // Setup stacked widget - auto& paletteSettings = settings::PaletteSettings::Instance(); - Q_UNUSED(paletteSettings); - for (auto& phenomenon : settings::PaletteSettings::alert_phenomena()) { QWidget* phenomenonWidget = CreateStackedWidgetPage(phenomenon); @@ -136,11 +112,31 @@ void AlertPaletteSettingsWidget::Impl::SetupUi() } phenomenonListView_->setCurrentRow(0); + + // Create phenomenon index pane layout + QVBoxLayout* phenomenonIndexLayout = new QVBoxLayout(self_); + phenomenonIndexLayout->addWidget(phenomenonLabel); + phenomenonIndexLayout->addWidget(phenomenonListView_); + + QWidget* phenomenonIndexPane = new QWidget(self_); + phenomenonIndexPane->setLayout(phenomenonIndexLayout); + + // Create primary widget layout + QGridLayout* gridLayout = new QGridLayout(self_); + gridLayout->setContentsMargins(0, 0, 0, 0); + gridLayout->addWidget(phenomenonIndexPane, 0, 0); + gridLayout->addWidget(phenomenonPagesWidget_, 0, 1); + + QSpacerItem* spacer = + new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); + gridLayout->addItem(spacer, 1, 0); + + self_->setLayout(gridLayout); } void AlertPaletteSettingsWidget::Impl::ConnectSignals() { - QObject::connect( + connect( phenomenonListView_->selectionModel(), &QItemSelectionModel::selectionChanged, self_, @@ -168,6 +164,31 @@ void AlertPaletteSettingsWidget::Impl::ConnectSignals() } } }); + + connect( + editLineDialog_, + &EditLineDialog::accepted, + self_, + [this]() + { + // If the active line label was set + if (activeLineLabel_ != nullptr) + { + // Update the active line label with selected line settings + activeLineLabel_->set_border_color(editLineDialog_->border_color()); + activeLineLabel_->set_highlight_color( + editLineDialog_->highlight_color()); + activeLineLabel_->set_line_color(editLineDialog_->line_color()); + + activeLineLabel_->set_border_width(editLineDialog_->border_width()); + activeLineLabel_->set_highlight_width( + editLineDialog_->highlight_width()); + activeLineLabel_->set_line_width(editLineDialog_->line_width()); + + // Reset the active line label + activeLineLabel_ = nullptr; + } + }); } void AlertPaletteSettingsWidget::Impl::SelectPhenomenon( @@ -229,9 +250,36 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( const std::string& name, QGridLayout* layout, int row) { + QToolButton* toolButton = new QToolButton(self_); + toolButton->setText(tr("...")); + + LineLabel* lineLabel = new LineLabel(self_); + layout->addWidget(new QLabel(tr(name.c_str()), self_), row, 0); - layout->addWidget(new LineLabel(self_), row, 1); - layout->addWidget(new QToolButton(self_), row, 2); + layout->addWidget(lineLabel, row, 1); + layout->addWidget(toolButton, row, 2); + + connect( + toolButton, + &QAbstractButton::clicked, + self_, + [this, lineLabel]() + { + // Set the active line label for when the dialog is finished + activeLineLabel_ = lineLabel; + + // Initialize dialog with current line settings + editLineDialog_->set_border_color(lineLabel->border_color()); + editLineDialog_->set_highlight_color(lineLabel->highlight_color()); + editLineDialog_->set_line_color(lineLabel->line_color()); + + editLineDialog_->set_border_width(lineLabel->border_width()); + editLineDialog_->set_highlight_width(lineLabel->highlight_width()); + editLineDialog_->set_line_width(lineLabel->line_width()); + + // Show the dialog + editLineDialog_->show(); + }); } } // namespace ui From 829d8a315294850de72e82039b77e2f75b2cea77 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 15 Sep 2024 23:54:21 -0500 Subject: [PATCH 066/762] Refactoring phenomena info to impact based warnings header --- .../alert_palette_settings_widget.cpp | 32 +------------------ .../scwx/awips/impact_based_warnings.hpp | 12 +++++++ .../scwx/awips/impact_based_warnings.cpp | 31 ++++++++++++++++++ 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index bf0d5970..c4680a62 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -23,36 +23,6 @@ namespace ui static const std::string logPrefix_ = "scwx::qt::ui::settings::alert_palette_settings_widget"; -struct PhenomenonInfo -{ - bool hasObservedTag_ {false}; - bool hasTornadoPossibleTag_ {false}; - std::vector threatCategories_ { - awips::ThreatCategory::Base}; -}; - -static const boost::unordered_flat_map - phenomenaInfo_ {{awips::Phenomenon::Marine, - PhenomenonInfo {.hasTornadoPossibleTag_ {true}}}, - {awips::Phenomenon::FlashFlood, - PhenomenonInfo {.threatCategories_ { - awips::ThreatCategory::Base, - awips::ThreatCategory::Considerable, - awips::ThreatCategory::Catastrophic}}}, - {awips::Phenomenon::SevereThunderstorm, - PhenomenonInfo {.hasTornadoPossibleTag_ {true}, - .threatCategories_ { - awips::ThreatCategory::Base, - awips::ThreatCategory::Considerable, - awips::ThreatCategory::Destructive}}}, - {awips::Phenomenon::SnowSquall, PhenomenonInfo {}}, - {awips::Phenomenon::Tornado, - PhenomenonInfo {.hasObservedTag_ {true}, - .threatCategories_ { - awips::ThreatCategory::Base, - awips::ThreatCategory::Considerable, - awips::ThreatCategory::Catastrophic}}}}; - class AlertPaletteSettingsWidget::Impl { public: @@ -208,7 +178,7 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( QGridLayout* gridLayout = new QGridLayout(self_); page->setLayout(gridLayout); - const auto& phenomenonInfo = phenomenaInfo_.at(phenomenon); + const auto& phenomenonInfo = awips::GetPhenomenonInfo(phenomenon); int row = 0; diff --git a/wxdata/include/scwx/awips/impact_based_warnings.hpp b/wxdata/include/scwx/awips/impact_based_warnings.hpp index a7b22288..7bb07f5a 100644 --- a/wxdata/include/scwx/awips/impact_based_warnings.hpp +++ b/wxdata/include/scwx/awips/impact_based_warnings.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include +#include namespace scwx { @@ -17,6 +20,15 @@ enum class ThreatCategory : int Unknown }; +struct PhenomenonInfo +{ + bool hasObservedTag_ {false}; + bool hasTornadoPossibleTag_ {false}; + std::vector threatCategories_ {ThreatCategory::Base}; +}; + +const PhenomenonInfo& GetPhenomenonInfo(Phenomenon phenomenon); + ThreatCategory GetThreatCategory(const std::string& name); const std::string& GetThreatCategoryName(ThreatCategory threatCategory); diff --git a/wxdata/source/scwx/awips/impact_based_warnings.cpp b/wxdata/source/scwx/awips/impact_based_warnings.cpp index 75f04d1e..1bf5e321 100644 --- a/wxdata/source/scwx/awips/impact_based_warnings.cpp +++ b/wxdata/source/scwx/awips/impact_based_warnings.cpp @@ -4,6 +4,7 @@ #include #include +#include namespace scwx { @@ -12,6 +13,26 @@ namespace awips static const std::string logPrefix_ = "scwx::awips::impact_based_warnings"; +static const boost::unordered_flat_map + phenomenaInfo_ { + {Phenomenon::Marine, PhenomenonInfo {.hasTornadoPossibleTag_ {true}}}, + {Phenomenon::FlashFlood, + PhenomenonInfo {.threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Catastrophic}}}, + {Phenomenon::SevereThunderstorm, + PhenomenonInfo {.hasTornadoPossibleTag_ {true}, + .threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Destructive}}}, + {Phenomenon::SnowSquall, PhenomenonInfo {}}, + {Phenomenon::Tornado, + PhenomenonInfo {.hasObservedTag_ {true}, + .threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Catastrophic}}}, + {Phenomenon::Unknown, PhenomenonInfo {}}}; + static const std::unordered_map threatCategoryName_ {{ThreatCategory::Base, "Base"}, {ThreatCategory::Significant, "Significant"}, @@ -20,6 +41,16 @@ static const std::unordered_map {ThreatCategory::Catastrophic, "Catastrophic"}, {ThreatCategory::Unknown, "?"}}; +const PhenomenonInfo& GetPhenomenonInfo(Phenomenon phenomenon) +{ + auto it = phenomenaInfo_.find(phenomenon); + if (it != phenomenaInfo_.cend()) + { + return it->second; + } + return phenomenaInfo_.at(Phenomenon::Unknown); +} + SCWX_GET_ENUM(ThreatCategory, GetThreatCategory, threatCategoryName_) const std::string& GetThreatCategoryName(ThreatCategory threatCategory) From 38a28317797f8bcd5725083513b7303397be747a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Sep 2024 21:05:07 -0500 Subject: [PATCH 067/762] Renaming PhenomenonInfo to ImpactBasedWarningInfo --- .../alert_palette_settings_widget.cpp | 9 ++-- .../scwx/awips/impact_based_warnings.hpp | 4 +- .../scwx/awips/impact_based_warnings.cpp | 44 ++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index c4680a62..2036769f 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -178,7 +178,8 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( QGridLayout* gridLayout = new QGridLayout(self_); page->setLayout(gridLayout); - const auto& phenomenonInfo = awips::GetPhenomenonInfo(phenomenon); + const auto& impactBasedWarningInfo = + awips::GetImpactBasedWarningInfo(phenomenon); int row = 0; @@ -187,17 +188,17 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( AddPhenomenonLine("Active", gridLayout, row++); - if (phenomenonInfo.hasObservedTag_) + if (impactBasedWarningInfo.hasObservedTag_) { AddPhenomenonLine("Observed", gridLayout, row++); } - if (phenomenonInfo.hasTornadoPossibleTag_) + if (impactBasedWarningInfo.hasTornadoPossibleTag_) { AddPhenomenonLine("Tornado Possible", gridLayout, row++); } - for (auto& category : phenomenonInfo.threatCategories_) + for (auto& category : impactBasedWarningInfo.threatCategories_) { if (category == awips::ThreatCategory::Base) { diff --git a/wxdata/include/scwx/awips/impact_based_warnings.hpp b/wxdata/include/scwx/awips/impact_based_warnings.hpp index 7bb07f5a..d45ffb8f 100644 --- a/wxdata/include/scwx/awips/impact_based_warnings.hpp +++ b/wxdata/include/scwx/awips/impact_based_warnings.hpp @@ -20,14 +20,14 @@ enum class ThreatCategory : int Unknown }; -struct PhenomenonInfo +struct ImpactBasedWarningInfo { bool hasObservedTag_ {false}; bool hasTornadoPossibleTag_ {false}; std::vector threatCategories_ {ThreatCategory::Base}; }; -const PhenomenonInfo& GetPhenomenonInfo(Phenomenon phenomenon); +const ImpactBasedWarningInfo& GetImpactBasedWarningInfo(Phenomenon phenomenon); ThreatCategory GetThreatCategory(const std::string& name); const std::string& GetThreatCategoryName(ThreatCategory threatCategory); diff --git a/wxdata/source/scwx/awips/impact_based_warnings.cpp b/wxdata/source/scwx/awips/impact_based_warnings.cpp index 1bf5e321..ca26fa7b 100644 --- a/wxdata/source/scwx/awips/impact_based_warnings.cpp +++ b/wxdata/source/scwx/awips/impact_based_warnings.cpp @@ -13,25 +13,29 @@ namespace awips static const std::string logPrefix_ = "scwx::awips::impact_based_warnings"; -static const boost::unordered_flat_map - phenomenaInfo_ { - {Phenomenon::Marine, PhenomenonInfo {.hasTornadoPossibleTag_ {true}}}, +static const boost::unordered_flat_map + impactBasedWarningInfo_ { + {Phenomenon::Marine, + ImpactBasedWarningInfo {.hasTornadoPossibleTag_ {true}}}, {Phenomenon::FlashFlood, - PhenomenonInfo {.threatCategories_ {ThreatCategory::Base, - ThreatCategory::Considerable, - ThreatCategory::Catastrophic}}}, + ImpactBasedWarningInfo { + .threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Catastrophic}}}, {Phenomenon::SevereThunderstorm, - PhenomenonInfo {.hasTornadoPossibleTag_ {true}, - .threatCategories_ {ThreatCategory::Base, - ThreatCategory::Considerable, - ThreatCategory::Destructive}}}, - {Phenomenon::SnowSquall, PhenomenonInfo {}}, + ImpactBasedWarningInfo { + .hasTornadoPossibleTag_ {true}, + .threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Destructive}}}, + {Phenomenon::SnowSquall, ImpactBasedWarningInfo {}}, {Phenomenon::Tornado, - PhenomenonInfo {.hasObservedTag_ {true}, - .threatCategories_ {ThreatCategory::Base, - ThreatCategory::Considerable, - ThreatCategory::Catastrophic}}}, - {Phenomenon::Unknown, PhenomenonInfo {}}}; + ImpactBasedWarningInfo { + .hasObservedTag_ {true}, + .threatCategories_ {ThreatCategory::Base, + ThreatCategory::Considerable, + ThreatCategory::Catastrophic}}}, + {Phenomenon::Unknown, ImpactBasedWarningInfo {}}}; static const std::unordered_map threatCategoryName_ {{ThreatCategory::Base, "Base"}, @@ -41,14 +45,14 @@ static const std::unordered_map {ThreatCategory::Catastrophic, "Catastrophic"}, {ThreatCategory::Unknown, "?"}}; -const PhenomenonInfo& GetPhenomenonInfo(Phenomenon phenomenon) +const ImpactBasedWarningInfo& GetImpactBasedWarningInfo(Phenomenon phenomenon) { - auto it = phenomenaInfo_.find(phenomenon); - if (it != phenomenaInfo_.cend()) + auto it = impactBasedWarningInfo_.find(phenomenon); + if (it != impactBasedWarningInfo_.cend()) { return it->second; } - return phenomenaInfo_.at(Phenomenon::Unknown); + return impactBasedWarningInfo_.at(Phenomenon::Unknown); } SCWX_GET_ENUM(ThreatCategory, GetThreatCategory, threatCategoryName_) From 7101cdf183d65ff0ded95379b06c8c2af876c638 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Sep 2024 21:13:45 -0500 Subject: [PATCH 068/762] Adding impact based warnings to ibw namespace --- scwx-qt/source/scwx/qt/model/alert_model.cpp | 19 +++++++++---------- .../alert_palette_settings_widget.cpp | 6 +++--- .../scwx/awips/impact_based_warnings.hpp | 3 +++ .../scwx/awips/text_product_message.hpp | 6 +++--- .../scwx/awips/impact_based_warnings.cpp | 5 ++++- .../scwx/awips/text_product_message.cpp | 13 +++++++------ 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index d4bb1111..fed7cc17 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -10,7 +10,6 @@ #include #include - #include #include @@ -37,9 +36,9 @@ public: explicit AlertModelImpl(); ~AlertModelImpl() = default; - bool GetObserved(const types::TextEventKey& key); - awips::ThreatCategory GetThreatCategory(const types::TextEventKey& key); - bool GetTornadoPossible(const types::TextEventKey& key); + bool GetObserved(const types::TextEventKey& key); + awips::ibw::ThreatCategory GetThreatCategory(const types::TextEventKey& key); + bool GetTornadoPossible(const types::TextEventKey& key); static std::string GetCounties(const types::TextEventKey& key); static std::string GetState(const types::TextEventKey& key); @@ -61,7 +60,7 @@ public: types::TextEventHash> observedMap_; std::unordered_map> threatCategoryMap_; std::unordered_map> - distanceMap_; - scwx::common::Coordinate previousPosition_; + distanceMap_; + scwx::common::Coordinate previousPosition_; }; AlertModel::AlertModel(QObject* parent) : @@ -158,7 +157,7 @@ QVariant AlertModel::data(const QModelIndex& index, int role) const case static_cast(Column::ThreatCategory): if (role == Qt::DisplayRole) { - return QString::fromStdString(awips::GetThreatCategoryName( + return QString::fromStdString(awips::ibw::GetThreatCategoryName( p->GetThreatCategory(textEventKey))); } else @@ -439,10 +438,10 @@ bool AlertModelImpl::GetObserved(const types::TextEventKey& key) return observed; } -awips::ThreatCategory +awips::ibw::ThreatCategory AlertModelImpl::GetThreatCategory(const types::TextEventKey& key) { - awips::ThreatCategory threatCategory = awips::ThreatCategory::Base; + awips::ibw::ThreatCategory threatCategory = awips::ibw::ThreatCategory::Base; auto it = threatCategoryMap_.find(key); if (it != threatCategoryMap_.cend()) diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 2036769f..0eb49807 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -179,7 +179,7 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( page->setLayout(gridLayout); const auto& impactBasedWarningInfo = - awips::GetImpactBasedWarningInfo(phenomenon); + awips::ibw::GetImpactBasedWarningInfo(phenomenon); int row = 0; @@ -200,13 +200,13 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( for (auto& category : impactBasedWarningInfo.threatCategories_) { - if (category == awips::ThreatCategory::Base) + if (category == awips::ibw::ThreatCategory::Base) { continue; } AddPhenomenonLine( - awips::GetThreatCategoryName(category), gridLayout, row++); + awips::ibw::GetThreatCategoryName(category), gridLayout, row++); } AddPhenomenonLine("Inactive", gridLayout, row++); diff --git a/wxdata/include/scwx/awips/impact_based_warnings.hpp b/wxdata/include/scwx/awips/impact_based_warnings.hpp index d45ffb8f..3d956893 100644 --- a/wxdata/include/scwx/awips/impact_based_warnings.hpp +++ b/wxdata/include/scwx/awips/impact_based_warnings.hpp @@ -9,6 +9,8 @@ namespace scwx { namespace awips { +namespace ibw +{ enum class ThreatCategory : int { @@ -32,5 +34,6 @@ const ImpactBasedWarningInfo& GetImpactBasedWarningInfo(Phenomenon phenomenon); ThreatCategory GetThreatCategory(const std::string& name); const std::string& GetThreatCategoryName(ThreatCategory threatCategory); +} // namespace ibw } // namespace awips } // namespace scwx diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index dec4af09..b043494f 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -64,9 +64,9 @@ struct Segment std::optional codedLocation_ {}; std::optional codedMotion_ {}; - bool observed_ {false}; - ThreatCategory threatCategory_ {ThreatCategory::Base}; - bool tornadoPossible_ {false}; + bool observed_ {false}; + ibw::ThreatCategory threatCategory_ {ibw::ThreatCategory::Base}; + bool tornadoPossible_ {false}; Segment() = default; diff --git a/wxdata/source/scwx/awips/impact_based_warnings.cpp b/wxdata/source/scwx/awips/impact_based_warnings.cpp index ca26fa7b..a5e63a76 100644 --- a/wxdata/source/scwx/awips/impact_based_warnings.cpp +++ b/wxdata/source/scwx/awips/impact_based_warnings.cpp @@ -10,8 +10,10 @@ namespace scwx { namespace awips { +namespace ibw +{ -static const std::string logPrefix_ = "scwx::awips::impact_based_warnings"; +static const std::string logPrefix_ = "scwx::awips::ibw::impact_based_warnings"; static const boost::unordered_flat_map impactBasedWarningInfo_ { @@ -62,5 +64,6 @@ const std::string& GetThreatCategoryName(ThreatCategory threatCategory) return threatCategoryName_.at(threatCategory); } +} // namespace ibw } // namespace awips } // namespace scwx diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 541a1a1a..54ce7e25 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -378,7 +378,7 @@ void ParseCodedInformation(std::shared_ptr segment, segment->tornadoPossible_ = true; } - else if (segment->threatCategory_ == ThreatCategory::Base && + else if (segment->threatCategory_ == ibw::ThreatCategory::Base && (threatTagIt = std::find_if(kThreatCategoryTags.cbegin(), kThreatCategoryTags.cend(), [&it](const std::string& tag) { @@ -389,18 +389,19 @@ void ParseCodedInformation(std::shared_ptr segment, const std::string threatCategoryName = it->substr(threatTagIt->length()); - ThreatCategory threatCategory = GetThreatCategory(threatCategoryName); + ibw::ThreatCategory threatCategory = + ibw::GetThreatCategory(threatCategoryName); switch (threatCategory) { - case ThreatCategory::Significant: + case ibw::ThreatCategory::Significant: // "Significant" is no longer an official tag, and has largely been // replaced with "Considerable". - threatCategory = ThreatCategory::Considerable; + threatCategory = ibw::ThreatCategory::Considerable; break; - case ThreatCategory::Unknown: - threatCategory = ThreatCategory::Base; + case ibw::ThreatCategory::Unknown: + threatCategory = ibw::ThreatCategory::Base; break; default: From 8fc392681a6a0885be55492eead969d7cd3476f5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Sep 2024 22:02:41 -0500 Subject: [PATCH 069/762] Initial impact based warning palette settings TODO: - Update settings to use line component data - Add interface to data --- .../scwx/qt/settings/palette_settings.cpp | 179 +++++++++++++----- .../scwx/qt/settings/palette_settings.hpp | 1 + 2 files changed, 134 insertions(+), 46 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 9d9c93fa..4c61a916 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -76,66 +77,59 @@ static const awips::Phenomenon kDefaultPhenomenon_ {awips::Phenomenon::Marine}; class PaletteSettings::Impl { public: - explicit Impl() + struct AlertData { - palette_.reserve(kPaletteKeys_.size()); - - for (const auto& name : kPaletteKeys_) + AlertData(awips::Phenomenon phenomenon) : phenomenon_ {phenomenon} { - const std::string& defaultValue = kDefaultPalettes_.at(name); - - auto result = - palette_.emplace(name, SettingsVariable {name}); - - SettingsVariable& settingsVariable = result.first->second; - - settingsVariable.SetDefault(defaultValue); - - variables_.push_back(&settingsVariable); - }; - - activeAlertColor_.reserve(kAlertColors_.size()); - inactiveAlertColor_.reserve(kAlertColors_.size()); - - for (auto& alert : kAlertColors_) - { - std::string phenomenonCode = awips::GetPhenomenonCode(alert.first); - std::string activeName = fmt::format("{}-active", phenomenonCode); - std::string inactiveName = fmt::format("{}-inactive", phenomenonCode); - - auto activeResult = activeAlertColor_.emplace( - alert.first, SettingsVariable {activeName}); - auto inactiveResult = inactiveAlertColor_.emplace( - alert.first, SettingsVariable {inactiveName}); - - SettingsVariable& activeVariable = - activeResult.first->second; - SettingsVariable& inactiveVariable = - inactiveResult.first->second; - - activeVariable.SetDefault( - util::color::ToArgbString(alert.second.first)); - inactiveVariable.SetDefault( - util::color::ToArgbString(alert.second.second)); - - activeVariable.SetValidator(&ValidateColor); - inactiveVariable.SetValidator(&ValidateColor); - - variables_.push_back(&activeVariable); - variables_.push_back(&inactiveVariable); + auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon); + for (auto& threatCategory : info.threatCategories_) + { + std::string threatCategoryName = + awips::ibw::GetThreatCategoryName(threatCategory); + boost::algorithm::to_lower(threatCategoryName); + threatCategoryMap_.emplace(threatCategory, threatCategoryName); + } } + + void RegisterVariables(SettingsCategory& settings); + + awips::Phenomenon phenomenon_; + + std::map> + threatCategoryMap_ {}; + + SettingsVariable observed_ {"observed"}; + SettingsVariable tornadoPossible_ {"tornado_possible"}; + SettingsVariable inactive_ {"inactive"}; + }; + + explicit Impl(PaletteSettings* self) : self_ {self} + { + InitializeColorTables(); + InitializeLegacyAlerts(); + InitializeAlerts(); } ~Impl() {} + void InitializeColorTables(); + void InitializeLegacyAlerts(); + void InitializeAlerts(); + static bool ValidateColor(const std::string& value); + PaletteSettings* self_; + std::unordered_map> palette_ {}; std::unordered_map> activeAlertColor_ {}; std::unordered_map> inactiveAlertColor_ {}; std::vector variables_ {}; + + std::unordered_map alertDataMap_ {}; + + std::vector alertSettings_ {}; }; bool PaletteSettings::Impl::ValidateColor(const std::string& value) @@ -145,7 +139,7 @@ bool PaletteSettings::Impl::ValidateColor(const std::string& value) } PaletteSettings::PaletteSettings() : - SettingsCategory("palette"), p(std::make_unique()) + SettingsCategory("palette"), p(std::make_unique(this)) { RegisterVariables(p->variables_); SetDefaults(); @@ -158,6 +152,99 @@ PaletteSettings::PaletteSettings(PaletteSettings&&) noexcept = default; PaletteSettings& PaletteSettings::operator=(PaletteSettings&&) noexcept = default; +void PaletteSettings::Impl::InitializeColorTables() +{ + palette_.reserve(kPaletteKeys_.size()); + + for (const auto& name : kPaletteKeys_) + { + const std::string& defaultValue = kDefaultPalettes_.at(name); + + auto result = + palette_.emplace(name, SettingsVariable {name}); + + SettingsVariable& settingsVariable = result.first->second; + + settingsVariable.SetDefault(defaultValue); + + variables_.push_back(&settingsVariable); + }; +} + +void PaletteSettings::Impl::InitializeLegacyAlerts() +{ + activeAlertColor_.reserve(kAlertColors_.size()); + inactiveAlertColor_.reserve(kAlertColors_.size()); + + for (auto& alert : kAlertColors_) + { + std::string phenomenonCode = awips::GetPhenomenonCode(alert.first); + std::string activeName = fmt::format("{}-active", phenomenonCode); + std::string inactiveName = fmt::format("{}-inactive", phenomenonCode); + + auto activeResult = activeAlertColor_.emplace( + alert.first, SettingsVariable {activeName}); + auto inactiveResult = inactiveAlertColor_.emplace( + alert.first, SettingsVariable {inactiveName}); + + SettingsVariable& activeVariable = + activeResult.first->second; + SettingsVariable& inactiveVariable = + inactiveResult.first->second; + + activeVariable.SetDefault(util::color::ToArgbString(alert.second.first)); + inactiveVariable.SetDefault( + util::color::ToArgbString(alert.second.second)); + + activeVariable.SetValidator(&ValidateColor); + inactiveVariable.SetValidator(&ValidateColor); + + variables_.push_back(&activeVariable); + variables_.push_back(&inactiveVariable); + } +} + +void PaletteSettings::Impl::InitializeAlerts() +{ + for (auto phenomenon : PaletteSettings::alert_phenomena()) + { + auto pair = alertDataMap_.emplace( + std::make_pair(phenomenon, AlertData {phenomenon})); + auto& alertData = pair.first->second; + + // Variable registration + auto& settings = alertSettings_.emplace_back( + SettingsCategory {awips::GetPhenomenonCode(phenomenon)}); + + alertData.RegisterVariables(settings); + } + + self_->RegisterSubcategoryArray("alerts", alertSettings_); +} + +void PaletteSettings::Impl::AlertData::RegisterVariables( + SettingsCategory& settings) +{ + auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon_); + + for (auto& threatCategory : threatCategoryMap_) + { + settings.RegisterVariables({&threatCategory.second}); + } + + if (info.hasObservedTag_) + { + settings.RegisterVariables({&observed_}); + } + + if (info.hasTornadoPossibleTag_) + { + settings.RegisterVariables({&tornadoPossible_}); + } + + settings.RegisterVariables({&inactive_}); +} + SettingsVariable& PaletteSettings::palette(const std::string& name) const { diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index c0f7985a..bf21e119 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include From c8dc8ed6303c39c294e56cafb461390d530fa05a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 19 Sep 2024 23:15:41 -0500 Subject: [PATCH 070/762] Refactor color validator from settings to utility source --- .../source/scwx/qt/settings/palette_settings.cpp | 13 ++----------- scwx-qt/source/scwx/qt/util/color.cpp | 7 +++++++ scwx-qt/source/scwx/qt/util/color.hpp | 9 +++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 4c61a916..12e3fe99 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -5,7 +5,6 @@ #include #include #include -#include namespace scwx { @@ -116,8 +115,6 @@ public: void InitializeLegacyAlerts(); void InitializeAlerts(); - static bool ValidateColor(const std::string& value); - PaletteSettings* self_; std::unordered_map> palette_ {}; @@ -132,12 +129,6 @@ public: std::vector alertSettings_ {}; }; -bool PaletteSettings::Impl::ValidateColor(const std::string& value) -{ - static constexpr LazyRE2 re = {"#[0-9A-Fa-f]{8}"}; - return RE2::FullMatch(value, *re); -} - PaletteSettings::PaletteSettings() : SettingsCategory("palette"), p(std::make_unique(this)) { @@ -196,8 +187,8 @@ void PaletteSettings::Impl::InitializeLegacyAlerts() inactiveVariable.SetDefault( util::color::ToArgbString(alert.second.second)); - activeVariable.SetValidator(&ValidateColor); - inactiveVariable.SetValidator(&ValidateColor); + activeVariable.SetValidator(&util::color::ValidateArgbString); + inactiveVariable.SetValidator(&util::color::ValidateArgbString); variables_.push_back(&activeVariable); variables_.push_back(&inactiveVariable); diff --git a/scwx-qt/source/scwx/qt/util/color.cpp b/scwx-qt/source/scwx/qt/util/color.cpp index 6e193dc9..16060bb9 100644 --- a/scwx-qt/source/scwx/qt/util/color.cpp +++ b/scwx-qt/source/scwx/qt/util/color.cpp @@ -1,6 +1,7 @@ #include #include +#include #include namespace scwx @@ -38,6 +39,12 @@ boost::gil::rgba32f_pixel_t ToRgba32fPixelT(const std::string& argbString) rgba8Pixel[3] / 255.0f}; } +bool ValidateArgbString(const std::string& argbString) +{ + static constexpr LazyRE2 re = {"#[0-9A-Fa-f]{8}"}; + return RE2::FullMatch(argbString, *re); +} + } // namespace color } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/color.hpp b/scwx-qt/source/scwx/qt/util/color.hpp index 73ca07f1..6d90fe56 100644 --- a/scwx-qt/source/scwx/qt/util/color.hpp +++ b/scwx-qt/source/scwx/qt/util/color.hpp @@ -39,6 +39,15 @@ boost::gil::rgba8_pixel_t ToRgba8PixelT(const std::string& argbString); */ boost::gil::rgba32f_pixel_t ToRgba32fPixelT(const std::string& argbString); +/** + * Validates an ARGB string used by Qt libraries. + * + * @param argbString + * + * @return Validity of ARGB string + */ +bool ValidateArgbString(const std::string& argbString); + } // namespace color } // namespace util } // namespace qt From cea299366506029219be9f8ff4cf35fa8fe28f9a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 19 Sep 2024 23:16:54 -0500 Subject: [PATCH 071/762] Add LineSettings class to use as subcategory --- scwx-qt/scwx-qt.cmake | 2 + .../source/scwx/qt/settings/line_settings.cpp | 121 ++++++++++++++++++ .../source/scwx/qt/settings/line_settings.hpp | 45 +++++++ 3 files changed, 168 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/settings/line_settings.cpp create mode 100644 scwx-qt/source/scwx/qt/settings/line_settings.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 52d26ccc..530f93a1 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -172,6 +172,7 @@ set(SRC_REQUEST source/scwx/qt/request/download_request.cpp set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/hotkey_settings.hpp + source/scwx/qt/settings/line_settings.hpp source/scwx/qt/settings/map_settings.hpp source/scwx/qt/settings/palette_settings.hpp source/scwx/qt/settings/product_settings.hpp @@ -188,6 +189,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/hotkey_settings.cpp + source/scwx/qt/settings/line_settings.cpp source/scwx/qt/settings/map_settings.cpp source/scwx/qt/settings/palette_settings.cpp source/scwx/qt/settings/product_settings.cpp diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp new file mode 100644 index 00000000..bc467186 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -0,0 +1,121 @@ +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +static const std::string logPrefix_ = "scwx::qt::settings::line_settings"; + +static const boost::gil::rgba8_pixel_t kTransparentColor_ {0, 0, 0, 0}; +static const std::string kTransparentColorString_ { + util::color::ToArgbString(kTransparentColor_)}; + +static const boost::gil::rgba8_pixel_t kBlackColor_ {0, 0, 0, 255}; +static const std::string kBlackColorString_ { + util::color::ToArgbString(kBlackColor_)}; + +static const boost::gil::rgba8_pixel_t kWhiteColor_ {255, 255, 255, 255}; +static const std::string kWhiteColorString_ { + util::color::ToArgbString(kWhiteColor_)}; + +class LineSettings::Impl +{ +public: + explicit Impl() + { + lineColor_.SetDefault(kWhiteColorString_); + highlightColor_.SetDefault(kTransparentColorString_); + borderColor_.SetDefault(kBlackColorString_); + + lineWidth_.SetDefault(3); + highlightWidth_.SetDefault(0); + borderWidth_.SetDefault(1); + + lineWidth_.SetMinimum(1); + highlightWidth_.SetMinimum(0); + borderWidth_.SetMinimum(0); + + lineWidth_.SetMaximum(9); + highlightWidth_.SetMaximum(9); + borderWidth_.SetMaximum(9); + + lineColor_.SetValidator(&util::color::ValidateArgbString); + highlightColor_.SetValidator(&util::color::ValidateArgbString); + borderColor_.SetValidator(&util::color::ValidateArgbString); + } + ~Impl() {} + + SettingsVariable lineColor_ {"line_color"}; + SettingsVariable highlightColor_ {"highlight_color"}; + SettingsVariable borderColor_ {"border_color"}; + + SettingsVariable lineWidth_ {"line_width"}; + SettingsVariable highlightWidth_ {"highlight_width"}; + SettingsVariable borderWidth_ {"border_width"}; +}; + +LineSettings::LineSettings(const std::string& name) : + SettingsCategory(name), p(std::make_unique()) +{ + RegisterVariables({&p->lineColor_, + &p->highlightColor_, + &p->borderColor_, + &p->lineWidth_, + &p->highlightWidth_, + &p->borderWidth_}); + SetDefaults(); +} +LineSettings::~LineSettings() = default; + +LineSettings::LineSettings(LineSettings&&) noexcept = default; +LineSettings& LineSettings::operator=(LineSettings&&) noexcept = default; + +SettingsVariable& LineSettings::border_color() const +{ + return p->borderColor_; +} + +SettingsVariable& LineSettings::highlight_color() const +{ + return p->highlightColor_; +} + +SettingsVariable& LineSettings::line_color() const +{ + return p->lineColor_; +} + +SettingsVariable& LineSettings::border_width() const +{ + return p->borderWidth_; +} + +SettingsVariable& LineSettings::highlight_width() const +{ + return p->highlightWidth_; +} + +SettingsVariable& LineSettings::line_width() const +{ + return p->lineWidth_; +} + +bool operator==(const LineSettings& lhs, const LineSettings& rhs) +{ + return (lhs.p->borderColor_ == rhs.p->borderColor_ && + lhs.p->highlightColor_ == rhs.p->highlightColor_ && + lhs.p->lineColor_ == rhs.p->lineColor_ && + lhs.p->borderWidth_ == rhs.p->borderWidth_ && + lhs.p->highlightWidth_ == rhs.p->highlightWidth_ && + lhs.p->lineWidth_ == rhs.p->lineWidth_); +} + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp new file mode 100644 index 00000000..e99d9b3e --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +class LineSettings : public SettingsCategory +{ +public: + explicit LineSettings(const std::string& name); + ~LineSettings(); + + LineSettings(const LineSettings&) = delete; + LineSettings& operator=(const LineSettings&) = delete; + + LineSettings(LineSettings&&) noexcept; + LineSettings& operator=(LineSettings&&) noexcept; + + SettingsVariable& border_color() const; + SettingsVariable& highlight_color() const; + SettingsVariable& line_color() const; + + SettingsVariable& border_width() const; + SettingsVariable& highlight_width() const; + SettingsVariable& line_width() const; + + friend bool operator==(const LineSettings& lhs, const LineSettings& rhs); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace settings +} // namespace qt +} // namespace scwx From 1a30743c0a0b6ca11793f529aaa3655e5a90603c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 08:28:09 -0500 Subject: [PATCH 072/762] Add subcategory functionality to settings, still need to read/write JSON --- .../scwx/qt/settings/settings_category.cpp | 40 +++++++++++++++++++ .../scwx/qt/settings/settings_category.hpp | 3 ++ 2 files changed, 43 insertions(+) diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index e6c929f8..a346c50d 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -25,6 +25,7 @@ public: std::vector>> subcategoryArrays_; + std::vector subcategories_; std::vector variables_; }; @@ -54,6 +55,12 @@ void SettingsCategory::SetDefaults() } } + // Set subcategory defaults + for (auto& subcategory : p->subcategories_) + { + subcategory->SetDefaults(); + } + // Set variable defaults for (auto& variable : p->variables_) { @@ -111,6 +118,14 @@ bool SettingsCategory::ReadJson(const boost::json::object& json) } } + // Read subcategories + for (auto& subcategory : p->subcategories_) + { + (void) subcategory; + // TODO: + // subcategory->ReadJson(object); + } + // Read variables for (auto& variable : p->variables_) { @@ -154,6 +169,14 @@ void SettingsCategory::WriteJson(boost::json::object& json) const object.insert_or_assign(subcategoryArray.first, arrayObject); } + // Write subcategories + for (auto& subcategory : p->subcategories_) + { + (void) subcategory; + // TODO: + // subcategory->WriteJson(); + } + // Write variables for (auto& variable : p->variables_) { @@ -163,6 +186,11 @@ void SettingsCategory::WriteJson(boost::json::object& json) const json.insert_or_assign(p->name_, object); } +void SettingsCategory::RegisterSubcategory(SettingsCategory& subcategory) +{ + p->subcategories_.push_back(&subcategory); +} + void SettingsCategory::RegisterSubcategoryArray( const std::string& name, std::vector& subcategories) { @@ -175,6 +203,18 @@ void SettingsCategory::RegisterSubcategoryArray( [](SettingsCategory& subcategory) { return &subcategory; }); } +void SettingsCategory::RegisterSubcategoryArray( + const std::string& name, std::vector& subcategories) +{ + auto& newSubcategories = p->subcategoryArrays_.emplace_back( + name, std::vector {}); + + std::transform(subcategories.begin(), + subcategories.end(), + std::back_inserter(newSubcategories.second), + [](SettingsCategory* subcategory) { return subcategory; }); +} + void SettingsCategory::RegisterVariables( std::initializer_list variables) { diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index 2da7b9ab..bea48659 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -50,8 +50,11 @@ public: */ virtual void WriteJson(boost::json::object& json) const; + void RegisterSubcategory(SettingsCategory& subcategory); void RegisterSubcategoryArray(const std::string& name, std::vector& subcategories); + void RegisterSubcategoryArray(const std::string& name, + std::vector& subcategories); void RegisterVariables(std::initializer_list variables); void RegisterVariables(std::vector variables); From efee1653e1589d32bc34e78ee53a9663a66ae8a1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 08:29:13 -0500 Subject: [PATCH 073/762] Add alert palette settings --- scwx-qt/scwx-qt.cmake | 6 +- .../qt/settings/alert_palette_settings.cpp | 112 ++++++++++++++++++ .../qt/settings/alert_palette_settings.hpp | 46 +++++++ 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp create mode 100644 scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 530f93a1..06646bbe 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -169,7 +169,8 @@ set(HDR_REQUEST source/scwx/qt/request/download_request.hpp source/scwx/qt/request/nexrad_file_request.hpp) set(SRC_REQUEST source/scwx/qt/request/download_request.cpp source/scwx/qt/request/nexrad_file_request.cpp) -set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp +set(HDR_SETTINGS source/scwx/qt/settings/alert_palette_settings.hpp + source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/hotkey_settings.hpp source/scwx/qt/settings/line_settings.hpp @@ -186,7 +187,8 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/text_settings.hpp source/scwx/qt/settings/ui_settings.hpp source/scwx/qt/settings/unit_settings.hpp) -set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp +set(SRC_SETTINGS source/scwx/qt/settings/alert_palette_settings.cpp + source/scwx/qt/settings/audio_settings.cpp source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/hotkey_settings.cpp source/scwx/qt/settings/line_settings.cpp diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp new file mode 100644 index 00000000..ed82ef7e --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp @@ -0,0 +1,112 @@ +#include +#include + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +static const std::string logPrefix_ = + "scwx::qt::settings::alert_palette_settings"; + +class AlertPaletteSettings::Impl +{ +public: + explicit Impl(awips::Phenomenon phenomenon) : phenomenon_ {phenomenon} + { + auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon); + for (auto& threatCategory : info.threatCategories_) + { + std::string threatCategoryName = + awips::ibw::GetThreatCategoryName(threatCategory); + boost::algorithm::to_lower(threatCategoryName); + threatCategoryMap_.emplace(threatCategory, threatCategoryName); + } + } + ~Impl() {} + + awips::Phenomenon phenomenon_; + + std::map threatCategoryMap_ {}; + + LineSettings observed_ {"observed"}; + LineSettings tornadoPossible_ {"tornado_possible"}; + LineSettings inactive_ {"inactive"}; +}; + +AlertPaletteSettings::AlertPaletteSettings(awips::Phenomenon phenomenon) : + SettingsCategory(awips::GetPhenomenonCode(phenomenon)), + p(std::make_unique(phenomenon)) +{ + auto& info = awips::ibw::GetImpactBasedWarningInfo(p->phenomenon_); + for (auto& threatCategory : p->threatCategoryMap_) + { + RegisterSubcategory(threatCategory.second); + } + + if (info.hasObservedTag_) + { + RegisterSubcategory(p->observed_); + } + + if (info.hasTornadoPossibleTag_) + { + RegisterSubcategory(p->tornadoPossible_); + } + + RegisterSubcategory(p->inactive_); + + SetDefaults(); +} +AlertPaletteSettings::~AlertPaletteSettings() = default; + +AlertPaletteSettings::AlertPaletteSettings(AlertPaletteSettings&&) noexcept = + default; +AlertPaletteSettings& +AlertPaletteSettings::operator=(AlertPaletteSettings&&) noexcept = default; + +LineSettings& AlertPaletteSettings::threat_category( + awips::ibw::ThreatCategory threatCategory) const +{ + auto it = p->threatCategoryMap_.find(threatCategory); + if (it != p->threatCategoryMap_.cend()) + { + return it->second; + } + return p->threatCategoryMap_.at(awips::ibw::ThreatCategory::Base); +} + +LineSettings& AlertPaletteSettings::inactive() const +{ + return p->inactive_; +} + +LineSettings& AlertPaletteSettings::observed() const +{ + return p->observed_; +} + +LineSettings& AlertPaletteSettings::tornado_possible() const +{ + return p->tornadoPossible_; +} + +bool operator==(const AlertPaletteSettings& lhs, + const AlertPaletteSettings& rhs) +{ + return (lhs.p->threatCategoryMap_ == rhs.p->threatCategoryMap_ && + lhs.p->inactive_ == rhs.p->inactive_ && + lhs.p->observed_ == rhs.p->observed_ && + lhs.p->tornadoPossible_ == rhs.p->tornadoPossible_); +} + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp new file mode 100644 index 00000000..152a6351 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +class AlertPaletteSettings : public SettingsCategory +{ +public: + explicit AlertPaletteSettings(awips::Phenomenon phenomenon); + ~AlertPaletteSettings(); + + AlertPaletteSettings(const AlertPaletteSettings&) = delete; + AlertPaletteSettings& operator=(const AlertPaletteSettings&) = delete; + + AlertPaletteSettings(AlertPaletteSettings&&) noexcept; + AlertPaletteSettings& operator=(AlertPaletteSettings&&) noexcept; + + LineSettings& + threat_category(awips::ibw::ThreatCategory threatCategory) const; + LineSettings& inactive() const; + LineSettings& observed() const; + LineSettings& tornado_possible() const; + + friend bool operator==(const AlertPaletteSettings& lhs, + const AlertPaletteSettings& rhs); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace settings +} // namespace qt +} // namespace scwx From 47b7d475c8d9d068e06f07fb4171704e5f95c5d9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 08:30:17 -0500 Subject: [PATCH 074/762] Use new alert palette settings in parent palette settings --- .../scwx/qt/settings/palette_settings.cpp | 75 ++++--------------- .../scwx/qt/settings/palette_settings.hpp | 2 + 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 12e3fe99..c5902fb3 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -76,32 +76,6 @@ static const awips::Phenomenon kDefaultPhenomenon_ {awips::Phenomenon::Marine}; class PaletteSettings::Impl { public: - struct AlertData - { - AlertData(awips::Phenomenon phenomenon) : phenomenon_ {phenomenon} - { - auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon); - for (auto& threatCategory : info.threatCategories_) - { - std::string threatCategoryName = - awips::ibw::GetThreatCategoryName(threatCategory); - boost::algorithm::to_lower(threatCategoryName); - threatCategoryMap_.emplace(threatCategory, threatCategoryName); - } - } - - void RegisterVariables(SettingsCategory& settings); - - awips::Phenomenon phenomenon_; - - std::map> - threatCategoryMap_ {}; - - SettingsVariable observed_ {"observed"}; - SettingsVariable tornadoPossible_ {"tornado_possible"}; - SettingsVariable inactive_ {"inactive"}; - }; - explicit Impl(PaletteSettings* self) : self_ {self} { InitializeColorTables(); @@ -124,9 +98,8 @@ public: inactiveAlertColor_ {}; std::vector variables_ {}; - std::unordered_map alertDataMap_ {}; - - std::vector alertSettings_ {}; + std::unordered_map + alertPaletteMap_ {}; }; PaletteSettings::PaletteSettings() : @@ -197,43 +170,19 @@ void PaletteSettings::Impl::InitializeLegacyAlerts() void PaletteSettings::Impl::InitializeAlerts() { + std::vector alertSettings {}; + for (auto phenomenon : PaletteSettings::alert_phenomena()) { - auto pair = alertDataMap_.emplace( - std::make_pair(phenomenon, AlertData {phenomenon})); - auto& alertData = pair.first->second; + auto result = alertPaletteMap_.emplace(phenomenon, phenomenon); + auto& it = result.first; + AlertPaletteSettings& alertPaletteSettings = it->second; // Variable registration - auto& settings = alertSettings_.emplace_back( - SettingsCategory {awips::GetPhenomenonCode(phenomenon)}); - - alertData.RegisterVariables(settings); + alertSettings.push_back(&alertPaletteSettings); } - self_->RegisterSubcategoryArray("alerts", alertSettings_); -} - -void PaletteSettings::Impl::AlertData::RegisterVariables( - SettingsCategory& settings) -{ - auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon_); - - for (auto& threatCategory : threatCategoryMap_) - { - settings.RegisterVariables({&threatCategory.second}); - } - - if (info.hasObservedTag_) - { - settings.RegisterVariables({&observed_}); - } - - if (info.hasTornadoPossibleTag_) - { - settings.RegisterVariables({&tornadoPossible_}); - } - - settings.RegisterVariables({&inactive_}); + self_->RegisterSubcategoryArray("alerts", alertSettings); } SettingsVariable& @@ -272,6 +221,12 @@ PaletteSettings::alert_color(awips::Phenomenon phenomenon, bool active) const } } +AlertPaletteSettings& +PaletteSettings::alert_palette(awips::Phenomenon phenomenon) +{ + return p->alertPaletteMap_.at(phenomenon); +} + const std::vector& PaletteSettings::alert_phenomena() { static const std::vector kAlertPhenomena_ { diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index bf21e119..eb52e600 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -30,6 +31,7 @@ public: SettingsVariable& palette(const std::string& name) const; SettingsVariable& alert_color(awips::Phenomenon phenomenon, bool active) const; + AlertPaletteSettings& alert_palette(awips::Phenomenon); static const std::vector& alert_phenomena(); From 9f4a798d673d2026f4d82dabebdf88949d864d0c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 17:15:22 -0500 Subject: [PATCH 075/762] Read and write settings subcategories --- scwx-qt/source/scwx/qt/settings/settings_category.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index a346c50d..2258baeb 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -121,9 +121,7 @@ bool SettingsCategory::ReadJson(const boost::json::object& json) // Read subcategories for (auto& subcategory : p->subcategories_) { - (void) subcategory; - // TODO: - // subcategory->ReadJson(object); + validated &= subcategory->ReadJson(object); } // Read variables @@ -172,9 +170,7 @@ void SettingsCategory::WriteJson(boost::json::object& json) const // Write subcategories for (auto& subcategory : p->subcategories_) { - (void) subcategory; - // TODO: - // subcategory->WriteJson(); + subcategory->WriteJson(object); } // Write variables From 8212d24d345200b6576277aa1862f4ea8fe49588 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 23:11:26 -0500 Subject: [PATCH 076/762] Connect line label to alert palette settings - Line label will initialize to settings value - Changes to the line label will stage settings --- scwx-qt/source/scwx/qt/ui/line_label.cpp | 66 ++++++++++++++++++- scwx-qt/source/scwx/qt/ui/line_label.hpp | 4 ++ .../alert_palette_settings_widget.cpp | 36 +++++++--- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp index 5248057c..a8be0507 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.cpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -20,6 +21,8 @@ public: explicit Impl() {}; ~Impl() = default; + void ResetLineSettings(); + QImage GenerateImage() const; std::size_t borderWidth_ {1}; @@ -30,6 +33,8 @@ public: boost::gil::rgba8_pixel_t highlightColor_ {255, 255, 0, 255}; boost::gil::rgba8_pixel_t lineColor_ {0, 0, 255, 255}; + settings::LineSettings* lineSettings_ {nullptr}; + QPixmap pixmap_ {}; bool pixmapDirty_ {true}; }; @@ -39,7 +44,15 @@ LineLabel::LineLabel(QWidget* parent) : { } -LineLabel::~LineLabel() {} +LineLabel::~LineLabel() +{ + p->ResetLineSettings(); +} + +void LineLabel::Impl::ResetLineSettings() +{ + lineSettings_ = nullptr; +} boost::gil::rgba8_pixel_t LineLabel::border_color() const { @@ -77,6 +90,11 @@ void LineLabel::set_border_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->border_width().StageValue(width); + } } void LineLabel::set_highlight_width(std::size_t width) @@ -85,6 +103,11 @@ void LineLabel::set_highlight_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->highlight_width().StageValue(width); + } } void LineLabel::set_line_width(std::size_t width) @@ -93,6 +116,11 @@ void LineLabel::set_line_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->line_width().StageValue(width); + } } void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) @@ -100,6 +128,12 @@ void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) p->borderColor_ = color; p->pixmapDirty_ = true; update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->border_color().StageValue( + util::color::ToArgbString(color)); + } } void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) @@ -107,6 +141,12 @@ void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) p->highlightColor_ = color; p->pixmapDirty_ = true; update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->highlight_color().StageValue( + util::color::ToArgbString(color)); + } } void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) @@ -114,6 +154,30 @@ void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) p->lineColor_ = color; p->pixmapDirty_ = true; update(); + + if (p->lineSettings_ != nullptr) + { + p->lineSettings_->line_color().StageValue( + util::color::ToArgbString(color)); + } +} + +void LineLabel::set_line_settings(settings::LineSettings& lineSettings) +{ + p->ResetLineSettings(); + + set_border_color(util::color::ToRgba8PixelT( + lineSettings.border_color().GetStagedOrValue())); + set_highlight_color(util::color::ToRgba8PixelT( + lineSettings.highlight_color().GetStagedOrValue())); + set_line_color( + util::color::ToRgba8PixelT(lineSettings.line_color().GetStagedOrValue())); + + set_border_width(lineSettings.border_width().GetStagedOrValue()); + set_highlight_width(lineSettings.highlight_width().GetStagedOrValue()); + set_line_width(lineSettings.line_width().GetStagedOrValue()); + + p->lineSettings_ = &lineSettings; } QSize LineLabel::minimumSizeHint() const diff --git a/scwx-qt/source/scwx/qt/ui/line_label.hpp b/scwx-qt/source/scwx/qt/ui/line_label.hpp index 2c2d516b..b746a98e 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.hpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -36,6 +38,8 @@ public: void set_highlight_width(std::size_t width); void set_line_width(std::size_t width); + void set_line_settings(settings::LineSettings& lineSettings); + protected: QSize minimumSizeHint() const override; QSize sizeHint() const override; diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 0eb49807..0a63add4 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -37,8 +37,10 @@ public: } ~Impl() = default; - void - AddPhenomenonLine(const std::string& name, QGridLayout* layout, int row); + void AddPhenomenonLine(const std::string& name, + settings::LineSettings& lineSettings, + QGridLayout* layout, + int row); QWidget* CreateStackedWidgetPage(awips::Phenomenon phenomenon); void ConnectSignals(); void SelectPhenomenon(awips::Phenomenon phenomenon); @@ -181,21 +183,31 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( const auto& impactBasedWarningInfo = awips::ibw::GetImpactBasedWarningInfo(phenomenon); + auto& alertPalette = + settings::PaletteSettings::Instance().alert_palette(phenomenon); + int row = 0; // Add a blank label to align left and right widgets gridLayout->addWidget(new QLabel(self_), row++, 0); - AddPhenomenonLine("Active", gridLayout, row++); + AddPhenomenonLine( + "Active", + alertPalette.threat_category(awips::ibw::ThreatCategory::Base), + gridLayout, + row++); if (impactBasedWarningInfo.hasObservedTag_) { - AddPhenomenonLine("Observed", gridLayout, row++); + AddPhenomenonLine("Observed", alertPalette.observed(), gridLayout, row++); } if (impactBasedWarningInfo.hasTornadoPossibleTag_) { - AddPhenomenonLine("Tornado Possible", gridLayout, row++); + AddPhenomenonLine("Tornado Possible", + alertPalette.tornado_possible(), + gridLayout, + row++); } for (auto& category : impactBasedWarningInfo.threatCategories_) @@ -205,11 +217,13 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( continue; } - AddPhenomenonLine( - awips::ibw::GetThreatCategoryName(category), gridLayout, row++); + AddPhenomenonLine(awips::ibw::GetThreatCategoryName(category), + alertPalette.threat_category(category), + gridLayout, + row++); } - AddPhenomenonLine("Inactive", gridLayout, row++); + AddPhenomenonLine("Inactive", alertPalette.inactive(), gridLayout, row++); QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); @@ -219,12 +233,16 @@ QWidget* AlertPaletteSettingsWidget::Impl::CreateStackedWidgetPage( } void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( - const std::string& name, QGridLayout* layout, int row) + const std::string& name, + settings::LineSettings& lineSettings, + QGridLayout* layout, + int row) { QToolButton* toolButton = new QToolButton(self_); toolButton->setText(tr("...")); LineLabel* lineLabel = new LineLabel(self_); + lineLabel->set_line_settings(lineSettings); layout->addWidget(new QLabel(tr(name.c_str()), self_), row, 0); layout->addWidget(lineLabel, row, 1); From 6b0aaea7738588a303357c94d54c78f29ba339f1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 23:31:47 -0500 Subject: [PATCH 077/762] Commit line settings changes on apply --- .../scwx/qt/settings/settings_category.cpp | 28 +++++++++++++++++++ .../scwx/qt/settings/settings_category.hpp | 9 ++++++ .../alert_palette_settings_widget.cpp | 2 ++ .../qt/ui/settings/settings_page_widget.cpp | 12 ++++++++ .../qt/ui/settings/settings_page_widget.hpp | 2 ++ 5 files changed, 53 insertions(+) diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 2258baeb..34859ae2 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -68,6 +68,34 @@ void SettingsCategory::SetDefaults() } } +bool SettingsCategory::Commit() +{ + bool committed = false; + + // Commit subcategory arrays + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + committed |= subcategory->Commit(); + } + } + + // Commit subcategories + for (auto& subcategory : p->subcategories_) + { + committed |= subcategory->Commit(); + } + + // Commit variables + for (auto& variable : p->variables_) + { + committed |= variable->Commit(); + } + + return committed; +} + bool SettingsCategory::ReadJson(const boost::json::object& json) { bool validated = true; diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index bea48659..9ccf7458 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -33,6 +33,15 @@ public: */ void SetDefaults(); + /** + * Sets the current value of all variables to the staged + * value. + * + * @return true if any staged value was committed, false if no staged values + * are present. + */ + bool Commit(); + /** * Reads the variables from the JSON object. * diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 0a63add4..2de6f283 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -248,6 +248,8 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( layout->addWidget(lineLabel, row, 1); layout->addWidget(toolButton, row, 2); + self_->AddSettingsCategory(&lineSettings); + connect( toolButton, &QAbstractButton::clicked, diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp index d174fbbd..a7c77380 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp @@ -19,6 +19,7 @@ public: explicit Impl() {} ~Impl() = default; + std::vector categories_; std::vector settings_; }; @@ -29,6 +30,12 @@ SettingsPageWidget::SettingsPageWidget(QWidget* parent) : SettingsPageWidget::~SettingsPageWidget() = default; +void SettingsPageWidget::AddSettingsCategory( + settings::SettingsCategory* category) +{ + p->categories_.push_back(category); +} + void SettingsPageWidget::AddSettingsInterface( settings::SettingsInterfaceBase* setting) { @@ -39,6 +46,11 @@ bool SettingsPageWidget::CommitChanges() { bool committed = false; + for (auto& category : p->categories_) + { + committed |= category->Commit(); + } + for (auto& setting : p->settings_) { committed |= setting->Commit(); diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp index 228badd6..2fbdfc9e 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -59,6 +60,7 @@ public: void ResetToDefault(); protected: + void AddSettingsCategory(settings::SettingsCategory* category); void AddSettingsInterface(settings::SettingsInterfaceBase* setting); private: From 928b3397d2b6630644260069670675162cbae1bf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 23:39:32 -0500 Subject: [PATCH 078/762] Add alert palette settings widget to settings dialog --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 11 +++++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 17 +++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index cd72b4ee..dde77454 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -224,6 +225,7 @@ public: manager::PositionManager::Instance()}; std::vector settingsPages_ {}; + AlertPaletteSettingsWidget* alertPaletteSettingsWidget_ {}; HotkeySettingsWidget* hotkeySettingsWidget_ {}; UnitSettingsWidget* unitSettingsWidget_ {}; @@ -807,6 +809,15 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() settings::PaletteSettings::Instance(); // Palettes > Alerts + QVBoxLayout* layout = new QVBoxLayout(self_->ui->alertsPalette); + + alertPaletteSettingsWidget_ = + new AlertPaletteSettingsWidget(self_->ui->hotkeys); + layout->addWidget(alertPaletteSettingsWidget_); + + settingsPages_.push_back(alertPaletteSettingsWidget_); + + // Palettes > Old Alerts QGridLayout* alertsLayout = reinterpret_cast(self_->ui->alertsFrame->layout()); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 88dacbbc..0237462a 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 @@ - 3 + 0 @@ -136,8 +136,8 @@ 0 0 - 274 - 691 + 513 + 622 @@ -610,8 +610,8 @@ 0 0 - 98 - 28 + 506 + 383 @@ -634,10 +634,15 @@ - + Alerts + + + + Old Alerts + From 6063de2095252ca4000c02142afab2042baf1802 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 21 Sep 2024 23:39:51 -0500 Subject: [PATCH 079/762] Settings dialog formatting --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 71 ++++++++----------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index dde77454..f7e22c09 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -23,12 +23,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -360,24 +360,23 @@ void SettingsDialogImpl::ConnectSignals() self_, [this]() { alertAudioRadarSiteDialog_->show(); }); - QObject::connect(alertAudioRadarSiteDialog_, - &RadarSiteDialog::accepted, - self_, - [this]() - { - std::string id = - alertAudioRadarSiteDialog_->radar_site(); + QObject::connect( + alertAudioRadarSiteDialog_, + &RadarSiteDialog::accepted, + self_, + [this]() + { + std::string id = alertAudioRadarSiteDialog_->radar_site(); - std::shared_ptr radarSite = - config::RadarSite::Get(id); + std::shared_ptr radarSite = + config::RadarSite::Get(id); - if (radarSite != nullptr) - { - self_->ui->alertAudioRadarSiteComboBox - ->setCurrentText(QString::fromStdString( - RadarSiteLabel(radarSite))); - } - }); + if (radarSite != nullptr) + { + self_->ui->alertAudioRadarSiteComboBox->setCurrentText( + QString::fromStdString(RadarSiteLabel(radarSite))); + } + }); QObject::connect(self_->ui->gpsSourceSelectButton, &QAbstractButton::clicked, @@ -923,13 +922,11 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() QObject::connect(activeButton, &QAbstractButton::clicked, self_, - [=, this]() - { ShowColorDialog(activeEdit); }); + [=, this]() { ShowColorDialog(activeEdit); }); QObject::connect(inactiveButton, &QAbstractButton::clicked, self_, - [=, this]() - { ShowColorDialog(inactiveEdit); }); + [=, this]() { ShowColorDialog(inactiveEdit); }); } } @@ -964,8 +961,7 @@ void SettingsDialogImpl::SetupAudioTab() locationMethod == types::LocationMethod::RadarSite; bool countyEntryEnabled = locationMethod == types::LocationMethod::County; - bool wfoEntryEnabled = - locationMethod == types::LocationMethod::WFO; + bool wfoEntryEnabled = locationMethod == types::LocationMethod::WFO; self_->ui->alertAudioLatitudeSpinBox->setEnabled( coordinateEntryEnabled); @@ -983,10 +979,8 @@ void SettingsDialogImpl::SetupAudioTab() self_->ui->resetAlertAudioRadarSiteButton->setEnabled( radarSiteEntryEnable); - self_->ui->alertAudioRadiusSpinBox->setEnabled( - radiusEntryEnable); - self_->ui->resetAlertAudioRadiusButton->setEnabled( - radiusEntryEnable); + self_->ui->alertAudioRadiusSpinBox->setEnabled(radiusEntryEnable); + self_->ui->resetAlertAudioRadiusButton->setEnabled(radiusEntryEnable); self_->ui->alertAudioCountyLineEdit->setEnabled(countyEntryEnabled); self_->ui->alertAudioCountySelectButton->setEnabled( @@ -1102,8 +1096,7 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioRadius_.SetSettingsVariable(audioSettings.alert_radius()); alertAudioRadius_.SetEditWidget(self_->ui->alertAudioRadiusSpinBox); - alertAudioRadius_.SetResetButton( - self_->ui->resetAlertAudioRadiusButton); + alertAudioRadius_.SetResetButton(self_->ui->resetAlertAudioRadiusButton); alertAudioRadius_.SetUnitLabel(self_->ui->alertAudioRadiusUnitsLabel); auto alertAudioRadiusUpdateUnits = [this](const std::string& newValue) { @@ -1217,14 +1210,10 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); - QObject::connect( - self_->ui->alertAudioWFOSelectButton, - &QAbstractButton::clicked, - self_, - [this]() - { - wfoDialog_->show(); - }); + QObject::connect(self_->ui->alertAudioWFOSelectButton, + &QAbstractButton::clicked, + self_, + [this]() { wfoDialog_->show(); }); QObject::connect(wfoDialog_, &WFODialog::accepted, self_, @@ -1243,9 +1232,8 @@ void SettingsDialogImpl::SetupAudioTab() self_, [this](const QString& text) { - std::string wfoName = - config::CountyDatabase::GetWFOName( - text.toStdString()); + std::string wfoName = config::CountyDatabase::GetWFOName( + text.toStdString()); self_->ui->alertAudioWFOLabel->setText( QString::fromStdString(wfoName)); }); @@ -1253,7 +1241,6 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioWFO_.SetSettingsVariable(audioSettings.alert_wfo()); alertAudioWFO_.SetEditWidget(self_->ui->alertAudioWFOLineEdit); alertAudioWFO_.SetResetButton(self_->ui->resetAlertAudioWFOButton); - } void SettingsDialogImpl::SetupTextTab() @@ -1455,8 +1442,6 @@ void SettingsDialogImpl::UpdateAlertRadarDialogLocation(const std::string& id) } } - - QFont SettingsDialogImpl::GetSelectedFont() { std::string fontFamily = fontFamilies_.at(selectedFontCategory_) From 76809de2df678ca713d03d6574d6578f4e6de768 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 22 Sep 2024 09:54:32 -0500 Subject: [PATCH 080/762] Implement discard functionality for line settings --- .../scwx/qt/settings/settings_category.cpp | 36 +++++++++++++++++++ .../scwx/qt/settings/settings_category.hpp | 17 +++++++-- .../scwx/qt/settings/settings_variable.hpp | 4 +-- .../qt/settings/settings_variable_base.hpp | 7 +++- .../alert_palette_settings_widget.cpp | 17 ++++++++- .../qt/ui/settings/settings_page_widget.cpp | 5 +++ .../qt/ui/settings/settings_page_widget.hpp | 10 +++++- 7 files changed, 89 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 34859ae2..29b3a022 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -4,6 +4,8 @@ #include +#include + namespace scwx { namespace qt @@ -27,6 +29,8 @@ public: subcategoryArrays_; std::vector subcategories_; std::vector variables_; + + boost::signals2::signal resetSignal_; }; SettingsCategory::SettingsCategory(const std::string& name) : @@ -96,6 +100,32 @@ bool SettingsCategory::Commit() return committed; } +void SettingsCategory::Reset() +{ + // Reset subcategory arrays + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + subcategory->Reset(); + } + } + + // Reset subcategories + for (auto& subcategory : p->subcategories_) + { + subcategory->Reset(); + } + + // Reset variables + for (auto& variable : p->variables_) + { + variable->Reset(); + } + + p->resetSignal_(); +} + bool SettingsCategory::ReadJson(const boost::json::object& json) { bool validated = true; @@ -252,6 +282,12 @@ void SettingsCategory::RegisterVariables( p->variables_.end(), variables.cbegin(), variables.cend()); } +boost::signals2::connection +SettingsCategory::RegisterResetCallback(std::function callback) +{ + return p->resetSignal_.connect(callback); +} + } // namespace settings } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index 9ccf7458..9b5613be 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -6,6 +6,7 @@ #include #include +#include namespace scwx { @@ -34,14 +35,18 @@ public: void SetDefaults(); /** - * Sets the current value of all variables to the staged - * value. + * Sets the current value of all variables to the staged value. * * @return true if any staged value was committed, false if no staged values * are present. */ bool Commit(); + /** + * Clears the staged value of all variables. + */ + void Reset(); + /** * Reads the variables from the JSON object. * @@ -68,6 +73,14 @@ public: RegisterVariables(std::initializer_list variables); void RegisterVariables(std::vector variables); + /** + * Registers a function to be called when the category is reset. + * + * @param callback Function to be called + */ + boost::signals2::connection + RegisterResetCallback(std::function callback); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index 72d61dde..d2e6c949 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -29,7 +29,7 @@ public: typedef std::function ValueCallbackFunction; explicit SettingsVariable(const std::string& name); - ~SettingsVariable(); + virtual ~SettingsVariable(); SettingsVariable(const SettingsVariable&) = delete; SettingsVariable& operator=(const SettingsVariable&) = delete; @@ -96,7 +96,7 @@ public: /** * Clears the staged value of the settings variable. */ - void Reset(); + void Reset() override; /** * Gets the staged value of the settings variable, if defined. diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp index f0444f45..d7211197 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp @@ -19,7 +19,7 @@ class SettingsVariableBase { protected: explicit SettingsVariableBase(const std::string& name); - ~SettingsVariableBase(); + virtual ~SettingsVariableBase(); public: SettingsVariableBase(const SettingsVariableBase&) = delete; @@ -48,6 +48,11 @@ public: */ virtual bool Commit() = 0; + /** + * Clears the staged value of the settings variable. + */ + virtual void Reset() = 0; + /** * Reads the value from the JSON object. If the read value is out of range, * the value is set to the minimum or maximum. If the read value fails diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 2de6f283..8e77bdb6 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -13,6 +13,8 @@ #include #include +#include + namespace scwx { namespace qt @@ -35,7 +37,13 @@ public: SetupUi(); ConnectSignals(); } - ~Impl() = default; + ~Impl() + { + for (auto& c : bs2Connections_) + { + c.disconnect(); + } + }; void AddPhenomenonLine(const std::string& name, settings::LineSettings& lineSettings, @@ -54,6 +62,8 @@ public: EditLineDialog* editLineDialog_; LineLabel* activeLineLabel_ {nullptr}; + std::vector bs2Connections_ {}; + boost::unordered_flat_map phenomenonPages_ {}; }; @@ -250,6 +260,11 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( self_->AddSettingsCategory(&lineSettings); + boost::signals2::connection c = lineSettings.RegisterResetCallback( + [lineLabel, &lineSettings]() + { lineLabel->set_line_settings(lineSettings); }); + bs2Connections_.push_back(c); + connect( toolButton, &QAbstractButton::clicked, diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp index a7c77380..41c43817 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.cpp @@ -61,6 +61,11 @@ bool SettingsPageWidget::CommitChanges() void SettingsPageWidget::DiscardChanges() { + for (auto& category : p->categories_) + { + category->Reset(); + } + for (auto& setting : p->settings_) { setting->Reset(); diff --git a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp index 2fbdfc9e..39d8647a 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/settings/settings_page_widget.hpp @@ -60,9 +60,17 @@ public: void ResetToDefault(); protected: - void AddSettingsCategory(settings::SettingsCategory* category); void AddSettingsInterface(settings::SettingsInterfaceBase* setting); + /** + * Commits and resets all settings within a category upon page commit or + * reset. The use of SettingsInterface is preferred, as it allows the binding + * of widgets to these actions. + * + * @param [in] category Settings category + */ + void AddSettingsCategory(settings::SettingsCategory* category); + private: class Impl; std::shared_ptr p; From 20dbc7f5b759410cc36d4fe9c2a2011e0432d61f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 22 Sep 2024 22:53:45 -0500 Subject: [PATCH 081/762] Default alert palettes --- .../qt/settings/alert_palette_settings.cpp | 135 +++++++++++++++++- 1 file changed, 132 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp index ed82ef7e..96bed7f9 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp @@ -5,6 +5,7 @@ #include #include +#include namespace scwx { @@ -16,22 +17,133 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::alert_palette_settings"; +static const boost::gil::rgba8_pixel_t kColorBlack_ {0, 0, 0, 255}; + +struct LineData +{ + boost::gil::rgba8_pixel_t borderColor_ {kColorBlack_}; + boost::gil::rgba8_pixel_t highlightColor_ {kColorBlack_}; + boost::gil::rgba8_pixel_t lineColor_; + std::int64_t borderWidth_ {1}; + std::int64_t highlightWidth_ {0}; + std::int64_t lineWidth_ {3}; +}; + +typedef boost::unordered_flat_map + ThreatCategoryPalette; + +static const boost::unordered_flat_map + kThreatCategoryPalettes_ // + {{awips::Phenomenon::Marine, + {{awips::ibw::ThreatCategory::Base, {.lineColor_ {255, 127, 0, 255}}}}}, + {awips::Phenomenon::FlashFlood, + {{awips::ibw::ThreatCategory::Base, {.lineColor_ {0, 255, 0, 255}}}, + {awips::ibw::ThreatCategory::Considerable, + {.highlightColor_ {0, 255, 0, 255}, + .lineColor_ {kColorBlack_}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}, + {awips::ibw::ThreatCategory::Catastrophic, + {.highlightColor_ {0, 255, 0, 255}, + .lineColor_ {255, 0, 0, 255}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}}}, + {awips::Phenomenon::SevereThunderstorm, + {{awips::ibw::ThreatCategory::Base, {.lineColor_ {255, 255, 0, 255}}}, + {awips::ibw::ThreatCategory::Considerable, + {.highlightColor_ {255, 255, 0, 255}, + .lineColor_ {255, 0, 0, 255}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}, + {awips::ibw::ThreatCategory::Destructive, + {.highlightColor_ {255, 255, 0, 255}, + .lineColor_ {255, 0, 0, 255}, + .highlightWidth_ {1}, + .lineWidth_ {2}}}}}, + {awips::Phenomenon::SnowSquall, + {{awips::ibw::ThreatCategory::Base, {.lineColor_ {0, 255, 255, 255}}}}}, + {awips::Phenomenon::Tornado, + {{awips::ibw::ThreatCategory::Base, {.lineColor_ {255, 0, 0, 255}}}, + {awips::ibw::ThreatCategory::Considerable, + {.lineColor_ {255, 0, 255, 255}}}, + {awips::ibw::ThreatCategory::Catastrophic, + {.highlightColor_ {255, 0, 255, 255}, + .lineColor_ {kColorBlack_}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}}}}; + +static const boost::unordered_flat_map + kObservedPalettes_ // + {{awips::Phenomenon::Tornado, + {.highlightColor_ {255, 0, 0, 255}, + .lineColor_ {kColorBlack_}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}}; + +static const boost::unordered_flat_map + kTornadoPossiblePalettes_ // + {{awips::Phenomenon::Marine, + {.highlightColor_ {255, 127, 0, 255}, + .lineColor_ {kColorBlack_}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}, + {awips::Phenomenon::SevereThunderstorm, + {.highlightColor_ {255, 255, 0, 255}, + .lineColor_ {kColorBlack_}, + .highlightWidth_ {1}, + .lineWidth_ {1}}}}; + +static const boost::unordered_flat_map + kInactivePalettes_ // + { + {awips::Phenomenon::Marine, {.lineColor_ {127, 63, 0, 255}}}, + {awips::Phenomenon::FlashFlood, {.lineColor_ {0, 127, 0, 255}}}, + {awips::Phenomenon::SevereThunderstorm, {.lineColor_ {127, 127, 0, 255}}}, + {awips::Phenomenon::SnowSquall, {.lineColor_ {0, 127, 127, 255}}}, + {awips::Phenomenon::Tornado, {.lineColor_ {127, 0, 0, 255}}}, + }; + class AlertPaletteSettings::Impl { public: explicit Impl(awips::Phenomenon phenomenon) : phenomenon_ {phenomenon} { - auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon); + const auto& info = awips::ibw::GetImpactBasedWarningInfo(phenomenon); + + const auto& threatCategoryPalettes = + kThreatCategoryPalettes_.at(phenomenon); + for (auto& threatCategory : info.threatCategories_) { std::string threatCategoryName = awips::ibw::GetThreatCategoryName(threatCategory); boost::algorithm::to_lower(threatCategoryName); - threatCategoryMap_.emplace(threatCategory, threatCategoryName); + auto result = + threatCategoryMap_.emplace(threatCategory, threatCategoryName); + auto& lineSettings = result.first->second; + + SetDefaultLineData(lineSettings, + threatCategoryPalettes.at(threatCategory)); } + + if (info.hasObservedTag_) + { + SetDefaultLineData(observed_, kObservedPalettes_.at(phenomenon)); + } + + if (info.hasTornadoPossibleTag_) + { + SetDefaultLineData(tornadoPossible_, + kTornadoPossiblePalettes_.at(phenomenon)); + } + + SetDefaultLineData(inactive_, kInactivePalettes_.at(phenomenon)); } ~Impl() {} + static void SetDefaultLineData(LineSettings& lineSettings, + const LineData& lineData); + awips::Phenomenon phenomenon_; std::map threatCategoryMap_ {}; @@ -65,6 +177,7 @@ AlertPaletteSettings::AlertPaletteSettings(awips::Phenomenon phenomenon) : SetDefaults(); } + AlertPaletteSettings::~AlertPaletteSettings() = default; AlertPaletteSettings::AlertPaletteSettings(AlertPaletteSettings&&) noexcept = @@ -98,10 +211,26 @@ LineSettings& AlertPaletteSettings::tornado_possible() const return p->tornadoPossible_; } +void AlertPaletteSettings::Impl::SetDefaultLineData(LineSettings& lineSettings, + const LineData& lineData) +{ + lineSettings.border_color().SetDefault( + util::color::ToArgbString(lineData.borderColor_)); + lineSettings.highlight_color().SetDefault( + util::color::ToArgbString(lineData.highlightColor_)); + lineSettings.line_color().SetDefault( + util::color::ToArgbString(lineData.lineColor_)); + + lineSettings.border_width().SetDefault(lineData.borderWidth_); + lineSettings.highlight_width().SetDefault(lineData.highlightWidth_); + lineSettings.line_width().SetDefault(lineData.lineWidth_); +} + bool operator==(const AlertPaletteSettings& lhs, const AlertPaletteSettings& rhs) { - return (lhs.p->threatCategoryMap_ == rhs.p->threatCategoryMap_ && + return (lhs.p->phenomenon_ == rhs.p->phenomenon_ && + lhs.p->threatCategoryMap_ == rhs.p->threatCategoryMap_ && lhs.p->inactive_ == rhs.p->inactive_ && lhs.p->observed_ == rhs.p->observed_ && lhs.p->tornadoPossible_ == rhs.p->tornadoPossible_); From 70cb3ab6d21f0cbea1be0311577f7e1247872e09 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 26 Sep 2024 04:41:56 -0500 Subject: [PATCH 082/762] Update settings category signals to be in line with variables and only fire once Properly connect line labels to the category signals --- .../source/scwx/qt/settings/line_settings.cpp | 23 +++- .../source/scwx/qt/settings/line_settings.hpp | 9 ++ .../scwx/qt/settings/settings_category.cpp | 120 ++++++++++++++++-- .../scwx/qt/settings/settings_category.hpp | 25 ++-- .../scwx/qt/settings/settings_variable.cpp | 18 +++ .../qt/settings/settings_variable_base.cpp | 13 ++ .../qt/settings/settings_variable_base.hpp | 15 +++ scwx-qt/source/scwx/qt/ui/line_label.cpp | 80 ++++-------- .../alert_palette_settings_widget.cpp | 66 ++++------ 9 files changed, 252 insertions(+), 117 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp index bc467186..105be812 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -1,8 +1,6 @@ #include #include -#include - namespace scwx { namespace qt @@ -106,6 +104,27 @@ SettingsVariable& LineSettings::line_width() const return p->lineWidth_; } +void LineSettings::StageValues(boost::gil::rgba8_pixel_t borderColor, + boost::gil::rgba8_pixel_t highlightColor, + boost::gil::rgba8_pixel_t lineColor, + std::int64_t borderWidth, + std::int64_t highlightWidth, + std::int64_t lineWidth) +{ + set_block_signals(true); + + p->borderColor_.StageValue(util::color::ToArgbString(borderColor)); + p->highlightColor_.StageValue(util::color::ToArgbString(highlightColor)); + p->lineColor_.StageValue(util::color::ToArgbString(lineColor)); + p->borderWidth_.StageValue(borderWidth); + p->highlightWidth_.StageValue(highlightWidth); + p->lineWidth_.StageValue(lineWidth); + + set_block_signals(false); + + staged_signal()(); +} + bool operator==(const LineSettings& lhs, const LineSettings& rhs) { return (lhs.p->borderColor_ == rhs.p->borderColor_ && diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp index e99d9b3e..bc9a9cb8 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace scwx { namespace qt @@ -33,6 +35,13 @@ public: SettingsVariable& highlight_width() const; SettingsVariable& line_width() const; + void StageValues(boost::gil::rgba8_pixel_t borderColor, + boost::gil::rgba8_pixel_t highlightColor, + boost::gil::rgba8_pixel_t lineColor, + std::int64_t borderWidth, + std::int64_t highlightWidth, + std::int64_t lineWidth); + friend bool operator==(const LineSettings& lhs, const LineSettings& rhs); private: diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 29b3a022..3714c4ca 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -4,8 +4,6 @@ #include -#include - namespace scwx { namespace qt @@ -23,6 +21,9 @@ public: ~Impl() {} + void ConnectSubcategory(SettingsCategory& category); + void ConnectVariable(SettingsVariableBase* variable); + const std::string name_; std::vector>> @@ -30,7 +31,11 @@ public: std::vector subcategories_; std::vector variables_; - boost::signals2::signal resetSignal_; + boost::signals2::signal changedSignal_ {}; + boost::signals2::signal stagedSignal_ {}; + bool blockSignals_ {false}; + + std::vector connections_ {}; }; SettingsCategory::SettingsCategory(const std::string& name) : @@ -48,8 +53,27 @@ std::string SettingsCategory::name() const return p->name_; } +boost::signals2::signal& SettingsCategory::changed_signal() +{ + return p->changedSignal_; +} + +boost::signals2::signal& SettingsCategory::staged_signal() +{ + return p->stagedSignal_; +} + +void SettingsCategory::set_block_signals(bool blockSignals) +{ + p->blockSignals_ = blockSignals; +} + void SettingsCategory::SetDefaults() { + // Don't allow individual variables to invoke the signal when operating over + // the entire category + p->blockSignals_ = true; + // Set subcategory array defaults for (auto& subcategoryArray : p->subcategoryArrays_) { @@ -70,12 +94,22 @@ void SettingsCategory::SetDefaults() { variable->SetValueToDefault(); } + + // Unblock signals + p->blockSignals_ = false; + + p->changedSignal_(); + p->stagedSignal_(); } bool SettingsCategory::Commit() { bool committed = false; + // Don't allow individual variables to invoke the signal when operating over + // the entire category + p->blockSignals_ = true; + // Commit subcategory arrays for (auto& subcategoryArray : p->subcategoryArrays_) { @@ -97,11 +131,23 @@ bool SettingsCategory::Commit() committed |= variable->Commit(); } + // Unblock signals + p->blockSignals_ = false; + + if (committed) + { + p->changedSignal_(); + } + return committed; } void SettingsCategory::Reset() { + // Don't allow individual variables to invoke the signal when operating over + // the entire category + p->blockSignals_ = true; + // Reset subcategory arrays for (auto& subcategoryArray : p->subcategoryArrays_) { @@ -123,7 +169,10 @@ void SettingsCategory::Reset() variable->Reset(); } - p->resetSignal_(); + // Unblock signals + p->blockSignals_ = false; + + p->stagedSignal_(); } bool SettingsCategory::ReadJson(const boost::json::object& json) @@ -242,6 +291,7 @@ void SettingsCategory::WriteJson(boost::json::object& json) const void SettingsCategory::RegisterSubcategory(SettingsCategory& subcategory) { + p->ConnectSubcategory(subcategory); p->subcategories_.push_back(&subcategory); } @@ -254,7 +304,11 @@ void SettingsCategory::RegisterSubcategoryArray( std::transform(subcategories.begin(), subcategories.end(), std::back_inserter(newSubcategories.second), - [](SettingsCategory& subcategory) { return &subcategory; }); + [this](SettingsCategory& subcategory) + { + p->ConnectSubcategory(subcategory); + return &subcategory; + }); } void SettingsCategory::RegisterSubcategoryArray( @@ -266,26 +320,74 @@ void SettingsCategory::RegisterSubcategoryArray( std::transform(subcategories.begin(), subcategories.end(), std::back_inserter(newSubcategories.second), - [](SettingsCategory* subcategory) { return subcategory; }); + [this](SettingsCategory* subcategory) + { + p->ConnectSubcategory(*subcategory); + return subcategory; + }); } void SettingsCategory::RegisterVariables( std::initializer_list variables) { + for (auto& variable : variables) + { + p->ConnectVariable(variable); + } p->variables_.insert(p->variables_.end(), variables); } void SettingsCategory::RegisterVariables( std::vector variables) { + for (auto& variable : variables) + { + p->ConnectVariable(variable); + } p->variables_.insert( p->variables_.end(), variables.cbegin(), variables.cend()); } -boost::signals2::connection -SettingsCategory::RegisterResetCallback(std::function callback) +void SettingsCategory::Impl::ConnectSubcategory(SettingsCategory& category) { - return p->resetSignal_.connect(callback); + connections_.emplace_back(category.changed_signal().connect( + [this]() + { + if (!blockSignals_) + { + changedSignal_(); + } + })); + + connections_.emplace_back(category.staged_signal().connect( + [this]() + { + if (!blockSignals_) + { + stagedSignal_(); + } + })); +} + +void SettingsCategory::Impl::ConnectVariable(SettingsVariableBase* variable) +{ + connections_.emplace_back(variable->changed_signal().connect( + [this]() + { + if (!blockSignals_) + { + changedSignal_(); + } + })); + + connections_.emplace_back(variable->staged_signal().connect( + [this]() + { + if (!blockSignals_) + { + stagedSignal_(); + } + })); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index 9b5613be..ee80ba46 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -6,7 +6,7 @@ #include #include -#include +#include namespace scwx { @@ -29,6 +29,20 @@ public: std::string name() const; + /** + * Gets the signal invoked when a variable within the category is changed. + * + * @return Changed signal + */ + boost::signals2::signal& changed_signal(); + + /** + * Gets the signal invoked when a variable within the category is staged. + * + * @return Staged signal + */ + boost::signals2::signal& staged_signal(); + /** * Set all variables to their defaults. */ @@ -73,13 +87,8 @@ public: RegisterVariables(std::initializer_list variables); void RegisterVariables(std::vector variables); - /** - * Registers a function to be called when the category is reset. - * - * @param callback Function to be called - */ - boost::signals2::connection - RegisterResetCallback(std::function callback); +protected: + void set_block_signals(bool blockSignals); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 71db421e..c4a5f6c9 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -81,10 +81,13 @@ bool SettingsVariable::SetValue(const T& value) p->value_ = (p->transform_ != nullptr) ? p->transform_(value) : value; validated = true; + changed_signal()(); for (auto& callback : p->valueChangedCallbackFunctions_) { callback.second(p->value_); } + + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); @@ -129,10 +132,13 @@ bool SettingsVariable::SetValueOrDefault(const T& value) p->value_ = p->default_; } + changed_signal()(); for (auto& callback : p->valueChangedCallbackFunctions_) { callback.second(p->value_); } + + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); @@ -146,10 +152,13 @@ void SettingsVariable::SetValueToDefault() { p->value_ = p->default_; + changed_signal()(); for (auto& callback : p->valueChangedCallbackFunctions_) { callback.second(p->value_); } + + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); @@ -168,6 +177,7 @@ void SettingsVariable::StageDefault() p->staged_.reset(); } + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->default_); @@ -194,6 +204,7 @@ bool SettingsVariable::StageValue(const T& value) validated = true; + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(transformed); @@ -214,10 +225,13 @@ bool SettingsVariable::Commit() p->staged_.reset(); committed = true; + changed_signal()(); for (auto& callback : p->valueChangedCallbackFunctions_) { callback.second(p->value_); } + + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); @@ -232,6 +246,7 @@ void SettingsVariable::Reset() { p->staged_.reset(); + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); @@ -336,10 +351,13 @@ bool SettingsVariable::ReadValue(const boost::json::object& json) p->value_ = p->default_; } + changed_signal()(); for (auto& callback : p->valueChangedCallbackFunctions_) { callback.second(p->value_); } + + staged_signal()(); for (auto& callback : p->valueStagedCallbackFunctions_) { callback.second(p->value_); diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp index 55ce72ea..7e31fb5f 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp @@ -18,6 +18,9 @@ public: ~Impl() {} const std::string name_; + + boost::signals2::signal changedSignal_ {}; + boost::signals2::signal stagedSignal_ {}; }; SettingsVariableBase::SettingsVariableBase(const std::string& name) : @@ -38,6 +41,16 @@ std::string SettingsVariableBase::name() const return p->name_; } +boost::signals2::signal& SettingsVariableBase::changed_signal() +{ + return p->changedSignal_; +} + +boost::signals2::signal& SettingsVariableBase::staged_signal() +{ + return p->stagedSignal_; +} + bool SettingsVariableBase::Equals(const SettingsVariableBase& o) const { return p->name_ == o.p->name_; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp index d7211197..fba1eff9 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace scwx { @@ -30,6 +31,20 @@ public: std::string name() const; + /** + * Gets the signal invoked when the settings variable is changed. + * + * @return Changed signal + */ + boost::signals2::signal& changed_signal(); + + /** + * Gets the signal invoked when the settings variable is staged. + * + * @return Staged signal + */ + boost::signals2::signal& staged_signal(); + /** * Sets the current value of the settings variable to default. */ diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp index a8be0507..0b2f8f75 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.cpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -18,12 +18,13 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class LineLabel::Impl { public: - explicit Impl() {}; + explicit Impl(LineLabel* self) : self_ {self} {}; ~Impl() = default; - void ResetLineSettings(); - QImage GenerateImage() const; + void UpdateLineLabel(const settings::LineSettings& lineSettings); + + LineLabel* self_; std::size_t borderWidth_ {1}; std::size_t highlightWidth_ {1}; @@ -33,26 +34,18 @@ public: boost::gil::rgba8_pixel_t highlightColor_ {255, 255, 0, 255}; boost::gil::rgba8_pixel_t lineColor_ {0, 0, 255, 255}; - settings::LineSettings* lineSettings_ {nullptr}; - QPixmap pixmap_ {}; bool pixmapDirty_ {true}; + + boost::signals2::scoped_connection settingsStaged_ {}; }; LineLabel::LineLabel(QWidget* parent) : - QFrame(parent), p {std::make_unique()} + QFrame(parent), p {std::make_unique(this)} { } -LineLabel::~LineLabel() -{ - p->ResetLineSettings(); -} - -void LineLabel::Impl::ResetLineSettings() -{ - lineSettings_ = nullptr; -} +LineLabel::~LineLabel() {} boost::gil::rgba8_pixel_t LineLabel::border_color() const { @@ -90,11 +83,6 @@ void LineLabel::set_border_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->border_width().StageValue(width); - } } void LineLabel::set_highlight_width(std::size_t width) @@ -103,11 +91,6 @@ void LineLabel::set_highlight_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->highlight_width().StageValue(width); - } } void LineLabel::set_line_width(std::size_t width) @@ -116,11 +99,6 @@ void LineLabel::set_line_width(std::size_t width) p->pixmapDirty_ = true; updateGeometry(); update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->line_width().StageValue(width); - } } void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) @@ -128,12 +106,6 @@ void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) p->borderColor_ = color; p->pixmapDirty_ = true; update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->border_color().StageValue( - util::color::ToArgbString(color)); - } } void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) @@ -141,12 +113,6 @@ void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) p->highlightColor_ = color; p->pixmapDirty_ = true; update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->highlight_color().StageValue( - util::color::ToArgbString(color)); - } } void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) @@ -154,30 +120,30 @@ void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) p->lineColor_ = color; p->pixmapDirty_ = true; update(); - - if (p->lineSettings_ != nullptr) - { - p->lineSettings_->line_color().StageValue( - util::color::ToArgbString(color)); - } } void LineLabel::set_line_settings(settings::LineSettings& lineSettings) { - p->ResetLineSettings(); + p->settingsStaged_ = lineSettings.staged_signal().connect( + [this, &lineSettings]() { p->UpdateLineLabel(lineSettings); }); - set_border_color(util::color::ToRgba8PixelT( + p->UpdateLineLabel(lineSettings); +} + +void LineLabel::Impl::UpdateLineLabel( + const settings::LineSettings& lineSettings) +{ + self_->set_border_color(util::color::ToRgba8PixelT( lineSettings.border_color().GetStagedOrValue())); - set_highlight_color(util::color::ToRgba8PixelT( + self_->set_highlight_color(util::color::ToRgba8PixelT( lineSettings.highlight_color().GetStagedOrValue())); - set_line_color( + self_->set_line_color( util::color::ToRgba8PixelT(lineSettings.line_color().GetStagedOrValue())); - set_border_width(lineSettings.border_width().GetStagedOrValue()); - set_highlight_width(lineSettings.highlight_width().GetStagedOrValue()); - set_line_width(lineSettings.line_width().GetStagedOrValue()); - - p->lineSettings_ = &lineSettings; + self_->set_border_width(lineSettings.border_width().GetStagedOrValue()); + self_->set_highlight_width( + lineSettings.highlight_width().GetStagedOrValue()); + self_->set_line_width(lineSettings.line_width().GetStagedOrValue()); } QSize LineLabel::minimumSizeHint() const diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 8e77bdb6..9775b9c8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -37,13 +37,7 @@ public: SetupUi(); ConnectSignals(); } - ~Impl() - { - for (auto& c : bs2Connections_) - { - c.disconnect(); - } - }; + ~Impl() {}; void AddPhenomenonLine(const std::string& name, settings::LineSettings& lineSettings, @@ -59,10 +53,8 @@ public: QStackedWidget* phenomenonPagesWidget_; QListWidget* phenomenonListView_; - EditLineDialog* editLineDialog_; - LineLabel* activeLineLabel_ {nullptr}; - - std::vector bs2Connections_ {}; + EditLineDialog* editLineDialog_; + settings::LineSettings* activeLineSettings_ {nullptr}; boost::unordered_flat_map phenomenonPages_ {}; }; @@ -147,30 +139,27 @@ void AlertPaletteSettingsWidget::Impl::ConnectSignals() } }); - connect( - editLineDialog_, - &EditLineDialog::accepted, - self_, - [this]() - { - // If the active line label was set - if (activeLineLabel_ != nullptr) - { - // Update the active line label with selected line settings - activeLineLabel_->set_border_color(editLineDialog_->border_color()); - activeLineLabel_->set_highlight_color( - editLineDialog_->highlight_color()); - activeLineLabel_->set_line_color(editLineDialog_->line_color()); + connect(editLineDialog_, + &EditLineDialog::accepted, + self_, + [this]() + { + // If the active line label was set + if (activeLineSettings_ != nullptr) + { + // Update the active line settings with selected line settings + activeLineSettings_->StageValues( + editLineDialog_->border_color(), + editLineDialog_->highlight_color(), + editLineDialog_->line_color(), + editLineDialog_->border_width(), + editLineDialog_->highlight_width(), + editLineDialog_->line_width()); - activeLineLabel_->set_border_width(editLineDialog_->border_width()); - activeLineLabel_->set_highlight_width( - editLineDialog_->highlight_width()); - activeLineLabel_->set_line_width(editLineDialog_->line_width()); - - // Reset the active line label - activeLineLabel_ = nullptr; - } - }); + // Reset the active line settings + activeLineSettings_ = nullptr; + } + }); } void AlertPaletteSettingsWidget::Impl::SelectPhenomenon( @@ -260,19 +249,14 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( self_->AddSettingsCategory(&lineSettings); - boost::signals2::connection c = lineSettings.RegisterResetCallback( - [lineLabel, &lineSettings]() - { lineLabel->set_line_settings(lineSettings); }); - bs2Connections_.push_back(c); - connect( toolButton, &QAbstractButton::clicked, self_, - [this, lineLabel]() + [this, lineLabel, &lineSettings]() { // Set the active line label for when the dialog is finished - activeLineLabel_ = lineLabel; + activeLineSettings_ = &lineSettings; // Initialize dialog with current line settings editLineDialog_->set_border_color(lineLabel->border_color()); From dafb71e75c021aacdd3d955f99bbaf04c1ecdd3b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 27 Sep 2024 07:28:18 -0500 Subject: [PATCH 083/762] Use new alert palettes in AlertLayer --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 141 +++++++++++++++--- .../source/scwx/qt/settings/line_settings.cpp | 15 ++ .../source/scwx/qt/settings/line_settings.hpp | 4 + 3 files changed, 138 insertions(+), 22 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index cc8bc29b..c0842758 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -111,25 +111,27 @@ signals: class AlertLayer::Impl { public: + struct LineData + { + boost::gil::rgba32f_pixel_t borderColor_ {}; + boost::gil::rgba32f_pixel_t highlightColor_ {}; + boost::gil::rgba32f_pixel_t lineColor_ {}; + + std::size_t borderWidth_ {}; + std::size_t highlightWidth_ {}; + std::size_t lineWidth_ {}; + }; + explicit Impl(AlertLayer* self, std::shared_ptr context, awips::Phenomenon phenomenon) : self_ {self}, phenomenon_ {phenomenon}, + ibw_ {awips::ibw::GetImpactBasedWarningInfo(phenomenon)}, geoLines_ {{false, std::make_shared(context)}, {true, std::make_shared(context)}} { - auto& paletteSettings = settings::PaletteSettings::Instance(); - - for (auto alertActive : {false, true}) - { - lineColor_.emplace( - alertActive, - util::color::ToRgba32fPixelT( - paletteSettings.alert_color(phenomenon_, alertActive) - .GetValue())); - } - + UpdateLineData(); ConnectSignals(); ScheduleRefresh(); } @@ -158,6 +160,9 @@ public: const QPointF& mouseGlobalPos); void ScheduleRefresh(); + LineData& GetLineData(std::shared_ptr& segment); + void UpdateLineData(); + void AddLine(std::shared_ptr& geoLines, std::shared_ptr& di, const common::Coordinate& p1, @@ -177,6 +182,8 @@ public: boost::container::stable_vector< std::shared_ptr>& drawItems); + static LineData CreateLineData(const settings::LineSettings& lineSettings); + boost::asio::thread_pool threadPool_ {1u}; AlertLayer* self_; @@ -184,7 +191,8 @@ public: boost::asio::system_timer refreshTimer_ {threadPool_}; std::mutex refreshMutex_; - const awips::Phenomenon phenomenon_; + const awips::Phenomenon phenomenon_; + const awips::ibw::ImpactBasedWarningInfo& ibw_; std::unique_ptr receiver_ {std::make_unique()}; @@ -199,7 +207,10 @@ public: segmentsByLine_; std::mutex linesMutex_ {}; - std::unordered_map lineColor_; + std::unordered_map + threatCategoryLineData_; + LineData observedLineData_ {}; + LineData tornadoPossibleLineData_ {}; std::chrono::system_clock::time_point selectedTime_ {}; @@ -445,8 +456,8 @@ void AlertLayer::Impl::AddAlert( auto& startTime = segmentRecord->segmentBegin_; auto& endTime = segmentRecord->segmentEnd_; - auto& lineColor = lineColor_.at(alertActive); - auto& geoLines = geoLines_.at(alertActive); + auto& lineData = GetLineData(segment); + auto& geoLines = geoLines_.at(alertActive); const auto& coordinates = segment->codedLocation_->coordinates(); @@ -462,30 +473,51 @@ void AlertLayer::Impl::AddAlert( // If draw items were added if (drawItems.second) { + const float borderWidth = lineData.borderWidth_; + const float highlightWidth = lineData.highlightWidth_; + const float lineWidth = lineData.lineWidth_; + + const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); + const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); + + constexpr bool borderHover = true; + constexpr bool highlightHover = false; + constexpr bool lineHover = false; + // Add border AddLines(geoLines, coordinates, - kBlack_, - 5.0f, + lineData.borderColor_, + totalBorderWidth, startTime, endTime, - true, + borderHover, drawItems.first->second); - // Add only border to segmentsByLine_ + // Add border to segmentsByLine_ for (auto& di : drawItems.first->second) { segmentsByLine_.insert({di, segmentRecord}); } + // Add highlight + AddLines(geoLines, + coordinates, + lineData.highlightColor_, + totalHighlightWidth, + startTime, + endTime, + highlightHover, + drawItems.first->second); + // Add line AddLines(geoLines, coordinates, - lineColor, - 3.0f, + lineData.lineColor_, + lineWidth, startTime, endTime, - false, + lineHover, drawItems.first->second); } } @@ -638,6 +670,71 @@ void AlertLayer::Impl::HandleGeoLinesHover( } } +AlertLayer::Impl::LineData +AlertLayer::Impl::CreateLineData(const settings::LineSettings& lineSettings) +{ + return LineData {.borderColor_ {lineSettings.GetBorderColorRgba32f()}, + .highlightColor_ {lineSettings.GetHighlightColorRgba32f()}, + .lineColor_ {lineSettings.GetLineColorRgba32f()}, + .borderWidth_ {static_cast( + lineSettings.border_width().GetValue())}, + .highlightWidth_ {static_cast( + lineSettings.highlight_width().GetValue())}, + .lineWidth_ {static_cast( + lineSettings.line_width().GetValue())}}; +} + +void AlertLayer::Impl::UpdateLineData() +{ + auto& alertPalette = + settings::PaletteSettings().Instance().alert_palette(phenomenon_); + + for (auto threatCategory : ibw_.threatCategories_) + { + auto& palette = alertPalette.threat_category(threatCategory); + threatCategoryLineData_.insert_or_assign(threatCategory, + CreateLineData(palette)); + } + + if (ibw_.hasObservedTag_) + { + observedLineData_ = CreateLineData(alertPalette.observed()); + } + + if (ibw_.hasTornadoPossibleTag_) + { + tornadoPossibleLineData_ = + CreateLineData(alertPalette.tornado_possible()); + } +} + +AlertLayer::Impl::LineData& +AlertLayer::Impl::GetLineData(std::shared_ptr& segment) +{ + for (auto& threatCategory : ibw_.threatCategories_) + { + if (segment->threatCategory_ == threatCategory) + { + if (threatCategory == awips::ibw::ThreatCategory::Base) + { + if (ibw_.hasObservedTag_ && segment->observed_) + { + return observedLineData_; + } + + if (ibw_.hasTornadoPossibleTag_ && segment->tornadoPossible_) + { + return tornadoPossibleLineData_; + } + } + + return threatCategoryLineData_.at(threatCategory); + } + } + + return threatCategoryLineData_.at(awips::ibw::ThreatCategory::Base); +}; + AlertLayerHandler& AlertLayerHandler::Instance() { static AlertLayerHandler alertLayerHandler_ {}; diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp index 105be812..45337fa2 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -104,6 +104,21 @@ SettingsVariable& LineSettings::line_width() const return p->lineWidth_; } +boost::gil::rgba32f_pixel_t LineSettings::GetBorderColorRgba32f() const +{ + return util::color::ToRgba32fPixelT(p->borderColor_.GetValue()); +} + +boost::gil::rgba32f_pixel_t LineSettings::GetHighlightColorRgba32f() const +{ + return util::color::ToRgba32fPixelT(p->highlightColor_.GetValue()); +} + +boost::gil::rgba32f_pixel_t LineSettings::GetLineColorRgba32f() const +{ + return util::color::ToRgba32fPixelT(p->lineColor_.GetValue()); +} + void LineSettings::StageValues(boost::gil::rgba8_pixel_t borderColor, boost::gil::rgba8_pixel_t highlightColor, boost::gil::rgba8_pixel_t lineColor, diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp index bc9a9cb8..6f6988a2 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -35,6 +35,10 @@ public: SettingsVariable& highlight_width() const; SettingsVariable& line_width() const; + boost::gil::rgba32f_pixel_t GetBorderColorRgba32f() const; + boost::gil::rgba32f_pixel_t GetHighlightColorRgba32f() const; + boost::gil::rgba32f_pixel_t GetLineColorRgba32f() const; + void StageValues(boost::gil::rgba8_pixel_t borderColor, boost::gil::rgba8_pixel_t highlightColor, boost::gil::rgba8_pixel_t lineColor, From 584f5943b6fcaf7b0885fb0beeb55eaf35319f41 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 27 Sep 2024 07:48:48 -0500 Subject: [PATCH 084/762] Re-add inactive alert palette display --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index c0842758..69ebddb9 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -160,7 +160,8 @@ public: const QPointF& mouseGlobalPos); void ScheduleRefresh(); - LineData& GetLineData(std::shared_ptr& segment); + LineData& GetLineData(std::shared_ptr& segment, + bool alertActive); void UpdateLineData(); void AddLine(std::shared_ptr& geoLines, @@ -211,6 +212,7 @@ public: threatCategoryLineData_; LineData observedLineData_ {}; LineData tornadoPossibleLineData_ {}; + LineData inactiveLineData_ {}; std::chrono::system_clock::time_point selectedTime_ {}; @@ -456,7 +458,7 @@ void AlertLayer::Impl::AddAlert( auto& startTime = segmentRecord->segmentBegin_; auto& endTime = segmentRecord->segmentEnd_; - auto& lineData = GetLineData(segment); + auto& lineData = GetLineData(segment, alertActive); auto& geoLines = geoLines_.at(alertActive); const auto& coordinates = segment->codedLocation_->coordinates(); @@ -706,11 +708,19 @@ void AlertLayer::Impl::UpdateLineData() tornadoPossibleLineData_ = CreateLineData(alertPalette.tornado_possible()); } + + inactiveLineData_ = CreateLineData(alertPalette.inactive()); } AlertLayer::Impl::LineData& -AlertLayer::Impl::GetLineData(std::shared_ptr& segment) +AlertLayer::Impl::GetLineData(std::shared_ptr& segment, + bool alertActive) { + if (!alertActive) + { + return inactiveLineData_; + } + for (auto& threatCategory : ibw_.threatCategories_) { if (segment->threatCategory_ == threatCategory) From 40d70b0a13eb8d3f2799ae14fca23f60a71c8264 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 27 Sep 2024 09:13:45 -0500 Subject: [PATCH 085/762] Don't use braces around scalar initializers --- .../qt/settings/alert_palette_settings.cpp | 32 +++++++++---------- .../scwx/awips/impact_based_warnings.cpp | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp index 96bed7f9..c684e1ed 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp @@ -41,25 +41,25 @@ static const boost::unordered_flat_map {awips::ibw::ThreatCategory::Considerable, {.highlightColor_ {0, 255, 0, 255}, .lineColor_ {kColorBlack_}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}, + .highlightWidth_ = 1, + .lineWidth_ = 1}}, {awips::ibw::ThreatCategory::Catastrophic, {.highlightColor_ {0, 255, 0, 255}, .lineColor_ {255, 0, 0, 255}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}}}, + .highlightWidth_ = 1, + .lineWidth_ = 1}}}}, {awips::Phenomenon::SevereThunderstorm, {{awips::ibw::ThreatCategory::Base, {.lineColor_ {255, 255, 0, 255}}}, {awips::ibw::ThreatCategory::Considerable, {.highlightColor_ {255, 255, 0, 255}, .lineColor_ {255, 0, 0, 255}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}, + .highlightWidth_ = 1, + .lineWidth_ = 1}}, {awips::ibw::ThreatCategory::Destructive, {.highlightColor_ {255, 255, 0, 255}, .lineColor_ {255, 0, 0, 255}, - .highlightWidth_ {1}, - .lineWidth_ {2}}}}}, + .highlightWidth_ = 1, + .lineWidth_ = 2}}}}, {awips::Phenomenon::SnowSquall, {{awips::ibw::ThreatCategory::Base, {.lineColor_ {0, 255, 255, 255}}}}}, {awips::Phenomenon::Tornado, @@ -69,29 +69,29 @@ static const boost::unordered_flat_map {awips::ibw::ThreatCategory::Catastrophic, {.highlightColor_ {255, 0, 255, 255}, .lineColor_ {kColorBlack_}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}}}}; + .highlightWidth_ = 1, + .lineWidth_ = 1}}}}}; static const boost::unordered_flat_map kObservedPalettes_ // {{awips::Phenomenon::Tornado, {.highlightColor_ {255, 0, 0, 255}, .lineColor_ {kColorBlack_}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}}; + .highlightWidth_ = 1, + .lineWidth_ = 1}}}; static const boost::unordered_flat_map kTornadoPossiblePalettes_ // {{awips::Phenomenon::Marine, {.highlightColor_ {255, 127, 0, 255}, .lineColor_ {kColorBlack_}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}, + .highlightWidth_ = 1, + .lineWidth_ = 1}}, {awips::Phenomenon::SevereThunderstorm, {.highlightColor_ {255, 255, 0, 255}, .lineColor_ {kColorBlack_}, - .highlightWidth_ {1}, - .lineWidth_ {1}}}}; + .highlightWidth_ = 1, + .lineWidth_ = 1}}}; static const boost::unordered_flat_map kInactivePalettes_ // diff --git a/wxdata/source/scwx/awips/impact_based_warnings.cpp b/wxdata/source/scwx/awips/impact_based_warnings.cpp index a5e63a76..40a20051 100644 --- a/wxdata/source/scwx/awips/impact_based_warnings.cpp +++ b/wxdata/source/scwx/awips/impact_based_warnings.cpp @@ -18,7 +18,7 @@ static const std::string logPrefix_ = "scwx::awips::ibw::impact_based_warnings"; static const boost::unordered_flat_map impactBasedWarningInfo_ { {Phenomenon::Marine, - ImpactBasedWarningInfo {.hasTornadoPossibleTag_ {true}}}, + ImpactBasedWarningInfo {.hasTornadoPossibleTag_ = true}}, {Phenomenon::FlashFlood, ImpactBasedWarningInfo { .threatCategories_ {ThreatCategory::Base, @@ -26,14 +26,14 @@ static const boost::unordered_flat_map ThreatCategory::Catastrophic}}}, {Phenomenon::SevereThunderstorm, ImpactBasedWarningInfo { - .hasTornadoPossibleTag_ {true}, + .hasTornadoPossibleTag_ = true, .threatCategories_ {ThreatCategory::Base, ThreatCategory::Considerable, ThreatCategory::Destructive}}}, {Phenomenon::SnowSquall, ImpactBasedWarningInfo {}}, {Phenomenon::Tornado, ImpactBasedWarningInfo { - .hasObservedTag_ {true}, + .hasObservedTag_ = true, .threatCategories_ {ThreatCategory::Base, ThreatCategory::Considerable, ThreatCategory::Catastrophic}}}, From 94edeefee0ea1275dfb16fba45dbb72d7c381b9b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 27 Sep 2024 09:32:50 -0500 Subject: [PATCH 086/762] Update test data for alert settings --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 20a1ca17..8a9e6bc2 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 20a1ca1752499222d33869e37148321936ca6354 +Subproject commit 8a9e6bc2e457e9ef69cbff575819228849b9c982 From 3434db279eabf8d94a61beb0373d9569c190e10f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 27 Sep 2024 12:01:38 -0500 Subject: [PATCH 087/762] More removing braces around scalar initializers --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 69ebddb9..d0e4d686 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -675,15 +675,16 @@ void AlertLayer::Impl::HandleGeoLinesHover( AlertLayer::Impl::LineData AlertLayer::Impl::CreateLineData(const settings::LineSettings& lineSettings) { - return LineData {.borderColor_ {lineSettings.GetBorderColorRgba32f()}, - .highlightColor_ {lineSettings.GetHighlightColorRgba32f()}, - .lineColor_ {lineSettings.GetLineColorRgba32f()}, - .borderWidth_ {static_cast( - lineSettings.border_width().GetValue())}, - .highlightWidth_ {static_cast( - lineSettings.highlight_width().GetValue())}, - .lineWidth_ {static_cast( - lineSettings.line_width().GetValue())}}; + return LineData { + .borderColor_ {lineSettings.GetBorderColorRgba32f()}, + .highlightColor_ {lineSettings.GetHighlightColorRgba32f()}, + .lineColor_ {lineSettings.GetLineColorRgba32f()}, + .borderWidth_ = + static_cast(lineSettings.border_width().GetValue()), + .highlightWidth_ = + static_cast(lineSettings.highlight_width().GetValue()), + .lineWidth_ = + static_cast(lineSettings.line_width().GetValue())}; } void AlertLayer::Impl::UpdateLineData() From d039fef4f13b90babe86fe82e6d013dfcc183ce3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Sep 2024 07:35:20 -0500 Subject: [PATCH 088/762] Ensure edit line dialog reset button resets dialog to proper defaults --- .../alert_palette_settings_widget.cpp | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 9775b9c8..5d974225 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -249,27 +249,25 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( self_->AddSettingsCategory(&lineSettings); - connect( - toolButton, - &QAbstractButton::clicked, - self_, - [this, lineLabel, &lineSettings]() - { - // Set the active line label for when the dialog is finished - activeLineSettings_ = &lineSettings; + connect(toolButton, + &QAbstractButton::clicked, + self_, + [this, lineLabel, &lineSettings]() + { + // Set the active line label for when the dialog is finished + activeLineSettings_ = &lineSettings; - // Initialize dialog with current line settings - editLineDialog_->set_border_color(lineLabel->border_color()); - editLineDialog_->set_highlight_color(lineLabel->highlight_color()); - editLineDialog_->set_line_color(lineLabel->line_color()); + // Initialize dialog with current line settings + editLineDialog_->Initialize(lineLabel->border_color(), + lineLabel->highlight_color(), + lineLabel->line_color(), + lineLabel->border_width(), + lineLabel->highlight_width(), + lineLabel->line_width()); - editLineDialog_->set_border_width(lineLabel->border_width()); - editLineDialog_->set_highlight_width(lineLabel->highlight_width()); - editLineDialog_->set_line_width(lineLabel->line_width()); - - // Show the dialog - editLineDialog_->show(); - }); + // Show the dialog + editLineDialog_->show(); + }); } } // namespace ui From 2d8c3c8175fa6a59cde8cfb5aaea30d8529cde5c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Sep 2024 07:41:46 -0500 Subject: [PATCH 089/762] Removing old alert palette configuration from settings dialog --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 164 +----------------- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 44 ----- 2 files changed, 1 insertion(+), 207 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index f7e22c09..c35607a9 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -182,7 +182,6 @@ public: void SetupTextTab(); void SetupHotkeysTab(); - void ShowColorDialog(QLineEdit* lineEdit); void UpdateRadarDialogLocation(const std::string& id); void UpdateAlertRadarDialogLocation(const std::string& id); @@ -204,8 +203,7 @@ public: const std::string& value, QLabel* imageLabel); static std::string - RadarSiteLabel(std::shared_ptr& radarSite); - static void SetBackgroundColor(const std::string& value, QFrame* frame); + RadarSiteLabel(std::shared_ptr& radarSite); SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; @@ -254,12 +252,6 @@ public: std::unordered_map> colorTables_ {}; - std::unordered_map> - activeAlertColors_ {}; - std::unordered_map> - inactiveAlertColors_ {}; settings::SettingsInterface alertAudioSoundFile_ {}; settings::SettingsInterface alertAudioLocationMethod_ {}; @@ -804,9 +796,6 @@ void SettingsDialogImpl::SetupPalettesColorTablesTab() void SettingsDialogImpl::SetupPalettesAlertsTab() { - settings::PaletteSettings& paletteSettings = - settings::PaletteSettings::Instance(); - // Palettes > Alerts QVBoxLayout* layout = new QVBoxLayout(self_->ui->alertsPalette); @@ -815,119 +804,6 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() layout->addWidget(alertPaletteSettingsWidget_); settingsPages_.push_back(alertPaletteSettingsWidget_); - - // Palettes > Old Alerts - QGridLayout* alertsLayout = - reinterpret_cast(self_->ui->alertsFrame->layout()); - - QLabel* phenomenonLabel = new QLabel(QObject::tr("Phenomenon"), self_); - QLabel* activeLabel = new QLabel(QObject::tr("Active"), self_); - QLabel* inactiveLabel = new QLabel(QObject::tr("Inactive"), self_); - - QFont boldFont; - boldFont.setBold(true); - phenomenonLabel->setFont(boldFont); - activeLabel->setFont(boldFont); - inactiveLabel->setFont(boldFont); - - alertsLayout->addWidget(phenomenonLabel, 0, 0); - alertsLayout->addWidget(activeLabel, 0, 1, 1, 4); - alertsLayout->addWidget(inactiveLabel, 0, 5, 1, 4); - - auto& alertPhenomena = settings::PaletteSettings::alert_phenomena(); - - activeAlertColors_.reserve(alertPhenomena.size()); - inactiveAlertColors_.reserve(alertPhenomena.size()); - - int alertsRow = 1; - for (auto& phenomenon : alertPhenomena) - { - QFrame* activeFrame = new QFrame(self_); - QFrame* inactiveFrame = new QFrame(self_); - - QLineEdit* activeEdit = new QLineEdit(self_); - QLineEdit* inactiveEdit = new QLineEdit(self_); - - QToolButton* activeButton = new QToolButton(self_); - QToolButton* inactiveButton = new QToolButton(self_); - QToolButton* activeResetButton = new QToolButton(self_); - QToolButton* inactiveResetButton = new QToolButton(self_); - - activeFrame->setMinimumHeight(24); - activeFrame->setMinimumWidth(24); - activeFrame->setFrameShape(QFrame::Shape::Box); - activeFrame->setFrameShadow(QFrame::Shadow::Plain); - inactiveFrame->setMinimumHeight(24); - inactiveFrame->setMinimumWidth(24); - inactiveFrame->setFrameShape(QFrame::Shape::Box); - inactiveFrame->setFrameShadow(QFrame::Shadow::Plain); - - activeButton->setIcon( - QIcon {":/res/icons/font-awesome-6/palette-solid.svg"}); - inactiveButton->setIcon( - QIcon {":/res/icons/font-awesome-6/palette-solid.svg"}); - activeResetButton->setIcon( - QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); - inactiveResetButton->setIcon( - QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); - - alertsLayout->addWidget( - new QLabel(QObject::tr(awips::GetPhenomenonText(phenomenon).c_str()), - self_), - alertsRow, - 0); - alertsLayout->addWidget(activeFrame, alertsRow, 1); - alertsLayout->addWidget(activeEdit, alertsRow, 2); - alertsLayout->addWidget(activeButton, alertsRow, 3); - alertsLayout->addWidget(activeResetButton, alertsRow, 4); - alertsLayout->addWidget(inactiveFrame, alertsRow, 5); - alertsLayout->addWidget(inactiveEdit, alertsRow, 6); - alertsLayout->addWidget(inactiveButton, alertsRow, 7); - alertsLayout->addWidget(inactiveResetButton, alertsRow, 8); - ++alertsRow; - - // Create settings interface - auto activeResult = activeAlertColors_.emplace( - phenomenon, settings::SettingsInterface {}); - auto inactiveResult = inactiveAlertColors_.emplace( - phenomenon, settings::SettingsInterface {}); - auto& activeColor = activeResult.first->second; - auto& inactiveColor = inactiveResult.first->second; - - // Add to settings list - settings_.push_back(&activeColor); - settings_.push_back(&inactiveColor); - - auto& activeSetting = paletteSettings.alert_color(phenomenon, true); - auto& inactiveSetting = paletteSettings.alert_color(phenomenon, false); - - activeColor.SetSettingsVariable(activeSetting); - activeColor.SetEditWidget(activeEdit); - activeColor.SetResetButton(activeResetButton); - - inactiveColor.SetSettingsVariable(inactiveSetting); - inactiveColor.SetEditWidget(inactiveEdit); - inactiveColor.SetResetButton(inactiveResetButton); - - SetBackgroundColor(activeSetting.GetValue(), activeFrame); - SetBackgroundColor(inactiveSetting.GetValue(), inactiveFrame); - - activeSetting.RegisterValueStagedCallback( - [activeFrame](const std::string& value) - { SetBackgroundColor(value, activeFrame); }); - inactiveSetting.RegisterValueStagedCallback( - [inactiveFrame](const std::string& value) - { SetBackgroundColor(value, inactiveFrame); }); - - QObject::connect(activeButton, - &QAbstractButton::clicked, - self_, - [=, this]() { ShowColorDialog(activeEdit); }); - QObject::connect(inactiveButton, - &QAbstractButton::clicked, - self_, - [=, this]() { ShowColorDialog(inactiveEdit); }); - } } void SettingsDialogImpl::SetupUnitsTab() @@ -1382,44 +1258,6 @@ void SettingsDialogImpl::LoadColorTablePreview(const std::string& key, }); } -void SettingsDialogImpl::ShowColorDialog(QLineEdit* lineEdit) -{ - QColorDialog* dialog = new QColorDialog(self_); - - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); - - QColor initialColor(lineEdit->text()); - if (initialColor.isValid()) - { - dialog->setCurrentColor(initialColor); - } - - QObject::connect( - dialog, - &QColorDialog::colorSelected, - self_, - [lineEdit](const QColor& color) - { - QString colorName = color.name(QColor::NameFormat::HexArgb); - - logger_->info("Selected color: {}", colorName.toStdString()); - lineEdit->setText(colorName); - - // setText does not emit the textEdited signal - Q_EMIT lineEdit->textEdited(colorName); - }); - - dialog->open(); -} - -void SettingsDialogImpl::SetBackgroundColor(const std::string& value, - QFrame* frame) -{ - frame->setStyleSheet( - QString::fromStdString(fmt::format("background-color: {}", value))); -} - void SettingsDialogImpl::UpdateRadarDialogLocation(const std::string& id) { std::shared_ptr radarSite = config::RadarSite::Get(id); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 0237462a..444e6705 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -639,50 +639,6 @@ Alerts - - - Old Alerts - - - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 239 - - - - - - From a1d9b25f0bbc0b517188801c5a20fcf8a97d4a40 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Sep 2024 08:19:51 -0500 Subject: [PATCH 090/762] Add alert palette reset buttons --- .../scwx/qt/settings/settings_category.cpp | 89 +++++++++++++++++++ .../scwx/qt/settings/settings_category.hpp | 23 +++++ .../scwx/qt/settings/settings_variable.cpp | 12 +++ .../scwx/qt/settings/settings_variable.hpp | 18 ++++ .../qt/settings/settings_variable_base.hpp | 18 ++++ .../alert_palette_settings_widget.cpp | 17 ++++ 6 files changed, 177 insertions(+) diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 3714c4ca..75a46bf8 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -48,6 +48,62 @@ SettingsCategory::SettingsCategory(SettingsCategory&&) noexcept = default; SettingsCategory& SettingsCategory::operator=(SettingsCategory&&) noexcept = default; +bool SettingsCategory::IsDefault() const +{ + bool isDefault = true; + + // Get subcategory array defaults + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + isDefault = isDefault && subcategory->IsDefault(); + } + } + + // Get subcategory defaults + for (auto& subcategory : p->subcategories_) + { + isDefault = isDefault && subcategory->IsDefault(); + } + + // Get variable defaults + for (auto& variable : p->variables_) + { + isDefault = isDefault && variable->IsDefault(); + } + + return isDefault; +} + +bool SettingsCategory::IsDefaultStaged() const +{ + bool isDefaultStaged = true; + + // Get subcategory array defaults + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + isDefaultStaged = isDefaultStaged && subcategory->IsDefaultStaged(); + } + } + + // Get subcategory defaults + for (auto& subcategory : p->subcategories_) + { + isDefaultStaged = isDefaultStaged && subcategory->IsDefaultStaged(); + } + + // Get variable defaults + for (auto& variable : p->variables_) + { + isDefaultStaged = isDefaultStaged && variable->IsDefaultStaged(); + } + + return isDefaultStaged; +} + std::string SettingsCategory::name() const { return p->name_; @@ -102,6 +158,39 @@ void SettingsCategory::SetDefaults() p->stagedSignal_(); } +void SettingsCategory::StageDefaults() +{ + // Don't allow individual variables to invoke the signal when operating over + // the entire category + p->blockSignals_ = true; + + // Stage subcategory array defaults + for (auto& subcategoryArray : p->subcategoryArrays_) + { + for (auto& subcategory : subcategoryArray.second) + { + subcategory->StageDefaults(); + } + } + + // Stage subcategory defaults + for (auto& subcategory : p->subcategories_) + { + subcategory->StageDefaults(); + } + + // Stage variable defaults + for (auto& variable : p->variables_) + { + variable->StageDefault(); + } + + // Unblock signals + p->blockSignals_ = false; + + p->stagedSignal_(); +} + bool SettingsCategory::Commit() { bool committed = false; diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index ee80ba46..167af06a 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -43,11 +43,34 @@ public: */ boost::signals2::signal& staged_signal(); + /** + * Gets whether or not all settings variables are currently set to default + * values. + * + * @return true if all settings variables are currently set to default + * values, otherwise false. + */ + bool IsDefault() const; + + /** + * Gets whether or not all settings variables currently have staged values + * set to default. + * + * @return true if all settings variables currently have staged values set + * to default, otherwise false. + */ + bool IsDefaultStaged() const; + /** * Set all variables to their defaults. */ void SetDefaults(); + /** + * Stage all variables to their defaults. + */ + void StageDefaults(); + /** * Sets the current value of all variables to the staged value. * diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index c4a5f6c9..a5387937 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -65,6 +65,18 @@ inline auto FormatParameter(const T& value) } } +template +bool SettingsVariable::IsDefault() const +{ + return p->value_ == p->default_; +} + +template +bool SettingsVariable::IsDefaultStaged() const +{ + return p->staged_.value_or(p->value_) == p->default_; +} + template T SettingsVariable::GetValue() const { diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index d2e6c949..df9184a1 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -37,6 +37,24 @@ public: SettingsVariable(SettingsVariable&&) noexcept; SettingsVariable& operator=(SettingsVariable&&) noexcept; + /** + * Gets whether or not the settings variable is currently set to its default + * value. + * + * @return true if the settings variable is currently set to its default + * value, otherwise false. + */ + bool IsDefault() const; + + /** + * Gets whether or not the settings variable currently has its staged value + * set to default. + * + * @return true if the settings variable currently has its staged value set + * to default, otherwise false. + */ + bool IsDefaultStaged() const; + /** * Gets the current value of the settings variable. * diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp index fba1eff9..f4e48934 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp @@ -45,6 +45,24 @@ public: */ boost::signals2::signal& staged_signal(); + /** + * Gets whether or not the settings variable is currently set to its default + * value. + * + * @return true if the settings variable is currently set to its default + * value, otherwise false. + */ + virtual bool IsDefault() const = 0; + + /** + * Gets whether or not the settings variable currently has its staged value + * set to default. + * + * @return true if the settings variable currently has its staged value set + * to default, otherwise false. + */ + virtual bool IsDefaultStaged() const = 0; + /** * Sets the current value of the settings variable to default. */ diff --git a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp index 5d974225..a3ae4642 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/alert_palette_settings_widget.cpp @@ -57,6 +57,8 @@ public: settings::LineSettings* activeLineSettings_ {nullptr}; boost::unordered_flat_map phenomenonPages_ {}; + + std::vector connections_ {}; }; AlertPaletteSettingsWidget::AlertPaletteSettingsWidget(QWidget* parent) : @@ -243,9 +245,15 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( LineLabel* lineLabel = new LineLabel(self_); lineLabel->set_line_settings(lineSettings); + QToolButton* resetButton = new QToolButton(self_); + resetButton->setIcon( + QIcon {":/res/icons/font-awesome-6/rotate-left-solid.svg"}); + resetButton->setVisible(!lineSettings.IsDefaultStaged()); + layout->addWidget(new QLabel(tr(name.c_str()), self_), row, 0); layout->addWidget(lineLabel, row, 1); layout->addWidget(toolButton, row, 2); + layout->addWidget(resetButton, row, 3); self_->AddSettingsCategory(&lineSettings); @@ -268,6 +276,15 @@ void AlertPaletteSettingsWidget::Impl::AddPhenomenonLine( // Show the dialog editLineDialog_->show(); }); + + connect(resetButton, + &QAbstractButton::clicked, + self_, + [&lineSettings]() { lineSettings.StageDefaults(); }); + + connections_.emplace_back(lineSettings.staged_signal().connect( + [resetButton, &lineSettings]() + { resetButton->setVisible(!lineSettings.IsDefaultStaged()); })); } } // namespace ui From 295bbda921f591d622be11aa03713ef244ac06ca Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Sep 2024 21:39:44 -0500 Subject: [PATCH 091/762] Properly mark settings variable functions as override --- scwx-qt/source/scwx/qt/settings/settings_variable.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index df9184a1..2c3b2a07 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -44,7 +44,7 @@ public: * @return true if the settings variable is currently set to its default * value, otherwise false. */ - bool IsDefault() const; + bool IsDefault() const override; /** * Gets whether or not the settings variable currently has its staged value @@ -53,7 +53,7 @@ public: * @return true if the settings variable currently has its staged value set * to default, otherwise false. */ - bool IsDefaultStaged() const; + bool IsDefaultStaged() const override; /** * Gets the current value of the settings variable. From f672ff553a1765629f8043d1151a75a0f115acaf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Sep 2024 06:14:17 -0500 Subject: [PATCH 092/762] Update alert lines on settings update --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 99 ++++++++++++++++++---- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index d0e4d686..5ed4e458 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -40,6 +41,8 @@ struct AlertTypeHash> size_t operator()(const std::pair& x) const; }; +static bool IsAlertActive(const std::shared_ptr& segment); + class AlertLayerHandler : public QObject { Q_OBJECT @@ -160,8 +163,8 @@ public: const QPointF& mouseGlobalPos); void ScheduleRefresh(); - LineData& GetLineData(std::shared_ptr& segment, - bool alertActive); + LineData& GetLineData(const std::shared_ptr& segment, + bool alertActive); void UpdateLineData(); void AddLine(std::shared_ptr& geoLines, @@ -182,6 +185,7 @@ public: bool enableHover, boost::container::stable_vector< std::shared_ptr>& drawItems); + void UpdateLines(); static LineData CreateLineData(const settings::LineSettings& lineSettings); @@ -218,6 +222,8 @@ public: std::shared_ptr lastHoverDi_ {nullptr}; std::string tooltip_ {}; + + std::vector connections_ {}; }; AlertLayer::AlertLayer(std::shared_ptr context, @@ -302,6 +308,15 @@ void AlertLayer::Deinitialize() DrawLayer::Deinitialize(); } +bool IsAlertActive(const std::shared_ptr& segment) +{ + auto& vtec = segment->header_->vtecString_.front(); + auto action = vtec.pVtec_.action(); + bool alertActive = (action != awips::PVtec::Action::Canceled); + + return alertActive; +} + void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, size_t messageIndex) { @@ -344,10 +359,9 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, continue; } - auto& vtec = segment->header_->vtecString_.front(); - auto action = vtec.pVtec_.action(); - awips::Phenomenon phenomenon = vtec.pVtec_.phenomenon(); - bool alertActive = (action != awips::PVtec::Action::Canceled); + auto& vtec = segment->header_->vtecString_.front(); + awips::Phenomenon phenomenon = vtec.pVtec_.phenomenon(); + bool alertActive = IsAlertActive(segment); auto& segmentsForType = segmentsByType_[{key.phenomenon_, alertActive}]; @@ -406,6 +420,8 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() void AlertLayer::Impl::ConnectSignals() { + auto& alertPaletteSettings = + settings::PaletteSettings::Instance().alert_palette(phenomenon_); auto timelineManager = manager::TimelineManager::Instance(); QObject::connect(timelineManager.get(), @@ -413,6 +429,13 @@ void AlertLayer::Impl::ConnectSignals() receiver_.get(), [this](std::chrono::system_clock::time_point dateTime) { selectedTime_ = dateTime; }); + + connections_.push_back(alertPaletteSettings.changed_signal().connect( + [this]() + { + UpdateLineData(); + UpdateLines(); + })); } void AlertLayer::Impl::ScheduleRefresh() @@ -452,9 +475,7 @@ void AlertLayer::Impl::AddAlert( { auto& segment = segmentRecord->segment_; - auto& vtec = segment->header_->vtecString_.front(); - auto action = vtec.pVtec_.action(); - bool alertActive = (action != awips::PVtec::Action::Canceled); + bool alertActive = IsAlertActive(segment); auto& startTime = segmentRecord->segmentBegin_; auto& endTime = segmentRecord->segmentEnd_; @@ -533,11 +554,8 @@ void AlertLayer::Impl::UpdateAlert( auto it = linesBySegment_.find(segmentRecord); if (it != linesBySegment_.cend()) { - auto& segment = segmentRecord->segment_; - - auto& vtec = segment->header_->vtecString_.front(); - auto action = vtec.pVtec_.action(); - bool alertActive = (action != awips::PVtec::Action::Canceled); + auto& segment = segmentRecord->segment_; + bool alertActive = IsAlertActive(segment); auto& geoLines = geoLines_.at(alertActive); @@ -624,6 +642,54 @@ void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, } } +void AlertLayer::Impl::UpdateLines() +{ + std::unique_lock lock {linesMutex_}; + + for (auto& segmentLine : linesBySegment_) + { + auto& segmentRecord = segmentLine.first; + auto& geoLineDrawItems = segmentLine.second; + auto& segment = segmentRecord->segment_; + bool alertActive = IsAlertActive(segment); + auto& lineData = GetLineData(segment, alertActive); + auto& geoLines = geoLines_.at(alertActive); + + const float borderWidth = lineData.borderWidth_; + const float highlightWidth = lineData.highlightWidth_; + const float lineWidth = lineData.lineWidth_; + + const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); + const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); + + // Border, highlight and line + std::size_t linesPerType = geoLineDrawItems.size() / 3; + + // Border + for (auto& borderLine : geoLineDrawItems | std::views::take(linesPerType)) + { + geoLines->SetLineModulate(borderLine, lineData.borderColor_); + geoLines->SetLineWidth(borderLine, totalBorderWidth); + } + + // Highlight + for (auto& highlightLine : geoLineDrawItems | + std::views::drop(linesPerType) | + std::views::take(linesPerType)) + { + geoLines->SetLineModulate(highlightLine, lineData.highlightColor_); + geoLines->SetLineWidth(highlightLine, totalHighlightWidth); + } + + // Line + for (auto& line : geoLineDrawItems | std::views::drop(linesPerType * 2)) + { + geoLines->SetLineModulate(line, lineData.lineColor_); + geoLines->SetLineWidth(line, lineWidth); + } + } +} + void AlertLayer::Impl::HandleGeoLinesEvent( std::shared_ptr& di, QEvent* ev) { @@ -713,9 +779,8 @@ void AlertLayer::Impl::UpdateLineData() inactiveLineData_ = CreateLineData(alertPalette.inactive()); } -AlertLayer::Impl::LineData& -AlertLayer::Impl::GetLineData(std::shared_ptr& segment, - bool alertActive) +AlertLayer::Impl::LineData& AlertLayer::Impl::GetLineData( + const std::shared_ptr& segment, bool alertActive) { if (!alertActive) { From c811b95129cdf709d40793203770883c07feeeb9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Sep 2024 06:22:24 -0500 Subject: [PATCH 093/762] Update test data to merged revision --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 8a9e6bc2..40a367ca 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 8a9e6bc2e457e9ef69cbff575819228849b9c982 +Subproject commit 40a367ca89b5b197353ca58dea547a3e3407c7f3 From a5484977671da9c2a4eeb9756d93c65c2d6a0649 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Oct 2024 06:31:02 -0500 Subject: [PATCH 094/762] Add partial handling for missing level 2 radials - Need to handle additional cases (1999-05-03 @ KTLX is a good sample) - Still crashes on getting bin level for hover text --- .../scwx/qt/view/level2_product_view.cpp | 106 +++++++++++++----- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 47558d81..a952b863 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -536,7 +536,7 @@ void Level2ProductView::ComputeSweep() return; } - const size_t radials = radarData->size(); + const std::size_t radials = radarData->crbegin()->first; p->ComputeCoordinates(radarData); @@ -829,7 +829,7 @@ void Level2ProductViewImpl::ComputeCoordinates( auto momentData0 = radarData0->moment_data_block(dataBlockType_); const std::uint16_t numRadials = - static_cast(radarData->size()); + static_cast(radarData->crbegin()->first + 1); const std::uint16_t numRangeBins = std::max(momentData0->number_of_data_moment_gates() + 1u, common::MAX_DATA_MOMENT_GATES); @@ -837,39 +837,83 @@ void Level2ProductViewImpl::ComputeCoordinates( auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); - std::for_each(std::execution::par_unseq, - radials.begin(), - radials.end(), - [&](std::uint32_t radial) - { - const units::degrees angle = - (*radarData)[radial]->azimuth_angle(); + std::for_each( + std::execution::par_unseq, + radials.begin(), + radials.end(), + [&](std::uint32_t radial) + { + units::degrees angle {}; - std::for_each(std::execution::par_unseq, - gates.begin(), - gates.end(), - [&](std::uint32_t gate) - { - const std::uint32_t radialGate = - radial * common::MAX_DATA_MOMENT_GATES + - gate; - const float range = (gate + 1) * gateSize; - const std::size_t offset = radialGate * 2; + auto radialData = radarData->find(radial); + if (radialData != radarData->cend()) + { + angle = radialData->second->azimuth_angle(); + } + else + { + auto prevRadial1 = radarData->find( + (radial >= 1) ? radial - 1 : numRadials - (1 - radial)); + auto prevRadial2 = radarData->find( + (radial >= 2) ? radial - 2 : numRadials - (2 - radial)); - double latitude; - double longitude; + if (prevRadial1 != radarData->cend() && + prevRadial2 != radarData->cend()) + { + const units::degrees prevAngle1 = + prevRadial1->second->azimuth_angle(); + const units::degrees prevAngle2 = + prevRadial2->second->azimuth_angle(); - geodesic.Direct(radarLatitude, - radarLongitude, - angle.value(), - range, - latitude, - longitude); + // No wrapping required since angle is only used for geodesic + // calculation + const units::degrees deltaAngle = prevAngle1 - prevAngle2; - coordinates_[offset] = latitude; - coordinates_[offset + 1] = longitude; - }); - }); + angle = prevAngle1 + deltaAngle; + } + else if (prevRadial1 != radarData->cend()) + { + const units::degrees prevAngle1 = + prevRadial1->second->azimuth_angle(); + + // Assume a half degree delta if there aren't enough angles + // to determine a delta angle + constexpr units::degrees deltaAngle = + units::degrees {0.5}; + + angle = prevAngle1 + deltaAngle; + } + else + { + // Not enough angles present to determine an angle + return; + } + } + + std::for_each(std::execution::par_unseq, + gates.begin(), + gates.end(), + [&](std::uint32_t gate) + { + const std::uint32_t radialGate = + radial * common::MAX_DATA_MOMENT_GATES + gate; + const float range = (gate + 1) * gateSize; + const std::size_t offset = radialGate * 2; + + double latitude; + double longitude; + + geodesic.Direct(radarLatitude, + radarLongitude, + angle.value(), + range, + latitude, + longitude); + + coordinates_[offset] = latitude; + coordinates_[offset + 1] = longitude; + }); + }); timer.stop(); logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } From fe4a324a04d005aaab77aff485434438b3a1311b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 2 Oct 2024 05:56:27 -0500 Subject: [PATCH 095/762] Use an extra vertex radial with missing data to prevent stretching --- .../scwx/qt/view/level2_product_view.cpp | 56 ++++++++++++++++--- wxdata/include/scwx/common/geographic.hpp | 13 +++++ wxdata/source/scwx/common/geographic.cpp | 28 ++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index a952b863..15112410 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -106,14 +106,17 @@ public: threadPool_.join(); }; - void - ComputeCoordinates(std::shared_ptr radarData); + void ComputeCoordinates( + const std::shared_ptr& radarData); void SetProduct(const std::string& productName); void SetProduct(common::Level2Product product); void UpdateOtherUnits(const std::string& name); void UpdateSpeedUnits(const std::string& name); + static bool IsRadarDataIncomplete( + const std::shared_ptr& radarData); + Level2ProductView* self_; boost::asio::thread_pool threadPool_ {1u}; @@ -536,7 +539,17 @@ void Level2ProductView::ComputeSweep() return; } - const std::size_t radials = radarData->crbegin()->first; + const std::size_t radials = radarData->crbegin()->first + 1; + std::size_t vertexRadials = radials; + + // When there is missing data, insert another empty vertex radial at the end + // to avoid stretching + const bool isRadarDataIncomplete = + Level2ProductViewImpl::IsRadarDataIncomplete(radarData); + if (isRadarDataIncomplete) + { + ++vertexRadials; + } p->ComputeCoordinates(radarData); @@ -574,7 +587,8 @@ void Level2ProductView::ComputeSweep() std::vector& vertices = p->vertices_; size_t vIndex = 0; vertices.clear(); - vertices.resize(radials * gates * VERTICES_PER_BIN * VALUES_PER_VERTEX); + vertices.resize(vertexRadials * gates * VERTICES_PER_BIN * + VALUES_PER_VERTEX); // Setup data moment vector std::vector& dataMoments8 = p->dataMoments8_; @@ -807,7 +821,7 @@ void Level2ProductView::ComputeSweep() } void Level2ProductViewImpl::ComputeCoordinates( - std::shared_ptr radarData) + const std::shared_ptr& radarData) { logger_->debug("ComputeCoordinates()"); @@ -828,12 +842,22 @@ void Level2ProductViewImpl::ComputeCoordinates( auto& radarData0 = (*radarData)[0]; auto momentData0 = radarData0->moment_data_block(dataBlockType_); - const std::uint16_t numRadials = + std::uint16_t numRadials = static_cast(radarData->crbegin()->first + 1); const std::uint16_t numRangeBins = std::max(momentData0->number_of_data_moment_gates() + 1u, common::MAX_DATA_MOMENT_GATES); + // Add an extra radial when incomplete data exists + if (IsRadarDataIncomplete(radarData)) + { + ++numRadials; + } + + // Limit radials + numRadials = + std::min(numRadials, common::MAX_0_5_DEGREE_RADIALS); + auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); @@ -878,8 +902,7 @@ void Level2ProductViewImpl::ComputeCoordinates( // Assume a half degree delta if there aren't enough angles // to determine a delta angle - constexpr units::degrees deltaAngle = - units::degrees {0.5}; + constexpr units::degrees deltaAngle {0.5f}; angle = prevAngle1 + deltaAngle; } @@ -918,6 +941,23 @@ void Level2ProductViewImpl::ComputeCoordinates( logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } +bool Level2ProductViewImpl::IsRadarDataIncomplete( + const std::shared_ptr& radarData) +{ + // Assume the data is incomplete when the delta between the first and last + // angles is greater than 2.5 degrees. + constexpr units::degrees kIncompleteDataAngleThreshold_ {2.5}; + + const units::degrees firstAngle = + radarData->cbegin()->second->azimuth_angle(); + const units::degrees lastAngle = + radarData->crbegin()->second->azimuth_angle(); + const units::degrees angleDelta = + common::GetAngleDelta(firstAngle, lastAngle); + + return angleDelta > kIncompleteDataAngleThreshold_; +} + std::optional Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const { diff --git a/wxdata/include/scwx/common/geographic.hpp b/wxdata/include/scwx/common/geographic.hpp index 8945db17..8b234fd2 100644 --- a/wxdata/include/scwx/common/geographic.hpp +++ b/wxdata/include/scwx/common/geographic.hpp @@ -3,6 +3,8 @@ #include #include +#include + namespace scwx { namespace common @@ -46,6 +48,17 @@ enum class DistanceType Miles }; +/** + * Calculate the absolute angle delta between two angles. + * + * @param [in] angle1 First angle + * @param [in] angle2 Second angle + * + * @return Absolute angle delta normalized to [0, 360) + */ +units::degrees GetAngleDelta(units::degrees angle1, + units::degrees angle2); + /** * Calculate the geographic midpoint of a set of coordinates. Uses Method A * described at http://www.geomidpoint.com/calculation.html. diff --git a/wxdata/source/scwx/common/geographic.cpp b/wxdata/source/scwx/common/geographic.cpp index bf7d1a2c..e9a494d3 100644 --- a/wxdata/source/scwx/common/geographic.cpp +++ b/wxdata/source/scwx/common/geographic.cpp @@ -14,6 +14,34 @@ static std::string GetDegreeString(double degrees, DegreeStringType type, const std::string& suffix); +units::degrees GetAngleDelta(units::degrees angle1, + units::degrees angle2) +{ + // Normalize angles to [0, 360) + while (angle1.value() < 0.0f) + { + angle1 += units::degrees {360.0f}; + } + while (angle2.value() < 0.0f) + { + angle2 += units::degrees {360.0f}; + } + angle1 = units::degrees {std::fmod(angle1.value(), 360.f)}; + angle2 = units::degrees {std::fmod(angle2.value(), 360.f)}; + + // Calculate the absolute difference + auto delta = angle1 - angle2; + if (delta < units::degrees {0.0f}) + { + delta *= -1.0f; + } + + // Account for wrapping + delta = std::min(delta, units::degrees {360.0f} - delta); + + return delta; +} + Coordinate GetCentroid(const std::vector& coordinates) { double x = 0.0; From 4eeac9f8303f34dbf88ba5c440ba3395ef8e09ed Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 2 Oct 2024 11:47:12 -0400 Subject: [PATCH 096/762] update KHDC position with lat/lon from api.weather.gov --- scwx-qt/res/config/radar_sites.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/res/config/radar_sites.json b/scwx-qt/res/config/radar_sites.json index 8f2a1b90..20d7397c 100644 --- a/scwx-qt/res/config/radar_sites.json +++ b/scwx-qt/res/config/radar_sites.json @@ -67,7 +67,7 @@ { "type": "wsr88d", "id": "KLVX", "lat": 37.9753058, "lon": -85.9438455, "country": "USA", "state": "KY", "place": "Louisville", "tz": "America/New_York", "elevation": 833.0 }, { "type": "wsr88d", "id": "KPAH", "lat": 37.068333, "lon": -88.771944, "country": "USA", "state": "KY", "place": "Paducah", "tz": "America/Chicago", "elevation": 506.0 }, { "type": "wsr88d", "id": "KPOE", "lat": 31.1556923, "lon": -92.9762596, "country": "USA", "state": "LA", "place": "Fort Polk", "tz": "America/Chicago", "elevation": 473.0 }, - { "type": "wsr88d", "id": "KHDC", "lat": 30.519306, "lon": -90.424028, "country": "USA", "state": "LA", "place": "New Orleans (Hammond)", "tz": "America/Chicago", "elevation": 43.0 }, + { "type": "wsr88d", "id": "KHDC", "lat": 30.5196, "lon": -90.4074, "country": "USA", "state": "LA", "place": "New Orleans (Hammond)", "tz": "America/Chicago", "elevation": 43.0 }, { "type": "wsr88d", "id": "KLCH", "lat": 30.125306, "lon": -93.215889, "country": "USA", "state": "LA", "place": "Lake Charles", "tz": "America/Chicago", "elevation": 137.0 }, { "type": "wsr88d", "id": "KSHV", "lat": 32.450833, "lon": -93.84125, "country": "USA", "state": "LA", "place": "Shreveport", "tz": "America/Chicago", "elevation": 387.0 }, { "type": "wsr88d", "id": "KLIX", "lat": 30.3367133, "lon": -89.8256618, "country": "USA", "state": "LA", "place": "New Orleans (Slidell)", "tz": "America/Chicago", "elevation": 179.0 }, From 41c112538842567945436ad77e69d32db581387a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 3 Oct 2024 05:44:40 -0500 Subject: [PATCH 097/762] Fix usage of vertexRadials in ComputeSweep --- .../scwx/qt/view/level2_product_view.cpp | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 15112410..a2c1b42e 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -539,8 +539,8 @@ void Level2ProductView::ComputeSweep() return; } - const std::size_t radials = radarData->crbegin()->first + 1; - std::size_t vertexRadials = radials; + std::size_t radials = radarData->crbegin()->first + 1; + std::size_t vertexRadials = radials; // When there is missing data, insert another empty vertex radial at the end // to avoid stretching @@ -551,6 +551,11 @@ void Level2ProductView::ComputeSweep() ++vertexRadials; } + // Limit radials + radials = std::min(radials, common::MAX_0_5_DEGREE_RADIALS); + vertexRadials = + std::min(vertexRadials, common::MAX_0_5_DEGREE_RADIALS); + p->ComputeCoordinates(radarData); const std::vector& coordinates = p->coordinates_; @@ -735,15 +740,16 @@ void Level2ProductView::ComputeSweep() { const std::uint16_t baseCoord = gate - 1; - std::size_t offset1 = ((startRadial + radial) % radials * + std::size_t offset1 = ((startRadial + radial) % vertexRadials * common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; std::size_t offset2 = offset1 + gateSize * 2; - std::size_t offset3 = (((startRadial + radial + 1) % radials) * - common::MAX_DATA_MOMENT_GATES + - baseCoord) * - 2; + std::size_t offset3 = + (((startRadial + radial + 1) % vertexRadials) * + common::MAX_DATA_MOMENT_GATES + + baseCoord) * + 2; std::size_t offset4 = offset3 + gateSize * 2; vertices[vIndex++] = coordinates[offset1]; @@ -770,14 +776,15 @@ void Level2ProductView::ComputeSweep() { const std::uint16_t baseCoord = gate; - std::size_t offset1 = ((startRadial + radial) % radials * - common::MAX_DATA_MOMENT_GATES + - baseCoord) * - 2; - std::size_t offset2 = (((startRadial + radial + 1) % radials) * + std::size_t offset1 = ((startRadial + radial) % vertexRadials * common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; + std::size_t offset2 = + (((startRadial + radial + 1) % vertexRadials) * + common::MAX_DATA_MOMENT_GATES + + baseCoord) * + 2; vertices[vIndex++] = p->latitude_; vertices[vIndex++] = p->longitude_; From 1436b7bba64ca1f82552a02fe8fc79d361234ee6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 3 Oct 2024 05:54:44 -0500 Subject: [PATCH 098/762] Handle missing level 2 radials when getting bin data --- .../scwx/qt/view/level2_product_view.cpp | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index a2c1b42e..0938f614 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -1007,8 +1007,19 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const } // Find Radial - const std::uint16_t numRadials = - static_cast(radarData->size()); + std::uint16_t numRadials = + static_cast(radarData->crbegin()->first + 1); + + // Add an extra radial when incomplete data exists + if (Level2ProductViewImpl::IsRadarDataIncomplete(radarData)) + { + ++numRadials; + } + + // Limit radials + numRadials = + std::min(numRadials, common::MAX_0_5_DEGREE_RADIALS); + auto radials = boost::irange(0u, numRadials); auto radial = std::find_if( // @@ -1017,25 +1028,59 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const radials.end(), [&](std::uint32_t i) { - bool found = false; - const units::degrees startAngle = - (*radarData)[i]->azimuth_angle(); - const units::degrees nextAngle = - (*radarData)[(i + 1) % numRadials]->azimuth_angle(); + bool hasNextAngle = false; + bool found = false; - if (startAngle < nextAngle) + units::degrees startAngle {}; + units::degrees nextAngle {}; + + auto radialData = radarData->find(i); + if (radialData != radarData->cend()) { - if (startAngle.value() <= azi1 && azi1 < nextAngle.value()) + startAngle = radialData->second->azimuth_angle(); + + auto nextRadial = radarData->find((i + 1) % numRadials); + if (nextRadial != radarData->cend()) { - found = true; + nextAngle = nextRadial->second->azimuth_angle(); + hasNextAngle = true; + } + else + { + // Next angle is not available, interpolate + auto prevRadial = + radarData->find((i >= 1) ? i - 1 : numRadials - (1 - i)); + + if (prevRadial != radarData->cend()) + { + const units::degrees prevAngle = + prevRadial->second->azimuth_angle(); + + const units::degrees deltaAngle = + common::GetAngleDelta(startAngle, prevAngle); + + nextAngle = startAngle + deltaAngle; + hasNextAngle = true; + } } } - else + + if (hasNextAngle) { - // If the bin crosses 0/360 degrees, special handling is needed - if (startAngle.value() <= azi1 || azi1 < nextAngle.value()) + if (startAngle < nextAngle) { - found = true; + if (startAngle.value() <= azi1 && azi1 < nextAngle.value()) + { + found = true; + } + } + else + { + // If the bin crosses 0/360 degrees, special handling is needed + if (startAngle.value() <= azi1 || azi1 < nextAngle.value()) + { + found = true; + } } } From fe9affcefe9f41f984e6d0f6930572ba64d05db5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:34:50 +0000 Subject: [PATCH 099/762] Update dependency libcurl to v8.10.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index a559e800..f7f015e3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -11,7 +11,7 @@ class SupercellWxConan(ConanFile): "glew/2.2.0", "glm/cci.20230113", "gtest/1.15.0", - "libcurl/8.10.0", + "libcurl/8.10.1", "libxml2/2.12.7", "openssl/3.3.2", "re2/20240702", From c2209908a0ddcff6a79f7d42fedf30e72e6975fe Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 2 Oct 2024 11:34:52 -0400 Subject: [PATCH 100/762] initial poi manager and types code --- .../source/scwx/qt/manager/poi_manager.cpp | 109 ++++++++++++++++++ .../source/scwx/qt/manager/poi_manager.hpp | 30 +++++ scwx-qt/source/scwx/qt/types/poi_types.hpp | 30 +++++ 3 files changed, 169 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/manager/poi_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/poi_manager.hpp create mode 100644 scwx-qt/source/scwx/qt/types/poi_types.hpp diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.cpp b/scwx-qt/source/scwx/qt/manager/poi_manager.cpp new file mode 100644 index 00000000..658ae4da --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/poi_manager.cpp @@ -0,0 +1,109 @@ +#include +#include +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::placefile_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static const std::string kNameName_ = "name"; +static const std::string kLatitudeName_ = "latitude"; +static const std::string kLongitudeName_ = "longitude"; + +class POIManager::Impl +{ +public: + class PointOfInterest; + + explicit Impl(POIManager* self) : self_ {self} {} + + std::string poiSettingsPath_ {}; + + POIManager* self_; + + void ReadPOISettings(); + +}; + +class POIManager::Impl::PointOfInterest +{ +public: + PointOfInterest(std::string name, double latitude, double longitude) : + name_ {name}, latitude_ {latitude}, longitude_ {longitude} + { + } + + std::string name_; + double latitude_; + double longitude_; + + friend void tag_invoke(boost::json::value_from_tag, + boost::json::value& jv, + const std::shared_ptr& record) + { + jv = {{kNameName_, record->name_}, + {kLatitudeName_, record->latitude_}, + {kLongitudeName_, record->longitude_}}; + } + + friend PointOfInterest tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) + { + return PointOfInterest( + boost::json::value_to(jv.at(kNameName_)), + boost::json::value_to(jv.at(kLatitudeName_)), + boost::json::value_to(jv.at(kLongitudeName_))); + } +}; + +POIManager::POIManager() : p(std::make_unique(this)) {} + +void POIManager::Impl::ReadPOISettings() +{ + logger_->info("Reading point of intrest settings"); + + boost::json::value poiJson = nullptr; + + // Determine if poi settings exists + if (std::filesystem::exists(poiSettingsPath_)) + { + poiJson = util::json::ReadJsonFile(poiSettingsPath_); + } + + if (poiJson != nullptr && poiJson.is_array()) + { + // For each poi entry + auto& poiArray = poiJson.as_array(); + for (auto& poiEntry : poiArray) + { + try + { + PointOfInterest record = + boost::json::value_to(poiEntry); + + if (!record.name_.empty()) + { + // Add record + } + } + catch (const std::exception& ex) + { + logger_->warn("Invalid point of interest entry: {}", ex.what()); + } + } + } +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.hpp b/scwx-qt/source/scwx/qt/manager/poi_manager.hpp new file mode 100644 index 00000000..b0bc574d --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/poi_manager.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class POIManager : public QObject +{ + Q_OBJECT + +public: + explicit POIManager(); + ~POIManager(); + + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/poi_types.hpp b/scwx-qt/source/scwx/qt/types/poi_types.hpp new file mode 100644 index 00000000..922ea364 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/poi_types.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +struct PointOfInterest +{ + PointOfInterest(std::string name, + double latitude, + double longitude) : + name_ {name}, + latitude_ {latitude}, + longitude_ {longitude} + { + } + + std::string name_; + double latitude_; + double longitude_; +}; + +} // namespace types +} // namespace qt +} // namespace scwx From 06a2a18c0670944b513f503f7850c39f27ca012c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 4 Oct 2024 05:40:00 -0500 Subject: [PATCH 101/762] Automatically refresh placefiles that failed to load --- .../scwx/qt/manager/placefile_manager.cpp | 42 +++++++++++++++++-- wxdata/source/scwx/gr/placefile.cpp | 10 ++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index b324fae3..6a0392be 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -105,6 +105,8 @@ public: void CancelRefresh(); void ScheduleRefresh(); + void ScheduleRefresh( + const std::chrono::system_clock::duration timeUntilNextUpdate); void Update(); void UpdateAsync(); @@ -150,6 +152,8 @@ public: std::string lastRadarSite_ {}; std::chrono::system_clock::time_point lastUpdateTime_ {}; + + std::size_t failureCount_ {}; }; PlacefileManager::PlacefileManager() : p(std::make_unique(this)) @@ -542,6 +546,11 @@ void PlacefileManager::Impl::PlacefileRecord::Update() if (url.isLocalFile()) { updatedPlacefile = gr::Placefile::Load(name); + + if (updatedPlacefile == nullptr) + { + logger_->error("Local placefile not found: {}", name); + } } else { @@ -625,6 +634,7 @@ void PlacefileManager::Impl::PlacefileRecord::Update() placefile_ = updatedPlacefile; title_ = placefile_->title(); lastUpdateTime_ = std::chrono::system_clock::now(); + failureCount_ = 0; // Update font resources { @@ -645,10 +655,30 @@ void PlacefileManager::Impl::PlacefileRecord::Update() // Notify slots of the placefile update Q_EMIT p->self_->PlacefileUpdated(name); } - } - // Update refresh timer - ScheduleRefresh(); + // Update refresh timer + ScheduleRefresh(); + } + else if (enabled_) + { + using namespace std::chrono_literals; + + ++failureCount_; + + // Update refresh timer if the file failed to load, in case it is able to + // be resolved later + if (url.isLocalFile()) + { + ScheduleRefresh(10s); + } + else + { + // Start attempting to refresh at 15 seconds, and start backing off + // until retrying every 60 seconds + ScheduleRefresh( + std::min(15s * failureCount_, 60s)); + } + } } void PlacefileManager::Impl::PlacefileRecord::ScheduleRefresh() @@ -666,6 +696,12 @@ void PlacefileManager::Impl::PlacefileRecord::ScheduleRefresh() auto nextUpdateTime = lastUpdateTime_ + refresh_time(); auto timeUntilNextUpdate = nextUpdateTime - std::chrono::system_clock::now(); + ScheduleRefresh(timeUntilNextUpdate); +} + +void PlacefileManager::Impl::PlacefileRecord::ScheduleRefresh( + const std::chrono::system_clock::duration timeUntilNextUpdate) +{ logger_->debug( "Scheduled refresh in {:%M:%S} ({})", std::chrono::duration_cast(timeUntilNextUpdate), diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp index 02bb3527..808ce19c 100644 --- a/wxdata/source/scwx/gr/placefile.cpp +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -154,9 +154,17 @@ std::shared_ptr Placefile::font(std::size_t i) std::shared_ptr Placefile::Load(const std::string& filename) { + std::shared_ptr placefile = nullptr; + logger_->debug("Loading placefile: {}", filename); std::ifstream f(filename, std::ios_base::in); - return Load(filename, f); + + if (f.is_open()) + { + placefile = Load(filename, f); + } + + return placefile; } std::shared_ptr Placefile::Load(const std::string& name, From ec4387112ebb81a6d542056b86e4826574cb4042 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 16:40:56 -0400 Subject: [PATCH 102/762] Add poi layer --- scwx-qt/source/scwx/qt/map/poi_layer.cpp | 97 ++++++++++++++++++++ scwx-qt/source/scwx/qt/map/poi_layer.hpp | 33 +++++++ scwx-qt/source/scwx/qt/types/layer_types.cpp | 1 + scwx-qt/source/scwx/qt/types/layer_types.hpp | 1 + 4 files changed, 132 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/map/poi_layer.cpp create mode 100644 scwx-qt/source/scwx/qt/map/poi_layer.hpp diff --git a/scwx-qt/source/scwx/qt/map/poi_layer.cpp b/scwx-qt/source/scwx/qt/map/poi_layer.cpp new file mode 100644 index 00000000..a1b0e630 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/poi_layer.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +static const std::string logPrefix_ = "scwx::qt::map::poi_layer"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + + +class POILayer::Impl +{ +public: + explicit Impl(std::shared_ptr context) : + geoIcons_ {std::make_shared(context)} + { + } + ~Impl() {} + + void ReloadPOIs(); + + const std::string& poiIconName_ { + types::GetTextureName(types::ImageTexture::Crosshairs24)}; + + std::shared_ptr geoIcons_; +}; + +void POILayer::Impl::ReloadPOIs() +{ + logger_->debug("ReloadPOIs"); + auto poiManager = manager::POIManager::Instance(); + + geoIcons_->StartIcons(); + + for (size_t i = 0; i < poiManager->poi_count(); i++) + { + types::PointOfInterest poi = poiManager->get_poi(i); + std::shared_ptr icon = geoIcons_->AddIcon(); + geoIcons_->SetIconTexture(icon, poiIconName_, 0); + geoIcons_->SetIconLocation(icon, poi.latitude_, poi.longitude_); + } + + geoIcons_->FinishIcons(); +} + +POILayer::POILayer(const std::shared_ptr& context) : + DrawLayer(context), + p(std::make_unique(context)) +{ + AddDrawItem(p->geoIcons_); +} + +POILayer::~POILayer() = default; + +void POILayer::Initialize() +{ + logger_->debug("Initialize()"); + DrawLayer::Initialize(); + + p->geoIcons_->StartIconSheets(); + p->geoIcons_->AddIconSheet(p->poiIconName_); + p->geoIcons_->FinishIconSheets(); +} + +void POILayer::Render( + const QMapLibre::CustomLayerRenderParameters& params) +{ + //auto poiManager = manager::POIManager::Instance(); + gl::OpenGLFunctions& gl = context()->gl(); + + // TODO. do not redo this every time + p->ReloadPOIs(); + + DrawLayer::Render(params); + + SCWX_GL_CHECK_ERROR(); +} + +void POILayer::Deinitialize() +{ + logger_->debug("Deinitialize()"); + + DrawLayer::Deinitialize(); +} + +} // namespace map +} // namespace qt +} // namespace scwx + diff --git a/scwx-qt/source/scwx/qt/map/poi_layer.hpp b/scwx-qt/source/scwx/qt/map/poi_layer.hpp new file mode 100644 index 00000000..5bc12660 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/poi_layer.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +class POILayer : public DrawLayer +{ + Q_OBJECT + +public: + explicit POILayer(const std::shared_ptr& context); + ~POILayer(); + + void Initialize() override final; + void Render(const QMapLibre::CustomLayerRenderParameters&) override final; + void Deinitialize() override final; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace map +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/layer_types.cpp b/scwx-qt/source/scwx/qt/types/layer_types.cpp index 6e66c5d1..3ab775e3 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.cpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.cpp @@ -31,6 +31,7 @@ static const std::unordered_map informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, {InformationLayer::RadarSite, "Radar Sites"}, {InformationLayer::ColorTable, "Color Table"}, + {InformationLayer::POILayer, "Point of Interest"}, {InformationLayer::Unknown, "?"}}; static const std::unordered_map mapLayerName_ { diff --git a/scwx-qt/source/scwx/qt/types/layer_types.hpp b/scwx-qt/source/scwx/qt/types/layer_types.hpp index f0561a6e..04a4aef9 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.hpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.hpp @@ -44,6 +44,7 @@ enum class InformationLayer MapOverlay, RadarSite, ColorTable, + POILayer, Unknown }; From f5d867cf1aef5b2d91a1620bdc9dbad0834f4c89 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 16:42:54 -0400 Subject: [PATCH 103/762] Add working poi manager implementation --- .../source/scwx/qt/manager/poi_manager.cpp | 193 ++++++++++++++++-- .../source/scwx/qt/manager/poi_manager.hpp | 11 + 2 files changed, 191 insertions(+), 13 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.cpp b/scwx-qt/source/scwx/qt/manager/poi_manager.cpp index 658ae4da..9124bed4 100644 --- a/scwx-qt/source/scwx/qt/manager/poi_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/poi_manager.cpp @@ -1,9 +1,14 @@ #include -#include +#include #include +#include +#include #include +#include +#include +#include #include namespace scwx @@ -13,7 +18,7 @@ namespace qt namespace manager { -static const std::string logPrefix_ = "scwx::qt::manager::placefile_manager"; +static const std::string logPrefix_ = "scwx::qt::manager::poi_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string kNameName_ = "name"; @@ -23,22 +28,27 @@ static const std::string kLongitudeName_ = "longitude"; class POIManager::Impl { public: - class PointOfInterest; + class POIRecord; explicit Impl(POIManager* self) : self_ {self} {} + ~Impl() {} std::string poiSettingsPath_ {}; + std::vector> poiRecords_ {}; POIManager* self_; + void InitializePOISettings(); void ReadPOISettings(); + void WritePOISettings(); + std::shared_ptr GetPOIByName(const std::string& name); }; -class POIManager::Impl::PointOfInterest +class POIManager::Impl::POIRecord { public: - PointOfInterest(std::string name, double latitude, double longitude) : + POIRecord(std::string name, double latitude, double longitude) : name_ {name}, latitude_ {latitude}, longitude_ {longitude} { } @@ -49,28 +59,45 @@ public: friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, - const std::shared_ptr& record) + const std::shared_ptr& record) { jv = {{kNameName_, record->name_}, {kLatitudeName_, record->latitude_}, {kLongitudeName_, record->longitude_}}; } - friend PointOfInterest tag_invoke(boost::json::value_to_tag, + friend POIRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - return PointOfInterest( + return POIRecord( boost::json::value_to(jv.at(kNameName_)), boost::json::value_to(jv.at(kLatitudeName_)), boost::json::value_to(jv.at(kLongitudeName_))); } }; -POIManager::POIManager() : p(std::make_unique(this)) {} + +void POIManager::Impl::InitializePOISettings() +{ + 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); + } + } + + poiSettingsPath_ = appDataPath + "/points_of_interest.json"; +} void POIManager::Impl::ReadPOISettings() { - logger_->info("Reading point of intrest settings"); + logger_->info("Reading point of interest settings"); boost::json::value poiJson = nullptr; @@ -84,16 +111,18 @@ void POIManager::Impl::ReadPOISettings() { // For each poi entry auto& poiArray = poiJson.as_array(); + poiRecords_.reserve(poiArray.size()); for (auto& poiEntry : poiArray) { try { - PointOfInterest record = - boost::json::value_to(poiEntry); + POIRecord record = + boost::json::value_to(poiEntry); if (!record.name_.empty()) { - // Add record + poiRecords_.emplace_back(std::make_shared( + record.name_, record.latitude_, record.longitude_)); } } catch (const std::exception& ex) @@ -101,9 +130,147 @@ void POIManager::Impl::ReadPOISettings() logger_->warn("Invalid point of interest entry: {}", ex.what()); } } + + logger_->debug("{} point of interest entries", poiRecords_.size()); } } +void POIManager::Impl::WritePOISettings() +{ + logger_->info("Saving point of interest settings"); + + auto poiJson = boost::json::value_from(poiRecords_); + util::json::WriteJsonFile(poiSettingsPath_, poiJson); +} + +std::shared_ptr +POIManager::Impl::GetPOIByName(const std::string& name) +{ + for (auto& poiRecord : poiRecords_) + { + if (poiRecord->name_ == name) + { + return poiRecord; + } + } + + return nullptr; +} + +POIManager::POIManager() : p(std::make_unique(this)) +{ + // TODO THREADING? + try + { + p->InitializePOISettings(); + + // Read POI settings on startup + //main::Application::WaitForInitialization(); + p->ReadPOISettings(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } +} + +POIManager::~POIManager() +{ + p->WritePOISettings(); +} + +size_t POIManager::poi_count() +{ + return p->poiRecords_.size(); +} + +// TODO deal with out of range/not found +types::PointOfInterest POIManager::get_poi(size_t index) +{ + std::shared_ptr poiRecord = + p->poiRecords_[index]; + return types::PointOfInterest( + poiRecord->name_, poiRecord->latitude_, poiRecord->longitude_); +} + +types::PointOfInterest POIManager::get_poi(const std::string& name) +{ + std::shared_ptr poiRecord = + p->GetPOIByName(name); + return types::PointOfInterest( + poiRecord->name_, poiRecord->latitude_, poiRecord->longitude_); +} + +void POIManager::set_poi(size_t index, const types::PointOfInterest& poi) +{ + std::shared_ptr poiRecord = + p->poiRecords_[index]; + poiRecord->name_ = poi.name_; + poiRecord->latitude_ = poi.latitude_; + poiRecord->longitude_ = poi.longitude_; +} + +void POIManager::set_poi(const std::string& name, + const types::PointOfInterest& poi) +{ + std::shared_ptr poiRecord = + p->GetPOIByName(name); + poiRecord->name_ = poi.name_; + poiRecord->latitude_ = poi.latitude_; + poiRecord->longitude_ = poi.longitude_; +} + +void POIManager::add_poi(const types::PointOfInterest& poi) +{ + p->poiRecords_.emplace_back(std::make_shared( + poi.name_, poi.latitude_, poi.longitude_)); +} + +void POIManager::move_poi(size_t from, size_t to) +{ + if (from >= p->poiRecords_.size() || to >= p->poiRecords_.size()) + { + return; + } + std::shared_ptr poiRecord = + p->poiRecords_[from]; + + if (from == to) + { + } + else if (from < to) + { + for (size_t i = from; i < to; i++) + { + p->poiRecords_[i] = p->poiRecords_[i + 1]; + } + p->poiRecords_[to] = poiRecord; + } + else + { + for (size_t i = from; i > to; i--) + { + p->poiRecords_[i] = p->poiRecords_[i - 1]; + } + p->poiRecords_[to] = poiRecord; + } +} + +std::shared_ptr POIManager::Instance() +{ + static std::weak_ptr poiManagerReference_ {}; + + std::shared_ptr poiManager = poiManagerReference_.lock(); + + if (poiManager == nullptr) + { + poiManager = std::make_shared(); + poiManagerReference_ = poiManager; + } + + return poiManager; +} + } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.hpp b/scwx-qt/source/scwx/qt/manager/poi_manager.hpp index b0bc574d..63144cc8 100644 --- a/scwx-qt/source/scwx/qt/manager/poi_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/poi_manager.hpp @@ -2,6 +2,8 @@ #include +#include + #include namespace scwx @@ -19,6 +21,15 @@ public: explicit POIManager(); ~POIManager(); + size_t poi_count(); + types::PointOfInterest get_poi(size_t index); + types::PointOfInterest get_poi(const std::string& name); + void set_poi(size_t index, const types::PointOfInterest& poi); + void set_poi(const std::string& name, const types::PointOfInterest& poi); + void add_poi(const types::PointOfInterest& poi); + void move_poi(size_t from, size_t to); + + static std::shared_ptr Instance(); private: class Impl; From 0a0989e5f43701250d819a122a170d430bac87c5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 16:43:40 -0400 Subject: [PATCH 104/762] Do initial work to get layer rendering --- scwx-qt/scwx-qt.cmake | 5 +++++ scwx-qt/source/scwx/qt/main/main_window.cpp | 3 +++ scwx-qt/source/scwx/qt/map/map_widget.cpp | 9 +++++++++ scwx-qt/source/scwx/qt/model/layer_model.cpp | 1 + 4 files changed, 18 insertions(+) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 06646bbe..01ff239e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -95,6 +95,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/log_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp + source/scwx/qt/manager/poi_manager.hpp source/scwx/qt/manager/position_manager.hpp source/scwx/qt/manager/radar_product_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp @@ -111,6 +112,7 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp source/scwx/qt/manager/log_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp + source/scwx/qt/manager/poi_manager.cpp source/scwx/qt/manager/position_manager.cpp source/scwx/qt/manager/radar_product_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp @@ -132,6 +134,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp source/scwx/qt/map/overlay_layer.hpp source/scwx/qt/map/overlay_product_layer.hpp source/scwx/qt/map/placefile_layer.hpp + source/scwx/qt/map/poi_layer.hpp source/scwx/qt/map/radar_product_layer.hpp source/scwx/qt/map/radar_range_layer.hpp source/scwx/qt/map/radar_site_layer.hpp) @@ -146,6 +149,7 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp source/scwx/qt/map/overlay_layer.cpp source/scwx/qt/map/overlay_product_layer.cpp source/scwx/qt/map/placefile_layer.cpp + source/scwx/qt/map/poi_layer.cpp source/scwx/qt/map/radar_product_layer.cpp source/scwx/qt/map/radar_range_layer.cpp source/scwx/qt/map/radar_site_layer.cpp) @@ -215,6 +219,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_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/poi_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 diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index cadd9c90..77c130a4 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,7 @@ public: updateDialog_ {nullptr}, alertManager_ {manager::AlertManager::Instance()}, placefileManager_ {manager::PlacefileManager::Instance()}, + poiManager_ {manager::POIManager::Instance()}, positionManager_ {manager::PositionManager::Instance()}, textEventManager_ {manager::TextEventManager::Instance()}, timelineManager_ {manager::TimelineManager::Instance()}, @@ -217,6 +219,7 @@ public: std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; std::shared_ptr placefileManager_; + std::shared_ptr poiManager_; std::shared_ptr positionManager_; std::shared_ptr textEventManager_; std::shared_ptr timelineManager_; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index e718bc0e..4837f387 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,7 @@ public: radarProductLayer_ {nullptr}, overlayLayer_ {nullptr}, placefileLayer_ {nullptr}, + poiLayer_ {nullptr}, colorTableLayer_ {nullptr}, autoRefreshEnabled_ {true}, autoUpdateEnabled_ {true}, @@ -223,6 +225,7 @@ public: std::shared_ptr overlayLayer_; std::shared_ptr overlayProductLayer_ {nullptr}; std::shared_ptr placefileLayer_; + std::shared_ptr poiLayer_; std::shared_ptr colorTableLayer_; std::shared_ptr radarSiteLayer_ {nullptr}; @@ -1232,6 +1235,12 @@ void MapWidgetImpl::AddLayer(types::LayerType type, { widget_->RadarSiteRequested(id); }); break; + // Create the radar site layer + case types::InformationLayer::POILayer: + poiLayer_ = std::make_shared(context_); + AddLayer(layerName, poiLayer_, before); + break; + default: break; } diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 999a2de9..fbc565b5 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -43,6 +43,7 @@ static const std::vector kDefaultLayers_ { types::InformationLayer::RadarSite, false, {false, false, false, false}}, + {types::LayerType::Information, types::InformationLayer::POILayer, true}, {types::LayerType::Data, types::DataLayer::RadarRange, true}, {types::LayerType::Alert, awips::Phenomenon::Tornado, true}, {types::LayerType::Alert, awips::Phenomenon::SnowSquall, true}, From 27828943f425d4e2973066bcd97abc63abe47c7e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 19:02:04 -0400 Subject: [PATCH 105/762] change texture for marker and remove unneeded logging --- scwx-qt/source/scwx/qt/map/poi_layer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/poi_layer.cpp b/scwx-qt/source/scwx/qt/map/poi_layer.cpp index a1b0e630..4bfb4338 100644 --- a/scwx-qt/source/scwx/qt/map/poi_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/poi_layer.cpp @@ -28,14 +28,13 @@ public: void ReloadPOIs(); const std::string& poiIconName_ { - types::GetTextureName(types::ImageTexture::Crosshairs24)}; + types::GetTextureName(types::ImageTexture::Cursor17)}; std::shared_ptr geoIcons_; }; void POILayer::Impl::ReloadPOIs() { - logger_->debug("ReloadPOIs"); auto poiManager = manager::POIManager::Instance(); geoIcons_->StartIcons(); From da271c326a660fbd7034484a926bf87046fb2abd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 19:05:37 -0400 Subject: [PATCH 106/762] rename files to marker instead of poi --- .../scwx/qt/manager/{poi_manager.cpp => marker_manager.cpp} | 0 .../scwx/qt/manager/{poi_manager.hpp => marker_manager.hpp} | 0 scwx-qt/source/scwx/qt/types/{poi_types.hpp => marker_types.hpp} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename scwx-qt/source/scwx/qt/manager/{poi_manager.cpp => marker_manager.cpp} (100%) rename scwx-qt/source/scwx/qt/manager/{poi_manager.hpp => marker_manager.hpp} (100%) rename scwx-qt/source/scwx/qt/types/{poi_types.hpp => marker_types.hpp} (100%) diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp similarity index 100% rename from scwx-qt/source/scwx/qt/manager/poi_manager.cpp rename to scwx-qt/source/scwx/qt/manager/marker_manager.cpp diff --git a/scwx-qt/source/scwx/qt/manager/poi_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/manager/poi_manager.hpp rename to scwx-qt/source/scwx/qt/manager/marker_manager.hpp diff --git a/scwx-qt/source/scwx/qt/types/poi_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/types/poi_types.hpp rename to scwx-qt/source/scwx/qt/types/marker_types.hpp From cd169026354806c4bdde7d81899d0b5641984103 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 19:10:34 -0400 Subject: [PATCH 107/762] rename another file to marker fro poi --- scwx-qt/source/scwx/qt/map/{poi_layer.cpp => marker_layer.cpp} | 0 scwx-qt/source/scwx/qt/map/{poi_layer.hpp => marker_layer.hpp} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scwx-qt/source/scwx/qt/map/{poi_layer.cpp => marker_layer.cpp} (100%) rename scwx-qt/source/scwx/qt/map/{poi_layer.hpp => marker_layer.hpp} (100%) diff --git a/scwx-qt/source/scwx/qt/map/poi_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp similarity index 100% rename from scwx-qt/source/scwx/qt/map/poi_layer.cpp rename to scwx-qt/source/scwx/qt/map/marker_layer.cpp diff --git a/scwx-qt/source/scwx/qt/map/poi_layer.hpp b/scwx-qt/source/scwx/qt/map/marker_layer.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/map/poi_layer.hpp rename to scwx-qt/source/scwx/qt/map/marker_layer.hpp From 31940441edd9444ca1e5cbaf48944bda3464c17b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Oct 2024 19:31:47 -0400 Subject: [PATCH 108/762] renamed all POI/point of intrest to marker --- scwx-qt/scwx-qt.cmake | 10 +- scwx-qt/source/scwx/qt/main/main_window.cpp | 6 +- .../source/scwx/qt/manager/marker_manager.cpp | 188 +++++++++--------- .../source/scwx/qt/manager/marker_manager.hpp | 24 +-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 12 +- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 44 ++-- scwx-qt/source/scwx/qt/map/marker_layer.hpp | 6 +- scwx-qt/source/scwx/qt/model/layer_model.cpp | 2 +- scwx-qt/source/scwx/qt/types/layer_types.cpp | 2 +- scwx-qt/source/scwx/qt/types/layer_types.hpp | 2 +- scwx-qt/source/scwx/qt/types/marker_types.hpp | 14 +- 11 files changed, 153 insertions(+), 157 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 01ff239e..6edb022d 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -95,7 +95,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/log_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp - source/scwx/qt/manager/poi_manager.hpp + source/scwx/qt/manager/marker_manager.hpp source/scwx/qt/manager/position_manager.hpp source/scwx/qt/manager/radar_product_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp @@ -112,7 +112,7 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp source/scwx/qt/manager/log_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp - source/scwx/qt/manager/poi_manager.cpp + source/scwx/qt/manager/marker_manager.cpp source/scwx/qt/manager/position_manager.cpp source/scwx/qt/manager/radar_product_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp @@ -134,7 +134,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp source/scwx/qt/map/overlay_layer.hpp source/scwx/qt/map/overlay_product_layer.hpp source/scwx/qt/map/placefile_layer.hpp - source/scwx/qt/map/poi_layer.hpp + source/scwx/qt/map/marker_layer.hpp source/scwx/qt/map/radar_product_layer.hpp source/scwx/qt/map/radar_range_layer.hpp source/scwx/qt/map/radar_site_layer.hpp) @@ -149,7 +149,7 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp source/scwx/qt/map/overlay_layer.cpp source/scwx/qt/map/overlay_product_layer.cpp source/scwx/qt/map/placefile_layer.cpp - source/scwx/qt/map/poi_layer.cpp + source/scwx/qt/map/marker_layer.cpp source/scwx/qt/map/radar_product_layer.cpp source/scwx/qt/map/radar_range_layer.cpp source/scwx/qt/map/radar_site_layer.cpp) @@ -219,7 +219,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_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/poi_types.hpp + source/scwx/qt/types/marker_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 diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 77c130a4..836d6784 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include @@ -92,7 +92,7 @@ public: updateDialog_ {nullptr}, alertManager_ {manager::AlertManager::Instance()}, placefileManager_ {manager::PlacefileManager::Instance()}, - poiManager_ {manager::POIManager::Instance()}, + markerManager_ {manager::MarkerManager::Instance()}, positionManager_ {manager::PositionManager::Instance()}, textEventManager_ {manager::TextEventManager::Instance()}, timelineManager_ {manager::TimelineManager::Instance()}, @@ -219,7 +219,7 @@ public: std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; std::shared_ptr placefileManager_; - std::shared_ptr poiManager_; + std::shared_ptr markerManager_; std::shared_ptr positionManager_; std::shared_ptr textEventManager_; std::shared_ptr timelineManager_; diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 9124bed4..15e8f044 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -1,5 +1,5 @@ -#include -#include +#include +#include #include #include #include @@ -18,37 +18,37 @@ namespace qt namespace manager { -static const std::string logPrefix_ = "scwx::qt::manager::poi_manager"; +static const std::string logPrefix_ = "scwx::qt::manager::marker_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string kNameName_ = "name"; static const std::string kLatitudeName_ = "latitude"; static const std::string kLongitudeName_ = "longitude"; -class POIManager::Impl +class MarkerManager::Impl { public: - class POIRecord; + class MarkerRecord; - explicit Impl(POIManager* self) : self_ {self} {} + explicit Impl(MarkerManager* self) : self_ {self} {} ~Impl() {} - std::string poiSettingsPath_ {}; - std::vector> poiRecords_ {}; + std::string markerSettingsPath_ {}; + std::vector> markerRecords_ {}; - POIManager* self_; + MarkerManager* self_; - void InitializePOISettings(); - void ReadPOISettings(); - void WritePOISettings(); - std::shared_ptr GetPOIByName(const std::string& name); + void InitializeMarkerSettings(); + void ReadMarkerSettings(); + void WriteMarkerSettings(); + std::shared_ptr GetMarkerByName(const std::string& name); }; -class POIManager::Impl::POIRecord +class MarkerManager::Impl::MarkerRecord { public: - POIRecord(std::string name, double latitude, double longitude) : + MarkerRecord(std::string name, double latitude, double longitude) : name_ {name}, latitude_ {latitude}, longitude_ {longitude} { } @@ -59,17 +59,17 @@ public: friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, - const std::shared_ptr& record) + const std::shared_ptr& record) { jv = {{kNameName_, record->name_}, {kLatitudeName_, record->latitude_}, {kLongitudeName_, record->longitude_}}; } - friend POIRecord tag_invoke(boost::json::value_to_tag, + friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - return POIRecord( + return MarkerRecord( boost::json::value_to(jv.at(kNameName_)), boost::json::value_to(jv.at(kLatitudeName_)), boost::json::value_to(jv.at(kLongitudeName_))); @@ -77,7 +77,7 @@ public: }; -void POIManager::Impl::InitializePOISettings() +void MarkerManager::Impl::InitializeMarkerSettings() { std::string appDataPath { QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) @@ -92,81 +92,81 @@ void POIManager::Impl::InitializePOISettings() } } - poiSettingsPath_ = appDataPath + "/points_of_interest.json"; + markerSettingsPath_ = appDataPath + "/location-markers.json"; } -void POIManager::Impl::ReadPOISettings() +void MarkerManager::Impl::ReadMarkerSettings() { - logger_->info("Reading point of interest settings"); + logger_->info("Reading location marker settings"); - boost::json::value poiJson = nullptr; + boost::json::value markerJson = nullptr; - // Determine if poi settings exists - if (std::filesystem::exists(poiSettingsPath_)) + // Determine if marker settings exists + if (std::filesystem::exists(markerSettingsPath_)) { - poiJson = util::json::ReadJsonFile(poiSettingsPath_); + markerJson = util::json::ReadJsonFile(markerSettingsPath_); } - if (poiJson != nullptr && poiJson.is_array()) + if (markerJson != nullptr && markerJson.is_array()) { - // For each poi entry - auto& poiArray = poiJson.as_array(); - poiRecords_.reserve(poiArray.size()); - for (auto& poiEntry : poiArray) + // For each marker entry + auto& markerArray = markerJson.as_array(); + markerRecords_.reserve(markerArray.size()); + for (auto& markerEntry : markerArray) { try { - POIRecord record = - boost::json::value_to(poiEntry); + MarkerRecord record = + boost::json::value_to(markerEntry); if (!record.name_.empty()) { - poiRecords_.emplace_back(std::make_shared( + markerRecords_.emplace_back(std::make_shared( record.name_, record.latitude_, record.longitude_)); } } catch (const std::exception& ex) { - logger_->warn("Invalid point of interest entry: {}", ex.what()); + logger_->warn("Invalid location marker entry: {}", ex.what()); } } - logger_->debug("{} point of interest entries", poiRecords_.size()); + logger_->debug("{} location marker entries", markerRecords_.size()); } } -void POIManager::Impl::WritePOISettings() +void MarkerManager::Impl::WriteMarkerSettings() { - logger_->info("Saving point of interest settings"); + logger_->info("Saving location marker settings"); - auto poiJson = boost::json::value_from(poiRecords_); - util::json::WriteJsonFile(poiSettingsPath_, poiJson); + auto markerJson = boost::json::value_from(markerRecords_); + util::json::WriteJsonFile(markerSettingsPath_, markerJson); } -std::shared_ptr -POIManager::Impl::GetPOIByName(const std::string& name) +std::shared_ptr +MarkerManager::Impl::GetMarkerByName(const std::string& name) { - for (auto& poiRecord : poiRecords_) + for (auto& markerRecord : markerRecords_) { - if (poiRecord->name_ == name) + if (markerRecord->name_ == name) { - return poiRecord; + return markerRecord; } } return nullptr; } -POIManager::POIManager() : p(std::make_unique(this)) +MarkerManager::MarkerManager() : p(std::make_unique(this)) { // TODO THREADING? try { - p->InitializePOISettings(); + p->InitializeMarkerSettings(); - // Read POI settings on startup + // Read Marker settings on startup //main::Application::WaitForInitialization(); - p->ReadPOISettings(); + p->ReadMarkerSettings(); } catch (const std::exception& ex) { @@ -174,66 +174,66 @@ POIManager::POIManager() : p(std::make_unique(this)) } } -POIManager::~POIManager() +MarkerManager::~MarkerManager() { - p->WritePOISettings(); + p->WriteMarkerSettings(); } -size_t POIManager::poi_count() +size_t MarkerManager::marker_count() { - return p->poiRecords_.size(); + return p->markerRecords_.size(); } // TODO deal with out of range/not found -types::PointOfInterest POIManager::get_poi(size_t index) +types::MarkerInfo MarkerManager::get_marker(size_t index) { - std::shared_ptr poiRecord = - p->poiRecords_[index]; - return types::PointOfInterest( - poiRecord->name_, poiRecord->latitude_, poiRecord->longitude_); + std::shared_ptr markerRecord = + p->markerRecords_[index]; + return types::MarkerInfo( + markerRecord->name_, markerRecord->latitude_, markerRecord->longitude_); } -types::PointOfInterest POIManager::get_poi(const std::string& name) +types::MarkerInfo MarkerManager::get_marker(const std::string& name) { - std::shared_ptr poiRecord = - p->GetPOIByName(name); - return types::PointOfInterest( - poiRecord->name_, poiRecord->latitude_, poiRecord->longitude_); + std::shared_ptr markerRecord = + p->GetMarkerByName(name); + return types::MarkerInfo( + markerRecord->name_, markerRecord->latitude_, markerRecord->longitude_); } -void POIManager::set_poi(size_t index, const types::PointOfInterest& poi) +void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) { - std::shared_ptr poiRecord = - p->poiRecords_[index]; - poiRecord->name_ = poi.name_; - poiRecord->latitude_ = poi.latitude_; - poiRecord->longitude_ = poi.longitude_; + std::shared_ptr markerRecord = + p->markerRecords_[index]; + markerRecord->name_ = marker.name_; + markerRecord->latitude_ = marker.latitude_; + markerRecord->longitude_ = marker.longitude_; } -void POIManager::set_poi(const std::string& name, - const types::PointOfInterest& poi) +void MarkerManager::set_marker(const std::string& name, + const types::MarkerInfo& marker) { - std::shared_ptr poiRecord = - p->GetPOIByName(name); - poiRecord->name_ = poi.name_; - poiRecord->latitude_ = poi.latitude_; - poiRecord->longitude_ = poi.longitude_; + std::shared_ptr markerRecord = + p->GetMarkerByName(name); + markerRecord->name_ = marker.name_; + markerRecord->latitude_ = marker.latitude_; + markerRecord->longitude_ = marker.longitude_; } -void POIManager::add_poi(const types::PointOfInterest& poi) +void MarkerManager::add_marker(const types::MarkerInfo& marker) { - p->poiRecords_.emplace_back(std::make_shared( - poi.name_, poi.latitude_, poi.longitude_)); + p->markerRecords_.emplace_back(std::make_shared( + marker.name_, marker.latitude_, marker.longitude_)); } -void POIManager::move_poi(size_t from, size_t to) +void MarkerManager::move_marker(size_t from, size_t to) { - if (from >= p->poiRecords_.size() || to >= p->poiRecords_.size()) + if (from >= p->markerRecords_.size() || to >= p->markerRecords_.size()) { return; } - std::shared_ptr poiRecord = - p->poiRecords_[from]; + std::shared_ptr markerRecord = + p->markerRecords_[from]; if (from == to) { @@ -242,33 +242,33 @@ void POIManager::move_poi(size_t from, size_t to) { for (size_t i = from; i < to; i++) { - p->poiRecords_[i] = p->poiRecords_[i + 1]; + p->markerRecords_[i] = p->markerRecords_[i + 1]; } - p->poiRecords_[to] = poiRecord; + p->markerRecords_[to] = markerRecord; } else { for (size_t i = from; i > to; i--) { - p->poiRecords_[i] = p->poiRecords_[i - 1]; + p->markerRecords_[i] = p->markerRecords_[i - 1]; } - p->poiRecords_[to] = poiRecord; + p->markerRecords_[to] = markerRecord; } } -std::shared_ptr POIManager::Instance() +std::shared_ptr MarkerManager::Instance() { - static std::weak_ptr poiManagerReference_ {}; + static std::weak_ptr markerManagerReference_ {}; - std::shared_ptr poiManager = poiManagerReference_.lock(); + std::shared_ptr markerManager = markerManagerReference_.lock(); - if (poiManager == nullptr) + if (markerManager == nullptr) { - poiManager = std::make_shared(); - poiManagerReference_ = poiManager; + markerManager = std::make_shared(); + markerManagerReference_ = markerManager; } - return poiManager; + return markerManager; } } // namespace manager diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 63144cc8..55631d6e 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include @@ -13,23 +13,23 @@ namespace qt namespace manager { -class POIManager : public QObject +class MarkerManager : public QObject { Q_OBJECT public: - explicit POIManager(); - ~POIManager(); + explicit MarkerManager(); + ~MarkerManager(); - size_t poi_count(); - types::PointOfInterest get_poi(size_t index); - types::PointOfInterest get_poi(const std::string& name); - void set_poi(size_t index, const types::PointOfInterest& poi); - void set_poi(const std::string& name, const types::PointOfInterest& poi); - void add_poi(const types::PointOfInterest& poi); - void move_poi(size_t from, size_t to); + size_t marker_count(); + types::MarkerInfo get_marker(size_t index); + types::MarkerInfo get_marker(const std::string& name); + void set_marker(size_t index, const types::MarkerInfo& marker); + void set_marker(const std::string& name, const types::MarkerInfo& marker); + void add_marker(const types::MarkerInfo& marker); + void move_marker(size_t from, size_t to); - static std::shared_ptr Instance(); + static std::shared_ptr Instance(); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 4837f387..098a6378 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include @@ -82,7 +82,7 @@ public: radarProductLayer_ {nullptr}, overlayLayer_ {nullptr}, placefileLayer_ {nullptr}, - poiLayer_ {nullptr}, + markerLayer_ {nullptr}, colorTableLayer_ {nullptr}, autoRefreshEnabled_ {true}, autoUpdateEnabled_ {true}, @@ -225,7 +225,7 @@ public: std::shared_ptr overlayLayer_; std::shared_ptr overlayProductLayer_ {nullptr}; std::shared_ptr placefileLayer_; - std::shared_ptr poiLayer_; + std::shared_ptr markerLayer_; std::shared_ptr colorTableLayer_; std::shared_ptr radarSiteLayer_ {nullptr}; @@ -1236,9 +1236,9 @@ void MapWidgetImpl::AddLayer(types::LayerType type, break; // Create the radar site layer - case types::InformationLayer::POILayer: - poiLayer_ = std::make_shared(context_); - AddLayer(layerName, poiLayer_, before); + case types::InformationLayer::Markers: + markerLayer_ = std::make_shared(context_); + AddLayer(layerName, markerLayer_, before); break; default: diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 4bfb4338..f899f55d 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -1,7 +1,7 @@ -#include -#include +#include +#include #include -#include +#include #include #include @@ -12,11 +12,11 @@ namespace qt namespace map { -static const std::string logPrefix_ = "scwx::qt::map::poi_layer"; +static const std::string logPrefix_ = "scwx::qt::map::marker_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class POILayer::Impl +class MarkerLayer::Impl { public: explicit Impl(std::shared_ptr context) : @@ -25,65 +25,65 @@ public: } ~Impl() {} - void ReloadPOIs(); + void ReloadMarkers(); - const std::string& poiIconName_ { + const std::string& markerIconName_ { types::GetTextureName(types::ImageTexture::Cursor17)}; std::shared_ptr geoIcons_; }; -void POILayer::Impl::ReloadPOIs() +void MarkerLayer::Impl::ReloadMarkers() { - auto poiManager = manager::POIManager::Instance(); + auto markerManager = manager::MarkerManager::Instance(); geoIcons_->StartIcons(); - for (size_t i = 0; i < poiManager->poi_count(); i++) + for (size_t i = 0; i < markerManager->marker_count(); i++) { - types::PointOfInterest poi = poiManager->get_poi(i); + types::MarkerInfo marker = markerManager->get_marker(i); std::shared_ptr icon = geoIcons_->AddIcon(); - geoIcons_->SetIconTexture(icon, poiIconName_, 0); - geoIcons_->SetIconLocation(icon, poi.latitude_, poi.longitude_); + geoIcons_->SetIconTexture(icon, markerIconName_, 0); + geoIcons_->SetIconLocation(icon, marker.latitude_, marker.longitude_); } geoIcons_->FinishIcons(); } -POILayer::POILayer(const std::shared_ptr& context) : +MarkerLayer::MarkerLayer(const std::shared_ptr& context) : DrawLayer(context), - p(std::make_unique(context)) + p(std::make_unique(context)) { AddDrawItem(p->geoIcons_); } -POILayer::~POILayer() = default; +MarkerLayer::~MarkerLayer() = default; -void POILayer::Initialize() +void MarkerLayer::Initialize() { logger_->debug("Initialize()"); DrawLayer::Initialize(); p->geoIcons_->StartIconSheets(); - p->geoIcons_->AddIconSheet(p->poiIconName_); + p->geoIcons_->AddIconSheet(p->markerIconName_); p->geoIcons_->FinishIconSheets(); } -void POILayer::Render( +void MarkerLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { - //auto poiManager = manager::POIManager::Instance(); + //auto markerManager = manager::MarkerManager::Instance(); gl::OpenGLFunctions& gl = context()->gl(); // TODO. do not redo this every time - p->ReloadPOIs(); + p->ReloadMarkers(); DrawLayer::Render(params); SCWX_GL_CHECK_ERROR(); } -void POILayer::Deinitialize() +void MarkerLayer::Deinitialize() { logger_->debug("Deinitialize()"); diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.hpp b/scwx-qt/source/scwx/qt/map/marker_layer.hpp index 5bc12660..9cd0674c 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.hpp @@ -11,13 +11,13 @@ namespace qt namespace map { -class POILayer : public DrawLayer +class MarkerLayer : public DrawLayer { Q_OBJECT public: - explicit POILayer(const std::shared_ptr& context); - ~POILayer(); + explicit MarkerLayer(const std::shared_ptr& context); + ~MarkerLayer(); void Initialize() override final; void Render(const QMapLibre::CustomLayerRenderParameters&) override final; diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index fbc565b5..23d05cd6 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -43,7 +43,7 @@ static const std::vector kDefaultLayers_ { types::InformationLayer::RadarSite, false, {false, false, false, false}}, - {types::LayerType::Information, types::InformationLayer::POILayer, true}, + {types::LayerType::Information, types::InformationLayer::Markers, true}, {types::LayerType::Data, types::DataLayer::RadarRange, true}, {types::LayerType::Alert, awips::Phenomenon::Tornado, true}, {types::LayerType::Alert, awips::Phenomenon::SnowSquall, true}, diff --git a/scwx-qt/source/scwx/qt/types/layer_types.cpp b/scwx-qt/source/scwx/qt/types/layer_types.cpp index 3ab775e3..bd607cc7 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.cpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.cpp @@ -31,7 +31,7 @@ static const std::unordered_map informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, {InformationLayer::RadarSite, "Radar Sites"}, {InformationLayer::ColorTable, "Color Table"}, - {InformationLayer::POILayer, "Point of Interest"}, + {InformationLayer::Markers, "Location Markers"}, {InformationLayer::Unknown, "?"}}; static const std::unordered_map mapLayerName_ { diff --git a/scwx-qt/source/scwx/qt/types/layer_types.hpp b/scwx-qt/source/scwx/qt/types/layer_types.hpp index 04a4aef9..bfc10839 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.hpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.hpp @@ -44,7 +44,7 @@ enum class InformationLayer MapOverlay, RadarSite, ColorTable, - POILayer, + Markers, Unknown }; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 922ea364..1fd02111 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -9,20 +9,16 @@ namespace qt namespace types { -struct PointOfInterest +struct MarkerInfo { - PointOfInterest(std::string name, - double latitude, - double longitude) : - name_ {name}, - latitude_ {latitude}, - longitude_ {longitude} + MarkerInfo(std::string name, double latitude, double longitude) : + name_ {name}, latitude_ {latitude}, longitude_ {longitude} { } std::string name_; - double latitude_; - double longitude_; + double latitude_; + double longitude_; }; } // namespace types From 621cbb3d5169531684efdb8bae5c6f4ab65c3da5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Oct 2024 04:11:28 -0500 Subject: [PATCH 109/762] Refresh placefiles as often as every 1 second --- scwx-qt/source/scwx/qt/manager/placefile_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index 6a0392be..58049fc6 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -348,8 +348,8 @@ PlacefileManager::Impl::PlacefileRecord::refresh_time() const if (refresh_enabled()) { - // Don't refresh more often than every 15 seconds - return std::max(placefile_->refresh(), 15s); + // Don't refresh more often than every 1 second + return std::max(placefile_->refresh(), 1s); } return -1s; From 74f3a15eb2fe127fd9b0c15d92af4b4c7390158d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 5 Oct 2024 09:24:14 -0400 Subject: [PATCH 110/762] Reformat marker formats after rename --- .../source/scwx/qt/manager/marker_manager.cpp | 29 +++++++++---------- .../source/scwx/qt/manager/marker_manager.hpp | 4 +-- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 12 +++----- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 15e8f044..e45f4458 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -33,16 +33,15 @@ public: explicit Impl(MarkerManager* self) : self_ {self} {} ~Impl() {} - std::string markerSettingsPath_ {}; + std::string markerSettingsPath_ {}; std::vector> markerRecords_ {}; MarkerManager* self_; - void InitializeMarkerSettings(); - void ReadMarkerSettings(); - void WriteMarkerSettings(); + void InitializeMarkerSettings(); + void ReadMarkerSettings(); + void WriteMarkerSettings(); std::shared_ptr GetMarkerByName(const std::string& name); - }; class MarkerManager::Impl::MarkerRecord @@ -58,7 +57,7 @@ public: double longitude_; friend void tag_invoke(boost::json::value_from_tag, - boost::json::value& jv, + boost::json::value& jv, const std::shared_ptr& record) { jv = {{kNameName_, record->name_}, @@ -67,7 +66,7 @@ public: } friend MarkerRecord tag_invoke(boost::json::value_to_tag, - const boost::json::value& jv) + const boost::json::value& jv) { return MarkerRecord( boost::json::value_to(jv.at(kNameName_)), @@ -76,7 +75,6 @@ public: } }; - void MarkerManager::Impl::InitializeMarkerSettings() { std::string appDataPath { @@ -165,7 +163,7 @@ MarkerManager::MarkerManager() : p(std::make_unique(this)) p->InitializeMarkerSettings(); // Read Marker settings on startup - //main::Application::WaitForInitialization(); + // main::Application::WaitForInitialization(); p->ReadMarkerSettings(); } catch (const std::exception& ex) @@ -210,8 +208,8 @@ void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) markerRecord->longitude_ = marker.longitude_; } -void MarkerManager::set_marker(const std::string& name, - const types::MarkerInfo& marker) +void MarkerManager::set_marker(const std::string& name, + const types::MarkerInfo& marker) { std::shared_ptr markerRecord = p->GetMarkerByName(name); @@ -235,9 +233,7 @@ void MarkerManager::move_marker(size_t from, size_t to) std::shared_ptr markerRecord = p->markerRecords_[from]; - if (from == to) - { - } + if (from == to) {} else if (from < to) { for (size_t i = from; i < to; i++) @@ -260,11 +256,12 @@ std::shared_ptr MarkerManager::Instance() { static std::weak_ptr markerManagerReference_ {}; - std::shared_ptr markerManager = markerManagerReference_.lock(); + std::shared_ptr markerManager = + markerManagerReference_.lock(); if (markerManager == nullptr) { - markerManager = std::make_shared(); + markerManager = std::make_shared(); markerManagerReference_ = markerManager; } diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 55631d6e..4fa5639c 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -21,10 +21,10 @@ public: explicit MarkerManager(); ~MarkerManager(); - size_t marker_count(); + size_t marker_count(); types::MarkerInfo get_marker(size_t index); types::MarkerInfo get_marker(const std::string& name); - void set_marker(size_t index, const types::MarkerInfo& marker); + void set_marker(size_t index, const types::MarkerInfo& marker); void set_marker(const std::string& name, const types::MarkerInfo& marker); void add_marker(const types::MarkerInfo& marker); void move_marker(size_t from, size_t to); diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index f899f55d..7b3ffb1b 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -15,7 +15,6 @@ namespace map static const std::string logPrefix_ = "scwx::qt::map::marker_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); - class MarkerLayer::Impl { public: @@ -30,7 +29,7 @@ public: const std::string& markerIconName_ { types::GetTextureName(types::ImageTexture::Cursor17)}; - std::shared_ptr geoIcons_; + std::shared_ptr geoIcons_; }; void MarkerLayer::Impl::ReloadMarkers() @@ -51,8 +50,7 @@ void MarkerLayer::Impl::ReloadMarkers() } MarkerLayer::MarkerLayer(const std::shared_ptr& context) : - DrawLayer(context), - p(std::make_unique(context)) + DrawLayer(context), p(std::make_unique(context)) { AddDrawItem(p->geoIcons_); } @@ -69,10 +67,9 @@ void MarkerLayer::Initialize() p->geoIcons_->FinishIconSheets(); } -void MarkerLayer::Render( - const QMapLibre::CustomLayerRenderParameters& params) +void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - //auto markerManager = manager::MarkerManager::Instance(); + // auto markerManager = manager::MarkerManager::Instance(); gl::OpenGLFunctions& gl = context()->gl(); // TODO. do not redo this every time @@ -93,4 +90,3 @@ void MarkerLayer::Deinitialize() } // namespace map } // namespace qt } // namespace scwx - From 84233868d665a8fed4d6ec63f4d90b11a84961d4 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 5 Oct 2024 09:40:43 -0400 Subject: [PATCH 111/762] Updated how MarkerRecord works to make adding entryies easier --- .../source/scwx/qt/manager/marker_manager.cpp | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index e45f4458..f99b2ded 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -47,22 +47,29 @@ public: class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(std::string name, double latitude, double longitude) : - name_ {name}, latitude_ {latitude}, longitude_ {longitude} + MarkerRecord(const std::string& name, double latitude, double longitude) : + markerInfo_ {types::MarkerInfo(name, latitude, longitude)} + { + } + MarkerRecord(const types::MarkerInfo& info) : + markerInfo_ {info} { } - std::string name_; - double latitude_; - double longitude_; + types::MarkerInfo toMarkerInfo() + { + return markerInfo_; + } + + types::MarkerInfo markerInfo_; friend void tag_invoke(boost::json::value_from_tag, boost::json::value& jv, const std::shared_ptr& record) { - jv = {{kNameName_, record->name_}, - {kLatitudeName_, record->latitude_}, - {kLongitudeName_, record->longitude_}}; + jv = {{kNameName_, record->markerInfo_.name_}, + {kLatitudeName_, record->markerInfo_.latitude_}, + {kLongitudeName_, record->markerInfo_.longitude_}}; } friend MarkerRecord tag_invoke(boost::json::value_to_tag, @@ -117,10 +124,10 @@ void MarkerManager::Impl::ReadMarkerSettings() MarkerRecord record = boost::json::value_to(markerEntry); - if (!record.name_.empty()) + if (!record.markerInfo_.name_.empty()) { - markerRecords_.emplace_back(std::make_shared( - record.name_, record.latitude_, record.longitude_)); + markerRecords_.emplace_back( + std::make_shared(record.markerInfo_)); } } catch (const std::exception& ex) @@ -146,7 +153,7 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) { for (auto& markerRecord : markerRecords_) { - if (markerRecord->name_ == name) + if (markerRecord->markerInfo_.name_ == name) { return markerRecord; } @@ -187,25 +194,21 @@ types::MarkerInfo MarkerManager::get_marker(size_t index) { std::shared_ptr markerRecord = p->markerRecords_[index]; - return types::MarkerInfo( - markerRecord->name_, markerRecord->latitude_, markerRecord->longitude_); + return markerRecord->toMarkerInfo(); } types::MarkerInfo MarkerManager::get_marker(const std::string& name) { std::shared_ptr markerRecord = p->GetMarkerByName(name); - return types::MarkerInfo( - markerRecord->name_, markerRecord->latitude_, markerRecord->longitude_); + return markerRecord->toMarkerInfo(); } void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) { std::shared_ptr markerRecord = p->markerRecords_[index]; - markerRecord->name_ = marker.name_; - markerRecord->latitude_ = marker.latitude_; - markerRecord->longitude_ = marker.longitude_; + markerRecord->markerInfo_ = marker; } void MarkerManager::set_marker(const std::string& name, @@ -213,15 +216,12 @@ void MarkerManager::set_marker(const std::string& name, { std::shared_ptr markerRecord = p->GetMarkerByName(name); - markerRecord->name_ = marker.name_; - markerRecord->latitude_ = marker.latitude_; - markerRecord->longitude_ = marker.longitude_; + markerRecord->markerInfo_ = marker; } void MarkerManager::add_marker(const types::MarkerInfo& marker) { - p->markerRecords_.emplace_back(std::make_shared( - marker.name_, marker.latitude_, marker.longitude_)); + p->markerRecords_.emplace_back(std::make_shared(marker)); } void MarkerManager::move_marker(size_t from, size_t to) From 19415cd0a16dad50ce30f713b904bca504a0f63b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 5 Oct 2024 13:09:55 -0400 Subject: [PATCH 112/762] Added a basic location marker manager. --- scwx-qt/scwx-qt.cmake | 8 + scwx-qt/source/scwx/qt/main/main_window.cpp | 11 + scwx-qt/source/scwx/qt/main/main_window.hpp | 1 + scwx-qt/source/scwx/qt/main/main_window.ui | 16 +- .../source/scwx/qt/manager/marker_manager.cpp | 53 ++-- .../source/scwx/qt/manager/marker_manager.hpp | 16 +- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 32 ++- scwx-qt/source/scwx/qt/model/marker_model.cpp | 258 ++++++++++++++++++ scwx-qt/source/scwx/qt/model/marker_model.hpp | 51 ++++ scwx-qt/source/scwx/qt/types/marker_types.hpp | 8 +- scwx-qt/source/scwx/qt/ui/marker_dialog.cpp | 45 +++ scwx-qt/source/scwx/qt/ui/marker_dialog.hpp | 35 +++ scwx-qt/source/scwx/qt/ui/marker_dialog.ui | 88 ++++++ .../scwx/qt/ui/marker_settings_widget.cpp | 105 +++++++ .../scwx/qt/ui/marker_settings_widget.hpp | 35 +++ .../scwx/qt/ui/marker_settings_widget.ui | 88 ++++++ 16 files changed, 806 insertions(+), 44 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/model/marker_model.cpp create mode 100644 scwx-qt/source/scwx/qt/model/marker_model.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/marker_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/marker_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/marker_dialog.ui create mode 100644 scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 6edb022d..71700cec 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -158,6 +158,7 @@ set(HDR_MODEL source/scwx/qt/model/alert_model.hpp source/scwx/qt/model/imgui_context_model.hpp source/scwx/qt/model/layer_model.hpp source/scwx/qt/model/placefile_model.hpp + source/scwx/qt/model/marker_model.hpp source/scwx/qt/model/radar_site_model.hpp source/scwx/qt/model/tree_item.hpp source/scwx/qt/model/tree_model.hpp) @@ -166,6 +167,7 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp source/scwx/qt/model/imgui_context_model.cpp source/scwx/qt/model/layer_model.cpp source/scwx/qt/model/placefile_model.cpp + source/scwx/qt/model/marker_model.cpp source/scwx/qt/model/radar_site_model.cpp source/scwx/qt/model/tree_item.cpp source/scwx/qt/model/tree_model.cpp) @@ -265,6 +267,8 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/open_url_dialog.hpp source/scwx/qt/ui/placefile_dialog.hpp source/scwx/qt/ui/placefile_settings_widget.hpp + source/scwx/qt/ui/marker_dialog.hpp + source/scwx/qt/ui/marker_settings_widget.hpp source/scwx/qt/ui/progress_dialog.hpp source/scwx/qt/ui/radar_site_dialog.hpp source/scwx/qt/ui/serial_port_dialog.hpp @@ -293,6 +297,8 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/open_url_dialog.cpp source/scwx/qt/ui/placefile_dialog.cpp source/scwx/qt/ui/placefile_settings_widget.cpp + source/scwx/qt/ui/marker_dialog.cpp + source/scwx/qt/ui/marker_settings_widget.cpp source/scwx/qt/ui/progress_dialog.cpp source/scwx/qt/ui/radar_site_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp @@ -312,6 +318,8 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/open_url_dialog.ui source/scwx/qt/ui/placefile_dialog.ui source/scwx/qt/ui/placefile_settings_widget.ui + source/scwx/qt/ui/marker_dialog.ui + source/scwx/qt/ui/marker_settings_widget.ui source/scwx/qt/ui/progress_dialog.ui source/scwx/qt/ui/radar_site_dialog.ui source/scwx/qt/ui/settings_dialog.ui diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 836d6784..ac5f73e1 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +88,7 @@ public: imGuiDebugDialog_ {nullptr}, layerDialog_ {nullptr}, placefileDialog_ {nullptr}, + markerDialog_ {nullptr}, radarSiteDialog_ {nullptr}, settingsDialog_ {nullptr}, updateDialog_ {nullptr}, @@ -205,6 +207,7 @@ public: ui::ImGuiDebugDialog* imGuiDebugDialog_; ui::LayerDialog* layerDialog_; ui::PlacefileDialog* placefileDialog_; + ui::MarkerDialog* markerDialog_; ui::RadarSiteDialog* radarSiteDialog_; ui::SettingsDialog* settingsDialog_; ui::UpdateDialog* updateDialog_; @@ -306,6 +309,9 @@ MainWindow::MainWindow(QWidget* parent) : // Placefile Manager Dialog p->placefileDialog_ = new ui::PlacefileDialog(this); + // Marker Manager Dialog + p->markerDialog_ = new ui::MarkerDialog(this); + // Layer Dialog p->layerDialog_ = new ui::LayerDialog(this); @@ -613,6 +619,11 @@ void MainWindow::on_actionPlacefileManager_triggered() p->placefileDialog_->show(); } +void MainWindow::on_actionMarkerManager_triggered() +{ + p->markerDialog_->show(); +} + void MainWindow::on_actionLayerManager_triggered() { p->layerDialog_->show(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index c6ea3a5f..6a4fb5b4 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -44,6 +44,7 @@ private slots: void on_actionRadarRange_triggered(bool checked); void on_actionRadarSites_triggered(bool checked); void on_actionPlacefileManager_triggered(); + void on_actionMarkerManager_triggered(); void on_actionLayerManager_triggered(); void on_actionImGuiDebug_triggered(); void on_actionDumpLayerList_triggered(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 9fab1adf..c5e877c9 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 - 33 + 22 @@ -104,6 +104,7 @@ + @@ -152,8 +153,8 @@ 0 0 - 190 - 686 + 205 + 701 @@ -487,6 +488,15 @@ &GPS Info + + + + :/res/icons/font-awesome-6/house-solid.svg:/res/icons/font-awesome-6/house-solid.svg + + + Location &Marker Manager + + diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index f99b2ded..15dc0509 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -56,7 +56,7 @@ public: { } - types::MarkerInfo toMarkerInfo() + const types::MarkerInfo& toMarkerInfo() { return markerInfo_; } @@ -67,9 +67,9 @@ public: boost::json::value& jv, const std::shared_ptr& record) { - jv = {{kNameName_, record->markerInfo_.name_}, - {kLatitudeName_, record->markerInfo_.latitude_}, - {kLongitudeName_, record->markerInfo_.longitude_}}; + jv = {{kNameName_, record->markerInfo_.name}, + {kLatitudeName_, record->markerInfo_.latitude}, + {kLongitudeName_, record->markerInfo_.longitude}}; } friend MarkerRecord tag_invoke(boost::json::value_to_tag, @@ -124,7 +124,7 @@ void MarkerManager::Impl::ReadMarkerSettings() MarkerRecord record = boost::json::value_to(markerEntry); - if (!record.markerInfo_.name_.empty()) + if (!record.markerInfo_.name.empty()) { markerRecords_.emplace_back( std::make_shared(record.markerInfo_)); @@ -138,6 +138,8 @@ void MarkerManager::Impl::ReadMarkerSettings() logger_->debug("{} location marker entries", markerRecords_.size()); } + + Q_EMIT self_->MarkersUpdated(); } void MarkerManager::Impl::WriteMarkerSettings() @@ -153,7 +155,7 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) { for (auto& markerRecord : markerRecords_) { - if (markerRecord->markerInfo_.name_ == name) + if (markerRecord->markerInfo_.name == name) { return markerRecord; } @@ -190,38 +192,44 @@ size_t MarkerManager::marker_count() } // TODO deal with out of range/not found -types::MarkerInfo MarkerManager::get_marker(size_t index) +const types::MarkerInfo& MarkerManager::get_marker(size_t index) { std::shared_ptr markerRecord = p->markerRecords_[index]; return markerRecord->toMarkerInfo(); } -types::MarkerInfo MarkerManager::get_marker(const std::string& name) -{ - std::shared_ptr markerRecord = - p->GetMarkerByName(name); - return markerRecord->toMarkerInfo(); -} - void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) { std::shared_ptr markerRecord = p->markerRecords_[index]; markerRecord->markerInfo_ = marker; -} - -void MarkerManager::set_marker(const std::string& name, - const types::MarkerInfo& marker) -{ - std::shared_ptr markerRecord = - p->GetMarkerByName(name); - markerRecord->markerInfo_ = marker; + Q_EMIT MarkersUpdated(); } void MarkerManager::add_marker(const types::MarkerInfo& marker) { p->markerRecords_.emplace_back(std::make_shared(marker)); + Q_EMIT MarkerAdded(); + Q_EMIT MarkersUpdated(); +} + +void MarkerManager::remove_marker(size_t index) +{ + if (index >= p->markerRecords_.size()) + { + return; + } + + for (size_t i = index; i < p->markerRecords_.size() - 1; i++) + { + p->markerRecords_[i] = p->markerRecords_[i + 1]; + } + + p->markerRecords_.pop_back(); + + Q_EMIT MarkerRemoved(index); + Q_EMIT MarkersUpdated(); } void MarkerManager::move_marker(size_t from, size_t to) @@ -250,6 +258,7 @@ void MarkerManager::move_marker(size_t from, size_t to) } p->markerRecords_[to] = markerRecord; } + Q_EMIT MarkersUpdated(); } std::shared_ptr MarkerManager::Instance() diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 4fa5639c..e21accbf 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -2,8 +2,6 @@ #include -#include - #include namespace scwx @@ -21,16 +19,20 @@ public: explicit MarkerManager(); ~MarkerManager(); - size_t marker_count(); - types::MarkerInfo get_marker(size_t index); - types::MarkerInfo get_marker(const std::string& name); - void set_marker(size_t index, const types::MarkerInfo& marker); - void set_marker(const std::string& name, const types::MarkerInfo& marker); + size_t marker_count(); + const types::MarkerInfo& get_marker(size_t index); + void set_marker(size_t index, const types::MarkerInfo& marker); void add_marker(const types::MarkerInfo& marker); + void remove_marker(size_t index); void move_marker(size_t from, size_t to); static std::shared_ptr Instance(); +signals: + void MarkersUpdated(); + void MarkerAdded(); + void MarkerRemoved(size_t index); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 7b3ffb1b..1a09a0c6 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -18,39 +18,56 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class MarkerLayer::Impl { public: - explicit Impl(std::shared_ptr context) : - geoIcons_ {std::make_shared(context)} + explicit Impl(MarkerLayer* self, std::shared_ptr context) : + self_ {self}, geoIcons_ {std::make_shared(context)} { + ConnectSignals(); } ~Impl() {} void ReloadMarkers(); + void ConnectSignals(); + MarkerLayer* self_; const std::string& markerIconName_ { types::GetTextureName(types::ImageTexture::Cursor17)}; std::shared_ptr geoIcons_; }; +void MarkerLayer::Impl::ConnectSignals() +{ + auto markerManager = manager::MarkerManager::Instance(); + + QObject::connect(markerManager.get(), + &manager::MarkerManager::MarkersUpdated, + self_, + [this]() + { + this->ReloadMarkers(); + }); +} + void MarkerLayer::Impl::ReloadMarkers() { + logger_->debug("ReloadMarkers()"); auto markerManager = manager::MarkerManager::Instance(); geoIcons_->StartIcons(); for (size_t i = 0; i < markerManager->marker_count(); i++) { - types::MarkerInfo marker = markerManager->get_marker(i); + const types::MarkerInfo& marker = markerManager->get_marker(i); std::shared_ptr icon = geoIcons_->AddIcon(); geoIcons_->SetIconTexture(icon, markerIconName_, 0); - geoIcons_->SetIconLocation(icon, marker.latitude_, marker.longitude_); + geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); } geoIcons_->FinishIcons(); } MarkerLayer::MarkerLayer(const std::shared_ptr& context) : - DrawLayer(context), p(std::make_unique(context)) + DrawLayer(context), p(std::make_unique(this, context)) { AddDrawItem(p->geoIcons_); } @@ -65,6 +82,8 @@ void MarkerLayer::Initialize() p->geoIcons_->StartIconSheets(); p->geoIcons_->AddIconSheet(p->markerIconName_); p->geoIcons_->FinishIconSheets(); + + p->ReloadMarkers(); } void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) @@ -72,9 +91,6 @@ void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) // auto markerManager = manager::MarkerManager::Instance(); gl::OpenGLFunctions& gl = context()->gl(); - // TODO. do not redo this every time - p->ReloadMarkers(); - DrawLayer::Render(params); SCWX_GL_CHECK_ERROR(); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp new file mode 100644 index 00000000..3061c764 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -0,0 +1,258 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace model +{ + +static const std::string logPrefix_ = "scwx::qt::model::marker_model"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr int kFirstColumn = + static_cast(MarkerModel::Column::Name); +static constexpr int kLastColumn = + static_cast(MarkerModel::Column::Longitude); +static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; + +class MarkerModel::Impl +{ +public: + explicit Impl() {} + ~Impl() = default; + std::shared_ptr markerManager_ { + manager::MarkerManager::Instance()}; +}; + +MarkerModel::MarkerModel(QObject* parent) : + QAbstractTableModel(parent), p(std::make_unique()) +{ + connect(p->markerManager_.get(), + &manager::MarkerManager::MarkerAdded, + this, + &MarkerModel::HandleMarkerAdded); +} + +MarkerModel::~MarkerModel() = default; + +int MarkerModel::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? + 0 : + static_cast(p->markerManager_->marker_count()); +} + +int MarkerModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : kNumColumns; +} + +Qt::ItemFlags MarkerModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags flags = QAbstractTableModel::flags(index); + + switch (index.column()) + { + case static_cast(Column::Name): + case static_cast(Column::Latitude): + case static_cast(Column::Longitude): + flags |= Qt::ItemFlag::ItemIsEditable; + break; + default: + break; + } + + return flags; +} + +QVariant MarkerModel::data(const QModelIndex& index, int role) const +{ + + static const char COORDINATE_FORMAT = 'g'; + static const int COORDINATE_PRECISION = 6; + + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= + p->markerManager_->marker_count()) + { + return QVariant(); + } + + const types::MarkerInfo markerInfo = + p->markerManager_->get_marker(index.row()); + + switch(index.column()) + { + case static_cast(Column::Name): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole || + role == Qt::ItemDataRole::EditRole) + { + return QString::fromStdString(markerInfo.name); + } + break; + + case static_cast(Column::Latitude): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole || + role == Qt::ItemDataRole::EditRole) + { + return QString::number( + markerInfo.latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); + } + break; + + case static_cast(Column::Longitude): + if (role == Qt::ItemDataRole::DisplayRole || + role == Qt::ItemDataRole::ToolTipRole || + role == Qt::ItemDataRole::EditRole) + { + return QString::number( + markerInfo.longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); + } + break; + + default: + break; + } + + return QVariant(); +} + +QVariant MarkerModel::headerData(int section, + Qt::Orientation orientation, + int role) const +{ + if (role == Qt::ItemDataRole::DisplayRole) + { + if (orientation == Qt::Horizontal) + { + switch (section) + { + case static_cast(Column::Name): + return tr("Name"); + + case static_cast(Column::Latitude): + return tr("Latitude"); + + case static_cast(Column::Longitude): + return tr("Longitude"); + + default: + break; + } + } + } + + return QVariant(); +} + +bool MarkerModel::setData(const QModelIndex& index, + const QVariant& value, + int role) +{ + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= + p->markerManager_->marker_count()) + { + return false; + } + + types::MarkerInfo markerInfo = p->markerManager_->get_marker(index.row()); + bool result = false; + + switch(index.column()) + { + case static_cast(Column::Name): + if (role == Qt::ItemDataRole::EditRole) + { + QString str = value.toString(); + markerInfo.name = str.toStdString(); + p->markerManager_->set_marker(index.row(), markerInfo); + result = true; + } + break; + + case static_cast(Column::Latitude): + if (role == Qt::ItemDataRole::EditRole) + { + QString str = value.toString(); + bool ok; + double latitude = str.toDouble(&ok); + if (str.isEmpty()) + { + markerInfo.latitude = 0; + p->markerManager_->set_marker(index.row(), markerInfo); + result = true; + } + else if (ok) + { + markerInfo.latitude = latitude; + p->markerManager_->set_marker(index.row(), markerInfo); + result = true; + } + } + break; + + case static_cast(Column::Longitude): + if (role == Qt::ItemDataRole::EditRole) + { + QString str = value.toString(); + bool ok; + double longitude = str.toDouble(&ok); + if (str.isEmpty()) + { + markerInfo.longitude = 0; + p->markerManager_->set_marker(index.row(), markerInfo); + result = true; + } + else if (ok) + { + markerInfo.longitude = longitude; + p->markerManager_->set_marker(index.row(), markerInfo); + result = true; + } + } + break; + + default: + break; + } + + if (result) + { + Q_EMIT dataChanged(index, index); + } + + return result; +} + +void MarkerModel::HandleMarkerAdded() +{ + QModelIndex topLeft = createIndex(0, kFirstColumn); + QModelIndex bottomRight = + createIndex(p->markerManager_->marker_count() - 1, kLastColumn); + + logger_->debug("marker_count: {}", p->markerManager_->marker_count()); + + const int newIndex = static_cast(p->markerManager_->marker_count() - 1); + beginInsertRows(QModelIndex(), newIndex, newIndex); + endInsertRows(); + + Q_EMIT dataChanged(topLeft, bottomRight); +} + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp new file mode 100644 index 00000000..a3d23546 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace model +{ + +class MarkerModel : public QAbstractTableModel +{ +public: + enum class Column : int + { + Name = 0, + Latitude = 1, + Longitude = 2 + }; + + explicit MarkerModel(QObject* parent = nullptr); + ~MarkerModel(); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QVariant data(const QModelIndex& index, + int role = Qt::DisplayRole) const override; + QVariant headerData(int section, + Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + bool setData(const QModelIndex& index, + const QVariant& value, + int role = Qt::EditRole) override; + + +public slots: + void HandleMarkerAdded(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 1fd02111..ffa94b3d 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -12,13 +12,13 @@ namespace types struct MarkerInfo { MarkerInfo(std::string name, double latitude, double longitude) : - name_ {name}, latitude_ {latitude}, longitude_ {longitude} + name {name}, latitude {latitude}, longitude {longitude} { } - std::string name_; - double latitude_; - double longitude_; + std::string name; + double latitude; + double longitude; }; } // namespace types diff --git a/scwx-qt/source/scwx/qt/ui/marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/marker_dialog.cpp new file mode 100644 index 00000000..3db33a06 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_dialog.cpp @@ -0,0 +1,45 @@ +#include "marker_dialog.hpp" +#include "ui_marker_dialog.h" + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::marker_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class MarkerDialogImpl +{ +public: + explicit MarkerDialogImpl() {} + ~MarkerDialogImpl() = default; + + MarkerSettingsWidget* markerSettingsWidget_ {nullptr}; +}; + +MarkerDialog::MarkerDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique()}, + ui(new Ui::MarkerDialog) +{ + ui->setupUi(this); + + p->markerSettingsWidget_ = new MarkerSettingsWidget(this); + p->markerSettingsWidget_->layout()->setContentsMargins(0, 0, 0, 0); + ui->contentsFrame->layout()->addWidget(p->markerSettingsWidget_); +} + +MarkerDialog::~MarkerDialog() +{ + delete ui; +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/marker_dialog.hpp new file mode 100644 index 00000000..4a9503e9 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_dialog.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace Ui +{ +class MarkerDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class MarkerDialogImpl; + +class MarkerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit MarkerDialog(QWidget* parent = nullptr); + ~MarkerDialog(); + +private: + friend class MarkerDialogImpl; + std::unique_ptr p; + Ui::MarkerDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui new file mode 100644 index 00000000..641775a5 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui @@ -0,0 +1,88 @@ + + + MarkerDialog + + + + 0 + 0 + 700 + 600 + + + + Marker Manager + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + accepted() + MarkerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MarkerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp new file mode 100644 index 00000000..8fc1fe6a --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -0,0 +1,105 @@ +#include "marker_settings_widget.hpp" +#include "ui_marker_settings_widget.h" + +#include +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::marker_settings_widget"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class MarkerSettingsWidgetImpl +{ +public: + explicit MarkerSettingsWidgetImpl(MarkerSettingsWidget* self) : + self_ {self}, + markerModel_ {new model::MarkerModel(self_)} + { + } + + void ConnectSignals(); + + MarkerSettingsWidget* self_; + model::MarkerModel* markerModel_; + std::shared_ptr markerManager_ { + manager::MarkerManager::Instance()}; +}; + + +MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : + QFrame(parent), + p {std::make_unique(this)}, + ui(new Ui::MarkerSettingsWidget) +{ + ui->setupUi(this); + + ui->removeButton->setEnabled(false); + + ui->markerView->setModel(p->markerModel_); + + p->ConnectSignals(); +} + +MarkerSettingsWidget::~MarkerSettingsWidget() +{ + delete ui; +} + +void MarkerSettingsWidgetImpl::ConnectSignals() +{ + QObject::connect(self_->ui->addButton, + &QPushButton::clicked, + self_, + [this]() + { + markerManager_->add_marker(types::MarkerInfo("", 0, 0)); + }); + QObject::connect(self_->ui->removeButton, + &QPushButton::clicked, + self_, + [this]() + { + auto selectionModel = + self_->ui->markerView->selectionModel(); + QModelIndex selected = + selectionModel + ->selectedRows(static_cast( + model::MarkerModel::Column::Name)) + .first(); + + markerManager_->remove_marker(selected.row()); + }); + QObject::connect( + self_->ui->markerView->selectionModel(), + &QItemSelectionModel::selectionChanged, + self_, + [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; + } + + bool itemSelected = selected.size() > 0; + self_->ui->removeButton->setEnabled(itemSelected); + }); +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp new file mode 100644 index 00000000..b784c418 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace Ui +{ +class MarkerSettingsWidget; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class MarkerSettingsWidgetImpl; + +class MarkerSettingsWidget : public QFrame +{ + Q_OBJECT + +public: + explicit MarkerSettingsWidget(QWidget* parent = nullptr); + ~MarkerSettingsWidget(); + +private: + friend class MarkerSettingsWidgetImpl; + std::unique_ptr p; + Ui::MarkerSettingsWidget* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui new file mode 100644 index 00000000..12315d24 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui @@ -0,0 +1,88 @@ + + + MarkerSettingsWidget + + + + 0 + 0 + 400 + 300 + + + + Frame + + + + + + true + + + 0 + + + true + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + &Add + + + + + + + false + + + R&emove + + + + + + + + + + + From 20fd03bbdb05450bd56f5706d66d9984a58a0531 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 5 Oct 2024 14:23:11 -0400 Subject: [PATCH 113/762] modified code to avoid cast from size_t to int properly --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 3061c764..6dbd46bc 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -241,12 +241,9 @@ bool MarkerModel::setData(const QModelIndex& index, void MarkerModel::HandleMarkerAdded() { QModelIndex topLeft = createIndex(0, kFirstColumn); - QModelIndex bottomRight = - createIndex(p->markerManager_->marker_count() - 1, kLastColumn); - - logger_->debug("marker_count: {}", p->markerManager_->marker_count()); - const int newIndex = static_cast(p->markerManager_->marker_count() - 1); + QModelIndex bottomRight = createIndex(newIndex, kLastColumn); + beginInsertRows(QModelIndex(), newIndex, newIndex); endInsertRows(); From 9730ae581b2ab1d6f15aca54ead8e38a8dda5d2d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:12:27 -0400 Subject: [PATCH 114/762] fix missed update of comment after copy/paste --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 098a6378..da5bef38 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1235,7 +1235,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, { widget_->RadarSiteRequested(id); }); break; - // Create the radar site layer + // Create the location marker layer case types::InformationLayer::Markers: markerLayer_ = std::make_shared(context_); AddLayer(layerName, markerLayer_, before); From ad10e019fe88ab95b7055730216185575d8b09ee Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:13:00 -0400 Subject: [PATCH 115/762] use referance to avoid unnecessary copy --- scwx-qt/source/scwx/qt/types/marker_types.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index ffa94b3d..0d9c575b 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -11,7 +11,7 @@ namespace types struct MarkerInfo { - MarkerInfo(std::string name, double latitude, double longitude) : + MarkerInfo(const std::string& name, double latitude, double longitude) : name {name}, latitude {latitude}, longitude {longitude} { } From 57625b9680cc7f2832a833fc24d23d738c52f7b2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:15:40 -0400 Subject: [PATCH 116/762] Add async code to marker_manager --- .../source/scwx/qt/manager/marker_manager.cpp | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 15dc0509..116f1c3b 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include namespace scwx { @@ -31,13 +33,15 @@ public: class MarkerRecord; explicit Impl(MarkerManager* self) : self_ {self} {} - ~Impl() {} + ~Impl() { threadPool_.join(); } std::string markerSettingsPath_ {}; std::vector> markerRecords_ {}; MarkerManager* self_; + boost::asio::thread_pool threadPool_ {1u}; + void InitializeMarkerSettings(); void ReadMarkerSettings(); void WriteMarkerSettings(); @@ -166,19 +170,23 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) MarkerManager::MarkerManager() : p(std::make_unique(this)) { - // TODO THREADING? - try - { - p->InitializeMarkerSettings(); - // Read Marker settings on startup - // main::Application::WaitForInitialization(); - p->ReadMarkerSettings(); - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } + boost::asio::post(p->threadPool_, + [this]() + { + try + { + p->InitializeMarkerSettings(); + + // Read Marker settings on startup + main::Application::WaitForInitialization(); + p->ReadMarkerSettings(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); } MarkerManager::~MarkerManager() @@ -264,6 +272,9 @@ void MarkerManager::move_marker(size_t from, size_t to) std::shared_ptr MarkerManager::Instance() { static std::weak_ptr markerManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); std::shared_ptr markerManager = markerManagerReference_.lock(); From 491a33794f714b42a8b3fcf5690a20947bca63f4 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:18:22 -0400 Subject: [PATCH 117/762] Updated MarkerModel to update correctly on add/remove --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 19 ++++++++++++++++++- scwx-qt/source/scwx/qt/model/marker_model.hpp | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 6dbd46bc..3f137c8c 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -42,6 +42,11 @@ MarkerModel::MarkerModel(QObject* parent) : &manager::MarkerManager::MarkerAdded, this, &MarkerModel::HandleMarkerAdded); + + connect(p->markerManager_.get(), + &manager::MarkerManager::MarkerRemoved, + this, + &MarkerModel::HandleMarkerRemoved); } MarkerModel::~MarkerModel() = default; @@ -240,8 +245,8 @@ bool MarkerModel::setData(const QModelIndex& index, void MarkerModel::HandleMarkerAdded() { - QModelIndex topLeft = createIndex(0, kFirstColumn); const int newIndex = static_cast(p->markerManager_->marker_count() - 1); + QModelIndex topLeft = createIndex(newIndex, kFirstColumn); QModelIndex bottomRight = createIndex(newIndex, kLastColumn); beginInsertRows(QModelIndex(), newIndex, newIndex); @@ -250,6 +255,18 @@ void MarkerModel::HandleMarkerAdded() Q_EMIT dataChanged(topLeft, bottomRight); } +void MarkerModel::HandleMarkerRemoved(size_t index) +{ + const int removedIndex = static_cast(index); + QModelIndex topLeft = createIndex(removedIndex, kFirstColumn); + QModelIndex bottomRight = createIndex(removedIndex, kLastColumn); + + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + endRemoveRows(); + + Q_EMIT dataChanged(topLeft, bottomRight); +} + } // namespace model } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index a3d23546..9c640238 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -40,6 +40,7 @@ public: public slots: void HandleMarkerAdded(); + void HandleMarkerRemoved(size_t index); private: class Impl; From 534b679d63caa1018b8a9459655a97d07cb3f846 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:19:54 -0400 Subject: [PATCH 118/762] Use std::vector::erase instead of self written code in remove_marker --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 116f1c3b..e4cce9e6 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -229,12 +229,7 @@ void MarkerManager::remove_marker(size_t index) return; } - for (size_t i = index; i < p->markerRecords_.size() - 1; i++) - { - p->markerRecords_[i] = p->markerRecords_[i + 1]; - } - - p->markerRecords_.pop_back(); + p->markerRecords_.erase(std::next(p->markerRecords_.begin(), index)); Q_EMIT MarkerRemoved(index); Q_EMIT MarkersUpdated(); From 250a535fc3b5dc139019e79873b93560f220487a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:21:21 -0400 Subject: [PATCH 119/762] Add proper bounds checks and update usage of references in marker_manager.cpp --- .../source/scwx/qt/manager/marker_manager.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index e4cce9e6..d74fe09f 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -199,17 +199,24 @@ size_t MarkerManager::marker_count() return p->markerRecords_.size(); } -// TODO deal with out of range/not found -const types::MarkerInfo& MarkerManager::get_marker(size_t index) +std::optional MarkerManager::get_marker(size_t index) { - std::shared_ptr markerRecord = + if (index >= p->markerRecords_.size()) + { + return {}; + } + std::shared_ptr& markerRecord = p->markerRecords_[index]; return markerRecord->toMarkerInfo(); } void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) { - std::shared_ptr markerRecord = + if (index >= p->markerRecords_.size()) + { + return; + } + std::shared_ptr& markerRecord = p->markerRecords_[index]; markerRecord->markerInfo_ = marker; Q_EMIT MarkersUpdated(); @@ -241,7 +248,7 @@ void MarkerManager::move_marker(size_t from, size_t to) { return; } - std::shared_ptr markerRecord = + std::shared_ptr& markerRecord = p->markerRecords_[from]; if (from == to) {} From 0ec81e5832121215ff45b2703d23c006270fab0b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:22:04 -0400 Subject: [PATCH 120/762] Add modified methods to header file from last commit --- scwx-qt/source/scwx/qt/manager/marker_manager.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index e21accbf..4fb81457 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace scwx { @@ -20,7 +21,7 @@ public: ~MarkerManager(); size_t marker_count(); - const types::MarkerInfo& get_marker(size_t index); + std::optional get_marker(size_t index); void set_marker(size_t index, const types::MarkerInfo& marker); void add_marker(const types::MarkerInfo& marker); void remove_marker(size_t index); From 0cfad829332e26ab2044d65e4f2ec944723cb069 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:23:45 -0400 Subject: [PATCH 121/762] Modify usage of get_marker for updated interface, and update checks for lat/lon input --- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 8 ++- scwx-qt/source/scwx/qt/model/marker_model.cpp | 56 ++++++++----------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 1a09a0c6..545b6c32 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -57,10 +57,14 @@ void MarkerLayer::Impl::ReloadMarkers() for (size_t i = 0; i < markerManager->marker_count(); i++) { - const types::MarkerInfo& marker = markerManager->get_marker(i); + std::optional marker = markerManager->get_marker(i); + if (!marker) + { + break; + } std::shared_ptr icon = geoIcons_->AddIcon(); geoIcons_->SetIconTexture(icon, markerIconName_, 0); - geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); + geoIcons_->SetIconLocation(icon, marker->latitude, marker->longitude); } geoIcons_->FinishIcons(); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 3f137c8c..d5a4db1b 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -87,15 +87,17 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const static const char COORDINATE_FORMAT = 'g'; static const int COORDINATE_PRECISION = 6; - if (!index.isValid() || index.row() < 0 || - static_cast(index.row()) >= - p->markerManager_->marker_count()) + if (!index.isValid() || index.row() < 0) { return QVariant(); } - const types::MarkerInfo markerInfo = + std::optional markerInfo = p->markerManager_->get_marker(index.row()); + if (!markerInfo) + { + return QVariant(); + } switch(index.column()) { @@ -104,7 +106,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const role == Qt::ItemDataRole::ToolTipRole || role == Qt::ItemDataRole::EditRole) { - return QString::fromStdString(markerInfo.name); + return QString::fromStdString(markerInfo->name); } break; @@ -114,7 +116,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const role == Qt::ItemDataRole::EditRole) { return QString::number( - markerInfo.latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); + markerInfo->latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); } break; @@ -124,7 +126,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const role == Qt::ItemDataRole::EditRole) { return QString::number( - markerInfo.longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); + markerInfo->longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); } break; @@ -167,14 +169,16 @@ bool MarkerModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || - static_cast(index.row()) >= - p->markerManager_->marker_count()) + if (!index.isValid() || index.row() < 0) + { + return false; + } + std::optional markerInfo = + p->markerManager_->get_marker(index.row()); + if (!markerInfo) { return false; } - - types::MarkerInfo markerInfo = p->markerManager_->get_marker(index.row()); bool result = false; switch(index.column()) @@ -183,8 +187,8 @@ bool MarkerModel::setData(const QModelIndex& index, if (role == Qt::ItemDataRole::EditRole) { QString str = value.toString(); - markerInfo.name = str.toStdString(); - p->markerManager_->set_marker(index.row(), markerInfo); + markerInfo->name = str.toStdString(); + p->markerManager_->set_marker(index.row(), *markerInfo); result = true; } break; @@ -195,16 +199,10 @@ bool MarkerModel::setData(const QModelIndex& index, QString str = value.toString(); bool ok; double latitude = str.toDouble(&ok); - if (str.isEmpty()) + if (ok && str.isEmpty() && -90 <= latitude && latitude <= 90) { - markerInfo.latitude = 0; - p->markerManager_->set_marker(index.row(), markerInfo); - result = true; - } - else if (ok) - { - markerInfo.latitude = latitude; - p->markerManager_->set_marker(index.row(), markerInfo); + markerInfo->latitude = latitude; + p->markerManager_->set_marker(index.row(), *markerInfo); result = true; } } @@ -216,16 +214,10 @@ bool MarkerModel::setData(const QModelIndex& index, QString str = value.toString(); bool ok; double longitude = str.toDouble(&ok); - if (str.isEmpty()) + if (str.isEmpty() && ok && -180 <= longitude && longitude <= 180) { - markerInfo.longitude = 0; - p->markerManager_->set_marker(index.row(), markerInfo); - result = true; - } - else if (ok) - { - markerInfo.longitude = longitude; - p->markerManager_->set_marker(index.row(), markerInfo); + markerInfo->longitude = longitude; + p->markerManager_->set_marker(index.row(), *markerInfo); result = true; } } From 3b8e0d8180eb2362ccdb75f3d8d942c2a34395ea Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 12:55:21 -0400 Subject: [PATCH 122/762] update to propery check if string is empty --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index d5a4db1b..04a65623 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -199,7 +199,7 @@ bool MarkerModel::setData(const QModelIndex& index, QString str = value.toString(); bool ok; double latitude = str.toDouble(&ok); - if (ok && str.isEmpty() && -90 <= latitude && latitude <= 90) + if (!str.isEmpty() && ok && -90 <= latitude && latitude <= 90) { markerInfo->latitude = latitude; p->markerManager_->set_marker(index.row(), *markerInfo); @@ -214,7 +214,7 @@ bool MarkerModel::setData(const QModelIndex& index, QString str = value.toString(); bool ok; double longitude = str.toDouble(&ok); - if (str.isEmpty() && ok && -180 <= longitude && longitude <= 180) + if (!str.isEmpty() && ok && -180 <= longitude && longitude <= 180) { markerInfo->longitude = longitude; p->markerManager_->set_marker(index.row(), *markerInfo); From aabf4fcbb0a0b141a890d64b682d385b69bd31fa Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Oct 2024 13:15:20 -0400 Subject: [PATCH 123/762] Updated marker to a custom marker so it is different from the center marker --- scwx-qt/res/textures/images/location-marker.svg | 9 +++++++++ scwx-qt/scwx-qt.qrc | 1 + scwx-qt/source/scwx/qt/map/marker_layer.cpp | 2 +- scwx-qt/source/scwx/qt/types/texture_types.cpp | 2 ++ scwx-qt/source/scwx/qt/types/texture_types.hpp | 1 + 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/res/textures/images/location-marker.svg diff --git a/scwx-qt/res/textures/images/location-marker.svg b/scwx-qt/res/textures/images/location-marker.svg new file mode 100644 index 00000000..055c0bf0 --- /dev/null +++ b/scwx-qt/res/textures/images/location-marker.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index c9e00337..9ed5651a 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -75,6 +75,7 @@ res/textures/images/cursor-17.png res/textures/images/crosshairs-24.png res/textures/images/dot-3.png + res/textures/images/location-marker.svg res/textures/images/mapbox-logo.svg res/textures/images/maptiler-logo.svg diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 545b6c32..5e8b6c61 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -30,7 +30,7 @@ public: MarkerLayer* self_; const std::string& markerIconName_ { - types::GetTextureName(types::ImageTexture::Cursor17)}; + types::GetTextureName(types::ImageTexture::LocationMarker)}; std::shared_ptr geoIcons_; }; diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp index 5f7da52b..7f0c7a24 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.cpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -25,6 +25,8 @@ static const std::unordered_map imageTextureInfo_ { {ImageTexture::Cursor17, {"images/cursor-17", ":/res/textures/images/cursor-17.png"}}, {ImageTexture::Dot3, {"images/dot-3", ":/res/textures/images/dot-3.png"}}, + {ImageTexture::LocationMarker, + {"images/location-marker", ":/res/textures/images/location-marker.svg"}}, {ImageTexture::MapboxLogo, {"images/mapbox-logo", ":/res/textures/images/mapbox-logo.svg"}}, {ImageTexture::MapTilerLogo, diff --git a/scwx-qt/source/scwx/qt/types/texture_types.hpp b/scwx-qt/source/scwx/qt/types/texture_types.hpp index 593d574d..307a7638 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.hpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.hpp @@ -18,6 +18,7 @@ enum class ImageTexture Crosshairs24, Cursor17, Dot3, + LocationMarker, MapboxLogo, MapTilerLogo }; From a03cf83d585a64e01c75931b916bce5213c57d51 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 8 Oct 2024 22:17:42 -0500 Subject: [PATCH 124/762] Bump dependency Qt to 6.8.0 --- .github/workflows/ci.yml | 6 +++--- setup-debug.bat | 2 +- setup-debug.sh | 2 +- setup-release.bat | 2 +- setup-release.sh | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24f0dc56..a5f3395f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: compiler: msvc msvc_arch: x64 msvc_version: 2022 - qt_version: 6.7.2 + qt_version: 6.8.0 qt_arch_aqt: win64_msvc2019_64 qt_arch_dir: msvc2019_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -46,7 +46,7 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc - qt_version: 6.7.2 + qt_version: 6.8.0 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -64,7 +64,7 @@ jobs: env_cc: clang-17 env_cxx: clang++-17 compiler: clang - qt_version: 6.7.2 + qt_version: 6.8.0 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport diff --git a/setup-debug.bat b/setup-debug.bat index 7ad46d78..f57d92f3 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -2,7 +2,7 @@ call tools\setup-common.bat set build_dir=build-debug set build_type=Debug -set qt_version=6.7.2 +set qt_version=6.8.0 mkdir %build_dir% cmake -B %build_dir% -S . ^ diff --git a/setup-debug.sh b/setup-debug.sh index 87067a3b..7516c1e8 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -3,7 +3,7 @@ build_dir=${1:-build-debug} build_type=Debug -qt_version=6.7.2 +qt_version=6.8.0 script_dir="$(dirname "$(readlink -f "$0")")" mkdir -p ${build_dir} diff --git a/setup-release.bat b/setup-release.bat index 3c08d78d..316c50a7 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -2,7 +2,7 @@ call tools\setup-common.bat set build_dir=build-release set build_type=Release -set qt_version=6.7.2 +set qt_version=6.8.0 mkdir %build_dir% cmake -B %build_dir% -S . ^ diff --git a/setup-release.sh b/setup-release.sh index 6d81ff8f..949cabfc 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -3,7 +3,7 @@ build_dir=${1:-build-release} build_type=Release -qt_version=6.7.2 +qt_version=6.8.0 script_dir="$(dirname "$(readlink -f "$0")")" mkdir -p ${build_dir} From 1fd51d23f36569558a31f45d40ae816cedb7b68d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 8 Oct 2024 22:21:25 -0500 Subject: [PATCH 125/762] Qt 6.8.0 bumped MSVC to 2022 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5f3395f..806f0200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,8 @@ jobs: msvc_arch: x64 msvc_version: 2022 qt_version: 6.8.0 - qt_arch_aqt: win64_msvc2019_64 - qt_arch_dir: msvc2019_64 + qt_arch_aqt: win64_msvc2022_64 + qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' conan_arch: x86_64 From 2f37e42b388a60e951c705d30aabe8f839ee49b6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 9 Oct 2024 05:54:57 -0500 Subject: [PATCH 126/762] Add qt_arch variable to setup scripts, updating Windows to msvc2022 --- setup-debug.bat | 3 ++- setup-debug.sh | 3 ++- setup-release.bat | 3 ++- setup-release.sh | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/setup-debug.bat b/setup-debug.bat index f57d92f3..14e4f870 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -3,10 +3,11 @@ call tools\setup-common.bat set build_dir=build-debug set build_type=Debug set qt_version=6.8.0 +set qt_arch=msvc2022_64 mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/msvc2019_64 + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% pause diff --git a/setup-debug.sh b/setup-debug.sh index 7516c1e8..4f9e72ea 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -4,6 +4,7 @@ build_dir=${1:-build-debug} build_type=Debug qt_version=6.8.0 +qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" mkdir -p ${build_dir} @@ -11,5 +12,5 @@ cmake -B ${build_dir} -S . \ -DCMAKE_BUILD_TYPE=${build_type} \ -DCMAKE_CONFIGURATION_TYPES=${build_type} \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ - -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/gcc_64 \ + -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ -G Ninja diff --git a/setup-release.bat b/setup-release.bat index 316c50a7..cfcbd3c9 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -3,10 +3,11 @@ call tools\setup-common.bat set build_dir=build-release set build_type=Release set qt_version=6.8.0 +set qt_arch=msvc2022_64 mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/msvc2019_64 + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% pause diff --git a/setup-release.sh b/setup-release.sh index 949cabfc..63f52ef6 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -4,6 +4,7 @@ build_dir=${1:-build-release} build_type=Release qt_version=6.8.0 +qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" mkdir -p ${build_dir} @@ -11,5 +12,5 @@ cmake -B ${build_dir} -S . \ -DCMAKE_BUILD_TYPE=${build_type} \ -DCMAKE_CONFIGURATION_TYPES=${build_type} \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ - -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/gcc_64 \ + -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ -G Ninja From 685f3cee10976fbdade67add79250f13c316fea6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 9 Oct 2024 05:57:27 -0500 Subject: [PATCH 127/762] Update usage of QCheckBox::stateChanged to QCheckBox::checkStateChanged --- scwx-qt/source/scwx/qt/main/main_window.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index cadd9c90..861bc69a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1080,9 +1080,9 @@ void MainWindowImpl::ConnectOtherSignals() } }); connect(mainWindow_->ui->trackLocationCheckBox, - &QCheckBox::stateChanged, + &QCheckBox::checkStateChanged, mainWindow_, - [this](int state) + [this](Qt::CheckState state) { bool trackingEnabled = (state == Qt::CheckState::Checked); From e9ee5c6911ec8c1e091fce4936a0d06b5b8c10ea Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 14 Jun 2024 00:41:45 -0500 Subject: [PATCH 128/762] Enable multi-processor compilation for maplibre --- external/maplibre-native-qt.cmake | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/external/maplibre-native-qt.cmake b/external/maplibre-native-qt.cmake index 6a508040..49c37bc1 100644 --- a/external/maplibre-native-qt.cmake +++ b/external/maplibre-native-qt.cmake @@ -19,6 +19,17 @@ if (MSVC) target_link_options(MLNQtCore PRIVATE "$<$:/DEBUG>") target_link_options(MLNQtCore PRIVATE "$<$:/OPT:REF>") target_link_options(MLNQtCore PRIVATE "$<$:/OPT:ICF>") + + # Enable multi-processor compilation + target_compile_options(MLNQtCore PRIVATE "/MP") + target_compile_options(mbgl-core PRIVATE "/MP") + target_compile_options(mbgl-vendor-csscolorparser PRIVATE "/MP") + target_compile_options(mbgl-vendor-nunicode PRIVATE "/MP") + target_compile_options(mbgl-vendor-parsedate PRIVATE "/MP") + + if (TARGET mbgl-vendor-sqlite) + target_compile_options(mbgl-vendor-sqlite PRIVATE "/MP") + endif() else() target_compile_options(mbgl-core PRIVATE "$<$:-g>") target_compile_options(MLNQtCore PRIVATE "$<$:-g>") From aec937aa97f07433f77d286ac6a6b8ebf508658b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 10 Oct 2024 08:38:33 -0400 Subject: [PATCH 129/762] add more handling to ensure that MarkerModel stays up to date with MarkerManager --- .../source/scwx/qt/manager/marker_manager.cpp | 3 ++ .../source/scwx/qt/manager/marker_manager.hpp | 2 ++ scwx-qt/source/scwx/qt/model/marker_model.cpp | 31 +++++++++++++++++++ scwx-qt/source/scwx/qt/model/marker_model.hpp | 2 ++ 4 files changed, 38 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index d74fe09f..19aa6a94 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -181,6 +181,8 @@ MarkerManager::MarkerManager() : p(std::make_unique(this)) // Read Marker settings on startup main::Application::WaitForInitialization(); p->ReadMarkerSettings(); + + Q_EMIT MarkersInitialized(p->markerRecords_.size()); } catch (const std::exception& ex) { @@ -219,6 +221,7 @@ void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) std::shared_ptr& markerRecord = p->markerRecords_[index]; markerRecord->markerInfo_ = marker; + Q_EMIT MarkerChanged(index); Q_EMIT MarkersUpdated(); } diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 4fb81457..2f073ab7 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -30,7 +30,9 @@ public: static std::shared_ptr Instance(); signals: + void MarkersInitialized(size_t count); void MarkersUpdated(); + void MarkerChanged(size_t index); void MarkerAdded(); void MarkerRemoved(size_t index); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 04a65623..5ad00a56 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -38,11 +38,22 @@ public: MarkerModel::MarkerModel(QObject* parent) : QAbstractTableModel(parent), p(std::make_unique()) { + + connect(p->markerManager_.get(), + &manager::MarkerManager::MarkersInitialized, + this, + &MarkerModel::HandleMarkersInitialized); + connect(p->markerManager_.get(), &manager::MarkerManager::MarkerAdded, this, &MarkerModel::HandleMarkerAdded); + connect(p->markerManager_.get(), + &manager::MarkerManager::MarkerChanged, + this, + &MarkerModel::HandleMarkerChanged); + connect(p->markerManager_.get(), &manager::MarkerManager::MarkerRemoved, this, @@ -235,6 +246,17 @@ bool MarkerModel::setData(const QModelIndex& index, return result; } +void MarkerModel::HandleMarkersInitialized(size_t count) +{ + QModelIndex topLeft = createIndex(0, kFirstColumn); + QModelIndex bottomRight = createIndex(count - 1, kLastColumn); + + beginInsertRows(QModelIndex(), 0, count - 1); + endInsertRows(); + + Q_EMIT dataChanged(topLeft, bottomRight); +} + void MarkerModel::HandleMarkerAdded() { const int newIndex = static_cast(p->markerManager_->marker_count() - 1); @@ -247,6 +269,15 @@ void MarkerModel::HandleMarkerAdded() Q_EMIT dataChanged(topLeft, bottomRight); } +void MarkerModel::HandleMarkerChanged(size_t index) +{ + const int changedIndex = static_cast(index); + QModelIndex topLeft = createIndex(changedIndex, kFirstColumn); + QModelIndex bottomRight = createIndex(changedIndex, kLastColumn); + + Q_EMIT dataChanged(topLeft, bottomRight); +} + void MarkerModel::HandleMarkerRemoved(size_t index) { const int removedIndex = static_cast(index); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index 9c640238..c93526b1 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -39,7 +39,9 @@ public: public slots: + void HandleMarkersInitialized(size_t count); void HandleMarkerAdded(); + void HandleMarkerChanged(size_t index); void HandleMarkerRemoved(size_t index); private: From 0c20e49831b322fc8b2dda7519e0e505fbee4f7c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 10 Oct 2024 09:43:16 -0400 Subject: [PATCH 130/762] Moved name to end of model to make it take all remaining space --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 8 ++------ scwx-qt/source/scwx/qt/model/marker_model.hpp | 6 +++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 5ad00a56..ed8c67dc 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -5,10 +5,6 @@ #include #include -#include -#include -#include -#include namespace scwx { @@ -21,9 +17,9 @@ static const std::string logPrefix_ = "scwx::qt::model::marker_model"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr int kFirstColumn = - static_cast(MarkerModel::Column::Name); + static_cast(MarkerModel::Column::Latitude); static constexpr int kLastColumn = - static_cast(MarkerModel::Column::Longitude); + static_cast(MarkerModel::Column::Name); static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; class MarkerModel::Impl diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index c93526b1..85112fa1 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -14,9 +14,9 @@ class MarkerModel : public QAbstractTableModel public: enum class Column : int { - Name = 0, - Latitude = 1, - Longitude = 2 + Latitude = 0, + Longitude = 1, + Name = 2, }; explicit MarkerModel(QObject* parent = nullptr); From 60a059078c98d6d1427cbf05e6929f18e417571d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 10 Oct 2024 11:00:32 -0400 Subject: [PATCH 131/762] fixed missing static cast on size_t to int convertion --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index ed8c67dc..9a046954 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -244,10 +244,11 @@ bool MarkerModel::setData(const QModelIndex& index, void MarkerModel::HandleMarkersInitialized(size_t count) { + const int index = static_cast(count - 1); QModelIndex topLeft = createIndex(0, kFirstColumn); - QModelIndex bottomRight = createIndex(count - 1, kLastColumn); + QModelIndex bottomRight = createIndex(index, kLastColumn); - beginInsertRows(QModelIndex(), 0, count - 1); + beginInsertRows(QModelIndex(), 0, index); endInsertRows(); Q_EMIT dataChanged(topLeft, bottomRight); From 78b453249a445ee7864adbe02ab6e864abcb8fb0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 11 Oct 2024 11:03:36 -0400 Subject: [PATCH 132/762] Change cordinates to be displayed in a consistent format --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 9a046954..3ea77ecc 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -92,7 +93,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const { static const char COORDINATE_FORMAT = 'g'; - static const int COORDINATE_PRECISION = 6; + static const int COORDINATE_PRECISION = 10; if (!index.isValid() || index.row() < 0) { @@ -119,8 +120,12 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const case static_cast(Column::Latitude): if (role == Qt::ItemDataRole::DisplayRole || - role == Qt::ItemDataRole::ToolTipRole || - role == Qt::ItemDataRole::EditRole) + role == Qt::ItemDataRole::ToolTipRole) + { + return QString::fromStdString( + common::GetLatitudeString(markerInfo->latitude)); + } + else if (role == Qt::ItemDataRole::EditRole) { return QString::number( markerInfo->latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); @@ -129,13 +134,18 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const case static_cast(Column::Longitude): if (role == Qt::ItemDataRole::DisplayRole || - role == Qt::ItemDataRole::ToolTipRole || - role == Qt::ItemDataRole::EditRole) + role == Qt::ItemDataRole::ToolTipRole) + { + return QString::fromStdString( + common::GetLongitudeString(markerInfo->longitude)); + } + else if (role == Qt::ItemDataRole::EditRole) { return QString::number( markerInfo->longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); } break; + break; default: break; From 67706dc186d640c0858fd89f4d195fc49d7008c5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Oct 2024 09:51:16 -0400 Subject: [PATCH 133/762] trim whitespace from placefile URL's --- scwx-qt/source/scwx/qt/util/network.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/util/network.cpp b/scwx-qt/source/scwx/qt/util/network.cpp index 2e8d442f..429fac95 100644 --- a/scwx-qt/source/scwx/qt/util/network.cpp +++ b/scwx-qt/source/scwx/qt/util/network.cpp @@ -17,7 +17,8 @@ std::string NormalizeUrl(const std::string& urlString) std::string normalizedUrl; // Normalize URL string - QUrl url = QUrl::fromUserInput(QString::fromStdString(urlString)); + QString trimmedUrlString = QString::fromStdString(urlString).trimmed(); + QUrl url = QUrl::fromUserInput(trimmedUrlString); if (url.isLocalFile()) { normalizedUrl = QDir::toNativeSeparators(url.toLocalFile()).toStdString(); From 25fb9bdae077e733d5b1b77ec6194a52ee273fc7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Oct 2024 09:59:24 -0400 Subject: [PATCH 134/762] Add whitespace trimming to some settings --- .../source/scwx/qt/settings/settings_interface.cpp | 13 ++++++++++++- .../source/scwx/qt/settings/settings_interface.hpp | 9 +++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 4 ++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 61a793e8..4d491563 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -56,6 +56,8 @@ public: double unitScale_ {1}; std::optional unitAbbreviation_ {}; bool unitEnabled_ {false}; + + bool trimmingEnabled_ {false}; }; template @@ -191,8 +193,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->context_.get(), [this](const QString& text) { + QString trimmedText = + p->trimmingEnabled_ ? text.trimmed() : text; + // Map to value if required - std::string value {text.toStdString()}; + std::string value {trimmedText.toStdString()}; if (p->mapToValue_ != nullptr) { value = p->mapToValue_(value); @@ -488,6 +493,12 @@ void SettingsInterface::SetUnit(const double& scale, p->UpdateUnitLabel(); } +template +void SettingsInterface::EnableTrimming(bool trimmingEnabled) +{ + p->trimmingEnabled_ = trimmingEnabled; +} + template template void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 2092b1b4..84812f4f 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -126,6 +126,15 @@ public: */ void SetUnit(const double& scale, const std::string& abbreviation); + /** + * Enables or disables whitespace trimming of text input. + * Removes whitespace ('\t', '\n', '\v', '\f', '\r', and ' ') at the + * beginning and end of the string from a QLineEdit. + * + * @param trimmingEnabled If trimming should be enabled. + */ + void EnableTrimming(bool trimmingEnabled = true); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index c35607a9..74831a47 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -603,14 +603,17 @@ void SettingsDialogImpl::SetupGeneralTab() mapboxApiKey_.SetSettingsVariable(generalSettings.mapbox_api_key()); mapboxApiKey_.SetEditWidget(self_->ui->mapboxApiKeyLineEdit); mapboxApiKey_.SetResetButton(self_->ui->resetMapboxApiKeyButton); + mapboxApiKey_.EnableTrimming(); mapTilerApiKey_.SetSettingsVariable(generalSettings.maptiler_api_key()); mapTilerApiKey_.SetEditWidget(self_->ui->mapTilerApiKeyLineEdit); mapTilerApiKey_.SetResetButton(self_->ui->resetMapTilerApiKeyButton); + mapTilerApiKey_.EnableTrimming(); customStyleUrl_.SetSettingsVariable(generalSettings.custom_style_url()); customStyleUrl_.SetEditWidget(self_->ui->customMapUrlLineEdit); customStyleUrl_.SetResetButton(self_->ui->resetCustomMapUrlButton); + customStyleUrl_.EnableTrimming(); customStyleDrawLayer_.SetSettingsVariable( generalSettings.custom_style_draw_layer()); @@ -676,6 +679,7 @@ void SettingsDialogImpl::SetupGeneralTab() warningsProvider_.SetSettingsVariable(generalSettings.warnings_provider()); warningsProvider_.SetEditWidget(self_->ui->warningsProviderLineEdit); warningsProvider_.SetResetButton(self_->ui->resetWarningsProviderButton); + warningsProvider_.EnableTrimming(); antiAliasingEnabled_.SetSettingsVariable( generalSettings.anti_aliasing_enabled()); From bc3b1ad3d250b190d56b119d4114121a2e62fc83 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Oct 2024 10:00:37 -0400 Subject: [PATCH 135/762] fix some spelling mistakes in comments --- scwx-qt/source/scwx/qt/settings/settings_interface.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 84812f4f..a0005098 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -121,8 +121,8 @@ public: /** * 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 + * @param scale The ratio of the current unit to the base unit + * @param abbreviation The abbreviation to be displayed */ void SetUnit(const double& scale, const std::string& abbreviation); From bf3454d9a49029952d4c022e89caa13be02bedc0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Oct 2024 10:07:58 -0400 Subject: [PATCH 136/762] Add whitespace trimming to more settings --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 74831a47..d4f4f096 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -675,6 +675,7 @@ void SettingsDialogImpl::SetupGeneralTab() nmeaSource_.SetSettingsVariable(generalSettings.nmea_source()); nmeaSource_.SetEditWidget(self_->ui->nmeaSourceLineEdit); nmeaSource_.SetResetButton(self_->ui->resetNmeaSourceButton); + nmeaSource_.EnableTrimming(); warningsProvider_.SetSettingsVariable(generalSettings.warnings_provider()); warningsProvider_.SetEditWidget(self_->ui->warningsProviderLineEdit); @@ -754,6 +755,7 @@ void SettingsDialogImpl::SetupPalettesColorTablesTab() colorTable.SetSettingsVariable(colorTableVariable); colorTable.SetEditWidget(lineEdit); colorTable.SetResetButton(resetButton); + colorTable.EnableTrimming(); colorTableVariable.RegisterValueStagedCallback( [colorTableType, imageLabel](const std::string& value) @@ -877,6 +879,7 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioSoundFile_.SetSettingsVariable(audioSettings.alert_sound_file()); alertAudioSoundFile_.SetEditWidget(self_->ui->alertAudioSoundLineEdit); alertAudioSoundFile_.SetResetButton(self_->ui->resetAlertAudioSoundButton); + alertAudioSoundFile_.EnableTrimming(); QObject::connect( self_->ui->alertAudioSoundSelectButton, @@ -1089,6 +1092,7 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioCounty_.SetSettingsVariable(audioSettings.alert_county()); alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); + alertAudioCounty_.EnableTrimming(); QObject::connect(self_->ui->alertAudioWFOSelectButton, &QAbstractButton::clicked, @@ -1121,6 +1125,7 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioWFO_.SetSettingsVariable(audioSettings.alert_wfo()); alertAudioWFO_.SetEditWidget(self_->ui->alertAudioWFOLineEdit); alertAudioWFO_.SetResetButton(self_->ui->resetAlertAudioWFOButton); + alertAudioWFO_.EnableTrimming(); } void SettingsDialogImpl::SetupTextTab() From 907d80943e2d7abc4c9bdba9a0df69f0abea1b5e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:05:31 +0000 Subject: [PATCH 137/762] Update dependency cpr to v1.11.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index f7f015e3..8c0b0ac4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -3,7 +3,7 @@ from conans import ConanFile class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.86.0", - "cpr/1.10.5", + "cpr/1.11.0", "fontconfig/2.15.0", "freetype/2.13.2", "geographiclib/2.4", From 9ee6e2ee25375c513293da523382d9a4c18b056b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 17 Oct 2024 06:17:27 -0500 Subject: [PATCH 138/762] Add Fusion Light and Fusion Dark styles --- scwx-qt/source/scwx/qt/main/main.cpp | 36 +++++++++++++------- scwx-qt/source/scwx/qt/types/qt_types.cpp | 41 ++++++++++++++--------- scwx-qt/source/scwx/qt/types/qt_types.hpp | 11 ++++-- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 566e9772..d93133a3 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -25,11 +25,13 @@ #include #include #include +#include #include static const std::string logPrefix_ = "scwx::main"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static void ConfigureTheme(const std::vector& args); static void OverrideDefaultStyle(const std::vector& args); int main(int argc, char* argv[]) @@ -103,18 +105,7 @@ int main(int argc, char* argv[]) scwx::qt::manager::ResourceManager::Initialize(); // Theme - auto uiStyle = scwx::qt::types::GetUiStyle( - scwx::qt::settings::GeneralSettings::Instance().theme().GetValue()); - - if (uiStyle == scwx::qt::types::UiStyle::Default) - { - OverrideDefaultStyle(args); - } - else - { - QApplication::setStyle( - QString::fromStdString(scwx::qt::types::GetUiStyleName(uiStyle))); - } + ConfigureTheme(args); // Run initial setup if required if (scwx::qt::ui::setup::SetupWizard::IsSetupRequired()) @@ -152,6 +143,27 @@ int main(int argc, char* argv[]) return result; } +static void ConfigureTheme(const std::vector& args) +{ + auto& generalSettings = scwx::qt::settings::GeneralSettings::Instance(); + + auto uiStyle = + scwx::qt::types::GetUiStyle(generalSettings.theme().GetValue()); + auto qtColorScheme = scwx::qt::types::GetQtColorScheme(uiStyle); + + if (uiStyle == scwx::qt::types::UiStyle::Default) + { + OverrideDefaultStyle(args); + } + else + { + QApplication::setStyle( + QString::fromStdString(scwx::qt::types::GetQtStyleName(uiStyle))); + } + + QGuiApplication::styleHints()->setColorScheme(qtColorScheme); +} + static void OverrideDefaultStyle([[maybe_unused]] const std::vector& args) { diff --git a/scwx-qt/source/scwx/qt/types/qt_types.cpp b/scwx-qt/source/scwx/qt/types/qt_types.cpp index 37717646..da2ef622 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.cpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -9,27 +10,37 @@ namespace qt namespace types { +static const std::unordered_map qtStyleName_ { + {UiStyle::Default, "Default"}, + {UiStyle::Fusion, "Fusion"}, + {UiStyle::FusionLight, "Fusion"}, + {UiStyle::FusionDark, "Fusion"}, + {UiStyle::Unknown, "?"}}; + static const std::unordered_map uiStyleName_ { {UiStyle::Default, "Default"}, {UiStyle::Fusion, "Fusion"}, + {UiStyle::FusionLight, "Fusion Light"}, + {UiStyle::FusionDark, "Fusion Dark"}, {UiStyle::Unknown, "?"}}; -UiStyle GetUiStyle(const std::string& name) -{ - auto result = - std::find_if(uiStyleName_.cbegin(), - uiStyleName_.cend(), - [&](const std::pair& pair) -> bool - { return boost::iequals(pair.second, name); }); +static const std::unordered_map qtColorSchemeMap_ { + {UiStyle::Default, Qt::ColorScheme::Unknown}, + {UiStyle::Fusion, Qt::ColorScheme::Unknown}, + {UiStyle::FusionLight, Qt::ColorScheme::Light}, + {UiStyle::FusionDark, Qt::ColorScheme::Dark}, + {UiStyle::Unknown, Qt::ColorScheme::Unknown}}; - if (result != uiStyleName_.cend()) - { - return result->first; - } - else - { - return UiStyle::Unknown; - } +SCWX_GET_ENUM(UiStyle, GetUiStyle, uiStyleName_) + +Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle) +{ + return qtColorSchemeMap_.at(uiStyle); +} + +std::string GetQtStyleName(UiStyle uiStyle) +{ + return qtStyleName_.at(uiStyle); } std::string GetUiStyleName(UiStyle uiStyle) diff --git a/scwx-qt/source/scwx/qt/types/qt_types.hpp b/scwx-qt/source/scwx/qt/types/qt_types.hpp index 26a41c25..817d5ca1 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.hpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.hpp @@ -20,17 +20,22 @@ enum ItemDataRole RawDataRole }; -enum UiStyle +enum class UiStyle { Default, Fusion, + FusionLight, + FusionDark, Unknown }; -typedef scwx::util::Iterator +typedef scwx::util::Iterator UiStyleIterator; +Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle); +std::string GetQtStyleName(UiStyle uiStyle); + UiStyle GetUiStyle(const std::string& name); -std::string GetUiStyleName(UiStyle alertAction); +std::string GetUiStyleName(UiStyle uiStyle); } // namespace types } // namespace qt From a8845514c65cc932a0176abfae2b1a06f730dcd5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Oct 2024 16:43:00 -0400 Subject: [PATCH 139/762] Updated SVG to be much closer to an equilateral triangle --- scwx-qt/res/textures/images/location-marker.svg | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scwx-qt/res/textures/images/location-marker.svg b/scwx-qt/res/textures/images/location-marker.svg index 055c0bf0..8ebb064f 100644 --- a/scwx-qt/res/textures/images/location-marker.svg +++ b/scwx-qt/res/textures/images/location-marker.svg @@ -1,9 +1,11 @@ - - + + + From ee998232f7ae78a384977b63b5f2d32f2eb210fa Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Oct 2024 17:50:17 -0400 Subject: [PATCH 140/762] Add mutexes to marker manager --- .../source/scwx/qt/manager/marker_manager.cpp | 127 ++++++++++-------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 19aa6a94..382aafd7 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -41,6 +42,7 @@ public: MarkerManager* self_; boost::asio::thread_pool threadPool_ {1u}; + std::shared_mutex markerRecordLock_ {}; void InitializeMarkerSettings(); void ReadMarkerSettings(); @@ -109,38 +111,41 @@ void MarkerManager::Impl::ReadMarkerSettings() logger_->info("Reading location marker settings"); boost::json::value markerJson = nullptr; - - // Determine if marker settings exists - if (std::filesystem::exists(markerSettingsPath_)) { - markerJson = util::json::ReadJsonFile(markerSettingsPath_); - } + std::unique_lock lock(markerRecordLock_); - if (markerJson != nullptr && markerJson.is_array()) - { - // For each marker entry - auto& markerArray = markerJson.as_array(); - markerRecords_.reserve(markerArray.size()); - for (auto& markerEntry : markerArray) + // Determine if marker settings exists + if (std::filesystem::exists(markerSettingsPath_)) { - try - { - MarkerRecord record = - boost::json::value_to(markerEntry); - - if (!record.markerInfo_.name.empty()) - { - markerRecords_.emplace_back( - std::make_shared(record.markerInfo_)); - } - } - catch (const std::exception& ex) - { - logger_->warn("Invalid location marker entry: {}", ex.what()); - } + markerJson = util::json::ReadJsonFile(markerSettingsPath_); } - logger_->debug("{} location marker entries", markerRecords_.size()); + if (markerJson != nullptr && markerJson.is_array()) + { + // For each marker entry + auto& markerArray = markerJson.as_array(); + markerRecords_.reserve(markerArray.size()); + for (auto& markerEntry : markerArray) + { + try + { + MarkerRecord record = + boost::json::value_to(markerEntry); + + if (!record.markerInfo_.name.empty()) + { + markerRecords_.emplace_back( + std::make_shared(record.markerInfo_)); + } + } + catch (const std::exception& ex) + { + logger_->warn("Invalid location marker entry: {}", ex.what()); + } + } + + logger_->debug("{} location marker entries", markerRecords_.size()); + } } Q_EMIT self_->MarkersUpdated(); @@ -150,6 +155,7 @@ void MarkerManager::Impl::WriteMarkerSettings() { logger_->info("Saving location marker settings"); + std::shared_lock lock(markerRecordLock_); auto markerJson = boost::json::value_from(markerRecords_); util::json::WriteJsonFile(markerSettingsPath_, markerJson); } @@ -203,6 +209,7 @@ size_t MarkerManager::marker_count() std::optional MarkerManager::get_marker(size_t index) { + std::shared_lock lock(p->markerRecordLock_); if (index >= p->markerRecords_.size()) { return {}; @@ -214,32 +221,41 @@ std::optional MarkerManager::get_marker(size_t index) void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) { - if (index >= p->markerRecords_.size()) { - return; + std::unique_lock lock(p->markerRecordLock_); + if (index >= p->markerRecords_.size()) + { + return; + } + std::shared_ptr& markerRecord = + p->markerRecords_[index]; + markerRecord->markerInfo_ = marker; } - std::shared_ptr& markerRecord = - p->markerRecords_[index]; - markerRecord->markerInfo_ = marker; Q_EMIT MarkerChanged(index); Q_EMIT MarkersUpdated(); } void MarkerManager::add_marker(const types::MarkerInfo& marker) { - p->markerRecords_.emplace_back(std::make_shared(marker)); + { + std::unique_lock lock(p->markerRecordLock_); + p->markerRecords_.emplace_back(std::make_shared(marker)); + } Q_EMIT MarkerAdded(); Q_EMIT MarkersUpdated(); } void MarkerManager::remove_marker(size_t index) { - if (index >= p->markerRecords_.size()) { - return; - } + std::unique_lock lock(p->markerRecordLock_); + if (index >= p->markerRecords_.size()) + { + return; + } - p->markerRecords_.erase(std::next(p->markerRecords_.begin(), index)); + p->markerRecords_.erase(std::next(p->markerRecords_.begin(), index)); + } Q_EMIT MarkerRemoved(index); Q_EMIT MarkersUpdated(); @@ -247,29 +263,32 @@ void MarkerManager::remove_marker(size_t index) void MarkerManager::move_marker(size_t from, size_t to) { - if (from >= p->markerRecords_.size() || to >= p->markerRecords_.size()) { - return; - } - std::shared_ptr& markerRecord = - p->markerRecords_[from]; + std::unique_lock lock(p->markerRecordLock_); + if (from >= p->markerRecords_.size() || to >= p->markerRecords_.size()) + { + return; + } + std::shared_ptr& markerRecord = + p->markerRecords_[from]; - if (from == to) {} - else if (from < to) - { - for (size_t i = from; i < to; i++) + if (from == to) {} + else if (from < to) { - p->markerRecords_[i] = p->markerRecords_[i + 1]; + for (size_t i = from; i < to; i++) + { + p->markerRecords_[i] = p->markerRecords_[i + 1]; + } + p->markerRecords_[to] = markerRecord; } - p->markerRecords_[to] = markerRecord; - } - else - { - for (size_t i = from; i > to; i--) + else { - p->markerRecords_[i] = p->markerRecords_[i - 1]; + for (size_t i = from; i > to; i--) + { + p->markerRecords_[i] = p->markerRecords_[i - 1]; + } + p->markerRecords_[to] = markerRecord; } - p->markerRecords_[to] = markerRecord; } Q_EMIT MarkersUpdated(); } From a82e379f9b86be2ee0436a7ba3fbf382f0c2cf37 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Oct 2024 17:52:00 -0400 Subject: [PATCH 141/762] Make marker layer emit NeedsRendering on updating markers --- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 5e8b6c61..ab97322f 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -68,6 +68,7 @@ void MarkerLayer::Impl::ReloadMarkers() } geoIcons_->FinishIcons(); + Q_EMIT self_->NeedsRendering(); } MarkerLayer::MarkerLayer(const std::shared_ptr& context) : From fe9311325b4897a504a95174624dfcf7cf3a40d1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Oct 2024 17:55:45 -0400 Subject: [PATCH 142/762] removed unneded dataChanged calls in MarkerModel --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 3ea77ecc..eb3e8bee 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -255,25 +255,17 @@ bool MarkerModel::setData(const QModelIndex& index, void MarkerModel::HandleMarkersInitialized(size_t count) { const int index = static_cast(count - 1); - QModelIndex topLeft = createIndex(0, kFirstColumn); - QModelIndex bottomRight = createIndex(index, kLastColumn); beginInsertRows(QModelIndex(), 0, index); endInsertRows(); - - Q_EMIT dataChanged(topLeft, bottomRight); } void MarkerModel::HandleMarkerAdded() { const int newIndex = static_cast(p->markerManager_->marker_count() - 1); - QModelIndex topLeft = createIndex(newIndex, kFirstColumn); - QModelIndex bottomRight = createIndex(newIndex, kLastColumn); beginInsertRows(QModelIndex(), newIndex, newIndex); endInsertRows(); - - Q_EMIT dataChanged(topLeft, bottomRight); } void MarkerModel::HandleMarkerChanged(size_t index) @@ -288,13 +280,9 @@ void MarkerModel::HandleMarkerChanged(size_t index) void MarkerModel::HandleMarkerRemoved(size_t index) { const int removedIndex = static_cast(index); - QModelIndex topLeft = createIndex(removedIndex, kFirstColumn); - QModelIndex bottomRight = createIndex(removedIndex, kLastColumn); beginRemoveRows(QModelIndex(), removedIndex, removedIndex); endRemoveRows(); - - Q_EMIT dataChanged(topLeft, bottomRight); } } // namespace model From 43d8fc520765981103c674ad40713189afb661c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:28:14 +0000 Subject: [PATCH 143/762] Update dependency freetype to v2.13.3 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index f7f015e3..a4db355e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,7 +5,7 @@ class SupercellWxConan(ConanFile): requires = ("boost/1.86.0", "cpr/1.10.5", "fontconfig/2.15.0", - "freetype/2.13.2", + "freetype/2.13.3", "geographiclib/2.4", "geos/3.13.0", "glew/2.2.0", From c04cbc292919ee3e53e55224eed443d371b139cf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 18 Oct 2024 02:43:50 +0000 Subject: [PATCH 144/762] cpr write callback now takes const std::string_view& --- scwx-qt/source/scwx/qt/manager/download_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/download_manager.cpp b/scwx-qt/source/scwx/qt/manager/download_manager.cpp index b0585b35..54dd4371 100644 --- a/scwx-qt/source/scwx/qt/manager/download_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/download_manager.cpp @@ -155,7 +155,7 @@ void DownloadManager::Impl::DownloadSync( return !request->IsCanceled(); }), cpr::WriteCallback( - [&](std::string data, std::intptr_t /* userdata */) + [&](const std::string_view& data, std::intptr_t /* userdata */) { // Write file ofs << data; From a5106f464011356089e04cd1c4f7d3087612412c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 18 Oct 2024 09:54:06 -0400 Subject: [PATCH 145/762] Revert to using ubuntu-22.02 for gcc build to ensure support for older Glibc versions (Debian) --- .github/workflows/ci.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 806f0200..6e5a87db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: conan_package_manager: '' artifact_suffix: windows-x64 - name: linux64_gcc - os: ubuntu-24.04 + os: ubuntu-22.04 build_type: Release env_cc: gcc-11 env_cxx: g++-11 @@ -58,6 +58,7 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 + packages: '' - name: linux64_clang os: ubuntu-24.04 build_type: Release @@ -76,6 +77,7 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-clang-x64 + packages: clang-17 name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} @@ -86,7 +88,7 @@ jobs: steps: - name: Setup run: git config --global core.longpaths true - + - name: Checkout uses: actions/checkout@v4 with: @@ -109,15 +111,13 @@ jobs: vsversion: ${{ matrix.msvc_version }} - name: Setup Ubuntu Environment - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} shell: bash run: | sudo apt-get install doxygen \ libfuse2 \ ninja-build \ - clang-17 \ - gcc-11 \ - g++-11 + ${{ matrix.packages }} - name: Setup Python Environment shell: pwsh @@ -153,7 +153,7 @@ jobs: ninja supercell-wx wxtest - name: Separate Debug Symbols (Linux) - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} shell: bash run: | cd build/ @@ -172,7 +172,7 @@ jobs: cmake --install . --component supercell-wx - name: Collect Artifacts - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} shell: bash run: | pushd supercell-wx/ @@ -204,14 +204,14 @@ jobs: path: ${{ github.workspace }}/build/bin/*.pdb - name: Upload Artifacts (Linux) - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} uses: actions/upload-artifact@v4 with: name: supercell-wx-${{ matrix.artifact_suffix }} path: ${{ github.workspace }}/supercell-wx-${{ matrix.artifact_suffix }}.tar.gz - name: Upload Debug Artifacts (Linux) - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} uses: actions/upload-artifact@v4 with: name: supercell-wx-debug-${{ matrix.artifact_suffix }} @@ -234,7 +234,7 @@ jobs: path: ${{ github.workspace }}/build/supercell-wx-*.msi* - name: Build AppImage (Linux) - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} env: APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx/ LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*x86_64.AppImage.zsync @@ -258,7 +258,7 @@ jobs: rm -f linuxdeploy-x86_64.AppImage - name: Upload AppImage (Linux) - if: matrix.os == 'ubuntu-24.04' + if: ${{ startsWith(matrix.os, 'ubuntu') }} uses: actions/upload-artifact@v4 with: name: supercell-wx-appimage-${{ matrix.artifact_suffix }} From 6c2e31e3cb4d22974233463d2fba90f721f26c36 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 18 Oct 2024 10:46:51 -0400 Subject: [PATCH 146/762] rename to in CI workflow --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e5a87db..c49f81fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-x64 - packages: '' + compiler_packages: '' - name: linux64_clang os: ubuntu-24.04 build_type: Release @@ -77,7 +77,7 @@ jobs: conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True artifact_suffix: linux-clang-x64 - packages: clang-17 + compiler_packages: clang-17 name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} @@ -117,7 +117,7 @@ jobs: sudo apt-get install doxygen \ libfuse2 \ ninja-build \ - ${{ matrix.packages }} + ${{ matrix.compiler_packages }} - name: Setup Python Environment shell: pwsh From 0bce0e2bd734c6c299768616f67d42ff4df6e2a2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 19 Oct 2024 09:41:42 -0400 Subject: [PATCH 147/762] First addition of qt6ct based color palettes to give dark mode on all platforms. --- ACKNOWLEDGEMENTS.md | 2 + scwx-qt/res/qt6ct_colors/airy.conf | 4 + scwx-qt/res/qt6ct_colors/darker.conf | 4 + scwx-qt/res/qt6ct_colors/dusk.conf | 4 + scwx-qt/res/qt6ct_colors/ia_ora.conf | 4 + scwx-qt/res/qt6ct_colors/sand.conf | 4 + scwx-qt/res/qt6ct_colors/simple.conf | 4 + scwx-qt/res/qt6ct_colors/waves.conf | 4 + scwx-qt/scwx-qt.cmake | 2 + scwx-qt/scwx-qt.qrc | 1 + scwx-qt/source/scwx/qt/main/main.cpp | 12 +++ scwx-qt/source/scwx/qt/types/qt_types.cpp | 3 + scwx-qt/source/scwx/qt/types/qt_types.hpp | 3 +- scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp | 87 +++++++++++++++++++ scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp | 50 +++++++++++ 15 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/res/qt6ct_colors/airy.conf create mode 100644 scwx-qt/res/qt6ct_colors/darker.conf create mode 100644 scwx-qt/res/qt6ct_colors/dusk.conf create mode 100644 scwx-qt/res/qt6ct_colors/ia_ora.conf create mode 100644 scwx-qt/res/qt6ct_colors/sand.conf create mode 100644 scwx-qt/res/qt6ct_colors/simple.conf create mode 100644 scwx-qt/res/qt6ct_colors/waves.conf create mode 100644 scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp create mode 100644 scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 154d0f6c..4aec61d2 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -35,6 +35,7 @@ Supercell Wx uses code from the following dependencies: | [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 Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | +| [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | [SQLite](https://www.sqlite.org/) | Public Domain | @@ -67,6 +68,7 @@ Supercell Wx uses assets from the following sources: | [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 | +| [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | | [Roboto Flex](https://fonts.google.com/specimen/Roboto+Flex) | SIL Open Font License | | [Supercell thunderstorm with dramatic clouds](https://www.shutterstock.com/image-photo/supercell-thunderstorm-dramatic-clouds-1354353521) | Shutterstock Standard License | Photo by John Sirlin diff --git a/scwx-qt/res/qt6ct_colors/airy.conf b/scwx-qt/res/qt6ct_colors/airy.conf new file mode 100644 index 00000000..68b32473 --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/airy.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ff000000, #ffdcdcdc, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff000000, #ff0a0a0a, #ff0a0a0a, #ffc8c8c8, #ffffffff, #ffe7e4e0, #ff0986d3, #ff0a0a0a, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ff050505, #80000000 +disabled_colors=#ffffffff, #ff424245, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff808080, #ffffffff, #ff808080, #ff969696, #ffc8c8c8, #ffe7e4e0, #ff0986d3, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ffffffff, #80000000 +inactive_colors=#ff323232, #ffb4b4b4, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff323232, #ff323232, #ff323232, #ff969696, #ffc8c8c8, #ffe7e4e0, #ff0986d3, #ff323232, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ff323232, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/darker.conf b/scwx-qt/res/qt6ct_colors/darker.conf new file mode 100644 index 00000000..cf2f69f7 --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/darker.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff +disabled_colors=#ff808080, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ffffffff, #ff808080, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff +inactive_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff diff --git a/scwx-qt/res/qt6ct_colors/dusk.conf b/scwx-qt/res/qt6ct_colors/dusk.conf new file mode 100644 index 00000000..e0b0059a --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/dusk.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ff000000, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ff7f7f7f, #ff7f7f7f, #ff707070, #ff308cc6, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 +disabled_colors=#ffbebebe, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ffbebebe, #ffffffff, #ffbebebe, #ff7f7f7f, #ff7f7f7f, #ffb1aeab, #ff7f7f7f, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 +inactive_colors=#ff000000, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ff7f7f7f, #ff7f7f7f, #ff707070, #ff308cc6, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/ia_ora.conf b/scwx-qt/res/qt6ct_colors/ia_ora.conf new file mode 100644 index 00000000..7875fc4d --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/ia_ora.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 +disabled_colors=#ff808080, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff808080, #ffffffff, #ff808080, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ff808080, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 +inactive_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/sand.conf b/scwx-qt/res/qt6ct_colors/sand.conf new file mode 100644 index 00000000..004267d1 --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/sand.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ff000000, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff000000, #ff000000, #ff000000, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 +disabled_colors=#ff4a4947, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff4a4947, #ff4a4947, #ff4a4947, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 +inactive_colors=#ff000000, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff000000, #ff000000, #ff000000, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/simple.conf b/scwx-qt/res/qt6ct_colors/simple.conf new file mode 100644 index 00000000..7b655ad5 --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/simple.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ff000000, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ffffffff, #ffefebe7, #ffb1aeab, #ff308cc6, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 +disabled_colors=#ffbebebe, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ffbebebe, #ffffffff, #ffbebebe, #ffefebe7, #ffefebe7, #ffb1aeab, #ff9f9d9a, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 +inactive_colors=#ff000000, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ffffffff, #ffefebe7, #ffb1aeab, #ff308cc6, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/waves.conf b/scwx-qt/res/qt6ct_colors/waves.conf new file mode 100644 index 00000000..e0de92fd --- /dev/null +++ b/scwx-qt/res/qt6ct_colors/waves.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 +disabled_colors=#ff808080, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ff808080, #ff808080, #ff00071d, #ff00071d, #ffb0b0b0, #ff00071d, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 +inactive_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 71700cec..2741f076 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -357,6 +357,7 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/texture_atlas.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp + source/scwx/qt/util/qt6ct_palette.hpp source/scwx/qt/util/time.hpp source/scwx/qt/util/tooltip.hpp) set(SRC_UTIL source/scwx/qt/util/color.cpp @@ -369,6 +370,7 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/texture_atlas.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp + source/scwx/qt/util/qt6ct_palette.cpp source/scwx/qt/util/time.cpp source/scwx/qt/util/tooltip.cpp) set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 9ed5651a..63ad1793 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -70,6 +70,7 @@ res/palettes/wct/SW.pal res/palettes/wct/VIL.pal res/palettes/wct/ZDR.pal + res/qt6ct_colors/darker.conf res/textures/lines/default-1x7.png res/textures/lines/test-pattern.png res/textures/images/cursor-17.png diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index d93133a3..d90ec0e9 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,8 @@ #include #include #include +#include +#include static const std::string logPrefix_ = "scwx::main"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); @@ -162,6 +165,15 @@ static void ConfigureTheme(const std::vector& args) } QGuiApplication::styleHints()->setColorScheme(qtColorScheme); + + if (uiStyle == scwx::qt::types::UiStyle::FusionQt6Ct) + { + auto palette = + std::make_unique(scwx::qt::util::qt6ct::loadColorScheme( + ":res/qt6ct_colors/darker.conf", + QApplication::style()->standardPalette())); + QApplication::setPalette(*palette); + } } static void diff --git a/scwx-qt/source/scwx/qt/types/qt_types.cpp b/scwx-qt/source/scwx/qt/types/qt_types.cpp index da2ef622..03d58a75 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.cpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.cpp @@ -15,6 +15,7 @@ static const std::unordered_map qtStyleName_ { {UiStyle::Fusion, "Fusion"}, {UiStyle::FusionLight, "Fusion"}, {UiStyle::FusionDark, "Fusion"}, + {UiStyle::FusionQt6Ct, "Fusion"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map uiStyleName_ { @@ -22,6 +23,7 @@ static const std::unordered_map uiStyleName_ { {UiStyle::Fusion, "Fusion"}, {UiStyle::FusionLight, "Fusion Light"}, {UiStyle::FusionDark, "Fusion Dark"}, + {UiStyle::FusionQt6Ct, "Fusion with qt6ct Palette"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map qtColorSchemeMap_ { @@ -29,6 +31,7 @@ static const std::unordered_map qtColorSchemeMap_ { {UiStyle::Fusion, Qt::ColorScheme::Unknown}, {UiStyle::FusionLight, Qt::ColorScheme::Light}, {UiStyle::FusionDark, Qt::ColorScheme::Dark}, + {UiStyle::FusionQt6Ct, Qt::ColorScheme::Unknown}, {UiStyle::Unknown, Qt::ColorScheme::Unknown}}; SCWX_GET_ENUM(UiStyle, GetUiStyle, uiStyleName_) diff --git a/scwx-qt/source/scwx/qt/types/qt_types.hpp b/scwx-qt/source/scwx/qt/types/qt_types.hpp index 817d5ca1..a9266c8b 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.hpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.hpp @@ -26,9 +26,10 @@ enum class UiStyle Fusion, FusionLight, FusionDark, + FusionQt6Ct, Unknown }; -typedef scwx::util::Iterator +typedef scwx::util::Iterator UiStyleIterator; Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle); diff --git a/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp b/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp new file mode 100644 index 00000000..b6319709 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp @@ -0,0 +1,87 @@ +/* This code is drawn from qt6ct, and loads qt6ct color palette files. + * qt6ct is licensed under BSD-2-Clause below. + * + * Copyright (c) 2020-2024, Ilya Kotov + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace qt6ct +{ + +QPalette loadColorScheme(const QString &filePath, const QPalette &fallback) +{ + QPalette customPalette; + QSettings settings(filePath, QSettings::IniFormat); + settings.beginGroup("ColorScheme"); + QStringList activeColors = settings.value("active_colors").toStringList(); + QStringList inactiveColors = settings.value("inactive_colors").toStringList(); + QStringList disabledColors = settings.value("disabled_colors").toStringList(); + settings.endGroup(); + + +#if (QT_VERSION >= QT_VERSION_CHECK(6,6,0)) + if(activeColors.count() == QPalette::Accent) + activeColors << activeColors.at(QPalette::Highlight); + if(inactiveColors.count() == QPalette::Accent) + inactiveColors << inactiveColors.at(QPalette::Highlight); + if(disabledColors.count() == QPalette::Accent) + disabledColors << disabledColors.at(QPalette::Highlight); +#endif + + + if(activeColors.count() >= QPalette::NColorRoles && + inactiveColors.count() >= QPalette::NColorRoles && + disabledColors.count() >= QPalette::NColorRoles) + { + for (int i = 0; i < QPalette::NColorRoles; i++) + { + QPalette::ColorRole role = QPalette::ColorRole(i); + customPalette.setColor(QPalette::Active, role, QColor(activeColors.at(i))); + customPalette.setColor(QPalette::Inactive, role, QColor(inactiveColors.at(i))); + customPalette.setColor(QPalette::Disabled, role, QColor(disabledColors.at(i))); + } + } + else + { + customPalette = fallback; //load fallback palette + } + + return customPalette; +} + +} // namespace qt6ct +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp b/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp new file mode 100644 index 00000000..9b168212 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp @@ -0,0 +1,50 @@ +#pragma once + +/* This code is drawn from qt6ct, and loads qt6ct color palette files. + * qt6ct is licensed under BSD-2-Clause below. + * + * Copyright (c) 2020-2024, Ilya Kotov + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ +namespace qt6ct +{ + +QPalette loadColorScheme(const QString &filePath, const QPalette &fallback); + +} // namespace qt6ct +} // namespace util +} // namespace qt +} // namespace scwx From 2c9a8a33a4152f04db4455acdaebe1d268083182 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 20 Oct 2024 12:34:12 -0400 Subject: [PATCH 148/762] fix error when reading an empty location marker file --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index eb3e8bee..8bcd93fa 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -254,6 +254,10 @@ bool MarkerModel::setData(const QModelIndex& index, void MarkerModel::HandleMarkersInitialized(size_t count) { + if (count == 0) + { + return; + } const int index = static_cast(count - 1); beginInsertRows(QModelIndex(), 0, index); From 236d7c1e353fdf0a2f5a17984a836bf8c3287728 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 20 Oct 2024 12:37:08 -0400 Subject: [PATCH 149/762] Added test cases for marker_model and marker_manager --- scwx-qt/source/scwx/qt/main/application.cpp | 8 + scwx-qt/source/scwx/qt/main/application.hpp | 2 + .../source/scwx/qt/manager/marker_manager.cpp | 12 +- .../source/scwx/qt/manager/marker_manager.hpp | 5 +- test/data | 2 +- .../scwx/qt/model/marker_model.test.cpp | 201 ++++++++++++++++++ test/test.cmake | 3 +- 7 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 test/source/scwx/qt/model/marker_model.test.cpp diff --git a/scwx-qt/source/scwx/qt/main/application.cpp b/scwx-qt/source/scwx/qt/main/application.cpp index d851c8ec..3f62120a 100644 --- a/scwx-qt/source/scwx/qt/main/application.cpp +++ b/scwx-qt/source/scwx/qt/main/application.cpp @@ -44,6 +44,14 @@ void WaitForInitialization() } } +// Only use for test cases +void ResetInitilization() +{ + logger_->debug("Application initialization reset"); + std::unique_lock lock(initializationMutex_); + initialized_ = false; +} + } // namespace Application } // namespace main } // namespace qt diff --git a/scwx-qt/source/scwx/qt/main/application.hpp b/scwx-qt/source/scwx/qt/main/application.hpp index 9b63ffad..0237f13c 100644 --- a/scwx-qt/source/scwx/qt/main/application.hpp +++ b/scwx-qt/source/scwx/qt/main/application.hpp @@ -11,6 +11,8 @@ namespace Application void FinishInitialization(); void WaitForInitialization(); +// Only use for test cases +void ResetInitilization(); } // namespace Application } // namespace main diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 382aafd7..c8ceda09 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -36,7 +36,7 @@ public: explicit Impl(MarkerManager* self) : self_ {self} {} ~Impl() { threadPool_.join(); } - std::string markerSettingsPath_ {}; + std::string markerSettingsPath_ {""}; std::vector> markerRecords_ {}; MarkerManager* self_; @@ -176,14 +176,13 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) MarkerManager::MarkerManager() : p(std::make_unique(this)) { + p->InitializeMarkerSettings(); boost::asio::post(p->threadPool_, [this]() { try { - p->InitializeMarkerSettings(); - // Read Marker settings on startup main::Application::WaitForInitialization(); p->ReadMarkerSettings(); @@ -293,6 +292,13 @@ void MarkerManager::move_marker(size_t from, size_t to) Q_EMIT MarkersUpdated(); } +// Only use for testing +void MarkerManager::set_marker_settings_path(const std::string& path) +{ + p->markerSettingsPath_ = path; +} + + std::shared_ptr MarkerManager::Instance() { static std::weak_ptr markerManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 2f073ab7..2166725f 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -20,13 +20,16 @@ public: explicit MarkerManager(); ~MarkerManager(); - size_t marker_count(); + size_t marker_count(); std::optional get_marker(size_t index); void set_marker(size_t index, const types::MarkerInfo& marker); void add_marker(const types::MarkerInfo& marker); void remove_marker(size_t index); void move_marker(size_t from, size_t to); + // Only use for testing + void set_marker_settings_path(const std::string& path); + static std::shared_ptr Instance(); signals: diff --git a/test/data b/test/data index 40a367ca..166c5a7b 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 40a367ca89b5b197353ca58dea547a3e3407c7f3 +Subproject commit 166c5a7bcaf8ad0a42bedf8f8dc5c4aa907e7151 diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp new file mode 100644 index 00000000..b237e182 --- /dev/null +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -0,0 +1,201 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include + + +namespace scwx +{ +namespace qt +{ +namespace model +{ + +static const std::string EMPTY_MARKERS_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-empty.json"; +static const std::string TEMP_MARKERS_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-temp.json"; +static const std::string ONE_MARKERS_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-one.json"; +static const std::string FIVE_MARKERS_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-five.json"; + +static std::mutex initializedMutex {}; +static std::condition_variable initializedCond {}; +static bool initialized; + +void CompareFiles(const std::string& file1, const std::string& file2) +{ + std::ifstream ifs1 {file1}; + std::stringstream buffer1; + buffer1 << ifs1.rdbuf(); + + std::ifstream ifs2 {file2}; + std::stringstream buffer2; + buffer2 << ifs2.rdbuf(); + + EXPECT_EQ(buffer1.str(), buffer2.str()); +} + +void CopyFile(const std::string& from, const std::string& to) +{ + std::filesystem::copy_file(from, to); + CompareFiles(from, to); +} + +typedef void TestFunction(std::shared_ptr manager, + MarkerModel& model); + +void RunTest(const std::string& filename, TestFunction testFunction) +{ + { + main::Application::ResetInitilization(); + MarkerModel model = MarkerModel(); + std::shared_ptr manager = + manager::MarkerManager::Instance(); + + manager->set_marker_settings_path(TEMP_MARKERS_FILE); + + initialized = false; + QObject::connect(manager.get(), + &manager::MarkerManager::MarkersInitialized, + []() + { + std::unique_lock lock(initializedMutex); + initialized = true; + initializedCond.notify_all(); + }); + + main::Application::FinishInitialization(); + + std::unique_lock lock(initializedMutex); + while (!initialized) + { + initializedCond.wait(lock); + } + + testFunction(manager, model); + } + + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), true); + + CompareFiles(TEMP_MARKERS_FILE, filename); +} + +TEST(MarkerModelTest, CreateJson) +{ + // Verify file doesn't exist prior to test start + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); + + RunTest(EMPTY_MARKERS_FILE, + [](std::shared_ptr, MarkerModel&) {}); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, LoadEmpty) +{ + CopyFile(EMPTY_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(EMPTY_MARKERS_FILE, + [](std::shared_ptr, MarkerModel&) {}); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, AddRemove) +{ + CopyFile(EMPTY_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(ONE_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { manager->add_marker(types::MarkerInfo("Null", 0, 0)); }); + RunTest(EMPTY_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { manager->remove_marker(0); }); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, AddFive) +{ + CopyFile(EMPTY_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(FIVE_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { + manager->add_marker(types::MarkerInfo("Null", 0, 0)); + manager->add_marker(types::MarkerInfo("North", 90, 0)); + manager->add_marker(types::MarkerInfo("South", -90, 0)); + manager->add_marker(types::MarkerInfo("East", 0, 90)); + manager->add_marker(types::MarkerInfo("West", 0, -90)); + }); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, AddFour) +{ + CopyFile(ONE_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(FIVE_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { + manager->add_marker(types::MarkerInfo("North", 90, 0)); + manager->add_marker(types::MarkerInfo("South", -90, 0)); + manager->add_marker(types::MarkerInfo("East", 0, 90)); + manager->add_marker(types::MarkerInfo("West", 0, -90)); + }); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, RemoveFive) +{ + CopyFile(FIVE_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(EMPTY_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { + manager->remove_marker(4); + manager->remove_marker(3); + manager->remove_marker(2); + manager->remove_marker(1); + manager->remove_marker(0); + }); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +TEST(MarkerModelTest, RemoveFour) +{ + CopyFile(FIVE_MARKERS_FILE, TEMP_MARKERS_FILE); + + RunTest(ONE_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel&) + { + manager->remove_marker(4); + manager->remove_marker(3); + manager->remove_marker(2); + manager->remove_marker(1); + }); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + +} // namespace model +} // namespace qt +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index b3cfedd2..dad5ab9b 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -25,7 +25,8 @@ set(SRC_QT_CONFIG_TESTS source/scwx/qt/config/county_database.test.cpp set(SRC_QT_MANAGER_TESTS source/scwx/qt/manager/settings_manager.test.cpp source/scwx/qt/manager/update_manager.test.cpp) 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_MODEL_TESTS source/scwx/qt/model/imgui_context_model.test.cpp + source/scwx/qt/model/marker_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 From 5c57ae0edc4c24e8568e297c50b628e0b31c9c71 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 21 Oct 2024 10:39:08 -0400 Subject: [PATCH 150/762] Switch to using a submodule for qt6ct external code --- .gitmodules | 3 + external/CMakeLists.txt | 1 + external/qt6ct | 1 + external/qt6ct.cmake | 6 ++ scwx-qt/scwx-qt.cmake | 3 +- scwx-qt/source/scwx/qt/main/main.cpp | 10 +-- scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp | 87 ------------------- scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp | 50 ----------- 8 files changed, 16 insertions(+), 145 deletions(-) create mode 160000 external/qt6ct create mode 100644 external/qt6ct.cmake delete mode 100644 scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp delete mode 100644 scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp diff --git a/.gitmodules b/.gitmodules index 52ede30b..afccf304 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "external/maplibre-native"] path = external/maplibre-native url = https://github.com/dpaulat/maplibre-gl-native.git +[submodule "external/qt6ct"] + path = external/qt6ct + url = https://github.com/trialuser02/qt6ct.git diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index bbc76c64..0a32377e 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -21,3 +21,4 @@ include(maplibre-native-qt.cmake) include(stb.cmake) include(textflowcpp.cmake) include(units.cmake) +include(qt6ct.cmake) diff --git a/external/qt6ct b/external/qt6ct new file mode 160000 index 00000000..55dba870 --- /dev/null +++ b/external/qt6ct @@ -0,0 +1 @@ +Subproject commit 55dba8704c0a748b0ce9f2d3cc2cf200ca3db464 diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake new file mode 100644 index 00000000..c1227711 --- /dev/null +++ b/external/qt6ct.cmake @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16.0) +set(PROJECT_NAME scwx-qt6ct) + +add_subdirectory(qt6ct/src/qt6ct-common) +set_target_properties(qt6ct-common PROPERTIES PUBLIC_HEADER qt6ct/src/qt6ct-common/qt6ct.h) +target_include_directories( qt6ct-common INTERFACE qt6ct/src ) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 2741f076..3d2f8368 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -357,7 +357,6 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/texture_atlas.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp - source/scwx/qt/util/qt6ct_palette.hpp source/scwx/qt/util/time.hpp source/scwx/qt/util/tooltip.hpp) set(SRC_UTIL source/scwx/qt/util/color.cpp @@ -370,7 +369,6 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/texture_atlas.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp - source/scwx/qt/util/qt6ct_palette.cpp source/scwx/qt/util/time.cpp source/scwx/qt/util/tooltip.cpp) set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp @@ -687,6 +685,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets GLEW::GLEW glm::glm imgui + qt6ct-common SQLite::SQLite3 wxdata) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index d90ec0e9..602a5fe8 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -24,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -168,11 +168,9 @@ static void ConfigureTheme(const std::vector& args) if (uiStyle == scwx::qt::types::UiStyle::FusionQt6Ct) { - auto palette = - std::make_unique(scwx::qt::util::qt6ct::loadColorScheme( - ":res/qt6ct_colors/darker.conf", - QApplication::style()->standardPalette())); - QApplication::setPalette(*palette); + QPalette palette = Qt6CT::loadColorScheme(":res/qt6ct_colors/darker.conf", + QApplication::style()->standardPalette());; + QApplication::setPalette(palette); } } diff --git a/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp b/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp deleted file mode 100644 index b6319709..00000000 --- a/scwx-qt/source/scwx/qt/util/qt6ct_palette.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* This code is drawn from qt6ct, and loads qt6ct color palette files. - * qt6ct is licensed under BSD-2-Clause below. - * - * Copyright (c) 2020-2024, Ilya Kotov - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include - -namespace scwx -{ -namespace qt -{ -namespace util -{ -namespace qt6ct -{ - -QPalette loadColorScheme(const QString &filePath, const QPalette &fallback) -{ - QPalette customPalette; - QSettings settings(filePath, QSettings::IniFormat); - settings.beginGroup("ColorScheme"); - QStringList activeColors = settings.value("active_colors").toStringList(); - QStringList inactiveColors = settings.value("inactive_colors").toStringList(); - QStringList disabledColors = settings.value("disabled_colors").toStringList(); - settings.endGroup(); - - -#if (QT_VERSION >= QT_VERSION_CHECK(6,6,0)) - if(activeColors.count() == QPalette::Accent) - activeColors << activeColors.at(QPalette::Highlight); - if(inactiveColors.count() == QPalette::Accent) - inactiveColors << inactiveColors.at(QPalette::Highlight); - if(disabledColors.count() == QPalette::Accent) - disabledColors << disabledColors.at(QPalette::Highlight); -#endif - - - if(activeColors.count() >= QPalette::NColorRoles && - inactiveColors.count() >= QPalette::NColorRoles && - disabledColors.count() >= QPalette::NColorRoles) - { - for (int i = 0; i < QPalette::NColorRoles; i++) - { - QPalette::ColorRole role = QPalette::ColorRole(i); - customPalette.setColor(QPalette::Active, role, QColor(activeColors.at(i))); - customPalette.setColor(QPalette::Inactive, role, QColor(inactiveColors.at(i))); - customPalette.setColor(QPalette::Disabled, role, QColor(disabledColors.at(i))); - } - } - else - { - customPalette = fallback; //load fallback palette - } - - return customPalette; -} - -} // namespace qt6ct -} // namespace util -} // namespace qt -} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp b/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp deleted file mode 100644 index 9b168212..00000000 --- a/scwx-qt/source/scwx/qt/util/qt6ct_palette.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -/* This code is drawn from qt6ct, and loads qt6ct color palette files. - * qt6ct is licensed under BSD-2-Clause below. - * - * Copyright (c) 2020-2024, Ilya Kotov - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include - -namespace scwx -{ -namespace qt -{ -namespace util -{ -namespace qt6ct -{ - -QPalette loadColorScheme(const QString &filePath, const QPalette &fallback); - -} // namespace qt6ct -} // namespace util -} // namespace qt -} // namespace scwx From 57d65cf0861b95159eecbc0ad2374c4411d56c61 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 21 Oct 2024 11:23:34 -0400 Subject: [PATCH 151/762] Change how new fusion styles are selected --- scwx-qt/scwx-qt.qrc | 6 ++++ scwx-qt/source/scwx/qt/main/main.cpp | 20 ++++++++++-- scwx-qt/source/scwx/qt/types/qt_types.cpp | 39 +++++++++++++++++++++-- scwx-qt/source/scwx/qt/types/qt_types.hpp | 16 +++++++--- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 63ad1793..fbe846ca 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -70,7 +70,13 @@ res/palettes/wct/SW.pal res/palettes/wct/VIL.pal res/palettes/wct/ZDR.pal + res/qt6ct_colors/airy.conf res/qt6ct_colors/darker.conf + res/qt6ct_colors/dusk.conf + res/qt6ct_colors/ia_ora.conf + res/qt6ct_colors/sand.conf + res/qt6ct_colors/simple.conf + res/qt6ct_colors/waves.conf res/textures/lines/default-1x7.png res/textures/lines/test-pattern.png res/textures/images/cursor-17.png diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 602a5fe8..11b7f098 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -166,10 +166,24 @@ static void ConfigureTheme(const std::vector& args) QGuiApplication::styleHints()->setColorScheme(qtColorScheme); - if (uiStyle == scwx::qt::types::UiStyle::FusionQt6Ct) + std::optional paletteFile = + scwx::qt::types::GetQtPaletteFile(uiStyle); + if (paletteFile) { - QPalette palette = Qt6CT::loadColorScheme(":res/qt6ct_colors/darker.conf", - QApplication::style()->standardPalette());; + QPalette defaultPalette = QApplication::style()->standardPalette(); + QPalette palette = + Qt6CT::loadColorScheme(QString::fromStdString(*paletteFile), + defaultPalette); + + if (defaultPalette == palette) + { + logger_->warn("Failed to load palette file '{}'", *paletteFile); + } + else + { + logger_->info("Loaded palette file '{}'", *paletteFile); + } + QApplication::setPalette(palette); } } diff --git a/scwx-qt/source/scwx/qt/types/qt_types.cpp b/scwx-qt/source/scwx/qt/types/qt_types.cpp index 03d58a75..5e5a96a7 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.cpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.cpp @@ -15,7 +15,12 @@ static const std::unordered_map qtStyleName_ { {UiStyle::Fusion, "Fusion"}, {UiStyle::FusionLight, "Fusion"}, {UiStyle::FusionDark, "Fusion"}, - {UiStyle::FusionQt6Ct, "Fusion"}, + {UiStyle::FusionAiry, "Fusion"}, + {UiStyle::FusionDarker, "Fusion"}, + {UiStyle::FusionDusk, "Fusion"}, + {UiStyle::FusionIaOra, "Fusion"}, + {UiStyle::FusionSand, "Fusion"}, + {UiStyle::FusionWaves, "Fusion"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map uiStyleName_ { @@ -23,7 +28,12 @@ static const std::unordered_map uiStyleName_ { {UiStyle::Fusion, "Fusion"}, {UiStyle::FusionLight, "Fusion Light"}, {UiStyle::FusionDark, "Fusion Dark"}, - {UiStyle::FusionQt6Ct, "Fusion with qt6ct Palette"}, + {UiStyle::FusionAiry, "Fusion Airy"}, + {UiStyle::FusionDarker, "Fusion Darker"}, + {UiStyle::FusionDusk, "Fusion Dusk"}, + {UiStyle::FusionIaOra, "Fusion IA Ora"}, + {UiStyle::FusionSand, "Fusion Sand"}, + {UiStyle::FusionWaves, "Fusion Waves"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map qtColorSchemeMap_ { @@ -31,9 +41,22 @@ static const std::unordered_map qtColorSchemeMap_ { {UiStyle::Fusion, Qt::ColorScheme::Unknown}, {UiStyle::FusionLight, Qt::ColorScheme::Light}, {UiStyle::FusionDark, Qt::ColorScheme::Dark}, - {UiStyle::FusionQt6Ct, Qt::ColorScheme::Unknown}, + {UiStyle::FusionAiry, Qt::ColorScheme::Unknown}, + {UiStyle::FusionDarker, Qt::ColorScheme::Unknown}, + {UiStyle::FusionDusk, Qt::ColorScheme::Unknown}, + {UiStyle::FusionIaOra, Qt::ColorScheme::Unknown}, + {UiStyle::FusionSand, Qt::ColorScheme::Unknown}, + {UiStyle::FusionWaves, Qt::ColorScheme::Unknown}, {UiStyle::Unknown, Qt::ColorScheme::Unknown}}; +static const std::unordered_map paletteFile_ { + {UiStyle::FusionAiry, ":res/qt6ct_colors/airy.conf"}, + {UiStyle::FusionDarker, ":res/qt6ct_colors/darker.conf"}, + {UiStyle::FusionDusk, ":res/qt6ct_colors/dusk.conf"}, + {UiStyle::FusionIaOra, ":res/qt6ct_colors/ia_ora.conf"}, + {UiStyle::FusionSand, ":res/qt6ct_colors/sand.conf"}, + {UiStyle::FusionWaves, ":res/qt6ct_colors/waves.conf"}}; + SCWX_GET_ENUM(UiStyle, GetUiStyle, uiStyleName_) Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle) @@ -46,6 +69,16 @@ std::string GetQtStyleName(UiStyle uiStyle) return qtStyleName_.at(uiStyle); } +std::optional GetQtPaletteFile(UiStyle uiStyle) +{ + if (paletteFile_.contains(uiStyle)) + { + return paletteFile_.at(uiStyle); + } + + return std::nullopt; +} + std::string GetUiStyleName(UiStyle uiStyle) { return uiStyleName_.at(uiStyle); diff --git a/scwx-qt/source/scwx/qt/types/qt_types.hpp b/scwx-qt/source/scwx/qt/types/qt_types.hpp index a9266c8b..9bb7ed5d 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.hpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.hpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -26,14 +27,21 @@ enum class UiStyle Fusion, FusionLight, FusionDark, - FusionQt6Ct, + FusionAiry, + FusionDarker, + FusionDusk, + FusionIaOra, + FusionSand, + FusionWaves, Unknown }; -typedef scwx::util::Iterator +typedef scwx::util::Iterator UiStyleIterator; -Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle); -std::string GetQtStyleName(UiStyle uiStyle); +Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle); +std::string GetQtStyleName(UiStyle uiStyle); + +std::optional GetQtPaletteFile(UiStyle uiStyle); UiStyle GetUiStyle(const std::string& name); std::string GetUiStyleName(UiStyle uiStyle); From 67c510cbd80aa66363164679bb9b3c6f83a878e0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 21 Oct 2024 14:00:34 -0400 Subject: [PATCH 152/762] Try adding Qt requirments from qt6ct into scwx-qt to fix windows build --- scwx-qt/scwx-qt.cmake | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 3d2f8368..1f4f4449 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -21,7 +21,9 @@ find_package(Python COMPONENTS Interpreter) find_package(SQLite3) find_package(QT NAMES Qt6 - COMPONENTS Gui + COMPONENTS BuildInternals + Core + Gui LinguistTools Multimedia Network @@ -30,10 +32,14 @@ find_package(QT NAMES Qt6 Positioning SerialPort Svg - Widgets REQUIRED) + Widgets + Sql + REQUIRED) find_package(Qt${QT_VERSION_MAJOR} - COMPONENTS Gui + COMPONENTS BuildInternals + Core + Gui LinguistTools Multimedia Network From 96c6baa6c90bdd965903206d3fcd9786cb422a93 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 21 Oct 2024 16:20:56 -0400 Subject: [PATCH 153/762] Add install location for qt6ct for windows (maybe) --- external/qt6ct.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake index c1227711..1c6bb947 100644 --- a/external/qt6ct.cmake +++ b/external/qt6ct.cmake @@ -4,3 +4,8 @@ set(PROJECT_NAME scwx-qt6ct) add_subdirectory(qt6ct/src/qt6ct-common) set_target_properties(qt6ct-common PROPERTIES PUBLIC_HEADER qt6ct/src/qt6ct-common/qt6ct.h) target_include_directories( qt6ct-common INTERFACE qt6ct/src ) +install(TARGETS qt6ct-common + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/qt6ct + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) From a7939a3a19ad5ac3cdd3dd2233d745d9943e7b75 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 26 Oct 2024 15:31:15 -0400 Subject: [PATCH 154/762] switch to static linking for qt6ct library --- external/qt6ct.cmake | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake index 1c6bb947..9ea8b8e3 100644 --- a/external/qt6ct.cmake +++ b/external/qt6ct.cmake @@ -1,11 +1,34 @@ cmake_minimum_required(VERSION 3.16.0) set(PROJECT_NAME scwx-qt6ct) -add_subdirectory(qt6ct/src/qt6ct-common) +#extract version from qt6ct.h +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/qt6ct/src/qt6ct-common/qt6ct.h" + QT6CT_VERSION_DATA REGEX "^#define[ \t]+QT6CT_VERSION_[A-Z]+[ \t]+[0-9]+.*$") + +if(QT6CT_VERSION_DATA) + foreach(item IN ITEMS MAJOR MINOR) + string(REGEX REPLACE ".*#define[ \t]+QT6CT_VERSION_${item}[ \t]+([0-9]+).*" + "\\1" QT6CT_VERSION_${item} ${QT6CT_VERSION_DATA}) + endforeach() + set(QT6CT_VERSION "${QT6CT_VERSION_MAJOR}.${QT6CT_VERSION_MINOR}") + set(QT6CT_SOVERSION "${QT6CT_VERSION_MAJOR}") + message(STATUS "qt6ct version: ${QT6CT_VERSION}") +else() + message(FATAL_ERROR "invalid header") +endif() + +add_definitions(-DQT6CT_LIBRARY) + +set(app_SRCS + qt6ct/src/qt6ct-common/qt6ct.cpp +) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/qt6ct/src/qt6ct-common) + +add_library(qt6ct-common STATIC ${app_SRCS}) +set_target_properties(qt6ct-common PROPERTIES VERSION ${QT6CT_VERSION}) +target_link_libraries(qt6ct-common PRIVATE Qt6::Gui) +install(TARGETS qt6ct-common DESTINATION ${CMAKE_INSTALL_LIBDIR}) + set_target_properties(qt6ct-common PROPERTIES PUBLIC_HEADER qt6ct/src/qt6ct-common/qt6ct.h) target_include_directories( qt6ct-common INTERFACE qt6ct/src ) -install(TARGETS qt6ct-common - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/qt6ct - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) From b5a89b51dbb5d83107b714299e6f493322db5964 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 26 Oct 2024 15:46:06 -0400 Subject: [PATCH 155/762] Switch to using qt6ct palettes from submodule instead of copy --- scwx-qt/res/qt6ct_colors/airy.conf | 4 ---- scwx-qt/res/qt6ct_colors/darker.conf | 4 ---- scwx-qt/res/qt6ct_colors/dusk.conf | 4 ---- scwx-qt/res/qt6ct_colors/ia_ora.conf | 4 ---- scwx-qt/res/qt6ct_colors/sand.conf | 4 ---- scwx-qt/res/qt6ct_colors/simple.conf | 4 ---- scwx-qt/res/qt6ct_colors/waves.conf | 4 ---- scwx-qt/scwx-qt.qrc | 14 +++++++------- 8 files changed, 7 insertions(+), 35 deletions(-) delete mode 100644 scwx-qt/res/qt6ct_colors/airy.conf delete mode 100644 scwx-qt/res/qt6ct_colors/darker.conf delete mode 100644 scwx-qt/res/qt6ct_colors/dusk.conf delete mode 100644 scwx-qt/res/qt6ct_colors/ia_ora.conf delete mode 100644 scwx-qt/res/qt6ct_colors/sand.conf delete mode 100644 scwx-qt/res/qt6ct_colors/simple.conf delete mode 100644 scwx-qt/res/qt6ct_colors/waves.conf diff --git a/scwx-qt/res/qt6ct_colors/airy.conf b/scwx-qt/res/qt6ct_colors/airy.conf deleted file mode 100644 index 68b32473..00000000 --- a/scwx-qt/res/qt6ct_colors/airy.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ff000000, #ffdcdcdc, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff000000, #ff0a0a0a, #ff0a0a0a, #ffc8c8c8, #ffffffff, #ffe7e4e0, #ff0986d3, #ff0a0a0a, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ff050505, #80000000 -disabled_colors=#ffffffff, #ff424245, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff808080, #ffffffff, #ff808080, #ff969696, #ffc8c8c8, #ffe7e4e0, #ff0986d3, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ffffffff, #80000000 -inactive_colors=#ff323232, #ffb4b4b4, #ffdcdcdc, #ff5e5c5b, #ff646464, #ffe1e1e1, #ff323232, #ff323232, #ff323232, #ff969696, #ffc8c8c8, #ffe7e4e0, #ff0986d3, #ff323232, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff646464, #ff323232, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/darker.conf b/scwx-qt/res/qt6ct_colors/darker.conf deleted file mode 100644 index cf2f69f7..00000000 --- a/scwx-qt/res/qt6ct_colors/darker.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff -disabled_colors=#ff808080, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ffffffff, #ff808080, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff -inactive_colors=#ffffffff, #ff424245, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffffffff, #ffffffff, #ffffffff, #ff3d3d3d, #ff222020, #ffe7e4e0, #ff12608a, #fff9f9f9, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff3f3f36, #ffffffff, #80ffffff diff --git a/scwx-qt/res/qt6ct_colors/dusk.conf b/scwx-qt/res/qt6ct_colors/dusk.conf deleted file mode 100644 index e0b0059a..00000000 --- a/scwx-qt/res/qt6ct_colors/dusk.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ff000000, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ff7f7f7f, #ff7f7f7f, #ff707070, #ff308cc6, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 -disabled_colors=#ffbebebe, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ffbebebe, #ffffffff, #ffbebebe, #ff7f7f7f, #ff7f7f7f, #ffb1aeab, #ff7f7f7f, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 -inactive_colors=#ff000000, #ff7f7f7f, #ffffffff, #ffcbc7c4, #ff7f7f7f, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ff7f7f7f, #ff7f7f7f, #ff707070, #ff308cc6, #ffffffff, #ff0000ff, #ffff00ff, #ff7f7f7f, #ff000000, #ff7f7f7f, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/ia_ora.conf b/scwx-qt/res/qt6ct_colors/ia_ora.conf deleted file mode 100644 index 7875fc4d..00000000 --- a/scwx-qt/res/qt6ct_colors/ia_ora.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 -disabled_colors=#ff808080, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff808080, #ffffffff, #ff808080, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ff808080, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 -inactive_colors=#ff000000, #ffeff3f7, #ffffffff, #ffe9e7e3, #ffc7cbce, #ffa0a0a4, #ff000000, #ffffffff, #ff000000, #ffeff3f7, #ffeff3f7, #ffb8bbbe, #ff4965ae, #ffffffff, #ff0000ff, #ffff00ff, #ffeff3f7, #ff000000, #ffffffdc, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/sand.conf b/scwx-qt/res/qt6ct_colors/sand.conf deleted file mode 100644 index 004267d1..00000000 --- a/scwx-qt/res/qt6ct_colors/sand.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ff000000, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff000000, #ff000000, #ff000000, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 -disabled_colors=#ff4a4947, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff4a4947, #ff4a4947, #ff4a4947, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 -inactive_colors=#ff000000, #ffffffdc, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff000000, #ff000000, #ff000000, #ffffffdc, #ffffffdc, #ffe7e4e0, #ff5f5b5d, #fff9f9f9, #ff0986d3, #ffa70b06, #ffffffdc, #ff000000, #ff3f3f36, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/simple.conf b/scwx-qt/res/qt6ct_colors/simple.conf deleted file mode 100644 index 7b655ad5..00000000 --- a/scwx-qt/res/qt6ct_colors/simple.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ff000000, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ffffffff, #ffefebe7, #ffb1aeab, #ff308cc6, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 -disabled_colors=#ffbebebe, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ffbebebe, #ffffffff, #ffbebebe, #ffefebe7, #ffefebe7, #ffb1aeab, #ff9f9d9a, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 -inactive_colors=#ff000000, #ffefebe7, #ffffffff, #ffcbc7c4, #ff9f9d9a, #ffb8b5b2, #ff000000, #ffffffff, #ff000000, #ffffffff, #ffefebe7, #ffb1aeab, #ff308cc6, #ffffffff, #ff0000ff, #ffff0000, #fff7f5f3, #ff000000, #ffffffdc, #ff000000, #80000000 diff --git a/scwx-qt/res/qt6ct_colors/waves.conf b/scwx-qt/res/qt6ct_colors/waves.conf deleted file mode 100644 index e0de92fd..00000000 --- a/scwx-qt/res/qt6ct_colors/waves.conf +++ /dev/null @@ -1,4 +0,0 @@ -[ColorScheme] -active_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 -disabled_colors=#ff808080, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ff808080, #ff808080, #ff808080, #ff00071d, #ff00071d, #ffb0b0b0, #ff00071d, #ff808080, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 -inactive_colors=#ffb0b0b0, #ff010b2c, #ff979797, #ff5e5c5b, #ff302f2e, #ff4a4947, #ffb0b0b0, #ffb0b0b0, #ffb0b0b0, #ff010b2c, #ff010b2c, #ffb0b0b0, #ff302f2e, #ffb0b0b0, #ff0986d3, #ffa70b06, #ff5c5b5a, #ffffffff, #ff0a0a0a, #ffffffff, #80b0b0b0 diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index fbe846ca..cca0f62c 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -70,13 +70,13 @@ res/palettes/wct/SW.pal res/palettes/wct/VIL.pal res/palettes/wct/ZDR.pal - res/qt6ct_colors/airy.conf - res/qt6ct_colors/darker.conf - res/qt6ct_colors/dusk.conf - res/qt6ct_colors/ia_ora.conf - res/qt6ct_colors/sand.conf - res/qt6ct_colors/simple.conf - res/qt6ct_colors/waves.conf + ../external/qt6ct/colors/airy.conf + ../external/qt6ct/colors/darker.conf + ../external/qt6ct/colors/dusk.conf + ../external/qt6ct/colors/ia_ora.conf + ../external/qt6ct/colors/sand.conf + ../external/qt6ct/colors/simple.conf + ../external/qt6ct/colors/waves.conf res/textures/lines/default-1x7.png res/textures/lines/test-pattern.png res/textures/images/cursor-17.png From ac40fd93b71eeeb1868520605aeb2001983fb54b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Oct 2024 09:02:29 -0500 Subject: [PATCH 156/762] Ensure widgets are always updated on the main thread Fixes crashes and widget freezes --- scwx-qt/source/scwx/qt/main/main_window.cpp | 25 +++++++++-------- scwx-qt/source/scwx/qt/map/map_widget.cpp | 28 ++++++++++--------- scwx-qt/source/scwx/qt/ui/line_label.cpp | 30 ++++++++++++++------- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 2c744ff7..cebdab48 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -494,12 +494,11 @@ void MainWindow::on_actionOpenNexrad_triggered() map::MapWidget* currentMap = p->activeMap_; // Make sure the parent window properly repaints on close - connect( - dialog, - &QFileDialog::finished, - this, - [this]() { update(); }, - Qt::QueuedConnection); + connect(dialog, + &QFileDialog::finished, + this, + static_cast(&MainWindow::update), + Qt::QueuedConnection); connect( dialog, @@ -560,12 +559,11 @@ void MainWindow::on_actionOpenTextEvent_triggered() dialog->setAttribute(Qt::WA_DeleteOnClose); // Make sure the parent window properly repaints on close - connect( - dialog, - &QFileDialog::finished, - this, - [this]() { update(); }, - Qt::QueuedConnection); + connect(dialog, + &QFileDialog::finished, + this, + static_cast(&MainWindow::update), + Qt::QueuedConnection); connect(dialog, &QFileDialog::fileSelected, @@ -1003,7 +1001,8 @@ void MainWindowImpl::ConnectAnimationSignals() { for (auto map : maps_) { - map->update(); + QMetaObject::invokeMethod( + map, static_cast(&QWidget::update)); } }); connect(timelineManager_.get(), diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index da5bef38..d3c7fefc 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -357,7 +357,7 @@ void MapWidgetImpl::ConnectSignals() connect(placefileManager_.get(), &manager::PlacefileManager::PlacefileUpdated, widget_, - [this]() { widget_->update(); }); + static_cast(&QWidget::update)); // When the layer model changes, update the layers connect(layerModel_.get(), @@ -903,7 +903,8 @@ void MapWidget::SelectTime(std::chrono::system_clock::time_point time) void MapWidget::SetActive(bool isActive) { p->context_->settings().isActive_ = isActive; - update(); + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void MapWidget::SetAutoRefresh(bool enabled) @@ -1026,7 +1027,8 @@ void MapWidget::UpdateMouseCoordinate(const common::Coordinate& coordinate) if (keyboardModifiers != Qt::KeyboardModifier::NoModifier || keyboardModifiers != p->lastKeyboardModifiers_) { - update(); + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } p->lastKeyboardModifiers_ = keyboardModifiers; @@ -1292,7 +1294,7 @@ void MapWidgetImpl::AddPlacefileLayer(const std::string& placefileName, connect(placefileLayer.get(), &PlacefileLayer::DataReloaded, widget_, - [this]() { widget_->update(); }); + static_cast(&QWidget::update)); } std::string @@ -1319,7 +1321,7 @@ void MapWidgetImpl::AddLayer(const std::string& id, connect(layer.get(), &GenericLayer::NeedsRendering, widget_, - [this]() { widget_->update(); }); + static_cast(&QWidget::update)); } catch (const std::exception&) { @@ -1825,12 +1827,11 @@ void MapWidgetImpl::RadarProductViewConnect() if (radarProductView != nullptr) { - connect( - radarProductView.get(), - &view::RadarProductView::ColorTableLutUpdated, - this, - [this]() { widget_->update(); }, - Qt::QueuedConnection); + connect(radarProductView.get(), + &view::RadarProductView::ColorTableLutUpdated, + widget_, + static_cast(&QWidget::update), + Qt::QueuedConnection); connect( radarProductView.get(), &view::RadarProductView::SweepComputed, @@ -1863,7 +1864,7 @@ void MapWidgetImpl::RadarProductViewDisconnect() { disconnect(radarProductView.get(), &view::RadarProductView::ColorTableLutUpdated, - this, + widget_, nullptr); disconnect(radarProductView.get(), &view::RadarProductView::SweepComputed, @@ -1913,7 +1914,8 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) void MapWidgetImpl::Update() { - widget_->update(); + QMetaObject::invokeMethod( + widget_, static_cast(&QWidget::update)); if (UpdateStoredMapParameters()) { diff --git a/scwx-qt/source/scwx/qt/ui/line_label.cpp b/scwx-qt/source/scwx/qt/ui/line_label.cpp index 0b2f8f75..03dbcaf6 100644 --- a/scwx-qt/source/scwx/qt/ui/line_label.cpp +++ b/scwx-qt/source/scwx/qt/ui/line_label.cpp @@ -81,45 +81,57 @@ void LineLabel::set_border_width(std::size_t width) { p->borderWidth_ = width; p->pixmapDirty_ = true; - updateGeometry(); - update(); + + QMetaObject::invokeMethod(this, &QWidget::updateGeometry); + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_highlight_width(std::size_t width) { p->highlightWidth_ = width; p->pixmapDirty_ = true; - updateGeometry(); - update(); + + QMetaObject::invokeMethod(this, &QWidget::updateGeometry); + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_line_width(std::size_t width) { p->lineWidth_ = width; p->pixmapDirty_ = true; - updateGeometry(); - update(); + + QMetaObject::invokeMethod(this, &QWidget::updateGeometry); + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_border_color(boost::gil::rgba8_pixel_t color) { p->borderColor_ = color; p->pixmapDirty_ = true; - update(); + + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_highlight_color(boost::gil::rgba8_pixel_t color) { p->highlightColor_ = color; p->pixmapDirty_ = true; - update(); + + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_line_color(boost::gil::rgba8_pixel_t color) { p->lineColor_ = color; p->pixmapDirty_ = true; - update(); + + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); } void LineLabel::set_line_settings(settings::LineSettings& lineSettings) From 8e114c555fe4d15ab9e237ede8dd341cba56efbc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 28 Oct 2024 10:14:49 -0400 Subject: [PATCH 157/762] Update CMake files to possibly work on windows --- external/CMakeLists.txt | 3 ++- external/qt6ct.cmake | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 0a32377e..5ef39ddc 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -11,7 +11,8 @@ set_property(DIRECTORY maplibre-native-qt.cmake stb.cmake textflowcpp.cmake - units.cmake) + units.cmake + qt6ct.cmake) include(aws-sdk-cpp.cmake) include(date.cmake) diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake index 9ea8b8e3..9c1c177a 100644 --- a/external/qt6ct.cmake +++ b/external/qt6ct.cmake @@ -1,6 +1,13 @@ cmake_minimum_required(VERSION 3.16.0) set(PROJECT_NAME scwx-qt6ct) +find_package(QT NAMES Qt6 + COMPONENTS Gui + REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} + COMPONENTS Gui + REQUIRED) + #extract version from qt6ct.h file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/qt6ct/src/qt6ct-common/qt6ct.h" QT6CT_VERSION_DATA REGEX "^#define[ \t]+QT6CT_VERSION_[A-Z]+[ \t]+[0-9]+.*$") @@ -17,18 +24,20 @@ else() message(FATAL_ERROR "invalid header") endif() -add_definitions(-DQT6CT_LIBRARY) - set(app_SRCS qt6ct/src/qt6ct-common/qt6ct.cpp ) -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/qt6ct/src/qt6ct-common) - add_library(qt6ct-common STATIC ${app_SRCS}) set_target_properties(qt6ct-common PROPERTIES VERSION ${QT6CT_VERSION}) target_link_libraries(qt6ct-common PRIVATE Qt6::Gui) -install(TARGETS qt6ct-common DESTINATION ${CMAKE_INSTALL_LIBDIR}) +target_compile_definitions(qt6ct-common PRIVATE QT6CT_LIBRARY) + +if (MSVC) + # Produce PDB file for debug + target_compile_options(qt6ct-common PRIVATE "$<$:/Zi>") +else() + target_compile_options(qt6ct-common PRIVATE "$<$:-g>") +endif() -set_target_properties(qt6ct-common PROPERTIES PUBLIC_HEADER qt6ct/src/qt6ct-common/qt6ct.h) target_include_directories( qt6ct-common INTERFACE qt6ct/src ) From ccf5f6a3d827172bb97359d3699bb19de3be8c3d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 28 Oct 2024 13:00:39 -0400 Subject: [PATCH 158/762] Added custom theme file selection dialog --- scwx-qt/source/scwx/qt/main/main.cpp | 16 +- .../scwx/qt/settings/general_settings.cpp | 9 + .../scwx/qt/settings/general_settings.hpp | 1 + scwx-qt/source/scwx/qt/types/qt_types.cpp | 3 + scwx-qt/source/scwx/qt/types/qt_types.hpp | 3 +- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 41 ++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 460 ++++++++++-------- 7 files changed, 311 insertions(+), 222 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 11b7f098..05b7c933 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -166,14 +166,20 @@ static void ConfigureTheme(const std::vector& args) QGuiApplication::styleHints()->setColorScheme(qtColorScheme); - std::optional paletteFile = - scwx::qt::types::GetQtPaletteFile(uiStyle); + std::optional paletteFile; + if (uiStyle == scwx::qt::types::UiStyle::FusionCustom) { + paletteFile = generalSettings.theme_file().GetValue(); + } + else + { + paletteFile = scwx::qt::types::GetQtPaletteFile(uiStyle); + } + if (paletteFile) { QPalette defaultPalette = QApplication::style()->standardPalette(); - QPalette palette = - Qt6CT::loadColorScheme(QString::fromStdString(*paletteFile), - defaultPalette); + QPalette palette = Qt6CT::loadColorScheme( + QString::fromStdString(*paletteFile), defaultPalette); if (defaultPalette == palette) { diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index fe3982e2..9eca4348 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -73,6 +73,7 @@ public: showMapCenter_.SetDefault(false); showMapLogo_.SetDefault(true); theme_.SetDefault(defaultThemeValue); + themeFile_.SetDefault(""); trackLocation_.SetDefault(false); updateNotificationsEnabled_.SetDefault(true); warningsProvider_.SetDefault(defaultWarningsProviderValue); @@ -161,6 +162,7 @@ public: SettingsVariable showMapCenter_ {"show_map_center"}; SettingsVariable showMapLogo_ {"show_map_logo"}; SettingsVariable theme_ {"theme"}; + SettingsVariable themeFile_ {"theme_file"}; SettingsVariable trackLocation_ {"track_location"}; SettingsVariable updateNotificationsEnabled_ {"update_notifications"}; SettingsVariable warningsProvider_ {"warnings_provider"}; @@ -193,6 +195,7 @@ GeneralSettings::GeneralSettings() : &p->showMapCenter_, &p->showMapLogo_, &p->theme_, + &p->themeFile_, &p->trackLocation_, &p->updateNotificationsEnabled_, &p->warningsProvider_}); @@ -325,6 +328,11 @@ SettingsVariable& GeneralSettings::theme() const return p->theme_; } +SettingsVariable& GeneralSettings::theme_file() const +{ + return p->themeFile_; +} + SettingsVariable& GeneralSettings::track_location() const { return p->trackLocation_; @@ -385,6 +393,7 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->showMapCenter_ == rhs.p->showMapCenter_ && lhs.p->showMapLogo_ == rhs.p->showMapLogo_ && lhs.p->theme_ == rhs.p->theme_ && + lhs.p->themeFile_ == rhs.p->themeFile_ && lhs.p->trackLocation_ == rhs.p->trackLocation_ && lhs.p->updateNotificationsEnabled_ == rhs.p->updateNotificationsEnabled_ && diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 2628aef1..3e527238 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -49,6 +49,7 @@ public: SettingsVariable& show_map_center() const; SettingsVariable& show_map_logo() const; SettingsVariable& theme() const; + SettingsVariable& theme_file() const; SettingsVariable& track_location() const; SettingsVariable& update_notifications_enabled() const; SettingsVariable& warnings_provider() const; diff --git a/scwx-qt/source/scwx/qt/types/qt_types.cpp b/scwx-qt/source/scwx/qt/types/qt_types.cpp index 5e5a96a7..0c10feb2 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.cpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.cpp @@ -21,6 +21,7 @@ static const std::unordered_map qtStyleName_ { {UiStyle::FusionIaOra, "Fusion"}, {UiStyle::FusionSand, "Fusion"}, {UiStyle::FusionWaves, "Fusion"}, + {UiStyle::FusionCustom, "Fusion"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map uiStyleName_ { @@ -34,6 +35,7 @@ static const std::unordered_map uiStyleName_ { {UiStyle::FusionIaOra, "Fusion IA Ora"}, {UiStyle::FusionSand, "Fusion Sand"}, {UiStyle::FusionWaves, "Fusion Waves"}, + {UiStyle::FusionCustom, "Fusion Custom"}, {UiStyle::Unknown, "?"}}; static const std::unordered_map qtColorSchemeMap_ { @@ -47,6 +49,7 @@ static const std::unordered_map qtColorSchemeMap_ { {UiStyle::FusionIaOra, Qt::ColorScheme::Unknown}, {UiStyle::FusionSand, Qt::ColorScheme::Unknown}, {UiStyle::FusionWaves, Qt::ColorScheme::Unknown}, + {UiStyle::FusionCustom, Qt::ColorScheme::Unknown}, {UiStyle::Unknown, Qt::ColorScheme::Unknown}}; static const std::unordered_map paletteFile_ { diff --git a/scwx-qt/source/scwx/qt/types/qt_types.hpp b/scwx-qt/source/scwx/qt/types/qt_types.hpp index 9bb7ed5d..b5779ff1 100644 --- a/scwx-qt/source/scwx/qt/types/qt_types.hpp +++ b/scwx-qt/source/scwx/qt/types/qt_types.hpp @@ -33,9 +33,10 @@ enum class UiStyle FusionIaOra, FusionSand, FusionWaves, + FusionCustom, Unknown }; -typedef scwx::util::Iterator +typedef scwx::util::Iterator UiStyleIterator; Qt::ColorScheme GetQtColorScheme(UiStyle uiStyle); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index d4f4f096..20616204 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -121,6 +121,7 @@ public: &mapboxApiKey_, &mapTilerApiKey_, &theme_, + &themeFile_, &defaultAlertAction_, &clockFormat_, &customStyleDrawLayer_, @@ -242,6 +243,7 @@ public: settings::SettingsInterface nmeaBaudRate_ {}; settings::SettingsInterface nmeaSource_ {}; settings::SettingsInterface theme_ {}; + settings::SettingsInterface themeFile_ {}; settings::SettingsInterface warningsProvider_ {}; settings::SettingsInterface antiAliasingEnabled_ {}; settings::SettingsInterface showMapAttribution_ {}; @@ -526,6 +528,45 @@ void SettingsDialogImpl::SetupGeneralTab() types::GetUiStyleName); theme_.SetResetButton(self_->ui->resetThemeButton); + themeFile_.SetSettingsVariable(generalSettings.theme_file()); + themeFile_.SetEditWidget(self_->ui->themeFileLineEdit); + themeFile_.SetResetButton(self_->ui->resetThemeFileButton); + //themeFile_.EnableTrimming(); + + QObject::connect( + self_->ui->themeFileSelectButton, + &QAbstractButton::clicked, + self_, + [this]() + { + static const std::string themeFilter = "Qt6Ct Theme File (*.conf)"; + static const std::string allFilter = "All Files (*)"; + + QFileDialog* dialog = new QFileDialog(self_); + + dialog->setFileMode(QFileDialog::ExistingFile); + + dialog->setNameFilters( + {QObject::tr(themeFilter.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 theme file: {}", path.toStdString()); + self_->ui->themeFileLineEdit->setText(path); + + // setText dows not emit the textEdited signal + Q_EMIT self_->ui->themeFileLineEdit->textEdited(path); + }); + + dialog->open(); + }); + auto radarSites = config::RadarSite::GetAll(); // Sort radar sites by ID diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 444e6705..f66aba6c 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - 0 - 513 - 622 + -133 + 511 + 676 @@ -159,15 +159,39 @@ 0 - - + + - MapTiler API Key + ... - - + + + + Mapbox API Key + + + + + + + Custom Map Layer + + + + + + + + + + GPS Plugin + + + + + ... @@ -180,15 +204,18 @@ - - + + + + + - Default Time Zone + ... - - + + ... @@ -198,30 +225,34 @@ - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Map Provider - - - + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + @@ -233,77 +264,9 @@ - - - - - - - - - - ... - - - - - - - QLineEdit::EchoMode::Password - - - - - - - GPS Plugin - - - - - - - GPS Baud Rate - - - - - - - - - - ... - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Grid Width - - - - - - - - - @@ -311,9 +274,16 @@ - + + + + + Warnings Provider + + + @@ -339,8 +309,8 @@ - - + + ... @@ -350,45 +320,66 @@ - - - - Theme + + + + 1 + + + 999999999 - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Mapbox API Key - - - - - - - Clock Format - - - - - + + QLineEdit::EchoMode::Password + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + GPS Baud Rate + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + @@ -400,8 +391,8 @@ - - + + ... @@ -411,35 +402,56 @@ + + + + Map Provider + + + + + + + + + + Theme + + + + + + + MapTiler API Key + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + Custom Map URL + + + - - - - Warnings Provider - - - - - - - 1 - - - 999999999 - - - - - - - Default Alert Action - - - - - + + ... @@ -449,6 +461,38 @@ + + + + Clock Format + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Grid Width + + + + + + + QLineEdit::EchoMode::Password + + + @@ -460,61 +504,45 @@ - - + + + + Default Time Zone + + + + + + + Default Alert Action + + + + + + + + + + + + + Theme File + + + + + + + + ... - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - - - - Custom Map URL - - - - - - - Custom Map Layer - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - + + ... @@ -610,8 +638,8 @@ 0 0 - 506 - 383 + 98 + 28 From 158a4171d232f6b2ae75968e0c7e954ae37b4ada Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 29 Oct 2024 09:01:32 -0400 Subject: [PATCH 159/762] Added theme_file to settings test cases --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 40a367ca..a642d730 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 40a367ca89b5b197353ca58dea547a3e3407c7f3 +Subproject commit a642d730bd8d6c9b291b90e61b3a3a389139f2f6 From 20b8c0da7d648afcb80f259d4589c586db3d6e25 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 29 Oct 2024 09:02:04 -0400 Subject: [PATCH 160/762] Add QT6CT_LIBRARY to avoid Q_DECL_IMPORT in include --- scwx-qt/source/scwx/qt/main/main.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 05b7c933..0e379961 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -31,6 +30,10 @@ #include #include +#define QT6CT_LIBRARY +#include +#undef QT6CT_LIBRARY + static const std::string logPrefix_ = "scwx::main"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); From e16db1823da721743841f1ecb200b1c9bb581bbd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 29 Oct 2024 11:25:16 -0400 Subject: [PATCH 161/762] Add disabling of theme file when custom theme is not selected --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 20616204..2d129355 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -528,10 +528,24 @@ void SettingsDialogImpl::SetupGeneralTab() types::GetUiStyleName); theme_.SetResetButton(self_->ui->resetThemeButton); + QObject::connect( + self_->ui->themeComboBox, + &QComboBox::currentTextChanged, + self_, + [this](const QString& text) + { + types::UiStyle style = types::GetUiStyle(text.toStdString()); + bool themeFileEnabled = style == types::UiStyle::FusionCustom; + + self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); + self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); + self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); + }); + themeFile_.SetSettingsVariable(generalSettings.theme_file()); themeFile_.SetEditWidget(self_->ui->themeFileLineEdit); themeFile_.SetResetButton(self_->ui->resetThemeFileButton); - //themeFile_.EnableTrimming(); + themeFile_.EnableTrimming(); QObject::connect( self_->ui->themeFileSelectButton, From 7a070b3e7f7227aeb8626a9254e561192c21c3fd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 3 Nov 2024 14:16:56 -0500 Subject: [PATCH 162/762] Move MarkerManager to using an ID system for markers, instead of index --- .../source/scwx/qt/manager/marker_manager.cpp | 85 +++++++++++++++++-- .../source/scwx/qt/manager/marker_manager.hpp | 16 ++-- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 16 ++-- scwx-qt/source/scwx/qt/model/marker_model.cpp | 66 +++++++++++--- scwx-qt/source/scwx/qt/model/marker_model.hpp | 8 +- scwx-qt/source/scwx/qt/types/marker_types.hpp | 3 + .../scwx/qt/ui/marker_settings_widget.cpp | 32 ++++--- .../scwx/qt/model/marker_model.test.cpp | 81 +++++++++++++----- 8 files changed, 233 insertions(+), 74 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index c8ceda09..e91f03fb 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -38,6 +38,8 @@ public: std::string markerSettingsPath_ {""}; std::vector> markerRecords_ {}; + std::unordered_map idToIndex_ {}; + MarkerManager* self_; @@ -48,6 +50,10 @@ public: void ReadMarkerSettings(); void WriteMarkerSettings(); std::shared_ptr GetMarkerByName(const std::string& name); + + void InitalizeIds(); + types::MarkerId NewId(); + types::MarkerId lastId_; }; class MarkerManager::Impl::MarkerRecord @@ -88,6 +94,16 @@ public: } }; +void MarkerManager::Impl::InitalizeIds() +{ + lastId_ = 0; +} + +types::MarkerId MarkerManager::Impl::NewId() +{ + return ++lastId_; +} + void MarkerManager::Impl::InitializeMarkerSettings() { std::string appDataPath { @@ -109,6 +125,7 @@ void MarkerManager::Impl::InitializeMarkerSettings() void MarkerManager::Impl::ReadMarkerSettings() { logger_->info("Reading location marker settings"); + InitalizeIds(); boost::json::value markerJson = nullptr; { @@ -125,6 +142,7 @@ void MarkerManager::Impl::ReadMarkerSettings() // For each marker entry auto& markerArray = markerJson.as_array(); markerRecords_.reserve(markerArray.size()); + idToIndex_.reserve(markerArray.size()); for (auto& markerEntry : markerArray) { try @@ -134,8 +152,12 @@ void MarkerManager::Impl::ReadMarkerSettings() if (!record.markerInfo_.name.empty()) { + types::MarkerId id = NewId(); + size_t index = markerRecords_.size(); + record.markerInfo_.id = id; markerRecords_.emplace_back( std::make_shared(record.markerInfo_)); + idToIndex_.emplace(id, index); } } catch (const std::exception& ex) @@ -206,11 +228,17 @@ size_t MarkerManager::marker_count() return p->markerRecords_.size(); } -std::optional MarkerManager::get_marker(size_t index) +std::optional MarkerManager::get_marker(types::MarkerId id) { std::shared_lock lock(p->markerRecordLock_); + if (!p->idToIndex_.contains(id)) + { + return {}; + } + size_t index = p->idToIndex_[id]; if (index >= p->markerRecords_.size()) { + logger_->warn("id in idToIndex_ but out of range!"); return {}; } std::shared_ptr& markerRecord = @@ -218,45 +246,81 @@ std::optional MarkerManager::get_marker(size_t index) return markerRecord->toMarkerInfo(); } -void MarkerManager::set_marker(size_t index, const types::MarkerInfo& marker) +std::optional MarkerManager::get_index(types::MarkerId id) +{ + std::shared_lock lock(p->markerRecordLock_); + if (!p->idToIndex_.contains(id)) + { + return {}; + } + return p->idToIndex_[id]; +} + +void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& marker) { { std::unique_lock lock(p->markerRecordLock_); + if (!p->idToIndex_.contains(id)) + { + return; + } + size_t index = p->idToIndex_[id]; if (index >= p->markerRecords_.size()) { + logger_->warn("id in idToIndex_ but out of range!"); return; } std::shared_ptr& markerRecord = p->markerRecords_[index]; markerRecord->markerInfo_ = marker; } - Q_EMIT MarkerChanged(index); + Q_EMIT MarkerChanged(id); Q_EMIT MarkersUpdated(); } void MarkerManager::add_marker(const types::MarkerInfo& marker) { + types::MarkerId id; { std::unique_lock lock(p->markerRecordLock_); + id = p->NewId(); + size_t index = p->markerRecords_.size(); + p->idToIndex_.emplace(id, index); p->markerRecords_.emplace_back(std::make_shared(marker)); + p->markerRecords_[index]->markerInfo_.id = id; } - Q_EMIT MarkerAdded(); + Q_EMIT MarkerAdded(id); Q_EMIT MarkersUpdated(); } -void MarkerManager::remove_marker(size_t index) +void MarkerManager::remove_marker(types::MarkerId id) { { std::unique_lock lock(p->markerRecordLock_); + if (!p->idToIndex_.contains(id)) + { + return; + } + size_t index = p->idToIndex_[id]; if (index >= p->markerRecords_.size()) { + logger_->warn("id in idToIndex_ but out of range!"); return; } p->markerRecords_.erase(std::next(p->markerRecords_.begin(), index)); + p->idToIndex_.erase(id); + + for (auto& pair : p->idToIndex_) + { + if (pair.second > index) + { + p->idToIndex_[pair.first] = pair.second - 1; + } + } } - Q_EMIT MarkerRemoved(index); + Q_EMIT MarkerRemoved(id); Q_EMIT MarkersUpdated(); } @@ -292,6 +356,15 @@ void MarkerManager::move_marker(size_t from, size_t to) Q_EMIT MarkersUpdated(); } +void MarkerManager::for_each(std::function func) +{ + std::shared_lock lock(p->markerRecordLock_); + for (auto marker : p->markerRecords_) + { + func(marker->markerInfo_); + } +} + // Only use for testing void MarkerManager::set_marker_settings_path(const std::string& path) { diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 2166725f..37ec1c31 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -12,6 +12,7 @@ namespace qt namespace manager { +typedef void MarkerForEachFunc(const types::MarkerInfo&); class MarkerManager : public QObject { Q_OBJECT @@ -21,12 +22,15 @@ public: ~MarkerManager(); size_t marker_count(); - std::optional get_marker(size_t index); - void set_marker(size_t index, const types::MarkerInfo& marker); + std::optional get_marker(types::MarkerId id); + std::optional get_index(types::MarkerId id); + void set_marker(types::MarkerId id, const types::MarkerInfo& marker); void add_marker(const types::MarkerInfo& marker); - void remove_marker(size_t index); + void remove_marker(types::MarkerId id); void move_marker(size_t from, size_t to); + void for_each(std::function func); + // Only use for testing void set_marker_settings_path(const std::string& path); @@ -35,9 +39,9 @@ public: signals: void MarkersInitialized(size_t count); void MarkersUpdated(); - void MarkerChanged(size_t index); - void MarkerAdded(); - void MarkerRemoved(size_t index); + void MarkerChanged(types::MarkerId id); + void MarkerAdded(types::MarkerId id); + void MarkerRemoved(types::MarkerId id); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index ab97322f..0480a63e 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -55,17 +55,13 @@ void MarkerLayer::Impl::ReloadMarkers() geoIcons_->StartIcons(); - for (size_t i = 0; i < markerManager->marker_count(); i++) - { - std::optional marker = markerManager->get_marker(i); - if (!marker) + markerManager->for_each( + [this](const types::MarkerInfo& marker) { - break; - } - std::shared_ptr icon = geoIcons_->AddIcon(); - geoIcons_->SetIconTexture(icon, markerIconName_, 0); - geoIcons_->SetIconLocation(icon, marker->latitude, marker->longitude); - } + std::shared_ptr icon = geoIcons_->AddIcon(); + geoIcons_->SetIconTexture(icon, markerIconName_, 0); + geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); + }); geoIcons_->FinishIcons(); Q_EMIT self_->NeedsRendering(); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 8bcd93fa..97cafcb1 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include namespace scwx @@ -30,6 +32,7 @@ public: ~Impl() = default; std::shared_ptr markerManager_ { manager::MarkerManager::Instance()}; + std::vector markerIds_; }; MarkerModel::MarkerModel(QObject* parent) : @@ -63,7 +66,7 @@ int MarkerModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : - static_cast(p->markerManager_->marker_count()); + static_cast(p->markerIds_.size()); } int MarkerModel::columnCount(const QModelIndex& parent) const @@ -95,15 +98,19 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const static const char COORDINATE_FORMAT = 'g'; static const int COORDINATE_PRECISION = 10; - if (!index.isValid() || index.row() < 0) + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= p->markerIds_.size()) { + logger_->debug("Failed to get data index {}", index.row()); return QVariant(); } + types::MarkerId id = p->markerIds_[index.row()]; std::optional markerInfo = - p->markerManager_->get_marker(index.row()); + p->markerManager_->get_marker(id); if (!markerInfo) { + logger_->debug("Failed to get data index {} id {}", index.row(), id); return QVariant(); } @@ -154,6 +161,16 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QVariant(); } +std::optional MarkerModel::getId(int index) +{ + if (index < 0 || static_cast(index) >= p->markerIds_.size()) + { + return {}; + } + + return p->markerIds_[index]; +} + QVariant MarkerModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -186,12 +203,16 @@ bool MarkerModel::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0) + + if (!index.isValid() || index.row() < 0 || + static_cast(index.row()) >= p->markerIds_.size()) { return false; } + + types::MarkerId id = p->markerIds_[index.row()]; std::optional markerInfo = - p->markerManager_->get_marker(index.row()); + p->markerManager_->get_marker(id); if (!markerInfo) { return false; @@ -205,7 +226,7 @@ bool MarkerModel::setData(const QModelIndex& index, { QString str = value.toString(); markerInfo->name = str.toStdString(); - p->markerManager_->set_marker(index.row(), *markerInfo); + p->markerManager_->set_marker(id, *markerInfo); result = true; } break; @@ -219,7 +240,7 @@ bool MarkerModel::setData(const QModelIndex& index, if (!str.isEmpty() && ok && -90 <= latitude && latitude <= 90) { markerInfo->latitude = latitude; - p->markerManager_->set_marker(index.row(), *markerInfo); + p->markerManager_->set_marker(id, *markerInfo); result = true; } } @@ -234,7 +255,7 @@ bool MarkerModel::setData(const QModelIndex& index, if (!str.isEmpty() && ok && -180 <= longitude && longitude <= 180) { markerInfo->longitude = longitude; - p->markerManager_->set_marker(index.row(), *markerInfo); + p->markerManager_->set_marker(id, *markerInfo); result = true; } } @@ -260,32 +281,49 @@ void MarkerModel::HandleMarkersInitialized(size_t count) } const int index = static_cast(count - 1); + p->markerIds_.reserve(count); beginInsertRows(QModelIndex(), 0, index); + p->markerManager_->for_each( + [this](const types::MarkerInfo& info) + { + p->markerIds_.push_back(info.id); + }); endInsertRows(); } -void MarkerModel::HandleMarkerAdded() +void MarkerModel::HandleMarkerAdded(types::MarkerId id) { - const int newIndex = static_cast(p->markerManager_->marker_count() - 1); + std::optional index = p->markerManager_->get_index(id); + const int newIndex = static_cast(*index); beginInsertRows(QModelIndex(), newIndex, newIndex); + p->markerIds_.emplace_back(id); endInsertRows(); } -void MarkerModel::HandleMarkerChanged(size_t index) +void MarkerModel::HandleMarkerChanged(types::MarkerId id) { - const int changedIndex = static_cast(index); + std::optional index = p->markerManager_->get_index(id); + const int changedIndex = static_cast(*index); + QModelIndex topLeft = createIndex(changedIndex, kFirstColumn); QModelIndex bottomRight = createIndex(changedIndex, kLastColumn); Q_EMIT dataChanged(topLeft, bottomRight); } -void MarkerModel::HandleMarkerRemoved(size_t index) +void MarkerModel::HandleMarkerRemoved(types::MarkerId id) { - const int removedIndex = static_cast(index); + auto it = std::find(p->markerIds_.begin(), p->markerIds_.end(), id); + if (it == p->markerIds_.end()) + { + return; + } + + const int removedIndex = std::distance(p->markerIds_.begin(), it); beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + p->markerIds_.erase(it); endRemoveRows(); } diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index 85112fa1..4fc6c95c 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace scwx { @@ -37,12 +38,13 @@ public: const QVariant& value, int role = Qt::EditRole) override; + std::optional getId(int index); public slots: void HandleMarkersInitialized(size_t count); - void HandleMarkerAdded(); - void HandleMarkerChanged(size_t index); - void HandleMarkerRemoved(size_t index); + void HandleMarkerAdded(types::MarkerId id); + void HandleMarkerChanged(types::MarkerId id); + void HandleMarkerRemoved(types::MarkerId id); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 0d9c575b..e3d28e26 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace scwx { @@ -8,6 +9,7 @@ namespace qt { namespace types { +typedef std::uint64_t MarkerId; struct MarkerInfo { @@ -16,6 +18,7 @@ struct MarkerInfo { } + MarkerId id; std::string name; double latitude; double longitude; diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index 8fc1fe6a..0c2bc614 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -65,21 +65,25 @@ void MarkerSettingsWidgetImpl::ConnectSignals() { markerManager_->add_marker(types::MarkerInfo("", 0, 0)); }); - QObject::connect(self_->ui->removeButton, - &QPushButton::clicked, - self_, - [this]() - { - auto selectionModel = - self_->ui->markerView->selectionModel(); - QModelIndex selected = - selectionModel - ->selectedRows(static_cast( - model::MarkerModel::Column::Name)) - .first(); + QObject::connect( + self_->ui->removeButton, + &QPushButton::clicked, + self_, + [this]() + { + auto selectionModel = self_->ui->markerView->selectionModel(); + QModelIndex selected = selectionModel + ->selectedRows(static_cast( + model::MarkerModel::Column::Name)) + .first(); + std::optional id = markerModel_->getId(selected.row()); + if (!id) + { + return; + } - markerManager_->remove_marker(selected.row()); - }); + markerManager_->remove_marker(*id); + }); QObject::connect( self_->ui->markerView->selectionModel(), &QItemSelectionModel::selectionChanged, diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp index b237e182..700ffc6e 100644 --- a/test/source/scwx/qt/model/marker_model.test.cpp +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -65,7 +65,7 @@ void RunTest(const std::string& filename, TestFunction testFunction) initialized = false; QObject::connect(manager.get(), &manager::MarkerManager::MarkersInitialized, - []() + [](size_t count) { std::unique_lock lock(initializedMutex); initialized = true; @@ -79,6 +79,8 @@ void RunTest(const std::string& filename, TestFunction testFunction) { initializedCond.wait(lock); } + // This is not jank + model.HandleMarkersInitialized(manager->marker_count()); testFunction(manager, model); } @@ -118,9 +120,17 @@ TEST(MarkerModelTest, AddRemove) RunTest(ONE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { manager->add_marker(types::MarkerInfo("Null", 0, 0)); }); - RunTest(EMPTY_MARKERS_FILE, - [](std::shared_ptr manager, MarkerModel&) - { manager->remove_marker(0); }); + RunTest( + EMPTY_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel& model) + { + std::optional id = model.getId(0); + EXPECT_TRUE(id); + if (id) + { + manager->remove_marker(*id); + } + }); std::filesystem::remove(TEMP_MARKERS_FILE); EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); @@ -165,15 +175,31 @@ TEST(MarkerModelTest, RemoveFive) { CopyFile(FIVE_MARKERS_FILE, TEMP_MARKERS_FILE); - RunTest(EMPTY_MARKERS_FILE, - [](std::shared_ptr manager, MarkerModel&) - { - manager->remove_marker(4); - manager->remove_marker(3); - manager->remove_marker(2); - manager->remove_marker(1); - manager->remove_marker(0); - }); + RunTest( + EMPTY_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel& model) + { + std::optional id; + id = model.getId(4); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(3); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(2); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(1); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(0); + EXPECT_TRUE(id); + manager->remove_marker(*id); + }); std::filesystem::remove(TEMP_MARKERS_FILE); EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); @@ -183,14 +209,27 @@ TEST(MarkerModelTest, RemoveFour) { CopyFile(FIVE_MARKERS_FILE, TEMP_MARKERS_FILE); - RunTest(ONE_MARKERS_FILE, - [](std::shared_ptr manager, MarkerModel&) - { - manager->remove_marker(4); - manager->remove_marker(3); - manager->remove_marker(2); - manager->remove_marker(1); - }); + RunTest( + ONE_MARKERS_FILE, + [](std::shared_ptr manager, MarkerModel& model) + { + std::optional id; + id = model.getId(4); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(3); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(2); + EXPECT_TRUE(id); + manager->remove_marker(*id); + + id = model.getId(1); + EXPECT_TRUE(id); + manager->remove_marker(*id); + }); std::filesystem::remove(TEMP_MARKERS_FILE); EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); From 3f52f792108b49f4e66305ca385e148d3d67ac83 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 3 Nov 2024 14:23:21 -0500 Subject: [PATCH 163/762] Fix spelling mistake in comment --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 2d129355..15c624b9 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -574,7 +574,7 @@ void SettingsDialogImpl::SetupGeneralTab() logger_->info("Selected theme file: {}", path.toStdString()); self_->ui->themeFileLineEdit->setText(path); - // setText dows not emit the textEdited signal + // setText does not emit the textEdited signal Q_EMIT self_->ui->themeFileLineEdit->textEdited(path); }); From 73d3eedcdc1e8a317d8f8305bb81cbcb7aa0d013 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 4 Nov 2024 09:51:52 -0500 Subject: [PATCH 164/762] Move connection creation earlier to cause it to be triggered on setting the current value to the combobox --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 15c624b9..a5a2f857 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -521,12 +521,6 @@ void SettingsDialogImpl::SetupGeneralTab() settings::GeneralSettings& generalSettings = settings::GeneralSettings::Instance(); - theme_.SetSettingsVariable(generalSettings.theme()); - SCWX_SETTINGS_COMBO_BOX(theme_, - self_->ui->themeComboBox, - types::UiStyleIterator(), - types::GetUiStyleName); - theme_.SetResetButton(self_->ui->resetThemeButton); QObject::connect( self_->ui->themeComboBox, @@ -542,6 +536,13 @@ void SettingsDialogImpl::SetupGeneralTab() self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); }); + theme_.SetSettingsVariable(generalSettings.theme()); + SCWX_SETTINGS_COMBO_BOX(theme_, + self_->ui->themeComboBox, + types::UiStyleIterator(), + types::GetUiStyleName); + theme_.SetResetButton(self_->ui->resetThemeButton); + themeFile_.SetSettingsVariable(generalSettings.theme_file()); themeFile_.SetEditWidget(self_->ui->themeFileLineEdit); themeFile_.SetResetButton(self_->ui->resetThemeFileButton); From 6dbd020591e62ae6b5df0d87c151403d82101a53 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 19 Oct 2024 13:59:32 -0500 Subject: [PATCH 165/762] Change radar product manager member variable initialization --- .../scwx/qt/manager/radar_product_manager.cpp | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 1c093e9b..8fb1fcef 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -91,13 +91,7 @@ public: const std::string& radarId, common::RadarProductGroup group, const std::string& product) : - radarId_ {radarId}, - group_ {group}, - product_ {product}, - refreshEnabled_ {false}, - refreshTimer_ {threadPool_}, - refreshTimerMutex_ {}, - provider_ {nullptr} + radarId_ {radarId}, group_ {group}, product_ {product} { connect(this, &ProviderManager::NewDataAvailable, @@ -115,10 +109,10 @@ public: const std::string radarId_; const common::RadarProductGroup group_; const std::string product_; - bool refreshEnabled_; - boost::asio::steady_timer refreshTimer_; - std::mutex refreshTimerMutex_; - std::shared_ptr provider_; + bool refreshEnabled_ {false}; + boost::asio::steady_timer refreshTimer_ {threadPool_}; + std::mutex refreshTimerMutex_ {}; + std::shared_ptr provider_ {nullptr}; signals: void NewDataAvailable(common::RadarProductGroup group, @@ -136,24 +130,8 @@ public: initialized_ {false}, level3ProductsInitialized_ {false}, radarSite_ {config::RadarSite::Get(radarId)}, - coordinates0_5Degree_ {}, - coordinates1Degree_ {}, - level2ProductRecords_ {}, - level2ProductRecentRecords_ {}, - level3ProductRecordsMap_ {}, - level3ProductRecentRecordsMap_ {}, - level2ProductRecordMutex_ {}, - level3ProductRecordMutex_ {}, level2ProviderManager_ {std::make_shared( - self_, radarId_, common::RadarProductGroup::Level2)}, - level3ProviderManagerMap_ {}, - level3ProviderManagerMutex_ {}, - initializeMutex_ {}, - level3ProductsInitializeMutex_ {}, - loadLevel2DataMutex_ {}, - loadLevel3DataMutex_ {}, - availableCategoryMap_ {}, - availableCategoryMutex_ {} + self_, radarId_, common::RadarProductGroup::Level2)} { if (radarSite_ == nullptr) { @@ -247,30 +225,30 @@ public: std::shared_ptr radarSite_; std::size_t cacheLimit_ {6u}; - std::vector coordinates0_5Degree_; - std::vector coordinates1Degree_; + std::vector coordinates0_5Degree_ {}; + std::vector coordinates1Degree_ {}; - RadarProductRecordMap level2ProductRecords_; - RadarProductRecordList level2ProductRecentRecords_; + RadarProductRecordMap level2ProductRecords_ {}; + RadarProductRecordList level2ProductRecentRecords_ {}; std::unordered_map - level3ProductRecordsMap_; + level3ProductRecordsMap_ {}; std::unordered_map - level3ProductRecentRecordsMap_; - std::shared_mutex level2ProductRecordMutex_; - std::shared_mutex level3ProductRecordMutex_; + level3ProductRecentRecordsMap_ {}; + std::shared_mutex level2ProductRecordMutex_ {}; + std::shared_mutex level3ProductRecordMutex_ {}; std::shared_ptr level2ProviderManager_; std::unordered_map> - level3ProviderManagerMap_; - std::shared_mutex level3ProviderManagerMutex_; + level3ProviderManagerMap_ {}; + std::shared_mutex level3ProviderManagerMutex_ {}; - std::mutex initializeMutex_; - std::mutex level3ProductsInitializeMutex_; - std::mutex loadLevel2DataMutex_; - std::mutex loadLevel3DataMutex_; + std::mutex initializeMutex_ {}; + std::mutex level3ProductsInitializeMutex_ {}; + std::mutex loadLevel2DataMutex_ {}; + std::mutex loadLevel3DataMutex_ {}; - common::Level3ProductCategoryMap availableCategoryMap_; - std::shared_mutex availableCategoryMutex_; + common::Level3ProductCategoryMap availableCategoryMap_ {}; + std::shared_mutex availableCategoryMutex_ {}; std::unordered_map, From 50b6a8dd9bab9b216c83fbe2237c6f0e479a864a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 20 Oct 2024 07:57:18 -0500 Subject: [PATCH 166/762] Animation dock widget should set 59 seconds for archive selections, instead of current time at initialization --- scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 12d94d27..e8519c47 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -65,6 +65,8 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : QDateTime currentDateTime = QDateTime::currentDateTimeUtc(); QDate currentDate = currentDateTime.date(); QTime currentTime = currentDateTime.time(); + currentTime = currentTime.addSecs(-currentTime.second() + 59); + ui->dateEdit->setDate(currentDate); ui->timeEdit->setTime(currentTime); ui->dateEdit->setMaximumDate(currentDateTime.date()); From b466ac818c6210634727993c7c1ae63775da7273 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 20 Oct 2024 08:02:19 -0500 Subject: [PATCH 167/762] Index multiple level 2 elevation scans --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 47 ++++++++++++++++--------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 6d951d6c..bdd1bcfc 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -65,7 +65,9 @@ public: std::map> radarData_ {}; std::map>> + std::map>>> index_ {}; std::list rawRecords_ {}; @@ -130,9 +132,9 @@ std::shared_ptr Ar2vFile::vcp_data() const } std::tuple, float, std::vector> -Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, - float elevation, - std::chrono::system_clock::time_point /*time*/) const +Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, + float elevation, + std::chrono::system_clock::time_point time) const { logger_->debug("GetElevationScan: {} degrees", elevation); @@ -152,6 +154,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, std::uint16_t lowerBound = scans.cbegin()->first; std::uint16_t upperBound = scans.crbegin()->first; + // Find closest elevation match for (auto& scan : scans) { if (scan.first > lowerBound && scan.first <= codedElevation) @@ -173,15 +176,25 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, std::abs(static_cast(codedElevation) - static_cast(upperBound)); - if (lowerDelta < upperDelta) + // Select closest elevation match + std::uint16_t elevationIndex = + (lowerDelta < upperDelta) ? lowerBound : upperBound; + elevationCut = elevationIndex / scaleFactor; + + // Select closest time match, not newer than the selected time + std::chrono::system_clock::time_point foundTime {}; + auto& elevationScans = scans.at(elevationIndex); + + for (auto& scan : elevationScans) { - elevationScan = scans.at(lowerBound); - elevationCut = lowerBound / scaleFactor; - } - else - { - elevationScan = scans.at(upperBound); - elevationCut = upperBound / scaleFactor; + auto& scanTime = scan.first; + + if (elevationScan == nullptr || + (scanTime <= time && scanTime > foundTime)) + { + elevationScan = scan.second; + foundTime = scanTime; + } } } @@ -460,8 +473,8 @@ void Ar2vFileImpl::IndexFile() waveformType = vcpData_->waveform_type(elevationCut.first); } else if ((digitalRadarData0 = - std::dynamic_pointer_cast( - (*elevationCut.second)[0])) != nullptr) + std::dynamic_pointer_cast(radial0)) != + nullptr) { elevationAngle = digitalRadarData0->elevation_angle_raw(); } @@ -488,8 +501,10 @@ void Ar2vFileImpl::IndexFile() if (momentData != nullptr) { - // TODO: Handle multiple elevation scans - index_[dataBlockType][elevationAngle] = elevationCut.second; + auto time = util::TimePoint(radial0->modified_julian_date(), + radial0->collection_time()); + + index_[dataBlockType][elevationAngle][time] = elevationCut.second; } } } From f1e35532b97a95cdab71f107a49d343e29e057b7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 20 Oct 2024 08:06:43 -0500 Subject: [PATCH 168/762] Initial handling of multiple time sweeps in individual level 2 files --- .../scwx/qt/manager/radar_product_manager.cpp | 147 ++++++++++++------ 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 8fb1fcef..2b201299 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -176,9 +176,9 @@ public: void RefreshData(std::shared_ptr providerManager); void RefreshDataSync(std::shared_ptr providerManager); - std::tuple, - std::chrono::system_clock::time_point> - GetLevel2ProductRecord(std::chrono::system_clock::time_point time); + std::map> + GetLevel2ProductRecords(std::chrono::system_clock::time_point time); std::tuple, std::chrono::system_clock::time_point> GetLevel3ProductRecord(const std::string& product, @@ -1151,14 +1151,15 @@ void RadarProductManagerImpl::PopulateProductTimes( }); } -std::tuple, - std::chrono::system_clock::time_point> -RadarProductManagerImpl::GetLevel2ProductRecord( +std::map> +RadarProductManagerImpl::GetLevel2ProductRecords( std::chrono::system_clock::time_point time) { - std::shared_ptr record {nullptr}; - RadarProductRecordMap::const_pointer recordPtr {nullptr}; - std::chrono::system_clock::time_point recordTime {time}; + std::map> + records {}; + std::vector recordPtrs {}; // Ensure Level 2 product records are updated PopulateLevel2ProductTimes(time); @@ -1167,44 +1168,69 @@ RadarProductManagerImpl::GetLevel2ProductRecord( time == std::chrono::system_clock::time_point {}) { // If a default-initialized time point is given, return the latest record - recordPtr = &(*level2ProductRecords_.rbegin()); + recordPtrs.push_back(&(*level2ProductRecords_.rbegin())); } else { - recordPtr = - scwx::util::GetBoundedElementPointer(level2ProductRecords_, time); - } + // Get the requested record + auto recordIt = + scwx::util::GetBoundedElementIterator(level2ProductRecords_, time); - if (recordPtr != nullptr) - { - // Don't check for an exact time match for level 2 products - recordTime = recordPtr->first; - record = recordPtr->second.lock(); - } + if (recordIt != level2ProductRecords_.cend()) + { + recordPtrs.push_back(&(*(recordIt))); - if (recordPtr != nullptr && record == nullptr && - recordTime != std::chrono::system_clock::time_point {}) - { - // Product is expired, reload it - std::shared_ptr request = - std::make_shared(radarId_); - - QObject::connect( - request.get(), - &request::NexradFileRequest::RequestComplete, - self_, - [this](std::shared_ptr request) + // The requested time may be in the previous record, so get that too + if (recordIt != level2ProductRecords_.cbegin()) { - if (request->radar_product_record() != nullptr) - { - Q_EMIT self_->DataReloaded(request->radar_product_record()); - } - }); - - self_->LoadLevel2Data(recordTime, request); + recordPtrs.push_back(&(*(--recordIt))); + } + } } - return {record, recordTime}; + // For each record pointer + for (auto& recordPtr : recordPtrs) + { + std::shared_ptr record {nullptr}; + std::chrono::system_clock::time_point recordTime {time}; + + if (recordPtr != nullptr) + { + // Don't check for an exact time match for level 2 products + recordTime = recordPtr->first; + record = recordPtr->second.lock(); + } + + if (recordPtr != nullptr && record == nullptr && + recordTime != std::chrono::system_clock::time_point {}) + { + // Product is expired, reload it + std::shared_ptr request = + std::make_shared(radarId_); + + QObject::connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + self_, + [this](std::shared_ptr request) + { + if (request->radar_product_record() != nullptr) + { + Q_EMIT self_->DataReloaded(request->radar_product_record()); + } + }); + + self_->LoadLevel2Data(recordTime, request); + } + + if (record != nullptr) + { + // Return valid records + records.insert_or_assign(recordTime, record); + } + } + + return records; } std::tuple, @@ -1377,19 +1403,46 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, { std::shared_ptr radarData = nullptr; float elevationCut = 0.0f; - std::vector elevationCuts; + std::vector elevationCuts {}; + std::chrono::system_clock::time_point foundTime {}; - std::shared_ptr record; - std::tie(record, time) = p->GetLevel2ProductRecord(time); + auto records = p->GetLevel2ProductRecords(time); - if (record != nullptr) + for (auto& recordPair : records) { - std::tie(radarData, elevationCut, elevationCuts) = - record->level2_file()->GetElevationScan( - dataBlockType, elevation, time); + auto& record = recordPair.second; + + if (record != nullptr) + { + std::shared_ptr recordRadarData = nullptr; + float recordElevationCut = 0.0f; + std::vector recordElevationCuts; + + std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = + record->level2_file()->GetElevationScan( + dataBlockType, elevation, time); + + if (recordRadarData != nullptr) + { + auto& radarData0 = (*recordRadarData)[0]; + auto collectionTime = + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time()); + + // Find the newest radar data, not newer than the selected time + if (radarData == nullptr || + (collectionTime <= time && foundTime < collectionTime)) + { + radarData = recordRadarData; + elevationCut = recordElevationCut; + elevationCuts = std::move(recordElevationCuts); + foundTime = collectionTime; + } + } + } } - return {radarData, elevationCut, elevationCuts, time}; + return {radarData, elevationCut, elevationCuts, foundTime}; } std::tuple, From d209ce97ea65f965d268eae254066a106f3e5725 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 25 Oct 2024 22:35:00 -0500 Subject: [PATCH 169/762] Ensure proper level 2 data gets selected after file is reloaded --- .../source/scwx/qt/view/level2_product_view.cpp | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 0938f614..61de7871 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -182,12 +182,9 @@ void Level2ProductView::ConnectRadarProductManager() [this](std::shared_ptr record) { if (record->radar_product_group() == - common::RadarProductGroup::Level2 && - std::chrono::floor(record->time()) == - selected_time()) + common::RadarProductGroup::Level2) { - // If the data associated with the currently selected time is - // reloaded, update the view + // If level 2 data associated was reloaded, update the view Update(); } }); @@ -517,17 +514,10 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; - std::chrono::system_clock::time_point foundTime; - std::tie(radarData, p->elevationCut_, p->elevationCuts_, foundTime) = + std::tie(radarData, p->elevationCut_, p->elevationCuts_, std::ignore) = radarProductManager->GetLevel2Data( p->dataBlockType_, p->selectedElevation_, requestedTime); - // If a different time was found than what was requested, update it - if (requestedTime != foundTime) - { - SelectTime(foundTime); - } - if (radarData == nullptr) { Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); From 2a9dc72721bb7752e37fa701ba8f195ea0089e7f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Oct 2024 08:45:15 -0500 Subject: [PATCH 170/762] Select product times based on current time, rather than volume time Ensures scans not included in the volume times (e.g., multiple level 2 scans per file) are selected --- scwx-qt/source/scwx/qt/main/main_window.cpp | 20 +++++++------------ .../scwx/qt/manager/timeline_manager.cpp | 12 ++--------- .../scwx/qt/view/level2_product_view.cpp | 4 +++- .../scwx/qt/view/level3_radial_view.cpp | 4 +++- .../scwx/qt/view/level3_raster_view.cpp | 4 +++- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index cebdab48..37b8b268 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -241,7 +241,7 @@ public: std::vector maps_; - std::chrono::system_clock::time_point volumeTime_ {}; + std::chrono::system_clock::time_point selectedTime_ {}; public slots: void UpdateMapParameters(double latitude, @@ -997,22 +997,15 @@ void MainWindowImpl::ConnectAnimationSignals() connect(timelineManager_.get(), &manager::TimelineManager::SelectedTimeUpdated, - [this]() - { - for (auto map : maps_) - { - QMetaObject::invokeMethod( - map, static_cast(&QWidget::update)); - } - }); - connect(timelineManager_.get(), - &manager::TimelineManager::VolumeTimeUpdated, [this](std::chrono::system_clock::time_point dateTime) { - volumeTime_ = dateTime; + selectedTime_ = dateTime; + for (auto map : maps_) { map->SelectTime(dateTime); + QMetaObject::invokeMethod( + map, static_cast(&QWidget::update)); } }); @@ -1400,7 +1393,8 @@ void MainWindowImpl::SelectRadarProduct(map::MapWidget* mapWidget, UpdateRadarProductSettings(); } - mapWidget->SelectRadarProduct(group, productName, productCode, volumeTime_); + mapWidget->SelectRadarProduct( + group, productName, productCode, selectedTime_); } void MainWindowImpl::SetActiveMap(map::MapWidget* mapWidget) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 1d48ac9c..10e12135 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -470,16 +470,8 @@ void TimelineManager::Impl::PlaySync() auto selectTimeEnd = std::chrono::steady_clock::now(); auto elapsedTime = selectTimeEnd - selectTimeStart; - if (volumeTimeUpdated) - { - // Wait for radar sweeps to update - RadarSweepMonitorWait(radarSweepMonitorLock); - } - else - { - // Disable radar sweep monitor - RadarSweepMonitorDisable(); - } + // Wait for radar sweeps to update + RadarSweepMonitorWait(radarSweepMonitorLock); // Calculate the interval until the next update, prior to selecting std::chrono::milliseconds interval; diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 61de7871..a5a26157 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -497,7 +497,7 @@ void Level2ProductView::UpdateColorTableLut() void Level2ProductView::ComputeSweep() { - logger_->debug("ComputeSweep()"); + logger_->trace("ComputeSweep()"); boost::timer::cpu_timer timer; @@ -529,6 +529,8 @@ void Level2ProductView::ComputeSweep() return; } + logger_->debug("Computing Sweep"); + std::size_t radials = radarData->crbegin()->first + 1; std::size_t vertexRadials = radials; diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 5611fdf5..5fa3531f 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -117,7 +117,7 @@ std::tuple Level3RadialView::GetMomentData() const void Level3RadialView::ComputeSweep() { - logger_->debug("ComputeSweep()"); + logger_->trace("ComputeSweep()"); boost::timer::cpu_timer timer; @@ -185,6 +185,8 @@ void Level3RadialView::ComputeSweep() return; } + logger_->debug("Computing Sweep"); + // A message with radial data should either have a Digital Radial Data // Array Packet, or a Radial Data Array Packet std::shared_ptr diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index fefeb587..b51c2cd0 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -101,7 +101,7 @@ std::tuple Level3RasterView::GetMomentData() const void Level3RasterView::ComputeSweep() { - logger_->debug("ComputeSweep()"); + logger_->trace("ComputeSweep()"); boost::timer::cpu_timer timer; @@ -169,6 +169,8 @@ void Level3RasterView::ComputeSweep() return; } + logger_->debug("Computing Sweep"); + // A message with raster data should have a Raster Data Packet std::shared_ptr rasterData = nullptr; From 845d5b570781317caa3f3127dc1c6c8a92728dae Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 3 Nov 2024 09:48:42 -0600 Subject: [PATCH 171/762] Update timeline manager step function to handle volume times between indexed values --- .../scwx/qt/manager/timeline_manager.cpp | 118 ++++++++---------- 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 10e12135..55c8a0e0 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -336,10 +336,10 @@ void TimelineManager::ReceiveMapWidgetPainted(std::size_t mapIndex) std::unique_lock lock {p->radarSweepMonitorMutex_}; // If the radar sweep has been updated - if (p->radarSweepsUpdated_.contains(mapIndex)) + if (p->radarSweepsUpdated_.contains(mapIndex) && + !p->radarSweepsComplete_.contains(mapIndex)) { // Mark the radar sweep complete - p->radarSweepsUpdated_.erase(mapIndex); p->radarSweepsComplete_.insert(mapIndex); // If all sweeps have completed rendering @@ -631,79 +631,63 @@ void TimelineManager::Impl::Step(Direction direction) // Take a lock for time selection std::unique_lock lock {selectTimeMutex_}; - // Determine time to get active volume times - std::chrono::system_clock::time_point queryTime = adjustedTime_; - if (queryTime == std::chrono::system_clock::time_point {}) + std::chrono::system_clock::time_point newTime = selectedTime_; + + if (newTime == std::chrono::system_clock::time_point {}) { - queryTime = std::chrono::system_clock::now(); - } - - // Request active volume times - auto radarProductManager = - manager::RadarProductManager::Instance(radarSite_); - auto volumeTimes = radarProductManager->GetActiveVolumeTimes(queryTime); - - if (volumeTimes.empty()) - { - logger_->debug("No products to step through"); - return; - } - - // Dynamically update maximum cached volume scans - UpdateCacheLimit(radarProductManager, volumeTimes); - - std::set::const_iterator it; - - if (adjustedTime_ == std::chrono::system_clock::time_point {}) - { - // If the adjusted time is live, get the last element in the set - it = std::prev(volumeTimes.cend()); - } - else - { - // Get the current element in the set - it = scwx::util::GetBoundedElementIterator(volumeTimes, adjustedTime_); - } - - if (it == volumeTimes.cend()) - { - // Should not get here, but protect against an error - logger_->error("No suitable volume time found"); - return; - } - - if (direction == Direction::Back) - { - // Only if we aren't at the beginning of the volume times set - if (it != volumeTimes.cbegin()) + if (direction == Direction::Back) { - // Select the previous time - adjustedTime_ = *(--it); - selectedTime_ = adjustedTime_; - - logger_->debug("Volume time updated: {}", - scwx::util::TimeString(adjustedTime_)); - - Q_EMIT self_->LiveStateUpdated(false); - Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); - Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); + newTime = std::chrono::floor( + std::chrono::system_clock::now()); + } + else + { + // Cannot step forward any further + return; } } - else + + // Unlock prior to selecting time + lock.unlock(); + + // Lock radar sweep monitor + std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_}; + + // Attempt to step forward or backward up to 30 minutes until an update is + // received on at least one map + for (std::size_t i = 0; i < 30; ++i) { - // Only if we aren't at the end of the volume times set - if (it != std::prev(volumeTimes.cend())) + using namespace std::chrono_literals; + + // Increment/decrement selected time by one minute + if (direction == Direction::Back) { - // Select the next time - adjustedTime_ = *(++it); - selectedTime_ = adjustedTime_; + newTime -= 1min; + } + else + { + newTime += 1min; - logger_->debug("Volume time updated: {}", - scwx::util::TimeString(adjustedTime_)); + // If the new time is more than 2 minutes in the future, stop stepping + if (newTime > std::chrono::system_clock::now() + 2min) + { + break; + } + } - Q_EMIT self_->LiveStateUpdated(false); - Q_EMIT self_->VolumeTimeUpdated(adjustedTime_); - Q_EMIT self_->SelectedTimeUpdated(adjustedTime_); + // Reset radar sweep monitor in preparation for update + RadarSweepMonitorReset(); + + // Select the time + SelectTime(newTime); + + // Wait for radar sweeps to update + RadarSweepMonitorWait(radarSweepMonitorLock); + + // Check for updates + if (!radarSweepsUpdated_.empty()) + { + break; } } } From f7949cc404713b93a6f41ad3de314de0b69bb017 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 15 Nov 2024 05:40:34 -0600 Subject: [PATCH 172/762] Avoid invalid iterator comparison after objects mutex is unlocked --- wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index 4e139bac..c4ac523b 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -183,6 +183,7 @@ AwsNexradDataProvider::GetTimePointsByDate( std::shared_lock lock(p->objectsMutex_); // Is the date present in the date list? + bool currentDatePresent; auto currentDateIterator = std::find(p->objectDates_.cbegin(), p->objectDates_.cend(), day); if (currentDateIterator == p->objectDates_.cend()) @@ -199,6 +200,12 @@ AwsNexradDataProvider::GetTimePointsByDate( // Re-lock mutex lock.lock(); + + currentDatePresent = false; + } + else + { + currentDatePresent = true; } // Determine objects to retrieve @@ -216,7 +223,7 @@ AwsNexradDataProvider::GetTimePointsByDate( // If we haven't updated the most recently queried dates yet, because the // date was already cached, update - if (currentDateIterator != p->objectDates_.cend()) + if (currentDatePresent) { p->UpdateObjectDates(date); } From 881502c97033901c6d3a5cf7333ecc783318e88a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 20 Nov 2024 07:38:32 -0600 Subject: [PATCH 173/762] Prevent the radar product cache limit from being set too small --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 2b201299..f6df6237 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1480,7 +1480,7 @@ std::vector RadarProductManager::GetLevel3Products() void RadarProductManager::SetCacheLimit(size_t cacheLimit) { - p->cacheLimit_ = cacheLimit; + p->cacheLimit_ = std::max(cacheLimit, 6u); } void RadarProductManager::UpdateAvailableProducts() diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 4a3edabf..17dfa551 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -132,6 +132,7 @@ public: /** * @brief Set the maximum number of products of each type that may be cached. + * The cache limit cannot be set lower than 6. * * @param [in] cacheLimit The maximum number of products of each type */ From 4471843f8bb3c17a508288c0b8e22182c24d6af3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 20 Nov 2024 19:39:32 -0600 Subject: [PATCH 174/762] Lock the level 2 product record mutex before searching for records --- .../scwx/qt/manager/radar_product_manager.cpp | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index f6df6237..4659f079 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1164,26 +1164,31 @@ RadarProductManagerImpl::GetLevel2ProductRecords( // Ensure Level 2 product records are updated PopulateLevel2ProductTimes(time); - if (!level2ProductRecords_.empty() && - time == std::chrono::system_clock::time_point {}) { - // If a default-initialized time point is given, return the latest record - recordPtrs.push_back(&(*level2ProductRecords_.rbegin())); - } - else - { - // Get the requested record - auto recordIt = - scwx::util::GetBoundedElementIterator(level2ProductRecords_, time); + std::shared_lock lock {level2ProductRecordMutex_}; - if (recordIt != level2ProductRecords_.cend()) + if (!level2ProductRecords_.empty() && + time == std::chrono::system_clock::time_point {}) { - recordPtrs.push_back(&(*(recordIt))); + // If a default-initialized time point is given, return the latest + // record + recordPtrs.push_back(&(*level2ProductRecords_.rbegin())); + } + else + { + // Get the requested record + auto recordIt = + scwx::util::GetBoundedElementIterator(level2ProductRecords_, time); - // The requested time may be in the previous record, so get that too - if (recordIt != level2ProductRecords_.cbegin()) + if (recordIt != level2ProductRecords_.cend()) { - recordPtrs.push_back(&(*(--recordIt))); + recordPtrs.push_back(&(*(recordIt))); + + // The requested time may be in the previous record, so get that too + if (recordIt != level2ProductRecords_.cbegin()) + { + recordPtrs.push_back(&(*(--recordIt))); + } } } } From ae91686d4c6362ea3c8b0c94a7f846ddce9f1f9e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 20 Nov 2024 21:47:08 -0600 Subject: [PATCH 175/762] Don't create unused variables returned from TimelineManager::SelectTime --- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 55c8a0e0..f0870f93 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -466,7 +466,7 @@ void TimelineManager::Impl::PlaySync() // Select the time auto selectTimeStart = std::chrono::steady_clock::now(); - auto [volumeTimeUpdated, selectedTimeUpdated] = SelectTime(newTime); + SelectTime(newTime); auto selectTimeEnd = std::chrono::steady_clock::now(); auto elapsedTime = selectTimeEnd - selectTimeStart; From 4cd98ef4ac76593cebb77feb810bc42818217ae9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Nov 2024 07:14:09 -0600 Subject: [PATCH 176/762] Set OK as default settings dialog button --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index a5a2f857..a9b81706 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include @@ -291,6 +292,10 @@ SettingsDialog::SettingsDialog(QWidget* parent) : { ui->setupUi(this); + // Set OK as default + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setDefault(true); + // General p->SetupGeneralTab(); From 0683cd23260d7d1fb1dd3380ae5c222a48a4de80 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Nov 2024 09:01:57 -0600 Subject: [PATCH 177/762] Use serial port location on Linux instead of port name --- .../source/scwx/qt/ui/serial_port_dialog.cpp | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp index d489c1d6..ca2daa42 100644 --- a/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/serial_port_dialog.cpp @@ -168,7 +168,17 @@ SerialPortDialog::~SerialPortDialog() std::string SerialPortDialog::serial_port() { - return p->selectedSerialPort_; + std::string serialPort = p->selectedSerialPort_; + +#if !defined(_WIN32) + auto it = p->portInfoMap_.find(p->selectedSerialPort_); + if (it != p->portInfoMap_.cend()) + { + serialPort = it->second.systemLocation().toStdString(); + } +#endif + + return serialPort; } int SerialPortDialog::baud_rate() @@ -225,7 +235,8 @@ void SerialPortDialog::Impl::UpdateModel() static const QStringList headerLabels { tr("Port"), tr("Description"), tr("Device")}; #else - static const QStringList headerLabels {tr("Port"), tr("Description")}; + static const QStringList headerLabels { + tr("Port"), tr("Location"), tr("Description")}; #endif // Clear existing serial ports @@ -260,8 +271,11 @@ void SerialPortDialog::Impl::UpdateModel() new QStandardItem(description), new QStandardItem(device)}); #else - root->appendRow( - {new QStandardItem(portName), new QStandardItem(description)}); + const QString systemLocation = port.second.systemLocation(); + + root->appendRow({new QStandardItem(portName), + new QStandardItem(systemLocation), + new QStandardItem(description)}); #endif } From 35f2c85a199f6f5b19ae46f807e73fa179e66314 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 25 Nov 2024 11:18:03 -0500 Subject: [PATCH 178/762] Some minor code changes, as well as properly using marker index --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 2 +- scwx-qt/source/scwx/qt/model/marker_model.cpp | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index e91f03fb..99208a10 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -315,7 +315,7 @@ void MarkerManager::remove_marker(types::MarkerId id) { if (pair.second > index) { - p->idToIndex_[pair.first] = pair.second - 1; + pair.second -= 1; } } } diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 97cafcb1..9bd057ea 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -297,14 +297,19 @@ void MarkerModel::HandleMarkerAdded(types::MarkerId id) const int newIndex = static_cast(*index); beginInsertRows(QModelIndex(), newIndex, newIndex); - p->markerIds_.emplace_back(id); + auto it = std::next(p->markerIds_.begin(), newIndex); + p->markerIds_.emplace(it, id); endInsertRows(); } void MarkerModel::HandleMarkerChanged(types::MarkerId id) { - std::optional index = p->markerManager_->get_index(id); - const int changedIndex = static_cast(*index); + auto it = std::find(p->markerIds_.begin(), p->markerIds_.end(), id); + if (it == p->markerIds_.end()) + { + return; + } + const int changedIndex = std::distance(p->markerIds_.begin(), it); QModelIndex topLeft = createIndex(changedIndex, kFirstColumn); QModelIndex bottomRight = createIndex(changedIndex, kLastColumn); From b13d2106d44afd8008c7ea434c9784da8fc9159e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 25 Nov 2024 11:42:17 -0500 Subject: [PATCH 179/762] Fixed issue with NormalizeUrl where whitespace trimming was not happening on non-local files, and added test cases. --- scwx-qt/source/scwx/qt/util/network.cpp | 2 +- test/source/scwx/qt/util/network.test.cpp | 40 +++++++++++++++++++++++ test/test.cmake | 3 +- 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 test/source/scwx/qt/util/network.test.cpp diff --git a/scwx-qt/source/scwx/qt/util/network.cpp b/scwx-qt/source/scwx/qt/util/network.cpp index 429fac95..a0a782f8 100644 --- a/scwx-qt/source/scwx/qt/util/network.cpp +++ b/scwx-qt/source/scwx/qt/util/network.cpp @@ -25,7 +25,7 @@ std::string NormalizeUrl(const std::string& urlString) } else { - normalizedUrl = urlString; + normalizedUrl = trimmedUrlString.toStdString(); } return normalizedUrl; diff --git a/test/source/scwx/qt/util/network.test.cpp b/test/source/scwx/qt/util/network.test.cpp new file mode 100644 index 00000000..8a240ffe --- /dev/null +++ b/test/source/scwx/qt/util/network.test.cpp @@ -0,0 +1,40 @@ +#include + +#include + + + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +const std::vector> testUrls = { + {" https://example.com/ ", "https://example.com/"}, + {"\thttps://example.com/\t", "https://example.com/"}, + {"\nhttps://example.com/\n", "https://example.com/"}, + {"\rhttps://example.com/\r", "https://example.com/"}, + {"\r\nhttps://example.com/\r\n", "https://example.com/"}, + {" https://example.com/ ", "https://example.com/"}, + {" \nhttps://example.com/ \n ", "https://example.com/"}, +}; + +TEST(network, NormalizeUrl) +{ + for (auto& pair : testUrls) + { + const std::string& preNormalized = pair.first; + const std::string& expNormalized = pair.second; + + std::string normalized = network::NormalizeUrl(preNormalized); + EXPECT_EQ(normalized, expNormalized); + } + +} + + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index b3cfedd2..bb36287a 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -29,7 +29,8 @@ 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 - source/scwx/qt/util/geographic_lib.test.cpp) + source/scwx/qt/util/geographic_lib.test.cpp + source/scwx/qt/util/network.test.cpp) set(SRC_UTIL_TESTS source/scwx/util/float.test.cpp source/scwx/util/rangebuf.test.cpp source/scwx/util/streams.test.cpp From 950620de6eccf58eed091e96298a1896b32afd34 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 25 Nov 2024 11:53:12 -0500 Subject: [PATCH 180/762] add whitspace trimming to api key entry in setup --- scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp index fc0c9ef1..0e5c3f98 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp @@ -209,9 +209,11 @@ void MapProviderPage::Impl::SetupSettingsInterface() mapProvider_.SetEditWidget(mapProviderComboBox_); mapboxApiKey_.SetSettingsVariable(generalSettings.mapbox_api_key()); + mapboxApiKey_.EnableTrimming(); mapboxApiKey_.SetEditWidget(mapboxGroup_.apiKeyEdit_); mapTilerApiKey_.SetSettingsVariable(generalSettings.maptiler_api_key()); + mapTilerApiKey_.EnableTrimming(); mapTilerApiKey_.SetEditWidget(maptilerGroup_.apiKeyEdit_); } From f0bfca8588aac082b4dbf410d81869a27ab96ee7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 25 Nov 2024 12:14:23 -0500 Subject: [PATCH 181/762] Added local file tests, and make URL tests more complete for NormalizeUrl --- test/source/scwx/qt/util/network.test.cpp | 50 +++++++++++++++++++---- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/test/source/scwx/qt/util/network.test.cpp b/test/source/scwx/qt/util/network.test.cpp index 8a240ffe..21524d0c 100644 --- a/test/source/scwx/qt/util/network.test.cpp +++ b/test/source/scwx/qt/util/network.test.cpp @@ -12,13 +12,49 @@ namespace util { const std::vector> testUrls = { - {" https://example.com/ ", "https://example.com/"}, - {"\thttps://example.com/\t", "https://example.com/"}, - {"\nhttps://example.com/\n", "https://example.com/"}, - {"\rhttps://example.com/\r", "https://example.com/"}, - {"\r\nhttps://example.com/\r\n", "https://example.com/"}, - {" https://example.com/ ", "https://example.com/"}, - {" \nhttps://example.com/ \n ", "https://example.com/"}, + {" https://example.com/path/to+a+test/file.txt ", + "https://example.com/path/to+a+test/file.txt"}, + {"\thttps://example.com/path/to+a+test/file.txt\t", + "https://example.com/path/to+a+test/file.txt"}, + {"\nhttps://example.com/path/to+a+test/file.txt\n", + "https://example.com/path/to+a+test/file.txt"}, + {"\rhttps://example.com/path/to+a+test/file.txt\r", + "https://example.com/path/to+a+test/file.txt"}, + {"\r\nhttps://example.com/path/to+a+test/file.txt\r\n", + "https://example.com/path/to+a+test/file.txt"}, + {" https://example.com/path/to+a+test/file.txt ", + "https://example.com/path/to+a+test/file.txt"}, + {" \nhttps://example.com/path/to+a+test/file.txt \n ", + "https://example.com/path/to+a+test/file.txt"}, + + // Only tested for this OS because NormalizeUrl uses native separators +#ifdef _WIN32 + {" C:\\path\\to a test\\file.txt ", "C:\\path\\to a test\\file.txt"}, + {"\tC:\\path\\to a test\\file.txt\t", "C:\\path\\to a test\\file.txt"}, + {"\nC:\\path\\to a test\\file.txt\n", "C:\\path\\to a test\\file.txt"}, + {"\rC:\\path\\to a test\\file.txt\r", "C:\\path\\to a test\\file.txt"}, + {"\r\nC:\\path\\to a test\\file.txt\r\n", "C:\\path\\to a test\\file.txt"}, + {" C:\\path\\to a test\\file.txt ", "C:\\path\\to a test\\file.txt"}, + {" \nC:\\path\\to a test\\file.txt \n ", + "C:\\path\\to a test\\file.txt"}, + + {" C:/path/to a test/file.txt ", "C:\\path\\to a test\\file.txt"}, + {"\tC:/path/to a test/file.txt\t", "C:\\path\\to a test\\file.txt"}, + {"\nC:/path/to a test/file.txt\n", "C:\\path\\to a test\\file.txt"}, + {"\rC:/path/to a test/file.txt\r", "C:\\path\\to a test\\file.txt"}, + {"\r\nC:/path/to a test/file.txt\r\n", "C:\\path\\to a test\\file.txt"}, + {" C:/path/to a test/file.txt ", "C:\\path\\to a test\\file.txt"}, + {" \nC:/path/to a test/file.txt \n ", "C:\\path\\to a test\\file.txt"}, +#else + + {" /path/to a test/file.txt ", "/path/to a test/file.txt"}, + {"\t/path/to a test/file.txt\t", "/path/to a test/file.txt"}, + {"\n/path/to a test/file.txt\n", "/path/to a test/file.txt"}, + {"\r/path/to a test/file.txt\r", "/path/to a test/file.txt"}, + {"\r\n/path/to a test/file.txt\r\n", "/path/to a test/file.txt"}, + {" /path/to a test/file.txt ", "/path/to a test/file.txt"}, + {" \n/path/to a test/file.txt \n ", "/path/to a test/file.txt"}, +#endif }; TEST(network, NormalizeUrl) From 756249c3ad0e9514e5fea731bd7085784eecad33 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Nov 2024 05:53:14 -0600 Subject: [PATCH 182/762] Validate parent row before dropping mime data in LayerModel --- scwx-qt/source/scwx/qt/model/layer_model.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 23d05cd6..014acb42 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -774,6 +774,13 @@ bool LayerModel::dropMimeData(const QMimeData* data, QDataStream stream(&mimeData, QIODevice::ReadOnly); std::vector sourceRows {}; + // Validate parent row + if (parent.row() < 0 || parent.row() >= static_cast(p->layers_.size())) + { + logger_->warn("Cannot perform drop action, invalid parent row"); + return false; + } + // Read source rows from QMimeData while (!stream.atEnd()) { From ee5719cb55772ef8d411a61c6b0ca8ebd440020c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 26 Nov 2024 08:57:10 -0500 Subject: [PATCH 183/762] Update test/data in location_markers_ids branch to match develop --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 166c5a7b..a642d730 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 166c5a7bcaf8ad0a42bedf8f8dc5c4aa907e7151 +Subproject commit a642d730bd8d6c9b291b90e61b3a3a389139f2f6 From 831e0f7d1b5cdbce69cb5f722888a63117b730c6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 27 Nov 2024 12:05:04 -0500 Subject: [PATCH 184/762] Add check to optional value to avoid undefined behavior in MarkerModel --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 9bd057ea..e550312e 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -294,6 +294,10 @@ void MarkerModel::HandleMarkersInitialized(size_t count) void MarkerModel::HandleMarkerAdded(types::MarkerId id) { std::optional index = p->markerManager_->get_index(id); + if (!index) + { + return; + } const int newIndex = static_cast(*index); beginInsertRows(QModelIndex(), newIndex, newIndex); From 5dd30131e3fec85b3e342c1fb86a9010ef86882f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 27 Nov 2024 21:56:59 -0600 Subject: [PATCH 185/762] Bump version to v0.4.6 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c49f81fd..4e59bc90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.5 + SCWX_VERSION: v0.4.6 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index c06a46e6..c4e86181 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} - VERSION 0.4.5 + VERSION 0.4.6 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -44,7 +44,7 @@ conan_basic_setup(TARGETS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.5") +set(SCWX_VERSION "0.4.6") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) From b1f32ab0f5073159f48871388190624dbab246f5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Nov 2024 21:45:54 -0600 Subject: [PATCH 186/762] Clang format check --- .github/workflows/clang-format-check.yml | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/clang-format-check.yml diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml new file mode 100644 index 00000000..8fe8c790 --- /dev/null +++ b/.github/workflows/clang-format-check.yml @@ -0,0 +1,30 @@ +name: clang-format-check + +on: + workflow_dispatch: + pull_request: + branches: + - 'develop' + +concurrency: + # Cancel in-progress jobs for the same pull request + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + format: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v4 + with: + path: source + submodules: false + + - name: Check Formatting + uses: jayllyz/clang-format-action@v1 + with: + check: true + clang-version: 17 + base-ref: refs/remotes/origin/develop From 71d2345e66277550ddfbcf692f7d89e73d005e96 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Nov 2024 23:45:05 -0600 Subject: [PATCH 187/762] Fixes for clang-format-check --- .github/workflows/clang-format-check.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 8fe8c790..c75bab14 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -13,18 +13,28 @@ concurrency: jobs: format: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 with: - path: source + fetch-depth: 0 submodules: false + - name: Update References + shell: bash + run: | + git fetch origin develop + + - name: Setup Ubuntu Environment + shell: bash + run: | + sudo apt-get install clang-format-17 + - name: Check Formatting - uses: jayllyz/clang-format-action@v1 - with: - check: true - clang-version: 17 - base-ref: refs/remotes/origin/develop + shell: bash + run: | + MERGE_BASE=$(git merge-base origin/develop ${{ github.ref }}) + echo "Comparing against ${MERGE_BASE}" + git clang-format-17 --diff --style=file -v ${MERGE_BASE} From d539f7723eee7f19511bf88bda0b16e413116ca5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 27 Nov 2024 07:42:46 -0600 Subject: [PATCH 188/762] Add clang-tidy-review --- .clang-tidy | 9 ++ .github/workflows/clang-tidy-review.yml | 107 ++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 .clang-tidy create mode 100644 .github/workflows/clang-tidy-review.yml diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..6b7191fd --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,9 @@ +Checks: + - '-*' + - 'bugprone-*' + - 'clang-analyzer-*' + - 'cppcoreguidelines-*' + - 'misc-*','modernize-*' + - 'performance-*' + - '-misc-include-cleaner' +FormatStyle: 'file' diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml new file mode 100644 index 00000000..e76ab6dc --- /dev/null +++ b/.github/workflows/clang-tidy-review.yml @@ -0,0 +1,107 @@ +name: clang-tidy-review + +on: + pull_request: + branches: + - 'develop' + +concurrency: + # Cancel in-progress jobs for the same pull request + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + strategy: + matrix: + include: + - name: linux64_clang + os: ubuntu-24.04 + build_type: Release + qt_version: 6.8.0 + qt_arch_aqt: linux_gcc_64 + qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport + qt_tools: '' + conan_arch: x86_64 + conan_compiler: gcc + conan_compiler_version: 14 + conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ + conan_compiler_runtime: '' + conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + compiler_packages: clang-17 + runs-on: ${{ matrix.os }} + steps: + + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ matrix.qt_version }} + arch: ${{ matrix.qt_arch_aqt }} + modules: ${{ matrix.qt_modules }} + tools: ${{ matrix.qt_tools }} + + - name: Setup Ubuntu Environment + if: ${{ startsWith(matrix.os, 'ubuntu') }} + shell: bash + run: | + sudo apt-get install doxygen \ + libfuse2 \ + ninja-build \ + ${{ matrix.compiler_packages }} + + - name: Setup Python Environment + shell: pwsh + run: | + pip install geopandas ` + GitPython + + - name: Install Conan Packages + shell: pwsh + run: | + pip install "conan<2.0" + conan profile new default --detect + conan install ./ ` + --remote conancenter ` + --build missing ` + --settings arch=${{ matrix.conan_arch }} ` + --settings build_type=${{ matrix.build_type }} ` + --settings compiler="${{ matrix.conan_compiler }}" ` + --settings compiler.version=${{ matrix.conan_compiler_version }} ` + ${{ matrix.conan_compiler_libcxx }} ` + ${{ matrix.conan_compiler_runtime }} ` + ${{ matrix.conan_package_manager }} + + - name: Autogenerate + shell: pwsh + run: | + mkdir build + cd build + cmake ../ ` + -G Ninja ` + -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" ` + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" ` + -DCMAKE_EXPORT_COMPILE_COMMANDS=on + ninja scwx-qt_generate_counties_db ` + scwx-qt_generate_versions ` + scwx-qt_update_radar_sites ` + scwx-qt_autogen + + - name: Review + id: review + uses: ZedThree/clang-tidy-review@v0.14.0 + with: + config_file: .clang-tidy + build_dir: build + lgtm_comment_body: '' + + - name: Upload Review + uses: ZedThree/clang-tidy-review/upload@v0.20.1 + + - name: Status Check + if: steps.review.outputs.total_comments > 0 + run: exit 1 From ed9e7ad72f2c4bf4186a430870c3cd7ba0bed648 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 06:52:41 -0600 Subject: [PATCH 189/762] Fixes for clang-tidy-review --- .clang-tidy | 5 ++- .github/workflows/clang-tidy-review.yml | 59 +++++++++++++++++++------ scwx-qt/scwx-qt.cmake | 1 + 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 6b7191fd..1eed15a2 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -3,7 +3,10 @@ Checks: - 'bugprone-*' - 'clang-analyzer-*' - 'cppcoreguidelines-*' - - 'misc-*','modernize-*' + - 'misc-*' + - 'modernize-*' - 'performance-*' - '-misc-include-cleaner' + - '-misc-non-private-member-variables-in-classes' + - '-modernize-use-trailing-return-type' FormatStyle: 'file' diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index e76ab6dc..7cec736c 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -15,28 +15,42 @@ jobs: strategy: matrix: include: - - name: linux64_clang + - name: linux64_clang-tidy os: ubuntu-24.04 build_type: Release + env_cc: clang-17 + env_cxx: clang++-17 qt_version: 6.8.0 qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' conan_arch: x86_64 - conan_compiler: gcc - conan_compiler_version: 14 - conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ + conan_compiler: clang + conan_compiler_version: 17 + conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True - compiler_packages: clang-17 + compiler_packages: clang-17 clang-tidy-17 + name: ${{ matrix.name }} runs-on: ${{ matrix.os }} + env: + CC: ${{ matrix.env_cc }} + CXX: ${{ matrix.env_cxx }} steps: - name: Checkout uses: actions/checkout@v4 with: + path: source submodules: recursive + - name: Checkout clang-tidy-review Repository + uses: actions/checkout@v4 + with: + repository: ZedThree/clang-tidy-review + ref: v0.20.1 + path: clang-tidy-review + - name: Install Qt uses: jurplel/install-qt-action@v3 with: @@ -59,13 +73,14 @@ jobs: run: | pip install geopandas ` GitPython + pip install --break-system-packages clang-tidy-review/post/clang_tidy_review - name: Install Conan Packages shell: pwsh run: | pip install "conan<2.0" conan profile new default --detect - conan install ./ ` + conan install ./source/ ` --remote conancenter ` --build missing ` --settings arch=${{ matrix.conan_arch }} ` @@ -81,7 +96,7 @@ jobs: run: | mkdir build cd build - cmake ../ ` + cmake ../source/ ` -G Ninja ` -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" ` -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" ` @@ -91,13 +106,31 @@ jobs: scwx-qt_update_radar_sites ` scwx-qt_autogen - - name: Review + - name: Code Review id: review - uses: ZedThree/clang-tidy-review@v0.14.0 - with: - config_file: .clang-tidy - build_dir: build - lgtm_comment_body: '' + shell: bash + run: | + cd source + review --clang_tidy_binary=clang-tidy-17 \ + --token=${{ github.token }} \ + --repo='${{ github.repository }}' \ + --pr='${{ github.event.pull_request.number }}' \ + --build_dir='../build' \ + --base_dir='${{ github.workspace }}/source' \ + --clang_tidy_checks='' \ + --config_file='.clang-tidy' \ + --include='*.[ch],*.[ch]xx,*.[ch]pp,*.[ch]++,*.cc,*.hh' \ + --exclude='' \ + --apt-packages='' \ + --cmake-command='' \ + --max-comments=25 \ + --lgtm-comment-body='' \ + --split_workflow=false \ + --annotations=false \ + --parallel=0 + rsync -avzh --ignore-missing-args clang-tidy-review-output.json ../ + rsync -avzh --ignore-missing-args clang-tidy-review-metadata.json ../ + rsync -avzh --ignore-missing-args clang_fixes.json ../ - name: Upload Review uses: ZedThree/clang-tidy-review/upload@v0.20.1 diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1f4f4449..b68c20c6 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -510,6 +510,7 @@ source_group("I18N Files" FILES ${TS_FILES}) add_library(scwx-qt OBJECT ${PROJECT_SOURCES}) set_property(TARGET scwx-qt PROPERTY AUTOMOC ON) +set_property(TARGET scwx-qt PROPERTY AUTOGEN_ORIGIN_DEPENDS OFF) add_custom_command(OUTPUT ${COUNTIES_SQLITE_DB} COMMAND ${Python_EXECUTABLE} From dab88ebb9891c3e5c8439bad9e1cfc8967b8d75c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 06:53:01 -0600 Subject: [PATCH 190/762] Fix deprecated header usage in log manager --- scwx-qt/source/scwx/qt/manager/log_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/log_manager.cpp b/scwx-qt/source/scwx/qt/manager/log_manager.cpp index 457c8d28..7ab18e56 100644 --- a/scwx-qt/source/scwx/qt/manager/log_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/log_manager.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include #include From fef83c8aa7fd6dc2064cbf490b34e86189fcfb7b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 20 Dec 2023 22:05:55 -0600 Subject: [PATCH 191/762] Can't use ${CMAKE_BUILD_TYPE} in conditional expression with multi-config --- CMakeLists.txt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c4e86181..588e0482 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,13 +22,6 @@ set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS conanfile.py) -# Don't use RelWithDebInfo Conan packages -if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") - set(conan_build_type "Release") -else() - set(conan_build_type ${CMAKE_BUILD_TYPE}) -endif() - conan_cmake_autodetect(settings BUILD_TYPE ${conan_build_type}) From 9ab9d6b2d0d27e6d5e315b93bc9e96c09f3ef425 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 20 Dec 2023 22:06:46 -0600 Subject: [PATCH 192/762] Bump minimum required to CMake 3.24 --- CMakeLists.txt | 2 +- external/CMakeLists.txt | 2 +- external/aws-sdk-cpp.cmake | 2 +- external/date.cmake | 2 +- external/hsluv-c.cmake | 2 +- external/imgui.cmake | 2 +- external/maplibre-native-qt.cmake | 2 +- external/stb.cmake | 2 +- external/textflowcpp.cmake | 2 +- external/units.cmake | 2 +- scwx-qt/CMakeLists.txt | 2 +- scwx-qt/scwx-qt.cmake | 2 +- test/CMakeLists.txt | 2 +- test/test.cmake | 2 +- wxdata/CMakeLists.txt | 2 +- wxdata/wxdata.cmake | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 588e0482..f85ef287 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} VERSION 0.4.6 diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 5ef39ddc..2137ae62 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-external) set_property(DIRECTORY diff --git a/external/aws-sdk-cpp.cmake b/external/aws-sdk-cpp.cmake index 1ba641db..3952f0f8 100644 --- a/external/aws-sdk-cpp.cmake +++ b/external/aws-sdk-cpp.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-aws-sdk-cpp) set(AWS_SDK_WARNINGS_ARE_ERRORS OFF) diff --git a/external/date.cmake b/external/date.cmake index a804f68c..7fce7ffc 100644 --- a/external/date.cmake +++ b/external/date.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-date) set(USE_SYSTEM_TZ_DB ON) diff --git a/external/hsluv-c.cmake b/external/hsluv-c.cmake index 0129d39d..eec8f0f2 100644 --- a/external/hsluv-c.cmake +++ b/external/hsluv-c.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-hsluv-c) set(HSLUV_C_TESTS OFF) diff --git a/external/imgui.cmake b/external/imgui.cmake index 443817ef..c16049fb 100644 --- a/external/imgui.cmake +++ b/external/imgui.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-imgui) find_package(QT NAMES Qt6 diff --git a/external/maplibre-native-qt.cmake b/external/maplibre-native-qt.cmake index 49c37bc1..736c6049 100644 --- a/external/maplibre-native-qt.cmake +++ b/external/maplibre-native-qt.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-mln) set(gtest_disable_pthreads ON) diff --git a/external/stb.cmake b/external/stb.cmake index c26bedaf..570af425 100644 --- a/external/stb.cmake +++ b/external/stb.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-stb) set(STB_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/stb PARENT_SCOPE) diff --git a/external/textflowcpp.cmake b/external/textflowcpp.cmake index 1e36da18..31020665 100644 --- a/external/textflowcpp.cmake +++ b/external/textflowcpp.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-textflowcpp) set(TEXTFLOWCPP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/textflowcpp PARENT_SCOPE) diff --git a/external/units.cmake b/external/units.cmake index d037ae54..cc70ac1c 100644 --- a/external/units.cmake +++ b/external/units.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME scwx-units) add_subdirectory(units) diff --git a/scwx-qt/CMakeLists.txt b/scwx-qt/CMakeLists.txt index f4e636e7..e47bc3fb 100644 --- a/scwx-qt/CMakeLists.txt +++ b/scwx-qt/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.24) set_property(DIRECTORY APPEND diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1f4f4449..da5bae9e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.24) project(scwx-qt LANGUAGES CXX) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d1db7851..b5428d86 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set_property(DIRECTORY APPEND diff --git a/test/test.cmake b/test/test.cmake index 5ce56cfb..3ec6ef19 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) project(scwx-test CXX) include(GoogleTest) diff --git a/wxdata/CMakeLists.txt b/wxdata/CMakeLists.txt index 845ab381..c5d91595 100644 --- a/wxdata/CMakeLists.txt +++ b/wxdata/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) set_property(DIRECTORY APPEND diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 434fc415..ddd998f1 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) project(scwx-data) From 75a8f7f351a6fd9a99ed46b3eaaa2e63804fd821 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 21 Dec 2023 10:41:42 -0600 Subject: [PATCH 193/762] Initial updates for Conan 2.x --- .github/workflows/ci.yml | 12 ++++++++---- CMakeLists.txt | 14 -------------- conanfile.py | 24 ++++++++++++------------ external/cmake-conan | 2 +- setup-debug.bat | 3 ++- setup-debug.sh | 1 + setup-multi.bat | 11 +++++++++++ setup-release.bat | 3 ++- setup-release.sh | 1 + tools/setup-common.bat | 2 +- tools/setup-common.sh | 2 +- 11 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 setup-multi.bat diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e59bc90..968adb52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,10 @@ jobs: qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' conan_arch: x86_64 - conan_compiler: Visual Studio - conan_compiler_version: 17 - conan_compiler_runtime: --settings compiler.runtime=MD + conan_compiler: msvc + conan_compiler_version: 193 + conan_compiler_cppstd: 20 + conan_compiler_runtime: --settings compiler.runtime=dynamic conan_compiler_libcxx: '' conan_package_manager: '' artifact_suffix: windows-x64 @@ -54,6 +55,7 @@ jobs: conan_arch: x86_64 conan_compiler: gcc conan_compiler_version: 11 + conan_compiler_cppstd: 20 conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True @@ -73,6 +75,7 @@ jobs: conan_arch: x86_64 conan_compiler: clang conan_compiler_version: 17 + conan_compiler_cppstd: 20 conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True @@ -128,7 +131,7 @@ jobs: - name: Install Conan Packages shell: pwsh run: | - pip install "conan<2.0" + pip install conan conan profile new default --detect conan install ./source/ ` --remote conancenter ` @@ -137,6 +140,7 @@ jobs: --settings build_type=${{ matrix.build_type }} ` --settings compiler="${{ matrix.conan_compiler }}" ` --settings compiler.version=${{ matrix.conan_compiler_version }} ` + --settings compiler.cppstd=${{ matrix.conan_compiler_cppstd }} ` ${{ matrix.conan_compiler_libcxx }} ` ${{ matrix.conan_compiler_runtime }} ` ${{ matrix.conan_package_manager }} diff --git a/CMakeLists.txt b/CMakeLists.txt index f85ef287..29e7ae9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,25 +15,11 @@ enable_testing() set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) -include(${PROJECT_SOURCE_DIR}/external/cmake-conan/conan.cmake) - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS conanfile.py) -conan_cmake_autodetect(settings - BUILD_TYPE ${conan_build_type}) - -conan_cmake_install(PATH_OR_REFERENCE ${PROJECT_SOURCE_DIR} - BUILD missing - REMOTE conancenter - SETTINGS ${settings}) - -include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) -include(${CMAKE_BINARY_DIR}/conan_paths.cmake) -conan_basic_setup(TARGETS) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) diff --git a/conanfile.py b/conanfile.py index 888f09c7..fe90a124 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,5 @@ -from conans import ConanFile +from conan import ConanFile +from conan.tools.files import copy class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") @@ -19,19 +20,18 @@ class SupercellWxConan(ConanFile): "sqlite3/3.46.1", "vulkan-loader/1.3.243.0", "zlib/1.3.1") - generators = ("cmake", - "cmake_find_package", - "cmake_paths") - default_options = {"geos:shared" : True, - "libiconv:shared" : True, - "openssl:no_module": True, - "openssl:shared" : True} + generators = ("CMakeDeps") + default_options = {"geos/*:shared" : True, + "libiconv/*:shared" : True, + "openssl/*:no_module": True, + "openssl/*:shared" : True} def requirements(self): if self.settings.os == "Linux": self.requires("onetbb/2021.12.0") - def imports(self): - self.copy("*.dll", dst="bin", src="bin") - self.copy("*.dylib", dst="bin", src="lib") - self.copy("license*", dst="licenses", src=".", folder=True, ignore_case=True) + def generate(self): + for dep in self.dependencies.values(): + if dep.cpp_info.libdirs: + copy(self, "*.dll", dep.cpp_info.libdirs[0], self.build_folder) + copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) diff --git a/external/cmake-conan b/external/cmake-conan index b240c809..c53fbf58 160000 --- a/external/cmake-conan +++ b/external/cmake-conan @@ -1 +1 @@ -Subproject commit b240c809b5ea097077fc8222cad71d2329288e48 +Subproject commit c53fbf58e4afe7cb93cb32e730d6647fc47a5dce diff --git a/setup-debug.bat b/setup-debug.bat index 14e4f870..1ff36fb2 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -9,5 +9,6 @@ mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake pause diff --git a/setup-debug.sh b/setup-debug.sh index 4f9e72ea..ceeff368 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -13,4 +13,5 @@ cmake -B ${build_dir} -S . \ -DCMAKE_CONFIGURATION_TYPES=${build_type} \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ -G Ninja diff --git a/setup-multi.bat b/setup-multi.bat new file mode 100644 index 00000000..778b1328 --- /dev/null +++ b/setup-multi.bat @@ -0,0 +1,11 @@ +call tools\setup-common.bat + +set build_dir=build-debug +set build_type=Debug +set qt_version=6.7.1 + +mkdir %build_dir% +cmake -B %build_dir% -S . ^ + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/msvc2019_64 ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake +pause diff --git a/setup-release.bat b/setup-release.bat index cfcbd3c9..332acdfa 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -9,5 +9,6 @@ mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake pause diff --git a/setup-release.sh b/setup-release.sh index 63f52ef6..be0fe858 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -13,4 +13,5 @@ cmake -B ${build_dir} -S . \ -DCMAKE_CONFIGURATION_TYPES=${build_type} \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ -G Ninja diff --git a/tools/setup-common.bat b/tools/setup-common.bat index 8362be70..b3cbb147 100644 --- a/tools/setup-common.bat +++ b/tools/setup-common.bat @@ -1,4 +1,4 @@ -pip install --upgrade "conan<2.0" +pip install --upgrade conan pip install --upgrade geopandas pip install --upgrade GitPython conan profile new default --detect diff --git a/tools/setup-common.sh b/tools/setup-common.sh index d2c61d5a..58d29d1d 100755 --- a/tools/setup-common.sh +++ b/tools/setup-common.sh @@ -1,5 +1,5 @@ #!/bin/bash -pip install --upgrade --user "conan<2.0" +pip install --upgrade --user conan pip install --upgrade --user geopandas pip install --upgrade --user GitPython conan profile new default --detect From 84bbfe441341045ab836a10286b0e0be07207354 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 21 Dec 2023 18:07:05 -0600 Subject: [PATCH 194/762] Fixes for Conan 2.x on Linux --- tools/setup-common.bat | 2 +- tools/setup-common.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/setup-common.bat b/tools/setup-common.bat index b3cbb147..a5696f89 100644 --- a/tools/setup-common.bat +++ b/tools/setup-common.bat @@ -1,4 +1,4 @@ pip install --upgrade conan pip install --upgrade geopandas pip install --upgrade GitPython -conan profile new default --detect +conan profile detect diff --git a/tools/setup-common.sh b/tools/setup-common.sh index 58d29d1d..d00ebc0a 100755 --- a/tools/setup-common.sh +++ b/tools/setup-common.sh @@ -2,4 +2,4 @@ pip install --upgrade --user conan pip install --upgrade --user geopandas pip install --upgrade --user GitPython -conan profile new default --detect +conan profile detect From 07cac3a03f4b98dbcc26e82ddca7beae24b98523 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Dec 2023 00:44:48 -0600 Subject: [PATCH 195/762] Additional project fixes for Conan 2.0 on Linux --- CMakeLists.txt | 4 ++++ conanfile.py | 10 ++++++++++ tools/scwx_config.cmake | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 tools/scwx_config.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 29e7ae9e..f3e5d690 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,10 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_POLICY_DEFAULT_CMP0079 NEW) set(CMAKE_POLICY_DEFAULT_CMP0148 OLD) # aws-sdk-cpp uses FindPythonInterp +include(tools/scwx_config.cmake) + +scwx_output_dirs_setup() + enable_testing() set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) diff --git a/conanfile.py b/conanfile.py index fe90a124..842e93e0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,5 @@ from conan import ConanFile +from conan.tools.cmake import CMake from conan.tools.files import copy class SupercellWxConan(ConanFile): @@ -35,3 +36,12 @@ class SupercellWxConan(ConanFile): if dep.cpp_info.libdirs: copy(self, "*.dll", dep.cpp_info.libdirs[0], self.build_folder) copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() diff --git a/tools/scwx_config.cmake b/tools/scwx_config.cmake new file mode 100644 index 00000000..0919b22e --- /dev/null +++ b/tools/scwx_config.cmake @@ -0,0 +1,19 @@ +macro(scwx_output_dirs_setup) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}/Release/bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_CURRENT_BINARY_DIR}/RelWithDebInfo/bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_CURRENT_BINARY_DIR}/MinSizeRel/bin) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}/Debug/bin) + + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}/Release/lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_CURRENT_BINARY_DIR}/RelWithDebInfo/lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_CURRENT_BINARY_DIR}/MinSizeRel/lib) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}/Debug/lib) + + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_BINARY_DIR}/Release/lib) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO ${CMAKE_CURRENT_BINARY_DIR}/RelWithDebInfo/lib) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_CURRENT_BINARY_DIR}/MinSizeRel/lib) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}/Debug/lib) +endmacro() From 6f70499105b0ef499ea604649aceb760986d1ecf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Dec 2023 13:53:27 -0600 Subject: [PATCH 196/762] Additional Conan 2.x, multi-config and CI updates --- .github/workflows/ci.yml | 9 +++++---- .gitignore | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 968adb52..3169d735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,7 +132,7 @@ jobs: shell: pwsh run: | pip install conan - conan profile new default --detect + conan profile detect conan install ./source/ ` --remote conancenter ` --build missing ` @@ -161,6 +161,7 @@ jobs: shell: bash run: | cd build/ + cd Release/ cd bin/ objcopy --only-keep-debug supercell-wx supercell-wx.debug objcopy --strip-debug --strip-unneeded supercell-wx @@ -205,7 +206,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: supercell-wx-debug-${{ matrix.artifact_suffix }} - path: ${{ github.workspace }}/build/bin/*.pdb + path: ${{ github.workspace }}/build/Release/bin/*.pdb - name: Upload Artifacts (Linux) if: ${{ startsWith(matrix.os, 'ubuntu') }} @@ -220,8 +221,8 @@ jobs: with: name: supercell-wx-debug-${{ matrix.artifact_suffix }} path: | - ${{ github.workspace }}/build/bin/*.debug - ${{ github.workspace }}/build/lib/*.debug + ${{ github.workspace }}/build/Release/bin/*.debug + ${{ github.workspace }}/build/Release/lib/*.debug - name: Build Installer (Windows) if: matrix.os == 'windows-2022' diff --git a/.gitignore b/.gitignore index 9e6da487..7002668a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ CMakeLists.txt.user CMakeCache.txt CMakeFiles CMakeScripts +CMakeUserPresets.json Testing cmake_install.cmake install_manifest.txt From 5d91bbf32f510e9866a0c4fc6d4c926a5e53943f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 13 Jun 2024 22:13:36 -0500 Subject: [PATCH 197/762] Update cmake-conan to latest develop2 (8036ecf) --- external/cmake-conan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/cmake-conan b/external/cmake-conan index c53fbf58..8036ecfd 160000 --- a/external/cmake-conan +++ b/external/cmake-conan @@ -1 +1 @@ -Subproject commit c53fbf58e4afe7cb93cb32e730d6647fc47a5dce +Subproject commit 8036ecfdcf8a8d28d19b60e83bc40ed1d1e06d1f From 3ec58659f53641d28452076fc3ec2656f69d9b3f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 14 Jun 2024 00:43:09 -0500 Subject: [PATCH 198/762] Require libpng explicitly --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index 842e93e0..2becff15 100644 --- a/conanfile.py +++ b/conanfile.py @@ -14,6 +14,7 @@ class SupercellWxConan(ConanFile): "glm/cci.20230113", "gtest/1.15.0", "libcurl/8.10.1", + "libpng/1.6.44", "libxml2/2.12.7", "openssl/3.3.2", "re2/20240702", From 5fb27151b3d9f70a18031e757ff040662385420f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 14 Jun 2024 01:22:29 -0500 Subject: [PATCH 199/762] DLLs should be copied from bindirs, not libdirs --- conanfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 2becff15..3e5719c0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -34,8 +34,9 @@ class SupercellWxConan(ConanFile): def generate(self): for dep in self.dependencies.values(): + if dep.cpp_info.bindirs: + copy(self, "*.dll", dep.cpp_info.bindirs[0], self.build_folder) if dep.cpp_info.libdirs: - copy(self, "*.dll", dep.cpp_info.libdirs[0], self.build_folder) copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) def build(self): From ff60b3c891f4ebb5fc0a87e6d49068e0cc98867d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 14 Jun 2024 19:10:18 -0500 Subject: [PATCH 200/762] MSVC compiler version for Visual Studio 2022 17.10 should be 194 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3169d735..87b73c08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: qt_tools: '' conan_arch: x86_64 conan_compiler: msvc - conan_compiler_version: 193 + conan_compiler_version: 194 conan_compiler_cppstd: 20 conan_compiler_runtime: --settings compiler.runtime=dynamic conan_compiler_libcxx: '' From 41f8434d35458d3abc79eedeed40f8f022fb7d6e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 15 Jun 2024 01:11:56 -0500 Subject: [PATCH 201/762] Add CMake top level includes to CI build --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87b73c08..6a4bb39e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,6 +153,7 @@ jobs: cmake ../source/ ` -G Ninja ` -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" ` + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${{ github.workspace }}/source/external/cmake-conan/conan_provider.cmake" ` -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" ninja supercell-wx wxtest From b7d4cdd51c4e7b37b6bc99f88e6557277ba77b79 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 08:22:23 -0600 Subject: [PATCH 202/762] Updating cmake-conan to latest develop2 (c22bbf0) --- external/cmake-conan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/cmake-conan b/external/cmake-conan index 8036ecfd..c22bbf0a 160000 --- a/external/cmake-conan +++ b/external/cmake-conan @@ -1 +1 @@ -Subproject commit 8036ecfdcf8a8d28d19b60e83bc40ed1d1e06d1f +Subproject commit c22bbf0af0b73d5f0def24a9cdf4ce503ae79e5d From a78d02617fedfa33b71b47c0ab4ef4cf7554ffa8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 08:31:56 -0600 Subject: [PATCH 203/762] Update GitHub workflow concurrency groups to prevent cancelling workflows --- .github/workflows/ci.yml | 2 +- .github/workflows/clang-format-check.yml | 2 +- .github/workflows/clang-tidy-review.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e59bc90..c4af3dc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: concurrency: # Cancel in-progress jobs for the same pull request - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index c75bab14..f3e883d6 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -8,7 +8,7 @@ on: concurrency: # Cancel in-progress jobs for the same pull request - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 7cec736c..0182e383 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -7,7 +7,7 @@ on: concurrency: # Cancel in-progress jobs for the same pull request - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: From 9e07b03b78f31788e3c6b6cbe70194d28d77c568 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 08:42:20 -0600 Subject: [PATCH 204/762] Add alternate reference for clang-format-check on pull request --- .github/workflows/clang-format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index f3e883d6..dbc0c2c9 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -35,6 +35,6 @@ jobs: - name: Check Formatting shell: bash run: | - MERGE_BASE=$(git merge-base origin/develop ${{ github.ref }}) + MERGE_BASE=$(git merge-base origin/develop ${{ github.head_ref || github.ref }}) echo "Comparing against ${MERGE_BASE}" git clang-format-17 --diff --style=file -v ${MERGE_BASE} From 8c7c04c79715cde5d2b542a9d6d88b2f01e2a159 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 08:54:53 -0600 Subject: [PATCH 205/762] Fix pull request alternate reference --- .github/workflows/clang-format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index dbc0c2c9..7e41cc61 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -35,6 +35,6 @@ jobs: - name: Check Formatting shell: bash run: | - MERGE_BASE=$(git merge-base origin/develop ${{ github.head_ref || github.ref }}) + MERGE_BASE=$(git merge-base origin/develop ${{ github.event.pull_request.head_sha || github.ref }}) echo "Comparing against ${MERGE_BASE}" git clang-format-17 --diff --style=file -v ${MERGE_BASE} From 1fd696d787fde879e78646e2576e5ade5f975fcc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 08:56:19 -0600 Subject: [PATCH 206/762] Fix pull request alternate reference (again) --- .github/workflows/clang-format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 7e41cc61..acdcd586 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -35,6 +35,6 @@ jobs: - name: Check Formatting shell: bash run: | - MERGE_BASE=$(git merge-base origin/develop ${{ github.event.pull_request.head_sha || github.ref }}) + MERGE_BASE=$(git merge-base origin/develop ${{ github.event.pull_request.head.sha || github.ref }}) echo "Comparing against ${MERGE_BASE}" git clang-format-17 --diff --style=file -v ${MERGE_BASE} From df1fc2fe543f6b810c4dc107902e701c2f4d8398 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 09:10:03 -0600 Subject: [PATCH 207/762] freetype dependency needs overridden due to fontconfig import --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 3e5719c0..d8dcb583 100644 --- a/conanfile.py +++ b/conanfile.py @@ -7,7 +7,6 @@ class SupercellWxConan(ConanFile): requires = ("boost/1.86.0", "cpr/1.11.0", "fontconfig/2.15.0", - "freetype/2.13.3", "geographiclib/2.4", "geos/3.13.0", "glew/2.2.0", @@ -29,6 +28,7 @@ class SupercellWxConan(ConanFile): "openssl/*:shared" : True} def requirements(self): + self.requires("freetype/2.13.3", override=True) if self.settings.os == "Linux": self.requires("onetbb/2021.12.0") From ed353248e0e8a8ec06c24f2b957eb287896b1462 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Nov 2024 23:46:07 -0600 Subject: [PATCH 208/762] Latch page required logic at startup to ensure appropriate pages display --- scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp index 92be250f..9b265bd4 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp @@ -22,6 +22,9 @@ class SetupWizard::Impl public: explicit Impl() = default; ~Impl() = default; + + bool mapProviderPageIsRequired_ {MapProviderPage::IsRequired()}; + bool audioCodecPageIsRequired_ {AudioCodecPage::IsRequired()}; }; SetupWizard::SetupWizard(QWidget* parent) : @@ -66,14 +69,14 @@ int SetupWizard::nextId() const { case static_cast(Page::MapProvider): case static_cast(Page::MapLayout): - if (MapProviderPage::IsRequired()) + if (p->mapProviderPageIsRequired_) { return nextId; } break; case static_cast(Page::AudioCodec): - if (AudioCodecPage::IsRequired()) + if (p->audioCodecPageIsRequired_) { return nextId; } From d726da5d73cab433550ed8362a7248cf2f7b1082 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 09:17:04 -0600 Subject: [PATCH 209/762] Use conan profiles in CI --- .github/workflows/ci.yml | 20 +++++++++++--------- tools/conan/profiles/scwx-linux_clang-17 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-11 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-12 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-13 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-14 | 8 ++++++++ tools/conan/profiles/scwx-win64_msvc2022 | 8 ++++++++ 7 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 tools/conan/profiles/scwx-linux_clang-17 create mode 100644 tools/conan/profiles/scwx-linux_gcc-11 create mode 100644 tools/conan/profiles/scwx-linux_gcc-12 create mode 100644 tools/conan/profiles/scwx-linux_gcc-13 create mode 100644 tools/conan/profiles/scwx-linux_gcc-14 create mode 100644 tools/conan/profiles/scwx-win64_msvc2022 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a4bb39e..846d76da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: conan_compiler_runtime: --settings compiler.runtime=dynamic conan_compiler_libcxx: '' conan_package_manager: '' + conan_profile: scwx-win64_msvc2022 artifact_suffix: windows-x64 - name: linux64_gcc os: ubuntu-22.04 @@ -59,6 +60,7 @@ jobs: conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + conan_profile: scwx-linux_gcc-11 artifact_suffix: linux-x64 compiler_packages: '' - name: linux64_clang @@ -79,6 +81,7 @@ jobs: conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + conan_profile: scwx-linux_clang-17 artifact_suffix: linux-clang-x64 compiler_packages: clang-17 name: ${{ matrix.name }} @@ -132,17 +135,14 @@ jobs: shell: pwsh run: | pip install conan - conan profile detect + conan profile detect -e + conan config install ` + ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` + -tf profiles conan install ./source/ ` --remote conancenter ` --build missing ` - --settings arch=${{ matrix.conan_arch }} ` - --settings build_type=${{ matrix.build_type }} ` - --settings compiler="${{ matrix.conan_compiler }}" ` - --settings compiler.version=${{ matrix.conan_compiler_version }} ` - --settings compiler.cppstd=${{ matrix.conan_compiler_cppstd }} ` - ${{ matrix.conan_compiler_libcxx }} ` - ${{ matrix.conan_compiler_runtime }} ` + --profile:all ${{ matrix.conan_profile }} ${{ matrix.conan_package_manager }} - name: Build Supercell Wx @@ -154,7 +154,9 @@ jobs: -G Ninja ` -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" ` -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${{ github.workspace }}/source/external/cmake-conan/conan_provider.cmake" ` - -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" ` + -DCONAN_HOST_PROFILE="${{ matrix.conan_profile }}" ` + -DCONAN_BUILD_PROFILE="${{ matrix.conan_profile }}" ninja supercell-wx wxtest - name: Separate Debug Symbols (Linux) diff --git a/tools/conan/profiles/scwx-linux_clang-17 b/tools/conan/profiles/scwx-linux_clang-17 new file mode 100644 index 00000000..1ee9f30a --- /dev/null +++ b/tools/conan/profiles/scwx-linux_clang-17 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=17 +os=Linux \ No newline at end of file diff --git a/tools/conan/profiles/scwx-linux_gcc-11 b/tools/conan/profiles/scwx-linux_gcc-11 new file mode 100644 index 00000000..cc41b528 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-11 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=11 +os=Linux \ No newline at end of file diff --git a/tools/conan/profiles/scwx-linux_gcc-12 b/tools/conan/profiles/scwx-linux_gcc-12 new file mode 100644 index 00000000..a7db9371 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-12 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=12 +os=Linux \ No newline at end of file diff --git a/tools/conan/profiles/scwx-linux_gcc-13 b/tools/conan/profiles/scwx-linux_gcc-13 new file mode 100644 index 00000000..ef3ef064 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-13 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=13 +os=Linux \ No newline at end of file diff --git a/tools/conan/profiles/scwx-linux_gcc-14 b/tools/conan/profiles/scwx-linux_gcc-14 new file mode 100644 index 00000000..94220e3e --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-14 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=14 +os=Linux \ No newline at end of file diff --git a/tools/conan/profiles/scwx-win64_msvc2022 b/tools/conan/profiles/scwx-win64_msvc2022 new file mode 100644 index 00000000..0c4513d8 --- /dev/null +++ b/tools/conan/profiles/scwx-win64_msvc2022 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=msvc +compiler.cppstd=20 +compiler.runtime=dynamic +compiler.version=194 +os=Windows \ No newline at end of file From 01f790020e7967294f6e16789545182eb8093ae5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 12:11:52 -0600 Subject: [PATCH 210/762] Configure OpenSSL crypto library for aws-sdk-cpp --- external/aws-sdk-cpp.cmake | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/external/aws-sdk-cpp.cmake b/external/aws-sdk-cpp.cmake index 3952f0f8..9be0c88c 100644 --- a/external/aws-sdk-cpp.cmake +++ b/external/aws-sdk-cpp.cmake @@ -21,6 +21,14 @@ set(MINIMIZE_SIZE OFF CACHE BOOL "If enabled, the SDK will be built via # Save off ${CMAKE_CXX_FLAGS} before modifying compiler settings set(CMAKE_CXX_FLAGS_PREV "${CMAKE_CXX_FLAGS}") +# Configure OpenSSL crypto library +find_package(OpenSSL) +set(crypto_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) +set(crypto_ROOT $<$:${openssl_PACKAGE_FOLDER_RELEASE}> + $<$:${openssl_PACKAGE_FOLDER_DEBUG}>) +set(crypto_SHARED_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.so libcrypto.dylib +set(crypto_STATIC_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.a + # Fix CMake errors for internal variables not set include(aws-sdk-cpp/cmake/compiler_settings.cmake) set_msvc_warnings() From e61ac8814de3a693e4f85ed5232026d8aacba166 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 12:12:48 -0600 Subject: [PATCH 211/762] CI conan install fixes --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 846d76da..b2b5537e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,16 +139,18 @@ jobs: conan config install ` ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` -tf profiles - conan install ./source/ ` + mkdir -p build + cd build + conan install ../source/ ` --remote conancenter ` --build missing ` - --profile:all ${{ matrix.conan_profile }} + --profile:all ${{ matrix.conan_profile }} ` ${{ matrix.conan_package_manager }} - name: Build Supercell Wx shell: pwsh run: | - mkdir build + mkdir -p build cd build cmake ../source/ ` -G Ninja ` From d64eb762b46ac7df1886aeec3edb76748bd135e2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 29 Nov 2024 15:07:04 -0500 Subject: [PATCH 212/762] Revert finding some Qt parts in cmake that appear to be causing issues with build on older versions of linux (ubuntu22.04) --- scwx-qt/scwx-qt.cmake | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b68c20c6..2b395246 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -21,9 +21,7 @@ find_package(Python COMPONENTS Interpreter) find_package(SQLite3) find_package(QT NAMES Qt6 - COMPONENTS BuildInternals - Core - Gui + COMPONENTS Gui LinguistTools Multimedia Network @@ -37,9 +35,7 @@ find_package(QT NAMES Qt6 REQUIRED) find_package(Qt${QT_VERSION_MAJOR} - COMPONENTS BuildInternals - Core - Gui + COMPONENTS Gui LinguistTools Multimedia Network From eb47443bbac45d6ad98bf1894f894b7bb4c63e0a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 20:17:59 -0600 Subject: [PATCH 213/762] Don't use a generator expression to determine ${crypto_ROOT} for AWS SDK --- external/aws-sdk-cpp.cmake | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/external/aws-sdk-cpp.cmake b/external/aws-sdk-cpp.cmake index 9be0c88c..e8332c4a 100644 --- a/external/aws-sdk-cpp.cmake +++ b/external/aws-sdk-cpp.cmake @@ -24,8 +24,16 @@ set(CMAKE_CXX_FLAGS_PREV "${CMAKE_CXX_FLAGS}") # Configure OpenSSL crypto library find_package(OpenSSL) set(crypto_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) -set(crypto_ROOT $<$:${openssl_PACKAGE_FOLDER_RELEASE}> - $<$:${openssl_PACKAGE_FOLDER_DEBUG}>) + +# FIXME: +# Cannot use a generator expression here, since this is needed at config time. +# But, this breaks multi-config. Need to find a better solution. +if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") + set(crypto_ROOT ${openssl_PACKAGE_FOLDER_DEBUG}) +else() + set(crypto_ROOT ${openssl_PACKAGE_FOLDER_RELEASE}) +endif() + set(crypto_SHARED_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.so libcrypto.dylib set(crypto_STATIC_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.a From 88807a05a17103ad303e007e4261587c82eeca69 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 20:18:45 -0600 Subject: [PATCH 214/762] freetype needs specified in main requires section, otherwise includes are missing --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index d8dcb583..24cbeb13 100644 --- a/conanfile.py +++ b/conanfile.py @@ -7,6 +7,7 @@ class SupercellWxConan(ConanFile): requires = ("boost/1.86.0", "cpr/1.11.0", "fontconfig/2.15.0", + "freetype/2.13.2", "geographiclib/2.4", "geos/3.13.0", "glew/2.2.0", @@ -28,7 +29,6 @@ class SupercellWxConan(ConanFile): "openssl/*:shared" : True} def requirements(self): - self.requires("freetype/2.13.3", override=True) if self.settings.os == "Linux": self.requires("onetbb/2021.12.0") From c74a56206c2cf45a1fe9ab4022776c14ca8f6a7a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 21:06:53 -0600 Subject: [PATCH 215/762] Add conan profiles to setup scripts --- setup-debug.bat | 7 ++++++- setup-debug.sh | 5 +++++ setup-release.bat | 7 ++++++- setup-release.sh | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/setup-debug.bat b/setup-debug.bat index 1ff36fb2..74100c83 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -2,13 +2,18 @@ call tools\setup-common.bat set build_dir=build-debug set build_type=Debug +set conan_profile=scwx-win64_msvc2022 set qt_version=6.8.0 set qt_arch=msvc2022_64 +conan install tools/conan/profiles/%conan_profile% -tf profiles + mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ + -DCONAN_HOST_PROFILE=${conan_profile} ^ + -DCONAN_BUILD_PROFILE=${conan_profile} pause diff --git a/setup-debug.sh b/setup-debug.sh index ceeff368..b832db88 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -3,10 +3,13 @@ build_dir=${1:-build-debug} build_type=Debug +conan_profile=${2:-scwx-linux_gcc-11} qt_version=6.8.0 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" +conan install tools/conan/profiles/${conan_profile} -tf profiles + mkdir -p ${build_dir} cmake -B ${build_dir} -S . \ -DCMAKE_BUILD_TYPE=${build_type} \ @@ -14,4 +17,6 @@ cmake -B ${build_dir} -S . \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ + -DCONAN_HOST_PROFILE=${conan_profile} \ + -DCONAN_BUILD_PROFILE=${conan_profile} \ -G Ninja diff --git a/setup-release.bat b/setup-release.bat index 332acdfa..c897302e 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -2,13 +2,18 @@ call tools\setup-common.bat set build_dir=build-release set build_type=Release +set conan_profile=scwx-win64_msvc2022 set qt_version=6.8.0 set qt_arch=msvc2022_64 +conan install tools/conan/profiles/%conan_profile% -tf profiles + mkdir %build_dir% cmake -B %build_dir% -S . ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ + -DCONAN_HOST_PROFILE=${conan_profile} ^ + -DCONAN_BUILD_PROFILE=${conan_profile} pause diff --git a/setup-release.sh b/setup-release.sh index be0fe858..75b05f66 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -3,10 +3,13 @@ build_dir=${1:-build-release} build_type=Release +conan_profile=${2:-scwx-linux_gcc-11} qt_version=6.8.0 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" +conan install tools/conan/profiles/${conan_profile} -tf profiles + mkdir -p ${build_dir} cmake -B ${build_dir} -S . \ -DCMAKE_BUILD_TYPE=${build_type} \ @@ -14,4 +17,6 @@ cmake -B ${build_dir} -S . \ -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ + -DCONAN_HOST_PROFILE=${conan_profile} \ + -DCONAN_BUILD_PROFILE=${conan_profile} \ -G Ninja From 6b3149fbceb1758dee312fcccd70de1532e8b63d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 21:08:05 -0600 Subject: [PATCH 216/762] Fix mkdir usage and output directory in ci.yml --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b5537e..fc3de9c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,18 +139,19 @@ jobs: conan config install ` ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` -tf profiles - mkdir -p build + mkdir build cd build + mkdir conan conan install ../source/ ` --remote conancenter ` --build missing ` --profile:all ${{ matrix.conan_profile }} ` + --output-folder ./conan/ ` ${{ matrix.conan_package_manager }} - name: Build Supercell Wx shell: pwsh run: | - mkdir -p build cd build cmake ../source/ ` -G Ninja ` From 1ea94b02cd35ae126d85a88e820b2260a589e3ec Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 21:37:07 -0600 Subject: [PATCH 217/762] Bump version to v0.4.7 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4af3dc5..afc6472a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.6 + SCWX_VERSION: v0.4.7 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index c4e86181..48c1d9ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} - VERSION 0.4.6 + VERSION 0.4.7 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -44,7 +44,7 @@ conan_basic_setup(TARGETS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.6") +set(SCWX_VERSION "0.4.7") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) From 036acf3c85e89a3b1d6543deef6937c386613e7a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 21:46:15 -0600 Subject: [PATCH 218/762] Update aws-sdk-cpp to 1.11.457 --- external/aws-sdk-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/aws-sdk-cpp b/external/aws-sdk-cpp index d5eb42fe..c95e71a9 160000 --- a/external/aws-sdk-cpp +++ b/external/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit d5eb42fe7c632868d4535b454ee2e5ba0e349b7f +Subproject commit c95e71a9d23bba0c2f6c6a7bc37ae63b7351e8b7 From 27b2d79a65ca90eb1cae4ad1efccc78db83dcf93 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 22:00:00 -0600 Subject: [PATCH 219/762] Collect artifacts for CI debug --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3de9c5..bc83f451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -288,3 +288,20 @@ jobs: with: name: supercell-wx-test-logs-${{ matrix.name }} path: ${{ github.workspace }}/build/Testing/ + + - name: CI Build Directory Listing + if: ${{ !cancelled() && startsWith(matrix.os, 'ubuntu') }} + shell: bash + run: | + ls -alRF ./build/ + + - name: Upload CI Build Debug + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: supercell-wx-ci-debug-logs-${{ matrix.name }} + path: | + ${{ github.workspace }}/build/.ninja_log + ${{ github.workspace }}/build/build.ninja + ${{ github.workspace }}/build/CMakeCache.txt + ${{ github.workspace }}/build/compile_commands.json From 6e13ed2c85b0aedb527cb1e3d585127415b47373 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 29 Nov 2024 22:57:33 -0600 Subject: [PATCH 220/762] Specify to AWS SDK which crypto library to use --- external/aws-sdk-cpp.cmake | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/external/aws-sdk-cpp.cmake b/external/aws-sdk-cpp.cmake index e8332c4a..f88df9ca 100644 --- a/external/aws-sdk-cpp.cmake +++ b/external/aws-sdk-cpp.cmake @@ -23,19 +23,7 @@ set(CMAKE_CXX_FLAGS_PREV "${CMAKE_CXX_FLAGS}") # Configure OpenSSL crypto library find_package(OpenSSL) -set(crypto_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) - -# FIXME: -# Cannot use a generator expression here, since this is needed at config time. -# But, this breaks multi-config. Need to find a better solution. -if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") - set(crypto_ROOT ${openssl_PACKAGE_FOLDER_DEBUG}) -else() - set(crypto_ROOT ${openssl_PACKAGE_FOLDER_RELEASE}) -endif() - -set(crypto_SHARED_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.so libcrypto.dylib -set(crypto_STATIC_LIBRARY_ROOT ${crypto_ROOT}) # libcrypto.a +add_library(crypto ALIAS OpenSSL::Crypto) # Fix CMake errors for internal variables not set include(aws-sdk-cpp/cmake/compiler_settings.cmake) From d6a0a17d3fdbb54d56aa7c8a426789c8c5a5a618 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 01:27:57 -0600 Subject: [PATCH 221/762] Setup script fixes --- setup-debug.bat | 12 +++++++++--- setup-debug.sh | 8 +++++++- setup-multi.bat | 20 +++++++++++++++----- setup-release.bat | 12 +++++++++--- setup-release.sh | 8 +++++++- tools/setup-common.bat | 2 +- tools/setup-common.sh | 2 +- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/setup-debug.bat b/setup-debug.bat index 74100c83..f082a0cc 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -6,7 +6,13 @@ set conan_profile=scwx-win64_msvc2022 set qt_version=6.8.0 set qt_arch=msvc2022_64 -conan install tools/conan/profiles/%conan_profile% -tf profiles +conan config install tools/conan/profiles/%conan_profile% -tf profiles +conan install . ^ + --remote conancenter ^ + --build missing ^ + --profile:all %conan_profile% ^ + --settings:all build_type=%build_type% ^ + --output-folder %build_dir%/conan mkdir %build_dir% cmake -B %build_dir% -S . ^ @@ -14,6 +20,6 @@ cmake -B %build_dir% -S . ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ - -DCONAN_HOST_PROFILE=${conan_profile} ^ - -DCONAN_BUILD_PROFILE=${conan_profile} + -DCONAN_HOST_PROFILE=%conan_profile% ^ + -DCONAN_BUILD_PROFILE=%conan_profile% pause diff --git a/setup-debug.sh b/setup-debug.sh index b832db88..0abb4696 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -8,7 +8,13 @@ qt_version=6.8.0 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" -conan install tools/conan/profiles/${conan_profile} -tf profiles +conan config install tools/conan/profiles/${conan_profile} -tf profiles +conan install . \ + --remote conancenter \ + --build missing \ + --profile:all ${conan_profile} \ + --settings:all build_type=${build_type} \ + --output-folder ${build_dir}/conan mkdir -p ${build_dir} cmake -B ${build_dir} -S . \ diff --git a/setup-multi.bat b/setup-multi.bat index 778b1328..9c555594 100644 --- a/setup-multi.bat +++ b/setup-multi.bat @@ -1,11 +1,21 @@ call tools\setup-common.bat -set build_dir=build-debug -set build_type=Debug -set qt_version=6.7.1 +set build_dir=build +set conan_profile=scwx-win64_msvc2022 +set qt_version=6.8.0 +set qt_arch=msvc2022_64 + +conan config install tools/conan/profiles/%conan_profile% -tf profiles +conan install . ^ + --remote conancenter ^ + --build missing ^ + --profile:all %conan_profile% ^ + --output-folder %build_dir%/conan mkdir %build_dir% cmake -B %build_dir% -S . ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/msvc2019_64 ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake + -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ + -DCONAN_HOST_PROFILE=%conan_profile% ^ + -DCONAN_BUILD_PROFILE=%conan_profile% pause diff --git a/setup-release.bat b/setup-release.bat index c897302e..404ebdcd 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -6,7 +6,13 @@ set conan_profile=scwx-win64_msvc2022 set qt_version=6.8.0 set qt_arch=msvc2022_64 -conan install tools/conan/profiles/%conan_profile% -tf profiles +conan config install tools/conan/profiles/%conan_profile% -tf profiles +conan install . ^ + --remote conancenter ^ + --build missing ^ + --profile:all %conan_profile% ^ + --settings:all build_type=%build_type% ^ + --output-folder %build_dir%/conan mkdir %build_dir% cmake -B %build_dir% -S . ^ @@ -14,6 +20,6 @@ cmake -B %build_dir% -S . ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ - -DCONAN_HOST_PROFILE=${conan_profile} ^ - -DCONAN_BUILD_PROFILE=${conan_profile} + -DCONAN_HOST_PROFILE=%conan_profile% ^ + -DCONAN_BUILD_PROFILE=%conan_profile% pause diff --git a/setup-release.sh b/setup-release.sh index 75b05f66..9b4f2779 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -8,7 +8,13 @@ qt_version=6.8.0 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" -conan install tools/conan/profiles/${conan_profile} -tf profiles +conan config install tools/conan/profiles/${conan_profile} -tf profiles +conan install . \ + --remote conancenter \ + --build missing \ + --profile:all ${conan_profile} \ + --settings:all build_type=${build_type} \ + --output-folder ${build_dir}/conan mkdir -p ${build_dir} cmake -B ${build_dir} -S . \ diff --git a/tools/setup-common.bat b/tools/setup-common.bat index a5696f89..bada34ed 100644 --- a/tools/setup-common.bat +++ b/tools/setup-common.bat @@ -1,4 +1,4 @@ pip install --upgrade conan pip install --upgrade geopandas pip install --upgrade GitPython -conan profile detect +conan profile detect -e diff --git a/tools/setup-common.sh b/tools/setup-common.sh index d00ebc0a..2533d6ec 100755 --- a/tools/setup-common.sh +++ b/tools/setup-common.sh @@ -2,4 +2,4 @@ pip install --upgrade --user conan pip install --upgrade --user geopandas pip install --upgrade --user GitPython -conan profile detect +conan profile detect -e From d45801bd505b30e18b3d0086984b3c57980c5bf2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 01:29:28 -0600 Subject: [PATCH 222/762] Revert test artifacts from CI build --- .github/workflows/ci.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc83f451..fc3de9c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -288,20 +288,3 @@ jobs: with: name: supercell-wx-test-logs-${{ matrix.name }} path: ${{ github.workspace }}/build/Testing/ - - - name: CI Build Directory Listing - if: ${{ !cancelled() && startsWith(matrix.os, 'ubuntu') }} - shell: bash - run: | - ls -alRF ./build/ - - - name: Upload CI Build Debug - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: supercell-wx-ci-debug-logs-${{ matrix.name }} - path: | - ${{ github.workspace }}/build/.ninja_log - ${{ github.workspace }}/build/build.ninja - ${{ github.workspace }}/build/CMakeCache.txt - ${{ github.workspace }}/build/compile_commands.json From f1e9296299e7f09185d0e4ce78ebb47326f5c077 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 01:32:11 -0600 Subject: [PATCH 223/762] Remove unused conan CI matrix variables, now defined in profile --- .github/workflows/ci.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3de9c5..060be896 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,12 +33,6 @@ jobs: qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_arch: x86_64 - conan_compiler: msvc - conan_compiler_version: 194 - conan_compiler_cppstd: 20 - conan_compiler_runtime: --settings compiler.runtime=dynamic - conan_compiler_libcxx: '' conan_package_manager: '' conan_profile: scwx-win64_msvc2022 artifact_suffix: windows-x64 @@ -53,12 +47,6 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_arch: x86_64 - conan_compiler: gcc - conan_compiler_version: 11 - conan_compiler_cppstd: 20 - conan_compiler_libcxx: --settings compiler.libcxx=libstdc++ - conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11 artifact_suffix: linux-x64 @@ -74,12 +62,6 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_arch: x86_64 - conan_compiler: clang - conan_compiler_version: 17 - conan_compiler_cppstd: 20 - conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 - conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 artifact_suffix: linux-clang-x64 From 0f9b595ce79b13f39d9636b94fa494f5ec8c2ac1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 01:37:42 -0600 Subject: [PATCH 224/762] Override build type in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3340670e..90c31612 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,7 @@ jobs: --remote conancenter ` --build missing ` --profile:all ${{ matrix.conan_profile }} ` + --settings:all build_type=${{ matrix.build_type }} ` --output-folder ./conan/ ` ${{ matrix.conan_package_manager }} From f12f209a0d5d25e2956a1c3208a00f7a498ec86f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 01:38:33 -0600 Subject: [PATCH 225/762] Update clang-tidy-review for conan 2 --- .github/workflows/clang-tidy-review.yml | 29 +++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 0182e383..1f26bd58 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -24,12 +24,8 @@ jobs: qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_arch: x86_64 - conan_compiler: clang - conan_compiler_version: 17 - conan_compiler_libcxx: --settings compiler.libcxx=libstdc++11 - conan_compiler_runtime: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + conan_profile: scwx-linux_clang-17 compiler_packages: clang-17 clang-tidy-17 name: ${{ matrix.name }} runs-on: ${{ matrix.os }} @@ -78,28 +74,33 @@ jobs: - name: Install Conan Packages shell: pwsh run: | - pip install "conan<2.0" - conan profile new default --detect + pip install conan + conan profile detect -e + conan config install ` + ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` + -tf profiles + mkdir build + cd build + mkdir conan conan install ./source/ ` --remote conancenter ` --build missing ` - --settings arch=${{ matrix.conan_arch }} ` - --settings build_type=${{ matrix.build_type }} ` - --settings compiler="${{ matrix.conan_compiler }}" ` - --settings compiler.version=${{ matrix.conan_compiler_version }} ` - ${{ matrix.conan_compiler_libcxx }} ` - ${{ matrix.conan_compiler_runtime }} ` + --profile:all ${{ matrix.conan_profile }} ` + --settings:all build_type=${{ matrix.build_type }} ` + --output-folder ./conan/ ` ${{ matrix.conan_package_manager }} - name: Autogenerate shell: pwsh run: | - mkdir build cd build cmake ../source/ ` -G Ninja ` -DCMAKE_BUILD_TYPE="${{ matrix.build_type }}" ` + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${{ github.workspace }}/source/external/cmake-conan/conan_provider.cmake" ` -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/supercell-wx" ` + -DCONAN_HOST_PROFILE="${{ matrix.conan_profile }}" ` + -DCONAN_BUILD_PROFILE="${{ matrix.conan_profile }}" ` -DCMAKE_EXPORT_COMPILE_COMMANDS=on ninja scwx-qt_generate_counties_db ` scwx-qt_generate_versions ` From 9e7075c7e5f0c58d5b28d4eeeec915e038e591e9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 09:20:32 -0600 Subject: [PATCH 226/762] Fix build folder for dependency deployment --- conanfile.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 24cbeb13..12a0d058 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,7 @@ from conan import ConanFile from conan.tools.cmake import CMake from conan.tools.files import copy +import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") @@ -33,11 +34,16 @@ class SupercellWxConan(ConanFile): self.requires("onetbb/2021.12.0") def generate(self): + build_folder = os.path.join(self.build_folder, + "..", + str(self.settings.build_type), + self.cpp_info.bindirs[0]) + for dep in self.dependencies.values(): if dep.cpp_info.bindirs: - copy(self, "*.dll", dep.cpp_info.bindirs[0], self.build_folder) + copy(self, "*.dll", dep.cpp_info.bindirs[0], build_folder) if dep.cpp_info.libdirs: - copy(self, "*.dylib", dep.cpp_info.libdirs[0], self.build_folder) + copy(self, "*.dylib", dep.cpp_info.libdirs[0], build_folder) def build(self): cmake = CMake(self) From 61a93dbc305fa46378b3da7e40539297a1a0a7a9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 09:40:07 -0600 Subject: [PATCH 227/762] setup-multi.bat should install Debug and Release conan dependencies --- setup-multi.bat | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup-multi.bat b/setup-multi.bat index 9c555594..b12a0c06 100644 --- a/setup-multi.bat +++ b/setup-multi.bat @@ -10,6 +10,13 @@ conan install . ^ --remote conancenter ^ --build missing ^ --profile:all %conan_profile% ^ + --settings:all build_type=Debug ^ + --output-folder %build_dir%/conan +conan install . ^ + --remote conancenter ^ + --build missing ^ + --profile:all %conan_profile% ^ + --settings:all build_type=Release ^ --output-folder %build_dir%/conan mkdir %build_dir% From 82cec1d7441c3c31766bd2f1a8d912187f318ff9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 09:40:42 -0600 Subject: [PATCH 228/762] Fix clang-tidy-review conan install source directory --- .github/workflows/clang-tidy-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 1f26bd58..e2a1d57d 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -82,7 +82,7 @@ jobs: mkdir build cd build mkdir conan - conan install ./source/ ` + conan install ../source/ ` --remote conancenter ` --build missing ` --profile:all ${{ matrix.conan_profile }} ` From 7b99dffb3ecfa9808a0147f04775645238a6bc3c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 17:35:35 +0000 Subject: [PATCH 229/762] Update dependency sqlite3 to v3.47.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 12a0d058..976cce47 100644 --- a/conanfile.py +++ b/conanfile.py @@ -20,7 +20,7 @@ class SupercellWxConan(ConanFile): "openssl/3.3.2", "re2/20240702", "spdlog/1.14.1", - "sqlite3/3.46.1", + "sqlite3/3.47.1", "vulkan-loader/1.3.243.0", "zlib/1.3.1") generators = ("CMakeDeps") From cee1831fb203b73304695fb9a570730fda061fad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 18:42:52 +0000 Subject: [PATCH 230/762] Update dependency spdlog to v1.15.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 976cce47..52d59c41 100644 --- a/conanfile.py +++ b/conanfile.py @@ -19,7 +19,7 @@ class SupercellWxConan(ConanFile): "libxml2/2.12.7", "openssl/3.3.2", "re2/20240702", - "spdlog/1.14.1", + "spdlog/1.15.0", "sqlite3/3.47.1", "vulkan-loader/1.3.243.0", "zlib/1.3.1") From 64b679a0028c8fbc6c4fe9b333923972a6c3f903 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 30 Nov 2024 15:43:50 -0500 Subject: [PATCH 231/762] Added cursor icon always on option to general settings --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 5 ++++- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 10 ++++++---- .../scwx/qt/settings/general_settings.cpp | 13 +++++++++++-- .../scwx/qt/settings/general_settings.hpp | 1 + scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 6 ++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 17 +++++++++++++++-- 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d3c7fefc..9c222ef6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1020,11 +1020,14 @@ void MapWidget::UpdateMouseCoordinate(const common::Coordinate& coordinate) { if (p->context_->mouse_coordinate() != coordinate) { + auto& generalSettings = settings::GeneralSettings::Instance(); + p->context_->set_mouse_coordinate(coordinate); auto keyboardModifiers = QGuiApplication::keyboardModifiers(); - if (keyboardModifiers != Qt::KeyboardModifier::NoModifier || + if (generalSettings.cursor_icon_always_on().GetValue() || + keyboardModifiers != Qt::KeyboardModifier::NoModifier || keyboardModifiers != p->lastKeyboardModifiers_) { QMetaObject::invokeMethod( diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 9640a705..b9393ee0 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -328,9 +328,13 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->activeBoxInner_->SetBorder(1.0f * pixelRatio, {255, 255, 255, 255}); } + auto& generalSettings = settings::GeneralSettings::Instance(); + // Cursor Icon - bool cursorIconVisible = QGuiApplication::keyboardModifiers() & - Qt::KeyboardModifier::ControlModifier; + bool cursorIconVisible = + generalSettings.cursor_icon_always_on().GetValue() || + (QGuiApplication::keyboardModifiers() & + Qt::KeyboardModifier::ControlModifier); p->geoIcons_->SetIconVisible(p->cursorIcon_, cursorIconVisible); if (cursorIconVisible) { @@ -434,8 +438,6 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) ImGui::End(); } - auto& generalSettings = settings::GeneralSettings::Instance(); - // Map Center Icon if (params.width != p->lastWidth_ || params.height != p->lastHeight_) { diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 9eca4348..3c0ee929 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -77,6 +77,7 @@ public: trackLocation_.SetDefault(false); updateNotificationsEnabled_.SetDefault(true); warningsProvider_.SetDefault(defaultWarningsProviderValue); + cursorIconAlwaysOn_.SetDefault(false); fontSizes_.SetElementMinimum(1); fontSizes_.SetElementMaximum(72); @@ -166,6 +167,7 @@ public: SettingsVariable trackLocation_ {"track_location"}; SettingsVariable updateNotificationsEnabled_ {"update_notifications"}; SettingsVariable warningsProvider_ {"warnings_provider"}; + SettingsVariable cursorIconAlwaysOn_ {"cursor_icon_always_on"}; }; GeneralSettings::GeneralSettings() : @@ -198,7 +200,8 @@ GeneralSettings::GeneralSettings() : &p->themeFile_, &p->trackLocation_, &p->updateNotificationsEnabled_, - &p->warningsProvider_}); + &p->warningsProvider_, + &p->cursorIconAlwaysOn_}); SetDefaults(); } GeneralSettings::~GeneralSettings() = default; @@ -348,6 +351,11 @@ SettingsVariable& GeneralSettings::warnings_provider() const return p->warningsProvider_; } +SettingsVariable& GeneralSettings::cursor_icon_always_on() const +{ + return p->cursorIconAlwaysOn_; +} + bool GeneralSettings::Shutdown() { bool dataChanged = false; @@ -397,7 +405,8 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->trackLocation_ == rhs.p->trackLocation_ && lhs.p->updateNotificationsEnabled_ == rhs.p->updateNotificationsEnabled_ && - lhs.p->warningsProvider_ == rhs.p->warningsProvider_); + lhs.p->warningsProvider_ == rhs.p->warningsProvider_ && + lhs.p->cursorIconAlwaysOn_ == rhs.p->cursorIconAlwaysOn_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 3e527238..46004c57 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -53,6 +53,7 @@ public: SettingsVariable& track_location() const; SettingsVariable& update_notifications_enabled() const; SettingsVariable& warnings_provider() const; + SettingsVariable& cursor_icon_always_on() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index a9b81706..69c3a387 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -137,6 +137,7 @@ public: &showMapCenter_, &showMapLogo_, &updateNotificationsEnabled_, + &cursorIconAlwaysOn_, &debugEnabled_, &alertAudioSoundFile_, &alertAudioLocationMethod_, @@ -251,6 +252,7 @@ public: settings::SettingsInterface showMapCenter_ {}; settings::SettingsInterface showMapLogo_ {}; settings::SettingsInterface updateNotificationsEnabled_ {}; + settings::SettingsInterface cursorIconAlwaysOn_ {}; settings::SettingsInterface debugEnabled_ {}; std::unordered_map> @@ -762,6 +764,10 @@ void SettingsDialogImpl::SetupGeneralTab() updateNotificationsEnabled_.SetEditWidget( self_->ui->enableUpdateNotificationsCheckBox); + cursorIconAlwaysOn_.SetSettingsVariable( + generalSettings.cursor_icon_always_on()); + cursorIconAlwaysOn_.SetEditWidget(self_->ui->cursorIconAlwaysOnCheckBox); + debugEnabled_.SetSettingsVariable(generalSettings.debug_enabled()); debugEnabled_.SetEditWidget(self_->ui->debugEnabledCheckBox); } diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index f66aba6c..12bcbc0e 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - -133 + -246 511 - 676 + 703 @@ -590,6 +590,19 @@ + + + + false + + + + + + Multi-Pane Cursor Marker Always On + + + From 372712745c248673956e97507d4d080abdcca985 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 15:42:15 -0600 Subject: [PATCH 232/762] Updating includes for fmt 11.x --- scwx-qt/source/scwx/qt/manager/font_manager.cpp | 1 + scwx-qt/source/scwx/qt/settings/settings_variable.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 20f5f9a6..72779f67 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index a5387937..6eda8437 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace scwx { From aba904af3c126f1fc8a9de4292e84f99b6d85fef Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 22:20:23 -0600 Subject: [PATCH 233/762] Ensure conan profiles have a newline at the end of the file --- tools/conan/profiles/scwx-linux_clang-17 | 2 +- tools/conan/profiles/scwx-linux_gcc-11 | 2 +- tools/conan/profiles/scwx-linux_gcc-12 | 2 +- tools/conan/profiles/scwx-linux_gcc-13 | 2 +- tools/conan/profiles/scwx-linux_gcc-14 | 2 +- tools/conan/profiles/scwx-win64_msvc2022 | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/conan/profiles/scwx-linux_clang-17 b/tools/conan/profiles/scwx-linux_clang-17 index 1ee9f30a..3ce71b57 100644 --- a/tools/conan/profiles/scwx-linux_clang-17 +++ b/tools/conan/profiles/scwx-linux_clang-17 @@ -5,4 +5,4 @@ compiler=clang compiler.cppstd=20 compiler.libcxx=libstdc++11 compiler.version=17 -os=Linux \ No newline at end of file +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-11 b/tools/conan/profiles/scwx-linux_gcc-11 index cc41b528..d0d52fc9 100644 --- a/tools/conan/profiles/scwx-linux_gcc-11 +++ b/tools/conan/profiles/scwx-linux_gcc-11 @@ -5,4 +5,4 @@ compiler=gcc compiler.cppstd=20 compiler.libcxx=libstdc++11 compiler.version=11 -os=Linux \ No newline at end of file +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-12 b/tools/conan/profiles/scwx-linux_gcc-12 index a7db9371..06f8e0bc 100644 --- a/tools/conan/profiles/scwx-linux_gcc-12 +++ b/tools/conan/profiles/scwx-linux_gcc-12 @@ -5,4 +5,4 @@ compiler=gcc compiler.cppstd=20 compiler.libcxx=libstdc++11 compiler.version=12 -os=Linux \ No newline at end of file +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-13 b/tools/conan/profiles/scwx-linux_gcc-13 index ef3ef064..25ad5988 100644 --- a/tools/conan/profiles/scwx-linux_gcc-13 +++ b/tools/conan/profiles/scwx-linux_gcc-13 @@ -5,4 +5,4 @@ compiler=gcc compiler.cppstd=20 compiler.libcxx=libstdc++11 compiler.version=13 -os=Linux \ No newline at end of file +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-14 b/tools/conan/profiles/scwx-linux_gcc-14 index 94220e3e..a315550a 100644 --- a/tools/conan/profiles/scwx-linux_gcc-14 +++ b/tools/conan/profiles/scwx-linux_gcc-14 @@ -5,4 +5,4 @@ compiler=gcc compiler.cppstd=20 compiler.libcxx=libstdc++11 compiler.version=14 -os=Linux \ No newline at end of file +os=Linux diff --git a/tools/conan/profiles/scwx-win64_msvc2022 b/tools/conan/profiles/scwx-win64_msvc2022 index 0c4513d8..072e4933 100644 --- a/tools/conan/profiles/scwx-win64_msvc2022 +++ b/tools/conan/profiles/scwx-win64_msvc2022 @@ -5,4 +5,4 @@ compiler=msvc compiler.cppstd=20 compiler.runtime=dynamic compiler.version=194 -os=Windows \ No newline at end of file +os=Windows From a287789d87f6699a24a23ac355cf42c973ec44ec Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Nov 2024 22:23:44 -0600 Subject: [PATCH 234/762] Use a split workflow for clang-tidy-review comments --- .github/workflows/clang-tidy-comments.yml | 20 ++++++++++++++++++++ .github/workflows/clang-tidy-review.yml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/clang-tidy-comments.yml diff --git a/.github/workflows/clang-tidy-comments.yml b/.github/workflows/clang-tidy-comments.yml new file mode 100644 index 00000000..accdbeca --- /dev/null +++ b/.github/workflows/clang-tidy-comments.yml @@ -0,0 +1,20 @@ +name: Post clang-tidy Review Comments + +on: + workflow_run: + workflows: ["clang-tidy-review"] + types: + - completed + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Post Comments + uses: ZedThree/clang-tidy-review/post@v0.20.1 + with: + lgtm_comment_body: '' + annotations: false + max_comments: 25 + num_comments_as_exitcode: false diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index e2a1d57d..25ec245e 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -126,7 +126,7 @@ jobs: --cmake-command='' \ --max-comments=25 \ --lgtm-comment-body='' \ - --split_workflow=false \ + --split_workflow=true \ --annotations=false \ --parallel=0 rsync -avzh --ignore-missing-args clang-tidy-review-output.json ../ From 13a015cd5f56caff6d4605933b8bd983c2a50a98 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 00:37:31 -0600 Subject: [PATCH 235/762] Refactoring Level2ProductViewImpl to Level2ProductView::Impl --- .../scwx/qt/view/level2_product_view.cpp | 26 +++++++++---------- .../scwx/qt/view/level2_product_view.hpp | 5 ++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index a5a26157..f1730098 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -53,11 +53,10 @@ static const std::unordered_map {common::Level2Product::CorrelationCoefficient, "%"}, {common::Level2Product::ClutterFilterPowerRemoved, "dB"}}; -class Level2ProductViewImpl +class Level2ProductView::Impl { public: - explicit Level2ProductViewImpl(Level2ProductView* self, - common::Level2Product product) : + explicit Impl(Level2ProductView* self, common::Level2Product product) : self_ {self}, product_ {product}, selectedElevation_ {0.0f}, @@ -94,7 +93,7 @@ public: UpdateOtherUnits(unitSettings.other_units().GetValue()); UpdateSpeedUnits(unitSettings.speed_units().GetValue()); } - ~Level2ProductViewImpl() + ~Impl() { auto& unitSettings = settings::UnitSettings::Instance(); @@ -164,7 +163,7 @@ Level2ProductView::Level2ProductView( common::Level2Product product, std::shared_ptr radarProductManager) : RadarProductView(radarProductManager), - p(std::make_unique(this, product)) + p(std::make_unique(this, product)) { ConnectRadarProductManager(); } @@ -379,12 +378,12 @@ void Level2ProductView::SelectProduct(const std::string& productName) p->SetProduct(productName); } -void Level2ProductViewImpl::SetProduct(const std::string& productName) +void Level2ProductView::Impl::SetProduct(const std::string& productName) { SetProduct(common::GetLevel2Product(productName)); } -void Level2ProductViewImpl::SetProduct(common::Level2Product product) +void Level2ProductView::Impl::SetProduct(common::Level2Product product) { product_ = product; @@ -401,12 +400,12 @@ void Level2ProductViewImpl::SetProduct(common::Level2Product product) } } -void Level2ProductViewImpl::UpdateOtherUnits(const std::string& name) +void Level2ProductView::Impl::UpdateOtherUnits(const std::string& name) { otherUnits_ = types::GetOtherUnitsFromName(name); } -void Level2ProductViewImpl::UpdateSpeedUnits(const std::string& name) +void Level2ProductView::Impl::UpdateSpeedUnits(const std::string& name) { speedUnits_ = types::GetSpeedUnitsFromName(name); } @@ -536,8 +535,7 @@ void Level2ProductView::ComputeSweep() // When there is missing data, insert another empty vertex radial at the end // to avoid stretching - const bool isRadarDataIncomplete = - Level2ProductViewImpl::IsRadarDataIncomplete(radarData); + const bool isRadarDataIncomplete = Impl::IsRadarDataIncomplete(radarData); if (isRadarDataIncomplete) { ++vertexRadials; @@ -819,7 +817,7 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepComputed(); } -void Level2ProductViewImpl::ComputeCoordinates( +void Level2ProductView::Impl::ComputeCoordinates( const std::shared_ptr& radarData) { logger_->debug("ComputeCoordinates()"); @@ -940,7 +938,7 @@ void Level2ProductViewImpl::ComputeCoordinates( logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } -bool Level2ProductViewImpl::IsRadarDataIncomplete( +bool Level2ProductView::Impl::IsRadarDataIncomplete( const std::shared_ptr& radarData) { // Assume the data is incomplete when the delta between the first and last @@ -1003,7 +1001,7 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const static_cast(radarData->crbegin()->first + 1); // Add an extra radial when incomplete data exists - if (Level2ProductViewImpl::IsRadarDataIncomplete(radarData)) + if (Impl::IsRadarDataIncomplete(radarData)) { ++numRadials; } diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp index 9e25a254..db8fc45c 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp @@ -15,8 +15,6 @@ namespace qt namespace view { -class Level2ProductViewImpl; - class Level2ProductView : public RadarProductView { Q_OBJECT @@ -73,7 +71,8 @@ protected slots: void ComputeSweep() override; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace view From 84f0003bef46496adb8444131b833b971f0cdaeb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 00:39:12 -0600 Subject: [PATCH 236/762] Compute smoothed coordinates --- .../scwx/qt/view/level2_product_view.cpp | 82 +++++++++++++++++-- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index f1730098..1b1fcf92 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -106,7 +106,8 @@ public: }; void ComputeCoordinates( - const std::shared_ptr& radarData); + const std::shared_ptr& radarData, + bool smoothingEnabled); void SetProduct(const std::string& productName); void SetProduct(common::Level2Product product); @@ -528,6 +529,9 @@ void Level2ProductView::ComputeSweep() return; } + // TODO: Where does this come from? + bool smoothingEnabled = false; + logger_->debug("Computing Sweep"); std::size_t radials = radarData->crbegin()->first + 1; @@ -546,7 +550,7 @@ void Level2ProductView::ComputeSweep() vertexRadials = std::min(vertexRadials, common::MAX_0_5_DEGREE_RADIALS); - p->ComputeCoordinates(radarData); + p->ComputeCoordinates(radarData, smoothingEnabled); const std::vector& coordinates = p->coordinates_; @@ -818,7 +822,8 @@ void Level2ProductView::ComputeSweep() } void Level2ProductView::Impl::ComputeCoordinates( - const std::shared_ptr& radarData) + const std::shared_ptr& radarData, + bool smoothingEnabled) { logger_->debug("ComputeCoordinates()"); @@ -858,6 +863,14 @@ void Level2ProductView::Impl::ComputeCoordinates( auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); + const float gateRangeOffset = (smoothingEnabled) ? + // Center of the first gate is half the gate + // size distance from the radar site + 0.5f : + // Far end of the first gate is the gate + // size distance from the radar site + 1.0f; + std::for_each( std::execution::par_unseq, radials.begin(), @@ -867,7 +880,7 @@ void Level2ProductView::Impl::ComputeCoordinates( units::degrees angle {}; auto radialData = radarData->find(radial); - if (radialData != radarData->cend()) + if (radialData != radarData->cend() && !smoothingEnabled) { angle = radialData->second->azimuth_angle(); } @@ -878,8 +891,42 @@ void Level2ProductView::Impl::ComputeCoordinates( auto prevRadial2 = radarData->find( (radial >= 2) ? radial - 2 : numRadials - (2 - radial)); - if (prevRadial1 != radarData->cend() && - prevRadial2 != radarData->cend()) + if (radialData != radarData->cend() && + prevRadial1 != radarData->cend() && smoothingEnabled) + { + const units::degrees currentAngle = + radialData->second->azimuth_angle(); + const units::degrees prevAngle = + prevRadial1->second->azimuth_angle(); + + // No wrapping required since angle is only used for geodesic + // calculation + const units::degrees deltaAngle = + currentAngle - prevAngle; + + // Delta scale is half the delta angle to reach the center of the + // bin, because smoothing is enabled + constexpr float deltaScale = 0.5f; + + angle = currentAngle + deltaAngle * deltaScale; + } + else if (radialData != radarData->cend() && smoothingEnabled) + { + const units::degrees currentAngle = + radialData->second->azimuth_angle(); + + // Assume a half degree delta if there aren't enough angles + // to determine a delta angle + constexpr units::degrees deltaAngle {0.5f}; + + // Delta scale is half the delta angle to reach the center of the + // bin, because smoothing is enabled + constexpr float deltaScale = 0.5f; + + angle = currentAngle + deltaAngle * deltaScale; + } + else if (prevRadial1 != radarData->cend() && + prevRadial2 != radarData->cend()) { const units::degrees prevAngle1 = prevRadial1->second->azimuth_angle(); @@ -890,7 +937,15 @@ void Level2ProductView::Impl::ComputeCoordinates( // calculation const units::degrees deltaAngle = prevAngle1 - prevAngle2; - angle = prevAngle1 + deltaAngle; + const float deltaScale = + (smoothingEnabled) ? + // Delta scale is 1.5x the delta angle to reach the center + // of the next bin, because smoothing is enabled + 1.5f : + // Delta scale is 1.0x the delta angle + 1.0f; + + angle = prevAngle1 + deltaAngle * deltaScale; } else if (prevRadial1 != radarData->cend()) { @@ -901,7 +956,15 @@ void Level2ProductView::Impl::ComputeCoordinates( // to determine a delta angle constexpr units::degrees deltaAngle {0.5f}; - angle = prevAngle1 + deltaAngle; + const float deltaScale = + (smoothingEnabled) ? + // Delta scale is 1.5x the delta angle to reach the center + // of the next bin, because smoothing is enabled + 1.5f : + // Delta scale is 1.0x the delta angle + 1.0f; + + angle = prevAngle1 + deltaAngle * deltaScale; } else { @@ -917,7 +980,8 @@ void Level2ProductView::Impl::ComputeCoordinates( { const std::uint32_t radialGate = radial * common::MAX_DATA_MOMENT_GATES + gate; - const float range = (gate + 1) * gateSize; + const float range = + (gate + gateRangeOffset) * gateSize; const std::size_t offset = radialGate * 2; double latitude; From 1c8d7f5be93ab8a1dc1562e5c31245822d8cd1d1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 00:41:01 -0600 Subject: [PATCH 237/762] Update vertices for smoothing --- .../scwx/qt/view/level2_product_view.cpp | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 1b1fcf92..e6177adc 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -540,7 +540,7 @@ void Level2ProductView::ComputeSweep() // When there is missing data, insert another empty vertex radial at the end // to avoid stretching const bool isRadarDataIncomplete = Impl::IsRadarDataIncomplete(radarData); - if (isRadarDataIncomplete) + if (isRadarDataIncomplete && !smoothingEnabled) { ++vertexRadials; } @@ -654,9 +654,13 @@ void Level2ProductView::ComputeSweep() const std::int32_t gateSize = std::max(1, dataMomentInterval / gateSizeMeters); + // TODO: Does startGate need to increase by 1 when smoothing? What about + // gate 1840? Does last gate need extended? + // Compute gate range [startGate, endGate) + const std::int32_t gateOffset = (smoothingEnabled) ? 1 : 0; const std::int32_t startGate = - (dataMomentRange - dataMomentIntervalH) / gateSizeMeters; + (dataMomentRange - dataMomentIntervalH) / gateSizeMeters + gateOffset; const std::int32_t numberOfDataMomentGates = std::min(momentData->number_of_data_moment_gates(), static_cast(gates)); @@ -732,6 +736,14 @@ void Level2ProductView::ComputeSweep() // Store vertices if (gate > 0) { + // Draw two triangles per gate + // + // 2 +---+ 4 + // | /| + // | / | + // |/ | + // 1 +---+ 3 + const std::uint16_t baseCoord = gate - 1; std::size_t offset1 = ((startRadial + radial) % vertexRadials * @@ -752,8 +764,11 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; @@ -761,9 +776,6 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; - vertexCount = 6; } else From 1ac85e253fa5244da014dbc57f57a268297ff606 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 00:42:05 -0600 Subject: [PATCH 238/762] Begin logic for smoothed data moment values --- .../scwx/qt/view/level2_product_view.cpp | 89 +++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index e6177adc..5b0aa314 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -703,33 +703,88 @@ void Level2ProductView::ComputeSweep() // Store data moment value if (dataMomentsArray8 != nullptr) { - std::uint8_t dataValue = dataMomentsArray8[i]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; - } - - for (std::size_t m = 0; m < vertexCount; m++) - { - dataMoments8[mIndex++] = dataMomentsArray8[i]; - - if (cfpMomentsArray != nullptr) + std::uint8_t dataValue = dataMomentsArray8[i]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) { - cfpMoments[mIndex - 1] = cfpMomentsArray[i]; + continue; } + + for (std::size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataMomentsArray8[i]; + + if (cfpMomentsArray != nullptr) + { + cfpMoments[mIndex - 1] = cfpMomentsArray[i]; + } + } + } + else if (gate > 0) + { + // TODO: What is the correct value for dm3? + const std::size_t dm1 = i; + const std::size_t dm2 = dm1 + 1; + const std::size_t dm3 = i; + const std::size_t dm4 = dm3 + 1; + + // TODO: Validate indices are all in range + + if (dataMomentsArray8[dm1] < snrThreshold && + dataMomentsArray8[dm1] != RANGE_FOLDED && + dataMomentsArray8[dm2] < snrThreshold && + dataMomentsArray8[dm2] != RANGE_FOLDED && + dataMomentsArray8[dm3] < snrThreshold && + dataMomentsArray8[dm3] != RANGE_FOLDED && + dataMomentsArray8[dm4] < snrThreshold && + dataMomentsArray8[dm4] != RANGE_FOLDED) + { + // Skip only if all data moments are hidden + continue; + } + + // The order must match the store vertices section below + dataMoments8[mIndex++] = dataMomentsArray8[dm1]; + dataMoments8[mIndex++] = dataMomentsArray8[dm2]; + dataMoments8[mIndex++] = dataMomentsArray8[dm4]; + dataMoments8[mIndex++] = dataMomentsArray8[dm1]; + dataMoments8[mIndex++] = dataMomentsArray8[dm3]; + dataMoments8[mIndex++] = dataMomentsArray8[dm4]; + + // cfpMoments is unused, so not populated here + } + else + { + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + continue; } } else { - std::uint16_t dataValue = dataMomentsArray16[i]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; - } + std::uint16_t dataValue = dataMomentsArray16[i]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } - for (std::size_t m = 0; m < vertexCount; m++) + for (std::size_t m = 0; m < vertexCount; m++) + { + dataMoments16[mIndex++] = dataMomentsArray16[i]; + } + } + else if (gate > 0) { - dataMoments16[mIndex++] = dataMomentsArray16[i]; + // TODO: Copy from dm8 + } + else + { + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + continue; } } From 608b1af24fa7fc320b7053ecfa734194cce0505d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 08:52:48 -0600 Subject: [PATCH 239/762] Update smoothing data moment calculations --- .../scwx/qt/view/level2_product_view.cpp | 127 ++++++++++++++---- 1 file changed, 101 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 5b0aa314..d6d08dce 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -629,11 +629,13 @@ void Level2ProductView::ComputeSweep() // Start radial is always 0, as coordinates are calculated for each sweep constexpr std::uint16_t startRadial = 0u; - for (auto& radialPair : *radarData) + for (auto it = radarData->cbegin(); it != radarData->cend(); ++it) { + const auto& radialPair = *it; std::uint16_t radial = radialPair.first; - auto& radialData = radialPair.second; - auto momentData = radialData->moment_data_block(p->dataBlockType_); + const auto& radialData = radialPair.second; + std::shared_ptr + momentData = radialData->moment_data_block(p->dataBlockType_); if (momentData0->data_word_size() != momentData->data_word_size()) { @@ -654,13 +656,9 @@ void Level2ProductView::ComputeSweep() const std::int32_t gateSize = std::max(1, dataMomentInterval / gateSizeMeters); - // TODO: Does startGate need to increase by 1 when smoothing? What about - // gate 1840? Does last gate need extended? - // Compute gate range [startGate, endGate) - const std::int32_t gateOffset = (smoothingEnabled) ? 1 : 0; - const std::int32_t startGate = - (dataMomentRange - dataMomentIntervalH) / gateSizeMeters + gateOffset; + std::int32_t startGate = + (dataMomentRange - dataMomentIntervalH) / gateSizeMeters; const std::int32_t numberOfDataMomentGates = std::min(momentData->number_of_data_moment_gates(), static_cast(gates)); @@ -668,9 +666,19 @@ void Level2ProductView::ComputeSweep() startGate + numberOfDataMomentGates * gateSize, static_cast(common::MAX_DATA_MOMENT_GATES)); - const std::uint8_t* dataMomentsArray8 = nullptr; - const std::uint16_t* dataMomentsArray16 = nullptr; - const std::uint8_t* cfpMomentsArray = nullptr; + if (smoothingEnabled) + { + // If smoothing is enabled, the start gate is incremented by one, as we + // are skipping the radar site origin. The end gate is unaffected, as + // we need to draw one less data point. + ++startGate; + } + + const std::uint8_t* dataMomentsArray8 = nullptr; + const std::uint16_t* dataMomentsArray16 = nullptr; + const std::uint8_t* nextDataMomentsArray8 = nullptr; + const std::uint16_t* nextDataMomentsArray16 = nullptr; + const std::uint8_t* cfpMomentsArray = nullptr; if (momentData->data_word_size() == 8) { @@ -690,6 +698,38 @@ void Level2ProductView::ComputeSweep() ->data_moments()); } + std::shared_ptr + nextMomentData = nullptr; + std::int32_t numberOfNextDataMomentGates = 0; + if (smoothingEnabled) + { + const auto& nextRadialPair = *(++it); + const auto& nextRadialData = nextRadialPair.second; + nextMomentData = nextRadialData->moment_data_block(p->dataBlockType_); + + if (momentData->data_word_size() != nextMomentData->data_word_size()) + { + // Data should be consistent between radials + logger_->warn("Invalid data moment size"); + continue; + } + + if (nextMomentData->data_word_size() == 8) + { + nextDataMomentsArray8 = reinterpret_cast( + nextMomentData->data_moments()); + } + else + { + nextDataMomentsArray16 = reinterpret_cast( + nextMomentData->data_moments()); + } + + numberOfNextDataMomentGates = std::min( + nextMomentData->number_of_data_moment_gates(), + static_cast(gates)); + } + for (std::int32_t gate = startGate, i = 0; gate + gateSize <= endGate; gate += gateSize, ++i) { @@ -723,22 +763,26 @@ void Level2ProductView::ComputeSweep() } else if (gate > 0) { - // TODO: What is the correct value for dm3? - const std::size_t dm1 = i; - const std::size_t dm2 = dm1 + 1; - const std::size_t dm3 = i; - const std::size_t dm4 = dm3 + 1; + const std::size_t dm1 = i; + const std::size_t dm2 = dm1 + 1; + const std::size_t& dm3 = dm1; + const std::size_t& dm4 = dm2; - // TODO: Validate indices are all in range + // Validate indices are all in range + if (dm2 >= numberOfDataMomentGates || + dm4 >= numberOfNextDataMomentGates) + { + continue; + } if (dataMomentsArray8[dm1] < snrThreshold && dataMomentsArray8[dm1] != RANGE_FOLDED && dataMomentsArray8[dm2] < snrThreshold && dataMomentsArray8[dm2] != RANGE_FOLDED && - dataMomentsArray8[dm3] < snrThreshold && - dataMomentsArray8[dm3] != RANGE_FOLDED && - dataMomentsArray8[dm4] < snrThreshold && - dataMomentsArray8[dm4] != RANGE_FOLDED) + nextDataMomentsArray8[dm3] < snrThreshold && + nextDataMomentsArray8[dm3] != RANGE_FOLDED && + nextDataMomentsArray8[dm4] < snrThreshold && + nextDataMomentsArray8[dm4] != RANGE_FOLDED) { // Skip only if all data moments are hidden continue; @@ -747,10 +791,10 @@ void Level2ProductView::ComputeSweep() // The order must match the store vertices section below dataMoments8[mIndex++] = dataMomentsArray8[dm1]; dataMoments8[mIndex++] = dataMomentsArray8[dm2]; - dataMoments8[mIndex++] = dataMomentsArray8[dm4]; + dataMoments8[mIndex++] = nextDataMomentsArray8[dm4]; dataMoments8[mIndex++] = dataMomentsArray8[dm1]; - dataMoments8[mIndex++] = dataMomentsArray8[dm3]; - dataMoments8[mIndex++] = dataMomentsArray8[dm4]; + dataMoments8[mIndex++] = nextDataMomentsArray8[dm3]; + dataMoments8[mIndex++] = nextDataMomentsArray8[dm4]; // cfpMoments is unused, so not populated here } @@ -778,7 +822,38 @@ void Level2ProductView::ComputeSweep() } else if (gate > 0) { - // TODO: Copy from dm8 + const std::size_t dm1 = i; + const std::size_t dm2 = dm1 + 1; + const std::size_t& dm3 = dm1; + const std::size_t& dm4 = dm2; + + // Validate indices are all in range + if (dm2 >= numberOfDataMomentGates || + dm4 >= numberOfNextDataMomentGates) + { + continue; + } + + if (dataMomentsArray16[dm1] < snrThreshold && + dataMomentsArray16[dm1] != RANGE_FOLDED && + dataMomentsArray16[dm2] < snrThreshold && + dataMomentsArray16[dm2] != RANGE_FOLDED && + nextDataMomentsArray16[dm3] < snrThreshold && + nextDataMomentsArray16[dm3] != RANGE_FOLDED && + nextDataMomentsArray16[dm4] < snrThreshold && + nextDataMomentsArray16[dm4] != RANGE_FOLDED) + { + // Skip only if all data moments are hidden + continue; + } + + // The order must match the store vertices section below + dataMoments16[mIndex++] = dataMomentsArray16[dm1]; + dataMoments16[mIndex++] = dataMomentsArray16[dm2]; + dataMoments16[mIndex++] = nextDataMomentsArray16[dm4]; + dataMoments16[mIndex++] = dataMomentsArray16[dm1]; + dataMoments16[mIndex++] = nextDataMomentsArray16[dm3]; + dataMoments16[mIndex++] = nextDataMomentsArray16[dm4]; } else { From 5be8d7b7c05d36d6fce507ba73498b8efc765a76 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 09:38:34 -0600 Subject: [PATCH 240/762] Fix iterator advancement --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index d6d08dce..ca285582 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -703,7 +703,14 @@ void Level2ProductView::ComputeSweep() std::int32_t numberOfNextDataMomentGates = 0; if (smoothingEnabled) { - const auto& nextRadialPair = *(++it); + // Smoothing requires the next radial pair as well + auto nextIt = std::next(it); + if (nextIt == radarData->cend()) + { + continue; + } + + const auto& nextRadialPair = *(nextIt); const auto& nextRadialData = nextRadialPair.second; nextMomentData = nextRadialData->moment_data_block(p->dataBlockType_); From bd4c8776a56f7d11ce37d14a6783948942628b4e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 1 Dec 2024 11:17:31 -0500 Subject: [PATCH 241/762] Added cursor_icon_always_on to settings test data --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index a642d730..eaf8f185 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit a642d730bd8d6c9b291b90e61b3a3a389139f2f6 +Subproject commit eaf8f185ce2b3a3248da1a4d6c8e2e9265638f15 From e93697b1625eb0212a3115bb01c7c9d2d516bd83 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 1 Dec 2024 23:12:03 -0600 Subject: [PATCH 242/762] Get rid of white/max reflectivity bins due to unsigned integer underflow --- scwx-qt/gl/radar.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/gl/radar.frag b/scwx-qt/gl/radar.frag index 0c605b8a..55c18773 100644 --- a/scwx-qt/gl/radar.frag +++ b/scwx-qt/gl/radar.frag @@ -16,7 +16,7 @@ layout (location = 0) out vec4 fragColor; void main() { - float texCoord = float(dataMoment - uDataMomentOffset) / uDataMomentScale; + float texCoord = (float(dataMoment) - float(uDataMomentOffset)) / uDataMomentScale; if (uCFPEnabled && cfpMoment > 8u) { From 80d5be956d9262bd46c7e7a7f59eb8fe211dc472 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 00:06:08 -0600 Subject: [PATCH 243/762] Clean up smoothed data moment population --- .../scwx/qt/view/level2_product_view.cpp | 78 +++++++++---------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index ca285582..97e0ba91 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -770,38 +770,34 @@ void Level2ProductView::ComputeSweep() } else if (gate > 0) { - const std::size_t dm1 = i; - const std::size_t dm2 = dm1 + 1; - const std::size_t& dm3 = dm1; - const std::size_t& dm4 = dm2; - // Validate indices are all in range - if (dm2 >= numberOfDataMomentGates || - dm4 >= numberOfNextDataMomentGates) + if (i + 1 >= numberOfDataMomentGates || + i + 1 >= numberOfNextDataMomentGates) { continue; } - if (dataMomentsArray8[dm1] < snrThreshold && - dataMomentsArray8[dm1] != RANGE_FOLDED && - dataMomentsArray8[dm2] < snrThreshold && - dataMomentsArray8[dm2] != RANGE_FOLDED && - nextDataMomentsArray8[dm3] < snrThreshold && - nextDataMomentsArray8[dm3] != RANGE_FOLDED && - nextDataMomentsArray8[dm4] < snrThreshold && - nextDataMomentsArray8[dm4] != RANGE_FOLDED) + const std::uint8_t& dm1 = dataMomentsArray8[i]; + const std::uint8_t& dm2 = dataMomentsArray8[i + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[i]; + const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; + + if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED) { // Skip only if all data moments are hidden continue; } // The order must match the store vertices section below - dataMoments8[mIndex++] = dataMomentsArray8[dm1]; - dataMoments8[mIndex++] = dataMomentsArray8[dm2]; - dataMoments8[mIndex++] = nextDataMomentsArray8[dm4]; - dataMoments8[mIndex++] = dataMomentsArray8[dm1]; - dataMoments8[mIndex++] = nextDataMomentsArray8[dm3]; - dataMoments8[mIndex++] = nextDataMomentsArray8[dm4]; + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm2; + dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm3; + dataMoments8[mIndex++] = dm4; // cfpMoments is unused, so not populated here } @@ -829,38 +825,36 @@ void Level2ProductView::ComputeSweep() } else if (gate > 0) { - const std::size_t dm1 = i; - const std::size_t dm2 = dm1 + 1; - const std::size_t& dm3 = dm1; - const std::size_t& dm4 = dm2; - // Validate indices are all in range - if (dm2 >= numberOfDataMomentGates || - dm4 >= numberOfNextDataMomentGates) + if (i + 1 >= numberOfDataMomentGates || + i + 1 >= numberOfNextDataMomentGates) { continue; } - if (dataMomentsArray16[dm1] < snrThreshold && - dataMomentsArray16[dm1] != RANGE_FOLDED && - dataMomentsArray16[dm2] < snrThreshold && - dataMomentsArray16[dm2] != RANGE_FOLDED && - nextDataMomentsArray16[dm3] < snrThreshold && - nextDataMomentsArray16[dm3] != RANGE_FOLDED && - nextDataMomentsArray16[dm4] < snrThreshold && - nextDataMomentsArray16[dm4] != RANGE_FOLDED) + const std::uint8_t& dm1 = dataMomentsArray16[i]; + const std::uint8_t& dm2 = dataMomentsArray16[i + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray16[i]; + const std::uint8_t& dm4 = nextDataMomentsArray16[i + 1]; + + if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED) { // Skip only if all data moments are hidden continue; } // The order must match the store vertices section below - dataMoments16[mIndex++] = dataMomentsArray16[dm1]; - dataMoments16[mIndex++] = dataMomentsArray16[dm2]; - dataMoments16[mIndex++] = nextDataMomentsArray16[dm4]; - dataMoments16[mIndex++] = dataMomentsArray16[dm1]; - dataMoments16[mIndex++] = nextDataMomentsArray16[dm3]; - dataMoments16[mIndex++] = nextDataMomentsArray16[dm4]; + dataMoments16[mIndex++] = dm1; + dataMoments16[mIndex++] = dm2; + dataMoments16[mIndex++] = dm4; + dataMoments16[mIndex++] = dm1; + dataMoments16[mIndex++] = dm3; + dataMoments16[mIndex++] = dm4; + + // cfpMoments is unused, so not populated here } else { From 1b07c6f5b54a14c0a402ec46a250bf4cbd838f89 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 00:10:22 -0600 Subject: [PATCH 244/762] Data moments should not have GLSL flat qualifiers to be sampled more than once per triangle --- scwx-qt/gl/radar.frag | 6 +++--- scwx-qt/gl/radar.vert | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scwx-qt/gl/radar.frag b/scwx-qt/gl/radar.frag index 55c18773..b6491e8b 100644 --- a/scwx-qt/gl/radar.frag +++ b/scwx-qt/gl/radar.frag @@ -9,14 +9,14 @@ uniform float uDataMomentScale; uniform bool uCFPEnabled; -flat in uint dataMoment; -flat in uint cfpMoment; +in float dataMoment; +in float cfpMoment; layout (location = 0) out vec4 fragColor; void main() { - float texCoord = (float(dataMoment) - float(uDataMomentOffset)) / uDataMomentScale; + float texCoord = (dataMoment - float(uDataMomentOffset)) / uDataMomentScale; if (uCFPEnabled && cfpMoment > 8u) { diff --git a/scwx-qt/gl/radar.vert b/scwx-qt/gl/radar.vert index b4da9f17..97754b73 100644 --- a/scwx-qt/gl/radar.vert +++ b/scwx-qt/gl/radar.vert @@ -13,8 +13,8 @@ layout (location = 2) in uint aCfpMoment; uniform mat4 uMVPMatrix; uniform vec2 uMapScreenCoord; -flat out uint dataMoment; -flat out uint cfpMoment; +out float dataMoment; +out float cfpMoment; vec2 latLngToScreenCoordinate(in vec2 latLng) { From a5919ac917a8eec74f51fd242708594deb7c8f68 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 22:34:42 -0600 Subject: [PATCH 245/762] The last smoothed radial needs the first radial to render --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 97e0ba91..4359a78c 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -707,7 +707,7 @@ void Level2ProductView::ComputeSweep() auto nextIt = std::next(it); if (nextIt == radarData->cend()) { - continue; + nextIt = radarData->cbegin(); } const auto& nextRadialPair = *(nextIt); From e0cd3610a6e2a708f889244349ab51aa3a3b465a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 22:35:13 -0600 Subject: [PATCH 246/762] An extra empty radial is still needed for incomplete data when smoothing --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 4359a78c..0254904b 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -540,7 +540,7 @@ void Level2ProductView::ComputeSweep() // When there is missing data, insert another empty vertex radial at the end // to avoid stretching const bool isRadarDataIncomplete = Impl::IsRadarDataIncomplete(radarData); - if (isRadarDataIncomplete && !smoothingEnabled) + if (isRadarDataIncomplete) { ++vertexRadials; } From d3ae404f7a37feed07f140c23130eea5738c1d94 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 22:38:12 -0600 Subject: [PATCH 247/762] Delta angles must be normalized before they can be scaled --- .../scwx/qt/view/level2_product_view.cpp | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 0254904b..c483ab28 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -116,6 +116,7 @@ public: static bool IsRadarDataIncomplete( const std::shared_ptr& radarData); + static units::degrees NormalizeAngle(units::degrees angle); Level2ProductView* self_; @@ -1042,10 +1043,9 @@ void Level2ProductView::Impl::ComputeCoordinates( const units::degrees prevAngle = prevRadial1->second->azimuth_angle(); - // No wrapping required since angle is only used for geodesic - // calculation + // Calculate delta angle const units::degrees deltaAngle = - currentAngle - prevAngle; + NormalizeAngle(currentAngle - prevAngle); // Delta scale is half the delta angle to reach the center of the // bin, because smoothing is enabled @@ -1076,9 +1076,9 @@ void Level2ProductView::Impl::ComputeCoordinates( const units::degrees prevAngle2 = prevRadial2->second->azimuth_angle(); - // No wrapping required since angle is only used for geodesic - // calculation - const units::degrees deltaAngle = prevAngle1 - prevAngle2; + // Calculate delta angle + const units::degrees deltaAngle = + NormalizeAngle(prevAngle1 - prevAngle2); const float deltaScale = (smoothingEnabled) ? @@ -1162,6 +1162,25 @@ bool Level2ProductView::Impl::IsRadarDataIncomplete( return angleDelta > kIncompleteDataAngleThreshold_; } +units::degrees +Level2ProductView::Impl::NormalizeAngle(units::degrees angle) +{ + constexpr auto angleLimit = units::degrees {180.0f}; + constexpr auto fullAngle = units::degrees {360.0f}; + + // Normalize angle to [-180, 180) + while (angle < -angleLimit) + { + angle += fullAngle; + } + while (angle >= angleLimit) + { + angle -= fullAngle; + } + + return angle; +} + std::optional Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const { From a8132ef9f152b982915e19a664090053f772991c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 2 Dec 2024 23:08:32 -0600 Subject: [PATCH 248/762] Allow selection of radar smoothing --- scwx-qt/source/scwx/qt/main/main_window.cpp | 16 ++++++++++++++ scwx-qt/source/scwx/qt/main/main_window.ui | 11 ++++++++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 21 ++++++++++++++++++- scwx-qt/source/scwx/qt/map/map_widget.hpp | 2 ++ .../scwx/qt/view/level2_product_view.cpp | 9 +++++--- .../scwx/qt/view/radar_product_view.cpp | 11 ++++++++++ .../scwx/qt/view/radar_product_view.hpp | 2 ++ 7 files changed, 66 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 37b8b268..e21484c4 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -322,6 +322,8 @@ MainWindow::MainWindow(QWidget* parent) : p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleLabel); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleComboBox); + p->mapSettingsGroup_->GetContentsLayout()->addWidget( + ui->smoothRadarDataCheckBox); p->mapSettingsGroup_->GetContentsLayout()->addWidget( ui->trackLocationCheckBox); ui->radarToolboxScrollAreaContents->layout()->replaceWidget( @@ -1085,6 +1087,16 @@ void MainWindowImpl::ConnectOtherSignals() } } }); + connect(mainWindow_->ui->smoothRadarDataCheckBox, + &QCheckBox::checkStateChanged, + mainWindow_, + [this](Qt::CheckState state) + { + bool smoothingEnabled = (state == Qt::CheckState::Checked); + + // Turn on smoothing + activeMap_->SetSmoothingEnabled(smoothingEnabled); + }); connect(mainWindow_->ui->trackLocationCheckBox, &QCheckBox::checkStateChanged, mainWindow_, @@ -1471,6 +1483,10 @@ void MainWindowImpl::UpdateRadarProductSettings() { level2SettingsGroup_->setVisible(false); } + + mainWindow_->ui->smoothRadarDataCheckBox->setCheckState( + activeMap_->GetSmoothingEnabled() ? Qt::CheckState::Checked : + Qt::CheckState::Unchecked); } void MainWindowImpl::UpdateRadarSite() diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index c5e877c9..42525199 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -153,8 +153,8 @@ 0 0 - 205 - 701 + 190 + 680 @@ -329,6 +329,13 @@ + + + + Smooth Radar Data + + + diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d3c7fefc..37300a96 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -225,7 +225,7 @@ public: std::shared_ptr overlayLayer_; std::shared_ptr overlayProductLayer_ {nullptr}; std::shared_ptr placefileLayer_; - std::shared_ptr markerLayer_; + std::shared_ptr markerLayer_; std::shared_ptr colorTableLayer_; std::shared_ptr radarSiteLayer_ {nullptr}; @@ -233,6 +233,7 @@ public: bool autoRefreshEnabled_; bool autoUpdateEnabled_; + bool smoothingEnabled_ {false}; common::Level2Product selectedLevel2Product_; @@ -727,6 +728,23 @@ std::uint16_t MapWidget::GetVcp() const } } +bool MapWidget::GetSmoothingEnabled() const +{ + return p->smoothingEnabled_; +} + +void MapWidget::SetSmoothingEnabled(bool smoothingEnabled) +{ + p->smoothingEnabled_ = smoothingEnabled; + + auto radarProductView = p->context_->radar_product_view(); + if (radarProductView != nullptr) + { + radarProductView->set_smoothing_enabled(smoothingEnabled); + radarProductView->Update(); + } +} + void MapWidget::SelectElevation(float elevation) { auto radarProductView = p->context_->radar_product_view(); @@ -775,6 +793,7 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, radarProductView = view::RadarProductViewFactory::Create( group, productName, productCode, p->radarProductManager_); + radarProductView->set_smoothing_enabled(p->smoothingEnabled_); p->context_->set_radar_product_view(radarProductView); p->RadarProductViewConnect(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 40f7df77..4254453e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -48,6 +48,7 @@ public: std::string GetRadarProductName() const; std::shared_ptr GetRadarSite() const; std::chrono::system_clock::time_point GetSelectedTime() const; + bool GetSmoothingEnabled() const; std::uint16_t GetVcp() const; void SelectElevation(float elevation); @@ -117,6 +118,7 @@ public: double pitch); void SetInitialMapStyle(const std::string& styleName); void SetMapStyle(const std::string& styleName); + void SetSmoothingEnabled(bool enabled); /** * Updates the coordinates associated with mouse movement from another map. diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index c483ab28..bcee898d 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -131,6 +131,8 @@ public: std::shared_ptr momentDataBlock0_; + bool prevSmoothingEnabled_ {false}; + std::vector coordinates_ {}; std::vector vertices_ {}; std::vector dataMoments8_ {}; @@ -512,6 +514,7 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; @@ -524,14 +527,14 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } - if (radarData == p->elevationScan_) + if (radarData == p->elevationScan_ && + smoothingEnabled == p->prevSmoothingEnabled_) { Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } - // TODO: Where does this come from? - bool smoothingEnabled = false; + p->prevSmoothingEnabled_ = smoothingEnabled; logger_->debug("Computing Sweep"); diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp index e2ca6c21..04534593 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -41,6 +41,7 @@ public: std::mutex sweepMutex_; std::chrono::system_clock::time_point selectedTime_; + bool smoothingEnabled_ {false}; std::shared_ptr radarProductManager_; }; @@ -87,6 +88,11 @@ std::chrono::system_clock::time_point RadarProductView::selected_time() const return p->selectedTime_; } +bool RadarProductView::smoothing_enabled() const +{ + return p->smoothingEnabled_; +} + std::chrono::system_clock::time_point RadarProductView::sweep_time() const { return {}; @@ -105,6 +111,11 @@ void RadarProductView::set_radar_product_manager( ConnectRadarProductManager(); } +void RadarProductView::set_smoothing_enabled(bool smoothingEnabled) +{ + p->smoothingEnabled_ = smoothingEnabled; +} + void RadarProductView::Initialize() { ComputeSweep(); diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index c695a9e5..9bda8795 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -49,10 +49,12 @@ public: std::shared_ptr radar_product_manager() const; std::chrono::system_clock::time_point selected_time() const; + bool smoothing_enabled() const; std::mutex& sweep_mutex(); void set_radar_product_manager( std::shared_ptr radarProductManager); + void set_smoothing_enabled(bool smoothingEnabled); void Initialize(); virtual void From 9f7026bf9cc72f29e3431bc833b2baf37757ffb1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 3 Dec 2024 23:28:18 -0600 Subject: [PATCH 249/762] Add pre-caluclated smoothed coordinates --- .../scwx/qt/manager/radar_product_manager.cpp | 161 ++++++++++++------ .../scwx/qt/manager/radar_product_manager.hpp | 3 +- 2 files changed, 114 insertions(+), 50 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 4659f079..a4e1e3dd 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(pop) @@ -206,6 +207,13 @@ public: void UpdateAvailableProductsSync(); + void + CalculateCoordinates(const boost::integer_range& radialGates, + const units::angle::degrees radialAngle, + const units::angle::degrees angleOffset, + const float gateRangeOffset, + std::vector& outputCoordinates); + static void PopulateProductTimes(std::shared_ptr providerManager, RadarProductRecordMap& productRecordMap, @@ -226,10 +234,12 @@ public: std::size_t cacheLimit_ {6u}; std::vector coordinates0_5Degree_ {}; + std::vector coordinates0_5DegreeSmooth_ {}; std::vector coordinates1Degree_ {}; + std::vector coordinates1DegreeSmooth_ {}; - RadarProductRecordMap level2ProductRecords_ {}; - RadarProductRecordList level2ProductRecentRecords_ {}; + RadarProductRecordMap level2ProductRecords_ {}; + RadarProductRecordList level2ProductRecentRecords_ {}; std::unordered_map level3ProductRecordsMap_ {}; std::unordered_map @@ -361,14 +371,29 @@ void RadarProductManager::DumpRecords() } const std::vector& -RadarProductManager::coordinates(common::RadialSize radialSize) const +RadarProductManager::coordinates(common::RadialSize radialSize, + bool smoothingEnabled) const { switch (radialSize) { case common::RadialSize::_0_5Degree: - return p->coordinates0_5Degree_; + if (smoothingEnabled) + { + return p->coordinates0_5DegreeSmooth_; + } + else + { + return p->coordinates0_5Degree_; + } case common::RadialSize::_1Degree: - return p->coordinates1Degree_; + if (smoothingEnabled) + { + return p->coordinates1DegreeSmooth_; + } + else + { + return p->coordinates1Degree_; + } default: throw std::invalid_argument("Invalid radial size"); } @@ -430,64 +455,107 @@ void RadarProductManager::Initialize() boost::timer::cpu_timer timer; - const GeographicLib::Geodesic& geodesic( - util::GeographicLib::DefaultGeodesic()); - - const QMapLibre::Coordinate radar(p->radarSite_->latitude(), - p->radarSite_->longitude()); - - const float gateSize = gate_size(); - // Calculate half degree azimuth coordinates timer.start(); std::vector& coordinates0_5Degree = p->coordinates0_5Degree_; coordinates0_5Degree.resize(NUM_COORIDNATES_0_5_DEGREE); - auto radialGates0_5Degree = + const auto radialGates0_5Degree = boost::irange(0, NUM_RADIAL_GATES_0_5_DEGREE); - std::for_each( - std::execution::par_unseq, - radialGates0_5Degree.begin(), - radialGates0_5Degree.end(), - [&](uint32_t radialGate) - { - const uint16_t gate = - static_cast(radialGate % common::MAX_DATA_MOMENT_GATES); - const uint16_t radial = - static_cast(radialGate / common::MAX_DATA_MOMENT_GATES); + p->CalculateCoordinates( + radialGates0_5Degree, + units::angle::degrees {0.5f}, // Radial angle + units::angle::degrees {0.0f}, // Angle offset + // Far end of the first gate is the gate size distance from the radar site + 1.0f, + coordinates0_5Degree); - const float angle = radial * 0.5f; // 0.5 degree radial - const float range = (gate + 1) * gateSize; - const size_t offset = radialGate * 2; - - double latitude; - double longitude; - - geodesic.Direct( - radar.first, radar.second, angle, range, latitude, longitude); - - coordinates0_5Degree[offset] = latitude; - coordinates0_5Degree[offset + 1] = longitude; - }); timer.stop(); logger_->debug("Coordinates (0.5 degree) calculated in {}", timer.format(6, "%ws")); + // Calculate half degree smooth azimuth coordinates + timer.start(); + std::vector& coordinates0_5DegreeSmooth = + p->coordinates0_5DegreeSmooth_; + + coordinates0_5DegreeSmooth.resize(NUM_COORIDNATES_0_5_DEGREE); + + p->CalculateCoordinates(radialGates0_5Degree, + units::angle::degrees {0.5f}, // Radial angle + units::angle::degrees {0.25f}, // Angle offset + // Center of the first gate is half the gate size + // distance from the radar site + 0.5f, + coordinates0_5DegreeSmooth); + + timer.stop(); + logger_->debug("Coordinates (0.5 degree smooth) calculated in {}", + timer.format(6, "%ws")); + // Calculate 1 degree azimuth coordinates timer.start(); std::vector& coordinates1Degree = p->coordinates1Degree_; coordinates1Degree.resize(NUM_COORIDNATES_1_DEGREE); - auto radialGates1Degree = + const auto radialGates1Degree = boost::irange(0, NUM_RADIAL_GATES_1_DEGREE); + p->CalculateCoordinates( + radialGates1Degree, + units::angle::degrees {1.0f}, // Radial angle + units::angle::degrees {0.0f}, // Angle offset + // Far end of the first gate is the gate size distance from the radar site + 1.0f, + coordinates1Degree); + + timer.stop(); + logger_->debug("Coordinates (1 degree) calculated in {}", + timer.format(6, "%ws")); + + // Calculate 1 degree smooth azimuth coordinates + timer.start(); + std::vector& coordinates1DegreeSmooth = p->coordinates1DegreeSmooth_; + + coordinates1DegreeSmooth.resize(NUM_COORIDNATES_1_DEGREE); + + p->CalculateCoordinates(radialGates1Degree, + units::angle::degrees {1.0f}, // Radial angle + units::angle::degrees {0.5f}, // Angle offset + // Center of the first gate is twice the gate size + // distance from the radar site + 2.0f, + coordinates1DegreeSmooth); + + timer.stop(); + logger_->debug("Coordinates (1 degree smooth) calculated in {}", + timer.format(6, "%ws")); + + p->initialized_ = true; +} + +void RadarProductManagerImpl::CalculateCoordinates( + const boost::integer_range& radialGates, + const units::angle::degrees radialAngle, + const units::angle::degrees angleOffset, + const float gateRangeOffset, + std::vector& outputCoordinates) +{ + const GeographicLib::Geodesic& geodesic( + util::GeographicLib::DefaultGeodesic()); + + const QMapLibre::Coordinate radar(radarSite_->latitude(), + radarSite_->longitude()); + + const float gateSize = self_->gate_size(); + std::for_each( std::execution::par_unseq, - radialGates1Degree.begin(), - radialGates1Degree.end(), + radialGates.begin(), + radialGates.end(), [&](uint32_t radialGate) { const uint16_t gate = @@ -495,8 +563,8 @@ void RadarProductManager::Initialize() const uint16_t radial = static_cast(radialGate / common::MAX_DATA_MOMENT_GATES); - const float angle = radial * 1.0f; // 1 degree radial - const float range = (gate + 1) * gateSize; + const float angle = radial * radialAngle.value() + angleOffset.value(); + const float range = (gate + gateRangeOffset) * gateSize; const size_t offset = radialGate * 2; double latitude; @@ -505,14 +573,9 @@ void RadarProductManager::Initialize() geodesic.Direct( radar.first, radar.second, angle, range, latitude, longitude); - coordinates1Degree[offset] = latitude; - coordinates1Degree[offset + 1] = longitude; + outputCoordinates[offset] = latitude; + outputCoordinates[offset + 1] = longitude; }); - timer.stop(); - logger_->debug("Coordinates (1 degree) calculated in {}", - timer.format(6, "%ws")); - - p->initialized_ = true; } std::shared_ptr diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 17dfa551..9232f7d9 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -41,7 +41,8 @@ public: */ static void DumpRecords(); - const std::vector& coordinates(common::RadialSize radialSize) const; + const std::vector& coordinates(common::RadialSize radialSize, + bool smoothingEnabled) const; const scwx::util::time_zone* default_time_zone() const; float gate_size() const; std::string radar_id() const; From 57f6b41a47abcd899ff271960860ad674d7ff71e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 3 Dec 2024 23:29:58 -0600 Subject: [PATCH 250/762] Level 3 radial product smoothing --- .../scwx/qt/view/level2_product_view.cpp | 2 + .../scwx/qt/view/level3_radial_view.cpp | 156 +++++++++++++----- 2 files changed, 119 insertions(+), 39 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index bcee898d..e445a585 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -809,6 +809,8 @@ void Level2ProductView::ComputeSweep() { // If smoothing is enabled, gate should never start at zero // (radar site origin) + logger_->error( + "Smoothing enabled, gate should not start at zero"); continue; } } diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 5fa3531f..9c0f2ad2 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -44,7 +44,8 @@ public: ~Impl() { threadPool_.join(); }; void ComputeCoordinates( - const std::shared_ptr& radialData); + const std::shared_ptr& radialData, + bool smoothingEnabled); Level3RadialView* self_; @@ -56,6 +57,8 @@ public: std::shared_ptr lastRadialData_ {}; + bool prevSmoothingEnabled_ {false}; + float latitude_; float longitude_; float range_; @@ -125,6 +128,7 @@ void Level3RadialView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -155,7 +159,8 @@ void Level3RadialView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } - else if (gpm == graphic_product_message()) + else if (gpm == graphic_product_message() && + smoothingEnabled == p->prevSmoothingEnabled_) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -163,6 +168,8 @@ void Level3RadialView::ComputeSweep() } set_graphic_product_message(gpm); + p->prevSmoothingEnabled_ = smoothingEnabled; + // A message with radial data should have a Product Description Block and // Product Symbology Block std::shared_ptr descriptionBlock = @@ -267,11 +274,11 @@ void Level3RadialView::ComputeSweep() const std::vector& coordinates = (radialSize == common::RadialSize::NonStandard) ? p->coordinates_ : - radarProductManager->coordinates(radialSize); + radarProductManager->coordinates(radialSize, smoothingEnabled); // There should be a positive number of range bins in radial data - const uint16_t gates = radialData->number_of_range_bins(); - if (gates < 1) + const uint16_t numberOfDataMomentGates = radialData->number_of_range_bins(); + if (numberOfDataMomentGates < 1) { logger_->warn("No range bins in radial data"); Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); @@ -293,13 +300,14 @@ void Level3RadialView::ComputeSweep() std::vector& vertices = p->vertices_; size_t vIndex = 0; vertices.clear(); - vertices.resize(radials * gates * VERTICES_PER_BIN * VALUES_PER_VERTEX); + vertices.resize(radials * numberOfDataMomentGates * VERTICES_PER_BIN * + VALUES_PER_VERTEX); // Setup data moment vector std::vector& dataMoments8 = p->dataMoments8_; size_t mIndex = 0; - dataMoments8.resize(radials * gates * VERTICES_PER_BIN); + dataMoments8.resize(radials * numberOfDataMomentGates * VERTICES_PER_BIN); // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); @@ -308,7 +316,7 @@ void Level3RadialView::ComputeSweep() std::uint16_t startRadial; if (radialSize == common::RadialSize::NonStandard) { - p->ComputeCoordinates(radialData); + p->ComputeCoordinates(radialData, smoothingEnabled); startRadial = 0; } else @@ -318,40 +326,95 @@ void Level3RadialView::ComputeSweep() startRadial = std::lroundf(startAngle * radialMultiplier); } - for (uint16_t radial = 0; radial < radialData->number_of_radials(); radial++) + // Compute gate interval + const std::uint16_t dataMomentInterval = + descriptionBlock->x_resolution_raw(); + + // Compute gate size (number of base gates per bin) + const std::uint16_t gateSize = std::max( + 1, + dataMomentInterval / + static_cast(radarProductManager->gate_size())); + + // Compute gate range [startGate, endGate) + std::uint16_t startGate = 0; + const std::uint16_t endGate = + std::min(startGate + numberOfDataMomentGates * gateSize, + common::MAX_DATA_MOMENT_GATES); + + if (smoothingEnabled) { - const auto dataMomentsArray8 = radialData->level(radial); + // If smoothing is enabled, the start gate is incremented by one, as we + // are skipping the radar site origin. The end gate is unaffected, as + // we need to draw one less data point. + ++startGate; + } - // Compute gate interval - const uint16_t dataMomentInterval = descriptionBlock->x_resolution_raw(); + for (std::uint16_t radial = 0; radial < radialData->number_of_radials(); + ++radial) + { + const auto& dataMomentsArray8 = radialData->level(radial); - // Compute gate size (number of base gates per bin) - const uint16_t gateSize = std::max( - 1, - dataMomentInterval / - static_cast(radarProductManager->gate_size())); + const std::uint16_t nextRadial = + (radial == radialData->number_of_radials() - 1) ? 0 : radial + 1; + const auto& nextDataMomentsArray8 = radialData->level(nextRadial); - // Compute gate range [startGate, endGate) - const uint16_t startGate = 0; - const uint16_t endGate = std::min( - startGate + gates * gateSize, common::MAX_DATA_MOMENT_GATES); - - for (uint16_t gate = startGate, i = 0; gate + gateSize <= endGate; + for (std::uint16_t gate = startGate, i = 0; gate + gateSize <= endGate; gate += gateSize, ++i) { size_t vertexCount = (gate > 0) ? 6 : 3; - // Store data moment value - uint8_t dataValue = - (i < dataMomentsArray8.size()) ? dataMomentsArray8[i] : 0; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; - } + // Store data moment value + uint8_t dataValue = + (i < dataMomentsArray8.size()) ? dataMomentsArray8[i] : 0; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } - for (size_t m = 0; m < vertexCount; m++) + for (size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataValue; + } + } + else if (gate > 0) { - dataMoments8[mIndex++] = dataValue; + // Validate indices are all in range + if (i + 1 >= numberOfDataMomentGates) + { + continue; + } + + const std::uint8_t& dm1 = dataMomentsArray8[i]; + const std::uint8_t& dm2 = dataMomentsArray8[i + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[i]; + const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; + + if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED) + { + // Skip only if all data moments are hidden + continue; + } + + // The order must match the store vertices section below + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm2; + dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm3; + dataMoments8[mIndex++] = dm4; + } + else + { + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + logger_->error("Smoothing enabled, gate should not start at zero"); + continue; } // Store vertices @@ -376,8 +439,11 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; @@ -385,9 +451,6 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; - vertexCount = 6; } else @@ -431,7 +494,8 @@ void Level3RadialView::ComputeSweep() } void Level3RadialView::Impl::ComputeCoordinates( - const std::shared_ptr& radialData) + const std::shared_ptr& radialData, + bool smoothingEnabled) { logger_->debug("ComputeCoordinates()"); @@ -455,12 +519,25 @@ void Level3RadialView::Impl::ComputeCoordinates( auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); + const float gateRangeOffset = (smoothingEnabled) ? + // Center of the first gate is half the gate + // size distance from the radar site + 0.5f : + // Far end of the first gate is the gate + // size distance from the radar site + 1.0f; + std::for_each(std::execution::par_unseq, radials.begin(), radials.end(), [&](std::uint32_t radial) { - const float angle = radialData->start_angle(radial); + float angle = radialData->start_angle(radial); + + if (smoothingEnabled) + { + angle += radialData->delta_angle(radial) * 0.5f; + } std::for_each(std::execution::par_unseq, gates.begin(), @@ -470,7 +547,8 @@ void Level3RadialView::Impl::ComputeCoordinates( const std::uint32_t radialGate = radial * common::MAX_DATA_MOMENT_GATES + gate; - const float range = (gate + 1) * gateSize; + const float range = + (gate + gateRangeOffset) * gateSize; const std::size_t offset = radialGate * 2; double latitude; From 44f91a3a86b2008a51ddda0d38dcfd7f1f9460e8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Dec 2024 06:00:39 -0600 Subject: [PATCH 251/762] Level 3 raster smoothing --- .../scwx/qt/view/level2_product_view.cpp | 6 +- .../scwx/qt/view/level3_radial_view.cpp | 7 +- .../scwx/qt/view/level3_raster_view.cpp | 100 +++++++++++++----- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index e445a585..a76d50c8 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -131,7 +131,7 @@ public: std::shared_ptr momentDataBlock0_; - bool prevSmoothingEnabled_ {false}; + bool lastSmoothingEnabled_ {false}; std::vector coordinates_ {}; std::vector vertices_ {}; @@ -528,13 +528,13 @@ void Level2ProductView::ComputeSweep() return; } if (radarData == p->elevationScan_ && - smoothingEnabled == p->prevSmoothingEnabled_) + smoothingEnabled == p->lastSmoothingEnabled_) { Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } - p->prevSmoothingEnabled_ = smoothingEnabled; + p->lastSmoothingEnabled_ = smoothingEnabled; logger_->debug("Computing Sweep"); diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 9c0f2ad2..efb732d3 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -56,8 +56,7 @@ public: std::vector dataMoments8_ {}; std::shared_ptr lastRadialData_ {}; - - bool prevSmoothingEnabled_ {false}; + bool lastSmoothingEnabled_ {false}; float latitude_; float longitude_; @@ -160,7 +159,7 @@ void Level3RadialView::ComputeSweep() return; } else if (gpm == graphic_product_message() && - smoothingEnabled == p->prevSmoothingEnabled_) + smoothingEnabled == p->lastSmoothingEnabled_) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -168,7 +167,7 @@ void Level3RadialView::ComputeSweep() } set_graphic_product_message(gpm); - p->prevSmoothingEnabled_ = smoothingEnabled; + p->lastSmoothingEnabled_ = smoothingEnabled; // A message with radial data should have a Product Description Block and // Product Symbology Block diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index b51c2cd0..d6b4bca7 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -39,6 +39,7 @@ public: std::vector dataMoments8_; std::shared_ptr lastRasterData_ {}; + bool lastSmoothingEnabled_ {false}; float latitude_; float longitude_; @@ -109,6 +110,7 @@ void Level3RasterView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -139,7 +141,8 @@ void Level3RasterView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } - else if (gpm == graphic_product_message()) + else if (gpm == graphic_product_message() && + smoothingEnabled == p->lastSmoothingEnabled_) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -147,6 +150,8 @@ void Level3RasterView::ComputeSweep() } set_graphic_product_message(gpm); + p->lastSmoothingEnabled_ = smoothingEnabled; + // A message with radial data should have a Product Description Block and // Product Symbology Block std::shared_ptr descriptionBlock = @@ -231,16 +236,18 @@ void Level3RasterView::ComputeSweep() const GeographicLib::Geodesic& geodesic = util::GeographicLib::DefaultGeodesic(); - const uint16_t xResolution = descriptionBlock->x_resolution_raw(); - const uint16_t yResolution = descriptionBlock->y_resolution_raw(); - double iCoordinate = + const std::uint16_t xResolution = descriptionBlock->x_resolution_raw(); + const std::uint16_t yResolution = descriptionBlock->y_resolution_raw(); + const double iCoordinate = (-rasterData->i_coordinate_start() - 1.0 - p->range_) * 1000.0; - double jCoordinate = + const double jCoordinate = (rasterData->j_coordinate_start() + 1.0 + p->range_) * 1000.0; + const double xOffset = (smoothingEnabled) ? xResolution * 0.5 : 0.0; + const double yOffset = (smoothingEnabled) ? yResolution * 0.5 : 0.0; - size_t numCoordinates = + const std::size_t numCoordinates = static_cast(rows + 1) * static_cast(maxColumns + 1); - auto coordinateRange = + const auto coordinateRange = boost::irange(0, static_cast(numCoordinates)); std::vector coordinates; @@ -260,8 +267,8 @@ void Level3RasterView::ComputeSweep() const uint32_t col = index % (rows + 1); const uint32_t row = index / (rows + 1); - const double i = iCoordinate + xResolution * col; - const double j = jCoordinate - yResolution * row; + const double i = iCoordinate + xResolution * col + xOffset; + const double j = jCoordinate - yResolution * row - yOffset; // Calculate polar coordinates based on i and j const double angle = std::atan2(i, j) * 180.0 / M_PI; @@ -299,25 +306,68 @@ void Level3RasterView::ComputeSweep() // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); - for (size_t row = 0; row < rasterData->number_of_rows(); ++row) + const std::size_t rowCount = (smoothingEnabled) ? + rasterData->number_of_rows() - 1 : + rasterData->number_of_rows(); + + for (size_t row = 0; row < rowCount; ++row) { - const auto dataMomentsArray8 = + const std::size_t nextRow = + (row == rasterData->number_of_rows() - 1) ? 0 : row + 1; + + const auto& dataMomentsArray8 = rasterData->level(static_cast(row)); + const auto& nextDataMomentsArray8 = + rasterData->level(static_cast(nextRow)); for (size_t bin = 0; bin < dataMomentsArray8.size(); ++bin) { - constexpr size_t vertexCount = 6; - - // Store data moment value - uint8_t dataValue = dataMomentsArray8[bin]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; + constexpr size_t vertexCount = 6; + + // Store data moment value + uint8_t dataValue = dataMomentsArray8[bin]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } + + for (size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataValue; + } } - - for (size_t m = 0; m < vertexCount; m++) + else { - dataMoments8[mIndex++] = dataValue; + // Validate indices are all in range + if (bin + 1 >= dataMomentsArray8.size() || + bin + 1 >= nextDataMomentsArray8.size()) + { + continue; + } + + const std::uint8_t& dm1 = dataMomentsArray8[bin]; + const std::uint8_t& dm2 = dataMomentsArray8[bin + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[bin]; + const std::uint8_t& dm4 = nextDataMomentsArray8[bin + 1]; + + if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED) + { + // Skip only if all data moments are hidden + continue; + } + + // The order must match the store vertices section below + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm2; + dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = dm1; + dataMoments8[mIndex++] = dm3; + dataMoments8[mIndex++] = dm4; } // Store vertices @@ -332,17 +382,17 @@ void Level3RasterView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; } } vertices.resize(vIndex); From 40149ae32ce401069b5ebec1625954b410a9e95a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Dec 2024 07:34:38 -0600 Subject: [PATCH 252/762] Fix integer comparison signedness --- scwx-qt/source/scwx/qt/view/level3_raster_view.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index d6b4bca7..75902b2a 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -310,10 +310,12 @@ void Level3RasterView::ComputeSweep() rasterData->number_of_rows() - 1 : rasterData->number_of_rows(); - for (size_t row = 0; row < rowCount; ++row) + for (std::size_t row = 0; row < rowCount; ++row) { const std::size_t nextRow = - (row == rasterData->number_of_rows() - 1) ? 0 : row + 1; + (row == static_cast(rasterData->number_of_rows() - 1)) ? + 0 : + row + 1; const auto& dataMomentsArray8 = rasterData->level(static_cast(row)); From ac48ac24ab97d636ada7dc80e9b5650280186bda Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 7 Dec 2024 23:33:57 -0600 Subject: [PATCH 253/762] Fix level 2 16-bit data moment references --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index a76d50c8..75981b21 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -838,10 +838,10 @@ void Level2ProductView::ComputeSweep() continue; } - const std::uint8_t& dm1 = dataMomentsArray16[i]; - const std::uint8_t& dm2 = dataMomentsArray16[i + 1]; - const std::uint8_t& dm3 = nextDataMomentsArray16[i]; - const std::uint8_t& dm4 = nextDataMomentsArray16[i + 1]; + const std::uint16_t& dm1 = dataMomentsArray16[i]; + const std::uint16_t& dm2 = dataMomentsArray16[i + 1]; + const std::uint16_t& dm3 = nextDataMomentsArray16[i]; + const std::uint16_t& dm4 = nextDataMomentsArray16[i + 1]; if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && dm2 < snrThreshold && dm2 != RANGE_FOLDED && From c492f1146640fb87c699a2e6c4c13b3003347093 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 7 Dec 2024 23:46:06 -0600 Subject: [PATCH 254/762] Perform level 2 data moment remapping when smoothing --- .../scwx/qt/view/level2_product_view.cpp | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 75981b21..0a644654 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -114,6 +114,10 @@ public: void UpdateOtherUnits(const std::string& name); void UpdateSpeedUnits(const std::string& name); + void ComputeEdgeValue(); + template + inline T RemapDataMoment(T dataMoment) const; + static bool IsRadarDataIncomplete( const std::shared_ptr& radarData); static units::degrees NormalizeAngle(units::degrees angle); @@ -138,6 +142,7 @@ public: std::vector dataMoments8_ {}; std::vector dataMoments16_ {}; std::vector cfpMoments_ {}; + std::uint16_t edgeValue_ {}; float latitude_; float longitude_; @@ -633,6 +638,13 @@ void Level2ProductView::ComputeSweep() // Start radial is always 0, as coordinates are calculated for each sweep constexpr std::uint16_t startRadial = 0u; + // For most products other than reflectivity, the edge should not go to the + // bottom of the color table + if (smoothingEnabled) + { + p->ComputeEdgeValue(); + } + for (auto it = radarData->cbegin(); it != radarData->cend(); ++it) { const auto& radialPair = *it; @@ -796,12 +808,12 @@ void Level2ProductView::ComputeSweep() } // The order must match the store vertices section below - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm2; - dataMoments8[mIndex++] = dm4; - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm3; - dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); // cfpMoments is unused, so not populated here } @@ -853,12 +865,12 @@ void Level2ProductView::ComputeSweep() } // The order must match the store vertices section below - dataMoments16[mIndex++] = dm1; - dataMoments16[mIndex++] = dm2; - dataMoments16[mIndex++] = dm4; - dataMoments16[mIndex++] = dm1; - dataMoments16[mIndex++] = dm3; - dataMoments16[mIndex++] = dm4; + dataMoments16[mIndex++] = p->RemapDataMoment(dm1); + dataMoments16[mIndex++] = p->RemapDataMoment(dm2); + dataMoments16[mIndex++] = p->RemapDataMoment(dm4); + dataMoments16[mIndex++] = p->RemapDataMoment(dm1); + dataMoments16[mIndex++] = p->RemapDataMoment(dm3); + dataMoments16[mIndex++] = p->RemapDataMoment(dm4); // cfpMoments is unused, so not populated here } @@ -970,6 +982,46 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepComputed(); } +void Level2ProductView::Impl::ComputeEdgeValue() +{ + const float offset = momentDataBlock0_->offset(); + + switch (dataBlockType_) + { + case wsr88d::rda::DataBlockType::MomentVel: + case wsr88d::rda::DataBlockType::MomentZdr: + edgeValue_ = offset; + break; + + case wsr88d::rda::DataBlockType::MomentSw: + case wsr88d::rda::DataBlockType::MomentPhi: + edgeValue_ = 2; + break; + + case wsr88d::rda::DataBlockType::MomentRho: + edgeValue_ = 255; + break; + + case wsr88d::rda::DataBlockType::MomentRef: + default: + edgeValue_ = 0; + break; + } +} + +template +T Level2ProductView::Impl::RemapDataMoment(T dataMoment) const +{ + if (dataMoment != 0) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + void Level2ProductView::Impl::ComputeCoordinates( const std::shared_ptr& radarData, bool smoothingEnabled) From b7970bb63134d34f564fa981c9b5d50e53137d46 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 8 Dec 2024 00:58:45 -0600 Subject: [PATCH 255/762] Perform level 3 data moment remapping when smoothing --- .../scwx/qt/view/level3_product_view.cpp | 44 +++++++++++++++++++ .../scwx/qt/view/level3_product_view.hpp | 2 + .../scwx/qt/view/level3_radial_view.cpp | 32 +++++++++++--- .../scwx/qt/view/level3_raster_view.cpp | 39 ++++++++++++---- 4 files changed, 103 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index d0bb0d47..642e0981 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -485,6 +485,50 @@ void Level3ProductView::UpdateColorTableLut() Q_EMIT ColorTableLutUpdated(); } +std::uint8_t Level3ProductView::ComputeEdgeValue() const +{ + std::uint8_t edgeValue = 0; + + std::shared_ptr descriptionBlock = + p->graphicMessage_->description_block(); + + const float offset = descriptionBlock->offset(); + const float scale = descriptionBlock->scale(); + + switch (p->category_) + { + case common::Level3ProductCategory::Velocity: + edgeValue = (scale > 0.0f) ? (-offset / scale) : -offset; + break; + + case common::Level3ProductCategory::DifferentialReflectivity: + edgeValue = -offset; + break; + + case common::Level3ProductCategory::SpectrumWidth: + case common::Level3ProductCategory::SpecificDifferentialPhase: + edgeValue = 2; + break; + + case common::Level3ProductCategory::CorrelationCoefficient: + edgeValue = static_cast( + std::max(255, descriptionBlock->number_of_levels())); + break; + + case common::Level3ProductCategory::Reflectivity: + case common::Level3ProductCategory::StormRelativeVelocity: + case common::Level3ProductCategory::VerticallyIntegratedLiquid: + case common::Level3ProductCategory::EchoTops: + case common::Level3ProductCategory::HydrometeorClassification: + case common::Level3ProductCategory::PrecipitationAccumulation: + default: + edgeValue = 0; + break; + } + + return edgeValue; +} + std::optional Level3ProductView::GetDataLevelCode(std::uint16_t level) const { diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp index e836c6e0..e5de1873 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp @@ -58,6 +58,8 @@ protected: void DisconnectRadarProductManager() override; void UpdateColorTableLut() override; + std::uint8_t ComputeEdgeValue() const; + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index efb732d3..8d561935 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -47,6 +47,8 @@ public: const std::shared_ptr& radialData, bool smoothingEnabled); + inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; + Level3RadialView* self_; boost::asio::thread_pool threadPool_ {1u}; @@ -54,6 +56,7 @@ public: std::vector coordinates_ {}; std::vector vertices_ {}; std::vector dataMoments8_ {}; + std::uint8_t edgeValue_ {}; std::shared_ptr lastRadialData_ {}; bool lastSmoothingEnabled_ {false}; @@ -347,6 +350,10 @@ void Level3RadialView::ComputeSweep() // are skipping the radar site origin. The end gate is unaffected, as // we need to draw one less data point. ++startGate; + + // For most products other than reflectivity, the edge should not go to + // the bottom of the color table + p->edgeValue_ = ComputeEdgeValue(); } for (std::uint16_t radial = 0; radial < radialData->number_of_radials(); @@ -401,12 +408,12 @@ void Level3RadialView::ComputeSweep() } // The order must match the store vertices section below - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm2; - dataMoments8[mIndex++] = dm4; - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm3; - dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); } else { @@ -492,6 +499,19 @@ void Level3RadialView::ComputeSweep() Q_EMIT SweepComputed(); } +std::uint8_t +Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const +{ + if (dataMoment != 0) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + void Level3RadialView::Impl::ComputeCoordinates( const std::shared_ptr& radialData, bool smoothingEnabled) diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index 75902b2a..433b51be 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -33,10 +33,13 @@ public: } ~Level3RasterViewImpl() { threadPool_.join(); }; + inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; + boost::asio::thread_pool threadPool_ {1u}; - std::vector vertices_; - std::vector dataMoments8_; + std::vector vertices_ {}; + std::vector dataMoments8_ {}; + std::uint8_t edgeValue_ {}; std::shared_ptr lastRasterData_ {}; bool lastSmoothingEnabled_ {false}; @@ -310,6 +313,13 @@ void Level3RasterView::ComputeSweep() rasterData->number_of_rows() - 1 : rasterData->number_of_rows(); + if (smoothingEnabled) + { + // For most products other than reflectivity, the edge should not go to + // the bottom of the color table + p->edgeValue_ = ComputeEdgeValue(); + } + for (std::size_t row = 0; row < rowCount; ++row) { const std::size_t nextRow = @@ -364,12 +374,12 @@ void Level3RasterView::ComputeSweep() } // The order must match the store vertices section below - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm2; - dataMoments8[mIndex++] = dm4; - dataMoments8[mIndex++] = dm1; - dataMoments8[mIndex++] = dm3; - dataMoments8[mIndex++] = dm4; + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); } // Store vertices @@ -411,6 +421,19 @@ void Level3RasterView::ComputeSweep() Q_EMIT SweepComputed(); } +std::uint8_t +Level3RasterViewImpl::RemapDataMoment(std::uint8_t dataMoment) const +{ + if (dataMoment != 0) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + std::optional Level3RasterView::GetBinLevel(const common::Coordinate& coordinate) const { From 3562edcb7a549877bf810d019f34f72c16498203 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 18:11:59 -0500 Subject: [PATCH 256/762] Change negative threshold values to act as inverted threshold --- scwx-qt/gl/threshold.geom | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scwx-qt/gl/threshold.geom b/scwx-qt/gl/threshold.geom index 677a80cd..deead87d 100644 --- a/scwx-qt/gl/threshold.geom +++ b/scwx-qt/gl/threshold.geom @@ -21,7 +21,9 @@ smooth out vec4 color; void main() { if (gsIn[0].displayed != 0 && - (gsIn[0].threshold <= 0 || // If Threshold: 0 was specified, no threshold + (gsIn[0].threshold == 0 || // If Threshold: 0 was specified, no threshold + uMapDistance == 0 || // If uMapDistance is zero, threshold is disabled + (gsIn[0].threshold < 0 && -(gsIn[0].threshold) <= uMapDistance) || // If Threshold is negative and below current map distance gsIn[0].threshold >= uMapDistance || // If Threshold is above current map distance gsIn[0].threshold >= 999) && // If Threshold: 999 was specified (or greater), no threshold (gsIn[0].timeRange[0] == 0 || // If there is no start time specified From 7c884fec5548f004d5b3eef749e2d082616b29a8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 18:44:35 -0500 Subject: [PATCH 257/762] add inverted threshold to placefile text --- scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index b8faa8f8..df4f650e 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -136,7 +136,10 @@ void PlacefileText::Impl::RenderTextDrawItem( std::chrono::system_clock::now() : selectedTime_; - if ((!thresholded_ || mapDistance_ <= di->threshold_) && + const bool thresholdMet = mapDistance_ <= di->threshold_ || + ((double)di->threshold_ < 0.0 && mapDistance_ >= -(di->threshold_)); + + if ((!thresholded_ || thresholdMet) && (di->startTime_ == std::chrono::system_clock::time_point {} || (di->startTime_ <= selectedTime && selectedTime < di->endTime_))) { From 1c4551522db61b34d867aaecc153301d92890c57 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 18:49:23 -0500 Subject: [PATCH 258/762] fix formatting issues in inverted threshold code --- scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index df4f650e..087f90d7 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -136,8 +136,9 @@ void PlacefileText::Impl::RenderTextDrawItem( std::chrono::system_clock::now() : selectedTime_; - const bool thresholdMet = mapDistance_ <= di->threshold_ || - ((double)di->threshold_ < 0.0 && mapDistance_ >= -(di->threshold_)); + const bool thresholdMet = + mapDistance_ <= di->threshold_ || + ((double) di->threshold_ < 0.0 && mapDistance_ >= -(di->threshold_)); if ((!thresholded_ || thresholdMet) && (di->startTime_ == std::chrono::system_clock::time_point {} || From 10aabce3a1fe16104241e83c480492bd0de3b148 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 10:20:54 -0500 Subject: [PATCH 259/762] Fix some minor style issues with inverted threshold code --- scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 087f90d7..640d1c52 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -137,10 +137,10 @@ void PlacefileText::Impl::RenderTextDrawItem( selectedTime_; const bool thresholdMet = - mapDistance_ <= di->threshold_ || - ((double) di->threshold_ < 0.0 && mapDistance_ >= -(di->threshold_)); + !thresholded_ || mapDistance_ <= di->threshold_ || + (di->threshold_.value() < 0.0 && mapDistance_ >= -(di->threshold_)); - if ((!thresholded_ || thresholdMet) && + if (thresholdMet && (di->startTime_ == std::chrono::system_clock::time_point {} || (di->startTime_ <= selectedTime && selectedTime < di->endTime_))) { From 83a1989cee17df144ba2666bf7c84279bcc49538 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 11 Dec 2024 06:30:33 -0600 Subject: [PATCH 260/762] Update imgui to v1.91.5 --- external/imgui | 2 +- external/imgui-backend-qt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/imgui b/external/imgui index 6ccc561a..f401021d 160000 --- a/external/imgui +++ b/external/imgui @@ -1 +1 @@ -Subproject commit 6ccc561a2ab497ad4ae6ee1dbd3b992ffada35cb +Subproject commit f401021d5a5d56fe2304056c391e78f81c8d4b8f diff --git a/external/imgui-backend-qt b/external/imgui-backend-qt index 0fe974eb..023345ca 160000 --- a/external/imgui-backend-qt +++ b/external/imgui-backend-qt @@ -1 +1 @@ -Subproject commit 0fe974ebd037844c9f23d6325dbcc128e9973749 +Subproject commit 023345ca8abf731fc50568c0197ceebe76bb4324 From e3cee80eba171a898fb5548c2ddbf3ead3887656 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 11 Dec 2024 21:59:39 -0600 Subject: [PATCH 261/762] Add imconfig.h instead of cmake defines for ImGui, and disable obsolete functions --- external/imgui.cmake | 5 +- .../include/scwx/external/imgui/imconfig.h | 138 ++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 external/include/scwx/external/imgui/imconfig.h diff --git a/external/imgui.cmake b/external/imgui.cmake index c16049fb..3eba6ee0 100644 --- a/external/imgui.cmake +++ b/external/imgui.cmake @@ -12,7 +12,7 @@ find_package(Qt${QT_VERSION_MAJOR} find_package(Freetype) -set(IMGUI_SOURCES imgui/imconfig.h +set(IMGUI_SOURCES include/scwx/external/imgui/imconfig.h imgui/imgui.cpp imgui/imgui.h imgui/imgui_demo.cpp @@ -33,8 +33,9 @@ set(IMGUI_SOURCES imgui/imconfig.h add_library(imgui STATIC ${IMGUI_SOURCES}) target_include_directories(imgui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/imgui) +target_include_directories(imgui PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) -target_compile_definitions(imgui PRIVATE IMGUI_ENABLE_FREETYPE) +target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG=) target_link_libraries(imgui PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Freetype::Freetype) diff --git a/external/include/scwx/external/imgui/imconfig.h b/external/include/scwx/external/imgui/imconfig.h new file mode 100644 index 00000000..024ec327 --- /dev/null +++ b/external/include/scwx/external/imgui/imconfig.h @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// DEAR IMGUI COMPILE-TIME OPTIONS +// Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. +// You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. +//----------------------------------------------------------------------------- +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. +// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using. +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. +//#define IM_ASSERT(_EXPR) MyAssert(_EXPR) +//#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts + +//---- Define attributes of all API symbols declarations, e.g. for DLL under Windows +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export +//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import +//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden + +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names. +#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//---- Disable all of Dear ImGui or don't implement standard windows/tools. +// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty. + +//---- Don't implement some functions to reduce linkage requirements. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. +//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Enable Test Engine / Automation features. +//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details. + +//---- Include imgui_user.h at the end of imgui.h as a convenience +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. +//#define IMGUI_INCLUDE_IMGUI_USER_H +//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" + +//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//#define IMGUI_USE_BGRA_PACKED_COLOR + +//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 + +//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. +//#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" +//#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined. +//#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined. + +//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. +//#define IMGUI_USE_STB_SPRINTF + +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +#define IMGUI_ENABLE_FREETYPE + +//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT) +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg. +// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions. +// - Both require headers to be available in the include path + program to be linked with the library code (not provided). +// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) +//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE + +//---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. +// This will be inlined as part of ImVec2 and ImVec4 class declarations. +/* +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ +//---- ...Or use Dear ImGui's own very basic math operators. +//#define IMGUI_DEFINE_MATH_OPERATORS + +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. +// Read about ImGuiBackendFlags_RendererHasVtxOffset for details. +//#define ImDrawIdx unsigned int + +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) +//struct ImDrawList; +//struct ImDrawCmd; +//typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); +//#define ImDrawCallback MyImDrawCallback + +//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase) +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) +//#define IM_DEBUG_BREAK IM_ASSERT(0) +//#define IM_DEBUG_BREAK __debugbreak() + +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID + +//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files) +/* +namespace ImGui +{ + void MyFunction(const char* name, MyMatrix44* mtx); +} +*/ From 6a5e04b35a636ed1ab36a140e3410732b8e3cae0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 11 Dec 2024 22:03:42 -0600 Subject: [PATCH 262/762] Disable formatting for imconfig.h to maintain consistency with upstream --- external/include/scwx/external/imgui/imconfig.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/external/include/scwx/external/imgui/imconfig.h b/external/include/scwx/external/imgui/imconfig.h index 024ec327..16f7692f 100644 --- a/external/include/scwx/external/imgui/imconfig.h +++ b/external/include/scwx/external/imgui/imconfig.h @@ -1,3 +1,4 @@ +// clang-format off //----------------------------------------------------------------------------- // DEAR IMGUI COMPILE-TIME OPTIONS // Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. @@ -136,3 +137,4 @@ namespace ImGui void MyFunction(const char* name, MyMatrix44* mtx); } */ +// clang-format on From 14b699d660bb2393c104673ee897592d70ebe589 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 11 Dec 2024 22:19:15 -0600 Subject: [PATCH 263/762] Update ImGui to v1.91.6 --- external/imgui | 2 +- external/include/scwx/external/imgui/imconfig.h | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/external/imgui b/external/imgui index f401021d..993fa347 160000 --- a/external/imgui +++ b/external/imgui @@ -1 +1 @@ -Subproject commit f401021d5a5d56fe2304056c391e78f81c8d4b8f +Subproject commit 993fa347495860ed44b83574254ef2a317d0c14f diff --git a/external/include/scwx/external/imgui/imconfig.h b/external/include/scwx/external/imgui/imconfig.h index 16f7692f..6064c40d 100644 --- a/external/include/scwx/external/imgui/imconfig.h +++ b/external/include/scwx/external/imgui/imconfig.h @@ -49,6 +49,7 @@ //#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) //#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. //#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert. //#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available //---- Enable Test Engine / Automation features. @@ -59,9 +60,12 @@ //#define IMGUI_INCLUDE_IMGUI_USER_H //#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" -//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support. //#define IMGUI_USE_BGRA_PACKED_COLOR +//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate. +//#define IMGUI_USE_LEGACY_CRC32_ADLER + //---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) //#define IMGUI_USE_WCHAR32 From 172203ec1620a131e77926ee19f59867960b7340 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Dec 2024 07:00:03 -0600 Subject: [PATCH 264/762] Fix 1 degree smooth coordinates location --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index a4e1e3dd..95247c06 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -525,9 +525,9 @@ void RadarProductManager::Initialize() p->CalculateCoordinates(radialGates1Degree, units::angle::degrees {1.0f}, // Radial angle units::angle::degrees {0.5f}, // Angle offset - // Center of the first gate is twice the gate size + // Center of the first gate is half the gate size // distance from the radar site - 2.0f, + 0.5f, coordinates1DegreeSmooth); timer.stop(); From 3e681abfdbb82048b822880159b3d8c3998c5798 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Dec 2024 22:36:25 -0600 Subject: [PATCH 265/762] Add smoothing settings --- .../source/scwx/qt/settings/map_settings.cpp | 25 ++++++++++++++++--- .../source/scwx/qt/settings/map_settings.hpp | 1 + .../scwx/qt/settings/product_settings.cpp | 16 ++++++++++-- .../scwx/qt/settings/product_settings.hpp | 1 + test/data | 2 +- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index f0efea78..61a64a14 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -27,12 +27,15 @@ static const std::string kMapStyleName_ {"map_style"}; static const std::string kRadarSiteName_ {"radar_site"}; static const std::string kRadarProductGroupName_ {"radar_product_group"}; static const std::string kRadarProductName_ {"radar_product"}; +static const std::string kSmoothingEnabledName_ {"smoothing_enabled"}; -static const std::string kDefaultMapStyle_ {"?"}; +static const std::string kDefaultMapStyle_ {"?"}; static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; +static constexpr bool kDefaultSmoothingEnabled_ {false}; + class MapSettings::Impl { public: @@ -43,6 +46,7 @@ public: SettingsVariable radarProductGroup_ { kRadarProductGroupName_}; SettingsVariable radarProduct_ {kRadarProductName_}; + SettingsVariable smoothingEnabled_ {kSmoothingEnabledName_}; }; explicit Impl() @@ -54,6 +58,7 @@ public: map_[i].radarProductGroup_.SetDefault( kDefaultRadarProductGroupString_); map_[i].radarProduct_.SetDefault(kDefaultRadarProduct_[i]); + map_[i].smoothingEnabled_.SetDefault(kDefaultSmoothingEnabled_); map_[i].radarSite_.SetValidator( [](const std::string& value) @@ -95,7 +100,8 @@ public: {&map_[i].mapStyle_, &map_[i].radarSite_, &map_[i].radarProductGroup_, - &map_[i].radarProduct_}); + &map_[i].radarProduct_, + &map_[i].smoothingEnabled_}); } } @@ -107,6 +113,7 @@ public: map_[i].radarSite_.SetValueToDefault(); map_[i].radarProductGroup_.SetValueToDefault(); map_[i].radarProduct_.SetValueToDefault(); + map_[i].smoothingEnabled_.SetValueToDefault(); } friend void tag_invoke(boost::json::value_from_tag, @@ -116,7 +123,8 @@ public: jv = {{kMapStyleName_, data.mapStyle_.GetValue()}, {kRadarSiteName_, data.radarSite_.GetValue()}, {kRadarProductGroupName_, data.radarProductGroup_.GetValue()}, - {kRadarProductName_, data.radarProduct_.GetValue()}}; + {kRadarProductName_, data.radarProduct_.GetValue()}, + {kSmoothingEnabledName_, data.smoothingEnabled_.GetValue()}}; } friend bool operator==(const MapData& lhs, const MapData& rhs) @@ -124,7 +132,8 @@ public: return (lhs.mapStyle_ == rhs.mapStyle_ && // lhs.radarSite_ == rhs.radarSite_ && lhs.radarProductGroup_ == rhs.radarProductGroup_ && - lhs.radarProduct_ == rhs.radarProduct_); + lhs.radarProduct_ == rhs.radarProduct_ && + lhs.smoothingEnabled_ == rhs.smoothingEnabled_); } std::array map_ {}; @@ -170,6 +179,11 @@ SettingsVariable& MapSettings::radar_product(std::size_t i) const return p->map_[i].radarProduct_; } +SettingsVariable& MapSettings::smoothing_enabled(std::size_t i) const +{ + return p->map_[i].smoothingEnabled_; +} + bool MapSettings::Shutdown() { bool dataChanged = false; @@ -180,6 +194,7 @@ bool MapSettings::Shutdown() Impl::MapData& mapRecordSettings = p->map_[i]; dataChanged |= mapRecordSettings.mapStyle_.Commit(); + dataChanged |= mapRecordSettings.smoothingEnabled_.Commit(); } return dataChanged; @@ -207,6 +222,8 @@ bool MapSettings::ReadJson(const boost::json::object& json) validated &= mapRecordSettings.radarSite_.ReadValue(mapRecord); validated &= mapRecordSettings.radarProductGroup_.ReadValue(mapRecord); + validated &= + mapRecordSettings.smoothingEnabled_.ReadValue(mapRecord); bool productValidated = mapRecordSettings.radarProduct_.ReadValue(mapRecord); diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index c8726491..188819ac 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -30,6 +30,7 @@ public: SettingsVariable& radar_site(std::size_t i) const; SettingsVariable& radar_product_group(std::size_t i) const; SettingsVariable& radar_product(std::size_t i) const; + SettingsVariable& smoothing_enabled(std::size_t i) const; bool Shutdown(); diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index 3cf47ef7..a265b6df 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -15,12 +15,15 @@ class ProductSettings::Impl public: explicit Impl() { + showSmoothedRangeFolding_.SetDefault(false); stiForecastEnabled_.SetDefault(true); stiPastEnabled_.SetDefault(true); } ~Impl() {} + SettingsVariable showSmoothedRangeFolding_ { + "show_smoothed_range_folding"}; SettingsVariable stiForecastEnabled_ {"sti_forecast_enabled"}; SettingsVariable stiPastEnabled_ {"sti_past_enabled"}; }; @@ -28,7 +31,9 @@ public: ProductSettings::ProductSettings() : SettingsCategory("product"), p(std::make_unique()) { - RegisterVariables({&p->stiForecastEnabled_, &p->stiPastEnabled_}); + RegisterVariables({&p->showSmoothedRangeFolding_, + &p->stiForecastEnabled_, + &p->stiPastEnabled_}); SetDefaults(); } ProductSettings::~ProductSettings() = default; @@ -37,6 +42,11 @@ ProductSettings::ProductSettings(ProductSettings&&) noexcept = default; ProductSettings& ProductSettings::operator=(ProductSettings&&) noexcept = default; +SettingsVariable& ProductSettings::show_smoothed_range_folding() const +{ + return p->showSmoothedRangeFolding_; +} + SettingsVariable& ProductSettings::sti_forecast_enabled() const { return p->stiForecastEnabled_; @@ -66,7 +76,9 @@ ProductSettings& ProductSettings::Instance() bool operator==(const ProductSettings& lhs, const ProductSettings& rhs) { - return (lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && + return (lhs.p->showSmoothedRangeFolding_ == + rhs.p->showSmoothedRangeFolding_ && + lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && lhs.p->stiPastEnabled_ == rhs.p->stiPastEnabled_); } diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index c7c09dd8..69abbddb 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -25,6 +25,7 @@ public: ProductSettings(ProductSettings&&) noexcept; ProductSettings& operator=(ProductSettings&&) noexcept; + SettingsVariable& show_smoothed_range_folding() const; SettingsVariable& sti_forecast_enabled() const; SettingsVariable& sti_past_enabled() const; diff --git a/test/data b/test/data index eaf8f185..0eb47590 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit eaf8f185ce2b3a3248da1a4d6c8e2e9265638f15 +Subproject commit 0eb475909f9e64ce81e7b8b39420d980b81b3baa From 77e02b76b1abb3b8cb91545a55cbe5d9f681e7b3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Dec 2024 22:48:04 -0600 Subject: [PATCH 266/762] Toggle for smoothed range folding in settings dialog --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 33 ++++++++++++------- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 33 +++++++++++-------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 69c3a387..59bc8881 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -136,6 +137,7 @@ public: &showMapAttribution_, &showMapCenter_, &showMapLogo_, + &showSmoothedRangeFolding_, &updateNotificationsEnabled_, &cursorIconAlwaysOn_, &debugEnabled_, @@ -251,6 +253,7 @@ public: settings::SettingsInterface showMapAttribution_ {}; settings::SettingsInterface showMapCenter_ {}; settings::SettingsInterface showMapLogo_ {}; + settings::SettingsInterface showSmoothedRangeFolding_ {}; settings::SettingsInterface updateNotificationsEnabled_ {}; settings::SettingsInterface cursorIconAlwaysOn_ {}; settings::SettingsInterface debugEnabled_ {}; @@ -527,21 +530,22 @@ void SettingsDialogImpl::SetupGeneralTab() { settings::GeneralSettings& generalSettings = settings::GeneralSettings::Instance(); - + settings::ProductSettings& productSettings = + settings::ProductSettings::Instance(); QObject::connect( - self_->ui->themeComboBox, - &QComboBox::currentTextChanged, - self_, - [this](const QString& text) - { - types::UiStyle style = types::GetUiStyle(text.toStdString()); - bool themeFileEnabled = style == types::UiStyle::FusionCustom; + self_->ui->themeComboBox, + &QComboBox::currentTextChanged, + self_, + [this](const QString& text) + { + types::UiStyle style = types::GetUiStyle(text.toStdString()); + bool themeFileEnabled = style == types::UiStyle::FusionCustom; - self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); - self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); - self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); - }); + self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); + self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); + self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); + }); theme_.SetSettingsVariable(generalSettings.theme()); SCWX_SETTINGS_COMBO_BOX(theme_, @@ -759,6 +763,11 @@ void SettingsDialogImpl::SetupGeneralTab() showMapLogo_.SetSettingsVariable(generalSettings.show_map_logo()); showMapLogo_.SetEditWidget(self_->ui->showMapLogoCheckBox); + showSmoothedRangeFolding_.SetSettingsVariable( + productSettings.show_smoothed_range_folding()); + showSmoothedRangeFolding_.SetEditWidget( + self_->ui->showSmoothedRangeFoldingCheckBox); + updateNotificationsEnabled_.SetSettingsVariable( generalSettings.update_notifications_enabled()); updateNotificationsEnabled_.SetEditWidget( diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 12bcbc0e..ec400682 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - -246 - 511 - 703 + -272 + 513 + 702 @@ -562,6 +562,19 @@ + + + + false + + + + + + Multi-Pane Cursor Marker Always On + + + @@ -584,22 +597,16 @@ - + - Update Notifications Enabled + Show Range Folding when Smoothing Radar Data - - - false - - - - + - Multi-Pane Cursor Marker Always On + Update Notifications Enabled From f010ea8fadaae8e68ded52b4bc3d8fc47c06a905 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Dec 2024 23:08:53 -0600 Subject: [PATCH 267/762] Add radar wireframe debug menu selection --- scwx-qt/source/scwx/qt/main/main_window.cpp | 8 ++++++++ scwx-qt/source/scwx/qt/main/main_window.hpp | 3 ++- scwx-qt/source/scwx/qt/main/main_window.ui | 10 ++++++++++ scwx-qt/source/scwx/qt/map/map_settings.hpp | 9 +++++---- scwx-qt/source/scwx/qt/map/map_widget.cpp | 12 ++++++++++++ scwx-qt/source/scwx/qt/map/map_widget.hpp | 2 ++ scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 14 ++++++++++++++ 7 files changed, 53 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index e21484c4..ca2cde28 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -644,6 +644,11 @@ void MainWindow::on_actionDumpRadarProductRecords_triggered() manager::RadarProductManager::DumpRecords(); } +void MainWindow::on_actionRadarWireframe_triggered(bool checked) +{ + p->activeMap_->SetRadarWireframeEnabled(checked); +} + void MainWindow::on_actionUserManual_triggered() { QDesktopServices::openUrl(QUrl {"https://supercell-wx.readthedocs.io/"}); @@ -1487,6 +1492,9 @@ void MainWindowImpl::UpdateRadarProductSettings() mainWindow_->ui->smoothRadarDataCheckBox->setCheckState( activeMap_->GetSmoothingEnabled() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); + + mainWindow_->ui->actionRadarWireframe->setChecked( + activeMap_->GetRadarWireframeEnabled()); } void MainWindowImpl::UpdateRadarSite() diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 6a4fb5b4..6eb7fee2 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -29,7 +29,7 @@ public: void keyPressEvent(QKeyEvent* ev) override final; void keyReleaseEvent(QKeyEvent* ev) override final; void showEvent(QShowEvent* event) override; - void closeEvent(QCloseEvent *event) override; + void closeEvent(QCloseEvent* event) override; signals: void ActiveMapMoved(double latitude, double longitude); @@ -49,6 +49,7 @@ private slots: void on_actionImGuiDebug_triggered(); void on_actionDumpLayerList_triggered(); void on_actionDumpRadarProductRecords_triggered(); + void on_actionRadarWireframe_triggered(bool checked); void on_actionUserManual_triggered(); void on_actionDiscord_triggered(); void on_actionGitHubRepository_triggered(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 42525199..5d856663 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -97,6 +97,8 @@ + + @@ -504,6 +506,14 @@ Location &Marker Manager + + + true + + + Radar &Wireframe + + diff --git a/scwx-qt/source/scwx/qt/map/map_settings.hpp b/scwx-qt/source/scwx/qt/map/map_settings.hpp index 642c8fa1..a5d445cf 100644 --- a/scwx-qt/source/scwx/qt/map/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/map/map_settings.hpp @@ -9,16 +9,17 @@ namespace map struct MapSettings { - explicit MapSettings() : isActive_ {false} {} + explicit MapSettings() {} ~MapSettings() = default; - MapSettings(const MapSettings&) = delete; + MapSettings(const MapSettings&) = delete; MapSettings& operator=(const MapSettings&) = delete; - MapSettings(MapSettings&&) noexcept = default; + MapSettings(MapSettings&&) noexcept = default; MapSettings& operator=(MapSettings&&) noexcept = default; - bool isActive_; + bool isActive_ {false}; + bool radarWireframeEnabled_ {false}; }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d7b1dff7..1fa62f97 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -728,6 +728,18 @@ std::uint16_t MapWidget::GetVcp() const } } +bool MapWidget::GetRadarWireframeEnabled() const +{ + return p->context_->settings().radarWireframeEnabled_; +} + +void MapWidget::SetRadarWireframeEnabled(bool wireframeEnabled) +{ + p->context_->settings().radarWireframeEnabled_ = wireframeEnabled; + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); +} + bool MapWidget::GetSmoothingEnabled() const { return p->smoothingEnabled_; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 4254453e..8f5b2951 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -47,6 +47,7 @@ public: common::RadarProductGroup GetRadarProductGroup() const; std::string GetRadarProductName() const; std::shared_ptr GetRadarSite() const; + bool GetRadarWireframeEnabled() const; std::chrono::system_clock::time_point GetSelectedTime() const; bool GetSmoothingEnabled() const; std::uint16_t GetVcp() const; @@ -118,6 +119,7 @@ public: double pitch); void SetInitialMapStyle(const std::string& styleName); void SetMapStyle(const std::string& styleName); + void SetRadarWireframeEnabled(bool enabled); void SetSmoothingEnabled(bool enabled); /** diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index be564926..53067ccc 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -267,6 +268,13 @@ void RadarProductLayer::Render( // Set OpenGL blend mode for transparency gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + bool wireframeEnabled = context()->settings().radarWireframeEnabled_; + if (wireframeEnabled) + { + // Set polygon mode to draw wireframe + gl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + if (p->colorTableNeedsUpdate_) { UpdateColorTable(); @@ -303,6 +311,12 @@ void RadarProductLayer::Render( gl.glBindVertexArray(p->vao_); gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + if (wireframeEnabled) + { + // Restore polygon mode to default + gl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + SCWX_GL_CHECK_ERROR(); } From cc0ebcd13cdd44a61b90dd395cf50360f8bc7b76 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 15 Dec 2024 06:39:26 -0600 Subject: [PATCH 268/762] Save radar smoothing state in settings --- scwx-qt/source/scwx/qt/main/main_window.cpp | 27 ++++++++++++++------- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 +++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index ca2cde28..313234d0 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1092,16 +1092,25 @@ void MainWindowImpl::ConnectOtherSignals() } } }); - connect(mainWindow_->ui->smoothRadarDataCheckBox, - &QCheckBox::checkStateChanged, - mainWindow_, - [this](Qt::CheckState state) - { - bool smoothingEnabled = (state == Qt::CheckState::Checked); + connect( + mainWindow_->ui->smoothRadarDataCheckBox, + &QCheckBox::checkStateChanged, + mainWindow_, + [this](Qt::CheckState state) + { + bool smoothingEnabled = (state == Qt::CheckState::Checked); - // Turn on smoothing - activeMap_->SetSmoothingEnabled(smoothingEnabled); - }); + auto it = std::find(maps_.cbegin(), maps_.cend(), activeMap_); + if (it != maps_.cend()) + { + std::size_t i = std::distance(maps_.cbegin(), it); + settings::MapSettings::Instance().smoothing_enabled(i).StageValue( + smoothingEnabled); + } + + // Turn on smoothing + activeMap_->SetSmoothingEnabled(smoothingEnabled); + }); connect(mainWindow_->ui->trackLocationCheckBox, &QCheckBox::checkStateChanged, mainWindow_, diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 1fa62f97..df45024a 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -105,13 +106,16 @@ public: map::AlertLayer::InitializeHandler(); auto& generalSettings = settings::GeneralSettings::Instance(); + auto& mapSettings = settings::MapSettings::Instance(); // Initialize context context_->set_map_provider( GetMapProvider(generalSettings.map_provider().GetValue())); context_->set_overlay_product_view(overlayProductView); + // Initialize map data SetRadarSite(generalSettings.default_radar_site().GetValue()); + smoothingEnabled_ = mapSettings.smoothing_enabled(id).GetValue(); // Create ImGui Context static size_t currentMapId_ {0u}; From a65504a2cb555bbd21ce14736d24cfaac9428e13 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 15 Dec 2024 07:21:46 -0600 Subject: [PATCH 269/762] Disable range folding display by default when smoothing --- .../scwx/qt/view/level2_product_view.cpp | 45 ++++++++++++++----- .../scwx/qt/view/level3_radial_view.cpp | 31 +++++++++---- .../scwx/qt/view/level3_raster_view.cpp | 33 ++++++++++---- .../scwx/qt/view/radar_product_view.cpp | 25 ++++++++++- .../scwx/qt/view/radar_product_view.hpp | 5 ++- 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 0a644654..6db1a8c6 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -135,6 +135,7 @@ public: std::shared_ptr momentDataBlock0_; + bool lastShowSmoothedRangeFolding_ {false}; bool lastSmoothingEnabled_ {false}; std::vector coordinates_ {}; @@ -144,6 +145,8 @@ public: std::vector cfpMoments_ {}; std::uint16_t edgeValue_ {}; + bool showSmoothedRangeFolding_ {false}; + float latitude_; float longitude_; float elevationCut_; @@ -519,7 +522,9 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; @@ -533,13 +538,16 @@ void Level2ProductView::ComputeSweep() return; } if (radarData == p->elevationScan_ && - smoothingEnabled == p->lastSmoothingEnabled_) + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } - p->lastSmoothingEnabled_ = smoothingEnabled; + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; logger_->debug("Computing Sweep"); @@ -798,10 +806,16 @@ void Level2ProductView::ComputeSweep() const std::uint8_t& dm3 = nextDataMomentsArray8[i]; const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; - if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && - dm2 < snrThreshold && dm2 != RANGE_FOLDED && - dm3 < snrThreshold && dm3 != RANGE_FOLDED && - dm4 < snrThreshold && dm4 != RANGE_FOLDED) + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) { // Skip only if all data moments are hidden continue; @@ -855,10 +869,16 @@ void Level2ProductView::ComputeSweep() const std::uint16_t& dm3 = nextDataMomentsArray16[i]; const std::uint16_t& dm4 = nextDataMomentsArray16[i + 1]; - if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && - dm2 < snrThreshold && dm2 != RANGE_FOLDED && - dm3 < snrThreshold && dm3 != RANGE_FOLDED && - dm4 < snrThreshold && dm4 != RANGE_FOLDED) + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) { // Skip only if all data moments are hidden continue; @@ -1012,7 +1032,8 @@ void Level2ProductView::Impl::ComputeEdgeValue() template T Level2ProductView::Impl::RemapDataMoment(T dataMoment) const { - if (dataMoment != 0) + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) { return dataMoment; } diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 8d561935..f161673e 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -58,7 +58,10 @@ public: std::vector dataMoments8_ {}; std::uint8_t edgeValue_ {}; + bool showSmoothedRangeFolding_ {false}; + std::shared_ptr lastRadialData_ {}; + bool lastShowSmoothedRangeFolding_ {false}; bool lastSmoothingEnabled_ {false}; float latitude_; @@ -130,7 +133,9 @@ void Level3RadialView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -162,7 +167,9 @@ void Level3RadialView::ComputeSweep() return; } else if (gpm == graphic_product_message() && - smoothingEnabled == p->lastSmoothingEnabled_) + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -170,7 +177,8 @@ void Level3RadialView::ComputeSweep() } set_graphic_product_message(gpm); - p->lastSmoothingEnabled_ = smoothingEnabled; + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; // A message with radial data should have a Product Description Block and // Product Symbology Block @@ -398,10 +406,16 @@ void Level3RadialView::ComputeSweep() const std::uint8_t& dm3 = nextDataMomentsArray8[i]; const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; - if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && - dm2 < snrThreshold && dm2 != RANGE_FOLDED && - dm3 < snrThreshold && dm3 != RANGE_FOLDED && - dm4 < snrThreshold && dm4 != RANGE_FOLDED) + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) { // Skip only if all data moments are hidden continue; @@ -502,7 +516,8 @@ void Level3RadialView::ComputeSweep() std::uint8_t Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const { - if (dataMoment != 0) + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) { return dataMoment; } diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index 433b51be..20985537 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -41,8 +41,11 @@ public: std::vector dataMoments8_ {}; std::uint8_t edgeValue_ {}; + bool showSmoothedRangeFolding_ {false}; + std::shared_ptr lastRasterData_ {}; - bool lastSmoothingEnabled_ {false}; + bool lastShowSmoothedRangeFolding_ {false}; + bool lastSmoothingEnabled_ {false}; float latitude_; float longitude_; @@ -113,7 +116,9 @@ void Level3RasterView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -145,7 +150,9 @@ void Level3RasterView::ComputeSweep() return; } else if (gpm == graphic_product_message() && - smoothingEnabled == p->lastSmoothingEnabled_) + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -153,7 +160,8 @@ void Level3RasterView::ComputeSweep() } set_graphic_product_message(gpm); - p->lastSmoothingEnabled_ = smoothingEnabled; + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; // A message with radial data should have a Product Description Block and // Product Symbology Block @@ -364,10 +372,16 @@ void Level3RasterView::ComputeSweep() const std::uint8_t& dm3 = nextDataMomentsArray8[bin]; const std::uint8_t& dm4 = nextDataMomentsArray8[bin + 1]; - if (dm1 < snrThreshold && dm1 != RANGE_FOLDED && - dm2 < snrThreshold && dm2 != RANGE_FOLDED && - dm3 < snrThreshold && dm3 != RANGE_FOLDED && - dm4 < snrThreshold && dm4 != RANGE_FOLDED) + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) { // Skip only if all data moments are hidden continue; @@ -424,7 +438,8 @@ void Level3RasterView::ComputeSweep() std::uint8_t Level3RasterViewImpl::RemapDataMoment(std::uint8_t dataMoment) const { - if (dataMoment != 0) + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) { return dataMoment; } diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp index 04534593..9c5a84de 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -28,27 +29,44 @@ class RadarProductViewImpl { public: explicit RadarProductViewImpl( + RadarProductView* self, std::shared_ptr radarProductManager) : + self_ {self}, initialized_ {false}, sweepMutex_ {}, selectedTime_ {}, radarProductManager_ {radarProductManager} { + auto& productSettings = settings::ProductSettings::Instance(); + connection_ = productSettings.changed_signal().connect( + [this]() + { + showSmoothedRangeFolding_ = settings::ProductSettings::Instance() + .show_smoothed_range_folding() + .GetValue(); + self_->Update(); + }); + ; } ~RadarProductViewImpl() {} + RadarProductView* self_; + bool initialized_; std::mutex sweepMutex_; std::chrono::system_clock::time_point selectedTime_; + bool showSmoothedRangeFolding_ {false}; bool smoothingEnabled_ {false}; std::shared_ptr radarProductManager_; + + boost::signals2::scoped_connection connection_; }; RadarProductView::RadarProductView( std::shared_ptr radarProductManager) : - p(std::make_unique(radarProductManager)) {}; + p(std::make_unique(this, radarProductManager)) {}; RadarProductView::~RadarProductView() = default; const std::vector& @@ -88,6 +106,11 @@ std::chrono::system_clock::time_point RadarProductView::selected_time() const return p->selectedTime_; } +bool RadarProductView::show_smoothed_range_folding() const +{ + return p->showSmoothedRangeFolding_; +} + bool RadarProductView::smoothing_enabled() const { return p->smoothingEnabled_; diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 9bda8795..141441bd 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -49,8 +49,9 @@ public: std::shared_ptr radar_product_manager() const; std::chrono::system_clock::time_point selected_time() const; - bool smoothing_enabled() const; - std::mutex& sweep_mutex(); + bool show_smoothed_range_folding() const; + bool smoothing_enabled() const; + std::mutex& sweep_mutex(); void set_radar_product_manager( std::shared_ptr radarProductManager); From 57b773d009ae792f0cecd2bfd9febb308de9b856 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 15 Dec 2024 08:22:19 -0600 Subject: [PATCH 270/762] Linting fixes --- .../scwx/qt/manager/radar_product_manager.cpp | 47 ++++++++----- .../scwx/qt/map/radar_product_layer.cpp | 2 - .../source/scwx/qt/settings/map_settings.cpp | 53 +++++++------- .../scwx/qt/view/level2_product_view.cpp | 69 +++++++++++-------- 4 files changed, 98 insertions(+), 73 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 95247c06..d25bb28d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -64,6 +63,8 @@ static constexpr uint32_t NUM_COORIDNATES_1_DEGREE = static const std::string kDefaultLevel3Product_ {"N0B"}; +static constexpr std::size_t kTimerPlaces_ {6u}; + static constexpr std::chrono::seconds kFastRetryInterval_ {15}; static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; @@ -464,6 +465,8 @@ void RadarProductManager::Initialize() const auto radialGates0_5Degree = boost::irange(0, NUM_RADIAL_GATES_0_5_DEGREE); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions p->CalculateCoordinates( radialGates0_5Degree, units::angle::degrees {0.5f}, // Radial angle @@ -471,10 +474,11 @@ void RadarProductManager::Initialize() // Far end of the first gate is the gate size distance from the radar site 1.0f, coordinates0_5Degree); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) timer.stop(); logger_->debug("Coordinates (0.5 degree) calculated in {}", - timer.format(6, "%ws")); + timer.format(kTimerPlaces_, "%ws")); // Calculate half degree smooth azimuth coordinates timer.start(); @@ -483,6 +487,8 @@ void RadarProductManager::Initialize() coordinates0_5DegreeSmooth.resize(NUM_COORIDNATES_0_5_DEGREE); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions p->CalculateCoordinates(radialGates0_5Degree, units::angle::degrees {0.5f}, // Radial angle units::angle::degrees {0.25f}, // Angle offset @@ -490,10 +496,11 @@ void RadarProductManager::Initialize() // distance from the radar site 0.5f, coordinates0_5DegreeSmooth); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) timer.stop(); logger_->debug("Coordinates (0.5 degree smooth) calculated in {}", - timer.format(6, "%ws")); + timer.format(kTimerPlaces_, "%ws")); // Calculate 1 degree azimuth coordinates timer.start(); @@ -504,6 +511,8 @@ void RadarProductManager::Initialize() const auto radialGates1Degree = boost::irange(0, NUM_RADIAL_GATES_1_DEGREE); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions p->CalculateCoordinates( radialGates1Degree, units::angle::degrees {1.0f}, // Radial angle @@ -511,10 +520,11 @@ void RadarProductManager::Initialize() // Far end of the first gate is the gate size distance from the radar site 1.0f, coordinates1Degree); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) timer.stop(); logger_->debug("Coordinates (1 degree) calculated in {}", - timer.format(6, "%ws")); + timer.format(kTimerPlaces_, "%ws")); // Calculate 1 degree smooth azimuth coordinates timer.start(); @@ -522,6 +532,8 @@ void RadarProductManager::Initialize() coordinates1DegreeSmooth.resize(NUM_COORIDNATES_1_DEGREE); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions p->CalculateCoordinates(radialGates1Degree, units::angle::degrees {1.0f}, // Radial angle units::angle::degrees {0.5f}, // Angle offset @@ -529,10 +541,11 @@ void RadarProductManager::Initialize() // distance from the radar site 0.5f, coordinates1DegreeSmooth); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) timer.stop(); logger_->debug("Coordinates (1 degree smooth) calculated in {}", - timer.format(6, "%ws")); + timer.format(kTimerPlaces_, "%ws")); p->initialized_ = true; } @@ -558,23 +571,25 @@ void RadarProductManagerImpl::CalculateCoordinates( radialGates.end(), [&](uint32_t radialGate) { - const uint16_t gate = - static_cast(radialGate % common::MAX_DATA_MOMENT_GATES); - const uint16_t radial = - static_cast(radialGate / common::MAX_DATA_MOMENT_GATES); + const auto gate = static_cast( + radialGate % common::MAX_DATA_MOMENT_GATES); + const auto radial = static_cast( + radialGate / common::MAX_DATA_MOMENT_GATES); - const float angle = radial * radialAngle.value() + angleOffset.value(); - const float range = (gate + gateRangeOffset) * gateSize; - const size_t offset = radialGate * 2; + const float angle = static_cast(radial) * radialAngle.value() + + angleOffset.value(); + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = static_cast(radialGate) * 2; - double latitude; - double longitude; + double latitude = 0.0; + double longitude = 0.0; geodesic.Direct( radar.first, radar.second, angle, range, latitude, longitude); - outputCoordinates[offset] = latitude; - outputCoordinates[offset + 1] = longitude; + outputCoordinates[offset] = static_cast(latitude); + outputCoordinates[offset + 1] = static_cast(longitude); }); } diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 53067ccc..a622b600 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -6,8 +6,6 @@ #include #include -#include - #if defined(_MSC_VER) # pragma warning(push, 0) #endif diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 61a64a14..fd3b3001 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -6,7 +6,6 @@ #include #include -#include #include @@ -53,21 +52,21 @@ public: { for (std::size_t i = 0; i < kCount_; i++) { - map_[i].mapStyle_.SetDefault(kDefaultMapStyle_); - map_[i].radarSite_.SetDefault(kDefaultRadarSite_); - map_[i].radarProductGroup_.SetDefault( + map_.at(i).mapStyle_.SetDefault(kDefaultMapStyle_); + map_.at(i).radarSite_.SetDefault(kDefaultRadarSite_); + map_.at(i).radarProductGroup_.SetDefault( kDefaultRadarProductGroupString_); - map_[i].radarProduct_.SetDefault(kDefaultRadarProduct_[i]); - map_[i].smoothingEnabled_.SetDefault(kDefaultSmoothingEnabled_); + map_.at(i).radarProduct_.SetDefault(kDefaultRadarProduct_.at(i)); + map_.at(i).smoothingEnabled_.SetDefault(kDefaultSmoothingEnabled_); - map_[i].radarSite_.SetValidator( + map_.at(i).radarSite_.SetValidator( [](const std::string& value) { // Radar site must exist return config::RadarSite::Get(value) != nullptr; }); - map_[i].radarProductGroup_.SetValidator( + map_.at(i).radarProductGroup_.SetValidator( [](const std::string& value) { // Radar product group must be valid @@ -76,12 +75,12 @@ public: return radarProductGroup != common::RadarProductGroup::Unknown; }); - map_[i].radarProduct_.SetValidator( + map_.at(i).radarProduct_.SetValidator( [this, i](const std::string& value) { common::RadarProductGroup radarProductGroup = common::GetRadarProductGroup( - map_[i].radarProductGroup_.GetValue()); + map_.at(i).radarProductGroup_.GetValue()); if (radarProductGroup == common::RadarProductGroup::Level2) { @@ -97,11 +96,11 @@ public: }); variables_.insert(variables_.cend(), - {&map_[i].mapStyle_, - &map_[i].radarSite_, - &map_[i].radarProductGroup_, - &map_[i].radarProduct_, - &map_[i].smoothingEnabled_}); + {&map_.at(i).mapStyle_, + &map_.at(i).radarSite_, + &map_.at(i).radarProductGroup_, + &map_.at(i).radarProduct_, + &map_.at(i).smoothingEnabled_}); } } @@ -109,11 +108,11 @@ public: void SetDefaults(std::size_t i) { - map_[i].mapStyle_.SetValueToDefault(); - map_[i].radarSite_.SetValueToDefault(); - map_[i].radarProductGroup_.SetValueToDefault(); - map_[i].radarProduct_.SetValueToDefault(); - map_[i].smoothingEnabled_.SetValueToDefault(); + map_.at(i).mapStyle_.SetValueToDefault(); + map_.at(i).radarSite_.SetValueToDefault(); + map_.at(i).radarProductGroup_.SetValueToDefault(); + map_.at(i).radarProduct_.SetValueToDefault(); + map_.at(i).smoothingEnabled_.SetValueToDefault(); } friend void tag_invoke(boost::json::value_from_tag, @@ -160,28 +159,28 @@ std::size_t MapSettings::count() const SettingsVariable& MapSettings::map_style(std::size_t i) const { - return p->map_[i].mapStyle_; + return p->map_.at(i).mapStyle_; } SettingsVariable& MapSettings::radar_site(std::size_t i) const { - return p->map_[i].radarSite_; + return p->map_.at(i).radarSite_; } SettingsVariable& MapSettings::radar_product_group(std::size_t i) const { - return p->map_[i].radarProductGroup_; + return p->map_.at(i).radarProductGroup_; } SettingsVariable& MapSettings::radar_product(std::size_t i) const { - return p->map_[i].radarProduct_; + return p->map_.at(i).radarProduct_; } SettingsVariable& MapSettings::smoothing_enabled(std::size_t i) const { - return p->map_[i].smoothingEnabled_; + return p->map_.at(i).smoothingEnabled_; } bool MapSettings::Shutdown() @@ -191,7 +190,7 @@ bool MapSettings::Shutdown() // Commit settings that are managed separate from the settings dialog for (std::size_t i = 0; i < kCount_; ++i) { - Impl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_.at(i); dataChanged |= mapRecordSettings.mapStyle_.Commit(); dataChanged |= mapRecordSettings.smoothingEnabled_.Commit(); @@ -215,7 +214,7 @@ bool MapSettings::ReadJson(const boost::json::object& json) if (i < mapArray.size() && mapArray.at(i).is_object()) { const boost::json::object& mapRecord = mapArray.at(i).as_object(); - Impl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_.at(i); // Load JSON Elements validated &= mapRecordSettings.mapStyle_.ReadValue(mapRecord); diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 6db1a8c6..41d050c3 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -25,6 +25,9 @@ static constexpr std::uint32_t kMaxRadialGates_ = common::MAX_0_5_DEGREE_RADIALS * common::MAX_DATA_MOMENT_GATES; static constexpr std::uint32_t kMaxCoordinates_ = kMaxRadialGates_ * 2u; +static constexpr std::size_t kVerticesPerGate_ = 6u; +static constexpr std::size_t kVerticesPerOriginGate_ = 3u; + static constexpr uint16_t RANGE_FOLDED = 1u; static constexpr uint32_t VERTICES_PER_BIN = 6u; static constexpr uint32_t VALUES_PER_VERTEX = 2u; @@ -769,7 +772,11 @@ void Level2ProductView::ComputeSweep() continue; } - std::size_t vertexCount = (gate > 0) ? 6 : 3; + std::size_t vertexCount = + (gate > 0) ? kVerticesPerGate_ : kVerticesPerOriginGate_; + + // Allow pointer arithmetic here, as bounds have already been checked + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) // Store data moment value if (dataMomentsArray8 != nullptr) @@ -902,6 +909,8 @@ void Level2ProductView::ComputeSweep() } } + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + // Store vertices if (gate > 0) { @@ -919,13 +928,15 @@ void Level2ProductView::ComputeSweep() common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; - std::size_t offset2 = offset1 + gateSize * 2; + std::size_t offset2 = + offset1 + static_cast(gateSize) * 2; std::size_t offset3 = (((startRadial + radial + 1) % vertexRadials) * common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; - std::size_t offset4 = offset3 + gateSize * 2; + std::size_t offset4 = + offset3 + static_cast(gateSize) * 2; vertices[vIndex++] = coordinates[offset1]; vertices[vIndex++] = coordinates[offset1 + 1]; @@ -945,7 +956,7 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - vertexCount = 6; + vertexCount = kVerticesPerGate_; } else { @@ -970,7 +981,7 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertexCount = 3; + vertexCount = kVerticesPerOriginGate_; } } } @@ -1010,7 +1021,7 @@ void Level2ProductView::Impl::ComputeEdgeValue() { case wsr88d::rda::DataBlockType::MomentVel: case wsr88d::rda::DataBlockType::MomentZdr: - edgeValue_ = offset; + edgeValue_ = static_cast(offset); break; case wsr88d::rda::DataBlockType::MomentSw: @@ -1019,7 +1030,7 @@ void Level2ProductView::Impl::ComputeEdgeValue() break; case wsr88d::rda::DataBlockType::MomentRho: - edgeValue_ = 255; + edgeValue_ = std::numeric_limits::max(); break; case wsr88d::rda::DataBlockType::MomentRef: @@ -1194,30 +1205,32 @@ void Level2ProductView::Impl::ComputeCoordinates( } } - std::for_each(std::execution::par_unseq, - gates.begin(), - gates.end(), - [&](std::uint32_t gate) - { - const std::uint32_t radialGate = - radial * common::MAX_DATA_MOMENT_GATES + gate; - const float range = - (gate + gateRangeOffset) * gateSize; - const std::size_t offset = radialGate * 2; + std::for_each( + std::execution::par_unseq, + gates.begin(), + gates.end(), + [&](std::uint32_t gate) + { + const std::uint32_t radialGate = + radial * common::MAX_DATA_MOMENT_GATES + gate; + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = + static_cast(radialGate) * 2; - double latitude; - double longitude; + double latitude = 0.0; + double longitude = 0.0; - geodesic.Direct(radarLatitude, - radarLongitude, - angle.value(), - range, - latitude, - longitude); + geodesic.Direct(radarLatitude, + radarLongitude, + angle.value(), + range, + latitude, + longitude); - coordinates_[offset] = latitude; - coordinates_[offset + 1] = longitude; - }); + coordinates_[offset] = static_cast(latitude); + coordinates_[offset + 1] = static_cast(longitude); + }); }); timer.stop(); logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); From a3eb53a649f7919a46e18e8a827db9baa45d9626 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 15 Dec 2024 22:32:25 -0600 Subject: [PATCH 271/762] Additional clang-tidy action cleanup --- .clang-tidy | 1 + scwx-qt/source/scwx/qt/main/main_window.cpp | 4 +- .../scwx/qt/manager/radar_product_manager.hpp | 12 +-- scwx-qt/source/scwx/qt/map/map_settings.hpp | 4 +- scwx-qt/source/scwx/qt/map/map_widget.hpp | 25 +++--- .../scwx/qt/map/radar_product_layer.cpp | 2 +- .../source/scwx/qt/settings/map_settings.cpp | 11 ++- .../source/scwx/qt/settings/map_settings.hpp | 10 +-- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 4 +- .../scwx/qt/view/level2_product_view.cpp | 51 +++++++----- .../scwx/qt/view/level3_product_view.cpp | 12 +-- .../scwx/qt/view/level3_product_view.hpp | 2 +- .../scwx/qt/view/level3_radial_view.cpp | 83 +++++++++---------- .../scwx/qt/view/level3_raster_view.cpp | 13 +-- .../scwx/qt/view/radar_product_view.hpp | 11 +-- 15 files changed, 128 insertions(+), 117 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 1eed15a2..602e3d0c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,7 @@ Checks: - 'misc-*' - 'modernize-*' - 'performance-*' + - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-modernize-use-trailing-return-type' diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 313234d0..ad504cc0 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1098,12 +1098,12 @@ void MainWindowImpl::ConnectOtherSignals() mainWindow_, [this](Qt::CheckState state) { - bool smoothingEnabled = (state == Qt::CheckState::Checked); + const bool smoothingEnabled = (state == Qt::CheckState::Checked); auto it = std::find(maps_.cbegin(), maps_.cend(), activeMap_); if (it != maps_.cend()) { - std::size_t i = std::distance(maps_.cbegin(), it); + const std::size_t i = std::distance(maps_.cbegin(), it); settings::MapSettings::Instance().smoothing_enabled(i).StageValue( smoothingEnabled); } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 9232f7d9..3f4899ea 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -41,12 +41,12 @@ public: */ static void DumpRecords(); - const std::vector& coordinates(common::RadialSize radialSize, - bool smoothingEnabled) const; - const scwx::util::time_zone* default_time_zone() const; - float gate_size() const; - std::string radar_id() const; - std::shared_ptr radar_site() const; + [[nodiscard]] const std::vector& + coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; + [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] float gate_size() const; + [[nodiscard]] std::string radar_id() const; + [[nodiscard]] std::shared_ptr radar_site() const; void Initialize(); diff --git a/scwx-qt/source/scwx/qt/map/map_settings.hpp b/scwx-qt/source/scwx/qt/map/map_settings.hpp index a5d445cf..a015aca3 100644 --- a/scwx-qt/source/scwx/qt/map/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/map/map_settings.hpp @@ -9,8 +9,8 @@ namespace map struct MapSettings { - explicit MapSettings() {} - ~MapSettings() = default; + explicit MapSettings() = default; + ~MapSettings() = default; MapSettings(const MapSettings&) = delete; MapSettings& operator=(const MapSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 8f5b2951..23d38680 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -39,18 +39,19 @@ public: void DumpLayerList() const; - common::Level3ProductCategoryMap GetAvailableLevel3Categories(); - float GetElevation() const; - std::vector GetElevationCuts() const; - std::vector GetLevel3Products(); - std::string GetMapStyle() const; - common::RadarProductGroup GetRadarProductGroup() const; - std::string GetRadarProductName() const; - std::shared_ptr GetRadarSite() const; - bool GetRadarWireframeEnabled() const; - std::chrono::system_clock::time_point GetSelectedTime() const; - bool GetSmoothingEnabled() const; - std::uint16_t GetVcp() const; + [[nodiscard]] common::Level3ProductCategoryMap + GetAvailableLevel3Categories(); + [[nodiscard]] float GetElevation() const; + [[nodiscard]] std::vector GetElevationCuts() const; + [[nodiscard]] std::vector GetLevel3Products(); + [[nodiscard]] std::string GetMapStyle() const; + [[nodiscard]] common::RadarProductGroup GetRadarProductGroup() const; + [[nodiscard]] std::string GetRadarProductName() const; + [[nodiscard]] std::shared_ptr GetRadarSite() const; + [[nodiscard]] bool GetRadarWireframeEnabled() const; + [[nodiscard]] std::chrono::system_clock::time_point GetSelectedTime() const; + [[nodiscard]] bool GetSmoothingEnabled() const; + [[nodiscard]] std::uint16_t GetVcp() const; void SelectElevation(float elevation); diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index a622b600..8d243973 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -266,7 +266,7 @@ void RadarProductLayer::Render( // Set OpenGL blend mode for transparency gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - bool wireframeEnabled = context()->settings().radarWireframeEnabled_; + const bool wireframeEnabled = context()->settings().radarWireframeEnabled_; if (wireframeEnabled) { // Set polygon mode to draw wireframe diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index fd3b3001..e1c8276e 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -157,28 +157,27 @@ std::size_t MapSettings::count() const return kCount_; } -SettingsVariable& MapSettings::map_style(std::size_t i) const +SettingsVariable& MapSettings::map_style(std::size_t i) { return p->map_.at(i).mapStyle_; } -SettingsVariable& MapSettings::radar_site(std::size_t i) const +SettingsVariable& MapSettings::radar_site(std::size_t i) { return p->map_.at(i).radarSite_; } -SettingsVariable& -MapSettings::radar_product_group(std::size_t i) const +SettingsVariable& MapSettings::radar_product_group(std::size_t i) { return p->map_.at(i).radarProductGroup_; } -SettingsVariable& MapSettings::radar_product(std::size_t i) const +SettingsVariable& MapSettings::radar_product(std::size_t i) { return p->map_.at(i).radarProduct_; } -SettingsVariable& MapSettings::smoothing_enabled(std::size_t i) const +SettingsVariable& MapSettings::smoothing_enabled(std::size_t i) { return p->map_.at(i).smoothingEnabled_; } diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index 188819ac..36ce6464 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -26,11 +26,11 @@ public: MapSettings& operator=(MapSettings&&) noexcept; std::size_t count() const; - SettingsVariable& map_style(std::size_t i) const; - SettingsVariable& radar_site(std::size_t i) const; - SettingsVariable& radar_product_group(std::size_t i) const; - SettingsVariable& radar_product(std::size_t i) const; - SettingsVariable& smoothing_enabled(std::size_t i) const; + SettingsVariable& map_style(std::size_t i); + SettingsVariable& radar_site(std::size_t i); + SettingsVariable& radar_product_group(std::size_t i); + SettingsVariable& radar_product(std::size_t i); + SettingsVariable& smoothing_enabled(std::size_t i); bool Shutdown(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 59bc8881..1872be26 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -539,8 +539,8 @@ void SettingsDialogImpl::SetupGeneralTab() self_, [this](const QString& text) { - types::UiStyle style = types::GetUiStyle(text.toStdString()); - bool themeFileEnabled = style == types::UiStyle::FusionCustom; + const types::UiStyle style = types::GetUiStyle(text.toStdString()); + const bool themeFileEnabled = style == types::UiStyle::FusionCustom; self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 41d050c3..16d3ab50 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -25,6 +25,9 @@ static constexpr std::uint32_t kMaxRadialGates_ = common::MAX_0_5_DEGREE_RADIALS * common::MAX_DATA_MOMENT_GATES; static constexpr std::uint32_t kMaxCoordinates_ = kMaxRadialGates_ * 2u; +static constexpr std::uint8_t kDataWordSize8_ = 8u; +static constexpr std::uint8_t kDataWordSize16_ = 16u; + static constexpr std::size_t kVerticesPerGate_ = 6u; static constexpr std::size_t kVerticesPerOriginGate_ = 3u; @@ -108,6 +111,12 @@ public: threadPool_.join(); }; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + Impl(Impl&&) noexcept = default; + Impl& operator=(Impl&&) noexcept = default; + void ComputeCoordinates( const std::shared_ptr& radarData, bool smoothingEnabled); @@ -119,7 +128,7 @@ public: void ComputeEdgeValue(); template - inline T RemapDataMoment(T dataMoment) const; + [[nodiscard]] inline T RemapDataMoment(T dataMoment) const; static bool IsRadarDataIncomplete( const std::shared_ptr& radarData); @@ -130,7 +139,8 @@ public: boost::asio::thread_pool threadPool_ {1u}; common::Level2Product product_; - wsr88d::rda::DataBlockType dataBlockType_; + wsr88d::rda::DataBlockType dataBlockType_ { + wsr88d::rda::DataBlockType::Unknown}; float selectedElevation_; @@ -525,9 +535,9 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); - p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); - bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; @@ -661,7 +671,7 @@ void Level2ProductView::ComputeSweep() const auto& radialPair = *it; std::uint16_t radial = radialPair.first; const auto& radialData = radialPair.second; - std::shared_ptr + const std::shared_ptr momentData = radialData->moment_data_block(p->dataBlockType_); if (momentData0->data_word_size() != momentData->data_word_size()) @@ -748,7 +758,7 @@ void Level2ProductView::ComputeSweep() continue; } - if (nextMomentData->data_word_size() == 8) + if (nextMomentData->data_word_size() == kDataWordSize8_) { nextDataMomentsArray8 = reinterpret_cast( nextMomentData->data_moments()); @@ -783,7 +793,7 @@ void Level2ProductView::ComputeSweep() { if (!smoothingEnabled) { - std::uint8_t dataValue = dataMomentsArray8[i]; + const std::uint8_t& dataValue = dataMomentsArray8[i]; if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) { continue; @@ -791,7 +801,7 @@ void Level2ProductView::ComputeSweep() for (std::size_t m = 0; m < vertexCount; m++) { - dataMoments8[mIndex++] = dataMomentsArray8[i]; + dataMoments8[mIndex++] = dataValue; if (cfpMomentsArray != nullptr) { @@ -851,7 +861,7 @@ void Level2ProductView::ComputeSweep() { if (!smoothingEnabled) { - std::uint16_t dataValue = dataMomentsArray16[i]; + const std::uint16_t& dataValue = dataMomentsArray16[i]; if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) { continue; @@ -859,7 +869,7 @@ void Level2ProductView::ComputeSweep() for (std::size_t m = 0; m < vertexCount; m++) { - dataMoments16[mIndex++] = dataMomentsArray16[i]; + dataMoments16[mIndex++] = dataValue; } } else if (gate > 0) @@ -924,18 +934,19 @@ void Level2ProductView::ComputeSweep() const std::uint16_t baseCoord = gate - 1; - std::size_t offset1 = ((startRadial + radial) % vertexRadials * - common::MAX_DATA_MOMENT_GATES + - baseCoord) * - 2; - std::size_t offset2 = + const std::size_t offset1 = + ((startRadial + radial) % vertexRadials * + common::MAX_DATA_MOMENT_GATES + + baseCoord) * + 2; + const std::size_t offset2 = offset1 + static_cast(gateSize) * 2; - std::size_t offset3 = + const std::size_t offset3 = (((startRadial + radial + 1) % vertexRadials) * common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; - std::size_t offset4 = + const std::size_t offset4 = offset3 + static_cast(gateSize) * 2; vertices[vIndex++] = coordinates[offset1]; @@ -955,8 +966,6 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertexCount = kVerticesPerGate_; } else { @@ -980,8 +989,6 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = kVerticesPerOriginGate_; } } } diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index 642e0981..3858ac11 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -489,8 +489,8 @@ std::uint8_t Level3ProductView::ComputeEdgeValue() const { std::uint8_t edgeValue = 0; - std::shared_ptr descriptionBlock = - p->graphicMessage_->description_block(); + const std::shared_ptr + descriptionBlock = p->graphicMessage_->description_block(); const float offset = descriptionBlock->offset(); const float scale = descriptionBlock->scale(); @@ -498,11 +498,12 @@ std::uint8_t Level3ProductView::ComputeEdgeValue() const switch (p->category_) { case common::Level3ProductCategory::Velocity: - edgeValue = (scale > 0.0f) ? (-offset / scale) : -offset; + edgeValue = static_cast((scale > 0.0f) ? (-offset / scale) : + -offset); break; case common::Level3ProductCategory::DifferentialReflectivity: - edgeValue = -offset; + edgeValue = static_cast(-offset); break; case common::Level3ProductCategory::SpectrumWidth: @@ -512,7 +513,8 @@ std::uint8_t Level3ProductView::ComputeEdgeValue() const case common::Level3ProductCategory::CorrelationCoefficient: edgeValue = static_cast( - std::max(255, descriptionBlock->number_of_levels())); + std::max(std::numeric_limits::max(), + descriptionBlock->number_of_levels())); break; case common::Level3ProductCategory::Reflectivity: diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp index e5de1873..b5e043b3 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp @@ -58,7 +58,7 @@ protected: void DisconnectRadarProductManager() override; void UpdateColorTableLut() override; - std::uint8_t ComputeEdgeValue() const; + [[nodiscard]] std::uint8_t ComputeEdgeValue() const; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index f161673e..135f5e65 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -47,7 +47,8 @@ public: const std::shared_ptr& radialData, bool smoothingEnabled); - inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; + [[nodiscard]] inline std::uint8_t + RemapDataMoment(std::uint8_t dataMoment) const; Level3RadialView* self_; @@ -133,9 +134,9 @@ void Level3RadialView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); - p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); - bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -381,7 +382,7 @@ void Level3RadialView::ComputeSweep() if (!smoothingEnabled) { // Store data moment value - uint8_t dataValue = + const uint8_t dataValue = (i < dataMomentsArray8.size()) ? dataMomentsArray8[i] : 0; if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) { @@ -470,8 +471,6 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertexCount = 6; } else { @@ -494,8 +493,6 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = 3; } } } @@ -561,44 +558,46 @@ void Level3RadialView::Impl::ComputeCoordinates( // size distance from the radar site 1.0f; - std::for_each(std::execution::par_unseq, - radials.begin(), - radials.end(), - [&](std::uint32_t radial) - { - float angle = radialData->start_angle(radial); + std::for_each( + std::execution::par_unseq, + radials.begin(), + radials.end(), + [&](std::uint32_t radial) + { + float angle = radialData->start_angle(radial); - if (smoothingEnabled) - { - angle += radialData->delta_angle(radial) * 0.5f; - } + if (smoothingEnabled) + { + static constexpr float kDeltaAngleFactor = 0.5f; + angle += radialData->delta_angle(radial) * kDeltaAngleFactor; + } - std::for_each(std::execution::par_unseq, - gates.begin(), - gates.end(), - [&](std::uint32_t gate) - { - const std::uint32_t radialGate = - radial * common::MAX_DATA_MOMENT_GATES + - gate; - const float range = - (gate + gateRangeOffset) * gateSize; - const std::size_t offset = radialGate * 2; + std::for_each( + std::execution::par_unseq, + gates.begin(), + gates.end(), + [&](std::uint32_t gate) + { + const std::uint32_t radialGate = + radial * common::MAX_DATA_MOMENT_GATES + gate; + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = static_cast(radialGate) * 2; - double latitude; - double longitude; + double latitude = 0.0; + double longitude = 0.0; - geodesic.Direct(radarLatitude, - radarLongitude, - angle, - range, - latitude, - longitude); + geodesic.Direct(radarLatitude, + radarLongitude, + angle, + range, + latitude, + longitude); - coordinates_[offset] = latitude; - coordinates_[offset + 1] = longitude; - }); - }); + coordinates_[offset] = static_cast(latitude); + coordinates_[offset + 1] = static_cast(longitude); + }); + }); timer.stop(); logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index 20985537..3056cc03 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -33,7 +33,8 @@ public: } ~Level3RasterViewImpl() { threadPool_.join(); }; - inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; + [[nodiscard]] inline std::uint8_t + RemapDataMoment(std::uint8_t dataMoment) const; boost::asio::thread_pool threadPool_ {1u}; @@ -116,9 +117,9 @@ void Level3RasterView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); - const bool smoothingEnabled = smoothing_enabled(); - p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); - bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -344,10 +345,10 @@ void Level3RasterView::ComputeSweep() { if (!smoothingEnabled) { - constexpr size_t vertexCount = 6; + static constexpr std::size_t vertexCount = 6; // Store data moment value - uint8_t dataValue = dataMomentsArray8[bin]; + const std::uint8_t& dataValue = dataMomentsArray8[bin]; if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) { continue; diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 141441bd..31d47840 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -47,11 +47,12 @@ public: virtual std::uint16_t vcp() const = 0; virtual const std::vector& vertices() const = 0; - std::shared_ptr radar_product_manager() const; - std::chrono::system_clock::time_point selected_time() const; - bool show_smoothed_range_folding() const; - bool smoothing_enabled() const; - std::mutex& sweep_mutex(); + [[nodiscard]] std::shared_ptr + radar_product_manager() const; + [[nodiscard]] std::chrono::system_clock::time_point selected_time() const; + [[nodiscard]] bool show_smoothed_range_folding() const; + [[nodiscard]] bool smoothing_enabled() const; + [[nodiscard]] std::mutex& sweep_mutex(); void set_radar_product_manager( std::shared_ptr radarProductManager); From 80c3cb70d26be0c234f552642397f70567883b56 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Dec 2024 06:25:55 -0600 Subject: [PATCH 272/762] Level2ProductView::Impl is not movable (thread pool is not movable) --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 16d3ab50..559fbc6c 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -114,8 +114,8 @@ public: Impl(const Impl&) = delete; Impl& operator=(const Impl&) = delete; - Impl(Impl&&) noexcept = default; - Impl& operator=(Impl&&) noexcept = default; + Impl(Impl&&) noexcept = delete; + Impl& operator=(Impl&&) noexcept = delete; void ComputeCoordinates( const std::shared_ptr& radarData, From 0c17744bc6b4318dd529d240132f1ee7b340e8d8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Dec 2024 06:26:48 -0600 Subject: [PATCH 273/762] More clang-tidy cleanup --- scwx-qt/source/scwx/qt/settings/product_settings.cpp | 6 +++--- scwx-qt/source/scwx/qt/settings/product_settings.hpp | 6 +++--- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index a265b6df..b9287474 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -42,17 +42,17 @@ ProductSettings::ProductSettings(ProductSettings&&) noexcept = default; ProductSettings& ProductSettings::operator=(ProductSettings&&) noexcept = default; -SettingsVariable& ProductSettings::show_smoothed_range_folding() const +SettingsVariable& ProductSettings::show_smoothed_range_folding() { return p->showSmoothedRangeFolding_; } -SettingsVariable& ProductSettings::sti_forecast_enabled() const +SettingsVariable& ProductSettings::sti_forecast_enabled() { return p->stiForecastEnabled_; } -SettingsVariable& ProductSettings::sti_past_enabled() const +SettingsVariable& ProductSettings::sti_past_enabled() { return p->stiPastEnabled_; } diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index 69abbddb..570c6b15 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -25,9 +25,9 @@ public: ProductSettings(ProductSettings&&) noexcept; ProductSettings& operator=(ProductSettings&&) noexcept; - SettingsVariable& show_smoothed_range_folding() const; - SettingsVariable& sti_forecast_enabled() const; - SettingsVariable& sti_past_enabled() const; + SettingsVariable& show_smoothed_range_folding(); + SettingsVariable& sti_forecast_enabled(); + SettingsVariable& sti_past_enabled(); static ProductSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 559fbc6c..362c209e 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -782,7 +782,7 @@ void Level2ProductView::ComputeSweep() continue; } - std::size_t vertexCount = + const std::size_t vertexCount = (gate > 0) ? kVerticesPerGate_ : kVerticesPerOriginGate_; // Allow pointer arithmetic here, as bounds have already been checked From 88b1d5c2a77f8abcc232577d0f27abc83c1d935c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Dec 2024 07:20:59 -0600 Subject: [PATCH 274/762] Remove unused kDataWordSize16_ --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 362c209e..8dc44ab2 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -25,8 +25,7 @@ static constexpr std::uint32_t kMaxRadialGates_ = common::MAX_0_5_DEGREE_RADIALS * common::MAX_DATA_MOMENT_GATES; static constexpr std::uint32_t kMaxCoordinates_ = kMaxRadialGates_ * 2u; -static constexpr std::uint8_t kDataWordSize8_ = 8u; -static constexpr std::uint8_t kDataWordSize16_ = 16u; +static constexpr std::uint8_t kDataWordSize8_ = 8u; static constexpr std::size_t kVerticesPerGate_ = 6u; static constexpr std::size_t kVerticesPerOriginGate_ = 3u; From f7a1668c3fee8dd8f8a882fbf47aff6070d05f57 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 11:44:24 -0500 Subject: [PATCH 275/762] Add radar site threshold setting --- .../source/scwx/qt/map/radar_site_layer.cpp | 13 +- .../scwx/qt/settings/general_settings.cpp | 15 +- .../scwx/qt/settings/general_settings.hpp | 1 + scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 8 + scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 627 +++++++++--------- 5 files changed, 362 insertions(+), 302 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 33486185..ef1de516 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -59,6 +60,16 @@ void RadarSiteLayer::Initialize() void RadarSiteLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { + p->hoverText_.clear(); + + auto mapDistance = util::maplibre::GetMapDistance(params); + auto threshold = units::length::nautical_miles( + settings::GeneralSettings::Instance().radar_site_threshold().GetValue()); + if (threshold.value() != 0.0 && mapDistance > threshold) + { + return; + } + gl::OpenGLFunctions& gl = context()->gl(); context()->set_render_parameters(params); @@ -73,8 +84,6 @@ void RadarSiteLayer::Render( p->halfWidth_ = params.width * 0.5f; p->halfHeight_ = params.height * 0.5f; - p->hoverText_.clear(); - // Radar site ImGui windows shouldn't have padding ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {0.0f, 0.0f}); diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 3c0ee929..64da33a1 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -78,6 +78,7 @@ public: updateNotificationsEnabled_.SetDefault(true); warningsProvider_.SetDefault(defaultWarningsProviderValue); cursorIconAlwaysOn_.SetDefault(false); + radarSiteThreshold_.SetDefault(0.0); fontSizes_.SetElementMinimum(1); fontSizes_.SetElementMaximum(72); @@ -95,6 +96,8 @@ public: loopTime_.SetMaximum(1440); nmeaBaudRate_.SetMinimum(1); nmeaBaudRate_.SetMaximum(999999999); + radarSiteThreshold_.SetMinimum(0); + radarSiteThreshold_.SetMaximum(999); customStyleDrawLayer_.SetTransform([](const std::string& value) { return boost::trim_copy(value); }); @@ -168,6 +171,7 @@ public: SettingsVariable updateNotificationsEnabled_ {"update_notifications"}; SettingsVariable warningsProvider_ {"warnings_provider"}; SettingsVariable cursorIconAlwaysOn_ {"cursor_icon_always_on"}; + SettingsVariable radarSiteThreshold_ {"radar_site_threshold"}; }; GeneralSettings::GeneralSettings() : @@ -201,7 +205,8 @@ GeneralSettings::GeneralSettings() : &p->trackLocation_, &p->updateNotificationsEnabled_, &p->warningsProvider_, - &p->cursorIconAlwaysOn_}); + &p->cursorIconAlwaysOn_, + &p->radarSiteThreshold_}); SetDefaults(); } GeneralSettings::~GeneralSettings() = default; @@ -356,6 +361,11 @@ SettingsVariable& GeneralSettings::cursor_icon_always_on() const return p->cursorIconAlwaysOn_; } +SettingsVariable& GeneralSettings::radar_site_threshold() const +{ + return p->radarSiteThreshold_; +} + bool GeneralSettings::Shutdown() { bool dataChanged = false; @@ -406,7 +416,8 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->updateNotificationsEnabled_ == rhs.p->updateNotificationsEnabled_ && lhs.p->warningsProvider_ == rhs.p->warningsProvider_ && - lhs.p->cursorIconAlwaysOn_ == rhs.p->cursorIconAlwaysOn_); + lhs.p->cursorIconAlwaysOn_ == rhs.p->cursorIconAlwaysOn_ && + lhs.p->radarSiteThreshold_ == rhs.p->radarSiteThreshold_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 46004c57..46b342d7 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -54,6 +54,7 @@ public: SettingsVariable& update_notifications_enabled() const; SettingsVariable& warnings_provider() const; SettingsVariable& cursor_icon_always_on() const; + SettingsVariable& radar_site_threshold() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 1872be26..9bf589bf 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -133,6 +133,7 @@ public: &nmeaBaudRate_, &nmeaSource_, &warningsProvider_, + &radarSiteThreshold_, &antiAliasingEnabled_, &showMapAttribution_, &showMapCenter_, @@ -249,6 +250,7 @@ public: settings::SettingsInterface theme_ {}; settings::SettingsInterface themeFile_ {}; settings::SettingsInterface warningsProvider_ {}; + settings::SettingsInterface radarSiteThreshold_ {}; settings::SettingsInterface antiAliasingEnabled_ {}; settings::SettingsInterface showMapAttribution_ {}; settings::SettingsInterface showMapCenter_ {}; @@ -749,6 +751,12 @@ void SettingsDialogImpl::SetupGeneralTab() warningsProvider_.SetResetButton(self_->ui->resetWarningsProviderButton); warningsProvider_.EnableTrimming(); + radarSiteThreshold_.SetSettingsVariable( + generalSettings.radar_site_threshold()); + radarSiteThreshold_.SetEditWidget( + self_->ui->radarSiteThresholdSpinBox); + radarSiteThreshold_.SetResetButton(self_->ui->resetRadarSiteThresholdButton); + antiAliasingEnabled_.SetSettingsVariable( generalSettings.anti_aliasing_enabled()); antiAliasingEnabled_.SetEditWidget(self_->ui->antiAliasingEnabledCheckBox); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index ec400682..5423c523 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - -272 - 513 - 702 + -302 + 511 + 733 @@ -159,156 +159,16 @@ 0 - - + + - ... - - - - - - - Mapbox API Key - - - - - - - Custom Map Layer - - - - - - - - - - GPS Plugin - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - - - - ... - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + Grid Width - - - - GPS Source - - - - - - - - - - Warnings Provider - - - - - - - Default Radar Site - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Grid Height - - - @@ -320,20 +180,31 @@ - - - - 1 - - - 999999999 + + + + ... - - - - QLineEdit::EchoMode::Password + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + Mapbox API Key @@ -348,110 +219,8 @@ - - - - - - - GPS Baud Rate - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Map Provider - - - - - - - - - - Theme - - - - - - - MapTiler API Key - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - Custom Map URL - - - - - - - - + + ... @@ -468,33 +237,8 @@ - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Grid Width - - - - - - - QLineEdit::EchoMode::Password - - - - - + + ... @@ -511,18 +255,33 @@ - - - - Default Alert Action + + + + QLineEdit::EchoMode::Password + + + + + + + QLineEdit::EchoMode::Password - - + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + @@ -531,8 +290,72 @@ - - + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + Default Alert Action + + + + + + + MapTiler API Key + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + @@ -541,8 +364,41 @@ - - + + + + GPS Source + + + + + + + + + + Custom Map URL + + + + + + + 1 + + + 999999999 + + + + + + + + + + + ... @@ -552,6 +408,181 @@ + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + Map Provider + + + + + + + Theme + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Warnings Provider + + + + + + + Default Radar Site + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + GPS Baud Rate + + + + + + + Custom Map Layer + + + + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + + + + Grid Height + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + GPS Plugin + + + + + + + + + + Radar Site Threshold + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + 0 + + + 999.000000000000000 + + + QAbstractSpinBox::StepType::DefaultStepType + + + From 74066aa195fb33f19f37068a6bb763001e169820 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 16:11:53 -0500 Subject: [PATCH 276/762] Added inverted threshold for radar sites --- scwx-qt/source/scwx/qt/map/radar_site_layer.cpp | 4 +++- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 4 ++-- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 3 +-- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 7 +++++-- test/data | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index ef1de516..396158f7 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -65,7 +65,9 @@ void RadarSiteLayer::Render( auto mapDistance = util::maplibre::GetMapDistance(params); auto threshold = units::length::nautical_miles( settings::GeneralSettings::Instance().radar_site_threshold().GetValue()); - if (threshold.value() != 0.0 && mapDistance > threshold) + + if (!(threshold.value() == 0.0 || mapDistance <= threshold || + (threshold.value() < 0 && mapDistance >= -threshold))) { return; } diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 64da33a1..bc07926b 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -96,8 +96,8 @@ public: loopTime_.SetMaximum(1440); nmeaBaudRate_.SetMinimum(1); nmeaBaudRate_.SetMaximum(999999999); - radarSiteThreshold_.SetMinimum(0); - radarSiteThreshold_.SetMaximum(999); + radarSiteThreshold_.SetMinimum(-10000); + radarSiteThreshold_.SetMaximum( 10000); customStyleDrawLayer_.SetTransform([](const std::string& value) { return boost::trim_copy(value); }); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 9bf589bf..1d895ffa 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -753,8 +753,7 @@ void SettingsDialogImpl::SetupGeneralTab() radarSiteThreshold_.SetSettingsVariable( generalSettings.radar_site_threshold()); - radarSiteThreshold_.SetEditWidget( - self_->ui->radarSiteThresholdSpinBox); + radarSiteThreshold_.SetEditWidget(self_->ui->radarSiteThresholdSpinBox); radarSiteThreshold_.SetResetButton(self_->ui->resetRadarSiteThresholdButton); antiAliasingEnabled_.SetSettingsVariable( diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 5423c523..bf33a80d 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,7 +135,7 @@ 0 - -302 + -303 511 733 @@ -575,8 +575,11 @@ 0 + + -10000.000000000000000 + - 999.000000000000000 + 10000.000000000000000 QAbstractSpinBox::StepType::DefaultStepType diff --git a/test/data b/test/data index 0eb47590..4b4d9c54 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 0eb475909f9e64ce81e7b8b39420d980b81b3baa +Subproject commit 4b4d9c54b8218aa2297dbd457e3747091570f0d2 From 7dabda88ffea69b52914b86831c9dda3b595082e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 16:19:14 -0500 Subject: [PATCH 277/762] fix formatting for radarSiteThreshold_.SetMaximum --- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index bc07926b..ccd7e886 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -97,7 +97,7 @@ public: nmeaBaudRate_.SetMinimum(1); nmeaBaudRate_.SetMaximum(999999999); radarSiteThreshold_.SetMinimum(-10000); - radarSiteThreshold_.SetMaximum( 10000); + radarSiteThreshold_.SetMaximum(10000); customStyleDrawLayer_.SetTransform([](const std::string& value) { return boost::trim_copy(value); }); From 9174a4de91aa4097b11939ae688ccc2300146492 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 16:34:22 -0500 Subject: [PATCH 278/762] Add NM unit suffix to radar site threshold spin box. --- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index bf33a80d..02fdb206 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -572,6 +572,9 @@ + + NM + 0 From c0b7f852780db04ff3b8a3a9388138a12fb52ecd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 13 Dec 2024 10:24:53 -0500 Subject: [PATCH 279/762] Added tooltips and changed units for radar site threshold. --- .../source/scwx/qt/map/radar_site_layer.cpp | 2 +- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 16 ++++++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 26 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 396158f7..8b78fa4c 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -63,7 +63,7 @@ void RadarSiteLayer::Render( p->hoverText_.clear(); auto mapDistance = util::maplibre::GetMapDistance(params); - auto threshold = units::length::nautical_miles( + auto threshold = units::length::kilometers( settings::GeneralSettings::Instance().radar_site_threshold().GetValue()); if (!(threshold.value() == 0.0 || mapDistance <= threshold || diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 1d895ffa..f4956170 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -755,6 +755,22 @@ void SettingsDialogImpl::SetupGeneralTab() generalSettings.radar_site_threshold()); radarSiteThreshold_.SetEditWidget(self_->ui->radarSiteThresholdSpinBox); radarSiteThreshold_.SetResetButton(self_->ui->resetRadarSiteThresholdButton); + radarSiteThreshold_.SetUnitLabel(self_->ui->radarSiteThresholdUnitLabel); + auto radarSiteThresholdUpdateUnits = [this](const std::string& newValue) + { + types::DistanceUnits radiusUnits = + types::GetDistanceUnitsFromName(newValue); + double radiusScale = types::GetDistanceUnitsScale(radiusUnits); + std::string abbreviation = + types::GetDistanceUnitsAbbreviation(radiusUnits); + + radarSiteThreshold_.SetUnit(radiusScale, abbreviation); + }; + settings::UnitSettings::Instance() + .distance_units() + .RegisterValueStagedCallback(radarSiteThresholdUpdateUnits); + radarSiteThresholdUpdateUnits( + settings::UnitSettings::Instance().distance_units().GetValue()); antiAliasingEnabled_.SetSettingsVariable( generalSettings.anti_aliasing_enabled()); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 02fdb206..be599bff 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 @@ -572,8 +572,17 @@ + + + 0 + 0 + + + + Set to 0 to disable + - NM + 0 @@ -589,6 +598,16 @@ + + + + + + + + + + @@ -822,6 +841,9 @@ + + Set to 0 to disable + 2 From 9d0c1c539ccdc4d46687594371c7d146a99bde6f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 13 Dec 2024 10:29:54 -0500 Subject: [PATCH 280/762] add const for set unit callbacks --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index f4956170..b3cbb427 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -758,10 +758,10 @@ void SettingsDialogImpl::SetupGeneralTab() radarSiteThreshold_.SetUnitLabel(self_->ui->radarSiteThresholdUnitLabel); auto radarSiteThresholdUpdateUnits = [this](const std::string& newValue) { - types::DistanceUnits radiusUnits = + const types::DistanceUnits radiusUnits = types::GetDistanceUnitsFromName(newValue); - double radiusScale = types::GetDistanceUnitsScale(radiusUnits); - std::string abbreviation = + const double radiusScale = types::GetDistanceUnitsScale(radiusUnits); + const std::string abbreviation = types::GetDistanceUnitsAbbreviation(radiusUnits); radarSiteThreshold_.SetUnit(radiusScale, abbreviation); @@ -1082,10 +1082,10 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioRadius_.SetUnitLabel(self_->ui->alertAudioRadiusUnitsLabel); auto alertAudioRadiusUpdateUnits = [this](const std::string& newValue) { - types::DistanceUnits radiusUnits = + const types::DistanceUnits radiusUnits = types::GetDistanceUnitsFromName(newValue); - double radiusScale = types::GetDistanceUnitsScale(radiusUnits); - std::string abbreviation = + const double radiusScale = types::GetDistanceUnitsScale(radiusUnits); + const std::string abbreviation = types::GetDistanceUnitsAbbreviation(radiusUnits); alertAudioRadius_.SetUnit(radiusScale, abbreviation); From ecf7579d8e9d6194e0840c88eb65e5519bcefdb1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Dec 2024 09:31:55 -0500 Subject: [PATCH 281/762] Add inverted threshold to mouse picking for placefiles. --- scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp | 6 ++++-- scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp index abc852f8..b321e149 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -737,8 +737,10 @@ bool PlacefileIcons::RunMousePicking( units::length::nautical_miles {icon.di_->threshold_} .value())) < 999 && - // Map distance is beyond the threshold - icon.di_->threshold_ < mapDistance) || + // Map distance is beyond/within the threshold + icon.di_->threshold_ < mapDistance && + (icon.di_->threshold_.value() >= 0.0 || + -(icon.di_->threshold_) > mapDistance)) || ( // Line has a start time diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index 58b86c37..6ec2750a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -544,8 +544,10 @@ bool PlacefileLines::RunMousePicking( units::length::nautical_miles {line.di_->threshold_} .value())) < 999 && - // Map distance is beyond the threshold - line.di_->threshold_ < mapDistance) || + // Map distance is beyond/within the threshold + line.di_->threshold_ < mapDistance && + (line.di_->threshold_.value() >= 0.0 || + -(line.di_->threshold_) > mapDistance)) || ( // Line has a start time From 27f8c1f56c10aec5bde1be1fd2f7982c2a527f67 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Dec 2024 09:38:14 -0500 Subject: [PATCH 282/762] Add inverted threshold to mouse picking for geo icons/lines --- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 6 ++++-- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 622718e3..f991893f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -947,8 +947,10 @@ bool GeoIcons::RunMousePicking( units::length::nautical_miles {icon.di_->threshold_} .value())) < 999 && - // Map distance is beyond the threshold - icon.di_->threshold_ < mapDistance) || + // Map distance is beyond/within the threshold + icon.di_->threshold_ < mapDistance && + (icon.di_->threshold_.value() >= 0.0 || + -(icon.di_->threshold_) > mapDistance)) || ( // Geo icon has a start time diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index d9f2c023..4b5fc05f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -740,8 +740,10 @@ bool GeoLines::RunMousePicking( units::length::nautical_miles {line.di_->threshold_} .value())) < 999 && - // Map distance is beyond the threshold - line.di_->threshold_ < mapDistance) || + // Map distance is beyond/within the threshold + line.di_->threshold_ < mapDistance && + (line.di_->threshold_.value() >= 0.0 || + -(line.di_->threshold_) > mapDistance)) || ( // Line has a start time From 9a171464ff23de26fab329b76b5afc7d85e7b03e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 25 Dec 2024 21:34:08 -0600 Subject: [PATCH 283/762] In the shader used by GeoLines, aDisplayed is an integer, not a float --- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index d9f2c023..ef5ddba5 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -230,12 +230,11 @@ void GeoLines::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed - gl.glVertexAttribPointer(7, - 1, - GL_INT, - GL_FALSE, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(3 * sizeof(float))); + gl.glVertexAttribIPointer(7, + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(3 * sizeof(GLint))); gl.glEnableVertexAttribArray(7); p->dirty_ = true; From 983e9b467d4c105300876423db3076cc71a12309 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 26 Dec 2024 12:11:55 -0600 Subject: [PATCH 284/762] Apply GeoLines aDisplayed fix to GeoIcons --- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index f991893f..9e2a8d17 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -256,12 +256,11 @@ void GeoIcons::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed - gl.glVertexAttribPointer(7, - 1, - GL_INT, - GL_FALSE, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(3 * sizeof(float))); + gl.glVertexAttribIPointer(7, + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(3 * sizeof(GLint))); gl.glEnableVertexAttribArray(7); p->dirty_ = true; From f34a3e2145acd513dfa62ff55fb7742a0cb4ae61 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 31 Dec 2024 12:50:20 -0500 Subject: [PATCH 285/762] Add check for symbolic font in fontconfig code --- .../source/scwx/qt/manager/font_manager.cpp | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 72779f67..89a1643f 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -472,6 +472,7 @@ FontManager::Impl::MatchFontFile(const std::string& family, FcPatternAddString(pattern, FC_FONTFORMAT, reinterpret_cast(kFcTrueType_.c_str())); + FcPatternAddBool(pattern, FC_SYMBOL, FcFalse); if (!styles.empty()) { @@ -485,29 +486,51 @@ FontManager::Impl::MatchFontFile(const std::string& family, FcDefaultSubstitute(pattern); // Find matching font - FcResult result; - FcPattern* match = FcFontMatch(nullptr, pattern, &result); + FcResult result {}; + FcFontSet* matches = FcFontSort(nullptr, pattern, FcFalse, nullptr, &result); FontRecord record {}; - if (match != nullptr) + if (matches != nullptr) { - FcChar8* fcFamily; - FcChar8* fcStyle; - FcChar8* fcFile; - - // Match was found, get properties - if (FcPatternGetString(match, FC_FAMILY, 0, &fcFamily) == FcResultMatch && - FcPatternGetString(match, FC_STYLE, 0, &fcStyle) == FcResultMatch && - FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch) + for (int i = 0; i < matches->nfont; i++) { - record.family_ = reinterpret_cast(fcFamily); - record.style_ = reinterpret_cast(fcStyle); - record.filename_ = reinterpret_cast(fcFile); + FcPattern* match = + // Using C code requires pointer arithmetic + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + FcFontRenderPrepare(nullptr, pattern, matches->fonts[i]); + if (match == nullptr) + { + continue; + } + FcChar8* fcFamily = nullptr; + FcChar8* fcStyle = nullptr; + FcChar8* fcFile = nullptr; + FcBool fcSymbol = FcFalse; - logger_->debug("Found matching font: {}:{} ({})", - record.family_, - record.style_, - record.filename_); + // Match was found, get properties + if (FcPatternGetString(match, FC_FAMILY, 0, &fcFamily) == + FcResultMatch && + FcPatternGetString(match, FC_STYLE, 0, &fcStyle) == + FcResultMatch && + FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch && + FcPatternGetBool(match, FC_SYMBOL, 0, &fcSymbol) == + FcResultMatch && + fcSymbol == FcFalse /*Must check fcSymbol manually*/) + { + record.family_ = reinterpret_cast(fcFamily); + record.style_ = reinterpret_cast(fcStyle); + record.filename_ = reinterpret_cast(fcFile); + + logger_->debug("Found matching font: {}:{} ({}) {}", + record.family_, + record.style_, + record.filename_, + fcSymbol); + FcPatternDestroy(match); + break; + } + + FcPatternDestroy(match); } } @@ -517,7 +540,7 @@ FontManager::Impl::MatchFontFile(const std::string& family, } // Cleanup - FcPatternDestroy(match); + FcFontSetDestroy(matches); FcPatternDestroy(pattern); return record; From f7a55ec85b7a64468869819240ffd2ca299f095f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 31 Dec 2024 13:03:12 -0500 Subject: [PATCH 286/762] Add logging for unloaded fonts in case a similar error happens again. --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index df45024a..d569f27e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1619,6 +1619,11 @@ void MapWidgetImpl::ImGuiCheckFonts() ImGui_ImplOpenGL3_CreateFontsTexture(); } + if (!model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + { + logger_->error("ImGui font atlas could not be built."); + } + imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; } From 0681b523feaba46e011f963e58726c3fcb7b5277 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 4 Jan 2025 23:17:35 +0000 Subject: [PATCH 287/762] Update dependency libcurl to v8.11.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 52d59c41..b40f5458 100644 --- a/conanfile.py +++ b/conanfile.py @@ -14,7 +14,7 @@ class SupercellWxConan(ConanFile): "glew/2.2.0", "glm/cci.20230113", "gtest/1.15.0", - "libcurl/8.10.1", + "libcurl/8.11.1", "libpng/1.6.44", "libxml2/2.12.7", "openssl/3.3.2", From 097df7894ea255d3a948f4563aff2f86ce84d065 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:21:24 +0000 Subject: [PATCH 288/762] Update dependency cpr to v1.11.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index b40f5458..b150acc5 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,7 @@ import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.86.0", - "cpr/1.11.0", + "cpr/1.11.1", "fontconfig/2.15.0", "freetype/2.13.2", "geographiclib/2.4", From 517013633d09efc120364fb3c23fdab43afc43be Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 6 Jan 2025 05:10:23 +0000 Subject: [PATCH 289/762] Use schannel with libcurl --- conanfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conanfile.py b/conanfile.py index b150acc5..71137d47 100644 --- a/conanfile.py +++ b/conanfile.py @@ -29,6 +29,10 @@ class SupercellWxConan(ConanFile): "openssl/*:no_module": True, "openssl/*:shared" : True} + def configure(self): + if self.settings.os == "Windows": + self.options["libcurl"].with_ssl = "schannel" + def requirements(self): if self.settings.os == "Linux": self.requires("onetbb/2021.12.0") From f1567161732d1f27cfab361aa06316ff7d6db53b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 6 Jan 2025 06:14:09 +0000 Subject: [PATCH 290/762] Add OpenSSL::Crypto as a dependency for wxdata --- wxdata/wxdata.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index ddd998f1..94b0e3a7 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -5,6 +5,7 @@ project(scwx-data) find_package(Boost) find_package(cpr) find_package(LibXml2) +find_package(OpenSSL) find_package(re2) find_package(spdlog) @@ -291,6 +292,7 @@ target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core aws-cpp-sdk-s3 cpr::cpr LibXml2::LibXml2 + OpenSSL::Crypto re2::re2 spdlog::spdlog units::units) From b1b06bc125d8a55441aad6b106f3af89284101bd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 6 Jan 2025 12:42:18 -0600 Subject: [PATCH 291/762] Disable placefile refresh on destruction --- scwx-qt/source/scwx/qt/manager/placefile_manager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index 58049fc6..df412e24 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -93,6 +93,7 @@ public: { std::unique_lock refreshLock(refreshMutex_); std::unique_lock timerLock(timerMutex_); + enabled_ = false; refreshTimer_.cancel(); timerLock.unlock(); refreshLock.unlock(); From 8ec983b654e2b7fc423869244214b2cb46a8a131 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:17:15 +0000 Subject: [PATCH 292/762] Update dependency libpng to v1.6.45 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 71137d47..96725d5c 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "glm/cci.20230113", "gtest/1.15.0", "libcurl/8.11.1", - "libpng/1.6.44", + "libpng/1.6.45", "libxml2/2.12.7", "openssl/3.3.2", "re2/20240702", From 2e973bf6549f8bc0ecea148f7632b6946a9e5ff8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 10 Jan 2025 09:57:38 -0500 Subject: [PATCH 293/762] Modified font atlas not build logging to only log once --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d569f27e..ab7b0cf6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1619,9 +1619,12 @@ void MapWidgetImpl::ImGuiCheckFonts() ImGui_ImplOpenGL3_CreateFontsTexture(); } - if (!model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + static bool haveLogged = false; + if (!model::ImGuiContextModel::Instance().font_atlas()->IsBuilt() && + !haveLogged) { logger_->error("ImGui font atlas could not be built."); + haveLogged = true; } imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; From b5241ff1f72767660b641d0419b04fe3c670a5a4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 10 Jan 2025 19:23:47 +0000 Subject: [PATCH 294/762] Legacy OpenSSL support is not needed as of Qt 6.5 --- conanfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 96725d5c..bbca7d1e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -26,7 +26,6 @@ class SupercellWxConan(ConanFile): generators = ("CMakeDeps") default_options = {"geos/*:shared" : True, "libiconv/*:shared" : True, - "openssl/*:no_module": True, "openssl/*:shared" : True} def configure(self): From 432d794626e296352c241bfc99824ea75cd55231 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 10 Jan 2025 19:27:57 +0000 Subject: [PATCH 295/762] As of Qt 6.2, schannel is available as a TLS backend, OpenSSL is not required --- conanfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index bbca7d1e..edc03b3c 100644 --- a/conanfile.py +++ b/conanfile.py @@ -25,12 +25,13 @@ class SupercellWxConan(ConanFile): "zlib/1.3.1") generators = ("CMakeDeps") default_options = {"geos/*:shared" : True, - "libiconv/*:shared" : True, - "openssl/*:shared" : True} + "libiconv/*:shared" : True} def configure(self): if self.settings.os == "Windows": self.options["libcurl"].with_ssl = "schannel" + elif self.settings.os == "Linux": + self.options["openssl"].shared = True def requirements(self): if self.settings.os == "Linux": From 4ba0c5e3ba42305a3d352be69647a39703bcf435 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 10 Jan 2025 23:40:27 -0600 Subject: [PATCH 296/762] Update conan dependencies to latest, missed by renovate --- conanfile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/conanfile.py b/conanfile.py index edc03b3c..10c6e716 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,20 +12,20 @@ class SupercellWxConan(ConanFile): "geographiclib/2.4", "geos/3.13.0", "glew/2.2.0", - "glm/cci.20230113", + "glm/1.0.1", "gtest/1.15.0", "libcurl/8.11.1", "libpng/1.6.45", - "libxml2/2.12.7", + "libxml2/2.13.4", "openssl/3.3.2", "re2/20240702", "spdlog/1.15.0", - "sqlite3/3.47.1", - "vulkan-loader/1.3.243.0", + "sqlite3/3.47.2", + "vulkan-loader/1.3.290.0", "zlib/1.3.1") generators = ("CMakeDeps") - default_options = {"geos/*:shared" : True, - "libiconv/*:shared" : True} + default_options = {"geos/*:shared" : True, + "libiconv/*:shared" : True} def configure(self): if self.settings.os == "Windows": From 5be46de20a8d31203717a4a429b041127689ae6e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 11 Jan 2025 00:06:17 -0600 Subject: [PATCH 297/762] Update aws-sdk-cpp to 1.11.483 --- external/aws-sdk-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/aws-sdk-cpp b/external/aws-sdk-cpp index c95e71a9..8e41fd05 160000 --- a/external/aws-sdk-cpp +++ b/external/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit c95e71a9d23bba0c2f6c6a7bc37ae63b7351e8b7 +Subproject commit 8e41fd058ce06a3f7b67d657a7f644f7a38af509 From c7be1d37e0830fedf5836ec344ac8e65de2ed24d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 11 Jan 2025 00:06:50 -0600 Subject: [PATCH 298/762] Update stb to 5c20573 (2024-11-09) --- external/stb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/stb b/external/stb index beebb24b..5c205738 160000 --- a/external/stb +++ b/external/stb @@ -1 +1 @@ -Subproject commit beebb24b945efdea3b9bba23affb8eb3ba8982e7 +Subproject commit 5c205738c191bcb0abc65c4febfa9bd25ff35234 From a792538e296bc4c58802b868b4529894aa7eec6b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 11 Jan 2025 01:06:54 -0600 Subject: [PATCH 299/762] Rebase maplibre-native updates onto 9f02e29 (2025-01-10) --- external/maplibre-native | 2 +- external/maplibre-native-qt | 2 +- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/external/maplibre-native b/external/maplibre-native index 3d4ca3fd..a77cb6b2 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit 3d4ca3fdf07c50db3002b11bff93c81ec380e493 +Subproject commit a77cb6b2d1cde3a5f2c147e25245aa019aacb74b diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt index 805ccf62..3939a82a 160000 --- a/external/maplibre-native-qt +++ b/external/maplibre-native-qt @@ -1 +1 @@ -Subproject commit 805ccf6204a546e43fed599631ad5d698f68ae86 +Subproject commit 3939a82ab9c2ff8bc2746e58ab9727cd12caef2c diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ab7b0cf6..bfe0908c 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1573,8 +1573,8 @@ void MapWidget::paintGL() // Render QMapLibre Map p->map_->resize(size()); - p->map_->setFramebufferObject(defaultFramebufferObject(), - size() * pixelRatio()); + p->map_->setOpenGLFramebufferObject(defaultFramebufferObject(), + size() * pixelRatio()); p->map_->render(); // Perform mouse picking From c78fd097a1e122ea4a543609ccb8c72cddc2eee9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 11 Jan 2025 01:24:34 -0600 Subject: [PATCH 300/762] Adding Ocean and Toner MapTiler styles --- scwx-qt/source/scwx/qt/map/map_provider.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index 4b9964f9..4a6b7fb7 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -149,6 +149,9 @@ static const std::unordered_map mapProviderInfo_ { {.name_ {"Landscape"}, .url_ {"https://api.maptiler.com/maps/landscape/style.json"}, .drawBelow_ {"Runway"}}, + {.name_ {"Ocean"}, + .url_ {"https://api.maptiler.com/maps/ocean/style.json"}, + .drawBelow_ {"Landform labels"}}, {.name_ {"Outdoor"}, .url_ {"https://api.maptiler.com/maps/outdoor-v2/style.json"}, .drawBelow_ {"aeroway_runway", "Aeroway"}}, @@ -167,6 +170,9 @@ static const std::unordered_map mapProviderInfo_ { .url_ {"https://api.maptiler.com/maps/ch-swisstopo-lbm-vivid/" "style.json"}, .drawBelow_ {"pattern_landcover_vineyard", "Vineyard pattern"}}, + {.name_ {"Toner"}, + .url_ {"https://api.maptiler.com/maps/toner-v2/style.json"}, + .drawBelow_ {"Bridge"}}, {.name_ {"Topo"}, .url_ {"https://api.maptiler.com/maps/topo-v2/style.json"}, .drawBelow_ {"aeroway_runway", "Runway"}}, From 84d88f5040e3537258645335e355c0304ae832a0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 11 Jan 2025 01:27:01 -0600 Subject: [PATCH 301/762] Don't run clang-tidy-comments on a canceled job --- .github/workflows/clang-tidy-comments.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/clang-tidy-comments.yml b/.github/workflows/clang-tidy-comments.yml index accdbeca..7328430f 100644 --- a/.github/workflows/clang-tidy-comments.yml +++ b/.github/workflows/clang-tidy-comments.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion != 'cancelled' }} steps: - name: Post Comments From c73698619df5397702d35569a1f3432e9be8a130 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 10:35:55 -0500 Subject: [PATCH 302/762] Use name as hover text for marker --- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 0480a63e..4e1a2dd0 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -61,6 +61,7 @@ void MarkerLayer::Impl::ReloadMarkers() std::shared_ptr icon = geoIcons_->AddIcon(); geoIcons_->SetIconTexture(icon, markerIconName_, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); + geoIcons_->SetIconHoverText(icon, marker.name); }); geoIcons_->FinishIcons(); From daa5bd24dc7082d7d952e1f62ef6e60f30e37f0f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 10:49:27 -0500 Subject: [PATCH 303/762] Add q_color_modulate for modulating the color of QIcons --- scwx-qt/scwx-qt.cmake | 2 + .../source/scwx/qt/util/q_color_modulate.cpp | 56 +++++++++++++++++++ .../source/scwx/qt/util/q_color_modulate.hpp | 22 ++++++++ 3 files changed, 80 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/util/q_color_modulate.cpp create mode 100644 scwx-qt/source/scwx/qt/util/q_color_modulate.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1c654b0e..54703605 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -357,6 +357,7 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/network.hpp source/scwx/qt/util/streams.hpp source/scwx/qt/util/texture_atlas.hpp + source/scwx/qt/util/q_color_modulate.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp source/scwx/qt/util/time.hpp @@ -369,6 +370,7 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/maplibre.cpp source/scwx/qt/util/network.cpp source/scwx/qt/util/texture_atlas.cpp + source/scwx/qt/util/q_color_modulate.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp source/scwx/qt/util/time.cpp diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp new file mode 100644 index 00000000..567fc62d --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp @@ -0,0 +1,56 @@ +#include + +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +void modulateColors_(QImage& image, const QColor& color) +{ + for (int y = 0; y < image.height(); ++y) + { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = 0; x < image.width(); ++x) + { + QRgb& rgb = line[x]; + int red = qRed(rgb) * color.redF(); + int green = qGreen(rgb) * color.greenF(); + int blue = qBlue(rgb) * color.blueF(); + int alpha = qAlpha(rgb) * color.alphaF(); + + rgb = qRgba(red, green, blue, alpha); + } + } +} + +QImage modulateColors(const QImage& image, const QColor& color) +{ + QImage copy = image.copy(); + modulateColors_(copy, color); + return copy; +} + +QPixmap modulateColors(const QPixmap& pixmap, const QColor& color) +{ + QImage image = pixmap.toImage(); + modulateColors_(image, color); + return QPixmap::fromImage(image); +} + +QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color) +{ + QPixmap pixmap = modulateColors(icon.pixmap(size), color); + return QIcon(pixmap); +} + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp new file mode 100644 index 00000000..c54b852f --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace util +{ + +QImage modulateColors(const QImage& image, const QColor& color); +QPixmap modulateColors(const QPixmap& pixmap, const QColor& color); +QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color); + +} // namespace util +} // namespace qt +} // namespace scwx From 1dce1b2b35972f53195cbf0480b9b65c42f430b9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 11:07:37 -0500 Subject: [PATCH 304/762] Added new icons for location markers. All icons are white, so they can be color modulated to the correct color. --- .../icons/font-awesome-6/briefcase-solid.svg | 1 + .../font-awesome-6/building-columns-solid.svg | 1 + .../icons/font-awesome-6/caravan-solid.svg | 1 + .../font-awesome-6/house-solid-white.svg | 1 + .../location-crosshairs-solid.svg | 1 + .../res/icons/font-awesome-6/location-pin.svg | 4 +++ .../icons/font-awesome-6/star-solid-white.svg | 1 + .../res/icons/font-awesome-6/tent-solid.svg | 1 + .../res/textures/images/location-marker.svg | 2 +- scwx-qt/scwx-qt.qrc | 8 ++++++ .../source/scwx/qt/types/texture_types.cpp | 27 +++++++++++++++++++ .../source/scwx/qt/types/texture_types.hpp | 9 +++++++ 12 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/caravan-solid.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/house-solid-white.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/location-pin.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/star-solid-white.svg create mode 100644 scwx-qt/res/icons/font-awesome-6/tent-solid.svg diff --git a/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg new file mode 100644 index 00000000..b16bc330 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/briefcase-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg new file mode 100644 index 00000000..cf0df19a --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/building-columns-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg new file mode 100644 index 00000000..c341214f --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/caravan-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg new file mode 100644 index 00000000..59f65e1e --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/house-solid-white.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg new file mode 100644 index 00000000..5bb1ea5c --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/location-crosshairs-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/location-pin.svg b/scwx-qt/res/icons/font-awesome-6/location-pin.svg new file mode 100644 index 00000000..4b6182cd --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/location-pin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg new file mode 100644 index 00000000..41bcd103 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/star-solid-white.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/icons/font-awesome-6/tent-solid.svg b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg new file mode 100644 index 00000000..9f159d60 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/tent-solid.svg @@ -0,0 +1 @@ + diff --git a/scwx-qt/res/textures/images/location-marker.svg b/scwx-qt/res/textures/images/location-marker.svg index 8ebb064f..3eef9d9e 100644 --- a/scwx-qt/res/textures/images/location-marker.svg +++ b/scwx-qt/res/textures/images/location-marker.svg @@ -6,6 +6,6 @@ + stroke="#ffffff" stroke-width="20" fill="none"/> diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index cca0f62c..ee3fcc8e 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -32,6 +32,9 @@ res/icons/font-awesome-6/angles-up-solid.svg res/icons/font-awesome-6/backward-step-solid.svg res/icons/font-awesome-6/book-solid.svg + res/icons/font-awesome-6/briefcase-solid.svg + res/icons/font-awesome-6/building-solid.svg + res/icons/font-awesome-6/caravan-solid.svg res/icons/font-awesome-6/copy-regular.svg res/icons/font-awesome-6/discord.svg res/icons/font-awesome-6/earth-americas-solid.svg @@ -40,8 +43,11 @@ 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/house-solid-white.svg res/icons/font-awesome-6/keyboard-regular.svg res/icons/font-awesome-6/layer-group-solid.svg + res/icons/font-awesome-6/location-crosshairs-solid.svg + res/icons/font-awesome-6/location-pin.svg res/icons/font-awesome-6/palette-solid.svg res/icons/font-awesome-6/pause-solid.svg res/icons/font-awesome-6/play-solid.svg @@ -53,7 +59,9 @@ 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/star-solid-white.svg res/icons/font-awesome-6/stop-solid.svg + res/icons/font-awesome-6/tent-solid.svg res/icons/font-awesome-6/volume-high-solid.svg res/palettes/wct/CC.pal res/palettes/wct/Default16.pal diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp index 7f0c7a24..336a26d8 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.cpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -25,8 +25,35 @@ static const std::unordered_map imageTextureInfo_ { {ImageTexture::Cursor17, {"images/cursor-17", ":/res/textures/images/cursor-17.png"}}, {ImageTexture::Dot3, {"images/dot-3", ":/res/textures/images/dot-3.png"}}, + {ImageTexture::LocationBriefcase, + {"images/location-briefcase", + ":/res/icons/font-awesome-6/briefcase-solid.svg"}}, + {ImageTexture::LocationBuildingColumns, + {"images/location-building-columns", + ":/res/icons/font-awesome-6/building-columns-solid.svg"}}, + {ImageTexture::LocationBuilding, + {"images/location-building", + ":/res/icons/font-awesome-6/building-solid.svg"}}, + {ImageTexture::LocationCaravan, + {"images/location-caravan", + ":/res/icons/font-awesome-6/caravan-solid.svg"}}, + {ImageTexture::LocationCrosshair, + {"images/location-crosshair", + ":/res/icons/font-awesome-6/location-crosshairs-solid.svg"}}, + {ImageTexture::LocationHouse, + {"images/location-house", + ":/res/icons/font-awesome-6/house-solid-white.svg"}}, {ImageTexture::LocationMarker, {"images/location-marker", ":/res/textures/images/location-marker.svg"}}, + {ImageTexture::LocationPin, + {"images/location-pin", + ":/res/icons/font-awesome-6/location-pin.svg"}}, + {ImageTexture::LocationStar, + {"images/location-star", + ":/res/icons/font-awesome-6/star-solid-white.svg"}}, + {ImageTexture::LocationTent, + {"images/location-tent", + ":/res/icons/font-awesome-6/tent-solid.svg"}}, {ImageTexture::MapboxLogo, {"images/mapbox-logo", ":/res/textures/images/mapbox-logo.svg"}}, {ImageTexture::MapTilerLogo, diff --git a/scwx-qt/source/scwx/qt/types/texture_types.hpp b/scwx-qt/source/scwx/qt/types/texture_types.hpp index 307a7638..d5eabc4a 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.hpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.hpp @@ -18,7 +18,16 @@ enum class ImageTexture Crosshairs24, Cursor17, Dot3, + LocationBriefcase, + LocationBuildingColumns, + LocationBuilding, + LocationCaravan, + LocationCrosshair, + LocationHouse, LocationMarker, + LocationPin, + LocationStar, + LocationTent, MapboxLogo, MapTilerLogo }; From 875fb8a8c97c733a2b3d4c75e7efd4793b5f5f72 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 11:14:32 -0500 Subject: [PATCH 305/762] Added building-solid.svg icon (missed in last commit) --- scwx-qt/res/icons/font-awesome-6/building-solid.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 scwx-qt/res/icons/font-awesome-6/building-solid.svg diff --git a/scwx-qt/res/icons/font-awesome-6/building-solid.svg b/scwx-qt/res/icons/font-awesome-6/building-solid.svg new file mode 100644 index 00000000..6f6d3f24 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/building-solid.svg @@ -0,0 +1 @@ + From 7ed89fdd5d4c5ea3a80902fa42299f311019acb7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 11:17:31 -0500 Subject: [PATCH 306/762] added marker_types code for dealing with new marker icons --- scwx-qt/scwx-qt.cmake | 3 +- scwx-qt/source/scwx/qt/types/marker_types.cpp | 34 +++++++++++++++++++ scwx-qt/source/scwx/qt/types/marker_types.hpp | 25 ++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/source/scwx/qt/types/marker_types.cpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 54703605..7aca4dff 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -222,8 +222,8 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.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/marker_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 @@ -239,6 +239,7 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.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/marker_types.cpp source/scwx/qt/types/media_types.cpp source/scwx/qt/types/qt_types.cpp source/scwx/qt/types/radar_product_record.cpp diff --git a/scwx-qt/source/scwx/qt/types/marker_types.cpp b/scwx-qt/source/scwx/qt/types/marker_types.cpp new file mode 100644 index 00000000..ce63490a --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/marker_types.cpp @@ -0,0 +1,34 @@ +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +const std::vector& getMarkerIcons() +{ + static std::vector markerIcons = {}; + if (markerIcons.size() == 0) + { + markerIcons = { + MarkerIconInfo(types::ImageTexture::LocationMarker, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationPin, 6, 16), + MarkerIconInfo(types::ImageTexture::LocationCrosshair, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationStar, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationBriefcase, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationBuildingColumns, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationBuilding, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationCaravan, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationHouse, -1, -1), + MarkerIconInfo(types::ImageTexture::LocationTent, -1, -1), + }; + } + + return markerIcons; +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index e3d28e26..80f7430f 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -1,8 +1,12 @@ #pragma once +#include + #include #include +#include + namespace scwx { namespace qt @@ -24,6 +28,27 @@ struct MarkerInfo double longitude; }; +struct MarkerIconInfo { + explicit MarkerIconInfo(types::ImageTexture texture, + std::int32_t hotX, + std::int32_t hotY) : + name{types::GetTextureName(texture)}, + path{types::GetTexturePath(texture)}, + hotX{hotX}, + hotY{hotY}, + qIcon{QIcon(QString::fromStdString(path))} + { + } + + std::string name; + std::string path; + std::int32_t hotX; + std::int32_t hotY; + QIcon qIcon; +}; + +const std::vector& getMarkerIcons(); + } // namespace types } // namespace qt } // namespace scwx From 6da34fc151d29e17c9859ec65bfbffebb05ecd13 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 11:50:58 -0500 Subject: [PATCH 307/762] Added location marker icon support to marker_types and marker_manager --- .../source/scwx/qt/manager/marker_manager.cpp | 49 ++++++++++++++++--- scwx-qt/source/scwx/qt/types/marker_types.hpp | 23 ++++++--- .../scwx/qt/ui/marker_settings_widget.cpp | 9 +++- 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 99208a10..2fece570 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -27,6 +28,12 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string kNameName_ = "name"; static const std::string kLatitudeName_ = "latitude"; static const std::string kLongitudeName_ = "longitude"; +static const std::string kIconName_ = "icon"; +static const std::string kIconColorName_ = "icon-color"; + +static const std::string defaultIconName = types::getMarkerIcons()[0].name; +static const boost::gil::rgba8_pixel_t defaultIconColor = + util::color::ToRgba8PixelT("#ffff0000"); class MarkerManager::Impl { @@ -59,10 +66,7 @@ public: class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(const std::string& name, double latitude, double longitude) : - markerInfo_ {types::MarkerInfo(name, latitude, longitude)} - { - } + MarkerRecord(const types::MarkerInfo& info) : markerInfo_ {info} { @@ -81,16 +85,47 @@ public: { jv = {{kNameName_, record->markerInfo_.name}, {kLatitudeName_, record->markerInfo_.latitude}, - {kLongitudeName_, record->markerInfo_.longitude}}; + {kLongitudeName_, record->markerInfo_.longitude}, + {kIconName_, record->markerInfo_.iconName}, + {kIconColorName_, util::color::ToArgbString(record->markerInfo_.iconColor)}}; } + friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - return MarkerRecord( + + const boost::json::object& jo = jv.as_object(); + + std::string iconName = defaultIconName; + boost::gil::rgba8_pixel_t iconColor = defaultIconColor; + + if (jo.contains(kIconName_) && jo.at(kIconName_).is_string()) + { + iconName = boost::json::value_to(jv.at(kIconName_)); + } + + if (jo.contains(kIconColorName_) && jo.at(kIconName_).is_string()) + { + try { + iconColor = util::color::ToRgba8PixelT( + boost::json::value_to(jv.at(kIconColorName_))); + } + catch (const std::exception& ex) + { + logger_->warn( + "Could not parse color value in location-markers.json with the " + "following exception: {}", + ex.what()); + } + } + + return MarkerRecord(types::MarkerInfo( boost::json::value_to(jv.at(kNameName_)), boost::json::value_to(jv.at(kLatitudeName_)), - boost::json::value_to(jv.at(kLongitudeName_))); + boost::json::value_to(jv.at(kLongitudeName_)), + iconName, + iconColor)); } }; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 80f7430f..2d1aa987 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace scwx @@ -17,15 +18,25 @@ typedef std::uint64_t MarkerId; struct MarkerInfo { - MarkerInfo(const std::string& name, double latitude, double longitude) : - name {name}, latitude {latitude}, longitude {longitude} + MarkerInfo(const std::string& name, + double latitude, + double longitude, + const std::string iconName, + boost::gil::rgba8_pixel_t iconColor) : + name {name}, + latitude {latitude}, + longitude {longitude}, + iconName {iconName}, + iconColor {iconColor} { } - MarkerId id; - std::string name; - double latitude; - double longitude; + MarkerId id; + std::string name; + double latitude; + double longitude; + std::string iconName; + boost::gil::rgba8_pixel_t iconColor; }; struct MarkerIconInfo { diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index 0c2bc614..6a1e04f3 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -7,6 +7,8 @@ #include #include +#include + #include namespace scwx @@ -63,7 +65,12 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this]() { - markerManager_->add_marker(types::MarkerInfo("", 0, 0)); + markerManager_->add_marker(types::MarkerInfo( + "", + 0, + 0, + types::getMarkerIcons()[0].name, + util::color::ToRgba8PixelT("#ffff0000"))); }); QObject::connect( self_->ui->removeButton, From 1a32748b8e0db88e52572a5f3c0975e9075c05fe Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 12:00:36 -0500 Subject: [PATCH 308/762] Add location marker icon and color rendering on the map. --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 6 +++--- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 2fece570..30cb0d4b 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -31,9 +31,6 @@ static const std::string kLongitudeName_ = "longitude"; static const std::string kIconName_ = "icon"; static const std::string kIconColorName_ = "icon-color"; -static const std::string defaultIconName = types::getMarkerIcons()[0].name; -static const boost::gil::rgba8_pixel_t defaultIconColor = - util::color::ToRgba8PixelT("#ffff0000"); class MarkerManager::Impl { @@ -94,6 +91,9 @@ public: friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { + static const std::string defaultIconName = types::getMarkerIcons()[0].name; + static const boost::gil::rgba8_pixel_t defaultIconColor = + util::color::ToRgba8PixelT("#ffff0000"); const boost::json::object& jo = jv.as_object(); diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 4e1a2dd0..311a347b 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include namespace scwx @@ -29,8 +28,6 @@ public: void ConnectSignals(); MarkerLayer* self_; - const std::string& markerIconName_ { - types::GetTextureName(types::ImageTexture::LocationMarker)}; std::shared_ptr geoIcons_; }; @@ -59,9 +56,10 @@ void MarkerLayer::Impl::ReloadMarkers() [this](const types::MarkerInfo& marker) { std::shared_ptr icon = geoIcons_->AddIcon(); - geoIcons_->SetIconTexture(icon, markerIconName_, 0); + geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); geoIcons_->SetIconHoverText(icon, marker.name); + geoIcons_->SetIconModulate(icon, marker.iconColor); }); geoIcons_->FinishIcons(); @@ -82,7 +80,11 @@ void MarkerLayer::Initialize() DrawLayer::Initialize(); p->geoIcons_->StartIconSheets(); - p->geoIcons_->AddIconSheet(p->markerIconName_); + for (auto& markerIcon : types::getMarkerIcons()) + { + p->geoIcons_->AddIconSheet( + markerIcon.name, 0, 0, markerIcon.hotX, markerIcon.hotY); + } p->geoIcons_->FinishIconSheets(); p->ReloadMarkers(); @@ -90,8 +92,8 @@ void MarkerLayer::Initialize() void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - // auto markerManager = manager::MarkerManager::Instance(); gl::OpenGLFunctions& gl = context()->gl(); + context()->set_render_parameters(params); DrawLayer::Render(params); From d0d9adfd1a3ecc5fbbf798222e9971dedc2c63a5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 12:33:14 -0500 Subject: [PATCH 309/762] Switch to using new edit_marker_dialog for marker adding and editting --- scwx-qt/scwx-qt.cmake | 3 + .../source/scwx/qt/manager/marker_manager.cpp | 3 +- .../source/scwx/qt/manager/marker_manager.hpp | 2 +- scwx-qt/source/scwx/qt/model/marker_model.cpp | 101 +------ .../source/scwx/qt/ui/edit_marker_dialog.cpp | 249 ++++++++++++++++++ .../source/scwx/qt/ui/edit_marker_dialog.hpp | 41 +++ .../source/scwx/qt/ui/edit_marker_dialog.ui | 188 +++++++++++++ .../scwx/qt/ui/marker_settings_widget.cpp | 37 ++- 8 files changed, 514 insertions(+), 110 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 7aca4dff..b8630306 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -256,6 +256,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/county_dialog.hpp source/scwx/qt/ui/download_dialog.hpp source/scwx/qt/ui/edit_line_dialog.hpp + source/scwx/qt/ui/edit_marker_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/gps_info_dialog.hpp source/scwx/qt/ui/hotkey_edit.hpp @@ -286,6 +287,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/county_dialog.cpp source/scwx/qt/ui/download_dialog.cpp source/scwx/qt/ui/edit_line_dialog.cpp + source/scwx/qt/ui/edit_marker_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/gps_info_dialog.cpp source/scwx/qt/ui/hotkey_edit.cpp @@ -315,6 +317,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/collapsible_group.ui source/scwx/qt/ui/county_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui + source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 30cb0d4b..fbfe2a2d 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -313,7 +313,7 @@ void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& mark Q_EMIT MarkersUpdated(); } -void MarkerManager::add_marker(const types::MarkerInfo& marker) +types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) { types::MarkerId id; { @@ -326,6 +326,7 @@ void MarkerManager::add_marker(const types::MarkerInfo& marker) } Q_EMIT MarkerAdded(id); Q_EMIT MarkersUpdated(); + return id; } void MarkerManager::remove_marker(types::MarkerId id) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 37ec1c31..17ccb8e6 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -25,7 +25,7 @@ public: std::optional get_marker(types::MarkerId id); std::optional get_index(types::MarkerId id); void set_marker(types::MarkerId id, const types::MarkerInfo& marker); - void add_marker(const types::MarkerInfo& marker); + types::MarkerId add_marker(const types::MarkerInfo& marker); void remove_marker(types::MarkerId id); void move_marker(size_t from, size_t to); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index e550312e..c035900a 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -78,26 +78,11 @@ Qt::ItemFlags MarkerModel::flags(const QModelIndex& index) const { Qt::ItemFlags flags = QAbstractTableModel::flags(index); - switch (index.column()) - { - case static_cast(Column::Name): - case static_cast(Column::Latitude): - case static_cast(Column::Longitude): - flags |= Qt::ItemFlag::ItemIsEditable; - break; - default: - break; - } - return flags; } QVariant MarkerModel::data(const QModelIndex& index, int role) const { - - static const char COORDINATE_FORMAT = 'g'; - static const int COORDINATE_PRECISION = 10; - if (!index.isValid() || index.row() < 0 || static_cast(index.row()) >= p->markerIds_.size()) { @@ -118,8 +103,7 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const { case static_cast(Column::Name): if (role == Qt::ItemDataRole::DisplayRole || - role == Qt::ItemDataRole::ToolTipRole || - role == Qt::ItemDataRole::EditRole) + role == Qt::ItemDataRole::ToolTipRole) { return QString::fromStdString(markerInfo->name); } @@ -132,11 +116,6 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QString::fromStdString( common::GetLatitudeString(markerInfo->latitude)); } - else if (role == Qt::ItemDataRole::EditRole) - { - return QString::number( - markerInfo->latitude, COORDINATE_FORMAT, COORDINATE_PRECISION); - } break; case static_cast(Column::Longitude): @@ -146,11 +125,6 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QString::fromStdString( common::GetLongitudeString(markerInfo->longitude)); } - else if (role == Qt::ItemDataRole::EditRole) - { - return QString::number( - markerInfo->longitude, COORDINATE_FORMAT, COORDINATE_PRECISION); - } break; break; @@ -199,78 +173,9 @@ QVariant MarkerModel::headerData(int section, return QVariant(); } -bool MarkerModel::setData(const QModelIndex& index, - const QVariant& value, - int role) +bool MarkerModel::setData(const QModelIndex&, const QVariant&, int) { - - if (!index.isValid() || index.row() < 0 || - static_cast(index.row()) >= p->markerIds_.size()) - { - return false; - } - - types::MarkerId id = p->markerIds_[index.row()]; - std::optional markerInfo = - p->markerManager_->get_marker(id); - if (!markerInfo) - { - return false; - } - bool result = false; - - switch(index.column()) - { - case static_cast(Column::Name): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - markerInfo->name = str.toStdString(); - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - break; - - case static_cast(Column::Latitude): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - bool ok; - double latitude = str.toDouble(&ok); - if (!str.isEmpty() && ok && -90 <= latitude && latitude <= 90) - { - markerInfo->latitude = latitude; - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - } - break; - - case static_cast(Column::Longitude): - if (role == Qt::ItemDataRole::EditRole) - { - QString str = value.toString(); - bool ok; - double longitude = str.toDouble(&ok); - if (!str.isEmpty() && ok && -180 <= longitude && longitude <= 180) - { - markerInfo->longitude = longitude; - p->markerManager_->set_marker(id, *markerInfo); - result = true; - } - } - break; - - default: - break; - } - - if (result) - { - Q_EMIT dataChanged(index, index); - } - - return result; + return false; } void MarkerModel::HandleMarkersInitialized(size_t count) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp new file mode 100644 index 00000000..d84b3c0c --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -0,0 +1,249 @@ +#include "edit_marker_dialog.hpp" +#include "ui_edit_marker_dialog.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::edit_marker_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class EditMarkerDialog::Impl +{ +public: + explicit Impl(EditMarkerDialog* self) : + self_{self} + { + } + + void show_color_dialog(); + void set_icon_color(const std::string& color); + + void connect_signals(); + + void handle_accepted(); + void handle_rejected(); + + EditMarkerDialog* self_; + QPushButton* deleteButton_; + QIcon get_colored_icon(size_t index, const std::string& color); + + std::shared_ptr markerManager_ = + manager::MarkerManager::Instance(); + const std::vector* icons_; + types::MarkerId editId_; + bool adding_; +}; + +QIcon EditMarkerDialog::Impl::get_colored_icon(size_t index, + const std::string& color) +{ + if (index >= icons_->size()) + { + return QIcon(); + } + + return util::modulateColors((*icons_)[index].qIcon, + self_->ui->iconComboBox->iconSize(), + QColor(QString::fromStdString(color))); +} + +EditMarkerDialog::EditMarkerDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::EditMarkerDialog) +{ + ui->setupUi(this); + + p->icons_ = &types::getMarkerIcons(); + for (auto& markerIcon : (*p->icons_)) + { + ui->iconComboBox->addItem(markerIcon.qIcon, + QString(""), + QString::fromStdString(markerIcon.name)); + } + p->deleteButton_ = + ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); + p->connect_signals(); +} + +EditMarkerDialog::~EditMarkerDialog() +{ + delete ui; +} + +void EditMarkerDialog::setup() +{ + setup(0, 0); +} + +void EditMarkerDialog::setup(double latitude, double longitude) +{ + ui->iconComboBox->setCurrentIndex(0); + // By default use foreground color as marker color, mainly so the icons + // are vissable in the dropdown menu. + QColor color = QWidget::palette().color(QWidget::foregroundRole()); + p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( + "", + latitude, + longitude, + ui->iconComboBox->currentData().toString().toStdString(), + boost::gil::rgba8_pixel_t {static_cast(color.red()), + static_cast(color.green()), + static_cast(color.blue()), + static_cast(color.alpha())})); + + setup(p->editId_); + p->adding_ = true; +} + +void EditMarkerDialog::setup(types::MarkerId id) +{ + std::optional marker = + p->markerManager_->get_marker(id); + if (!marker) + { + return; + } + + p->editId_ = id; + p->adding_ = false; + + int iconIndex = + ui->iconComboBox->findData(QString::fromStdString(marker->iconName)); + if (iconIndex < 0 || marker->iconName == "") + { + iconIndex = 0; + } + + std::string iconColorStr = util::color::ToArgbString(marker->iconColor); + + ui->nameLineEdit->setText(QString::fromStdString(marker->name)); + ui->iconComboBox->setCurrentIndex(iconIndex); + ui->latitudeDoubleSpinBox->setValue(marker->latitude); + ui->longitudeDoubleSpinBox->setValue(marker->longitude); + ui->iconColorLineEdit->setText(QString::fromStdString(iconColorStr)); + + p->set_icon_color(iconColorStr); +} + +types::MarkerInfo EditMarkerDialog::get_marker_info() const +{ + QString colorName = ui->iconColorLineEdit->text(); + boost::gil::rgba8_pixel_t color = + util::color::ToRgba8PixelT(colorName.toStdString()); + + return types::MarkerInfo( + ui->nameLineEdit->text().toStdString(), + ui->latitudeDoubleSpinBox->value(), + ui->longitudeDoubleSpinBox->value(), + ui->iconComboBox->currentData().toString().toStdString(), + color); +} + +void EditMarkerDialog::Impl::show_color_dialog() +{ + + QColorDialog* dialog = new QColorDialog(self_); + + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); + + QColor initialColor(self_->ui->iconColorLineEdit->text()); + if (initialColor.isValid()) + { + dialog->setCurrentColor(initialColor); + } + + QObject::connect(dialog, + &QColorDialog::colorSelected, + self_, + [this](const QColor& qColor) + { + QString colorName = + qColor.name(QColor::NameFormat::HexArgb); + self_->ui->iconColorLineEdit->setText(colorName); + }); + dialog->open(); +} + +void EditMarkerDialog::Impl::connect_signals() +{ + connect(self_, + &EditMarkerDialog::accepted, + self_, + [this]() { handle_accepted(); }); + + connect(self_, + &EditMarkerDialog::rejected, + self_, + [this]() { handle_rejected(); }); + + connect(deleteButton_, + &QPushButton::clicked, + self_, + [this]() + { + markerManager_->remove_marker(editId_); + self_->done(0); + }); + + connect(self_->ui->iconColorLineEdit, + &QLineEdit::textEdited, + self_, + [=, this](const QString& text) + { set_icon_color(text.toStdString()); }); + + connect(self_->ui->iconColorButton, + &QAbstractButton::clicked, + self_, + [=, this]() { self_->p->show_color_dialog(); }); +} + +void EditMarkerDialog::Impl::set_icon_color(const std::string& color) +{ + self_->ui->iconColorFrame->setStyleSheet( + QString::fromStdString(fmt::format("background-color: {}", color))); + + for (size_t i = 0; i < icons_->size(); i++) + { + self_->ui->iconComboBox->setItemIcon(static_cast(i), + get_colored_icon(i, color)); + } +} + +void EditMarkerDialog::Impl::handle_accepted() +{ + markerManager_->set_marker(editId_, self_->get_marker_info()); +} + +void EditMarkerDialog::Impl::handle_rejected() +{ + if (adding_) + { + markerManager_->remove_marker(editId_); + } +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp new file mode 100644 index 00000000..eb66e889 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -0,0 +1,41 @@ +#pragma once +#include + +#include + +namespace Ui +{ +class EditMarkerDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +class EditMarkerDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(EditMarkerDialog) + +public: + explicit EditMarkerDialog(QWidget* parent = nullptr); + ~EditMarkerDialog(); + + void setup(); + void setup(double latitude, double longitude); + void setup(types::MarkerId id); + + types::MarkerInfo get_marker_info() const; + +private: + class Impl; + std::unique_ptr p; + Ui::EditMarkerDialog* ui; +}; + + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui new file mode 100644 index 00000000..d6b6dc87 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui @@ -0,0 +1,188 @@ + + + EditMarkerDialog + + + + 0 + 0 + 400 + 211 + + + + Edit Location Marker + + + + + + Icon + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + true + + + + + + + Latitude + + + + + + + 4 + + + -180.000000000000000 + + + 180.000000000000000 + + + + + + + + + + Name + + + + + + + 4 + + + -90.000000000000000 + + + 90.000000000000000 + + + + + + + Longitude + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Icon Color + + + + + + + + + + 24 + 24 + + + + QFrame::Shape::Box + + + QFrame::Shadow::Plain + + + + + + + #ffffffff + + + + + + + ... + + + + :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + + + + + + + + + + + buttonBox + accepted() + EditMarkerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditMarkerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index 6a1e04f3..ba6cf88b 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -4,11 +4,9 @@ #include #include #include -#include +#include #include -#include - #include namespace scwx @@ -36,6 +34,7 @@ public: model::MarkerModel* markerModel_; std::shared_ptr markerManager_ { manager::MarkerManager::Instance()}; + std::shared_ptr editMarkerDialog_ {nullptr}; }; @@ -47,9 +46,10 @@ MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : ui->setupUi(this); ui->removeButton->setEnabled(false); - ui->markerView->setModel(p->markerModel_); + p->editMarkerDialog_ = std::make_shared(this); + p->ConnectSignals(); } @@ -65,12 +65,8 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this]() { - markerManager_->add_marker(types::MarkerInfo( - "", - 0, - 0, - types::getMarkerIcons()[0].name, - util::color::ToRgba8PixelT("#ffff0000"))); + editMarkerDialog_->setup(); + editMarkerDialog_->show(); }); QObject::connect( self_->ui->removeButton, @@ -109,6 +105,27 @@ void MarkerSettingsWidgetImpl::ConnectSignals() bool itemSelected = selected.size() > 0; self_->ui->removeButton->setEnabled(itemSelected); }); + QObject::connect(self_->ui->markerView, + &QAbstractItemView::doubleClicked, + self_, + [this](const QModelIndex& index) + { + int row = index.row(); + if (row < 0) + { + return; + } + + std::optional id = + markerModel_->getId(row); + if (!id) + { + return; + } + + editMarkerDialog_->setup(*id); + editMarkerDialog_->show(); + }); } } // namespace ui From cfed61c6ffa79a63e18313bd86dfb1cea2f2b6be Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 12:46:26 -0500 Subject: [PATCH 310/762] Added Icon column to location marker manager dialog --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 22 ++++++++++++++++++- scwx-qt/source/scwx/qt/model/marker_model.hpp | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index c035900a..0fc22ab0 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -38,7 +39,6 @@ public: MarkerModel::MarkerModel(QObject* parent) : QAbstractTableModel(parent), p(std::make_unique()) { - connect(p->markerManager_.get(), &manager::MarkerManager::MarkersInitialized, this, @@ -127,6 +127,23 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const } break; break; + case static_cast(Column::Icon): + if (role == Qt::ItemDataRole::DecorationRole) + { + for (auto& icon : types::getMarkerIcons()) + { + if (icon.name == markerInfo->iconName) + { + return util::modulateColors(icon.qIcon, + QSize(30, 30), + QColor(markerInfo->iconColor[0], + markerInfo->iconColor[1], + markerInfo->iconColor[2], + markerInfo->iconColor[3])); + } + } + } + break; default: break; @@ -164,6 +181,9 @@ QVariant MarkerModel::headerData(int section, case static_cast(Column::Longitude): return tr("Longitude"); + case static_cast(Column::Icon): + return tr("Icon"); + default: break; } diff --git a/scwx-qt/source/scwx/qt/model/marker_model.hpp b/scwx-qt/source/scwx/qt/model/marker_model.hpp index 4fc6c95c..91c8854f 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.hpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -17,7 +17,8 @@ public: { Latitude = 0, Longitude = 1, - Name = 2, + Icon = 2, + Name = 3, }; explicit MarkerModel(QObject* parent = nullptr); From fc8d65d4d1e6831023669867fd63a13fc431a7c6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 13:09:34 -0500 Subject: [PATCH 311/762] Add right click to edit location marker --- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 23 +++++++++--- scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp | 10 ++++++ scwx-qt/source/scwx/qt/map/marker_layer.cpp | 38 ++++++++++++++++++-- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 9e2a8d17..71501930 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -38,7 +38,7 @@ static constexpr std::size_t kIntegersPerVertex_ = 4; static constexpr std::size_t kIntegerBufferLength_ = kNumTriangles * kVerticesPerTriangle * kIntegersPerVertex_; -struct GeoIconDrawItem +struct GeoIconDrawItem : types::EventHandler { units::length::nautical_miles threshold_ {}; std::chrono::sys_time startTime_ {}; @@ -691,7 +691,7 @@ void GeoIcons::Impl::UpdateSingleBuffer( hoverIcons.end(), [&di](auto& entry) { return entry.di_ == di; }); - if (di->visible_ && !di->hoverText_.empty()) + if (di->visible_ && (!di->hoverText_.empty() || di->event_ != nullptr)) { const units::angle::radians radians = angle; @@ -903,7 +903,7 @@ bool GeoIcons::RunMousePicking( const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, const common::Coordinate& /* mouseGeoCoords */, - std::shared_ptr& /* eventHandler */) + std::shared_ptr& eventHandler ) { std::unique_lock lock {p->iconMutex_}; @@ -993,12 +993,27 @@ bool GeoIcons::RunMousePicking( if (it != p->currentHoverIcons_.crend()) { itemPicked = true; - util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + if (!it->di_->hoverText_.empty()) + { + // Show tooltip + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + } + if (it->di_->event_ != nullptr) + { + eventHandler = it->di_; + } } return itemPicked; } +void GeoIcons::RegisterEventHandler( + const std::shared_ptr& di, + const std::function& eventHandler) +{ + di->event_ = eventHandler; +} + } // namespace draw } // namespace gl } // namespace qt diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp index 4d819681..073fc118 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.hpp @@ -183,6 +183,16 @@ public: */ void FinishIcons(); + /** + * Registers an event handler for an icon. + * + * @param [in] di Icon draw item + * @param [in] eventHandler Event handler function + */ + static void + RegisterEventHandler(const std::shared_ptr& di, + const std::function& eventHandler); + private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 311a347b..e552c749 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -1,8 +1,14 @@ #include #include #include -#include #include +#include +#include + +#include +#include + +#include namespace scwx { @@ -18,7 +24,9 @@ class MarkerLayer::Impl { public: explicit Impl(MarkerLayer* self, std::shared_ptr context) : - self_ {self}, geoIcons_ {std::make_shared(context)} + self_ {self}, + geoIcons_ {std::make_shared(context)}, + editMarkerDialog_ {std::make_shared()} { ConnectSignals(); } @@ -30,6 +38,7 @@ public: MarkerLayer* self_; std::shared_ptr geoIcons_; + std::shared_ptr editMarkerDialog_; }; void MarkerLayer::Impl::ConnectSignals() @@ -55,11 +64,36 @@ void MarkerLayer::Impl::ReloadMarkers() markerManager->for_each( [this](const types::MarkerInfo& marker) { + // must use local ID, instead of reference to marker in event handler + // callback. + types::MarkerId id = marker.id; + std::shared_ptr icon = geoIcons_->AddIcon(); geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); geoIcons_->SetIconHoverText(icon, marker.name); geoIcons_->SetIconModulate(icon, marker.iconColor); + geoIcons_->RegisterEventHandler( + icon, + [this, id](QEvent* ev) + { + switch (ev->type()) + { + case QEvent::Type::MouseButtonPress: + { + QMouseEvent* mouseEvent = reinterpret_cast(ev); + if (mouseEvent->buttons() == Qt::MouseButton::RightButton) + { + editMarkerDialog_->setup(id); + editMarkerDialog_->show(); + } + } + break; + + default: + break; + } + }); }); geoIcons_->FinishIcons(); From 05515c59b89f9b4c9218a1a87b174019922132d6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 13:27:03 -0500 Subject: [PATCH 312/762] Added hotkey for adding new location markers --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 21 ++++++++++++++++--- .../scwx/qt/settings/hotkey_settings.cpp | 1 + scwx-qt/source/scwx/qt/types/hotkey_types.cpp | 2 ++ scwx-qt/source/scwx/qt/types/hotkey_types.hpp | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ab7b0cf6..9c68a68a 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -9,10 +9,10 @@ #include #include #include +#include #include #include #include -#include #include #include #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,7 @@ public: map_(), layerList_ {}, imGuiRendererInitialized_ {false}, + editMarkerDialog_ {nullptr}, radarProductManager_ {nullptr}, radarProductLayer_ {nullptr}, overlayLayer_ {nullptr}, @@ -127,8 +129,6 @@ public: ImGui_ImplQt_Init(); InitializeCustomStyles(); - - ConnectSignals(); } ~MapWidgetImpl() @@ -219,6 +219,8 @@ public: std::shared_ptr layerModel_ { model::LayerModel::Instance()}; + std::shared_ptr editMarkerDialog_; + std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; std::shared_ptr placefileManager_ { @@ -283,6 +285,9 @@ MapWidget::MapWidget(std::size_t id, const QMapLibre::Settings& settings) : setFocusPolicy(Qt::StrongFocus); ImGui_ImplQt_RegisterWidget(this); + + p->editMarkerDialog_ = std::make_shared(this); + p->ConnectSignals(); } MapWidget::~MapWidget() @@ -429,6 +434,16 @@ void MapWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat) switch (hotkey) { + case types::Hotkey::AddLocationMarker: + if (hasMouse_) + { + auto coordinate = map_->coordinateForPixel(lastPos_); + + editMarkerDialog_->setup(coordinate.first, coordinate.second); + editMarkerDialog_->show(); + } + break; + case types::Hotkey::ChangeMapStyle: if (context_->settings().isActive_) { diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp index 0edaf840..e9c6b007 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp @@ -12,6 +12,7 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::hotkey_settings"; static const std::unordered_map kDefaultHotkeys_ { + {types::Hotkey::AddLocationMarker, QKeySequence {Qt::Key::Key_M}}, {types::Hotkey::ChangeMapStyle, QKeySequence {Qt::Key::Key_Z}}, {types::Hotkey::CopyCursorCoordinates, QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp index 8a4d0ee5..72c77f63 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp @@ -13,6 +13,7 @@ namespace types { static const std::unordered_map hotkeyShortName_ { + {Hotkey::AddLocationMarker, "add_location_marker"}, {Hotkey::ChangeMapStyle, "change_map_style"}, {Hotkey::CopyCursorCoordinates, "copy_cursor_coordinates"}, {Hotkey::CopyMapCoordinates, "copy_map_coordinates"}, @@ -52,6 +53,7 @@ static const std::unordered_map hotkeyShortName_ { {Hotkey::Unknown, "?"}}; static const std::unordered_map hotkeyLongName_ { + {Hotkey::AddLocationMarker, "Add Location Marker"}, {Hotkey::ChangeMapStyle, "Change Map Style"}, {Hotkey::CopyCursorCoordinates, "Copy Cursor Coordinates"}, {Hotkey::CopyMapCoordinates, "Copy Map Coordinates"}, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp index c2118a4f..2107a009 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp @@ -13,6 +13,7 @@ namespace types enum class Hotkey { + AddLocationMarker, ChangeMapStyle, CopyCursorCoordinates, CopyMapCoordinates, @@ -52,7 +53,7 @@ enum class Hotkey Unknown }; typedef scwx::util:: - Iterator + Iterator HotkeyIterator; Hotkey GetHotkeyFromShortName(const std::string& name); From 1a5503a6f659a229b97546cb6b144917dc66bfac Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 13:45:21 -0500 Subject: [PATCH 313/762] Fix issue where set_marker removed id from record --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index fbfe2a2d..e4f69598 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -291,7 +291,8 @@ std::optional MarkerManager::get_index(types::MarkerId id) return p->idToIndex_[id]; } -void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& marker) +void MarkerManager::set_marker(types::MarkerId id, + const types::MarkerInfo& marker) { { std::unique_lock lock(p->markerRecordLock_); @@ -308,6 +309,7 @@ void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& mark std::shared_ptr& markerRecord = p->markerRecords_[index]; markerRecord->markerInfo_ = marker; + markerRecord->markerInfo_.id = id; } Q_EMIT MarkerChanged(id); Q_EMIT MarkersUpdated(); From 7b72cb4c7188aaad3449c85e771823abc6861411 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 13:54:03 -0500 Subject: [PATCH 314/762] Add building-columns-solid.svg to qrc (missed in earlier commit) --- scwx-qt/scwx-qt.qrc | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index ee3fcc8e..e7d8315a 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -33,6 +33,7 @@ res/icons/font-awesome-6/backward-step-solid.svg res/icons/font-awesome-6/book-solid.svg res/icons/font-awesome-6/briefcase-solid.svg + res/icons/font-awesome-6/building-columns-solid.svg res/icons/font-awesome-6/building-solid.svg res/icons/font-awesome-6/caravan-solid.svg res/icons/font-awesome-6/copy-regular.svg From 3685599693f3f5a1eb2ed049d776f23dbe5e67ec Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 28 Nov 2024 21:01:08 -0500 Subject: [PATCH 315/762] Set icon color properly from dialog in editMarkerDialog --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index d84b3c0c..93d4c1e0 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -182,6 +182,7 @@ void EditMarkerDialog::Impl::show_color_dialog() QString colorName = qColor.name(QColor::NameFormat::HexArgb); self_->ui->iconColorLineEdit->setText(colorName); + set_icon_color(colorName.toStdString()); }); dialog->open(); } From 5bb4a7f95a4d36e220bc90e6a475e3ac8967d2d2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 13:47:01 -0500 Subject: [PATCH 316/762] Add custom marker icons, and rework how marker icons are handled. --- scwx-qt/scwx-qt.cmake | 1 - .../source/scwx/qt/manager/marker_manager.cpp | 118 +++++++++-- .../source/scwx/qt/manager/marker_manager.hpp | 9 + scwx-qt/source/scwx/qt/map/marker_layer.cpp | 58 +++--- scwx-qt/source/scwx/qt/model/marker_model.cpp | 23 ++- scwx-qt/source/scwx/qt/types/marker_types.cpp | 34 ---- scwx-qt/source/scwx/qt/types/marker_types.hpp | 33 +++- .../source/scwx/qt/ui/edit_marker_dialog.cpp | 110 ++++++++--- .../source/scwx/qt/ui/edit_marker_dialog.hpp | 2 +- .../source/scwx/qt/ui/edit_marker_dialog.ui | 184 ++++++++++-------- 10 files changed, 368 insertions(+), 204 deletions(-) delete mode 100644 scwx-qt/source/scwx/qt/types/marker_types.cpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b8630306..706ac038 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -239,7 +239,6 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.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/marker_types.cpp source/scwx/qt/types/media_types.cpp source/scwx/qt/types/qt_types.cpp source/scwx/qt/types/radar_product_record.cpp diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index e4f69598..75eb16fa 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -2,13 +2,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include +#include #include #include @@ -25,12 +28,13 @@ namespace manager static const std::string logPrefix_ = "scwx::qt::manager::marker_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static const std::string kNameName_ = "name"; -static const std::string kLatitudeName_ = "latitude"; -static const std::string kLongitudeName_ = "longitude"; -static const std::string kIconName_ = "icon"; -static const std::string kIconColorName_ = "icon-color"; +static const std::string kNameName_ = "name"; +static const std::string kLatitudeName_ = "latitude"; +static const std::string kLongitudeName_ = "longitude"; +static const std::string kIconName_ = "icon"; +static const std::string kIconColorName_ = "icon-color"; +static const std::string defaultIconName = "images/location-marker"; class MarkerManager::Impl { @@ -40,15 +44,16 @@ public: explicit Impl(MarkerManager* self) : self_ {self} {} ~Impl() { threadPool_.join(); } - std::string markerSettingsPath_ {""}; - std::vector> markerRecords_ {}; + std::string markerSettingsPath_ {""}; + std::vector> markerRecords_ {}; std::unordered_map idToIndex_ {}; - + std::unordered_map markerIcons_ {}; MarkerManager* self_; boost::asio::thread_pool threadPool_ {1u}; std::shared_mutex markerRecordLock_ {}; + std::shared_mutex markerIconsLock_ {}; void InitializeMarkerSettings(); void ReadMarkerSettings(); @@ -57,7 +62,7 @@ public: void InitalizeIds(); types::MarkerId NewId(); - types::MarkerId lastId_; + types::MarkerId lastId_ {0}; }; class MarkerManager::Impl::MarkerRecord @@ -84,14 +89,14 @@ public: {kLatitudeName_, record->markerInfo_.latitude}, {kLongitudeName_, record->markerInfo_.longitude}, {kIconName_, record->markerInfo_.iconName}, - {kIconColorName_, util::color::ToArgbString(record->markerInfo_.iconColor)}}; + {kIconColorName_, + util::color::ToArgbString(record->markerInfo_.iconColor)}}; } friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { - static const std::string defaultIconName = types::getMarkerIcons()[0].name; static const boost::gil::rgba8_pixel_t defaultIconColor = util::color::ToRgba8PixelT("#ffff0000"); @@ -120,12 +125,12 @@ public: } } - return MarkerRecord(types::MarkerInfo( + return {types::MarkerInfo( boost::json::value_to(jv.at(kNameName_)), boost::json::value_to(jv.at(kLatitudeName_)), boost::json::value_to(jv.at(kLongitudeName_)), iconName, - iconColor)); + iconColor)}; } }; @@ -176,14 +181,14 @@ void MarkerManager::Impl::ReadMarkerSettings() { // For each marker entry auto& markerArray = markerJson.as_array(); + //std::vector fileNames {}; markerRecords_.reserve(markerArray.size()); idToIndex_.reserve(markerArray.size()); for (auto& markerEntry : markerArray) { try { - MarkerRecord record = - boost::json::value_to(markerEntry); + auto record = boost::json::value_to(markerEntry); if (!record.markerInfo_.name.empty()) { @@ -193,6 +198,8 @@ void MarkerManager::Impl::ReadMarkerSettings() markerRecords_.emplace_back( std::make_shared(record.markerInfo_)); idToIndex_.emplace(id, index); + + self_->add_icon(record.markerInfo_.iconName, true); } } catch (const std::exception& ex) @@ -201,10 +208,14 @@ void MarkerManager::Impl::ReadMarkerSettings() } } + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.BuildAtlas(2048, 2048); // TODO should code be moved to ResourceManager (probrably) + logger_->debug("{} location marker entries", markerRecords_.size()); } } + Q_EMIT self_->MarkersUpdated(); } @@ -233,17 +244,41 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) MarkerManager::MarkerManager() : p(std::make_unique(this)) { + const std::vector defaultMarkerIcons_ { + types::MarkerIconInfo(types::ImageTexture::LocationMarker, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationPin, 6, 16), + types::MarkerIconInfo(types::ImageTexture::LocationCrosshair, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationStar, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationBriefcase, -1, -1), + types::MarkerIconInfo( + types::ImageTexture::LocationBuildingColumns, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationBuilding, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationCaravan, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationHouse, -1, -1), + types::MarkerIconInfo(types::ImageTexture::LocationTent, -1, -1), + }; + p->InitializeMarkerSettings(); boost::asio::post(p->threadPool_, - [this]() + [this, defaultMarkerIcons_]() { try { // Read Marker settings on startup main::Application::WaitForInitialization(); + { + std::unique_lock lock(p->markerIconsLock_); + p->markerIcons_.reserve( + defaultMarkerIcons_.size()); + for (auto& icon : defaultMarkerIcons_) + { + p->markerIcons_.emplace(icon.name, icon); + } + } p->ReadMarkerSettings(); + Q_EMIT IconsReady(); Q_EMIT MarkersInitialized(p->markerRecords_.size()); } catch (const std::exception& ex) @@ -310,6 +345,8 @@ void MarkerManager::set_marker(types::MarkerId id, p->markerRecords_[index]; markerRecord->markerInfo_ = marker; markerRecord->markerInfo_.id = id; + + add_icon(marker.iconName); } Q_EMIT MarkerChanged(id); Q_EMIT MarkersUpdated(); @@ -325,6 +362,8 @@ types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) p->idToIndex_.emplace(id, index); p->markerRecords_.emplace_back(std::make_shared(marker)); p->markerRecords_[index]->markerInfo_.id = id; + + add_icon(marker.iconName); } Q_EMIT MarkerAdded(id); Q_EMIT MarkersUpdated(); @@ -403,6 +442,48 @@ void MarkerManager::for_each(std::function func) } } +void MarkerManager::add_icon(const std::string& name, bool startup) +{ + { + std::unique_lock lock(p->markerIconsLock_); + if (p->markerIcons_.contains(name)) + { + return; + } + std::shared_ptr image = + ResourceManager::LoadImageResource(name); + + auto icon = types::MarkerIconInfo(name, -1, -1, image); + p->markerIcons_.emplace(name, icon); + } + + if (!startup) + { + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.BuildAtlas(2048, 2048); // TODO should code be moved to ResourceManager (probrably) + Q_EMIT IconAdded(name); + } +} + +std::optional +MarkerManager::get_icon(const std::string& name) +{ + std::shared_lock lock(p->markerIconsLock_); + if (p->markerIcons_.contains(name)) + { + return p->markerIcons_.at(name); + } + + return {}; +} + +const std::unordered_map +MarkerManager::get_icons() +{ + std::shared_lock lock(p->markerIconsLock_); + return p->markerIcons_; +} + // Only use for testing void MarkerManager::set_marker_settings_path(const std::string& path) { @@ -429,6 +510,11 @@ std::shared_ptr MarkerManager::Instance() return markerManager; } +const std::string& MarkerManager::getDefaultIconName() +{ + return defaultIconName; +} + } // namespace manager } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 17ccb8e6..9e897660 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -29,12 +29,17 @@ public: void remove_marker(types::MarkerId id); void move_marker(size_t from, size_t to); + void add_icon(const std::string& name, bool startup = false); + std::optional get_icon(const std::string& name); + const std::unordered_map get_icons(); + void for_each(std::function func); // Only use for testing void set_marker_settings_path(const std::string& path); static std::shared_ptr Instance(); + static const std::string& getDefaultIconName(); signals: void MarkersInitialized(size_t count); @@ -43,6 +48,10 @@ signals: void MarkerAdded(types::MarkerId id); void MarkerRemoved(types::MarkerId id); + void IconsReady(); + void IconAdded(std::string name); + + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index e552c749..0bad94f6 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -30,11 +30,16 @@ public: { ConnectSignals(); } - ~Impl() {} + ~Impl() = default; void ReloadMarkers(); void ConnectSignals(); + std::shared_ptr markerManager_ { + manager::MarkerManager::Instance()}; + + void set_icon_sheets(); + MarkerLayer* self_; std::shared_ptr geoIcons_; @@ -43,25 +48,26 @@ public: void MarkerLayer::Impl::ConnectSignals() { - auto markerManager = manager::MarkerManager::Instance(); - - QObject::connect(markerManager.get(), - &manager::MarkerManager::MarkersUpdated, - self_, - [this]() - { - this->ReloadMarkers(); - }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::MarkersUpdated, + self_, + [this]() { ReloadMarkers(); }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::IconsReady, + self_, + [this]() { set_icon_sheets(); }); + QObject::connect(markerManager_.get(), + &manager::MarkerManager::IconAdded, + self_, + [this]() { set_icon_sheets(); }); } void MarkerLayer::Impl::ReloadMarkers() { logger_->debug("ReloadMarkers()"); - auto markerManager = manager::MarkerManager::Instance(); geoIcons_->StartIcons(); - - markerManager->for_each( + markerManager_->for_each( [this](const types::MarkerInfo& marker) { // must use local ID, instead of reference to marker in event handler @@ -69,6 +75,7 @@ void MarkerLayer::Impl::ReloadMarkers() types::MarkerId id = marker.id; std::shared_ptr icon = geoIcons_->AddIcon(); + geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); geoIcons_->SetIconHoverText(icon, marker.name); @@ -81,7 +88,7 @@ void MarkerLayer::Impl::ReloadMarkers() { case QEvent::Type::MouseButtonPress: { - QMouseEvent* mouseEvent = reinterpret_cast(ev); + auto* mouseEvent = reinterpret_cast(ev); if (mouseEvent->buttons() == Qt::MouseButton::RightButton) { editMarkerDialog_->setup(id); @@ -113,17 +120,24 @@ void MarkerLayer::Initialize() logger_->debug("Initialize()"); DrawLayer::Initialize(); - p->geoIcons_->StartIconSheets(); - for (auto& markerIcon : types::getMarkerIcons()) - { - p->geoIcons_->AddIconSheet( - markerIcon.name, 0, 0, markerIcon.hotX, markerIcon.hotY); - } - p->geoIcons_->FinishIconSheets(); - + p->set_icon_sheets(); p->ReloadMarkers(); } +void MarkerLayer::Impl::set_icon_sheets() +{ + geoIcons_->StartIconSheets(); + for (auto& markerIcon : markerManager_->get_icons()) + { + geoIcons_->AddIconSheet(markerIcon.second.name, + 0, + 0, + markerIcon.second.hotX, + markerIcon.second.hotY); + } + geoIcons_->FinishIconSheets(); +} + void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = context()->gl(); diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 0fc22ab0..7c012921 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -19,6 +19,7 @@ namespace model static const std::string logPrefix_ = "scwx::qt::model::marker_model"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static const int iconSize_ = 30; static constexpr int kFirstColumn = static_cast(MarkerModel::Column::Latitude); @@ -130,17 +131,19 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const case static_cast(Column::Icon): if (role == Qt::ItemDataRole::DecorationRole) { - for (auto& icon : types::getMarkerIcons()) + std::optional icon = + p->markerManager_->get_icon(markerInfo->iconName); + if (icon) { + return util::modulateColors(icon->qIcon, + QSize(iconSize_, iconSize_), + QColor(markerInfo->iconColor[0], + markerInfo->iconColor[1], + markerInfo->iconColor[2], + markerInfo->iconColor[3])); + } + else { - if (icon.name == markerInfo->iconName) - { - return util::modulateColors(icon.qIcon, - QSize(30, 30), - QColor(markerInfo->iconColor[0], - markerInfo->iconColor[1], - markerInfo->iconColor[2], - markerInfo->iconColor[3])); - } + return {}; } } break; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.cpp b/scwx-qt/source/scwx/qt/types/marker_types.cpp deleted file mode 100644 index ce63490a..00000000 --- a/scwx-qt/source/scwx/qt/types/marker_types.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include - -namespace scwx -{ -namespace qt -{ -namespace types -{ - -const std::vector& getMarkerIcons() -{ - static std::vector markerIcons = {}; - if (markerIcons.size() == 0) - { - markerIcons = { - MarkerIconInfo(types::ImageTexture::LocationMarker, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationPin, 6, 16), - MarkerIconInfo(types::ImageTexture::LocationCrosshair, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationStar, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationBriefcase, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationBuildingColumns, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationBuilding, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationCaravan, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationHouse, -1, -1), - MarkerIconInfo(types::ImageTexture::LocationTent, -1, -1), - }; - } - - return markerIcons; -} - -} // namespace types -} // namespace qt -} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 2d1aa987..1848adfa 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -14,15 +14,15 @@ namespace qt { namespace types { -typedef std::uint64_t MarkerId; +using MarkerId = std::uint64_t; struct MarkerInfo { - MarkerInfo(const std::string& name, - double latitude, - double longitude, - const std::string iconName, - boost::gil::rgba8_pixel_t iconColor) : + MarkerInfo(const std::string& name, + double latitude, + double longitude, + const std::string& iconName, + const boost::gil::rgba8_pixel_t& iconColor) : name {name}, latitude {latitude}, longitude {longitude}, @@ -31,7 +31,7 @@ struct MarkerInfo { } - MarkerId id; + MarkerId id{0}; std::string name; double latitude; double longitude; @@ -47,7 +47,21 @@ struct MarkerIconInfo { path{types::GetTexturePath(texture)}, hotX{hotX}, hotY{hotY}, - qIcon{QIcon(QString::fromStdString(path))} + qIcon{QIcon(QString::fromStdString(path))}, + image{} + { + } + + explicit MarkerIconInfo(const std::string& path, + std::int32_t hotX, + std::int32_t hotY, + std::shared_ptr image) : + name{path}, + path{path}, + hotX{hotX}, + hotY{hotY}, + qIcon{QIcon(QString::fromStdString(path))}, + image{image} { } @@ -56,10 +70,9 @@ struct MarkerIconInfo { std::int32_t hotX; std::int32_t hotY; QIcon qIcon; + std::optional> image; }; -const std::vector& getMarkerIcons(); - } // namespace types } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index 93d4c1e0..f182735c 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include namespace scwx { @@ -36,6 +36,8 @@ public: } void show_color_dialog(); + void show_icon_file_dialog(); + void set_icon_color(const std::string& color); void connect_signals(); @@ -45,24 +47,20 @@ public: EditMarkerDialog* self_; QPushButton* deleteButton_; - QIcon get_colored_icon(size_t index, const std::string& color); + QIcon get_colored_icon(const types::MarkerIconInfo& marker, + const std::string& color); std::shared_ptr markerManager_ = manager::MarkerManager::Instance(); - const std::vector* icons_; types::MarkerId editId_; bool adding_; + std::string setIconOnAdded_{""}; }; -QIcon EditMarkerDialog::Impl::get_colored_icon(size_t index, - const std::string& color) +QIcon EditMarkerDialog::Impl::get_colored_icon( + const types::MarkerIconInfo& marker, const std::string& color) { - if (index >= icons_->size()) - { - return QIcon(); - } - - return util::modulateColors((*icons_)[index].qIcon, + return util::modulateColors(marker.qIcon, self_->ui->iconComboBox->iconSize(), QColor(QString::fromStdString(color))); } @@ -74,12 +72,11 @@ EditMarkerDialog::EditMarkerDialog(QWidget* parent) : { ui->setupUi(this); - p->icons_ = &types::getMarkerIcons(); - for (auto& markerIcon : (*p->icons_)) + for (auto& markerIcon : p->markerManager_->get_icons()) { - ui->iconComboBox->addItem(markerIcon.qIcon, + ui->iconComboBox->addItem(markerIcon.second.qIcon, QString(""), - QString::fromStdString(markerIcon.name)); + QString::fromStdString(markerIcon.second.name)); } p->deleteButton_ = ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); @@ -98,7 +95,6 @@ void EditMarkerDialog::setup() void EditMarkerDialog::setup(double latitude, double longitude) { - ui->iconComboBox->setCurrentIndex(0); // By default use foreground color as marker color, mainly so the icons // are vissable in the dropdown menu. QColor color = QWidget::palette().color(QWidget::foregroundRole()); @@ -106,7 +102,7 @@ void EditMarkerDialog::setup(double latitude, double longitude) "", latitude, longitude, - ui->iconComboBox->currentData().toString().toStdString(), + manager::MarkerManager::getDefaultIconName(), boost::gil::rgba8_pixel_t {static_cast(color.red()), static_cast(color.green()), static_cast(color.blue()), @@ -128,6 +124,9 @@ void EditMarkerDialog::setup(types::MarkerId id) p->editId_ = id; p->adding_ = false; + std::string iconColorStr = util::color::ToArgbString(marker->iconColor); + p->set_icon_color(iconColorStr); + int iconIndex = ui->iconComboBox->findData(QString::fromStdString(marker->iconName)); if (iconIndex < 0 || marker->iconName == "") @@ -135,15 +134,11 @@ void EditMarkerDialog::setup(types::MarkerId id) iconIndex = 0; } - std::string iconColorStr = util::color::ToArgbString(marker->iconColor); - ui->nameLineEdit->setText(QString::fromStdString(marker->name)); ui->iconComboBox->setCurrentIndex(iconIndex); ui->latitudeDoubleSpinBox->setValue(marker->latitude); ui->longitudeDoubleSpinBox->setValue(marker->longitude); ui->iconColorLineEdit->setText(QString::fromStdString(iconColorStr)); - - p->set_icon_color(iconColorStr); } types::MarkerInfo EditMarkerDialog::get_marker_info() const @@ -163,7 +158,7 @@ types::MarkerInfo EditMarkerDialog::get_marker_info() const void EditMarkerDialog::Impl::show_color_dialog() { - QColorDialog* dialog = new QColorDialog(self_); + auto* dialog = new QColorDialog(self_); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); @@ -187,6 +182,28 @@ void EditMarkerDialog::Impl::show_color_dialog() dialog->open(); } +void EditMarkerDialog::Impl::show_icon_file_dialog() +{ + + auto* dialog = new QFileDialog(self_); + + dialog->setFileMode(QFileDialog::ExistingFile); + dialog->setNameFilters({"Icon (*.png *.svg)", "All (*)"}); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + QObject::connect(dialog, + &QFileDialog::fileSelected, + self_, + [this](const QString& file) + { + std::string path = + QDir::toNativeSeparators(file).toStdString(); + setIconOnAdded_ = path; + markerManager_->add_icon(path); + }); + dialog->open(); +} + void EditMarkerDialog::Impl::connect_signals() { connect(self_, @@ -217,7 +234,33 @@ void EditMarkerDialog::Impl::connect_signals() connect(self_->ui->iconColorButton, &QAbstractButton::clicked, self_, - [=, this]() { self_->p->show_color_dialog(); }); + [=, this]() { show_color_dialog(); }); + + connect(self_->ui->iconFileOpenButton, + &QPushButton::clicked, + self_, + [this]() { show_icon_file_dialog(); }); + + connect(markerManager_.get(), + &manager::MarkerManager::IconAdded, + self_, + [this]() + { + std::string color = + self_->ui->iconColorLineEdit->text().toStdString(); + set_icon_color(color); + + if (setIconOnAdded_ != "") + { + int i = self_->ui->iconComboBox->findData( + QString::fromStdString(setIconOnAdded_)); + if (i >= 0) + { + self_->ui->iconComboBox->setCurrentIndex(i); + setIconOnAdded_ = ""; + } + } + }); } void EditMarkerDialog::Impl::set_icon_color(const std::string& color) @@ -225,10 +268,25 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) self_->ui->iconColorFrame->setStyleSheet( QString::fromStdString(fmt::format("background-color: {}", color))); - for (size_t i = 0; i < icons_->size(); i++) + auto* iconComboBox = self_->ui->iconComboBox; + + + QVariant data = self_->ui->iconComboBox->currentData(); + self_->ui->iconComboBox->clear(); + for (auto& markerIcon : markerManager_->get_icons()) { - self_->ui->iconComboBox->setItemIcon(static_cast(i), - get_colored_icon(i, color)); + int i = + iconComboBox->findData(QString::fromStdString(markerIcon.second.name)); + QIcon icon = get_colored_icon(markerIcon.second, color); + if (i < 0) + { + iconComboBox->addItem( + icon, QString(""), QString::fromStdString(markerIcon.second.name)); + } + else + { + self_->ui->iconComboBox->setItemIcon(i, icon); + } } } diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp index eb66e889..5dac04ea 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -21,7 +21,7 @@ class EditMarkerDialog : public QDialog public: explicit EditMarkerDialog(QWidget* parent = nullptr); - ~EditMarkerDialog(); + ~EditMarkerDialog() override; void setup(); void setup(double latitude, double longitude); diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui index d6b6dc87..f6d9c28d 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui @@ -7,88 +7,14 @@ 0 0 400 - 211 + 249 Edit Location Marker - - - - Icon - - - - - - - Qt::Orientation::Horizontal - - - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok - - - - - - - true - - - - - - - Latitude - - - - - - - 4 - - - -180.000000000000000 - - - 180.000000000000000 - - - - - - - - - - Name - - - - - - - 4 - - - -90.000000000000000 - - - 90.000000000000000 - - - - - - - Longitude - - - - + Qt::Orientation::Vertical @@ -101,14 +27,7 @@ - - - - Icon Color - - - - + @@ -146,6 +65,103 @@ + + + + 4 + + + -90.000000000000000 + + + 90.000000000000000 + + + + + + + Name + + + + + + + Icon + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + Longitude + + + + + + + Icon Color + + + + + + + Latitude + + + + + + + Add Custom Icon + + + ... + + + + + + + 4 + + + -180.000000000000000 + + + 180.000000000000000 + + + + + + + + + + + 0 + 0 + + + + true + + + From 4432bcb93c4f72d8a47731f197249dc98576886a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 14:12:18 -0500 Subject: [PATCH 317/762] Fixed existing tests in location_marker_part2 --- test/data | 2 +- .../scwx/qt/model/marker_model.test.cpp | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/test/data b/test/data index 4b4d9c54..42783ea4 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 4b4d9c54b8218aa2297dbd457e3747091570f0d2 +Subproject commit 42783ea4f3b118b2b0f3266efb53f9b4384f069b diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp index 700ffc6e..8846c9cd 100644 --- a/test/source/scwx/qt/model/marker_model.test.cpp +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -30,6 +31,9 @@ static std::mutex initializedMutex {}; static std::condition_variable initializedCond {}; static bool initialized; +static const boost::gil::rgba8_pixel_t defaultIconColor = + util::color::ToRgba8PixelT("#ffff0000"); + void CompareFiles(const std::string& file1, const std::string& file2) { std::ifstream ifs1 {file1}; @@ -49,8 +53,7 @@ void CopyFile(const std::string& from, const std::string& to) CompareFiles(from, to); } -typedef void TestFunction(std::shared_ptr manager, - MarkerModel& model); +using TestFunction = void (std::shared_ptr, MarkerModel &); void RunTest(const std::string& filename, TestFunction testFunction) { @@ -65,7 +68,7 @@ void RunTest(const std::string& filename, TestFunction testFunction) initialized = false; QObject::connect(manager.get(), &manager::MarkerManager::MarkersInitialized, - [](size_t count) + []() { std::unique_lock lock(initializedMutex); initialized = true; @@ -119,7 +122,7 @@ TEST(MarkerModelTest, AddRemove) RunTest(ONE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) - { manager->add_marker(types::MarkerInfo("Null", 0, 0)); }); + { manager->add_marker(types::MarkerInfo("Null", 0, 0, "images/location-marker", defaultIconColor)); }); RunTest( EMPTY_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel& model) @@ -143,11 +146,11 @@ TEST(MarkerModelTest, AddFive) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("Null", 0, 0)); - manager->add_marker(types::MarkerInfo("North", 90, 0)); - manager->add_marker(types::MarkerInfo("South", -90, 0)); - manager->add_marker(types::MarkerInfo("East", 0, 90)); - manager->add_marker(types::MarkerInfo("West", 0, -90)); + manager->add_marker(types::MarkerInfo("Null", 0, 0, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("North", 90, 0, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("South", -90, 0, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("East", 0, 90, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("West", 0, -90, "images/location-marker", defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -161,10 +164,10 @@ TEST(MarkerModelTest, AddFour) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("North", 90, 0)); - manager->add_marker(types::MarkerInfo("South", -90, 0)); - manager->add_marker(types::MarkerInfo("East", 0, 90)); - manager->add_marker(types::MarkerInfo("West", 0, -90)); + manager->add_marker(types::MarkerInfo("North", 90, 0, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("South", -90, 0, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("East", 0, 90, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("West", 0, -90, "images/location-marker", defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); From 5d8f3786de20c8344f3c498b071cdd146ac501a7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 15:12:53 -0500 Subject: [PATCH 318/762] Remove unneeded data variable --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index f182735c..70625be4 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -271,7 +271,6 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) auto* iconComboBox = self_->ui->iconComboBox; - QVariant data = self_->ui->iconComboBox->currentData(); self_->ui->iconComboBox->clear(); for (auto& markerIcon : markerManager_->get_icons()) { From d8233a2c4107fb35e654b3aa019970f5d709c545 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 16:24:52 -0500 Subject: [PATCH 319/762] enable having markers without names --- .../source/scwx/qt/manager/marker_manager.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 75eb16fa..b4396b2f 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -190,17 +190,14 @@ void MarkerManager::Impl::ReadMarkerSettings() { auto record = boost::json::value_to(markerEntry); - if (!record.markerInfo_.name.empty()) - { - types::MarkerId id = NewId(); - size_t index = markerRecords_.size(); - record.markerInfo_.id = id; - markerRecords_.emplace_back( - std::make_shared(record.markerInfo_)); - idToIndex_.emplace(id, index); + types::MarkerId id = NewId(); + size_t index = markerRecords_.size(); + record.markerInfo_.id = id; + markerRecords_.emplace_back( + std::make_shared(record.markerInfo_)); + idToIndex_.emplace(id, index); - self_->add_icon(record.markerInfo_.iconName, true); - } + self_->add_icon(record.markerInfo_.iconName, true); } catch (const std::exception& ex) { From 89ef2566478584e3d13753a58f0f7e7985f43d01 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 17:44:46 -0500 Subject: [PATCH 320/762] Modify lat/lon dialog to make them somewhat nicer to work with --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui index f6d9c28d..3bfad9a6 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui @@ -67,8 +67,11 @@ + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + - 4 + 5 -90.000000000000000 @@ -135,8 +138,11 @@ + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + - 4 + 5 -180.000000000000000 From 3629dd36f2f01cbc88d9e13fcd97353cdc6dc76e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 9 Dec 2024 17:46:09 -0500 Subject: [PATCH 321/762] Add part1 to part2 location marker conversion test --- .../scwx/qt/model/marker_model.test.cpp | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp index 8846c9cd..f152ddec 100644 --- a/test/source/scwx/qt/model/marker_model.test.cpp +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -26,6 +26,8 @@ static const std::string ONE_MARKERS_FILE = std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-one.json"; static const std::string FIVE_MARKERS_FILE = std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-five.json"; +static const std::string PART1_MARKER_FILE = + std::string(SCWX_TEST_DATA_DIR) + "/json/markers/markers-part1.json"; static std::mutex initializedMutex {}; static std::condition_variable initializedCond {}; @@ -33,6 +35,8 @@ static bool initialized; static const boost::gil::rgba8_pixel_t defaultIconColor = util::color::ToRgba8PixelT("#ffff0000"); +static const std::string defaultIconName = "images/location-marker"; + void CompareFiles(const std::string& file1, const std::string& file2) { @@ -122,7 +126,7 @@ TEST(MarkerModelTest, AddRemove) RunTest(ONE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) - { manager->add_marker(types::MarkerInfo("Null", 0, 0, "images/location-marker", defaultIconColor)); }); + { manager->add_marker(types::MarkerInfo("Null", 0, 0, defaultIconName, defaultIconColor)); }); RunTest( EMPTY_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel& model) @@ -146,11 +150,11 @@ TEST(MarkerModelTest, AddFive) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("Null", 0, 0, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("North", 90, 0, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("South", -90, 0, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("East", 0, 90, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("West", 0, -90, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("Null", 0, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -164,10 +168,10 @@ TEST(MarkerModelTest, AddFour) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("North", 90, 0, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("South", -90, 0, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("East", 0, 90, "images/location-marker", defaultIconColor)); - manager->add_marker(types::MarkerInfo("West", 0, -90, "images/location-marker", defaultIconColor)); + manager->add_marker(types::MarkerInfo("North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo("West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -238,6 +242,17 @@ TEST(MarkerModelTest, RemoveFour) EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); } +TEST(MarkerModelTest, UpdateFromPart1) +{ + CopyFile(PART1_MARKER_FILE, TEMP_MARKERS_FILE); + + RunTest(ONE_MARKERS_FILE, + [](std::shared_ptr, MarkerModel&) {}); + + std::filesystem::remove(TEMP_MARKERS_FILE); + EXPECT_EQ(std::filesystem::exists(TEMP_MARKERS_FILE), false); +} + } // namespace model } // namespace qt } // namespace scwx From e62ef3a7f3b676ca001e4d37e9e595ab658775d2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 10 Dec 2024 10:14:42 -0500 Subject: [PATCH 322/762] modified comments from TODO to question. Still needs checking --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index b4396b2f..5678106f 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -206,7 +206,7 @@ void MarkerManager::Impl::ReadMarkerSettings() } util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); // TODO should code be moved to ResourceManager (probrably) + textureAtlas.BuildAtlas(2048, 2048); // Should this code be moved to ResourceManager? logger_->debug("{} location marker entries", markerRecords_.size()); } @@ -457,7 +457,7 @@ void MarkerManager::add_icon(const std::string& name, bool startup) if (!startup) { util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); // TODO should code be moved to ResourceManager (probrably) + textureAtlas.BuildAtlas(2048, 2048); // Should this code be moved to ResourceManager? Q_EMIT IconAdded(name); } } From cac89129af36676159ebc4c1888a732472b18147 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 14 Dec 2024 10:16:18 -0500 Subject: [PATCH 323/762] Location markers part2 clang-format fixes --- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 2 +- .../source/scwx/qt/manager/marker_manager.cpp | 25 +++++------ .../source/scwx/qt/manager/marker_manager.hpp | 9 ++-- scwx-qt/source/scwx/qt/model/marker_model.cpp | 3 +- scwx-qt/source/scwx/qt/types/marker_types.hpp | 45 ++++++++++--------- .../source/scwx/qt/types/texture_types.cpp | 6 +-- .../source/scwx/qt/ui/edit_marker_dialog.cpp | 25 +++++------ .../source/scwx/qt/ui/edit_marker_dialog.hpp | 3 +- .../scwx/qt/model/marker_model.test.cpp | 37 +++++++++------ 9 files changed, 79 insertions(+), 76 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 71501930..743b0df9 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -903,7 +903,7 @@ bool GeoIcons::RunMousePicking( const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, const common::Coordinate& /* mouseGeoCoords */, - std::shared_ptr& eventHandler ) + std::shared_ptr& eventHandler) { std::unique_lock lock {p->iconMutex_}; diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 5678106f..21cb90cd 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -28,11 +28,11 @@ namespace manager static const std::string logPrefix_ = "scwx::qt::manager::marker_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static const std::string kNameName_ = "name"; -static const std::string kLatitudeName_ = "latitude"; -static const std::string kLongitudeName_ = "longitude"; -static const std::string kIconName_ = "icon"; -static const std::string kIconColorName_ = "icon-color"; +static const std::string kNameName_ = "name"; +static const std::string kLatitudeName_ = "latitude"; +static const std::string kLongitudeName_ = "longitude"; +static const std::string kIconName_ = "icon"; +static const std::string kIconColorName_ = "icon-color"; static const std::string defaultIconName = "images/location-marker"; @@ -68,7 +68,6 @@ public: class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(const types::MarkerInfo& info) : markerInfo_ {info} { @@ -93,7 +92,6 @@ public: util::color::ToArgbString(record->markerInfo_.iconColor)}}; } - friend MarkerRecord tag_invoke(boost::json::value_to_tag, const boost::json::value& jv) { @@ -102,7 +100,7 @@ public: const boost::json::object& jo = jv.as_object(); - std::string iconName = defaultIconName; + std::string iconName = defaultIconName; boost::gil::rgba8_pixel_t iconColor = defaultIconColor; if (jo.contains(kIconName_) && jo.at(kIconName_).is_string()) @@ -112,7 +110,8 @@ public: if (jo.contains(kIconColorName_) && jo.at(kIconName_).is_string()) { - try { + try + { iconColor = util::color::ToRgba8PixelT( boost::json::value_to(jv.at(kIconColorName_))); } @@ -181,7 +180,6 @@ void MarkerManager::Impl::ReadMarkerSettings() { // For each marker entry auto& markerArray = markerJson.as_array(); - //std::vector fileNames {}; markerRecords_.reserve(markerArray.size()); idToIndex_.reserve(markerArray.size()); for (auto& markerEntry : markerArray) @@ -206,13 +204,13 @@ void MarkerManager::Impl::ReadMarkerSettings() } util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); // Should this code be moved to ResourceManager? + textureAtlas.BuildAtlas( + 2048, 2048); // Should this code be moved to ResourceManager? logger_->debug("{} location marker entries", markerRecords_.size()); } } - Q_EMIT self_->MarkersUpdated(); } @@ -457,7 +455,8 @@ void MarkerManager::add_icon(const std::string& name, bool startup) if (!startup) { util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); // Should this code be moved to ResourceManager? + textureAtlas.BuildAtlas( + 2048, 2048); // Should this code be moved to ResourceManager? Q_EMIT IconAdded(name); } } diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp index 9e897660..7004e117 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -23,11 +23,11 @@ public: size_t marker_count(); std::optional get_marker(types::MarkerId id); - std::optional get_index(types::MarkerId id); + std::optional get_index(types::MarkerId id); void set_marker(types::MarkerId id, const types::MarkerInfo& marker); types::MarkerId add_marker(const types::MarkerInfo& marker); - void remove_marker(types::MarkerId id); - void move_marker(size_t from, size_t to); + void remove_marker(types::MarkerId id); + void move_marker(size_t from, size_t to); void add_icon(const std::string& name, bool startup = false); std::optional get_icon(const std::string& name); @@ -39,7 +39,7 @@ public: void set_marker_settings_path(const std::string& path); static std::shared_ptr Instance(); - static const std::string& getDefaultIconName(); + static const std::string& getDefaultIconName(); signals: void MarkersInitialized(size_t count); @@ -51,7 +51,6 @@ signals: void IconsReady(); void IconAdded(std::string name); - private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 7c012921..32294de0 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -133,7 +133,8 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const { std::optional icon = p->markerManager_->get_icon(markerInfo->iconName); - if (icon) { + if (icon) + { return util::modulateColors(icon->qIcon, QSize(iconSize_, iconSize_), QColor(markerInfo->iconColor[0], diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 1848adfa..09f9a06b 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -31,7 +31,7 @@ struct MarkerInfo { } - MarkerId id{0}; + MarkerId id {0}; std::string name; double latitude; double longitude; @@ -39,37 +39,38 @@ struct MarkerInfo boost::gil::rgba8_pixel_t iconColor; }; -struct MarkerIconInfo { +struct MarkerIconInfo +{ explicit MarkerIconInfo(types::ImageTexture texture, std::int32_t hotX, std::int32_t hotY) : - name{types::GetTextureName(texture)}, - path{types::GetTexturePath(texture)}, - hotX{hotX}, - hotY{hotY}, - qIcon{QIcon(QString::fromStdString(path))}, - image{} + name {types::GetTextureName(texture)}, + path {types::GetTexturePath(texture)}, + hotX {hotX}, + hotY {hotY}, + qIcon {QIcon(QString::fromStdString(path))}, + image {} { } - explicit MarkerIconInfo(const std::string& path, - std::int32_t hotX, - std::int32_t hotY, + explicit MarkerIconInfo(const std::string& path, + std::int32_t hotX, + std::int32_t hotY, std::shared_ptr image) : - name{path}, - path{path}, - hotX{hotX}, - hotY{hotY}, - qIcon{QIcon(QString::fromStdString(path))}, - image{image} + name {path}, + path {path}, + hotX {hotX}, + hotY {hotY}, + qIcon {QIcon(QString::fromStdString(path))}, + image {image} { } - std::string name; - std::string path; - std::int32_t hotX; - std::int32_t hotY; - QIcon qIcon; + std::string name; + std::string path; + std::int32_t hotX; + std::int32_t hotY; + QIcon qIcon; std::optional> image; }; diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp index 336a26d8..18efd9b9 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.cpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -46,14 +46,12 @@ static const std::unordered_map imageTextureInfo_ { {ImageTexture::LocationMarker, {"images/location-marker", ":/res/textures/images/location-marker.svg"}}, {ImageTexture::LocationPin, - {"images/location-pin", - ":/res/icons/font-awesome-6/location-pin.svg"}}, + {"images/location-pin", ":/res/icons/font-awesome-6/location-pin.svg"}}, {ImageTexture::LocationStar, {"images/location-star", ":/res/icons/font-awesome-6/star-solid-white.svg"}}, {ImageTexture::LocationTent, - {"images/location-tent", - ":/res/icons/font-awesome-6/tent-solid.svg"}}, + {"images/location-tent", ":/res/icons/font-awesome-6/tent-solid.svg"}}, {ImageTexture::MapboxLogo, {"images/mapbox-logo", ":/res/textures/images/mapbox-logo.svg"}}, {ImageTexture::MapTilerLogo, diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index 70625be4..b3c34e96 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -30,10 +30,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class EditMarkerDialog::Impl { public: - explicit Impl(EditMarkerDialog* self) : - self_{self} - { - } + explicit Impl(EditMarkerDialog* self) : self_ {self} {} void show_color_dialog(); void show_icon_file_dialog(); @@ -46,15 +43,15 @@ public: void handle_rejected(); EditMarkerDialog* self_; - QPushButton* deleteButton_; + QPushButton* deleteButton_; QIcon get_colored_icon(const types::MarkerIconInfo& marker, const std::string& color); std::shared_ptr markerManager_ = manager::MarkerManager::Instance(); types::MarkerId editId_; - bool adding_; - std::string setIconOnAdded_{""}; + bool adding_; + std::string setIconOnAdded_ {""}; }; QIcon EditMarkerDialog::Impl::get_colored_icon( @@ -66,9 +63,9 @@ QIcon EditMarkerDialog::Impl::get_colored_icon( } EditMarkerDialog::EditMarkerDialog(QWidget* parent) : - QDialog(parent), - p {std::make_unique(this)}, - ui(new Ui::EditMarkerDialog) + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::EditMarkerDialog) { ui->setupUi(this); @@ -98,7 +95,7 @@ void EditMarkerDialog::setup(double latitude, double longitude) // By default use foreground color as marker color, mainly so the icons // are vissable in the dropdown menu. QColor color = QWidget::palette().color(QWidget::foregroundRole()); - p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( + p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( "", latitude, longitude, @@ -114,8 +111,7 @@ void EditMarkerDialog::setup(double latitude, double longitude) void EditMarkerDialog::setup(types::MarkerId id) { - std::optional marker = - p->markerManager_->get_marker(id); + std::optional marker = p->markerManager_->get_marker(id); if (!marker) { return; @@ -143,7 +139,7 @@ void EditMarkerDialog::setup(types::MarkerId id) types::MarkerInfo EditMarkerDialog::get_marker_info() const { - QString colorName = ui->iconColorLineEdit->text(); + QString colorName = ui->iconColorLineEdit->text(); boost::gil::rgba8_pixel_t color = util::color::ToRgba8PixelT(colorName.toStdString()); @@ -270,7 +266,6 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) auto* iconComboBox = self_->ui->iconComboBox; - self_->ui->iconComboBox->clear(); for (auto& markerIcon : markerManager_->get_icons()) { diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp index 5dac04ea..4008b990 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -32,10 +32,9 @@ public: private: class Impl; std::unique_ptr p; - Ui::EditMarkerDialog* ui; + Ui::EditMarkerDialog* ui; }; - } // namespace ui } // namespace qt } // namespace scwx diff --git a/test/source/scwx/qt/model/marker_model.test.cpp b/test/source/scwx/qt/model/marker_model.test.cpp index f152ddec..74ad28a9 100644 --- a/test/source/scwx/qt/model/marker_model.test.cpp +++ b/test/source/scwx/qt/model/marker_model.test.cpp @@ -10,7 +10,6 @@ #include #include - namespace scwx { namespace qt @@ -37,7 +36,6 @@ static const boost::gil::rgba8_pixel_t defaultIconColor = util::color::ToRgba8PixelT("#ffff0000"); static const std::string defaultIconName = "images/location-marker"; - void CompareFiles(const std::string& file1, const std::string& file2) { std::ifstream ifs1 {file1}; @@ -57,7 +55,8 @@ void CopyFile(const std::string& from, const std::string& to) CompareFiles(from, to); } -using TestFunction = void (std::shared_ptr, MarkerModel &); +using TestFunction = void(std::shared_ptr, + MarkerModel&); void RunTest(const std::string& filename, TestFunction testFunction) { @@ -126,7 +125,10 @@ TEST(MarkerModelTest, AddRemove) RunTest(ONE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) - { manager->add_marker(types::MarkerInfo("Null", 0, 0, defaultIconName, defaultIconColor)); }); + { + manager->add_marker(types::MarkerInfo( + "Null", 0, 0, defaultIconName, defaultIconColor)); + }); RunTest( EMPTY_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel& model) @@ -150,11 +152,16 @@ TEST(MarkerModelTest, AddFive) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("Null", 0, 0, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("North", 90, 0, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("South", -90, 0, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("East", 0, 90, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("West", 0, -90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "Null", 0, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); @@ -168,10 +175,14 @@ TEST(MarkerModelTest, AddFour) RunTest(FIVE_MARKERS_FILE, [](std::shared_ptr manager, MarkerModel&) { - manager->add_marker(types::MarkerInfo("North", 90, 0, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("South", -90, 0, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("East", 0, 90, defaultIconName, defaultIconColor)); - manager->add_marker(types::MarkerInfo("West", 0, -90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "North", 90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "South", -90, 0, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "East", 0, 90, defaultIconName, defaultIconColor)); + manager->add_marker(types::MarkerInfo( + "West", 0, -90, defaultIconName, defaultIconColor)); }); std::filesystem::remove(TEMP_MARKERS_FILE); From 7ab12e7b4bc9ed14dc0a38a0cf1f4ad00b121888 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 14 Dec 2024 11:17:30 -0500 Subject: [PATCH 324/762] location markers part2 initial clang-tidy changes --- .../source/scwx/qt/manager/marker_manager.cpp | 40 ++++++++--------- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 5 ++- .../source/scwx/qt/ui/edit_marker_dialog.cpp | 44 ++++++++++--------- .../source/scwx/qt/ui/edit_marker_dialog.hpp | 2 +- .../scwx/qt/ui/marker_settings_widget.cpp | 4 +- .../source/scwx/qt/util/q_color_modulate.cpp | 23 +++++++--- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 21cb90cd..cf25cbcb 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -168,7 +168,7 @@ void MarkerManager::Impl::ReadMarkerSettings() boost::json::value markerJson = nullptr; { - std::unique_lock lock(markerRecordLock_); + const std::unique_lock lock(markerRecordLock_); // Determine if marker settings exists if (std::filesystem::exists(markerSettingsPath_)) @@ -188,9 +188,9 @@ void MarkerManager::Impl::ReadMarkerSettings() { auto record = boost::json::value_to(markerEntry); - types::MarkerId id = NewId(); - size_t index = markerRecords_.size(); - record.markerInfo_.id = id; + const types::MarkerId id = NewId(); + const size_t index = markerRecords_.size(); + record.markerInfo_.id = id; markerRecords_.emplace_back( std::make_shared(record.markerInfo_)); idToIndex_.emplace(id, index); @@ -218,7 +218,7 @@ void MarkerManager::Impl::WriteMarkerSettings() { logger_->info("Saving location marker settings"); - std::shared_lock lock(markerRecordLock_); + const std::shared_lock lock(markerRecordLock_); auto markerJson = boost::json::value_from(markerRecords_); util::json::WriteJsonFile(markerSettingsPath_, markerJson); } @@ -263,7 +263,7 @@ MarkerManager::MarkerManager() : p(std::make_unique(this)) // Read Marker settings on startup main::Application::WaitForInitialization(); { - std::unique_lock lock(p->markerIconsLock_); + const std::unique_lock lock(p->markerIconsLock_); p->markerIcons_.reserve( defaultMarkerIcons_.size()); for (auto& icon : defaultMarkerIcons_) @@ -295,7 +295,7 @@ size_t MarkerManager::marker_count() std::optional MarkerManager::get_marker(types::MarkerId id) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return {}; @@ -313,7 +313,7 @@ std::optional MarkerManager::get_marker(types::MarkerId id) std::optional MarkerManager::get_index(types::MarkerId id) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return {}; @@ -325,7 +325,7 @@ void MarkerManager::set_marker(types::MarkerId id, const types::MarkerInfo& marker) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return; @@ -336,9 +336,9 @@ void MarkerManager::set_marker(types::MarkerId id, logger_->warn("id in idToIndex_ but out of range!"); return; } - std::shared_ptr& markerRecord = + const std::shared_ptr& markerRecord = p->markerRecords_[index]; - markerRecord->markerInfo_ = marker; + markerRecord->markerInfo_ = marker; markerRecord->markerInfo_.id = id; add_icon(marker.iconName); @@ -351,7 +351,7 @@ types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) { types::MarkerId id; { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); id = p->NewId(); size_t index = p->markerRecords_.size(); p->idToIndex_.emplace(id, index); @@ -368,7 +368,7 @@ types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) void MarkerManager::remove_marker(types::MarkerId id) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (!p->idToIndex_.contains(id)) { return; @@ -399,7 +399,7 @@ void MarkerManager::remove_marker(types::MarkerId id) void MarkerManager::move_marker(size_t from, size_t to) { { - std::unique_lock lock(p->markerRecordLock_); + const std::unique_lock lock(p->markerRecordLock_); if (from >= p->markerRecords_.size() || to >= p->markerRecords_.size()) { return; @@ -430,7 +430,7 @@ void MarkerManager::move_marker(size_t from, size_t to) void MarkerManager::for_each(std::function func) { - std::shared_lock lock(p->markerRecordLock_); + const std::shared_lock lock(p->markerRecordLock_); for (auto marker : p->markerRecords_) { func(marker->markerInfo_); @@ -440,12 +440,12 @@ void MarkerManager::for_each(std::function func) void MarkerManager::add_icon(const std::string& name, bool startup) { { - std::unique_lock lock(p->markerIconsLock_); + const std::unique_lock lock(p->markerIconsLock_); if (p->markerIcons_.contains(name)) { return; } - std::shared_ptr image = + const std::shared_ptr image = ResourceManager::LoadImageResource(name); auto icon = types::MarkerIconInfo(name, -1, -1, image); @@ -464,7 +464,7 @@ void MarkerManager::add_icon(const std::string& name, bool startup) std::optional MarkerManager::get_icon(const std::string& name) { - std::shared_lock lock(p->markerIconsLock_); + const std::shared_lock lock(p->markerIconsLock_); if (p->markerIcons_.contains(name)) { return p->markerIcons_.at(name); @@ -476,7 +476,7 @@ MarkerManager::get_icon(const std::string& name) const std::unordered_map MarkerManager::get_icons() { - std::shared_lock lock(p->markerIconsLock_); + const std::shared_lock lock(p->markerIconsLock_); return p->markerIcons_; } @@ -492,7 +492,7 @@ std::shared_ptr MarkerManager::Instance() static std::weak_ptr markerManagerReference_ {}; static std::mutex instanceMutex_ {}; - std::unique_lock lock(instanceMutex_); + const std::unique_lock lock(instanceMutex_); std::shared_ptr markerManager = markerManagerReference_.lock(); diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 0bad94f6..a0ac668f 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -72,9 +72,10 @@ void MarkerLayer::Impl::ReloadMarkers() { // must use local ID, instead of reference to marker in event handler // callback. - types::MarkerId id = marker.id; + const types::MarkerId id = marker.id; - std::shared_ptr icon = geoIcons_->AddIcon(); + const std::shared_ptr icon = + geoIcons_->AddIcon(); geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index b3c34e96..47bfb07f 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -30,7 +30,12 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class EditMarkerDialog::Impl { public: - explicit Impl(EditMarkerDialog* self) : self_ {self} {} + explicit Impl(EditMarkerDialog* self) : + self_ {self}, + deleteButton_ {self_->ui->buttonBox->addButton( + "Delete", QDialogButtonBox::DestructiveRole)} + { + } void show_color_dialog(); void show_icon_file_dialog(); @@ -49,8 +54,8 @@ public: std::shared_ptr markerManager_ = manager::MarkerManager::Instance(); - types::MarkerId editId_; - bool adding_; + types::MarkerId editId_ {0}; + bool adding_ {false}; std::string setIconOnAdded_ {""}; }; @@ -75,8 +80,6 @@ EditMarkerDialog::EditMarkerDialog(QWidget* parent) : QString(""), QString::fromStdString(markerIcon.second.name)); } - p->deleteButton_ = - ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); p->connect_signals(); } @@ -94,8 +97,8 @@ void EditMarkerDialog::setup(double latitude, double longitude) { // By default use foreground color as marker color, mainly so the icons // are vissable in the dropdown menu. - QColor color = QWidget::palette().color(QWidget::foregroundRole()); - p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( + const QColor color = QWidget::palette().color(QWidget::foregroundRole()); + p->editId_ = p->markerManager_->add_marker(types::MarkerInfo( "", latitude, longitude, @@ -120,7 +123,8 @@ void EditMarkerDialog::setup(types::MarkerId id) p->editId_ = id; p->adding_ = false; - std::string iconColorStr = util::color::ToArgbString(marker->iconColor); + const std::string iconColorStr = + util::color::ToArgbString(marker->iconColor); p->set_icon_color(iconColorStr); int iconIndex = @@ -139,8 +143,8 @@ void EditMarkerDialog::setup(types::MarkerId id) types::MarkerInfo EditMarkerDialog::get_marker_info() const { - QString colorName = ui->iconColorLineEdit->text(); - boost::gil::rgba8_pixel_t color = + const QString colorName = ui->iconColorLineEdit->text(); + const boost::gil::rgba8_pixel_t color = util::color::ToRgba8PixelT(colorName.toStdString()); return types::MarkerInfo( @@ -159,7 +163,7 @@ void EditMarkerDialog::Impl::show_color_dialog() dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setOption(QColorDialog::ColorDialogOption::ShowAlphaChannel); - QColor initialColor(self_->ui->iconColorLineEdit->text()); + const QColor initialColor(self_->ui->iconColorLineEdit->text()); if (initialColor.isValid()) { dialog->setCurrentColor(initialColor); @@ -170,7 +174,7 @@ void EditMarkerDialog::Impl::show_color_dialog() self_, [this](const QColor& qColor) { - QString colorName = + const QString colorName = qColor.name(QColor::NameFormat::HexArgb); self_->ui->iconColorLineEdit->setText(colorName); set_icon_color(colorName.toStdString()); @@ -180,7 +184,6 @@ void EditMarkerDialog::Impl::show_color_dialog() void EditMarkerDialog::Impl::show_icon_file_dialog() { - auto* dialog = new QFileDialog(self_); dialog->setFileMode(QFileDialog::ExistingFile); @@ -192,7 +195,7 @@ void EditMarkerDialog::Impl::show_icon_file_dialog() self_, [this](const QString& file) { - std::string path = + const std::string path = QDir::toNativeSeparators(file).toStdString(); setIconOnAdded_ = path; markerManager_->add_icon(path); @@ -224,13 +227,12 @@ void EditMarkerDialog::Impl::connect_signals() connect(self_->ui->iconColorLineEdit, &QLineEdit::textEdited, self_, - [=, this](const QString& text) - { set_icon_color(text.toStdString()); }); + [this](const QString& text) { set_icon_color(text.toStdString()); }); connect(self_->ui->iconColorButton, &QAbstractButton::clicked, self_, - [=, this]() { show_color_dialog(); }); + [this]() { show_color_dialog(); }); connect(self_->ui->iconFileOpenButton, &QPushButton::clicked, @@ -242,13 +244,13 @@ void EditMarkerDialog::Impl::connect_signals() self_, [this]() { - std::string color = + const std::string color = self_->ui->iconColorLineEdit->text().toStdString(); set_icon_color(color); if (setIconOnAdded_ != "") { - int i = self_->ui->iconComboBox->findData( + const int i = self_->ui->iconComboBox->findData( QString::fromStdString(setIconOnAdded_)); if (i >= 0) { @@ -269,9 +271,9 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) self_->ui->iconComboBox->clear(); for (auto& markerIcon : markerManager_->get_icons()) { - int i = + const int i = iconComboBox->findData(QString::fromStdString(markerIcon.second.name)); - QIcon icon = get_colored_icon(markerIcon.second, color); + const QIcon icon = get_colored_icon(markerIcon.second, color); if (i < 0) { iconComboBox->addItem( diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp index 4008b990..6c6f6e6c 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -27,7 +27,7 @@ public: void setup(double latitude, double longitude); void setup(types::MarkerId id); - types::MarkerInfo get_marker_info() const; + [[nodiscard]] types::MarkerInfo get_marker_info() const; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index ba6cf88b..cc98302d 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -102,7 +102,7 @@ void MarkerSettingsWidgetImpl::ConnectSignals() return; } - bool itemSelected = selected.size() > 0; + const bool itemSelected = selected.size() > 0; self_->ui->removeButton->setEnabled(itemSelected); }); QObject::connect(self_->ui->markerView, @@ -110,7 +110,7 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this](const QModelIndex& index) { - int row = index.row(); + const int row = index.row(); if (row < 0) { return; diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp index 567fc62d..007ceb47 100644 --- a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp @@ -20,11 +20,22 @@ void modulateColors_(QImage& image, const QColor& color) QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); ++x) { - QRgb& rgb = line[x]; - int red = qRed(rgb) * color.redF(); - int green = qGreen(rgb) * color.greenF(); - int blue = qBlue(rgb) * color.blueF(); - int alpha = qAlpha(rgb) * color.alphaF(); + QRgb& rgb = line[x]; + /* clang-format off + * NOLINTBEGIN(cppcoreguidelines-narrowing-conversions, bugprone-narrowing-conversions) + * qRed/qGreen/qBlue/qAlpha return values 0-255, handlable by float + * redF/greenF/blueF/alphaF are all 0-1, so output is 0-255 + * Rounding is fine for this. + * clang-format on + */ + const int red = qRed(rgb) * color.redF(); + const int green = qGreen(rgb) * color.greenF(); + const int blue = qBlue(rgb) * color.blueF(); + const int alpha = qAlpha(rgb) * color.alphaF(); + /* clang-format off + * NOLINTEND(cppcoreguidelines-narrowing-conversions, bugprone-narrowing-conversions) + * clang-format on + */ rgb = qRgba(red, green, blue, alpha); } @@ -47,7 +58,7 @@ QPixmap modulateColors(const QPixmap& pixmap, const QColor& color) QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color) { - QPixmap pixmap = modulateColors(icon.pixmap(size), color); + const QPixmap pixmap = modulateColors(icon.pixmap(size), color); return QIcon(pixmap); } From d9d8f8de8aecbee193f0153925ea2f412f137e07 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 14 Dec 2024 11:32:09 -0500 Subject: [PATCH 325/762] Fix preemptive initialization because of clang-tidy changes --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index 47bfb07f..ef7966d1 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -30,12 +30,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class EditMarkerDialog::Impl { public: - explicit Impl(EditMarkerDialog* self) : - self_ {self}, - deleteButton_ {self_->ui->buttonBox->addButton( - "Delete", QDialogButtonBox::DestructiveRole)} - { - } + explicit Impl(EditMarkerDialog* self) : self_ {self} {} void show_color_dialog(); void show_icon_file_dialog(); @@ -48,7 +43,7 @@ public: void handle_rejected(); EditMarkerDialog* self_; - QPushButton* deleteButton_; + QPushButton* deleteButton_ {nullptr}; QIcon get_colored_icon(const types::MarkerIconInfo& marker, const std::string& color); @@ -80,6 +75,8 @@ EditMarkerDialog::EditMarkerDialog(QWidget* parent) : QString(""), QString::fromStdString(markerIcon.second.name)); } + p->deleteButton_ = + ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); p->connect_signals(); } From 9a5d24544e4e32a34210ddc0b54fe19d93296f19 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 14 Dec 2024 12:17:44 -0500 Subject: [PATCH 326/762] Revert to selected icon when recoloring all the icons --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index ef7966d1..882bbe68 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -265,6 +265,8 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) auto* iconComboBox = self_->ui->iconComboBox; + const QVariant currentIcon = iconComboBox->currentData(); + self_->ui->iconComboBox->clear(); for (auto& markerIcon : markerManager_->get_icons()) { @@ -281,6 +283,15 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) self_->ui->iconComboBox->setItemIcon(i, icon); } } + + const int i = + iconComboBox->findData(currentIcon); + if (i < 0) + { + return; + } + + iconComboBox->setCurrentIndex(i); } void EditMarkerDialog::Impl::handle_accepted() From 23a99f081aa3b52279b0423a932cf6dc1b656bf2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 14 Dec 2024 12:20:47 -0500 Subject: [PATCH 327/762] Fix formatting on most for clang-format for location_markers_part2 --- scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index 882bbe68..e186a4df 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -284,8 +284,7 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) } } - const int i = - iconComboBox->findData(currentIcon); + const int i = iconComboBox->findData(currentIcon); if (i < 0) { return; From 91b4d6c2c2ec539d2708b2335391b1cb1146a70b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 27 Dec 2024 10:32:07 -0500 Subject: [PATCH 328/762] added new test data from other pull requests --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 42783ea4..0d085b1d 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 42783ea4f3b118b2b0f3266efb53f9b4384f069b +Subproject commit 0d085b1df59045e14ca996982b4907b1a0da4fdb From dc284974b35957dafada7c838d1c00836081b7f7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 10:32:21 -0500 Subject: [PATCH 329/762] First pass fixes from discussions. Mostly linter fixes --- .clang-tidy | 2 ++ .../source/scwx/qt/manager/marker_manager.cpp | 17 +++++----- .../scwx/qt/manager/resource_manager.cpp | 14 +++++++-- .../scwx/qt/manager/resource_manager.hpp | 1 + scwx-qt/source/scwx/qt/map/map_widget.cpp | 7 +++-- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 14 ++++++++- scwx-qt/source/scwx/qt/model/marker_model.cpp | 15 ++++++++- scwx-qt/source/scwx/qt/types/marker_types.hpp | 29 +++++++++-------- .../source/scwx/qt/ui/edit_marker_dialog.cpp | 31 ++++++++++--------- .../source/scwx/qt/ui/edit_marker_dialog.hpp | 10 ++---- .../source/scwx/qt/ui/edit_marker_dialog.ui | 2 +- .../scwx/qt/ui/marker_settings_widget.cpp | 16 +++++++--- .../source/scwx/qt/util/q_color_modulate.cpp | 13 +++----- .../source/scwx/qt/util/q_color_modulate.hpp | 10 ++---- 14 files changed, 107 insertions(+), 74 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 602e3d0c..3c98e81d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,4 +10,6 @@ Checks: - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-modernize-use-trailing-return-type' + - '-bugprone-easily-swappable-parameters' + - '-modernize-return-braced-init-list' FormatStyle: 'file' diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index cf25cbcb..a9214d03 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -203,9 +203,7 @@ void MarkerManager::Impl::ReadMarkerSettings() } } - util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas( - 2048, 2048); // Should this code be moved to ResourceManager? + ResourceManager::BuildAtlas(); logger_->debug("{} location marker entries", markerRecords_.size()); } @@ -239,7 +237,7 @@ MarkerManager::Impl::GetMarkerByName(const std::string& name) MarkerManager::MarkerManager() : p(std::make_unique(this)) { - const std::vector defaultMarkerIcons_ { + static const std::vector defaultMarkerIcons_ { types::MarkerIconInfo(types::ImageTexture::LocationMarker, -1, -1), types::MarkerIconInfo(types::ImageTexture::LocationPin, 6, 16), types::MarkerIconInfo(types::ImageTexture::LocationCrosshair, -1, -1), @@ -256,7 +254,7 @@ MarkerManager::MarkerManager() : p(std::make_unique(this)) p->InitializeMarkerSettings(); boost::asio::post(p->threadPool_, - [this, defaultMarkerIcons_]() + [this]() { try { @@ -454,9 +452,7 @@ void MarkerManager::add_icon(const std::string& name, bool startup) if (!startup) { - util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas( - 2048, 2048); // Should this code be moved to ResourceManager? + ResourceManager::BuildAtlas(); Q_EMIT IconAdded(name); } } @@ -465,9 +461,10 @@ std::optional MarkerManager::get_icon(const std::string& name) { const std::shared_lock lock(p->markerIconsLock_); - if (p->markerIcons_.contains(name)) + auto it = p->markerIcons_.find(name); + if (it != p->markerIcons_.end()) { - return p->markerIcons_.at(name); + return it->second; } return {}; diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 443d771b..5299d6ff 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -22,6 +22,9 @@ namespace ResourceManager static const std::string logPrefix_ = "scwx::qt::manager::resource_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static const size_t atlasWidth = 2048; +static const size_t atlasHeight = 2048; + static void LoadFonts(); static void LoadTextures(); @@ -68,8 +71,7 @@ LoadImageResources(const std::vector& urlStrings) if (!images.empty()) { - util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(2048, 2048); + BuildAtlas(); } return images; @@ -103,7 +105,13 @@ static void LoadTextures() GetTexturePath(lineTexture)); } - textureAtlas.BuildAtlas(2048, 2048); + BuildAtlas(); +} + +void BuildAtlas() +{ + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.BuildAtlas(atlasWidth, atlasHeight); } } // namespace ResourceManager diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index 00658891..ec7cf65e 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -22,6 +22,7 @@ std::shared_ptr LoadImageResource(const std::string& urlString); std::vector> LoadImageResources(const std::vector& urlStrings); +void BuildAtlas(); } // namespace ResourceManager } // namespace manager diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 9c68a68a..c5c07b56 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -219,7 +219,7 @@ public: std::shared_ptr layerModel_ { model::LayerModel::Instance()}; - std::shared_ptr editMarkerDialog_; + ui::EditMarkerDialog* editMarkerDialog_; std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; @@ -286,7 +286,10 @@ MapWidget::MapWidget(std::size_t id, const QMapLibre::Settings& settings) : ImGui_ImplQt_RegisterWidget(this); - p->editMarkerDialog_ = std::make_shared(this); + // Qt parent deals with memory management + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + p->editMarkerDialog_ = new ui::EditMarkerDialog(this); + p->ConnectSignals(); } diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index a0ac668f..ba7f3e27 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -77,9 +77,21 @@ void MarkerLayer::Impl::ReloadMarkers() const std::shared_ptr icon = geoIcons_->AddIcon(); + const std::string latitudeString = + common::GetLatitudeString(marker.latitude); + const std::string longitudeString = + common::GetLongitudeString(marker.longitude); + + const std::string hoverText = + marker.name != "" ? + fmt::format( + "{}\n{}, {}", marker.name, latitudeString, longitudeString) : + fmt::format("{}, {}", latitudeString, longitudeString); + + geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); - geoIcons_->SetIconHoverText(icon, marker.name); + geoIcons_->SetIconHoverText(icon, hoverText); geoIcons_->SetIconModulate(icon, marker.iconColor); geoIcons_->RegisterEventHandler( icon, diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 32294de0..77fb7ab7 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -129,7 +129,20 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const break; break; case static_cast(Column::Icon): - if (role == Qt::ItemDataRole::DecorationRole) + if (role == Qt::ItemDataRole::DisplayRole) + { + std::optional icon = + p->markerManager_->get_icon(markerInfo->iconName); + if (icon) + { + return QString::fromStdString(icon->shortName); + } + else + { + return {}; + } + } + else if (role == Qt::ItemDataRole::DecorationRole) { std::optional icon = p->markerManager_->get_icon(markerInfo->iconName); diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index 09f9a06b..f8b1c710 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -2,31 +2,29 @@ #include -#include #include +#include +#include #include +#include #include -namespace scwx -{ -namespace qt -{ -namespace types +namespace scwx::qt::types { using MarkerId = std::uint64_t; struct MarkerInfo { - MarkerInfo(const std::string& name, + MarkerInfo(std::string name, double latitude, double longitude, - const std::string& iconName, + std::string iconName, const boost::gil::rgba8_pixel_t& iconColor) : - name {name}, + name {std::move(name)}, latitude {latitude}, longitude {longitude}, - iconName {iconName}, + iconName {std::move(iconName)}, iconColor {iconColor} { } @@ -41,6 +39,7 @@ struct MarkerInfo struct MarkerIconInfo { + // Initializer for default icons (which use a texture) explicit MarkerIconInfo(types::ImageTexture texture, std::int32_t hotX, std::int32_t hotY) : @@ -51,14 +50,19 @@ struct MarkerIconInfo qIcon {QIcon(QString::fromStdString(path))}, image {} { + auto qName = QString::fromStdString(name); + QStringList parts = qName.split("location-"); + shortName = parts.last().toStdString(); } + // Initializer for custom icons (which use a file path) explicit MarkerIconInfo(const std::string& path, std::int32_t hotX, std::int32_t hotY, std::shared_ptr image) : name {path}, path {path}, + shortName {QFileInfo(path.c_str()).fileName().toStdString()}, hotX {hotX}, hotY {hotY}, qIcon {QIcon(QString::fromStdString(path))}, @@ -68,12 +72,11 @@ struct MarkerIconInfo std::string name; std::string path; + std::string shortName; std::int32_t hotX; std::int32_t hotY; QIcon qIcon; std::optional> image; }; -} // namespace types -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::types diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index e186a4df..a6aaa4d4 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -17,11 +17,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace ui +namespace scwx::qt::ui { static const std::string logPrefix_ = "scwx::qt::ui::edit_marker_dialog"; @@ -71,9 +67,10 @@ EditMarkerDialog::EditMarkerDialog(QWidget* parent) : for (auto& markerIcon : p->markerManager_->get_icons()) { - ui->iconComboBox->addItem(markerIcon.second.qIcon, - QString(""), - QString::fromStdString(markerIcon.second.name)); + ui->iconComboBox->addItem( + markerIcon.second.qIcon, + QString::fromStdString(markerIcon.second.shortName), + QString::fromStdString(markerIcon.second.name)); } p->deleteButton_ = ui->buttonBox->addButton("Delete", QDialogButtonBox::DestructiveRole); @@ -154,7 +151,8 @@ types::MarkerInfo EditMarkerDialog::get_marker_info() const void EditMarkerDialog::Impl::show_color_dialog() { - + // WA_DeleteOnClose manages memory + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) auto* dialog = new QColorDialog(self_); dialog->setAttribute(Qt::WA_DeleteOnClose); @@ -184,7 +182,7 @@ void EditMarkerDialog::Impl::show_icon_file_dialog() auto* dialog = new QFileDialog(self_); dialog->setFileMode(QFileDialog::ExistingFile); - dialog->setNameFilters({"Icon (*.png *.svg)", "All (*)"}); + dialog->setNameFilters({"Icon (*.png *.svg)", "All Files (*)"}); dialog->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(dialog, @@ -256,6 +254,11 @@ void EditMarkerDialog::Impl::connect_signals() } } }); + + connect(self_->ui->buttonBox->button(QDialogButtonBox::Apply), + &QAbstractButton::clicked, + self_, + [this]() { handle_accepted(); }); } void EditMarkerDialog::Impl::set_icon_color(const std::string& color) @@ -276,7 +279,9 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) if (i < 0) { iconComboBox->addItem( - icon, QString(""), QString::fromStdString(markerIcon.second.name)); + icon, + QString::fromStdString(markerIcon.second.shortName), + QString::fromStdString(markerIcon.second.name)); } else { @@ -306,6 +311,4 @@ void EditMarkerDialog::Impl::handle_rejected() } } -} // namespace ui -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp index 6c6f6e6c..c20ebe27 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -8,11 +8,7 @@ namespace Ui class EditMarkerDialog; } -namespace scwx -{ -namespace qt -{ -namespace ui +namespace scwx::qt::ui { class EditMarkerDialog : public QDialog { @@ -35,6 +31,4 @@ private: Ui::EditMarkerDialog* ui; }; -} // namespace ui -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui index 3bfad9a6..d3d47500 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.ui @@ -101,7 +101,7 @@ Qt::Orientation::Horizontal - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index cc98302d..b3ba8440 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -23,15 +23,21 @@ class MarkerSettingsWidgetImpl { public: explicit MarkerSettingsWidgetImpl(MarkerSettingsWidget* self) : - self_ {self}, - markerModel_ {new model::MarkerModel(self_)} + self_ {self}, + markerModel_ {new model::MarkerModel(self_)}, + proxyModel_ {new QSortFilterProxyModel(self_)} { + proxyModel_->setSourceModel(markerModel_); + proxyModel_->setSortRole(Qt::DisplayRole); // TODO types::SortRole + proxyModel_->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel_->setFilterKeyColumn(-1); } void ConnectSignals(); - MarkerSettingsWidget* self_; - model::MarkerModel* markerModel_; + MarkerSettingsWidget* self_; + model::MarkerModel* markerModel_; + QSortFilterProxyModel* proxyModel_; std::shared_ptr markerManager_ { manager::MarkerManager::Instance()}; std::shared_ptr editMarkerDialog_ {nullptr}; @@ -46,7 +52,7 @@ MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : ui->setupUi(this); ui->removeButton->setEnabled(false); - ui->markerView->setModel(p->markerModel_); + ui->markerView->setModel(p->proxyModel_); p->editMarkerDialog_ = std::make_shared(this); diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp index 007ceb47..c205ce88 100644 --- a/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.cpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace util +namespace scwx::qt::util { void modulateColors_(QImage& image, const QColor& color) @@ -20,6 +16,9 @@ void modulateColors_(QImage& image, const QColor& color) QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); ++x) { + // This is pulled from Qt Documentation + // https://doc.qt.io/qt-6/qimage.html#scanLine + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) QRgb& rgb = line[x]; /* clang-format off * NOLINTBEGIN(cppcoreguidelines-narrowing-conversions, bugprone-narrowing-conversions) @@ -62,6 +61,4 @@ QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color) return QIcon(pixmap); } -} // namespace util -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::util diff --git a/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp index c54b852f..35326293 100644 --- a/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp +++ b/scwx-qt/source/scwx/qt/util/q_color_modulate.hpp @@ -6,17 +6,11 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace util +namespace scwx::qt::util { QImage modulateColors(const QImage& image, const QColor& color); QPixmap modulateColors(const QPixmap& pixmap, const QColor& color); QIcon modulateColors(const QIcon& icon, const QSize& size, const QColor& color); -} // namespace util -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::util From 2fd94c6575b226ba20036c80c99e3ddc9453b2dd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 10:33:40 -0500 Subject: [PATCH 330/762] Gracefully fallback when icon cannot be loaded --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 13 +++++++++++-- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 10 +++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index a9214d03..798f94b2 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -446,8 +446,17 @@ void MarkerManager::add_icon(const std::string& name, bool startup) const std::shared_ptr image = ResourceManager::LoadImageResource(name); - auto icon = types::MarkerIconInfo(name, -1, -1, image); - p->markerIcons_.emplace(name, icon); + if (image) + { + auto icon = types::MarkerIconInfo(name, -1, -1, image); + p->markerIcons_.emplace(name, icon); + } + else + { + // defaultIconName should always be in markerIcons, so at is fine + auto icon = p->markerIcons_.at(defaultIconName); + p->markerIcons_.emplace(name, icon); + } } if (!startup) diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index ba7f3e27..906ef7c1 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -88,8 +88,16 @@ void MarkerLayer::Impl::ReloadMarkers() "{}\n{}, {}", marker.name, latitudeString, longitudeString) : fmt::format("{}, {}", latitudeString, longitudeString); + auto iconInfo = markerManager_->get_icon(marker.iconName); + if (iconInfo) + { + geoIcons_->SetIconTexture(icon, iconInfo->name, 0); + } + else + { + geoIcons_->SetIconTexture(icon, marker.iconName, 0); + } - geoIcons_->SetIconTexture(icon, marker.iconName, 0); geoIcons_->SetIconLocation(icon, marker.latitude, marker.longitude); geoIcons_->SetIconHoverText(icon, hoverText); geoIcons_->SetIconModulate(icon, marker.iconColor); From 7edafe8d7874da549f0c2eeb1743b0b9dfc736dd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 10:46:04 -0500 Subject: [PATCH 331/762] Location Markers Part2 clang-format/tidy suggestions --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 2 +- scwx-qt/source/scwx/qt/manager/resource_manager.cpp | 4 ++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 +- scwx-qt/source/scwx/qt/types/marker_types.hpp | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 798f94b2..fd7bee13 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -470,7 +470,7 @@ std::optional MarkerManager::get_icon(const std::string& name) { const std::shared_lock lock(p->markerIconsLock_); - auto it = p->markerIcons_.find(name); + auto it = p->markerIcons_.find(name); if (it != p->markerIcons_.end()) { return it->second; diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 5299d6ff..c6c40a26 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -110,8 +110,8 @@ static void LoadTextures() void BuildAtlas() { - util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - textureAtlas.BuildAtlas(atlasWidth, atlasHeight); + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + textureAtlas.BuildAtlas(atlasWidth, atlasHeight); } } // namespace ResourceManager diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index c5c07b56..0e9846dd 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -219,7 +219,7 @@ public: std::shared_ptr layerModel_ { model::LayerModel::Instance()}; - ui::EditMarkerDialog* editMarkerDialog_; + ui::EditMarkerDialog* editMarkerDialog_ {nullptr}; std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp index f8b1c710..661aa207 100644 --- a/scwx-qt/source/scwx/qt/types/marker_types.hpp +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -50,9 +50,9 @@ struct MarkerIconInfo qIcon {QIcon(QString::fromStdString(path))}, image {} { - auto qName = QString::fromStdString(name); + auto qName = QString::fromStdString(name); QStringList parts = qName.split("location-"); - shortName = parts.last().toStdString(); + shortName = parts.last().toStdString(); } // Initializer for custom icons (which use a file path) From 736fd43b46365bc4798eb72fee73dc444cb034aa Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 10:58:30 -0500 Subject: [PATCH 332/762] Location Markers Part 2 linter suggestions (post rebase) --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 0e9846dd..e363e3dc 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -80,7 +80,6 @@ public: map_(), layerList_ {}, imGuiRendererInitialized_ {false}, - editMarkerDialog_ {nullptr}, radarProductManager_ {nullptr}, radarProductLayer_ {nullptr}, overlayLayer_ {nullptr}, From a947dc6b9f8ef8831c8ec30651c3ed53fc3bed79 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 10 Jan 2025 10:51:25 -0600 Subject: [PATCH 333/762] Add check for NahimicOSD.dll --- scwx-qt/scwx-qt.cmake | 6 +- scwx-qt/source/scwx/qt/main/main.cpp | 7 +- .../scwx/qt/main/process_validation.cpp | 109 ++++++++++++++++++ .../scwx/qt/main/process_validation.hpp | 8 ++ .../scwx/qt/settings/general_settings.cpp | 24 +++- .../scwx/qt/settings/general_settings.hpp | 13 ++- test/data | 2 +- 7 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/main/process_validation.cpp create mode 100644 scwx-qt/source/scwx/qt/main/process_validation.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 706ac038..92ca70a4 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -51,9 +51,11 @@ find_package(Qt${QT_VERSION_MAJOR} set(SRC_EXE_MAIN source/scwx/qt/main/main.cpp) set(HDR_MAIN source/scwx/qt/main/application.hpp - source/scwx/qt/main/main_window.hpp) + source/scwx/qt/main/main_window.hpp + source/scwx/qt/main/process_validation.hpp) set(SRC_MAIN source/scwx/qt/main/application.cpp - source/scwx/qt/main/main_window.cpp) + source/scwx/qt/main/main_window.cpp + source/scwx/qt/main/process_validation.cpp) set(UI_MAIN source/scwx/qt/main/main_window.ui) set(HDR_CONFIG source/scwx/qt/config/county_database.hpp source/scwx/qt/config/radar_site.hpp) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 0e379961..7d66c80d 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,9 @@ int main(int argc, char* argv[]) // Theme ConfigureTheme(args); + // Check process modules for compatibility + scwx::qt::main::CheckProcessModules(); + // Run initial setup if required if (scwx::qt::ui::setup::SetupWizard::IsSetupRequired()) { @@ -170,7 +174,8 @@ static void ConfigureTheme(const std::vector& args) QGuiApplication::styleHints()->setColorScheme(qtColorScheme); std::optional paletteFile; - if (uiStyle == scwx::qt::types::UiStyle::FusionCustom) { + if (uiStyle == scwx::qt::types::UiStyle::FusionCustom) + { paletteFile = generalSettings.theme_file().GetValue(); } else diff --git a/scwx-qt/source/scwx/qt/main/process_validation.cpp b/scwx-qt/source/scwx/qt/main/process_validation.cpp new file mode 100644 index 00000000..61f2cb29 --- /dev/null +++ b/scwx-qt/source/scwx/qt/main/process_validation.cpp @@ -0,0 +1,109 @@ +#include +#include + +#if defined(_WIN32) +# include + +# include +# include + +# include +# include +# include +# include +# include +#endif + +namespace scwx::qt::main +{ + +static const std::string logPrefix_ = "scwx::qt::main::process_validation"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +void CheckProcessModules() +{ +#if defined(_WIN32) + HANDLE process = GetCurrentProcess(); + HMODULE modules[1024]; + DWORD cbNeeded = 0; + + std::vector incompatibleDlls {}; + std::vector descriptions {}; + + auto& processModuleWarningsEnabled = + settings::GeneralSettings::Instance().process_module_warnings_enabled(); + + if (EnumProcessModules(process, modules, sizeof(modules), &cbNeeded)) + { + std::uint32_t numModules = cbNeeded / sizeof(HMODULE); + for (std::uint32_t i = 0; i < numModules; ++i) + { + char modulePath[MAX_PATH]; + if (GetModuleFileNameExA(process, modules[i], modulePath, MAX_PATH)) + { + std::string path = modulePath; + + logger_->trace("DLL Found: {}", path); + + if (boost::algorithm::iends_with(path, "NahimicOSD.dll")) + { + std::string description = + QObject::tr( + "ASUS Sonic Studio injects a Nahimic driver, which causes " + "Supercell Wx to hang. It is suggested to disable the " + "Nahimic service, or to uninstall ASUS Sonic Studio and " + "the Nahimic driver.") + .toStdString(); + + logger_->warn("Incompatible DLL found: {}", path); + logger_->warn("{}", description); + + // Only populate vectors for the message box if warnings are + // enabled + if (processModuleWarningsEnabled.GetValue()) + { + incompatibleDlls.push_back(path); + descriptions.push_back(description); + } + } + } + } + } + + if (!incompatibleDlls.empty()) + { + const std::string header = + QObject::tr( + "The following DLLs have been injected into the Supercell Wx " + "process:") + .toStdString(); + const std::string defaultMessage = + QObject::tr( + "Supercell Wx is known to not run correctly with these DLLs " + "injected. We suggest stopping or uninstalling these services if " + "you experience crashes or unexpected behavior while using " + "Supercell Wx.") + .toStdString(); + + std::string message = fmt::format("{}\n\n{}\n\n{}\n\n{}", + header, + fmt::join(incompatibleDlls, "\n"), + defaultMessage, + fmt::join(descriptions, "\n")); + + QMessageBox dialog(QMessageBox::Icon::Warning, + QObject::tr("Supercell Wx"), + QString::fromStdString(message)); + QCheckBox* checkBox = + new QCheckBox(QObject::tr("Don't show this message again"), &dialog); + dialog.setCheckBox(checkBox); + dialog.exec(); + + // Stage the result of the checkbox. This value will be committed on + // shutdown. + processModuleWarningsEnabled.StageValue(!checkBox->isChecked()); + } +#endif +} + +} // namespace scwx::qt::main diff --git a/scwx-qt/source/scwx/qt/main/process_validation.hpp b/scwx-qt/source/scwx/qt/main/process_validation.hpp new file mode 100644 index 00000000..fb8fabaf --- /dev/null +++ b/scwx-qt/source/scwx/qt/main/process_validation.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace scwx::qt::main +{ + +void CheckProcessModules(); + +} // namespace scwx::qt::main diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index ccd7e886..77ca764e 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -69,6 +69,7 @@ public: nmeaBaudRate_.SetDefault(9600); nmeaSource_.SetDefault(""); positioningPlugin_.SetDefault(defaultPositioningPlugin); + processModuleWarningsEnabled_.SetDefault(true); showMapAttribution_.SetDefault(true); showMapCenter_.SetDefault(false); showMapLogo_.SetDefault(true); @@ -162,12 +163,14 @@ public: SettingsVariable nmeaBaudRate_ {"nmea_baud_rate"}; SettingsVariable nmeaSource_ {"nmea_source"}; SettingsVariable positioningPlugin_ {"positioning_plugin"}; - SettingsVariable showMapAttribution_ {"show_map_attribution"}; - SettingsVariable showMapCenter_ {"show_map_center"}; - SettingsVariable showMapLogo_ {"show_map_logo"}; - SettingsVariable theme_ {"theme"}; - SettingsVariable themeFile_ {"theme_file"}; - SettingsVariable trackLocation_ {"track_location"}; + SettingsVariable processModuleWarningsEnabled_ { + "process_module_warnings_enabled"}; + SettingsVariable showMapAttribution_ {"show_map_attribution"}; + SettingsVariable showMapCenter_ {"show_map_center"}; + SettingsVariable showMapLogo_ {"show_map_logo"}; + SettingsVariable theme_ {"theme"}; + SettingsVariable themeFile_ {"theme_file"}; + SettingsVariable trackLocation_ {"track_location"}; SettingsVariable updateNotificationsEnabled_ {"update_notifications"}; SettingsVariable warningsProvider_ {"warnings_provider"}; SettingsVariable cursorIconAlwaysOn_ {"cursor_icon_always_on"}; @@ -197,6 +200,7 @@ GeneralSettings::GeneralSettings() : &p->nmeaBaudRate_, &p->nmeaSource_, &p->positioningPlugin_, + &p->processModuleWarningsEnabled_, &p->showMapAttribution_, &p->showMapCenter_, &p->showMapLogo_, @@ -316,6 +320,11 @@ SettingsVariable& GeneralSettings::positioning_plugin() const return p->positioningPlugin_; } +SettingsVariable& GeneralSettings::process_module_warnings_enabled() const +{ + return p->processModuleWarningsEnabled_; +} + SettingsVariable& GeneralSettings::show_map_attribution() const { return p->showMapAttribution_; @@ -374,6 +383,7 @@ bool GeneralSettings::Shutdown() dataChanged |= p->loopDelay_.Commit(); dataChanged |= p->loopSpeed_.Commit(); dataChanged |= p->loopTime_.Commit(); + dataChanged |= p->processModuleWarningsEnabled_.Commit(); dataChanged |= p->trackLocation_.Commit(); return dataChanged; @@ -407,6 +417,8 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->nmeaBaudRate_ == rhs.p->nmeaBaudRate_ && lhs.p->nmeaSource_ == rhs.p->nmeaSource_ && lhs.p->positioningPlugin_ == rhs.p->positioningPlugin_ && + lhs.p->processModuleWarningsEnabled_ == + rhs.p->processModuleWarningsEnabled_ && lhs.p->showMapAttribution_ == rhs.p->showMapAttribution_ && lhs.p->showMapCenter_ == rhs.p->showMapCenter_ && lhs.p->showMapLogo_ == rhs.p->showMapLogo_ && diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 46b342d7..6a49566b 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -45,12 +45,13 @@ public: SettingsVariable& nmea_baud_rate() const; SettingsVariable& nmea_source() const; SettingsVariable& positioning_plugin() const; - SettingsVariable& show_map_attribution() const; - SettingsVariable& show_map_center() const; - SettingsVariable& show_map_logo() const; - SettingsVariable& theme() const; - SettingsVariable& theme_file() const; - SettingsVariable& track_location() const; + SettingsVariable& process_module_warnings_enabled() const; + SettingsVariable& show_map_attribution() const; + SettingsVariable& show_map_center() const; + SettingsVariable& show_map_logo() const; + SettingsVariable& theme() const; + SettingsVariable& theme_file() const; + SettingsVariable& track_location() const; SettingsVariable& update_notifications_enabled() const; SettingsVariable& warnings_provider() const; SettingsVariable& cursor_icon_always_on() const; diff --git a/test/data b/test/data index 0d085b1d..24ececcd 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 0d085b1df59045e14ca996982b4907b1a0da4fdb +Subproject commit 24ececcd183d3b8961e5638da89f0eb36309cd6b From 71f967d53657fdc2b87f582abfc092b475870db5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Dec 2024 14:31:12 -0500 Subject: [PATCH 334/762] Added basic high privilege checks and message box --- scwx-qt/scwx-qt.cmake | 6 ++- scwx-qt/source/scwx/qt/main/main.cpp | 17 ++++++++ .../source/scwx/qt/util/check_privilege.cpp | 43 +++++++++++++++++++ .../source/scwx/qt/util/check_privilege.hpp | 11 +++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/util/check_privilege.cpp create mode 100644 scwx-qt/source/scwx/qt/util/check_privilege.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 92ca70a4..087c61e5 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -353,7 +353,8 @@ set(SRC_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.cpp source/scwx/qt/ui/setup/map_provider_page.cpp source/scwx/qt/ui/setup/setup_wizard.cpp source/scwx/qt/ui/setup/welcome_page.cpp) -set(HDR_UTIL source/scwx/qt/util/color.hpp +set(HDR_UTIL source/scwx/qt/util/check_privilege.hpp + source/scwx/qt/util/color.hpp source/scwx/qt/util/file.hpp source/scwx/qt/util/geographic_lib.hpp source/scwx/qt/util/imgui.hpp @@ -367,7 +368,8 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/q_file_input_stream.hpp source/scwx/qt/util/time.hpp source/scwx/qt/util/tooltip.hpp) -set(SRC_UTIL source/scwx/qt/util/color.cpp +set(SRC_UTIL source/scwx/qt/util/check_privilege.cpp + source/scwx/qt/util/color.cpp source/scwx/qt/util/file.cpp source/scwx/qt/util/geographic_lib.cpp source/scwx/qt/util/imgui.cpp diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 7d66c80d..e8a833b4 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,8 @@ #include #include +#include + #define QT6CT_LIBRARY #include #undef QT6CT_LIBRARY @@ -73,6 +76,20 @@ int main(int argc, char* argv[]) QCoreApplication::installTranslator(&translator); } + // Test to see if scwx was run with high privilege + if (scwx::qt::util::is_high_privilege()) + { + QMessageBox::StandardButton pressed = QMessageBox::warning( + nullptr, + "Warning: Running with High Privileges", + "Although Supercell-Wx can be run with high privileges, " + "it is not recommended", + QMessageBox::Ok | QMessageBox::Close); + if (pressed & QMessageBox::Ok) { + return 0; + } + } + if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) { QStandardPaths::setTestModeEnabled(true); diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.cpp b/scwx-qt/source/scwx/qt/util/check_privilege.cpp new file mode 100644 index 00000000..c33f87b4 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/check_privilege.cpp @@ -0,0 +1,43 @@ +#include "scwx/qt/util/check_privilege.hpp" +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace scwx { +namespace qt { +namespace util { + +bool is_high_privilege() +{ +#if defined(_WIN32) + bool isAdmin = false; + HANDLE token = NULL; + TOKEN_ELEVATION elevation; + DWORD elevationSize = sizeof(TOKEN_ELEVATION); + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) + { + return false; + } + if (!GetTokenInformation( + token, TokenElevation, &elevation, elevationSize, &elevationSize)) + { + CloseHandle(token); + return false; + } + isAdmin = elevation.TokenIsElevated; + CloseHandle(token); + return isAdmin; +#elif defined(Q_OS_UNIX) + // On UNIX root is always uid 0. On Linux this is enforced by the kernal. + return geteuid() == 0; +#endif +} + +} // namespace util +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.hpp b/scwx-qt/source/scwx/qt/util/check_privilege.hpp new file mode 100644 index 00000000..1234f0ef --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/check_privilege.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace scwx { +namespace qt { +namespace util { + +bool is_high_privilege(); + +} // namespace util +} // namespace qt +} // namespace scwx From 923dad4e2ec93750697586171607e8f4ee7148de Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Dec 2024 17:08:46 -0500 Subject: [PATCH 335/762] Add specific dialog and setting for high privilege warning --- scwx-qt/scwx-qt.cmake | 3 + scwx-qt/source/scwx/qt/main/main.cpp | 85 ++++++++++++--- .../scwx/qt/settings/general_settings.cpp | 15 ++- .../scwx/qt/settings/general_settings.hpp | 1 + .../scwx/qt/ui/high_privilege_dialog.cpp | 47 ++++++++ .../scwx/qt/ui/high_privilege_dialog.hpp | 40 +++++++ .../scwx/qt/ui/high_privilege_dialog.ui | 101 ++++++++++++++++++ 7 files changed, 274 insertions(+), 18 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 087c61e5..a386d8b9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -260,6 +260,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/edit_marker_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/gps_info_dialog.hpp + source/scwx/qt/ui/high_privilege_dialog.hpp source/scwx/qt/ui/hotkey_edit.hpp source/scwx/qt/ui/imgui_debug_dialog.hpp source/scwx/qt/ui/imgui_debug_widget.hpp @@ -291,6 +292,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/edit_marker_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/gps_info_dialog.cpp + source/scwx/qt/ui/high_privilege_dialog.cpp source/scwx/qt/ui/hotkey_edit.cpp source/scwx/qt/ui/imgui_debug_dialog.cpp source/scwx/qt/ui/imgui_debug_widget.cpp @@ -320,6 +322,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui + source/scwx/qt/ui/high_privilege_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui source/scwx/qt/ui/open_url_dialog.ui diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index e8a833b4..4a6445d0 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +#include #include #include @@ -32,8 +34,6 @@ #include #include -#include - #define QT6CT_LIBRARY #include #undef QT6CT_LIBRARY @@ -46,6 +46,9 @@ static void OverrideDefaultStyle(const std::vector& args); int main(int argc, char* argv[]) { + bool disableHighPrivilegeWarning = false; + bool highPrivilegeChecked = false; + // Store arguments std::vector args {}; for (int i = 0; i < argc; ++i) @@ -76,25 +79,35 @@ int main(int argc, char* argv[]) QCoreApplication::installTranslator(&translator); } - // Test to see if scwx was run with high privilege - if (scwx::qt::util::is_high_privilege()) - { - QMessageBox::StandardButton pressed = QMessageBox::warning( - nullptr, - "Warning: Running with High Privileges", - "Although Supercell-Wx can be run with high privileges, " - "it is not recommended", - QMessageBox::Ok | QMessageBox::Close); - if (pressed & QMessageBox::Ok) { - return 0; - } - } - if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) { QStandardPaths::setTestModeEnabled(true); } + // Test to see if scwx was run with high privilege + std::string appDataPath { + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + .toStdString()}; + + // Check if high privilege before writing settings, assuming no settings + // have been written + if (!std::filesystem::exists(appDataPath) && + scwx::qt::util::is_high_privilege()) + { + auto dialog = + scwx::qt::ui::HighPrivilegeDialog(); // TODO does this need cleaned up? + const int result = dialog.exec(); + + disableHighPrivilegeWarning = dialog.disable_high_privilege_message(); + highPrivilegeChecked = true; + + if (result == QDialog::Rejected) + { + // TODO any other cleanup needed here? + return 0; + } + } + // Start the io_context main loop boost::asio::io_context& ioContext = scwx::util::io_context(); auto work = boost::asio::make_work_guard(ioContext); @@ -133,6 +146,46 @@ int main(int argc, char* argv[]) // Check process modules for compatibility scwx::qt::main::CheckProcessModules(); + auto& generalSettings = scwx::qt::settings::GeneralSettings::Instance(); + + if (!highPrivilegeChecked && + generalSettings.high_privilege_warning_enabled().GetValue() && + scwx::qt::util::is_high_privilege()) + { + auto dialog = + scwx::qt::ui::HighPrivilegeDialog(); // TODO does this need cleaned up? + const int result = dialog.exec(); + + disableHighPrivilegeWarning = dialog.disable_high_privilege_message(); + + if (result == QDialog::Rejected) + { + // Deinitialize application + scwx::qt::manager::RadarProductManager::Cleanup(); + + // Stop Qt Threads + scwx::qt::manager::ThreadManager::Instance().StopThreads(); + + // Gracefully stop the io_context main loop + work.reset(); + threadPool.join(); + + // Shutdown application + scwx::qt::manager::ResourceManager::Shutdown(); + scwx::qt::manager::SettingsManager::Instance().Shutdown(); + + // Shutdown AWS SDK + Aws::ShutdownAPI(awsSdkOptions); + return 0; + } + } + + // Save high privilege settings + if (disableHighPrivilegeWarning) + { + generalSettings.high_privilege_warning_enabled().SetValue(false); + scwx::qt::manager::SettingsManager::Instance().SaveSettings(); + } // Run initial setup if required if (scwx::qt::ui::setup::SetupWizard::IsSetupRequired()) diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 77ca764e..0abb45e9 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -80,6 +80,7 @@ public: warningsProvider_.SetDefault(defaultWarningsProviderValue); cursorIconAlwaysOn_.SetDefault(false); radarSiteThreshold_.SetDefault(0.0); + highPrivilegeWarningEnabled_.SetDefault(true); fontSizes_.SetElementMinimum(1); fontSizes_.SetElementMaximum(72); @@ -175,6 +176,8 @@ public: SettingsVariable warningsProvider_ {"warnings_provider"}; SettingsVariable cursorIconAlwaysOn_ {"cursor_icon_always_on"}; SettingsVariable radarSiteThreshold_ {"radar_site_threshold"}; + SettingsVariable highPrivilegeWarningEnabled_ { + "high_privilege_warning_enabled"}; }; GeneralSettings::GeneralSettings() : @@ -210,7 +213,8 @@ GeneralSettings::GeneralSettings() : &p->updateNotificationsEnabled_, &p->warningsProvider_, &p->cursorIconAlwaysOn_, - &p->radarSiteThreshold_}); + &p->radarSiteThreshold_, + &p->highPrivilegeWarningEnabled_}); SetDefaults(); } GeneralSettings::~GeneralSettings() = default; @@ -375,6 +379,11 @@ SettingsVariable& GeneralSettings::radar_site_threshold() const return p->radarSiteThreshold_; } +SettingsVariable& GeneralSettings::high_privilege_warning_enabled() const +{ + return p->highPrivilegeWarningEnabled_; +} + bool GeneralSettings::Shutdown() { bool dataChanged = false; @@ -429,7 +438,9 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) rhs.p->updateNotificationsEnabled_ && lhs.p->warningsProvider_ == rhs.p->warningsProvider_ && lhs.p->cursorIconAlwaysOn_ == rhs.p->cursorIconAlwaysOn_ && - lhs.p->radarSiteThreshold_ == rhs.p->radarSiteThreshold_); + lhs.p->radarSiteThreshold_ == rhs.p->radarSiteThreshold_ && + lhs.p->highPrivilegeWarningEnabled_ == + rhs.p->highPrivilegeWarningEnabled_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 6a49566b..074b956e 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -56,6 +56,7 @@ public: SettingsVariable& warnings_provider() const; SettingsVariable& cursor_icon_always_on() const; SettingsVariable& radar_site_threshold() const; + SettingsVariable& high_privilege_warning_enabled() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp new file mode 100644 index 00000000..0c60052a --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp @@ -0,0 +1,47 @@ +#include "high_privilege_dialog.hpp" +#include "ui_high_privilege_dialog.h" + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::high_privilege_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class HighPrivilegeDialogImpl +{ +public: + explicit HighPrivilegeDialogImpl(HighPrivilegeDialog* self) : + self_ {self} {}; + ~HighPrivilegeDialogImpl() = default; + + HighPrivilegeDialog* self_; +}; + +HighPrivilegeDialog::HighPrivilegeDialog(QWidget* parent) : + QDialog(parent), + p {std::make_unique(this)}, + ui(new Ui::HighPrivilegeDialog) +{ + ui->setupUi(this); +} + +bool HighPrivilegeDialog::disable_high_privilege_message() { + return ui->highPrivilegeCheckBox->isChecked(); +} + +HighPrivilegeDialog::~HighPrivilegeDialog() +{ + delete ui; +} + +} // namespace ui +} // namespace qt +} // namespace scwx + diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp new file mode 100644 index 00000000..5271d245 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +namespace Ui +{ +class HighPrivilegeDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +class HighPrivilegeDialogImpl; + +class HighPrivilegeDialog : public QDialog +{ + Q_OBJECT + +private: + Q_DISABLE_COPY(HighPrivilegeDialog) + +public: + explicit HighPrivilegeDialog(QWidget* parent = nullptr); + ~HighPrivilegeDialog(); + + bool disable_high_privilege_message(); + +private: + friend HighPrivilegeDialogImpl; + std::unique_ptr p; + Ui::HighPrivilegeDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui new file mode 100644 index 00000000..4d6f5075 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui @@ -0,0 +1,101 @@ + + + HighPrivilegeDialog + + + + 0 + 0 + 301 + 269 + + + + + 0 + 0 + + + + Warning: High Privilege + + + + + + + 0 + 0 + + + + <html><head/><body><h1 align="center" style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:700; color:#ff0000;">Warning: Run Supercell Wx with Low Privileges.</span></h1><p align="center">Please run Supercell Wx without admin or root permissions. Supercell Wx should not need such permissions to run. If you do not want to run Supercell Wx with high privilege, click &quot;Close&quot;, and relaunch with lower permissions. Otherwise, click &quot;Ignore&quot;. You may disable this warning with the checkbox below.</p></body></html> + + + Qt::TextFormat::RichText + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop + + + true + + + + + + + Disable High Privilege Warning + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Ignore + + + + + + + + + + + buttonBox + accepted() + HighPrivilegeDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + HighPrivilegeDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From 6408e1b876a1c162f428621a9dc6941897e97203 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 19 Dec 2024 17:24:36 -0500 Subject: [PATCH 336/762] Fix formatting for high_privilege_warning --- scwx-qt/source/scwx/qt/main/main.cpp | 4 ++-- .../scwx/qt/ui/high_privilege_dialog.cpp | 6 +++--- .../scwx/qt/ui/high_privilege_dialog.hpp | 2 +- .../source/scwx/qt/util/check_privilege.cpp | 19 +++++++++++-------- .../source/scwx/qt/util/check_privilege.hpp | 9 ++++++--- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 4a6445d0..468aa1e6 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -85,7 +85,7 @@ int main(int argc, char* argv[]) } // Test to see if scwx was run with high privilege - std::string appDataPath { + const std::string appDataPath { QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) .toStdString()}; @@ -99,7 +99,7 @@ int main(int argc, char* argv[]) const int result = dialog.exec(); disableHighPrivilegeWarning = dialog.disable_high_privilege_message(); - highPrivilegeChecked = true; + highPrivilegeChecked = true; if (result == QDialog::Rejected) { diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp index 0c60052a..c057e6a4 100644 --- a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp @@ -19,7 +19,7 @@ class HighPrivilegeDialogImpl public: explicit HighPrivilegeDialogImpl(HighPrivilegeDialog* self) : self_ {self} {}; - ~HighPrivilegeDialogImpl() = default; + ~HighPrivilegeDialogImpl() = default; HighPrivilegeDialog* self_; }; @@ -32,7 +32,8 @@ HighPrivilegeDialog::HighPrivilegeDialog(QWidget* parent) : ui->setupUi(this); } -bool HighPrivilegeDialog::disable_high_privilege_message() { +bool HighPrivilegeDialog::disable_high_privilege_message() +{ return ui->highPrivilegeCheckBox->isChecked(); } @@ -44,4 +45,3 @@ HighPrivilegeDialog::~HighPrivilegeDialog() } // namespace ui } // namespace qt } // namespace scwx - diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp index 5271d245..b5d79ada 100644 --- a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp @@ -25,7 +25,7 @@ private: public: explicit HighPrivilegeDialog(QWidget* parent = nullptr); - ~HighPrivilegeDialog(); + ~HighPrivilegeDialog() override; bool disable_high_privilege_message(); diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.cpp b/scwx-qt/source/scwx/qt/util/check_privilege.cpp index c33f87b4..2d7d1227 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/util/check_privilege.cpp @@ -2,20 +2,23 @@ #include #ifdef _WIN32 -#include +# include #else -#include +# include #endif -namespace scwx { -namespace qt { -namespace util { +namespace scwx +{ +namespace qt +{ +namespace util +{ bool is_high_privilege() { #if defined(_WIN32) - bool isAdmin = false; - HANDLE token = NULL; + bool isAdmin = false; + HANDLE token = NULL; TOKEN_ELEVATION elevation; DWORD elevationSize = sizeof(TOKEN_ELEVATION); @@ -33,7 +36,7 @@ bool is_high_privilege() CloseHandle(token); return isAdmin; #elif defined(Q_OS_UNIX) - // On UNIX root is always uid 0. On Linux this is enforced by the kernal. + // On UNIX root is always uid 0. On Linux this is enforced by the kernel. return geteuid() == 0; #endif } diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.hpp b/scwx-qt/source/scwx/qt/util/check_privilege.hpp index 1234f0ef..7a44a070 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/util/check_privilege.hpp @@ -1,8 +1,11 @@ #pragma once -namespace scwx { -namespace qt { -namespace util { +namespace scwx +{ +namespace qt +{ +namespace util +{ bool is_high_privilege(); From 4a4075b50fea82a6cb73aa75a109727493eb7c88 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 13:36:32 -0500 Subject: [PATCH 337/762] Move dialog logic into check_privilege --- scwx-qt/source/scwx/qt/main/main.cpp | 93 ++++------------ .../scwx/qt/settings/general_settings.cpp | 1 + .../source/scwx/qt/util/check_privilege.cpp | 104 ++++++++++++++++-- .../source/scwx/qt/util/check_privilege.hpp | 27 +++-- 4 files changed, 136 insertions(+), 89 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 468aa1e6..4212050e 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -46,9 +46,6 @@ static void OverrideDefaultStyle(const std::vector& args); int main(int argc, char* argv[]) { - bool disableHighPrivilegeWarning = false; - bool highPrivilegeChecked = false; - // Store arguments std::vector args {}; for (int i = 0; i < argc; ++i) @@ -85,27 +82,10 @@ int main(int argc, char* argv[]) } // Test to see if scwx was run with high privilege - const std::string appDataPath { - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) - .toStdString()}; - - // Check if high privilege before writing settings, assuming no settings - // have been written - if (!std::filesystem::exists(appDataPath) && - scwx::qt::util::is_high_privilege()) + scwx::qt::util::PrivilegeChecker privilegeChecker; + if (privilegeChecker.first_check()) { - auto dialog = - scwx::qt::ui::HighPrivilegeDialog(); // TODO does this need cleaned up? - const int result = dialog.exec(); - - disableHighPrivilegeWarning = dialog.disable_high_privilege_message(); - highPrivilegeChecked = true; - - if (result == QDialog::Rejected) - { - // TODO any other cleanup needed here? - return 0; - } + return 0; } // Start the io_context main loop @@ -146,61 +126,28 @@ int main(int argc, char* argv[]) // Check process modules for compatibility scwx::qt::main::CheckProcessModules(); - auto& generalSettings = scwx::qt::settings::GeneralSettings::Instance(); - if (!highPrivilegeChecked && - generalSettings.high_privilege_warning_enabled().GetValue() && - scwx::qt::util::is_high_privilege()) + int result = 0; + if (privilegeChecker.second_check()) { - auto dialog = - scwx::qt::ui::HighPrivilegeDialog(); // TODO does this need cleaned up? - const int result = dialog.exec(); - - disableHighPrivilegeWarning = dialog.disable_high_privilege_message(); - - if (result == QDialog::Rejected) + result = 1; + } + else + { + // Run initial setup if required + if (scwx::qt::ui::setup::SetupWizard::IsSetupRequired()) { - // Deinitialize application - scwx::qt::manager::RadarProductManager::Cleanup(); - - // Stop Qt Threads - scwx::qt::manager::ThreadManager::Instance().StopThreads(); - - // Gracefully stop the io_context main loop - work.reset(); - threadPool.join(); - - // Shutdown application - scwx::qt::manager::ResourceManager::Shutdown(); - scwx::qt::manager::SettingsManager::Instance().Shutdown(); - - // Shutdown AWS SDK - Aws::ShutdownAPI(awsSdkOptions); - return 0; + scwx::qt::ui::setup::SetupWizard w; + w.show(); + a.exec(); } - } - // Save high privilege settings - if (disableHighPrivilegeWarning) - { - generalSettings.high_privilege_warning_enabled().SetValue(false); - scwx::qt::manager::SettingsManager::Instance().SaveSettings(); - } - - // Run initial setup if required - if (scwx::qt::ui::setup::SetupWizard::IsSetupRequired()) - { - scwx::qt::ui::setup::SetupWizard w; - w.show(); - a.exec(); - } - - // Run Qt main loop - int result; - { - scwx::qt::main::MainWindow w; - w.show(); - result = a.exec(); + // Run Qt main loop + { + scwx::qt::main::MainWindow w; + w.show(); + result = a.exec(); + } } // Deinitialize application diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 0abb45e9..6f254c6b 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -394,6 +394,7 @@ bool GeneralSettings::Shutdown() dataChanged |= p->loopTime_.Commit(); dataChanged |= p->processModuleWarningsEnabled_.Commit(); dataChanged |= p->trackLocation_.Commit(); + dataChanged |= p->highPrivilegeWarningEnabled_.Commit(); return dataChanged; } diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.cpp b/scwx-qt/source/scwx/qt/util/check_privilege.cpp index 2d7d1227..62a4c4d0 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/util/check_privilege.cpp @@ -1,5 +1,11 @@ +#include "scwx/qt/settings/general_settings.hpp" #include "scwx/qt/util/check_privilege.hpp" #include +#include +#include +#include +#include +#include #ifdef _WIN32 # include @@ -7,11 +13,7 @@ # include #endif -namespace scwx -{ -namespace qt -{ -namespace util +namespace scwx::qt::util { bool is_high_privilege() @@ -38,9 +40,95 @@ bool is_high_privilege() #elif defined(Q_OS_UNIX) // On UNIX root is always uid 0. On Linux this is enforced by the kernel. return geteuid() == 0; +#else + return false; #endif } -} // namespace util -} // namespace qt -} // namespace scwx +#if defined(_WIN32) +static const QString message = QObject::tr( + "Supercell Wx has been run with administrator permissions. It is " + "recommended to run it without administrator permissions Do you wish to " + "continue?"); +#elif defined(Q_OS_UNIX) +static const QString message = QObject::tr( + "Supercell Wx has been run as root. It is recommended to run it as a normal " + "user. Do you wish to continue?"); +#else +static const QString message = QObject::tr(""); +#endif + +static const QString title = QObject::tr("Supercell Wx"); +static const QString checkBoxText = + QObject::tr("Do not show this warning again."); + +class PrivilegeChecker::Impl +{ +public: + explicit Impl() : + highPrivilege_ {is_high_privilege()}, + dialog_ {QMessageBox::Icon::Warning, title, message}, + checkBox_(new QCheckBox(checkBoxText, &dialog_)) + { + const std::string appDataPath { + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + .toStdString()}; + hasAppData_ = std::filesystem::exists(appDataPath); + + dialog_.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + dialog_.setCheckBox(checkBox_); + }; + + bool hasAppData_; + bool firstCheckCheckBoxState_ {true}; + bool highPrivilege_; + + QMessageBox dialog_; + QCheckBox* checkBox_; +}; + +PrivilegeChecker::PrivilegeChecker() : p(std::make_unique()) +{ +} + +PrivilegeChecker::~PrivilegeChecker() = default; + +bool PrivilegeChecker::first_check() +{ + if (p->hasAppData_ || !p->highPrivilege_) + { + return false; + } + + int result = p->dialog_.exec(); + p->firstCheckCheckBoxState_ = p->checkBox_->isChecked(); + + return result != QMessageBox::Yes; +} + +bool PrivilegeChecker::second_check() +{ + auto& highPrivilegeWarningEnabled = + settings::GeneralSettings::Instance().high_privilege_warning_enabled(); + if (!highPrivilegeWarningEnabled.GetValue() || !p->highPrivilege_) + { + return false; + } + else if (!p->hasAppData_) + { + highPrivilegeWarningEnabled.StageValue(!p->firstCheckCheckBoxState_); + return false; + } + + switch (p->dialog_.exec()) + { + case QMessageBox::Yes: + highPrivilegeWarningEnabled.StageValue(!p->checkBox_->isChecked()); + return false; + case QMessageBox::No: + default: + return true; + } +} + +} // namespace scwx::qt::util diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.hpp b/scwx-qt/source/scwx/qt/util/check_privilege.hpp index 7a44a070..7935e54a 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/util/check_privilege.hpp @@ -1,14 +1,25 @@ #pragma once -namespace scwx -{ -namespace qt -{ -namespace util +#include + +namespace scwx::qt::util { bool is_high_privilege(); -} // namespace util -} // namespace qt -} // namespace scwx +class PrivilegeChecker +{ +public: + explicit PrivilegeChecker(); + ~PrivilegeChecker(); + + // returning true means check failed. + bool first_check(); + bool second_check(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::qt::util From 2f9908b54e3f10926707962f2bbfd10122e4aa15 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 13:46:32 -0500 Subject: [PATCH 338/762] move check_privilege.* to main --- scwx-qt/scwx-qt.cmake | 8 ++++---- scwx-qt/source/scwx/qt/{util => main}/check_privilege.cpp | 7 ++++--- scwx-qt/source/scwx/qt/{util => main}/check_privilege.hpp | 4 ++-- scwx-qt/source/scwx/qt/main/main.cpp | 5 ++--- 4 files changed, 12 insertions(+), 12 deletions(-) rename scwx-qt/source/scwx/qt/{util => main}/check_privilege.cpp (95%) rename scwx-qt/source/scwx/qt/{util => main}/check_privilege.hpp (84%) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index a386d8b9..dece221d 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -51,9 +51,11 @@ find_package(Qt${QT_VERSION_MAJOR} set(SRC_EXE_MAIN source/scwx/qt/main/main.cpp) set(HDR_MAIN source/scwx/qt/main/application.hpp + source/scwx/qt/main/check_privilege.hpp source/scwx/qt/main/main_window.hpp source/scwx/qt/main/process_validation.hpp) set(SRC_MAIN source/scwx/qt/main/application.cpp + source/scwx/qt/main/check_privilege.cpp source/scwx/qt/main/main_window.cpp source/scwx/qt/main/process_validation.cpp) set(UI_MAIN source/scwx/qt/main/main_window.ui) @@ -356,8 +358,7 @@ set(SRC_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.cpp source/scwx/qt/ui/setup/map_provider_page.cpp source/scwx/qt/ui/setup/setup_wizard.cpp source/scwx/qt/ui/setup/welcome_page.cpp) -set(HDR_UTIL source/scwx/qt/util/check_privilege.hpp - source/scwx/qt/util/color.hpp +set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/file.hpp source/scwx/qt/util/geographic_lib.hpp source/scwx/qt/util/imgui.hpp @@ -371,8 +372,7 @@ set(HDR_UTIL source/scwx/qt/util/check_privilege.hpp source/scwx/qt/util/q_file_input_stream.hpp source/scwx/qt/util/time.hpp source/scwx/qt/util/tooltip.hpp) -set(SRC_UTIL source/scwx/qt/util/check_privilege.cpp - source/scwx/qt/util/color.cpp +set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/file.cpp source/scwx/qt/util/geographic_lib.cpp source/scwx/qt/util/imgui.cpp diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.cpp b/scwx-qt/source/scwx/qt/main/check_privilege.cpp similarity index 95% rename from scwx-qt/source/scwx/qt/util/check_privilege.cpp rename to scwx-qt/source/scwx/qt/main/check_privilege.cpp index 62a4c4d0..25f29f22 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.cpp @@ -1,5 +1,5 @@ #include "scwx/qt/settings/general_settings.hpp" -#include "scwx/qt/util/check_privilege.hpp" +#include "scwx/qt/main/check_privilege.hpp" #include #include #include @@ -13,7 +13,7 @@ # include #endif -namespace scwx::qt::util +namespace scwx::qt::main { bool is_high_privilege() @@ -76,6 +76,7 @@ public: hasAppData_ = std::filesystem::exists(appDataPath); dialog_.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + dialog_.setDefaultButton(QMessageBox::No); dialog_.setCheckBox(checkBox_); }; @@ -131,4 +132,4 @@ bool PrivilegeChecker::second_check() } } -} // namespace scwx::qt::util +} // namespace scwx::qt::main diff --git a/scwx-qt/source/scwx/qt/util/check_privilege.hpp b/scwx-qt/source/scwx/qt/main/check_privilege.hpp similarity index 84% rename from scwx-qt/source/scwx/qt/util/check_privilege.hpp rename to scwx-qt/source/scwx/qt/main/check_privilege.hpp index 7935e54a..34da5bfc 100644 --- a/scwx-qt/source/scwx/qt/util/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.hpp @@ -2,7 +2,7 @@ #include -namespace scwx::qt::util +namespace scwx::qt::main { bool is_high_privilege(); @@ -22,4 +22,4 @@ private: std::unique_ptr p; }; -} // namespace scwx::qt::util +} // namespace scwx::qt::main diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 4212050e..dde35cbb 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -22,7 +22,6 @@ #include #include -#include #include #include @@ -82,7 +81,7 @@ int main(int argc, char* argv[]) } // Test to see if scwx was run with high privilege - scwx::qt::util::PrivilegeChecker privilegeChecker; + scwx::qt::main::PrivilegeChecker privilegeChecker; if (privilegeChecker.first_check()) { return 0; From 87e43a528e71556f128b8977a6dfa0255f5a725a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 13:50:49 -0500 Subject: [PATCH 339/762] use for descriptive names for when checks are made --- scwx-qt/source/scwx/qt/main/check_privilege.cpp | 4 ++-- scwx-qt/source/scwx/qt/main/check_privilege.hpp | 4 ++-- scwx-qt/source/scwx/qt/main/main.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.cpp b/scwx-qt/source/scwx/qt/main/check_privilege.cpp index 25f29f22..0d1d8cba 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.cpp @@ -94,7 +94,7 @@ PrivilegeChecker::PrivilegeChecker() : p(std::make_uniquehasAppData_ || !p->highPrivilege_) { @@ -107,7 +107,7 @@ bool PrivilegeChecker::first_check() return result != QMessageBox::Yes; } -bool PrivilegeChecker::second_check() +bool PrivilegeChecker::post_settings_check() { auto& highPrivilegeWarningEnabled = settings::GeneralSettings::Instance().high_privilege_warning_enabled(); diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.hpp b/scwx-qt/source/scwx/qt/main/check_privilege.hpp index 34da5bfc..f9fcadd9 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.hpp @@ -14,8 +14,8 @@ public: ~PrivilegeChecker(); // returning true means check failed. - bool first_check(); - bool second_check(); + bool pre_settings_check(); + bool post_settings_check(); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index dde35cbb..c1a7c891 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -82,7 +82,7 @@ int main(int argc, char* argv[]) // Test to see if scwx was run with high privilege scwx::qt::main::PrivilegeChecker privilegeChecker; - if (privilegeChecker.first_check()) + if (privilegeChecker.pre_settings_check()) { return 0; } @@ -127,7 +127,7 @@ int main(int argc, char* argv[]) scwx::qt::main::CheckProcessModules(); int result = 0; - if (privilegeChecker.second_check()) + if (privilegeChecker.post_settings_check()) { result = 1; } From eb3b76dcf4fb02d107f9cbe7951244edd7139bf5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 14:22:19 -0500 Subject: [PATCH 340/762] get rid of old high_privilege_dialog build --- scwx-qt/scwx-qt.cmake | 3 - .../source/scwx/qt/main/check_privilege.cpp | 7 +- scwx-qt/source/scwx/qt/main/main.cpp | 1 - .../scwx/qt/ui/high_privilege_dialog.cpp | 47 -------- .../scwx/qt/ui/high_privilege_dialog.hpp | 40 ------- .../scwx/qt/ui/high_privilege_dialog.ui | 101 ------------------ 6 files changed, 4 insertions(+), 195 deletions(-) delete mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp delete mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp delete mode 100644 scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index dece221d..09ea6fe3 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -262,7 +262,6 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/edit_marker_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/gps_info_dialog.hpp - source/scwx/qt/ui/high_privilege_dialog.hpp source/scwx/qt/ui/hotkey_edit.hpp source/scwx/qt/ui/imgui_debug_dialog.hpp source/scwx/qt/ui/imgui_debug_widget.hpp @@ -294,7 +293,6 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/edit_marker_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/gps_info_dialog.cpp - source/scwx/qt/ui/high_privilege_dialog.cpp source/scwx/qt/ui/hotkey_edit.cpp source/scwx/qt/ui/imgui_debug_dialog.cpp source/scwx/qt/ui/imgui_debug_widget.cpp @@ -324,7 +322,6 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui - source/scwx/qt/ui/high_privilege_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui source/scwx/qt/ui/open_url_dialog.ui diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.cpp b/scwx-qt/source/scwx/qt/main/check_privilege.cpp index 0d1d8cba..965df9f8 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.cpp @@ -58,7 +58,7 @@ static const QString message = QObject::tr( static const QString message = QObject::tr(""); #endif -static const QString title = QObject::tr("Supercell Wx"); +static const QString title = QObject::tr("Supercell Wx"); static const QString checkBoxText = QObject::tr("Do not show this warning again."); @@ -88,7 +88,8 @@ public: QCheckBox* checkBox_; }; -PrivilegeChecker::PrivilegeChecker() : p(std::make_unique()) +PrivilegeChecker::PrivilegeChecker() : + p(std::make_unique()) { } @@ -101,7 +102,7 @@ bool PrivilegeChecker::pre_settings_check() return false; } - int result = p->dialog_.exec(); + int result = p->dialog_.exec(); p->firstCheckCheckBoxState_ = p->checkBox_->isChecked(); return result != QMessageBox::Yes; diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index c1a7c891..65d7e998 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp deleted file mode 100644 index c057e6a4..00000000 --- a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "high_privilege_dialog.hpp" -#include "ui_high_privilege_dialog.h" - -#include -#include - -namespace scwx -{ -namespace qt -{ -namespace ui -{ - -static const std::string logPrefix_ = "scwx::qt::ui::high_privilege_dialog"; -static const auto logger_ = scwx::util::Logger::Create(logPrefix_); - -class HighPrivilegeDialogImpl -{ -public: - explicit HighPrivilegeDialogImpl(HighPrivilegeDialog* self) : - self_ {self} {}; - ~HighPrivilegeDialogImpl() = default; - - HighPrivilegeDialog* self_; -}; - -HighPrivilegeDialog::HighPrivilegeDialog(QWidget* parent) : - QDialog(parent), - p {std::make_unique(this)}, - ui(new Ui::HighPrivilegeDialog) -{ - ui->setupUi(this); -} - -bool HighPrivilegeDialog::disable_high_privilege_message() -{ - return ui->highPrivilegeCheckBox->isChecked(); -} - -HighPrivilegeDialog::~HighPrivilegeDialog() -{ - delete ui; -} - -} // namespace ui -} // namespace qt -} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp deleted file mode 100644 index b5d79ada..00000000 --- a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include - -namespace Ui -{ -class HighPrivilegeDialog; -} - -namespace scwx -{ -namespace qt -{ -namespace ui -{ - -class HighPrivilegeDialogImpl; - -class HighPrivilegeDialog : public QDialog -{ - Q_OBJECT - -private: - Q_DISABLE_COPY(HighPrivilegeDialog) - -public: - explicit HighPrivilegeDialog(QWidget* parent = nullptr); - ~HighPrivilegeDialog() override; - - bool disable_high_privilege_message(); - -private: - friend HighPrivilegeDialogImpl; - std::unique_ptr p; - Ui::HighPrivilegeDialog* ui; -}; - -} // namespace ui -} // namespace qt -} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui b/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui deleted file mode 100644 index 4d6f5075..00000000 --- a/scwx-qt/source/scwx/qt/ui/high_privilege_dialog.ui +++ /dev/null @@ -1,101 +0,0 @@ - - - HighPrivilegeDialog - - - - 0 - 0 - 301 - 269 - - - - - 0 - 0 - - - - Warning: High Privilege - - - - - - - 0 - 0 - - - - <html><head/><body><h1 align="center" style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:700; color:#ff0000;">Warning: Run Supercell Wx with Low Privileges.</span></h1><p align="center">Please run Supercell Wx without admin or root permissions. Supercell Wx should not need such permissions to run. If you do not want to run Supercell Wx with high privilege, click &quot;Close&quot;, and relaunch with lower permissions. Otherwise, click &quot;Ignore&quot;. You may disable this warning with the checkbox below.</p></body></html> - - - Qt::TextFormat::RichText - - - Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop - - - true - - - - - - - Disable High Privilege Warning - - - - - - - Qt::Orientation::Horizontal - - - QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Ignore - - - - - - - - - - - buttonBox - accepted() - HighPrivilegeDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - HighPrivilegeDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - From 3ea37ba10468611d7e66bcc62a466a63b0393886 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 11 Jan 2025 14:27:54 -0500 Subject: [PATCH 341/762] privilege check clang-tidy fix --- scwx-qt/source/scwx/qt/main/check_privilege.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.cpp b/scwx-qt/source/scwx/qt/main/check_privilege.cpp index 965df9f8..710f70be 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.cpp @@ -102,7 +102,7 @@ bool PrivilegeChecker::pre_settings_check() return false; } - int result = p->dialog_.exec(); + const int result = p->dialog_.exec(); p->firstCheckCheckBoxState_ = p->checkBox_->isChecked(); return result != QMessageBox::Yes; From be8237da721e9f5be5984674fa0d59fff08f2e26 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 12 Jan 2025 09:24:48 -0500 Subject: [PATCH 342/762] Added fixes from GitHub comments for high_privilege_warning --- scwx-qt/source/scwx/qt/main/check_privilege.cpp | 4 ++-- scwx-qt/source/scwx/qt/main/check_privilege.hpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.cpp b/scwx-qt/source/scwx/qt/main/check_privilege.cpp index 710f70be..e6402af5 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.cpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.cpp @@ -1,5 +1,5 @@ -#include "scwx/qt/settings/general_settings.hpp" -#include "scwx/qt/main/check_privilege.hpp" +#include +#include #include #include #include diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.hpp b/scwx-qt/source/scwx/qt/main/check_privilege.hpp index f9fcadd9..7fe33eb3 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.hpp @@ -13,6 +13,11 @@ public: explicit PrivilegeChecker(); ~PrivilegeChecker(); + PrivilegeChecker(const PrivilegeChecker&) = delete; + PrivilegeChecker& operator=(const PrivilegeChecker&) = delete; + PrivilegeChecker(const PrivilegeChecker&&) = delete; + PrivilegeChecker& operator=(const PrivilegeChecker&&) = delete; + // returning true means check failed. bool pre_settings_check(); bool post_settings_check(); From f7d2346924ffed5e6b72101616b6e5ba43c713e9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 12 Jan 2025 09:35:59 -0500 Subject: [PATCH 343/762] update test/data for high_privilege_warning --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 24ececcd..1f3e1259 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 24ececcd183d3b8961e5638da89f0eb36309cd6b +Subproject commit 1f3e1259130a5eb4a6df37d721fe6c8301213e7e From 0a749c1245247fc8a4eeb45584a9fd05b8a7f135 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 12 Jan 2025 09:39:12 -0500 Subject: [PATCH 344/762] clang-format fixes for high_privilege_warning --- scwx-qt/source/scwx/qt/main/check_privilege.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/check_privilege.hpp b/scwx-qt/source/scwx/qt/main/check_privilege.hpp index 7fe33eb3..8ec3f8e2 100644 --- a/scwx-qt/source/scwx/qt/main/check_privilege.hpp +++ b/scwx-qt/source/scwx/qt/main/check_privilege.hpp @@ -13,9 +13,9 @@ public: explicit PrivilegeChecker(); ~PrivilegeChecker(); - PrivilegeChecker(const PrivilegeChecker&) = delete; - PrivilegeChecker& operator=(const PrivilegeChecker&) = delete; - PrivilegeChecker(const PrivilegeChecker&&) = delete; + PrivilegeChecker(const PrivilegeChecker&) = delete; + PrivilegeChecker& operator=(const PrivilegeChecker&) = delete; + PrivilegeChecker(const PrivilegeChecker&&) = delete; PrivilegeChecker& operator=(const PrivilegeChecker&&) = delete; // returning true means check failed. From e7b2981bd6d86944ebc63bbcaff0d65e60038e9e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 15 Jan 2025 10:01:55 -0500 Subject: [PATCH 345/762] Bump dependency Qt to 6.8.1 --- .github/workflows/ci.yml | 6 +++--- setup-debug.bat | 2 +- setup-debug.sh | 2 +- setup-multi.bat | 2 +- setup-release.bat | 2 +- setup-release.sh | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90c31612..cee8a72c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: compiler: msvc msvc_arch: x64 msvc_version: 2022 - qt_version: 6.8.0 + qt_version: 6.8.1 qt_arch_aqt: win64_msvc2022_64 qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -42,7 +42,7 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc - qt_version: 6.8.0 + qt_version: 6.8.1 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -57,7 +57,7 @@ jobs: env_cc: clang-17 env_cxx: clang++-17 compiler: clang - qt_version: 6.8.0 + qt_version: 6.8.1 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport diff --git a/setup-debug.bat b/setup-debug.bat index f082a0cc..afcd52c2 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -3,7 +3,7 @@ call tools\setup-common.bat set build_dir=build-debug set build_type=Debug set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.0 +set qt_version=6.8.1 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-debug.sh b/setup-debug.sh index 0abb4696..e48efe9f 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -4,7 +4,7 @@ build_dir=${1:-build-debug} build_type=Debug conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.0 +qt_version=6.8.1 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" diff --git a/setup-multi.bat b/setup-multi.bat index b12a0c06..9a51b1f9 100644 --- a/setup-multi.bat +++ b/setup-multi.bat @@ -2,7 +2,7 @@ call tools\setup-common.bat set build_dir=build set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.0 +set qt_version=6.8.1 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-release.bat b/setup-release.bat index 404ebdcd..a5481508 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -3,7 +3,7 @@ call tools\setup-common.bat set build_dir=build-release set build_type=Release set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.0 +set qt_version=6.8.1 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-release.sh b/setup-release.sh index 9b4f2779..d242ca1c 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -4,7 +4,7 @@ build_dir=${1:-build-release} build_type=Release conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.0 +qt_version=6.8.1 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" From 8e2674f83ef65e74068d79186417e834c8c74d0b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 15 Jan 2025 15:23:53 -0500 Subject: [PATCH 346/762] update clang-tidy-review for qt 6.8.1 --- .github/workflows/clang-tidy-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 25ec245e..d9366674 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -20,7 +20,7 @@ jobs: build_type: Release env_cc: clang-17 env_cxx: clang++-17 - qt_version: 6.8.0 + qt_version: 6.8.1 qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' From a3e9d68dc43351e6e674627d425449e2675f0544 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 17 Jan 2025 11:01:02 -0500 Subject: [PATCH 347/762] A few UI changes to location markers part2 --- .../source/scwx/qt/ui/edit_marker_dialog.cpp | 33 ++++++++++++++++--- .../source/scwx/qt/ui/edit_marker_dialog.hpp | 2 +- scwx-qt/source/scwx/qt/ui/marker_dialog.ui | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp index a6aaa4d4..4f310488 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.cpp @@ -23,6 +23,9 @@ namespace scwx::qt::ui static const std::string logPrefix_ = "scwx::qt::ui::edit_marker_dialog"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +static const QString addingTitle_ = QObject::tr("Add Location Marker"); +static const QString editingTitle_ = QObject::tr("Edit Location Marker"); + class EditMarkerDialog::Impl { public: @@ -32,6 +35,7 @@ public: void show_icon_file_dialog(); void set_icon_color(const std::string& color); + void set_adding(bool adding); void connect_signals(); @@ -58,6 +62,24 @@ QIcon EditMarkerDialog::Impl::get_colored_icon( QColor(QString::fromStdString(color))); } +void EditMarkerDialog::Impl::set_adding(bool adding) +{ + if (adding == adding_) + { + return; + } + + if (adding) + { + self_->setWindowTitle(addingTitle_); + } + else + { + self_->setWindowTitle(editingTitle_); + } + adding_ = adding; +} + EditMarkerDialog::EditMarkerDialog(QWidget* parent) : QDialog(parent), p {std::make_unique(this)}, @@ -84,7 +106,7 @@ EditMarkerDialog::~EditMarkerDialog() void EditMarkerDialog::setup() { - setup(0, 0); + setup(0.0, 0.0); } void EditMarkerDialog::setup(double latitude, double longitude) @@ -102,11 +124,10 @@ void EditMarkerDialog::setup(double latitude, double longitude) static_cast(color.blue()), static_cast(color.alpha())})); - setup(p->editId_); - p->adding_ = true; + setup(p->editId_, true); } -void EditMarkerDialog::setup(types::MarkerId id) +void EditMarkerDialog::setup(types::MarkerId id, bool adding) { std::optional marker = p->markerManager_->get_marker(id); if (!marker) @@ -115,7 +136,7 @@ void EditMarkerDialog::setup(types::MarkerId id) } p->editId_ = id; - p->adding_ = false; + p->set_adding(adding); const std::string iconColorStr = util::color::ToArgbString(marker->iconColor); @@ -300,6 +321,8 @@ void EditMarkerDialog::Impl::set_icon_color(const std::string& color) void EditMarkerDialog::Impl::handle_accepted() { + // switch to editing to that canceling after applying does not delete it + set_adding(false); markerManager_->set_marker(editId_, self_->get_marker_info()); } diff --git a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp index c20ebe27..3990b6c0 100644 --- a/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/edit_marker_dialog.hpp @@ -21,7 +21,7 @@ public: void setup(); void setup(double latitude, double longitude); - void setup(types::MarkerId id); + void setup(types::MarkerId id, bool adding = false); [[nodiscard]] types::MarkerInfo get_marker_info() const; diff --git a/scwx-qt/source/scwx/qt/ui/marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui index 641775a5..6256b756 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui @@ -11,7 +11,7 @@ - Marker Manager + Location Marker Manager From 607d72d7bb7b6d7ff338908feb96502b5334304c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 19 Jan 2025 23:41:42 -0600 Subject: [PATCH 348/762] In level 2 debug output, convert Julian date and milliseconds to standard format --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index bdd1bcfc..89c7613b 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #if defined(__GNUC__) # pragma GCC diagnostic pop @@ -253,10 +254,13 @@ bool Ar2vFile::LoadData(std::istream& is) if (dataValid) { + auto timePoint = util::TimePoint(p->julianDate_, p->milliseconds_); + logger_->debug("Filename: {}", p->tapeFilename_); logger_->debug("Extension: {}", p->extensionNumber_); - logger_->debug("Date: {}", p->julianDate_); - logger_->debug("Time: {}", p->milliseconds_); + logger_->debug("Date: {} ({:%Y-%m-%d})", p->julianDate_, timePoint); + logger_->debug( + "Time: {} ({:%H:%M:%S})", p->milliseconds_, timePoint); logger_->debug("ICAO: {}", p->icao_); size_t decompressedRecords = p->DecompressLDMRecords(is); From e522f85a72bd890280b8d6db2826d68f209b6bbb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 19 Jan 2025 23:42:37 -0600 Subject: [PATCH 349/762] Collection time should not include milliseconds when selecting based off time --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index d25bb28d..b51d5c70 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1507,10 +1507,10 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, if (recordRadarData != nullptr) { - auto& radarData0 = (*recordRadarData)[0]; - auto collectionTime = + auto& radarData0 = (*recordRadarData)[0]; + auto collectionTime = std::chrono::floor( scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time()); + radarData0->collection_time())); // Find the newest radar data, not newer than the selected time if (radarData == nullptr || From c94e483c6ee50a7cc24324358440abd4813a91c2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 20 Jan 2025 00:03:31 -0600 Subject: [PATCH 350/762] Ignore milliseconds when retrieving scan from level 2 file by time --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 89c7613b..ca92db56 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -188,7 +188,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, for (auto& scan : elevationScans) { - auto& scanTime = scan.first; + auto scanTime = std::chrono::floor(scan.first); if (elevationScan == nullptr || (scanTime <= time && scanTime > foundTime)) From a92248b4d93fbb7f9ad6de5a71a79a578f3a7487 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 20 Jan 2025 00:23:14 -0600 Subject: [PATCH 351/762] Correlation coefficient should be scaled by 100 when a % sign is shown --- scwx-qt/source/scwx/qt/view/level2_product_view.cpp | 12 ++++++++++++ scwx-qt/source/scwx/qt/view/level3_product_view.cpp | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 8dc44ab2..4e7181e7 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -51,6 +51,9 @@ static const std::unordered_map productScale_ { + {common::Level2Product::CorrelationCoefficient, 100.0f}}; + static const std::unordered_map productUnits_ {{common::Level2Product::Reflectivity, "dBZ"}, {common::Level2Product::DifferentialReflectivity, "dB"}, @@ -295,6 +298,15 @@ float Level2ProductView::unit_scale() const break; } + if (p->otherUnits_ == types::OtherUnits::Default) + { + auto it = productScale_.find(p->product_); + if (it != productScale_.cend()) + { + return it->second; + } + } + return 1.0f; } diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index 3858ac11..97985a39 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -29,6 +29,10 @@ static const auto logger_ = util::Logger::Create(logPrefix_); static constexpr uint16_t RANGE_FOLDED = 1u; +static const std::unordered_map + categoryScale_ { + {common::Level3ProductCategory::CorrelationCoefficient, 100.0f}}; + static const std::unordered_map categoryUnits_ { {common::Level3ProductCategory::Reflectivity, "dBZ"}, @@ -217,6 +221,15 @@ float Level3ProductView::unit_scale() const break; } + if (p->otherUnits_ == types::OtherUnits::Default) + { + auto it = categoryScale_.find(p->category_); + if (it != categoryScale_.cend()) + { + return it->second; + } + } + return 1.0f; } From 69d5a36f551966b01442a62a49f6b405847a01b4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 20 Jan 2025 22:15:14 -0600 Subject: [PATCH 352/762] When viewing live level 2 data, ensure the latest scan is selected --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 11 ++++++++++- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index f7ba99ae..cc501408 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1801,7 +1801,16 @@ void MapWidgetImpl::RadarProductManagerConnect() (group == common::RadarProductGroup::Level2 || context_->radar_product() == product)) { - widget_->SelectRadarProduct(record); + if (group == common::RadarProductGroup::Level2) + { + // Level 2 products may have multiple time points, + // ensure the latest is selected + widget_->SelectRadarProduct(group, product); + } + else + { + widget_->SelectRadarProduct(record); + } } }); } diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index ca92db56..1069fbcd 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -191,7 +191,9 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, auto scanTime = std::chrono::floor(scan.first); if (elevationScan == nullptr || - (scanTime <= time && scanTime > foundTime)) + ((scanTime <= time || + time == std::chrono::system_clock::time_point {}) && + scanTime > foundTime)) { elevationScan = scan.second; foundTime = scanTime; From 9d0a75d032d3b6a78270cd9ac95ef40c21928667 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 20 Jan 2025 23:56:49 -0600 Subject: [PATCH 353/762] Bump version to v0.4.8 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cee8a72c..41a53fc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.7 + SCWX_VERSION: v0.4.8 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 3df5ba4a..0184bb65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} - VERSION 0.4.7 + VERSION 0.4.8 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -27,7 +27,7 @@ set_property(DIRECTORY set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.7") +set(SCWX_VERSION "0.4.8") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) From 467cb35648987c999b46b82ae28e8d123e8c04c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:32:59 +0000 Subject: [PATCH 354/762] Update dependency sqlite3 to v3.48.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 10c6e716..e503c87f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -20,7 +20,7 @@ class SupercellWxConan(ConanFile): "openssl/3.3.2", "re2/20240702", "spdlog/1.15.0", - "sqlite3/3.47.2", + "sqlite3/3.48.0", "vulkan-loader/1.3.290.0", "zlib/1.3.1") generators = ("CMakeDeps") From 47a3c1a0155acea7666696442c1b9358bbf33065 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 23 Jan 2025 00:27:40 +0000 Subject: [PATCH 355/762] Revert libcurl 8.11.1 to 8.10.1 libcurl 8.11.1 has a bug with improperly closing file handles --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 10c6e716..0814bc3a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -14,7 +14,7 @@ class SupercellWxConan(ConanFile): "glew/2.2.0", "glm/1.0.1", "gtest/1.15.0", - "libcurl/8.11.1", + "libcurl/8.10.1", "libpng/1.6.45", "libxml2/2.13.4", "openssl/3.3.2", From 3055284664e9631595aaa96a98dda0c72c19dad5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 24 Jan 2025 14:18:08 -0500 Subject: [PATCH 356/762] Set parameters in each layer to ensure params is set for mouse picking. --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 1 + scwx-qt/source/scwx/qt/map/marker_layer.cpp | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 7d03d13a..e6716c98 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -48,6 +48,7 @@ void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = p->context_->gl(); p->textureAtlas_ = p->context_->GetTextureAtlas(); + p->context_->set_render_parameters(params); // Determine if the texture atlas changed since last render std::uint64_t newTextureAtlasBuildCount = diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 906ef7c1..39e43a57 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -162,7 +162,6 @@ void MarkerLayer::Impl::set_icon_sheets() void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = context()->gl(); - context()->set_render_parameters(params); DrawLayer::Render(params); From 1d564ff7b9dfedc408b9744f64b764512906fde0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 23 Jan 2025 20:20:17 -0600 Subject: [PATCH 357/762] Initial Linux Arm64 build --- .github/workflows/ci.yml | 39 +++++++++++++++----- tools/conan/profiles/scwx-linux_gcc-11_armv8 | 8 ++++ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 tools/conan/profiles/scwx-linux_gcc-11_armv8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41a53fc2..f9209e04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,8 +35,9 @@ jobs: qt_tools: '' conan_package_manager: '' conan_profile: scwx-win64_msvc2022 + appimage_arch: '' artifact_suffix: windows-x64 - - name: linux64_gcc + - name: linux_gcc_x64 os: ubuntu-22.04 build_type: Release env_cc: gcc-11 @@ -49,9 +50,10 @@ jobs: qt_tools: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11 + appimage_arch: x86_64 artifact_suffix: linux-x64 compiler_packages: '' - - name: linux64_clang + - name: linux_clang_x64 os: ubuntu-24.04 build_type: Release env_cc: clang-17 @@ -64,8 +66,25 @@ jobs: qt_tools: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 + appimage_arch: x86_64 artifact_suffix: linux-clang-x64 compiler_packages: clang-17 + - name: linux_gcc_arm64 + os: ubuntu-22.04-arm + build_type: Release + env_cc: gcc-11 + env_cxx: g++-11 + compiler: gcc + qt_version: 6.8.1 + qt_arch_aqt: linux_gcc_arm64 + qt_arch_dir: gcc_arm64 + qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport + qt_tools: '' + conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True + conan_profile: scwx-linux_gcc-11_armv8 + appimage_arch: aarch64 + artifact_suffix: linux-arm64 + compiler_packages: '' name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} @@ -231,14 +250,14 @@ jobs: if: ${{ startsWith(matrix.os, 'ubuntu') }} env: APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx/ - LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*x86_64.AppImage.zsync - LDAI_OUTPUT: supercell-wx-${{ env.SCWX_VERSION }}-x86_64.AppImage + LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*${{ matrix.appimage_arch }}.AppImage.zsync + LDAI_OUTPUT: supercell-wx-${{ env.SCWX_VERSION }}-${{ matrix.appimage_arch }}.AppImage LINUXDEPLOY_OUTPUT_APP_NAME: supercell-wx LINUXDEPLOY_OUTPUT_VERSION: ${{ env.SCWX_VERSION }} shell: bash run: | - wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage - chmod +x linuxdeploy-x86_64.AppImage + wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${{ matrix.appimage_arch }}.AppImage + chmod +x linuxdeploy-${{ matrix.appimage_arch }}.AppImage cp "${{ github.workspace }}/source/scwx-qt/res/icons/scwx-256.png" supercell-wx.png cp "${{ github.workspace }}/source/scwx-qt/res/linux/supercell-wx.desktop" . pushd "${{ env.APPIMAGE_DIR }}" @@ -247,16 +266,16 @@ jobs: mv lib/ usr/ mv plugins/ usr/ popd - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.APPIMAGE_DIR }} -i supercell-wx.png -d supercell-wx.desktop - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.APPIMAGE_DIR }} --output appimage - rm -f linuxdeploy-x86_64.AppImage + ./linuxdeploy-${{ matrix.appimage_arch }}.AppImage --appdir ${{ env.APPIMAGE_DIR }} -i supercell-wx.png -d supercell-wx.desktop + ./linuxdeploy-${{ matrix.appimage_arch }}.AppImage --appdir ${{ env.APPIMAGE_DIR }} --output appimage + rm -f linuxdeploy-${{ matrix.appimage_arch }}.AppImage - name: Upload AppImage (Linux) if: ${{ startsWith(matrix.os, 'ubuntu') }} uses: actions/upload-artifact@v4 with: name: supercell-wx-appimage-${{ matrix.artifact_suffix }} - path: ${{ github.workspace }}/*-x86_64.AppImage* + path: ${{ github.workspace }}/*-${{ matrix.appimage_arch }}.AppImage* - name: Test Supercell Wx working-directory: ${{ github.workspace }}/build diff --git a/tools/conan/profiles/scwx-linux_gcc-11_armv8 b/tools/conan/profiles/scwx-linux_gcc-11_armv8 new file mode 100644 index 00000000..803d1c8d --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-11_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=11 +os=Linux From 85eadf6f93c991dbdf23406a972034b06094f4d3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 23 Jan 2025 22:07:14 -0600 Subject: [PATCH 358/762] Bump install-qt-action to v4 --- .github/workflows/ci.yml | 2 +- .github/workflows/clang-tidy-review.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9209e04..9b242293 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: submodules: recursive - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index d9366674..97beace8 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: include: - - name: linux64_clang-tidy + - name: linux_clang-tidy_x64 os: ubuntu-24.04 build_type: Release env_cc: clang-17 @@ -48,7 +48,7 @@ jobs: path: clang-tidy-review - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} From 17982013be65095c1c471c644e1ea37caa3a42d8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 25 Jan 2025 09:11:59 -0600 Subject: [PATCH 359/762] Use install-qt-action fork that supports Arm64 builds --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b242293..0a9470a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: submodules: recursive - name: Install Qt - uses: jurplel/install-qt-action@v4 + uses: dpaulat/install-qt-action@b45c67aaa9e0ea77e59a7031ec14a12d5ddf4b35 with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} From 8e11ae5efddd1e0d236309e9619dfe30fdf695fa Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 25 Jan 2025 12:56:51 -0600 Subject: [PATCH 360/762] Build linux arm64 with Ubuntu 24.04 (Qt requirement) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a9470a3..408cacf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: artifact_suffix: linux-clang-x64 compiler_packages: clang-17 - name: linux_gcc_arm64 - os: ubuntu-22.04-arm + os: ubuntu-24.04-arm build_type: Release env_cc: gcc-11 env_cxx: g++-11 @@ -84,7 +84,7 @@ jobs: conan_profile: scwx-linux_gcc-11_armv8 appimage_arch: aarch64 artifact_suffix: linux-arm64 - compiler_packages: '' + compiler_packages: g++-11 name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} From 517ec9b266fb87afe72f101eeff2f3ccfd59c444 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 26 Jan 2025 13:03:01 -0600 Subject: [PATCH 361/762] Add additional armv8 profiles --- tools/conan/profiles/scwx-linux_clang-17_armv8 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-12_armv8 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-13_armv8 | 8 ++++++++ tools/conan/profiles/scwx-linux_gcc-14_armv8 | 8 ++++++++ 4 files changed, 32 insertions(+) create mode 100644 tools/conan/profiles/scwx-linux_clang-17_armv8 create mode 100644 tools/conan/profiles/scwx-linux_gcc-12_armv8 create mode 100644 tools/conan/profiles/scwx-linux_gcc-13_armv8 create mode 100644 tools/conan/profiles/scwx-linux_gcc-14_armv8 diff --git a/tools/conan/profiles/scwx-linux_clang-17_armv8 b/tools/conan/profiles/scwx-linux_clang-17_armv8 new file mode 100644 index 00000000..21ab5248 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_clang-17_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=17 +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-12_armv8 b/tools/conan/profiles/scwx-linux_gcc-12_armv8 new file mode 100644 index 00000000..0a540c20 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-12_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=12 +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-13_armv8 b/tools/conan/profiles/scwx-linux_gcc-13_armv8 new file mode 100644 index 00000000..7e0c0c66 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-13_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=13 +os=Linux diff --git a/tools/conan/profiles/scwx-linux_gcc-14_armv8 b/tools/conan/profiles/scwx-linux_gcc-14_armv8 new file mode 100644 index 00000000..1b95e2bb --- /dev/null +++ b/tools/conan/profiles/scwx-linux_gcc-14_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=gcc +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=14 +os=Linux From 0a3913bdee3664f0eb3c0a465fe1ecdd550e8a33 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 09:44:01 -0500 Subject: [PATCH 362/762] NOLINT setting settings defaults/minimums/maximums for magic numbers --- scwx-qt/source/scwx/qt/settings/audio_settings.cpp | 4 +++- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 3 +++ scwx-qt/source/scwx/qt/settings/line_settings.cpp | 3 +++ scwx-qt/source/scwx/qt/settings/product_settings.cpp | 3 +++ scwx-qt/source/scwx/qt/settings/text_settings.cpp | 6 ++++++ scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 3 +++ scwx-qt/source/scwx/qt/settings/unit_settings.cpp | 3 +++ 7 files changed, 24 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index a799793b..e620a6f4 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -33,6 +33,8 @@ public: boost::to_lower(defaultAlertLocationMethodValue); + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) alertSoundFile_.SetDefault(defaultAlertSoundFileValue); alertLocationMethod_.SetDefault(defaultAlertLocationMethodValue); alertLatitude_.SetDefault(0.0); @@ -48,7 +50,7 @@ public: alertLongitude_.SetMaximum(180.0); alertRadius_.SetMinimum(0.0); alertRadius_.SetMaximum(9999999999); - + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) alertLocationMethod_.SetValidator( SCWX_SETTINGS_ENUM_VALIDATOR(types::LocationMethod, diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 6f254c6b..a103a393 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -50,6 +50,8 @@ public: boost::to_lower(defaultPositioningPlugin); boost::to_lower(defaultThemeValue); + // SetDefault, SetMinimum, and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) antiAliasingEnabled_.SetDefault(true); clockFormat_.SetDefault(defaultClockFormatValue); customStyleDrawLayer_.SetDefault(".*\\.annotations\\.points"); @@ -100,6 +102,7 @@ public: nmeaBaudRate_.SetMaximum(999999999); radarSiteThreshold_.SetMinimum(-10000); radarSiteThreshold_.SetMaximum(10000); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) customStyleDrawLayer_.SetTransform([](const std::string& value) { return boost::trim_copy(value); }); diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp index 45337fa2..70a82761 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -27,6 +27,8 @@ class LineSettings::Impl public: explicit Impl() { + // SetDefault, SetMinimum, and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) lineColor_.SetDefault(kWhiteColorString_); highlightColor_.SetDefault(kTransparentColorString_); borderColor_.SetDefault(kBlackColorString_); @@ -42,6 +44,7 @@ public: lineWidth_.SetMaximum(9); highlightWidth_.SetMaximum(9); borderWidth_.SetMaximum(9); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) lineColor_.SetValidator(&util::color::ValidateArgbString); highlightColor_.SetValidator(&util::color::ValidateArgbString); diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index b9287474..be54fbd4 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -15,9 +15,12 @@ class ProductSettings::Impl public: explicit Impl() { + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) showSmoothedRangeFolding_.SetDefault(false); stiForecastEnabled_.SetDefault(true); stiPastEnabled_.SetDefault(true); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) } ~Impl() {} diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index 942ad4f8..2d0dc197 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -50,12 +50,15 @@ public: boost::to_lower(defaultTooltipMethodValue); + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) hoverTextWrap_.SetDefault(80); hoverTextWrap_.SetMinimum(0); hoverTextWrap_.SetMaximum(999); placefileTextDropShadowEnabled_.SetDefault(true); radarSiteHoverTextEnabled_.SetDefault(true); tooltipMethod_.SetDefault(defaultTooltipMethodValue); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) tooltipMethod_.SetValidator( [](const std::string& value) @@ -141,8 +144,11 @@ void TextSettings::Impl::InitializeFontVariables() { return !value.empty(); }); // Font point size must be between 6 and 72 + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) font.fontPointSize_.SetMinimum(6.0); font.fontPointSize_.SetMaximum(72.0); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) // Variable registration auto& settings = fontSettings_.emplace_back( diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index eca2c8e1..81fec044 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -14,6 +14,8 @@ class UiSettingsImpl public: explicit UiSettingsImpl() { + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) level2ProductsExpanded_.SetDefault(false); level2SettingsExpanded_.SetDefault(true); level3ProductsExpanded_.SetDefault(true); @@ -21,6 +23,7 @@ public: timelineExpanded_.SetDefault(true); mainUIState_.SetDefault(""); mainUIGeometry_.SetDefault(""); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) } ~UiSettingsImpl() {} diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp index c2cb0f11..79390301 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp @@ -35,11 +35,14 @@ public: boost::to_lower(defaultSpeedUnitsValue); boost::to_lower(defaultDistanceUnitsValue); + // SetDefault, SetMinimum and SetMaximum are descriptive + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) accumulationUnits_.SetDefault(defaultAccumulationUnitsValue); echoTopsUnits_.SetDefault(defaultEchoTopsUnitsValue); otherUnits_.SetDefault(defaultOtherUnitsValue); speedUnits_.SetDefault(defaultSpeedUnitsValue); distanceUnits_.SetDefault(defaultDistanceUnitsValue); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) accumulationUnits_.SetValidator( SCWX_SETTINGS_ENUM_VALIDATOR(types::AccumulationUnits, From ba62004002998b41f07dce72d03ada4feb0596e5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 09:55:30 -0500 Subject: [PATCH 363/762] Combine namespaces in settings files into single line --- .../source/scwx/qt/settings/alert_palette_settings.cpp | 10 ++-------- .../source/scwx/qt/settings/alert_palette_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/audio_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/audio_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/general_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/line_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/line_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/map_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/palette_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/palette_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/product_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/product_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_category.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_category.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_container.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_container.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_interface.hpp | 10 ++-------- .../scwx/qt/settings/settings_interface_base.cpp | 10 ++-------- .../scwx/qt/settings/settings_interface_base.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_variable.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/settings_variable.hpp | 10 ++-------- .../source/scwx/qt/settings/settings_variable_base.cpp | 10 ++-------- .../source/scwx/qt/settings/settings_variable_base.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/text_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/text_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/ui_settings.hpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/unit_settings.cpp | 10 ++-------- scwx-qt/source/scwx/qt/settings/unit_settings.hpp | 10 ++-------- 34 files changed, 68 insertions(+), 272 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp index c684e1ed..b15b184e 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp @@ -7,11 +7,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = @@ -236,6 +232,4 @@ bool operator==(const AlertPaletteSettings& lhs, lhs.p->tornadoPossible_ == rhs.p->tornadoPossible_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp index 152a6351..0741654a 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp @@ -8,11 +8,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class AlertPaletteSettings : public SettingsCategory @@ -41,6 +37,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index e620a6f4..f954cf72 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -9,11 +9,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::audio_settings"; @@ -210,6 +206,4 @@ bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) lhs.p->alertEnabled_ == rhs.p->alertEnabled_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index 579d3599..539fde8a 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -7,11 +7,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class AudioSettings : public SettingsCategory @@ -46,6 +42,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index a103a393..9df9f227 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -13,11 +13,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::general_settings"; @@ -447,6 +443,4 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) rhs.p->highPrivilegeWarningEnabled_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 074b956e..19938adf 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class GeneralSettings : public SettingsCategory @@ -70,6 +66,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp index e9c6b007..d9261970 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::hotkey_settings"; @@ -142,6 +138,4 @@ static bool IsHotkeyValid(const std::string& value) .toStdString() == value; } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp index 9fa56cc4..f7b8a3cd 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp @@ -7,11 +7,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class HotkeySettings : public SettingsCategory @@ -37,6 +33,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp index 70a82761..9910ce6b 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -1,11 +1,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::line_settings"; @@ -153,6 +149,4 @@ bool operator==(const LineSettings& lhs, const LineSettings& rhs) lhs.p->lineWidth_ == rhs.p->lineWidth_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp index 6f6988a2..28f82ccb 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -8,11 +8,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class LineSettings : public SettingsCategory @@ -53,6 +49,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index e1c8276e..6cd91815 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -9,11 +9,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::map_settings"; @@ -276,6 +272,4 @@ bool operator==(const MapSettings& lhs, const MapSettings& rhs) return (lhs.p->map_ == rhs.p->map_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index 36ce6464..31956326 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class MapSettings : public SettingsCategory @@ -60,6 +56,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index c5902fb3..6046f79b 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::palette_settings"; @@ -252,6 +248,4 @@ bool operator==(const PaletteSettings& lhs, const PaletteSettings& rhs) lhs.p->inactiveAlertColor_ == rhs.p->inactiveAlertColor_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index eb52e600..961b17d5 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -9,11 +9,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class PaletteSettings : public SettingsCategory @@ -45,6 +41,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index be54fbd4..9bcc3a1a 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -1,11 +1,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::product_settings"; @@ -85,6 +81,4 @@ bool operator==(const ProductSettings& lhs, const ProductSettings& rhs) lhs.p->stiPastEnabled_ == rhs.p->stiPastEnabled_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index 570c6b15..2ebf5ca9 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class ProductSettings : public SettingsCategory @@ -41,6 +37,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 75a46bf8..184d530d 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -4,11 +4,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::settings_category"; @@ -479,6 +475,4 @@ void SettingsCategory::Impl::ConnectVariable(SettingsVariableBase* variable) })); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index 167af06a..e5935636 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -8,11 +8,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class SettingsCategory @@ -118,6 +114,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.cpp b/scwx-qt/source/scwx/qt/settings/settings_container.cpp index cf8f9c18..ab6ac1e5 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.cpp @@ -1,11 +1,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::settings_container"; @@ -172,6 +168,4 @@ bool SettingsContainer::Equals(const SettingsVariableBase& o) const template class SettingsContainer>; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.hpp b/scwx-qt/source/scwx/qt/settings/settings_container.hpp index 9c2ea487..1f57e0b3 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { template @@ -99,6 +95,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 4d491563..9aa58ac3 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -13,11 +13,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::settings_interface"; @@ -633,6 +629,4 @@ template class SettingsInterface; // Containers are not to be used directly template class SettingsInterface>; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index a0005098..0561cf12 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -9,11 +9,7 @@ class QLabel; -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { template @@ -140,6 +136,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp index 37e1ec25..9bbf9402 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = @@ -29,6 +25,4 @@ SettingsInterfaceBase::SettingsInterfaceBase(SettingsInterfaceBase&&) noexcept = SettingsInterfaceBase& SettingsInterfaceBase::operator=(SettingsInterfaceBase&&) noexcept = default; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp index d0dc2ff2..16e89469 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface_base.hpp @@ -5,11 +5,7 @@ class QAbstractButton; class QWidget; -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class SettingsInterfaceBase @@ -70,6 +66,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 6eda8437..1e6ae3e4 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -7,11 +7,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::settings_variable"; @@ -439,6 +435,4 @@ template class SettingsVariable; // Containers are not to be used directly template class SettingsVariable>; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index 2c3b2a07..612f57e7 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -10,11 +10,7 @@ class QAbstractButton; class QWidget; -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { template @@ -257,6 +253,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp index 7e31fb5f..6d4e54f6 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp @@ -1,10 +1,6 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = @@ -62,6 +58,4 @@ bool operator==(const SettingsVariableBase& lhs, return typeid(lhs) == typeid(rhs) && lhs.Equals(rhs); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp index f4e48934..870a0c9d 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { /** @@ -117,6 +113,4 @@ private: bool operator==(const SettingsVariableBase& lhs, const SettingsVariableBase& rhs); -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index 2d0dc197..63fcb2cd 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -3,11 +3,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::text_settings"; @@ -216,6 +212,4 @@ bool operator==(const TextSettings& lhs, const TextSettings& rhs) lhs.p->tooltipMethod_ == rhs.p->tooltipMethod_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.hpp b/scwx-qt/source/scwx/qt/settings/text_settings.hpp index 2be5ab13..c4cc64af 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.hpp @@ -7,11 +7,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class TextSettings : public SettingsCategory @@ -48,6 +44,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 81fec044..5a3175e9 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -1,10 +1,6 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::ui_settings"; @@ -122,6 +118,4 @@ bool operator==(const UiSettings& lhs, const UiSettings& rhs) lhs.p->mainUIGeometry_ == rhs.p->mainUIGeometry_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index 0a9f95ef..e052eb90 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class UiSettingsImpl; @@ -45,6 +41,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp index 79390301..37ec8a0c 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp @@ -4,11 +4,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { static const std::string logPrefix_ = "scwx::qt::settings::unit_settings"; @@ -130,6 +126,4 @@ bool operator==(const UnitSettings& lhs, const UnitSettings& rhs) lhs.p->distanceUnits_ == rhs.p->distanceUnits_); } -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp index 15518492..403e2b9c 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace settings +namespace scwx::qt::settings { class UnitSettings : public SettingsCategory @@ -40,6 +36,4 @@ private: std::unique_ptr p; }; -} // namespace settings -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::settings From 36339e613f333b1e56767aecbefbe96e69fda4a9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 10:06:46 -0500 Subject: [PATCH 364/762] Add [[nodiscard]] to functions which need it in settings files --- .../qt/settings/alert_palette_settings.hpp | 8 +-- .../scwx/qt/settings/audio_settings.hpp | 21 +++--- .../scwx/qt/settings/general_settings.hpp | 66 ++++++++++--------- .../scwx/qt/settings/hotkey_settings.hpp | 3 +- .../source/scwx/qt/settings/line_settings.hpp | 18 ++--- .../source/scwx/qt/settings/map_settings.hpp | 2 +- .../scwx/qt/settings/palette_settings.hpp | 7 +- .../scwx/qt/settings/settings_category.hpp | 6 +- .../scwx/qt/settings/settings_container.hpp | 3 +- .../scwx/qt/settings/settings_variable.hpp | 3 +- .../qt/settings/settings_variable_base.hpp | 8 +-- .../source/scwx/qt/settings/text_settings.hpp | 15 +++-- .../source/scwx/qt/settings/ui_settings.hpp | 14 ++-- .../source/scwx/qt/settings/unit_settings.hpp | 10 +-- 14 files changed, 96 insertions(+), 88 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp index 0741654a..ec77d060 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp @@ -23,11 +23,11 @@ public: AlertPaletteSettings(AlertPaletteSettings&&) noexcept; AlertPaletteSettings& operator=(AlertPaletteSettings&&) noexcept; - LineSettings& + [[nodiscard]] LineSettings& threat_category(awips::ibw::ThreatCategory threatCategory) const; - LineSettings& inactive() const; - LineSettings& observed() const; - LineSettings& tornado_possible() const; + [[nodiscard]] LineSettings& inactive() const; + [[nodiscard]] LineSettings& observed() const; + [[nodiscard]] LineSettings& tornado_possible() const; friend bool operator==(const AlertPaletteSettings& lhs, const AlertPaletteSettings& rhs); diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index 539fde8a..c6e6befd 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -22,16 +22,17 @@ public: AudioSettings(AudioSettings&&) noexcept; AudioSettings& operator=(AudioSettings&&) noexcept; - SettingsVariable& alert_sound_file() const; - SettingsVariable& alert_location_method() const; - SettingsVariable& alert_latitude() const; - SettingsVariable& alert_longitude() const; - SettingsVariable& alert_radius() const; - SettingsVariable& alert_radar_site() const; - SettingsVariable& alert_county() const; - SettingsVariable& alert_wfo() const; - SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; - SettingsVariable& ignore_missing_codecs() const; + [[nodiscard]] SettingsVariable& alert_sound_file() const; + [[nodiscard]] SettingsVariable& alert_location_method() const; + [[nodiscard]] SettingsVariable& alert_latitude() const; + [[nodiscard]] SettingsVariable& alert_longitude() const; + [[nodiscard]] SettingsVariable& alert_radius() const; + [[nodiscard]] SettingsVariable& alert_radar_site() const; + [[nodiscard]] SettingsVariable& alert_county() const; + [[nodiscard]] SettingsVariable& alert_wfo() const; + [[nodiscard]] SettingsVariable& + alert_enabled(awips::Phenomenon phenomenon) const; + [[nodiscard]] SettingsVariable& ignore_missing_codecs() const; static AudioSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 19938adf..8e890ce3 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -21,38 +21,40 @@ public: GeneralSettings(GeneralSettings&&) noexcept; GeneralSettings& operator=(GeneralSettings&&) noexcept; - SettingsVariable& anti_aliasing_enabled() const; - SettingsVariable& clock_format() const; - SettingsVariable& custom_style_draw_layer() const; - SettingsVariable& custom_style_url() const; - SettingsVariable& debug_enabled() const; - SettingsVariable& default_alert_action() const; - SettingsVariable& default_radar_site() const; - SettingsVariable& default_time_zone() const; - SettingsContainer>& font_sizes() const; - SettingsVariable& grid_height() const; - SettingsVariable& grid_width() const; - SettingsVariable& loop_delay() const; - SettingsVariable& loop_speed() const; - SettingsVariable& loop_time() const; - SettingsVariable& map_provider() const; - SettingsVariable& mapbox_api_key() const; - SettingsVariable& maptiler_api_key() const; - SettingsVariable& nmea_baud_rate() const; - SettingsVariable& nmea_source() const; - SettingsVariable& positioning_plugin() const; - SettingsVariable& process_module_warnings_enabled() const; - SettingsVariable& show_map_attribution() const; - SettingsVariable& show_map_center() const; - SettingsVariable& show_map_logo() const; - SettingsVariable& theme() const; - SettingsVariable& theme_file() const; - SettingsVariable& track_location() const; - SettingsVariable& update_notifications_enabled() const; - SettingsVariable& warnings_provider() const; - SettingsVariable& cursor_icon_always_on() const; - SettingsVariable& radar_site_threshold() const; - SettingsVariable& high_privilege_warning_enabled() const; + [[nodiscard]] SettingsVariable& anti_aliasing_enabled() const; + [[nodiscard]] SettingsVariable& clock_format() const; + [[nodiscard]] SettingsVariable& custom_style_draw_layer() const; + [[nodiscard]] SettingsVariable& custom_style_url() const; + [[nodiscard]] SettingsVariable& debug_enabled() const; + [[nodiscard]] SettingsVariable& default_alert_action() const; + [[nodiscard]] SettingsVariable& default_radar_site() const; + [[nodiscard]] SettingsVariable& default_time_zone() const; + [[nodiscard]] SettingsContainer>& + font_sizes() const; + [[nodiscard]] SettingsVariable& grid_height() const; + [[nodiscard]] SettingsVariable& grid_width() const; + [[nodiscard]] SettingsVariable& loop_delay() const; + [[nodiscard]] SettingsVariable& loop_speed() const; + [[nodiscard]] SettingsVariable& loop_time() const; + [[nodiscard]] SettingsVariable& map_provider() const; + [[nodiscard]] SettingsVariable& mapbox_api_key() const; + [[nodiscard]] SettingsVariable& maptiler_api_key() const; + [[nodiscard]] SettingsVariable& nmea_baud_rate() const; + [[nodiscard]] SettingsVariable& nmea_source() const; + [[nodiscard]] SettingsVariable& positioning_plugin() const; + [[nodiscard]] SettingsVariable& + process_module_warnings_enabled() const; + [[nodiscard]] SettingsVariable& show_map_attribution() const; + [[nodiscard]] SettingsVariable& show_map_center() const; + [[nodiscard]] SettingsVariable& show_map_logo() const; + [[nodiscard]] SettingsVariable& theme() const; + [[nodiscard]] SettingsVariable& theme_file() const; + [[nodiscard]] SettingsVariable& track_location() const; + [[nodiscard]] SettingsVariable& update_notifications_enabled() const; + [[nodiscard]] SettingsVariable& warnings_provider() const; + [[nodiscard]] SettingsVariable& cursor_icon_always_on() const; + [[nodiscard]] SettingsVariable& radar_site_threshold() const; + [[nodiscard]] SettingsVariable& high_privilege_warning_enabled() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp index f7b8a3cd..02371f59 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp @@ -22,7 +22,8 @@ public: HotkeySettings(HotkeySettings&&) noexcept; HotkeySettings& operator=(HotkeySettings&&) noexcept; - SettingsVariable& hotkey(scwx::qt::types::Hotkey hotkey) const; + [[nodiscard]] SettingsVariable& + hotkey(scwx::qt::types::Hotkey hotkey) const; static HotkeySettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp index 28f82ccb..53d11681 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -23,17 +23,17 @@ public: LineSettings(LineSettings&&) noexcept; LineSettings& operator=(LineSettings&&) noexcept; - SettingsVariable& border_color() const; - SettingsVariable& highlight_color() const; - SettingsVariable& line_color() const; + [[nodiscard]] SettingsVariable& border_color() const; + [[nodiscard]] SettingsVariable& highlight_color() const; + [[nodiscard]] SettingsVariable& line_color() const; - SettingsVariable& border_width() const; - SettingsVariable& highlight_width() const; - SettingsVariable& line_width() const; + [[nodiscard]] SettingsVariable& border_width() const; + [[nodiscard]] SettingsVariable& highlight_width() const; + [[nodiscard]] SettingsVariable& line_width() const; - boost::gil::rgba32f_pixel_t GetBorderColorRgba32f() const; - boost::gil::rgba32f_pixel_t GetHighlightColorRgba32f() const; - boost::gil::rgba32f_pixel_t GetLineColorRgba32f() const; + [[nodiscard]] boost::gil::rgba32f_pixel_t GetBorderColorRgba32f() const; + [[nodiscard]] boost::gil::rgba32f_pixel_t GetHighlightColorRgba32f() const; + [[nodiscard]] boost::gil::rgba32f_pixel_t GetLineColorRgba32f() const; void StageValues(boost::gil::rgba8_pixel_t borderColor, boost::gil::rgba8_pixel_t highlightColor, diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index 31956326..72eeac88 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -21,7 +21,7 @@ public: MapSettings(MapSettings&&) noexcept; MapSettings& operator=(MapSettings&&) noexcept; - std::size_t count() const; + [[nodiscard]] std::size_t count() const; SettingsVariable& map_style(std::size_t i); SettingsVariable& radar_site(std::size_t i); SettingsVariable& radar_product_group(std::size_t i); diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index 961b17d5..3c46f29a 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -24,9 +24,10 @@ public: PaletteSettings(PaletteSettings&&) noexcept; PaletteSettings& operator=(PaletteSettings&&) noexcept; - SettingsVariable& palette(const std::string& name) const; - SettingsVariable& alert_color(awips::Phenomenon phenomenon, - bool active) const; + [[nodiscard]] SettingsVariable& + palette(const std::string& name) const; + [[nodiscard]] SettingsVariable& + alert_color(awips::Phenomenon phenomenon, bool active) const; AlertPaletteSettings& alert_palette(awips::Phenomenon); static const std::vector& alert_phenomena(); diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.hpp b/scwx-qt/source/scwx/qt/settings/settings_category.hpp index e5935636..c47bfdeb 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.hpp @@ -23,7 +23,7 @@ public: SettingsCategory(SettingsCategory&&) noexcept; SettingsCategory& operator=(SettingsCategory&&) noexcept; - std::string name() const; + [[nodiscard]] std::string name() const; /** * Gets the signal invoked when a variable within the category is changed. @@ -46,7 +46,7 @@ public: * @return true if all settings variables are currently set to default * values, otherwise false. */ - bool IsDefault() const; + [[nodiscard]] bool IsDefault() const; /** * Gets whether or not all settings variables currently have staged values @@ -55,7 +55,7 @@ public: * @return true if all settings variables currently have staged values set * to default, otherwise false. */ - bool IsDefaultStaged() const; + [[nodiscard]] bool IsDefaultStaged() const; /** * Set all variables to their defaults. diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.hpp b/scwx-qt/source/scwx/qt/settings/settings_container.hpp index 1f57e0b3..0e378a81 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.hpp @@ -88,7 +88,8 @@ public: void SetElementValidator(std::function validator); protected: - virtual bool Equals(const SettingsVariableBase& o) const override; + [[nodiscard]] virtual bool + Equals(const SettingsVariableBase& o) const override; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp index 612f57e7..3fc0b81d 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.hpp @@ -246,7 +246,8 @@ public: void UnregisterValueStagedCallback(boost::uuids::uuid uuid); protected: - virtual bool Equals(const SettingsVariableBase& o) const override; + [[nodiscard]] virtual bool + Equals(const SettingsVariableBase& o) const override; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp index 870a0c9d..d613da91 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.hpp @@ -25,7 +25,7 @@ public: SettingsVariableBase(SettingsVariableBase&&) noexcept; SettingsVariableBase& operator=(SettingsVariableBase&&) noexcept; - std::string name() const; + [[nodiscard]] std::string name() const; /** * Gets the signal invoked when the settings variable is changed. @@ -48,7 +48,7 @@ public: * @return true if the settings variable is currently set to its default * value, otherwise false. */ - virtual bool IsDefault() const = 0; + [[nodiscard]] virtual bool IsDefault() const = 0; /** * Gets whether or not the settings variable currently has its staged value @@ -57,7 +57,7 @@ public: * @return true if the settings variable currently has its staged value set * to default, otherwise false. */ - virtual bool IsDefaultStaged() const = 0; + [[nodiscard]] virtual bool IsDefaultStaged() const = 0; /** * Sets the current value of the settings variable to default. @@ -103,7 +103,7 @@ public: protected: friend bool operator==(const SettingsVariableBase& lhs, const SettingsVariableBase& rhs); - virtual bool Equals(const SettingsVariableBase& o) const; + [[nodiscard]] virtual bool Equals(const SettingsVariableBase& o) const; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.hpp b/scwx-qt/source/scwx/qt/settings/text_settings.hpp index c4cc64af..cbc96f4f 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.hpp @@ -22,17 +22,18 @@ public: TextSettings(TextSettings&&) noexcept; TextSettings& operator=(TextSettings&&) noexcept; - SettingsVariable& + [[nodiscard]] SettingsVariable& font_family(types::FontCategory fontCategory) const; - SettingsVariable& + [[nodiscard]] SettingsVariable& font_style(types::FontCategory fontCategory) const; - SettingsVariable& + [[nodiscard]] SettingsVariable& font_point_size(types::FontCategory fontCategory) const; - SettingsVariable& hover_text_wrap() const; - SettingsVariable& placefile_text_drop_shadow_enabled() const; - SettingsVariable& radar_site_hover_text_enabled() const; - SettingsVariable& tooltip_method() const; + [[nodiscard]] SettingsVariable& hover_text_wrap() const; + [[nodiscard]] SettingsVariable& + placefile_text_drop_shadow_enabled() const; + [[nodiscard]] SettingsVariable& radar_site_hover_text_enabled() const; + [[nodiscard]] SettingsVariable& tooltip_method() const; static TextSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index e052eb90..b5ffd745 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -23,13 +23,13 @@ public: UiSettings(UiSettings&&) noexcept; UiSettings& operator=(UiSettings&&) noexcept; - SettingsVariable& level2_products_expanded() const; - SettingsVariable& level2_settings_expanded() const; - SettingsVariable& level3_products_expanded() const; - SettingsVariable& map_settings_expanded() const; - SettingsVariable& timeline_expanded() const; - SettingsVariable& main_ui_state() const; - SettingsVariable& main_ui_geometry() const; + [[nodiscard]] SettingsVariable& level2_products_expanded() const; + [[nodiscard]] SettingsVariable& level2_settings_expanded() const; + [[nodiscard]] SettingsVariable& level3_products_expanded() const; + [[nodiscard]] SettingsVariable& map_settings_expanded() const; + [[nodiscard]] SettingsVariable& timeline_expanded() const; + [[nodiscard]] SettingsVariable& main_ui_state() const; + [[nodiscard]] SettingsVariable& main_ui_geometry() const; bool Shutdown(); diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp index 403e2b9c..44be2f73 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp @@ -21,11 +21,11 @@ public: UnitSettings(UnitSettings&&) noexcept; UnitSettings& operator=(UnitSettings&&) noexcept; - SettingsVariable& accumulation_units() const; - SettingsVariable& echo_tops_units() const; - SettingsVariable& other_units() const; - SettingsVariable& speed_units() const; - SettingsVariable& distance_units() const; + [[nodiscard]] SettingsVariable& accumulation_units() const; + [[nodiscard]] SettingsVariable& echo_tops_units() const; + [[nodiscard]] SettingsVariable& other_units() const; + [[nodiscard]] SettingsVariable& speed_units() const; + [[nodiscard]] SettingsVariable& distance_units() const; static UnitSettings& Instance(); From f84a86a3a3b41e18fa0dc8365f20abab5367abce Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 10:10:14 -0500 Subject: [PATCH 365/762] Add override to destructors of settings catigories --- scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/audio_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/general_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/line_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/map_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/palette_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/product_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/text_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/ui_settings.hpp | 2 +- scwx-qt/source/scwx/qt/settings/unit_settings.hpp | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp index ec77d060..c4a12e58 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.hpp @@ -15,7 +15,7 @@ class AlertPaletteSettings : public SettingsCategory { public: explicit AlertPaletteSettings(awips::Phenomenon phenomenon); - ~AlertPaletteSettings(); + ~AlertPaletteSettings() override; AlertPaletteSettings(const AlertPaletteSettings&) = delete; AlertPaletteSettings& operator=(const AlertPaletteSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index c6e6befd..50606001 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -14,7 +14,7 @@ class AudioSettings : public SettingsCategory { public: explicit AudioSettings(); - ~AudioSettings(); + ~AudioSettings() override; AudioSettings(const AudioSettings&) = delete; AudioSettings& operator=(const AudioSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 8e890ce3..ad49761b 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -13,7 +13,7 @@ class GeneralSettings : public SettingsCategory { public: explicit GeneralSettings(); - ~GeneralSettings(); + ~GeneralSettings() override; GeneralSettings(const GeneralSettings&) = delete; GeneralSettings& operator=(const GeneralSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp index 02371f59..13e56b7d 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.hpp @@ -14,7 +14,7 @@ class HotkeySettings : public SettingsCategory { public: explicit HotkeySettings(); - ~HotkeySettings(); + ~HotkeySettings() override; HotkeySettings(const HotkeySettings&) = delete; HotkeySettings& operator=(const HotkeySettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.hpp b/scwx-qt/source/scwx/qt/settings/line_settings.hpp index 53d11681..3f13c601 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.hpp @@ -15,7 +15,7 @@ class LineSettings : public SettingsCategory { public: explicit LineSettings(const std::string& name); - ~LineSettings(); + ~LineSettings() override; LineSettings(const LineSettings&) = delete; LineSettings& operator=(const LineSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index 72eeac88..9ffb3540 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -13,7 +13,7 @@ class MapSettings : public SettingsCategory { public: explicit MapSettings(); - ~MapSettings(); + ~MapSettings() override; MapSettings(const MapSettings&) = delete; MapSettings& operator=(const MapSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp index 3c46f29a..c7147a30 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.hpp @@ -16,7 +16,7 @@ class PaletteSettings : public SettingsCategory { public: explicit PaletteSettings(); - ~PaletteSettings(); + ~PaletteSettings() override; PaletteSettings(const PaletteSettings&) = delete; PaletteSettings& operator=(const PaletteSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index 2ebf5ca9..fd28bca5 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -13,7 +13,7 @@ class ProductSettings : public SettingsCategory { public: explicit ProductSettings(); - ~ProductSettings(); + ~ProductSettings() override; ProductSettings(const ProductSettings&) = delete; ProductSettings& operator=(const ProductSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.hpp b/scwx-qt/source/scwx/qt/settings/text_settings.hpp index cbc96f4f..593702fe 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.hpp @@ -14,7 +14,7 @@ class TextSettings : public SettingsCategory { public: explicit TextSettings(); - ~TextSettings(); + ~TextSettings() override; TextSettings(const TextSettings&) = delete; TextSettings& operator=(const TextSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp index b5ffd745..d8970c73 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.hpp @@ -15,7 +15,7 @@ class UiSettings : public SettingsCategory { public: explicit UiSettings(); - ~UiSettings(); + ~UiSettings() override; UiSettings(const UiSettings&) = delete; UiSettings& operator=(const UiSettings&) = delete; diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp index 44be2f73..a7d06eed 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp @@ -13,7 +13,7 @@ class UnitSettings : public SettingsCategory { public: explicit UnitSettings(); - ~UnitSettings(); + ~UnitSettings() override; UnitSettings(const UnitSettings&) = delete; UnitSettings& operator=(const UnitSettings&) = delete; From 05c05fec5c301b1af091dc205a1fb135ae60a550 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 10:18:26 -0500 Subject: [PATCH 366/762] Use default destructor, and add move/copy operators to settings impls --- .../source/scwx/qt/settings/alert_palette_settings.cpp | 7 ++++++- scwx-qt/source/scwx/qt/settings/audio_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/line_settings.cpp | 7 ++++++- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/palette_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/product_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/settings_category.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/settings_container.cpp | 8 ++++++-- scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 6 +++++- .../source/scwx/qt/settings/settings_interface_base.cpp | 8 ++++++-- scwx-qt/source/scwx/qt/settings/settings_variable.cpp | 8 ++++++-- .../source/scwx/qt/settings/settings_variable_base.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/text_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/ui_settings.cpp | 6 +++++- scwx-qt/source/scwx/qt/settings/unit_settings.cpp | 6 +++++- 17 files changed, 90 insertions(+), 20 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp index b15b184e..1c7aca31 100644 --- a/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/alert_palette_settings.cpp @@ -135,7 +135,12 @@ public: SetDefaultLineData(inactive_, kInactivePalettes_.at(phenomenon)); } - ~Impl() {} + + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; static void SetDefaultLineData(LineSettings& lineSettings, const LineData& lineData); diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index f954cf72..c4b07ff8 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -92,7 +92,11 @@ public: SettingsVariable {"alert_disabled"}); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; SettingsVariable alertSoundFile_ {"alert_sound_file"}; SettingsVariable alertLocationMethod_ {"alert_location_method"}; diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 9df9f227..c83118c2 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -140,7 +140,11 @@ public: { return QUrl {QString::fromStdString(value)}.isValid(); }); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; SettingsVariable antiAliasingEnabled_ {"anti_aliasing_enabled"}; SettingsVariable clockFormat_ {"clock_format"}; diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp index d9261970..ba5b4e3e 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp @@ -90,7 +90,11 @@ public: SettingsVariable {"?"}); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::unordered_map> hotkey_ {}; std::vector variables_ {}; diff --git a/scwx-qt/source/scwx/qt/settings/line_settings.cpp b/scwx-qt/source/scwx/qt/settings/line_settings.cpp index 9910ce6b..2b013105 100644 --- a/scwx-qt/source/scwx/qt/settings/line_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/line_settings.cpp @@ -46,7 +46,12 @@ public: highlightColor_.SetValidator(&util::color::ValidateArgbString); borderColor_.SetValidator(&util::color::ValidateArgbString); } - ~Impl() {} + + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; SettingsVariable lineColor_ {"line_color"}; SettingsVariable highlightColor_ {"highlight_color"}; diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 6cd91815..416c0a6d 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -100,7 +100,11 @@ public: } } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void SetDefaults(std::size_t i) { diff --git a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp index 6046f79b..0b88b687 100644 --- a/scwx-qt/source/scwx/qt/settings/palette_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/palette_settings.cpp @@ -79,7 +79,11 @@ public: InitializeAlerts(); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void InitializeColorTables(); void InitializeLegacyAlerts(); diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index 9bcc3a1a..7cb77903 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -19,7 +19,11 @@ public: // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; SettingsVariable showSmoothedRangeFolding_ { "show_smoothed_range_folding"}; diff --git a/scwx-qt/source/scwx/qt/settings/settings_category.cpp b/scwx-qt/source/scwx/qt/settings/settings_category.cpp index 184d530d..b0e1b416 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_category.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_category.cpp @@ -15,7 +15,11 @@ class SettingsCategory::Impl public: explicit Impl(const std::string& name) : name_ {name} {} - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void ConnectSubcategory(SettingsCategory& category); void ConnectVariable(SettingsVariableBase* variable); diff --git a/scwx-qt/source/scwx/qt/settings/settings_container.cpp b/scwx-qt/source/scwx/qt/settings/settings_container.cpp index ab6ac1e5..df0d7e41 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_container.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_container.cpp @@ -11,9 +11,13 @@ template class SettingsContainer::Impl { public: - explicit Impl() {} + explicit Impl() = default; - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; T elementDefault_ {}; std::optional elementMinimum_ {}; diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 9aa58ac3..89e876a7 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -27,7 +27,11 @@ public: context_->moveToThread(QCoreApplication::instance()->thread()); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; template void SetWidgetText(U* widget, const T& currentValue); diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp index 9bbf9402..e7060573 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface_base.cpp @@ -11,8 +11,12 @@ static const std::string logPrefix_ = class SettingsInterfaceBase::Impl { public: - explicit Impl() {} - ~Impl() {} + explicit Impl() = default; + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; }; SettingsInterfaceBase::SettingsInterfaceBase() : p(std::make_unique()) {} diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp index 1e6ae3e4..9682cbe4 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable.cpp @@ -17,8 +17,12 @@ template class SettingsVariable::Impl { public: - explicit Impl() {} - ~Impl() {} + explicit Impl() = default; + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; T value_ {}; T default_ {}; diff --git a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp index 6d4e54f6..f2941b9a 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_variable_base.cpp @@ -11,7 +11,11 @@ class SettingsVariableBase::Impl public: explicit Impl(const std::string& name) : name_ {name} {} - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; const std::string name_; diff --git a/scwx-qt/source/scwx/qt/settings/text_settings.cpp b/scwx-qt/source/scwx/qt/settings/text_settings.cpp index 63fcb2cd..b66268e0 100644 --- a/scwx-qt/source/scwx/qt/settings/text_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/text_settings.cpp @@ -80,7 +80,11 @@ public: InitializeFontVariables(); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void InitializeFontVariables(); diff --git a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp index 5a3175e9..e4689120 100644 --- a/scwx-qt/source/scwx/qt/settings/ui_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/ui_settings.cpp @@ -22,7 +22,11 @@ public: // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) } - ~UiSettingsImpl() {} + ~UiSettingsImpl() = default; + UiSettingsImpl(const UiSettingsImpl&) = delete; + UiSettingsImpl& operator=(const UiSettingsImpl&) = delete; + UiSettingsImpl(const UiSettingsImpl&&) = delete; + UiSettingsImpl& operator=(const UiSettingsImpl&&) = delete; SettingsVariable level2ProductsExpanded_ {"level2_products_expanded"}; SettingsVariable level2SettingsExpanded_ {"level2_settings_expanded"}; diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp index 37ec8a0c..542ae153 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp @@ -62,7 +62,11 @@ public: types::GetDistanceUnitsName)); } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; SettingsVariable accumulationUnits_ {"accumulation_units"}; SettingsVariable echoTopsUnits_ {"echo_tops_units"}; From 64d24591f3ac9d1148800111b88301ceea356baf Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 10:22:45 -0500 Subject: [PATCH 367/762] Move includes only used in cpp files from hpp files in settings --- scwx-qt/source/scwx/qt/settings/product_settings.cpp | 2 ++ scwx-qt/source/scwx/qt/settings/product_settings.hpp | 1 - scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 1 + scwx-qt/source/scwx/qt/settings/settings_interface.hpp | 1 - 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index 7cb77903..1221d717 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -1,6 +1,8 @@ #include #include +#include + namespace scwx::qt::settings { diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index fd28bca5..d267b127 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -4,7 +4,6 @@ #include #include -#include namespace scwx::qt::settings { diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 89e876a7..ab36ebd9 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace scwx::qt::settings { diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 0561cf12..3d7d9d85 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -5,7 +5,6 @@ #include #include #include -#include class QLabel; From f3c846f0b19ff8f50da3dc967bc3820d91c732ed Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 10:30:08 -0500 Subject: [PATCH 368/762] remove unneeded include form general_settings.cpp --- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index c83118c2..c6e93c76 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -8,8 +8,6 @@ #include #include -#include - #include #include From bc79ed11a3c60aebac4eac6ab52a0050ab4fe3e8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 29 Jan 2025 11:04:17 -0500 Subject: [PATCH 369/762] Add checks to prevent files being saved before being fully read. --- scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 7 +++++++ scwx-qt/source/scwx/qt/manager/placefile_manager.cpp | 7 +++++++ scwx-qt/source/scwx/qt/manager/settings_manager.cpp | 3 ++- scwx-qt/source/scwx/qt/model/layer_model.cpp | 8 ++++++++ scwx-qt/source/scwx/qt/model/radar_site_model.cpp | 7 +++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index fd7bee13..952dea44 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -60,6 +60,8 @@ public: void WriteMarkerSettings(); std::shared_ptr GetMarkerByName(const std::string& name); + bool markerFileRead_ {false}; + void InitalizeIds(); types::MarkerId NewId(); types::MarkerId lastId_ {0}; @@ -209,11 +211,16 @@ void MarkerManager::Impl::ReadMarkerSettings() } } + markerFileRead_ = true; Q_EMIT self_->MarkersUpdated(); } void MarkerManager::Impl::WriteMarkerSettings() { + if (!markerFileRead_) + { + return; + } logger_->info("Saving location marker settings"); const std::shared_lock lock(markerRecordLock_); diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index df412e24..a6158773 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -70,6 +70,8 @@ public: boost::unordered_flat_map> placefileRecordMap_ {}; std::shared_mutex placefileRecordLock_ {}; + + bool placefileSettingsRead_ {false}; }; class PlacefileManager::Impl::PlacefileRecord @@ -413,10 +415,15 @@ void PlacefileManager::Impl::ReadPlacefileSettings() } } } + placefileSettingsRead_ = true; } void PlacefileManager::Impl::WritePlacefileSettings() { + if (!placefileSettingsRead_) + { + return; + } logger_->info("Saving placefile settings"); std::shared_lock lock {placefileRecordLock_}; diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index 056799ae..5b2e9cbb 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -67,9 +67,10 @@ void SettingsManager::Initialize() } p->settingsPath_ = appDataPath + "/settings.json"; - p->initialized_ = true; ReadSettings(p->settingsPath_); + + p->initialized_ = true; p->ValidateSettings(); } diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 014acb42..2f1b8a9d 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -96,6 +96,8 @@ public: manager::PlacefileManager::Instance()}; types::LayerVector layers_ {}; + + bool fileRead_ {false}; }; LayerModel::LayerModel(QObject* parent) : @@ -201,6 +203,8 @@ void LayerModel::Impl::ReadLayerSettings() // Assign read layers layers_.swap(newLayers); } + + fileRead_ = true; } void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) @@ -314,6 +318,10 @@ void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) void LayerModel::Impl::WriteLayerSettings() { + if (!fileRead_) + { + return; + } logger_->info("Saving layer settings"); auto layerJson = boost::json::value_from(layers_); 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 482c9828..e1a593d7 100644 --- a/scwx-qt/source/scwx/qt/model/radar_site_model.cpp +++ b/scwx-qt/source/scwx/qt/model/radar_site_model.cpp @@ -68,6 +68,8 @@ public: scwx::common::Coordinate previousPosition_; QIcon starIcon_ {":/res/icons/font-awesome-6/star-solid.svg"}; + + bool presetsRead_ {false}; }; RadarSiteModel::RadarSiteModel(QObject* parent) : @@ -146,10 +148,15 @@ void RadarSiteModelImpl::ReadPresets() } } } + presetsRead_ = true; } void RadarSiteModelImpl::WritePresets() { + if (!presetsRead_) + { + return; + } logger_->info("Saving presets"); auto presetsJson = boost::json::value_from(presets_); From 36e8690ad1b46dd12862f112a9cb5dbf2dc35f01 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 3 Feb 2025 10:47:09 -0500 Subject: [PATCH 370/762] Use information available during DoMousePicking instead of params from last render in MapWidget --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 1 - scwx-qt/source/scwx/qt/map/map_context.cpp | 12 ------------ scwx-qt/source/scwx/qt/map/map_context.hpp | 3 --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 11 +++++++++-- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 -- scwx-qt/source/scwx/qt/map/radar_site_layer.cpp | 2 -- 6 files changed, 9 insertions(+), 22 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index e6716c98..7d03d13a 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -48,7 +48,6 @@ void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = p->context_->gl(); p->textureAtlas_ = p->context_->GetTextureAtlas(); - p->context_->set_render_parameters(params); // Determine if the texture atlas changed since last render std::uint64_t newTextureAtlasBuildCount = diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index c659c432..38a41e1f 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -27,7 +27,6 @@ public: common::RadarProductGroup::Unknown}; std::string radarProduct_ {"???"}; int16_t radarProductCode_ {0}; - QMapLibre::CustomLayerRenderParameters renderParameters_ {}; MapProvider mapProvider_ {MapProvider::Unknown}; std::string mapCopyrights_ {}; @@ -110,11 +109,6 @@ int16_t MapContext::radar_product_code() const return p->radarProductCode_; } -QMapLibre::CustomLayerRenderParameters MapContext::render_parameters() const -{ - return p->renderParameters_; -} - void MapContext::set_map(const std::shared_ptr& map) { p->map_ = map; @@ -173,12 +167,6 @@ void MapContext::set_radar_product_code(int16_t radarProductCode) p->radarProductCode_ = radarProductCode; } -void MapContext::set_render_parameters( - const QMapLibre::CustomLayerRenderParameters& params) -{ - p->renderParameters_ = params; -} - } // namespace map } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 86f49b8f..59fb8a5b 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -50,7 +50,6 @@ public: common::RadarProductGroup radar_product_group() const; std::string radar_product() const; int16_t radar_product_code() const; - QMapLibre::CustomLayerRenderParameters render_parameters() const; void set_map(const std::shared_ptr& map); void set_map_copyrights(const std::string& copyrights); @@ -65,8 +64,6 @@ public: void set_radar_product_group(common::RadarProductGroup radarProductGroup); void set_radar_product(const std::string& radarProduct); void set_radar_product_code(int16_t radarProductCode); - void - set_render_parameters(const QMapLibre::CustomLayerRenderParameters& params); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index cc501408..b7e9abee 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1649,8 +1649,15 @@ void MapWidgetImpl::ImGuiCheckFonts() void MapWidgetImpl::RunMousePicking() { - const QMapLibre::CustomLayerRenderParameters params = - context_->render_parameters(); + const QMapLibre::CustomLayerRenderParameters params = { + .width = static_cast(widget_->size().width()), + .height = static_cast(widget_->size().height()), + .latitude = map_->coordinate().first, + .longitude = map_->coordinate().second, + .zoom = map_->zoom(), + .bearing = map_->bearing(), + .pitch = map_->pitch(), + .fieldOfView = 0}; auto coordinate = map_->coordinateForPixel(lastPos_); auto mouseScreenCoordinate = diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index b9393ee0..da82ef7f 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -292,8 +292,6 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) auto& settings = context()->settings(); const float pixelRatio = context()->pixel_ratio(); - context()->set_render_parameters(params); - p->sweepTimePicked_ = false; if (radarProductView != nullptr) diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 8b78fa4c..d2fd7547 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -74,8 +74,6 @@ void RadarSiteLayer::Render( gl::OpenGLFunctions& gl = context()->gl(); - context()->set_render_parameters(params); - // Update map screen coordinate and scale information p->mapScreenCoordLocation_ = util::maplibre::LatLongToScreenCoordinate( {params.latitude, params.longitude}); From e8e7cd6dea26844b70d48116f15923a5f3ef258f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 5 Feb 2025 22:18:53 -0600 Subject: [PATCH 371/762] Update date library to ca57278 (2025-01-14) Fixes date parsing with certain versions of tzdata --- external/date | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/date b/external/date index cc4685a2..ca572785 160000 --- a/external/date +++ b/external/date @@ -1 +1 @@ -Subproject commit cc4685a21e4a4fdae707ad1233c61bbaff241f93 +Subproject commit ca5727855bd1bae12b2c6ca36cd88649d43ec862 From e43c0594d1469db254e044e145b21d42a7cd3186 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 9 Feb 2025 13:07:42 -0500 Subject: [PATCH 372/762] Change IsPointInPolygon to check polygon with equal points correctly --- scwx-qt/source/scwx/qt/util/maplibre.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 3414af41..5ff8b4ef 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -47,6 +47,7 @@ bool IsPointInPolygon(const std::vector& vertices, const glm::vec2& point) { bool inPolygon = true; + bool allSame = true; // For each vertex, assume counterclockwise order for (std::size_t i = 0; i < vertices.size(); ++i) @@ -55,6 +56,8 @@ bool IsPointInPolygon(const std::vector& vertices, const auto& p2 = (i == vertices.size() - 1) ? vertices[0] : vertices[i + 1]; + allSame = allSame && p1.x == p2.x && p1.y == p2.y; + // Test which side of edge point lies on const float a = -(p2.y - p1.y); const float b = p2.x - p1.x; @@ -70,6 +73,12 @@ bool IsPointInPolygon(const std::vector& vertices, } } + if (allSame) + { + inPolygon = vertices.size() > 0 && vertices[0].x == point.x && + vertices[0].y == point.y; + } + return inPolygon; } From fbed3fc8abab28759fbd89e0ef113f427748f94a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 9 Feb 2025 13:13:15 -0500 Subject: [PATCH 373/762] Clang tidy/format fixes for fix_tooltips_at_high_zooms --- scwx-qt/source/scwx/qt/util/maplibre.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 5ff8b4ef..37387f99 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -47,7 +47,7 @@ bool IsPointInPolygon(const std::vector& vertices, const glm::vec2& point) { bool inPolygon = true; - bool allSame = true; + bool allSame = true; // For each vertex, assume counterclockwise order for (std::size_t i = 0; i < vertices.size(); ++i) From 44b1cdd801884cab80184f82ee7e1b17d9e5fe8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:54:12 +0000 Subject: [PATCH 374/762] Update dependency openssl to v3.4.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 0814bc3a..044b678a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -17,7 +17,7 @@ class SupercellWxConan(ConanFile): "libcurl/8.10.1", "libpng/1.6.45", "libxml2/2.13.4", - "openssl/3.3.2", + "openssl/3.4.1", "re2/20240702", "spdlog/1.15.0", "sqlite3/3.47.2", From 75c4741d9a07b7648f45c524c03d9fd96c3b23ef Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 13 Feb 2025 22:42:02 -0600 Subject: [PATCH 375/762] Take a lock on the refresh timer mutex before checking whether refresh is enabled --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index b51d5c70..ae9d0313 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -794,10 +794,10 @@ void RadarProductManagerImpl::RefreshDataSync( interval = kSlowRetryInterval_; } + std::unique_lock lock(providerManager->refreshTimerMutex_); + if (providerManager->refreshEnabled_) { - std::unique_lock lock(providerManager->refreshTimerMutex_); - logger_->debug( "[{}] Scheduled refresh in {:%M:%S}", providerManager->name(), From 7b70d91093fa1621e226fe0ae6fb22e11444b1b4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 13 Feb 2025 23:19:34 -0600 Subject: [PATCH 376/762] Update scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index ae9d0313..9768c9da 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -794,7 +794,7 @@ void RadarProductManagerImpl::RefreshDataSync( interval = kSlowRetryInterval_; } - std::unique_lock lock(providerManager->refreshTimerMutex_); + std::unique_lock const lock(providerManager->refreshTimerMutex_); if (providerManager->refreshEnabled_) { From 84ce6589a314decbb66bc4176c2dc7aaa8da0078 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Feb 2025 08:32:34 -0500 Subject: [PATCH 377/762] clang-tidy fixes for fix_tooltips_at_high_zooms --- scwx-qt/source/scwx/qt/util/maplibre.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 37387f99..63f5112e 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -46,6 +46,8 @@ glm::vec2 GetMapScale(const QMapLibre::CustomLayerRenderParameters& params) bool IsPointInPolygon(const std::vector& vertices, const glm::vec2& point) { + // All members of these unions are floats, so no type safety violation + // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) bool inPolygon = true; bool allSame = true; @@ -80,6 +82,7 @@ bool IsPointInPolygon(const std::vector& vertices, } return inPolygon; + // NOLINTEND(cppcoreguidelines-pro-type-union-access) } glm::vec2 LatLongToScreenCoordinate(const QMapLibre::Coordinate& coordinate) From e189abea4d9af2e19a4df2c0708b8d79b79d8d49 Mon Sep 17 00:00:00 2001 From: Aaron Dunlap Date: Mon, 17 Feb 2025 14:00:18 -0600 Subject: [PATCH 378/762] fix: rfind to find for more consistent site parsing --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index b3cbb427..5491bb21 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -639,7 +639,7 @@ void SettingsDialogImpl::SetupGeneralTab() [](const std::string& text) -> std::string { // Find the position of location details - size_t pos = text.rfind(" ("); + size_t pos = text.find(" ("); if (pos == std::string::npos) { @@ -1060,7 +1060,7 @@ void SettingsDialogImpl::SetupAudioTab() [](const std::string& text) -> std::string { // Find the position of location details - size_t pos = text.rfind(" ("); + size_t pos = text.find(" ("); if (pos == std::string::npos) { From e0c2f537a3deefa6cf7bb205d88e959038e8024b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 04:41:05 +0000 Subject: [PATCH 379/762] scwx-qt_update_radar_sites does not need to be run for clang-tidy --- .github/workflows/clang-tidy-review.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 97beace8..dcc99a46 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -104,7 +104,6 @@ jobs: -DCMAKE_EXPORT_COMPILE_COMMANDS=on ninja scwx-qt_generate_counties_db ` scwx-qt_generate_versions ` - scwx-qt_update_radar_sites ` scwx-qt_autogen - name: Code Review From 506d05b3cabb9910c5f0ba1170686e84d09cc7e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 05:38:31 +0000 Subject: [PATCH 380/762] chore(deps): update dependency libpng to v1.6.46 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 142dd83a..5dde351a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "glm/1.0.1", "gtest/1.15.0", "libcurl/8.10.1", - "libpng/1.6.45", + "libpng/1.6.46", "libxml2/2.13.4", "openssl/3.4.1", "re2/20240702", From 7cb2ca3e05356a3830415d285575f17a15b4a700 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:50:10 +0000 Subject: [PATCH 381/762] chore(deps): update dependency libpng to v1.6.47 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 5dde351a..969d6cfd 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "glm/1.0.1", "gtest/1.15.0", "libcurl/8.10.1", - "libpng/1.6.46", + "libpng/1.6.47", "libxml2/2.13.4", "openssl/3.4.1", "re2/20240702", From 2bdde92d1b29482bb018f2e69d173b437dc483f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:00:30 +0000 Subject: [PATCH 382/762] chore(deps): update dependency spdlog to v1.15.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 5dde351a..fc1462d1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -19,7 +19,7 @@ class SupercellWxConan(ConanFile): "libxml2/2.13.4", "openssl/3.4.1", "re2/20240702", - "spdlog/1.15.0", + "spdlog/1.15.1", "sqlite3/3.48.0", "vulkan-loader/1.3.290.0", "zlib/1.3.1") From b4c133ac77d39148fd0c53fc4048dbe4266fa828 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:03:06 +0000 Subject: [PATCH 383/762] chore(deps): update dependency onetbb to v2022 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index e98e6dcf..b515c887 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,7 +35,7 @@ class SupercellWxConan(ConanFile): def requirements(self): if self.settings.os == "Linux": - self.requires("onetbb/2021.12.0") + self.requires("onetbb/2022.0.0") def generate(self): build_folder = os.path.join(self.build_folder, From 17b1413c05d18e4e98c77ff755c1e4aac01ba985 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:03:41 +0000 Subject: [PATCH 384/762] chore(deps): update dependency libxml2 to v2.13.6 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index e98e6dcf..5e137266 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ class SupercellWxConan(ConanFile): "gtest/1.15.0", "libcurl/8.10.1", "libpng/1.6.47", - "libxml2/2.13.4", + "libxml2/2.13.6", "openssl/3.4.1", "re2/20240702", "spdlog/1.15.1", From 68f05ece8548261274f3bcf8e8286aa3e022805b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:04:18 +0000 Subject: [PATCH 385/762] chore(deps): update dependency boost to v1.87.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 5e137266..95c74262 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,7 +5,7 @@ import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") - requires = ("boost/1.86.0", + requires = ("boost/1.87.0", "cpr/1.11.1", "fontconfig/2.15.0", "freetype/2.13.2", From adf3e7fed648371df555b7e38d49b41e7c46e770 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:52:03 +0000 Subject: [PATCH 386/762] chore(deps): update dependency sqlite3 to v3.49.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index fd1c875b..faa5b2f0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -20,7 +20,7 @@ class SupercellWxConan(ConanFile): "openssl/3.4.1", "re2/20240702", "spdlog/1.15.1", - "sqlite3/3.48.0", + "sqlite3/3.49.1", "vulkan-loader/1.3.290.0", "zlib/1.3.1") generators = ("CMakeDeps") From 8be9cc72de5388ce1fc66b0ec8126b6ce577fa30 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 28 Feb 2025 12:20:12 -0500 Subject: [PATCH 387/762] Fall back to default color palette on failure to load color palette. --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 42 +++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index b7e9abee..d2d480de 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1866,26 +1866,48 @@ void MapWidgetImpl::InitializeNewRadarProductView( const std::string& colorPalette) { boost::asio::post(threadPool_, - [=, this]() + [colorPalette, this]() { try { auto radarProductView = context_->radar_product_view(); + auto& paletteSetting = + settings::PaletteSettings::Instance().palette( + colorPalette); + std::string colorTableFile = - settings::PaletteSettings::Instance() - .palette(colorPalette) - .GetValue(); - if (!colorTableFile.empty()) + paletteSetting.GetValue(); + if (colorTableFile.empty()) { - std::unique_ptr colorTableStream = - util::OpenFile(colorTableFile); - std::shared_ptr colorTable = - common::ColorTable::Load(*colorTableStream); - radarProductView->LoadColorTable(colorTable); + colorTableFile = paletteSetting.GetDefault(); } + std::unique_ptr colorTableStream = + util::OpenFile(colorTableFile); + if (colorTableStream->fail()) + { + logger_->warn("Could not open color table {}", + colorTableFile); + colorTableStream = + util::OpenFile(paletteSetting.GetDefault()); + } + + std::shared_ptr colorTable = + common::ColorTable::Load(*colorTableStream); + if (!colorTable->IsValid()) + { + logger_->warn("Could not load color table {}", + colorTableFile); + colorTableStream = + util::OpenFile(paletteSetting.GetDefault()); + colorTable = + common::ColorTable::Load(*colorTableStream); + } + + radarProductView->LoadColorTable(colorTable); + radarProductView->Initialize(); } catch (const std::exception& ex) From dc3258d149e3afb14e435c8222e569e714b818fe Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 28 Feb 2025 15:49:56 -0500 Subject: [PATCH 388/762] Clang tidy/format fixes for color_palette_fallback_warning --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 81 +++++++++++------------ 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d2d480de..9096a4b7 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1865,56 +1865,49 @@ void MapWidgetImpl::RadarProductManagerDisconnect() void MapWidgetImpl::InitializeNewRadarProductView( const std::string& colorPalette) { - boost::asio::post(threadPool_, - [colorPalette, this]() - { - try - { - auto radarProductView = - context_->radar_product_view(); + boost::asio::post( + threadPool_, + [colorPalette, this]() + { + try + { + auto radarProductView = context_->radar_product_view(); - auto& paletteSetting = - settings::PaletteSettings::Instance().palette( - colorPalette); + auto& paletteSetting = + settings::PaletteSettings::Instance().palette(colorPalette); - std::string colorTableFile = - paletteSetting.GetValue(); - if (colorTableFile.empty()) - { - colorTableFile = paletteSetting.GetDefault(); - } + std::string colorTableFile = paletteSetting.GetValue(); + if (colorTableFile.empty()) + { + colorTableFile = paletteSetting.GetDefault(); + } - std::unique_ptr colorTableStream = - util::OpenFile(colorTableFile); - if (colorTableStream->fail()) - { - logger_->warn("Could not open color table {}", - colorTableFile); - colorTableStream = - util::OpenFile(paletteSetting.GetDefault()); - } + std::unique_ptr colorTableStream = + util::OpenFile(colorTableFile); + if (colorTableStream->fail()) + { + logger_->warn("Could not open color table {}", colorTableFile); + colorTableStream = util::OpenFile(paletteSetting.GetDefault()); + } - std::shared_ptr colorTable = - common::ColorTable::Load(*colorTableStream); - if (!colorTable->IsValid()) - { - logger_->warn("Could not load color table {}", - colorTableFile); - colorTableStream = - util::OpenFile(paletteSetting.GetDefault()); - colorTable = - common::ColorTable::Load(*colorTableStream); - } + std::shared_ptr colorTable = + common::ColorTable::Load(*colorTableStream); + if (!colorTable->IsValid()) + { + logger_->warn("Could not load color table {}", colorTableFile); + colorTableStream = util::OpenFile(paletteSetting.GetDefault()); + colorTable = common::ColorTable::Load(*colorTableStream); + } - radarProductView->LoadColorTable(colorTable); + radarProductView->LoadColorTable(colorTable); - radarProductView->Initialize(); - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } - }); + radarProductView->Initialize(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); if (map_ != nullptr) { From d633f7746c544a4664cdad45d4cf5276ae89ab9c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 1 Mar 2025 10:58:46 -0500 Subject: [PATCH 389/762] Fix negative radar elevations being reported as large possitive elevations --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 47 +++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 1069fbcd..ed976c24 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -66,7 +66,7 @@ public: std::map> radarData_ {}; std::map>>> index_ {}; @@ -139,52 +139,41 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, { logger_->debug("GetElevationScan: {} degrees", elevation); - constexpr float scaleFactor = 8.0f / 0.043945f; - std::shared_ptr elevationScan = nullptr; float elevationCut = 0.0f; std::vector elevationCuts; - std::uint16_t codedElevation = - static_cast(std::lroundf(elevation * scaleFactor)); - if (p->index_.contains(dataBlockType)) { auto& scans = p->index_.at(dataBlockType); - std::uint16_t lowerBound = scans.cbegin()->first; - std::uint16_t upperBound = scans.crbegin()->first; + float lowerBound = scans.cbegin()->first; + float upperBound = scans.crbegin()->first; // Find closest elevation match for (auto& scan : scans) { - if (scan.first > lowerBound && scan.first <= codedElevation) + if (scan.first > lowerBound && scan.first <= elevation) { lowerBound = scan.first; } - if (scan.first < upperBound && scan.first >= codedElevation) + if (scan.first < upperBound && scan.first >= elevation) { upperBound = scan.first; } - elevationCuts.push_back(scan.first / scaleFactor); + elevationCuts.push_back(scan.first); } - std::int32_t lowerDelta = - std::abs(static_cast(codedElevation) - - static_cast(lowerBound)); - std::int32_t upperDelta = - std::abs(static_cast(codedElevation) - - static_cast(upperBound)); + const float lowerDelta = std::abs(elevation - lowerBound); + const float upperDelta = std::abs(elevation - upperBound); // Select closest elevation match - std::uint16_t elevationIndex = - (lowerDelta < upperDelta) ? lowerBound : upperBound; - elevationCut = elevationIndex / scaleFactor; + elevationCut = (lowerDelta < upperDelta) ? lowerBound : upperBound; // Select closest time match, not newer than the selected time std::chrono::system_clock::time_point foundTime {}; - auto& elevationScans = scans.at(elevationIndex); + auto& elevationScans = scans.at(elevationCut); for (auto& scan : elevationScans) { @@ -457,6 +446,8 @@ void Ar2vFileImpl::IndexFile() { logger_->debug("Indexing file"); + constexpr float scaleFactor = 8.0f / 0.043945f; + for (auto& elevationCut : radarData_) { std::uint16_t elevationAngle {}; @@ -510,7 +501,19 @@ void Ar2vFileImpl::IndexFile() auto time = util::TimePoint(radial0->modified_julian_date(), radial0->collection_time()); - index_[dataBlockType][elevationAngle][time] = elevationCut.second; + // NOLINTNEXTLINE This conversion is accurate + float elevationAngleConverted = elevationAngle / scaleFactor; + // Any elevation above 90 degrees should be interpreted as a + // negative angle + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (elevationAngleConverted > 90) + { + elevationAngleConverted -= 360; + } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + index_[dataBlockType][elevationAngleConverted][time] = + elevationCut.second; } } } From 7e02add8de5db42b04e5d3508d6d414d436a1cf6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Mar 2025 23:42:25 -0600 Subject: [PATCH 390/762] Updating zone and county files for 18 March 2025 release --- data | 2 +- scwx-qt/scwx-qt.cmake | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/data b/data index 8eb89b19..fd72b32c 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 8eb89b19fdd1c78e896cc6cb47e07425bb473699 +Subproject commit fd72b32cc12419b4a9c9a72487e58ffa04fb2a70 diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 09ea6fe3..77024a0d 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -420,13 +420,13 @@ set(JSON_FILES res/config/radar_sites.json) set(TS_FILES ts/scwx_en_US.ts) set(RADAR_SITES_FILE ${scwx-qt_SOURCE_DIR}/res/config/radar_sites.json) -set(COUNTY_DBF_FILES ${SCWX_DIR}/data/db/c_05mr24.dbf) -set(ZONE_DBF_FILES ${SCWX_DIR}/data/db/fz05mr24.dbf - ${SCWX_DIR}/data/db/mz05mr24.dbf - ${SCWX_DIR}/data/db/oz05mr24.dbf - ${SCWX_DIR}/data/db/z_05mr24.dbf) -set(STATE_DBF_FILES ${SCWX_DIR}/data/db/s_05mr24.dbf) -set(WFO_DBF_FILES ${SCWX_DIR}/data/db/w_05mr24.dbf) +set(COUNTY_DBF_FILES ${SCWX_DIR}/data/db/c_18mr25.dbf) +set(ZONE_DBF_FILES ${SCWX_DIR}/data/db/fz18mr25.dbf + ${SCWX_DIR}/data/db/mz18mr25.dbf + ${SCWX_DIR}/data/db/oz18mr25.dbf + ${SCWX_DIR}/data/db/z_18mr25.dbf) +set(STATE_DBF_FILES ${SCWX_DIR}/data/db/s_18mr25.dbf) +set(WFO_DBF_FILES ${SCWX_DIR}/data/db/w_18mr25.dbf) set(COUNTIES_SQLITE_DB ${scwx-qt_BINARY_DIR}/res/db/counties.db) set(RESOURCE_INPUT ${scwx-qt_SOURCE_DIR}/res/scwx-qt.rc.in) From 61d52fc45f793aa431532a7d0c31e6fd6c64df84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:35:33 +0000 Subject: [PATCH 391/762] chore(deps): update dependency libcurl to v8.12.1 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index faa5b2f0..621198ff 100644 --- a/conanfile.py +++ b/conanfile.py @@ -14,7 +14,7 @@ class SupercellWxConan(ConanFile): "glew/2.2.0", "glm/1.0.1", "gtest/1.15.0", - "libcurl/8.10.1", + "libcurl/8.12.1", "libpng/1.6.47", "libxml2/2.13.6", "openssl/3.4.1", From e8e25e08cc16eccfeeb8aa095a6e985cfee01ce1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:14:01 +0000 Subject: [PATCH 392/762] chore(deps): update dependency gtest to v1.16.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 621198ff..cbfc9896 100644 --- a/conanfile.py +++ b/conanfile.py @@ -13,7 +13,7 @@ class SupercellWxConan(ConanFile): "geos/3.13.0", "glew/2.2.0", "glm/1.0.1", - "gtest/1.15.0", + "gtest/1.16.0", "libcurl/8.12.1", "libpng/1.6.47", "libxml2/2.13.6", From a8568d24f6025e573636324bf70430c2e673a2fd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 12 Mar 2025 13:00:56 -0400 Subject: [PATCH 393/762] Fix AlertLayer circular reference --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 5ed4e458..d7e8b029 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -157,8 +157,8 @@ public: const std::shared_ptr& segmentRecord); void ConnectAlertHandlerSignals(); void ConnectSignals(); - void HandleGeoLinesEvent(std::shared_ptr& di, - QEvent* ev); + void HandleGeoLinesEvent(std::weak_ptr& di, + QEvent* ev); void HandleGeoLinesHover(std::shared_ptr& di, const QPointF& mouseGlobalPos); void ScheduleRefresh(); @@ -633,11 +633,12 @@ void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, std::placeholders::_1, std::placeholders::_2)); + std::weak_ptr diWeak = di; gl::draw::GeoLines::RegisterEventHandler( di, std::bind(&AlertLayer::Impl::HandleGeoLinesEvent, this, - di, + diWeak, std::placeholders::_1)); } } @@ -691,8 +692,14 @@ void AlertLayer::Impl::UpdateLines() } void AlertLayer::Impl::HandleGeoLinesEvent( - std::shared_ptr& di, QEvent* ev) + std::weak_ptr& diWeak, QEvent* ev) { + std::shared_ptr di = diWeak.lock(); + if (di == nullptr) + { + return; + } + switch (ev->type()) { case QEvent::Type::MouseButtonPress: From f82ed0d9d4c3f18077e7591299c83d8793adcf50 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 12 Mar 2025 13:10:03 -0400 Subject: [PATCH 394/762] Clang tidy/format fixes for fix_AlertLayer_circular_reference --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index d7e8b029..b05084c9 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -633,7 +633,7 @@ void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, std::placeholders::_1, std::placeholders::_2)); - std::weak_ptr diWeak = di; + const std::weak_ptr diWeak = di; gl::draw::GeoLines::RegisterEventHandler( di, std::bind(&AlertLayer::Impl::HandleGeoLinesEvent, @@ -694,7 +694,7 @@ void AlertLayer::Impl::UpdateLines() void AlertLayer::Impl::HandleGeoLinesEvent( std::weak_ptr& diWeak, QEvent* ev) { - std::shared_ptr di = diWeak.lock(); + const std::shared_ptr di = diWeak.lock(); if (di == nullptr) { return; From 0fff5f9e4db3b3f04ac1159e42221cf9f2217170 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 13 Mar 2025 11:21:28 -0400 Subject: [PATCH 395/762] Fix boost::containers::stable_vector memory leak in MapWidget --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 9096a4b7..ffedbcc7 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1185,6 +1185,8 @@ void MapWidgetImpl::AddLayers() layerList_.clear(); genericLayers_.clear(); placefileLayers_.clear(); + customLayers_.clear(); + customLayers_.shrink_to_fit(); // Update custom layer list from model customLayers_ = model::LayerModel::Instance()->GetLayers(); From 61ac1e56124d31342ead7edb9072cc0bd76200f2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 12:25:18 -0400 Subject: [PATCH 396/762] Fix boost::containers::stable_vector memory leak in MapWidget by local variable --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ffedbcc7..51aa98ef 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -204,8 +205,7 @@ public: const std::vector emptyStyles_ {}; std::vector customStyles_ { MapStyle {.name_ {"Custom"}, .url_ {}, .drawBelow_ {}}}; - QStringList styleLayers_; - types::LayerVector customLayers_; + QStringList styleLayers_; boost::uuids::uuid customStyleUrlChangedCallbackId_ {}; boost::uuids::uuid customStyleDrawBelowChangedCallbackId_ {}; @@ -1185,22 +1185,20 @@ void MapWidgetImpl::AddLayers() layerList_.clear(); genericLayers_.clear(); placefileLayers_.clear(); - customLayers_.clear(); - customLayers_.shrink_to_fit(); // Update custom layer list from model - customLayers_ = model::LayerModel::Instance()->GetLayers(); + types::LayerVector customLayers = model::LayerModel::Instance()->GetLayers(); // Start by drawing layers before any style-defined layers std::string before = styleLayers_.front().toStdString(); // Loop through each custom layer in reverse order - for (auto it = customLayers_.crbegin(); it != customLayers_.crend(); ++it) + for (const auto & customLayer : std::ranges::reverse_view(customLayers)) { - if (it->type_ == types::LayerType::Map) + if (customLayer.type_ == types::LayerType::Map) { // Style-defined map layers - switch (std::get(it->description_)) + switch (std::get(customLayer.description_)) { // Subsequent layers are drawn underneath the map symbology layer case types::MapLayer::MapUnderlay: @@ -1216,10 +1214,10 @@ void MapWidgetImpl::AddLayers() break; } } - else if (it->displayed_[id_]) + else if (customLayer.displayed_[id_]) { // If the layer is displayed for the current map, add it - AddLayer(it->type_, it->description_, before); + AddLayer(customLayer.type_, customLayer.description_, before); } } } From 3ef794a25e4661a828722ab2e098402f062e1530 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 12:31:37 -0400 Subject: [PATCH 397/762] Clang tidy/format fixes for fix_stable_vector_memory_leak --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 51aa98ef..466a03fe 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1193,7 +1193,7 @@ void MapWidgetImpl::AddLayers() std::string before = styleLayers_.front().toStdString(); // Loop through each custom layer in reverse order - for (const auto & customLayer : std::ranges::reverse_view(customLayers)) + for (const auto& customLayer : std::ranges::reverse_view(customLayers)) { if (customLayer.type_ == types::LayerType::Map) { @@ -1214,6 +1214,8 @@ void MapWidgetImpl::AddLayers() break; } } + // id_ is always < 4, so this is safe + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) else if (customLayer.displayed_[id_]) { // If the layer is displayed for the current map, add it From f7ee395eba7135a619de9fe5e9f6877cd7076f99 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 16 Mar 2025 18:05:10 -0500 Subject: [PATCH 398/762] Add cache for conan artifacts --- .github/workflows/ci.yml | 15 ++++++++++++--- .github/workflows/clang-tidy-review.yml | 13 ++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 408cacf5..474bff6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' + conan_path: '%USERPROFILE%\.conan2' conan_package_manager: '' conan_profile: scwx-win64_msvc2022 appimage_arch: '' @@ -48,6 +49,7 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' + conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11 appimage_arch: x86_64 @@ -64,6 +66,7 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' + conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 appimage_arch: x86_64 @@ -80,6 +83,7 @@ jobs: qt_arch_dir: gcc_arm64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' + conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11_armv8 appimage_arch: aarch64 @@ -130,13 +134,18 @@ jobs: shell: pwsh run: | pip install geopandas ` - GitPython + GitPython ` + conan + + - name: Cache Conan Packages + uses: actions/cache@v4 + with: + path: ${{ matrix.conan_path }} + key: ${{ matrix.name }}-build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} - name: Install Conan Packages shell: pwsh run: | - pip install conan - conan profile detect -e conan config install ` ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` -tf profiles diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index dcc99a46..e7bd9281 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -24,6 +24,8 @@ jobs: qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' + conan_cache_name: linux_clang_x64 + conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 compiler_packages: clang-17 clang-tidy-17 @@ -68,14 +70,19 @@ jobs: shell: pwsh run: | pip install geopandas ` - GitPython + GitPython ` + conan pip install --break-system-packages clang-tidy-review/post/clang_tidy_review + - name: Cache Conan Packages + uses: actions/cache@v4 + with: + path: ${{ matrix.conan_path }} + key: ${{ matrix.conan_cache_name }}-build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} + - name: Install Conan Packages shell: pwsh run: | - pip install conan - conan profile detect -e conan config install ` ./source/tools/conan/profiles/${{ matrix.conan_profile }} ` -tf profiles From 087f6ef310a4ca19ca4f45dbc28b7da9dfec19bc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 16 Mar 2025 20:07:11 -0400 Subject: [PATCH 399/762] Speed up geolines by avoiding iterative finds --- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 94 ++++++++++---------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index b18e68eb..c3999c36 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -50,6 +50,7 @@ struct GeoLineDrawItem : types::EventHandler units::angle::degrees angle_ {}; std::string hoverText_ {}; GeoLines::HoverCallback hoverCallback_ {nullptr}; + size_t lineIndex_ {0}; }; class GeoLines::Impl @@ -87,10 +88,10 @@ public: void UpdateBuffers(); void UpdateModifiedLineBuffers(); void UpdateSingleBuffer(const std::shared_ptr& di, - std::size_t lineIndex, std::vector& linesBuffer, - std::vector& integerBuffer, - std::vector& hoverLines); + std::vector& integerBuffer, + std::unordered_map, + LineHoverEntry>& hoverLines); std::shared_ptr context_; @@ -112,8 +113,10 @@ public: std::vector newLinesBuffer_ {}; std::vector newIntegerBuffer_ {}; - std::vector currentHoverLines_ {}; - std::vector newHoverLines_ {}; + std::unordered_map, LineHoverEntry> + currentHoverLines_ {}; + std::unordered_map, LineHoverEntry> + newHoverLines_ {}; std::shared_ptr shaderProgram_; GLint uMVPMatrixLocation_; @@ -323,7 +326,9 @@ void GeoLines::StartLines() std::shared_ptr GeoLines::AddLine() { - return p->newLineList_.emplace_back(std::make_shared()); + auto& di = p->newLineList_.emplace_back(std::make_shared()); + di->lineIndex_ = p->newLineList_.size() - 1; + return di; } void GeoLines::SetLineLocation(const std::shared_ptr& di, @@ -470,7 +475,7 @@ void GeoLines::Impl::UpdateBuffers() // Update line buffer UpdateSingleBuffer( - di, i, newLinesBuffer_, newIntegerBuffer_, newHoverLines_); + di, newLinesBuffer_, newIntegerBuffer_, newHoverLines_); } // All lines have been updated @@ -488,23 +493,15 @@ void GeoLines::Impl::UpdateModifiedLineBuffers() // Update buffers for modified lines for (auto& di : dirtyLines_) { - // Find modified line in the current list - auto it = - std::find(currentLineList_.cbegin(), currentLineList_.cend(), di); - - // Ignore invalid lines - if (it == currentLineList_.cend()) + // Check if modified line is in the current list + if (currentLineList_.size() < di->lineIndex_ || + currentLineList_[di->lineIndex_] != di) { continue; } - auto lineIndex = std::distance(currentLineList_.cbegin(), it); - - UpdateSingleBuffer(di, - lineIndex, - currentLinesBuffer_, - currentIntegerBuffer_, - currentHoverLines_); + UpdateSingleBuffer( + di, currentLinesBuffer_, currentIntegerBuffer_, currentHoverLines_); } // Clear list of modified lines @@ -517,10 +514,10 @@ void GeoLines::Impl::UpdateModifiedLineBuffers() void GeoLines::Impl::UpdateSingleBuffer( const std::shared_ptr& di, - std::size_t lineIndex, std::vector& lineBuffer, std::vector& integerBuffer, - std::vector& hoverLines) + std::unordered_map, LineHoverEntry>& + hoverLines) { // Threshold value units::length::nautical_miles threshold = di->threshold_; @@ -588,10 +585,10 @@ void GeoLines::Impl::UpdateSingleBuffer( // Buffer position data auto lineBufferPosition = lineBuffer.end(); - auto lineBufferOffset = lineIndex * kLineBufferLength_; + auto lineBufferOffset = di->lineIndex_ * kLineBufferLength_; auto integerBufferPosition = integerBuffer.end(); - auto integerBufferOffset = lineIndex * kIntegerBufferLength_; + auto integerBufferOffset = di->lineIndex_ * kIntegerBufferLength_; if (lineBufferOffset < lineBuffer.size()) { @@ -620,9 +617,7 @@ void GeoLines::Impl::UpdateSingleBuffer( std::copy(integerData.begin(), integerData.end(), integerBufferPosition); } - auto hoverIt = std::find_if(hoverLines.begin(), - hoverLines.end(), - [&di](auto& entry) { return entry.di_ == di; }); + auto hoverIt = hoverLines.find(di); if (di->visible_ && (!di->hoverText_.empty() || di->hoverCallback_ != nullptr || di->event_ != nullptr)) @@ -644,17 +639,23 @@ void GeoLines::Impl::UpdateSingleBuffer( if (hoverIt == hoverLines.end()) { - hoverLines.emplace_back( - LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr}); + hoverLines.emplace(di, + LineHoverEntry {.di_ = di, + .p1_ = sc1, + .p2_ = sc2, + .otl_ = otl, + .otr_ = otr, + .obl_ = obl, + .obr_ = obr}); } else { - hoverIt->p1_ = sc1; - hoverIt->p2_ = sc2; - hoverIt->otl_ = otl; - hoverIt->otr_ = otr; - hoverIt->obl_ = obl; - hoverIt->obr_ = obr; + hoverIt->second.p1_ = sc1; + hoverIt->second.p2_ = sc2; + hoverIt->second.otl_ = otl; + hoverIt->second.otr_ = otr; + hoverIt->second.obl_ = obl; + hoverIt->second.obr_ = obr; } } else if (hoverIt != hoverLines.end()) @@ -726,10 +727,12 @@ bool GeoLines::RunMousePicking( // For each pickable line auto it = std::find_if( std::execution::par_unseq, - p->currentHoverLines_.rbegin(), - p->currentHoverLines_.rend(), - [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line) + p->currentHoverLines_.cbegin(), + p->currentHoverLines_.cend(), + [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords]( + const auto& lineIt) { + const auto& line = lineIt.second; if (( // Placefile is thresholded mapDistance > units::length::meters {0.0} && @@ -785,24 +788,25 @@ bool GeoLines::RunMousePicking( return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); }); - if (it != p->currentHoverLines_.crend()) + if (it != p->currentHoverLines_.cend()) { itemPicked = true; - if (!it->di_->hoverText_.empty()) + if (!it->second.di_->hoverText_.empty()) { // Show tooltip - util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); + util::tooltip::Show(it->second.di_->hoverText_, mouseGlobalPos); } - else if (it->di_->hoverCallback_ != nullptr) + else if (it->second.di_->hoverCallback_ != nullptr) { - it->di_->hoverCallback_(it->di_, mouseGlobalPos); + std::shared_ptr di = it->second.di_; + it->second.di_->hoverCallback_(di, mouseGlobalPos); } - if (it->di_->event_ != nullptr) + if (it->second.di_->event_ != nullptr) { // Register event handler - eventHandler = it->di_; + eventHandler = it->second.di_; } } From e667155cd5d99d0b593010a9b6227882b4acd463 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 16 Mar 2025 20:55:15 -0500 Subject: [PATCH 400/762] Tweak cache path, update cache name --- .github/workflows/ci.yml | 8 ++------ .github/workflows/clang-tidy-review.yml | 6 ++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 474bff6f..f6682484 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,6 @@ jobs: qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_path: '%USERPROFILE%\.conan2' conan_package_manager: '' conan_profile: scwx-win64_msvc2022 appimage_arch: '' @@ -49,7 +48,6 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11 appimage_arch: x86_64 @@ -66,7 +64,6 @@ jobs: qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 appimage_arch: x86_64 @@ -83,7 +80,6 @@ jobs: qt_arch_dir: gcc_arm64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_gcc-11_armv8 appimage_arch: aarch64 @@ -140,8 +136,8 @@ jobs: - name: Cache Conan Packages uses: actions/cache@v4 with: - path: ${{ matrix.conan_path }} - key: ${{ matrix.name }}-build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} + path: ~/.conan2 + key: build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} - name: Install Conan Packages shell: pwsh diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index e7bd9281..f8c4495e 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -24,8 +24,6 @@ jobs: qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' - conan_cache_name: linux_clang_x64 - conan_path: '~/.conan2' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True conan_profile: scwx-linux_clang-17 compiler_packages: clang-17 clang-tidy-17 @@ -77,8 +75,8 @@ jobs: - name: Cache Conan Packages uses: actions/cache@v4 with: - path: ${{ matrix.conan_path }} - key: ${{ matrix.conan_cache_name }}-build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} + path: ~/.conan2 + key: build-${{ matrix.conan_profile }}-${{ hashFiles('./source/conanfile.py', './source/tools/conan/profiles/*') }} - name: Install Conan Packages shell: pwsh From fea9083f7d6c26a15996b2713d2e1eb0d76d5797 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 17 Mar 2025 09:39:01 -0400 Subject: [PATCH 401/762] Remove unnecessary copy, and make callback argument const in geolines --- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 5 ++--- scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp | 2 +- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 9 +++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index c3999c36..aa4d2ccb 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -494,7 +494,7 @@ void GeoLines::Impl::UpdateModifiedLineBuffers() for (auto& di : dirtyLines_) { // Check if modified line is in the current list - if (currentLineList_.size() < di->lineIndex_ || + if (di->lineIndex_ >= currentLineList_.size() || currentLineList_[di->lineIndex_] != di) { continue; @@ -799,8 +799,7 @@ bool GeoLines::RunMousePicking( } else if (it->second.di_->hoverCallback_ != nullptr) { - std::shared_ptr di = it->second.di_; - it->second.di_->hoverCallback_(di, mouseGlobalPos); + it->second.di_->hoverCallback_(it->second.di_, mouseGlobalPos); } if (it->second.di_->event_ != nullptr) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp index b7b792d0..4bc29b26 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp @@ -19,7 +19,7 @@ struct GeoLineDrawItem; class GeoLines : public DrawItem { public: - typedef std::function&, + typedef std::function&, const QPointF&)> HoverCallback; diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index b05084c9..c88f6220 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -159,8 +159,9 @@ public: void ConnectSignals(); void HandleGeoLinesEvent(std::weak_ptr& di, QEvent* ev); - void HandleGeoLinesHover(std::shared_ptr& di, - const QPointF& mouseGlobalPos); + void + HandleGeoLinesHover(const std::shared_ptr& di, + const QPointF& mouseGlobalPos); void ScheduleRefresh(); LineData& GetLineData(const std::shared_ptr& segment, @@ -720,8 +721,8 @@ void AlertLayer::Impl::HandleGeoLinesEvent( } void AlertLayer::Impl::HandleGeoLinesHover( - std::shared_ptr& di, - const QPointF& mouseGlobalPos) + const std::shared_ptr& di, + const QPointF& mouseGlobalPos) { if (di != lastHoverDi_) { From 35d642e76ff91b9c19d1002edb9bfc068822ba56 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 18 Mar 2025 11:29:25 -0400 Subject: [PATCH 402/762] Clang tidy/format fixes for speed_up_geolines --- scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp index 4bc29b26..d6727110 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp @@ -19,9 +19,8 @@ struct GeoLineDrawItem; class GeoLines : public DrawItem { public: - typedef std::function&, - const QPointF&)> - HoverCallback; + using HoverCallback = std::function&, const QPointF&)>; explicit GeoLines(std::shared_ptr context); ~GeoLines(); From 6d107f6c2d57da163e1b93ee971362ce830dd284 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 4 Jan 2025 13:49:31 -0500 Subject: [PATCH 403/762] Inital code for per map layer ImGui contexts --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 106 +++++++++++++++++- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 5 + scwx-qt/source/scwx/qt/map/map_context.cpp | 20 ++++ scwx-qt/source/scwx/qt/map/map_context.hpp | 2 + scwx-qt/source/scwx/qt/map/map_widget.cpp | 25 +++-- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 6 +- .../source/scwx/qt/map/radar_site_layer.cpp | 4 + 7 files changed, 153 insertions(+), 15 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 7d03d13a..4dc24f89 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -1,7 +1,14 @@ +#include #include +#include #include #include +#include +#include +#include +#include + namespace scwx { namespace qt @@ -16,18 +23,65 @@ class DrawLayerImpl { public: explicit DrawLayerImpl(std::shared_ptr context) : - context_ {context}, drawList_ {}, textureAtlas_ {GL_INVALID_INDEX} + context_ {context}, + drawList_ {}, + textureAtlas_ {GL_INVALID_INDEX}, + imGuiRendererInitialized_ {false} { + static size_t currentMapId_ {0u}; + imGuiContextName_ = fmt::format("Layer {}", ++currentMapId_); + imGuiContext_ = + model::ImGuiContextModel::Instance().CreateContext(imGuiContextName_); + + // Initialize ImGui Qt backend + ImGui_ImplQt_Init(); } - ~DrawLayerImpl() {} + ~DrawLayerImpl() + { + // Set ImGui Context + ImGui::SetCurrentContext(imGuiContext_); + + // Shutdown ImGui Context + if (imGuiRendererInitialized_) + { + ImGui_ImplOpenGL3_Shutdown(); + } + ImGui_ImplQt_Shutdown(); + + // Destroy ImGui Context + model::ImGuiContextModel::Instance().DestroyContext(imGuiContextName_); + } + + void ImGuiCheckFonts(); std::shared_ptr context_; std::vector> drawList_; GLuint textureAtlas_; std::uint64_t textureAtlasBuildCount_ {}; + + std::string imGuiContextName_; + ImGuiContext* imGuiContext_; + bool imGuiRendererInitialized_; + std::uint64_t imGuiFontsBuildCount_ {}; }; +void DrawLayerImpl::ImGuiCheckFonts() +{ + // Update ImGui Fonts if required + std::uint64_t currentImGuiFontsBuildCount = + manager::FontManager::Instance().imgui_fonts_build_count(); + + if (imGuiFontsBuildCount_ != currentImGuiFontsBuildCount || + !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) + { + ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplOpenGL3_CreateFontsTexture(); + } + + imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; +} + DrawLayer::DrawLayer(const std::shared_ptr& context) : GenericLayer(context), p(std::make_unique(context)) { @@ -42,9 +96,48 @@ void DrawLayer::Initialize() { item->Initialize(); } + + ImGuiInitialize(); } -void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void DrawLayer::StartImGuiFrame() +{ + auto defaultFont = manager::FontManager::Instance().GetImGuiFont( + types::FontCategory::Default); + + // Setup ImGui Frame + ImGui::SetCurrentContext(p->imGuiContext_); + + // Start ImGui Frame + ImGui_ImplQt_NewFrame(p->context_->widget()); + ImGui_ImplOpenGL3_NewFrame(); + p->ImGuiCheckFonts(); + ImGui::NewFrame(); + ImGui::PushFont(defaultFont->font()); +} + +void DrawLayer::EndImGuiFrame() +{ + // Pop default font + ImGui::PopFont(); + + // Render ImGui Frame + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); +} + +void DrawLayer::ImGuiInitialize() +{ + ImGui::SetCurrentContext(p->imGuiContext_); + ImGui_ImplQt_RegisterWidget(p->context_->widget()); + ImGui_ImplOpenGL3_Init(); + p->imGuiFontsBuildCount_ = + manager::FontManager::Instance().imgui_fonts_build_count(); + p->imGuiRendererInitialized_ = true; +} + +void DrawLayer::RenderWithoutImGui( + const QMapLibre::CustomLayerRenderParameters& params) { gl::OpenGLFunctions& gl = p->context_->gl(); p->textureAtlas_ = p->context_->GetTextureAtlas(); @@ -69,6 +162,13 @@ void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->textureAtlasBuildCount_ = newTextureAtlasBuildCount; } + void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +{ + StartImGuiFrame(); + RenderWithoutImGui(params); + EndImGuiFrame(); +} + void DrawLayer::Deinitialize() { p->textureAtlas_ = GL_INVALID_INDEX; diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index 22dfa76c..416c4e7f 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -32,6 +32,11 @@ public: protected: void AddDrawItem(const std::shared_ptr& drawItem); + void StartImGuiFrame(); + void EndImGuiFrame(); + void ImGuiInitialize(); + void + RenderWithoutImGui(const QMapLibre::CustomLayerRenderParameters& params); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 38a41e1f..7c0683d4 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -36,6 +36,8 @@ public: std::shared_ptr overlayProductView_ {nullptr}; std::shared_ptr radarProductView_; + + QWidget* widget_; }; MapContext::MapContext( @@ -109,6 +111,19 @@ int16_t MapContext::radar_product_code() const return p->radarProductCode_; } +<<<<<<< HEAD +======= +QMapLibre::CustomLayerRenderParameters MapContext::render_parameters() const +{ + return p->renderParameters_; +} + +QWidget* MapContext::widget() const +{ + return p->widget_; +} + +>>>>>>> 513a41d3 (Inital code for per map layer ImGui contexts) void MapContext::set_map(const std::shared_ptr& map) { p->map_ = map; @@ -167,6 +182,11 @@ void MapContext::set_radar_product_code(int16_t radarProductCode) p->radarProductCode_ = radarProductCode; } +void MapContext::set_widget(QWidget* widget) +{ + p->widget_ = widget; +} + } // namespace map } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 59fb8a5b..8e79b028 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -50,6 +50,7 @@ public: common::RadarProductGroup radar_product_group() const; std::string radar_product() const; int16_t radar_product_code() const; + QWidget* widget() const; void set_map(const std::shared_ptr& map); void set_map_copyrights(const std::string& copyrights); @@ -64,6 +65,7 @@ public: void set_radar_product_group(common::RadarProductGroup radarProductGroup); void set_radar_product(const std::string& radarProduct); void set_radar_product_code(int16_t radarProductCode); + void set_widget(QWidget* widget); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 466a03fe..2f801f00 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -114,6 +114,7 @@ public: context_->set_map_provider( GetMapProvider(generalSettings.map_provider().GetValue())); context_->set_overlay_product_view(overlayProductView); + context_->set_widget(widget); // Initialize map data SetRadarSite(generalSettings.default_radar_site().GetValue()); @@ -1571,21 +1572,10 @@ void MapWidget::paintGL() // Handle hotkey updates p->HandleHotkeyUpdates(); - // Setup ImGui Frame - ImGui::SetCurrentContext(p->imGuiContext_); - // Lock ImGui font atlas prior to new ImGui frame std::shared_lock imguiFontAtlasLock { manager::FontManager::Instance().imgui_font_atlas_mutex()}; - // Start ImGui Frame - ImGui_ImplQt_NewFrame(this); - ImGui_ImplOpenGL3_NewFrame(); - p->ImGuiCheckFonts(); - ImGui::NewFrame(); - - // Set default font - ImGui::PushFont(defaultFont->font()); // Update pixel ratio p->context_->set_pixel_ratio(pixelRatio()); @@ -1596,6 +1586,19 @@ void MapWidget::paintGL() size() * pixelRatio()); p->map_->render(); + // ImGui tool tip code + // Setup ImGui Frame + ImGui::SetCurrentContext(p->imGuiContext_); + + // Start ImGui Frame + ImGui_ImplQt_NewFrame(this); + ImGui_ImplOpenGL3_NewFrame(); + p->ImGuiCheckFonts(); + ImGui::NewFrame(); + + // Set default font + ImGui::PushFont(defaultFont->font()); + // Perform mouse picking if (p->hasMouse_) { diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index da82ef7f..1f7110a2 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -292,6 +292,8 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) auto& settings = context()->settings(); const float pixelRatio = context()->pixel_ratio(); + StartImGuiFrame(); + p->sweepTimePicked_ = false; if (radarProductView != nullptr) @@ -457,7 +459,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->icons_->SetIconVisible(p->mapLogoIcon_, generalSettings.show_map_logo().GetValue()); - DrawLayer::Render(params); + DrawLayer::RenderWithoutImGui(params); auto mapCopyrights = context()->map_copyrights(); if (mapCopyrights.length() > 0 && @@ -491,6 +493,8 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->lastFontSize_ = ImGui::GetFontSize(); p->lastColorTableMargins_ = colorTableMargins; + EndImGuiFrame(); + SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index d2fd7547..431af597 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -55,6 +55,8 @@ void RadarSiteLayer::Initialize() logger_->debug("Initialize()"); p->radarSites_ = config::RadarSite::GetAll(); + + ImGuiInitialize(); } void RadarSiteLayer::Render( @@ -84,6 +86,7 @@ void RadarSiteLayer::Render( p->halfWidth_ = params.width * 0.5f; p->halfHeight_ = params.height * 0.5f; + StartImGuiFrame(); // Radar site ImGui windows shouldn't have padding ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {0.0f, 0.0f}); @@ -93,6 +96,7 @@ void RadarSiteLayer::Render( } ImGui::PopStyleVar(); + EndImGuiFrame(); SCWX_GL_CHECK_ERROR(); } From 9bd5af03f9dff370a34bbc919efd78a4f36a242f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 6 Jan 2025 08:57:55 -0500 Subject: [PATCH 404/762] fix crash when selecting radar site with new ImGui contexts --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 6 +++++- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 1 + scwx-qt/source/scwx/qt/map/radar_site_layer.cpp | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 4dc24f89..9588a160 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -161,8 +161,12 @@ void DrawLayer::RenderWithoutImGui( p->textureAtlasBuildCount_ = newTextureAtlasBuildCount; } +void DrawLayer::ImGuiSelectContext() +{ + ImGui::SetCurrentContext(p->imGuiContext_); +} - void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { StartImGuiFrame(); RenderWithoutImGui(params); diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index 416c4e7f..a6fd685a 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -37,6 +37,7 @@ protected: void ImGuiInitialize(); void RenderWithoutImGui(const QMapLibre::CustomLayerRenderParameters& params); + void ImGuiSelectContext(); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 431af597..7398ab36 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -140,6 +140,7 @@ void RadarSiteLayer::Impl::RenderRadarSite( if (ImGui::Button(radarSite->id().c_str())) { Q_EMIT self_->RadarSiteSelected(radarSite->id()); + self_->ImGuiSelectContext(); } // Store hover text for mouse picking pass From c6596b3e7d287e58bafdfa03afae2a9220daf7e7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 18 Jan 2025 09:45:24 -0500 Subject: [PATCH 405/762] Move ImGuiCheckFonts back to only being called in map_widget (lock makes this safe) --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 29 +++-------------------- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 +++- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 9588a160..a0ad64e6 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -25,8 +25,7 @@ public: explicit DrawLayerImpl(std::shared_ptr context) : context_ {context}, drawList_ {}, - textureAtlas_ {GL_INVALID_INDEX}, - imGuiRendererInitialized_ {false} + textureAtlas_ {GL_INVALID_INDEX} { static size_t currentMapId_ {0u}; imGuiContextName_ = fmt::format("Layer {}", ++currentMapId_); @@ -52,36 +51,17 @@ public: model::ImGuiContextModel::Instance().DestroyContext(imGuiContextName_); } - void ImGuiCheckFonts(); - std::shared_ptr context_; std::vector> drawList_; GLuint textureAtlas_; std::uint64_t textureAtlasBuildCount_ {}; - std::string imGuiContextName_; + std::string imGuiContextName_; ImGuiContext* imGuiContext_; - bool imGuiRendererInitialized_; - std::uint64_t imGuiFontsBuildCount_ {}; + bool imGuiRendererInitialized_{}; }; -void DrawLayerImpl::ImGuiCheckFonts() -{ - // Update ImGui Fonts if required - std::uint64_t currentImGuiFontsBuildCount = - manager::FontManager::Instance().imgui_fonts_build_count(); - - if (imGuiFontsBuildCount_ != currentImGuiFontsBuildCount || - !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) - { - ImGui_ImplOpenGL3_DestroyFontsTexture(); - ImGui_ImplOpenGL3_CreateFontsTexture(); - } - - imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; -} - DrawLayer::DrawLayer(const std::shared_ptr& context) : GenericLayer(context), p(std::make_unique(context)) { @@ -111,7 +91,6 @@ void DrawLayer::StartImGuiFrame() // Start ImGui Frame ImGui_ImplQt_NewFrame(p->context_->widget()); ImGui_ImplOpenGL3_NewFrame(); - p->ImGuiCheckFonts(); ImGui::NewFrame(); ImGui::PushFont(defaultFont->font()); } @@ -131,8 +110,6 @@ void DrawLayer::ImGuiInitialize() ImGui::SetCurrentContext(p->imGuiContext_); ImGui_ImplQt_RegisterWidget(p->context_->widget()); ImGui_ImplOpenGL3_Init(); - p->imGuiFontsBuildCount_ = - manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 2f801f00..f2599579 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1576,6 +1576,9 @@ void MapWidget::paintGL() std::shared_lock imguiFontAtlasLock { manager::FontManager::Instance().imgui_font_atlas_mutex()}; + // Check ImGui fonts + ImGui::SetCurrentContext(p->imGuiContext_); + p->ImGuiCheckFonts(); // Update pixel ratio p->context_->set_pixel_ratio(pixelRatio()); @@ -1593,7 +1596,6 @@ void MapWidget::paintGL() // Start ImGui Frame ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); - p->ImGuiCheckFonts(); ImGui::NewFrame(); // Set default font From a9daf47741c8e26617202af7b0ebd64645ca0685 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 21 Jan 2025 10:56:43 -0500 Subject: [PATCH 406/762] add support for opentype fonts --- .../source/scwx/qt/manager/font_manager.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 89a1643f..4f5ad99f 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -29,6 +29,7 @@ static const std::string logPrefix_ = "scwx::qt::manager::font_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string kFcTrueType_ {"TrueType"}; +static const std::string kFcOpenType_ {"CFF"}; struct FontRecord { @@ -70,6 +71,7 @@ public: const std::vector& GetRawFontData(const std::string& filename); + static bool CheckFontFormat(const FcChar8* format); static FontRecord MatchFontFile(const std::string& family, const std::vector& styles); @@ -457,6 +459,13 @@ void FontManager::Impl::FinalizeFontconfig() FcFini(); } +bool FontManager::Impl::CheckFontFormat(const FcChar8* format) +{ + const std::string stdFormat = reinterpret_cast(format); + + return stdFormat == kFcTrueType_ || stdFormat == kFcOpenType_; +} + FontRecord FontManager::Impl::MatchFontFile(const std::string& family, const std::vector& styles) @@ -469,9 +478,6 @@ FontManager::Impl::MatchFontFile(const std::string& family, FcPatternAddString( pattern, FC_FAMILY, reinterpret_cast(family.c_str())); - FcPatternAddString(pattern, - FC_FONTFORMAT, - reinterpret_cast(kFcTrueType_.c_str())); FcPatternAddBool(pattern, FC_SYMBOL, FcFalse); if (!styles.empty()) @@ -505,6 +511,7 @@ FontManager::Impl::MatchFontFile(const std::string& family, FcChar8* fcFamily = nullptr; FcChar8* fcStyle = nullptr; FcChar8* fcFile = nullptr; + FcChar8* fcFormat = nullptr; FcBool fcSymbol = FcFalse; // Match was found, get properties @@ -515,7 +522,10 @@ FontManager::Impl::MatchFontFile(const std::string& family, FcPatternGetString(match, FC_FILE, 0, &fcFile) == FcResultMatch && FcPatternGetBool(match, FC_SYMBOL, 0, &fcSymbol) == FcResultMatch && - fcSymbol == FcFalse /*Must check fcSymbol manually*/) + FcPatternGetString(match, FC_FONTFORMAT, 0, &fcFormat) == + FcResultMatch && + fcSymbol == FcFalse /*Must check fcSymbol manually*/ && + CheckFontFormat(fcFormat)) { record.family_ = reinterpret_cast(fcFamily); record.style_ = reinterpret_cast(fcStyle); From c139b156ee73defa472d6804d3f437d683002d39 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 24 Jan 2025 12:37:44 -0500 Subject: [PATCH 407/762] Make naming more consistent for some ImGui DrawLayer Functions --- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 8 ++++---- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 4 ++-- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 4 ++-- scwx-qt/source/scwx/qt/map/radar_site_layer.cpp | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index a0ad64e6..91dace1c 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -80,7 +80,7 @@ void DrawLayer::Initialize() ImGuiInitialize(); } -void DrawLayer::StartImGuiFrame() +void DrawLayer::ImGuiFrameStart() { auto defaultFont = manager::FontManager::Instance().GetImGuiFont( types::FontCategory::Default); @@ -95,7 +95,7 @@ void DrawLayer::StartImGuiFrame() ImGui::PushFont(defaultFont->font()); } -void DrawLayer::EndImGuiFrame() +void DrawLayer::ImGuiFrameEnd() { // Pop default font ImGui::PopFont(); @@ -145,9 +145,9 @@ void DrawLayer::ImGuiSelectContext() void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - StartImGuiFrame(); + ImGuiFrameStart(); RenderWithoutImGui(params); - EndImGuiFrame(); + ImGuiFrameEnd(); } void DrawLayer::Deinitialize() diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index a6fd685a..8bfe8a5d 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -32,8 +32,8 @@ public: protected: void AddDrawItem(const std::shared_ptr& drawItem); - void StartImGuiFrame(); - void EndImGuiFrame(); + void ImGuiFrameStart(); + void ImGuiFrameEnd(); void ImGuiInitialize(); void RenderWithoutImGui(const QMapLibre::CustomLayerRenderParameters& params); diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 1f7110a2..53557222 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -292,7 +292,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) auto& settings = context()->settings(); const float pixelRatio = context()->pixel_ratio(); - StartImGuiFrame(); + ImGuiFrameStart(); p->sweepTimePicked_ = false; @@ -493,7 +493,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->lastFontSize_ = ImGui::GetFontSize(); p->lastColorTableMargins_ = colorTableMargins; - EndImGuiFrame(); + ImGuiFrameEnd(); SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 7398ab36..1cb5e222 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -86,7 +86,7 @@ void RadarSiteLayer::Render( p->halfWidth_ = params.width * 0.5f; p->halfHeight_ = params.height * 0.5f; - StartImGuiFrame(); + ImGuiFrameStart(); // Radar site ImGui windows shouldn't have padding ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {0.0f, 0.0f}); @@ -96,7 +96,7 @@ void RadarSiteLayer::Render( } ImGui::PopStyleVar(); - EndImGuiFrame(); + ImGuiFrameEnd(); SCWX_GL_CHECK_ERROR(); } From ab682567c69a4788524ae0ab605a62473283cce5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 3 Mar 2025 10:24:41 -0500 Subject: [PATCH 408/762] Fix merge conflict during rebasing of rework_layer_text --- scwx-qt/source/scwx/qt/map/map_context.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 7c0683d4..46c2b6fa 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -111,19 +111,11 @@ int16_t MapContext::radar_product_code() const return p->radarProductCode_; } -<<<<<<< HEAD -======= -QMapLibre::CustomLayerRenderParameters MapContext::render_parameters() const -{ - return p->renderParameters_; -} - QWidget* MapContext::widget() const { return p->widget_; } ->>>>>>> 513a41d3 (Inital code for per map layer ImGui contexts) void MapContext::set_map(const std::shared_ptr& map) { p->map_ = map; From ec296d98eb1d3e313bdc4b48ba19a2f4ddf86035 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 3 Mar 2025 10:52:39 -0500 Subject: [PATCH 409/762] clang format/tidy fixes for rework_layer_text --- .../source/scwx/qt/manager/font_manager.cpp | 4 +- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 43 ++++++++++--------- scwx-qt/source/scwx/qt/map/map_context.hpp | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 4f5ad99f..d92d3bd7 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -71,7 +71,7 @@ public: const std::vector& GetRawFontData(const std::string& filename); - static bool CheckFontFormat(const FcChar8* format); + static bool CheckFontFormat(const FcChar8* format); static FontRecord MatchFontFile(const std::string& family, const std::vector& styles); @@ -461,7 +461,7 @@ void FontManager::Impl::FinalizeFontconfig() bool FontManager::Impl::CheckFontFormat(const FcChar8* format) { - const std::string stdFormat = reinterpret_cast(format); + const std::string stdFormat = reinterpret_cast(format); return stdFormat == kFcTrueType_ || stdFormat == kFcOpenType_; } diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 91dace1c..c39b9f4c 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -6,14 +7,11 @@ #include #include +#include #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::draw_layer"; @@ -23,13 +21,13 @@ class DrawLayerImpl { public: explicit DrawLayerImpl(std::shared_ptr context) : - context_ {context}, - drawList_ {}, - textureAtlas_ {GL_INVALID_INDEX} + context_ {std::move(context)}, drawList_ {} { static size_t currentMapId_ {0u}; imGuiContextName_ = fmt::format("Layer {}", ++currentMapId_); - imGuiContext_ = + // This must be initialized after the last line + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) + imGuiContext_ = model::ImGuiContextModel::Instance().CreateContext(imGuiContextName_); // Initialize ImGui Qt backend @@ -51,15 +49,20 @@ public: model::ImGuiContextModel::Instance().DestroyContext(imGuiContextName_); } + DrawLayerImpl(const DrawLayerImpl&) = delete; + DrawLayerImpl& operator=(const DrawLayerImpl&) = delete; + DrawLayerImpl(const DrawLayerImpl&&) = delete; + DrawLayerImpl& operator=(const DrawLayerImpl&&) = delete; + std::shared_ptr context_; std::vector> drawList_; - GLuint textureAtlas_; + GLuint textureAtlas_ {GL_INVALID_INDEX}; std::uint64_t textureAtlasBuildCount_ {}; std::string imGuiContextName_; ImGuiContext* imGuiContext_; - bool imGuiRendererInitialized_{}; + bool imGuiRendererInitialized_ {}; }; DrawLayer::DrawLayer(const std::shared_ptr& context) : @@ -171,15 +174,15 @@ bool DrawLayer::RunMousePicking( bool itemPicked = false; // For each draw item in the draw list in reverse - for (auto it = p->drawList_.rbegin(); it != p->drawList_.rend(); ++it) + for (auto& it : std::ranges::reverse_view(p->drawList_)) { // Run mouse picking on each draw item - if ((*it)->RunMousePicking(params, - mouseLocalPos, - mouseGlobalPos, - mouseCoords, - mouseGeoCoords, - eventHandler)) + if (it->RunMousePicking(params, + mouseLocalPos, + mouseGlobalPos, + mouseCoords, + mouseGeoCoords, + eventHandler)) { // If a draw item was picked, don't process additional items itemPicked = true; @@ -195,6 +198,4 @@ void DrawLayer::AddDrawItem(const std::shared_ptr& drawItem) p->drawList_.push_back(drawItem); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 8e79b028..57640263 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -50,7 +50,7 @@ public: common::RadarProductGroup radar_product_group() const; std::string radar_product() const; int16_t radar_product_code() const; - QWidget* widget() const; + [[nodiscard]] QWidget* widget() const; void set_map(const std::shared_ptr& map); void set_map_copyrights(const std::string& copyrights); From 2be140d29117ec9da09382396fc6cead61ee43f5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 19 Mar 2025 14:14:43 -0400 Subject: [PATCH 410/762] Use layer names in ImGui Context names --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 5 +++- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 14 ++++++---- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 3 +- scwx-qt/source/scwx/qt/map/map_context.hpp | 28 ++++++++++--------- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 3 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 3 +- .../scwx/qt/map/overlay_product_layer.cpp | 3 +- .../source/scwx/qt/map/placefile_layer.cpp | 2 +- .../source/scwx/qt/map/radar_site_layer.cpp | 2 +- 9 files changed, 38 insertions(+), 25 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index c88f6220..7c5c9db2 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -229,7 +229,10 @@ public: AlertLayer::AlertLayer(std::shared_ptr context, awips::Phenomenon phenomenon) : - DrawLayer(context), p(std::make_unique(this, context, phenomenon)) + DrawLayer( + context, + fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), + p(std::make_unique(this, context, phenomenon)) { for (auto alertActive : {false, true}) { diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index c39b9f4c..13d06780 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -20,11 +20,13 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class DrawLayerImpl { public: - explicit DrawLayerImpl(std::shared_ptr context) : + explicit DrawLayerImpl(std::shared_ptr context, + const std::string& imGuiContextName) : context_ {std::move(context)}, drawList_ {} { - static size_t currentMapId_ {0u}; - imGuiContextName_ = fmt::format("Layer {}", ++currentMapId_); + static size_t currentLayerId_ {0u}; + imGuiContextName_ = + fmt::format("{} {}", imGuiContextName, ++currentLayerId_); // This must be initialized after the last line // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) imGuiContext_ = @@ -65,8 +67,10 @@ public: bool imGuiRendererInitialized_ {}; }; -DrawLayer::DrawLayer(const std::shared_ptr& context) : - GenericLayer(context), p(std::make_unique(context)) +DrawLayer::DrawLayer(const std::shared_ptr& context, + const std::string& imGuiContextName) : + GenericLayer(context), + p(std::make_unique(context, imGuiContextName)) { } DrawLayer::~DrawLayer() = default; diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index 8bfe8a5d..6cfa5aae 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -15,7 +15,8 @@ class DrawLayerImpl; class DrawLayer : public GenericLayer { public: - explicit DrawLayer(const std::shared_ptr& context); + explicit DrawLayer(const std::shared_ptr& context, + const std::string& imGuiContextName); virtual ~DrawLayer(); virtual void Initialize() override; diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 57640263..680f9ddd 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -38,19 +38,21 @@ public: MapContext(MapContext&&) noexcept; MapContext& operator=(MapContext&&) noexcept; - std::weak_ptr map() const; - std::string map_copyrights() const; - MapProvider map_provider() const; - MapSettings& settings(); - QMargins color_table_margins() const; - float pixel_ratio() const; - common::Coordinate mouse_coordinate() const; - std::shared_ptr overlay_product_view() const; - std::shared_ptr radar_product_view() const; - common::RadarProductGroup radar_product_group() const; - std::string radar_product() const; - int16_t radar_product_code() const; - [[nodiscard]] QWidget* widget() const; + [[nodiscard]] std::weak_ptr map() const; + [[nodiscard]] std::string map_copyrights() const; + [[nodiscard]] MapProvider map_provider() const; + [[nodiscard]] MapSettings& settings(); + [[nodiscard]] QMargins color_table_margins() const; + [[nodiscard]] float pixel_ratio() const; + [[nodiscard]] common::Coordinate mouse_coordinate() const; + [[nodiscard]] std::shared_ptr + overlay_product_view() const; + [[nodiscard]] std::shared_ptr + radar_product_view() const; + [[nodiscard]] common::RadarProductGroup radar_product_group() const; + [[nodiscard]] std::string radar_product() const; + [[nodiscard]] int16_t radar_product_code() const; + [[nodiscard]] QWidget* widget() const; void set_map(const std::shared_ptr& map); void set_map_copyrights(const std::string& copyrights); diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 39e43a57..aec23f84 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -129,7 +129,8 @@ void MarkerLayer::Impl::ReloadMarkers() } MarkerLayer::MarkerLayer(const std::shared_ptr& context) : - DrawLayer(context), p(std::make_unique(this, context)) + DrawLayer(context, "MarkerLayer"), + p(std::make_unique(this, context)) { AddDrawItem(p->geoIcons_); } diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 53557222..3522ba40 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -143,7 +143,8 @@ public: }; OverlayLayer::OverlayLayer(std::shared_ptr context) : - DrawLayer(context), p(std::make_unique(this, context)) + DrawLayer(context, "OverlayLayer"), + p(std::make_unique(this, context)) { AddDrawItem(p->activeBoxOuter_); AddDrawItem(p->activeBoxInner_); diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 64745fd5..76eeaa8b 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -109,7 +109,8 @@ public: }; OverlayProductLayer::OverlayProductLayer(std::shared_ptr context) : - DrawLayer(context), p(std::make_unique(this, context)) + DrawLayer(context, "OverlayProductLayer"), + p(std::make_unique(this, context)) { auto overlayProductView = context->overlay_product_view(); connect(overlayProductView.get(), diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index be2d9a18..df9828eb 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -66,7 +66,7 @@ public: PlacefileLayer::PlacefileLayer(const std::shared_ptr& context, const std::string& placefileName) : - DrawLayer(context), + DrawLayer(context, fmt::format("PlacefileLayer {}", placefileName)), p(std::make_unique(this, context, placefileName)) { AddDrawItem(p->placefileImages_); diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 1cb5e222..65b53f14 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -44,7 +44,7 @@ public: }; RadarSiteLayer::RadarSiteLayer(std::shared_ptr context) : - DrawLayer(context), p(std::make_unique(this)) + DrawLayer(context, "RadarSiteLayer"), p(std::make_unique(this)) { } From f0ba7296d546fe9d38bb5c6fb20ec0a2be0253e1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 11:09:43 -0400 Subject: [PATCH 411/762] use non-standard coordinates for tdwr and optimize there generation --- .../scwx/qt/manager/radar_product_manager.cpp | 15 +++++- .../scwx/qt/manager/radar_product_manager.hpp | 1 + .../scwx/qt/view/level3_radial_view.cpp | 50 +++++++++++++------ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 9768c9da..edafecf5 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -428,9 +428,16 @@ const scwx::util::time_zone* RadarProductManager::default_time_zone() const } } +bool RadarProductManager::is_tdwr() const +{ + return p->radarSite_->type() == "tdwr"; +} + float RadarProductManager::gate_size() const { - return (p->radarSite_->type() == "tdwr") ? 150.0f : 250.0f; + // tdwr is 150 meter per gate, wsr88d is 250 meter per gate + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return (is_tdwr()) ? 150.0f : 250.0f; } std::string RadarProductManager::radar_id() const @@ -454,6 +461,12 @@ void RadarProductManager::Initialize() logger_->debug("Initialize()"); + if (is_tdwr()) + { + p->initialized_ = true; + return; + } + boost::timer::cpu_timer timer; // Calculate half degree azimuth coordinates diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 3f4899ea..ee54f147 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -44,6 +44,7 @@ public: [[nodiscard]] const std::vector& coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] bool is_tdwr() const; [[nodiscard]] float gate_size() const; [[nodiscard]] std::string radar_id() const; [[nodiscard]] std::shared_ptr radar_site() const; diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 135f5e65..29afba2d 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -45,7 +45,8 @@ public: void ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled); + bool smoothingEnabled, + float gateSize); [[nodiscard]] inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; @@ -269,17 +270,24 @@ void Level3RadialView::ComputeSweep() } common::RadialSize radialSize; - if (radials == common::MAX_0_5_DEGREE_RADIALS) + if (radarProductManager->is_tdwr()) { - radialSize = common::RadialSize::_0_5Degree; - } - else if (radials == common::MAX_1_DEGREE_RADIALS) - { - radialSize = common::RadialSize::_1Degree; + radialSize = common::RadialSize::NonStandard; } else { - radialSize = common::RadialSize::NonStandard; + if (radials == common::MAX_0_5_DEGREE_RADIALS) + { + radialSize = common::RadialSize::_0_5Degree; + } + else if (radials == common::MAX_1_DEGREE_RADIALS) + { + radialSize = common::RadialSize::_1Degree; + } + else + { + radialSize = common::RadialSize::NonStandard; + } } const std::vector& coordinates = @@ -323,11 +331,21 @@ void Level3RadialView::ComputeSweep() // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); + // Compute gate interval + const std::uint16_t dataMomentInterval = + descriptionBlock->x_resolution_raw(); + + // Get the gate length in meters. Use dataMomentInterval for NonStandard to + // avoid generating >1 base gates per bin. + const float gateLength = radialSize == common::RadialSize::NonStandard ? + static_cast(dataMomentInterval) : + radarProductManager->gate_size(); + // Determine which radial to start at std::uint16_t startRadial; if (radialSize == common::RadialSize::NonStandard) { - p->ComputeCoordinates(radialData, smoothingEnabled); + p->ComputeCoordinates(radialData, smoothingEnabled, gateLength); startRadial = 0; } else @@ -337,15 +355,11 @@ void Level3RadialView::ComputeSweep() startRadial = std::lroundf(startAngle * radialMultiplier); } - // Compute gate interval - const std::uint16_t dataMomentInterval = - descriptionBlock->x_resolution_raw(); - // Compute gate size (number of base gates per bin) const std::uint16_t gateSize = std::max( 1, dataMomentInterval / - static_cast(radarProductManager->gate_size())); + static_cast(gateLength)); // Compute gate range [startGate, endGate) std::uint16_t startGate = 0; @@ -526,7 +540,8 @@ Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const void Level3RadialView::Impl::ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled) + bool smoothingEnabled, + float gateSize) { logger_->debug("ComputeCoordinates()"); @@ -537,7 +552,6 @@ void Level3RadialView::Impl::ComputeCoordinates( auto radarProductManager = self_->radar_product_manager(); auto radarSite = radarProductManager->radar_site(); - const float gateSize = radarProductManager->gate_size(); const double radarLatitude = radarSite->latitude(); const double radarLongitude = radarSite->longitude(); @@ -583,6 +597,10 @@ void Level3RadialView::Impl::ComputeCoordinates( const float range = (static_cast(gate) + gateRangeOffset) * gateSize; const std::size_t offset = static_cast(radialGate) * 2; + if (offset + 1 >= coordinates_.size()) + { + return; + } double latitude = 0.0; double longitude = 0.0; From 504cde0e8b09a6bf43154bd89909501a8d645fd9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 11:11:55 -0400 Subject: [PATCH 412/762] Add long range reflectivity (TZL) product --- wxdata/source/scwx/common/products.cpp | 6 ++++-- wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/wxdata/source/scwx/common/products.cpp b/wxdata/source/scwx/common/products.cpp index f3ecd1e8..3e7a3fe6 100644 --- a/wxdata/source/scwx/common/products.cpp +++ b/wxdata/source/scwx/common/products.cpp @@ -49,7 +49,7 @@ static const std::unordered_map level3ProductCodeMap_ { {153, "SDR"}, {154, "SDV"}, {159, "DZD"}, {161, "DCC"}, {163, "DKD"}, {165, "DHC"}, {166, "ML"}, {169, "OHA"}, {170, "DAA"}, {172, "DTA"}, {173, "DUA"}, {174, "DOD"}, {175, "DSD"}, {177, "HHC"}, {180, "TDR"}, - {182, "TDV"}}; + {182, "TDV"}, {186, "TZL"}}; static const std::unordered_map level3ProductDescription_ { @@ -84,6 +84,7 @@ static const std::unordered_map {"HHC", "Hybrid Hydrometeor Classification"}, {"TDR", "Digital Reflectivity"}, {"TDV", "Digital Velocity"}, + {"TZL", "Long Range Reflectivity"}, {"?", "Unknown"}}; static const std::unordered_map> @@ -91,6 +92,7 @@ static const std::unordered_map> // Reflectivity {"SDR", {"NXB", "NYB", "NZB", "N0B", "NAB", "N1B", "NBB", "N2B", "N3B"}}, {"DR", {"NXQ", "NYQ", "NZQ", "N0Q", "NAQ", "N1Q", "NBQ", "N2Q", "N3Q"}}, + {"TZL", {"TZL"}}, {"TDR", {"TZ0", "TZ1", "TZ2"}}, {"NCR", {"NCR"}}, @@ -184,7 +186,7 @@ static const std::unordered_map static const std::unordered_map> level3CategoryProductList_ { - {Level3ProductCategory::Reflectivity, {"SDR", "DR", "TDR", "NCR"}}, + {Level3ProductCategory::Reflectivity, {"SDR", "DR", "TZL", "TDR", "NCR"}}, {Level3ProductCategory::Velocity, {"SDV", "DV", "TDV"}}, {Level3ProductCategory::StormRelativeVelocity, {"SRM"}}, {Level3ProductCategory::SpectrumWidth, {"SW"}}, diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 1a0effaa..4cb525ae 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -57,7 +57,7 @@ static const std::unordered_map rangeMap_ { {163, 300}, {165, 300}, {166, 230}, {167, 300}, {168, 300}, {169, 230}, {170, 230}, {171, 230}, {172, 230}, {173, 230}, {174, 230}, {175, 230}, {176, 230}, {177, 230}, {178, 300}, {179, 300}, {180, 89}, {181, 89}, - {182, 89}, {184, 89}, {186, 412}, {193, 460}, {195, 460}, {196, 50}}; + {182, 89}, {184, 89}, {186, 417}, {193, 460}, {195, 460}, {196, 50}}; static const std::unordered_map xResolutionMap_ { {19, 1000}, {20, 2000}, {27, 1000}, {30, 1000}, {31, 2000}, {32, 1000}, From 6509fc7043fc7d77885e82182d99fcdcef6ce91b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 12:10:58 -0400 Subject: [PATCH 413/762] Update L3 product category selection to work better for tdwr --- .../scwx/qt/ui/level3_products_widget.cpp | 17 ++++++++++++-- wxdata/include/scwx/common/products.hpp | 5 +++-- wxdata/source/scwx/common/products.cpp | 22 +++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index 98f091ac..18a90313 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -60,7 +60,9 @@ public: categoryButtons_ {}, productTiltMap_ {}, awipsProductMap_ {}, - awipsProductMutex_ {} + awipsProductMutex_ {}, + categoryMap_ {}, + categoryMapMutex_ {} { layout_->setContentsMargins(0, 0, 0, 0); layout_->addWidget(productsWidget_); @@ -183,6 +185,9 @@ public: std::unordered_map awipsProductMap_; std::shared_mutex awipsProductMutex_; + common::Level3ProductCategoryMap categoryMap_; + std::shared_mutex categoryMapMutex_; + std::string currentAwipsId_ {}; QAction* currentProductTiltAction_ {nullptr}; @@ -322,9 +327,11 @@ void Level3ProductsWidgetImpl::SelectProductCategory( { UpdateCategorySelection(category); + std::shared_lock lock {categoryMapMutex_}; + Q_EMIT self_->RadarProductSelected( common::RadarProductGroup::Level3, - common::GetLevel3CategoryDefaultProduct(category), + common::GetLevel3CategoryDefaultProduct(category, categoryMap_), 0); } @@ -333,6 +340,12 @@ void Level3ProductsWidget::UpdateAvailableProducts( { logger_->trace("UpdateAvailableProducts()"); + // Save the category map + { + std::unique_lock lock {p->categoryMapMutex_}; + p->categoryMap_ = updatedCategoryMap; + } + // Iterate through each category tool button std::for_each( p->categoryButtons_.cbegin(), diff --git a/wxdata/include/scwx/common/products.hpp b/wxdata/include/scwx/common/products.hpp index 97d9c324..f4891c6a 100644 --- a/wxdata/include/scwx/common/products.hpp +++ b/wxdata/include/scwx/common/products.hpp @@ -73,8 +73,9 @@ Level2Product GetLevel2Product(const std::string& name); const std::string& GetLevel3CategoryName(Level3ProductCategory category); const std::string& GetLevel3CategoryDescription(Level3ProductCategory category); -const std::string& -GetLevel3CategoryDefaultProduct(Level3ProductCategory category); +std::string +GetLevel3CategoryDefaultProduct(Level3ProductCategory category, + const Level3ProductCategoryMap& categoryMap); Level3ProductCategory GetLevel3Category(const std::string& categoryName); Level3ProductCategory GetLevel3CategoryByProduct(const std::string& productName); diff --git a/wxdata/source/scwx/common/products.cpp b/wxdata/source/scwx/common/products.cpp index 3e7a3fe6..3478b7dd 100644 --- a/wxdata/source/scwx/common/products.cpp +++ b/wxdata/source/scwx/common/products.cpp @@ -298,9 +298,27 @@ const std::string& GetLevel3CategoryDescription(Level3ProductCategory category) return level3CategoryDescription_.at(category); } -const std::string& -GetLevel3CategoryDefaultProduct(Level3ProductCategory category) +std::string +GetLevel3CategoryDefaultProduct(Level3ProductCategory category, + const Level3ProductCategoryMap& categoryMap) { + const auto& productsIt = categoryMap.find(category); + if (productsIt == categoryMap.cend()) + { + return level3CategoryDefaultAwipsId_.at(category); + } + + const auto& productsSiteHas = productsIt->second; + const auto& productList = level3CategoryProductList_.at(category); + for (auto& product : productList) + { + const auto& tiltsIt = productsSiteHas.find(product); + if (tiltsIt != productsSiteHas.cend() && tiltsIt->second.size() > 0) + { + return tiltsIt->second[0]; + } + } + return level3CategoryDefaultAwipsId_.at(category); } From dc72e9fbfc12b723ccab16d8bcf9f2535880cc83 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 13:18:00 -0400 Subject: [PATCH 414/762] clang tidy/format fixes for update_tdwr_products --- scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp | 6 +++--- scwx-qt/source/scwx/qt/view/level3_radial_view.cpp | 8 +++----- wxdata/include/scwx/common/products.hpp | 2 +- wxdata/source/scwx/common/products.cpp | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index 18a90313..e6de32b2 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -186,7 +186,7 @@ public: std::shared_mutex awipsProductMutex_; common::Level3ProductCategoryMap categoryMap_; - std::shared_mutex categoryMapMutex_; + std::shared_mutex categoryMapMutex_; std::string currentAwipsId_ {}; QAction* currentProductTiltAction_ {nullptr}; @@ -327,7 +327,7 @@ void Level3ProductsWidgetImpl::SelectProductCategory( { UpdateCategorySelection(category); - std::shared_lock lock {categoryMapMutex_}; + const std::shared_lock lock {categoryMapMutex_}; Q_EMIT self_->RadarProductSelected( common::RadarProductGroup::Level3, @@ -342,7 +342,7 @@ void Level3ProductsWidget::UpdateAvailableProducts( // Save the category map { - std::unique_lock lock {p->categoryMapMutex_}; + const std::unique_lock lock {p->categoryMapMutex_}; p->categoryMap_ = updatedCategoryMap; } diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 29afba2d..18edefb3 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -45,7 +45,7 @@ public: void ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled, + bool smoothingEnabled, float gateSize); [[nodiscard]] inline std::uint8_t @@ -357,9 +357,7 @@ void Level3RadialView::ComputeSweep() // Compute gate size (number of base gates per bin) const std::uint16_t gateSize = std::max( - 1, - dataMomentInterval / - static_cast(gateLength)); + 1, dataMomentInterval / static_cast(gateLength)); // Compute gate range [startGate, endGate) std::uint16_t startGate = 0; @@ -540,7 +538,7 @@ Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const void Level3RadialView::Impl::ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled, + bool smoothingEnabled, float gateSize) { logger_->debug("ComputeCoordinates()"); diff --git a/wxdata/include/scwx/common/products.hpp b/wxdata/include/scwx/common/products.hpp index f4891c6a..7451aa39 100644 --- a/wxdata/include/scwx/common/products.hpp +++ b/wxdata/include/scwx/common/products.hpp @@ -74,7 +74,7 @@ Level2Product GetLevel2Product(const std::string& name); const std::string& GetLevel3CategoryName(Level3ProductCategory category); const std::string& GetLevel3CategoryDescription(Level3ProductCategory category); std::string -GetLevel3CategoryDefaultProduct(Level3ProductCategory category, +GetLevel3CategoryDefaultProduct(Level3ProductCategory category, const Level3ProductCategoryMap& categoryMap); Level3ProductCategory GetLevel3Category(const std::string& categoryName); Level3ProductCategory diff --git a/wxdata/source/scwx/common/products.cpp b/wxdata/source/scwx/common/products.cpp index 3478b7dd..48969e13 100644 --- a/wxdata/source/scwx/common/products.cpp +++ b/wxdata/source/scwx/common/products.cpp @@ -309,7 +309,7 @@ GetLevel3CategoryDefaultProduct(Level3ProductCategory category, } const auto& productsSiteHas = productsIt->second; - const auto& productList = level3CategoryProductList_.at(category); + const auto& productList = level3CategoryProductList_.at(category); for (auto& product : productList) { const auto& tiltsIt = productsSiteHas.find(product); From befa5f3dedc156fedcd66c7a80467ba4e2c5079d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 14 Mar 2025 13:23:07 -0400 Subject: [PATCH 415/762] Only Allocate cordinates in level3_radial_view when they are needed --- scwx-qt/source/scwx/qt/view/level3_radial_view.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 18edefb3..0b3f42fa 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -39,7 +39,6 @@ public: vcp_ {}, sweepTime_ {} { - coordinates_.resize(kMaxCoordinates_); } ~Impl() { threadPool_.join(); }; @@ -556,6 +555,8 @@ void Level3RadialView::Impl::ComputeCoordinates( // Calculate azimuth coordinates timer.start(); + coordinates_.resize(kMaxCoordinates_); + const std::uint16_t numRadials = radialData->number_of_radials(); const std::uint16_t numRangeBins = radialData->number_of_range_bins(); From 4ab4a8825b43b05c3e7703865eb2bc426046c003 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 19 Mar 2025 12:41:01 -0400 Subject: [PATCH 416/762] Check for product availability on radar site change. --- .../scwx/qt/manager/radar_product_manager.cpp | 8 ++ scwx-qt/source/scwx/qt/map/map_widget.cpp | 112 +++++++++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index edafecf5..48584a2d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -230,6 +230,7 @@ public: const std::string radarId_; bool initialized_; bool level3ProductsInitialized_; + bool level3AvailabilityReady_ {false}; std::shared_ptr radarSite_; std::size_t cacheLimit_ {6u}; @@ -1585,6 +1586,12 @@ void RadarProductManager::UpdateAvailableProducts() if (p->level3ProductsInitialized_) { + if (p->level3AvailabilityReady_) + { + // Multiple maps may use the same manager, so this ensures that all get + // notified of the change + Q_EMIT Level3ProductsChanged(); + } return; } @@ -1660,6 +1667,7 @@ void RadarProductManagerImpl::UpdateAvailableProductsSync() } } + level3AvailabilityReady_ = true; Q_EMIT self_->Level3ProductsChanged(); } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index f2599579..b4882345 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -178,9 +179,11 @@ public: void SelectNearestRadarSite(double latitude, double longitude, std::optional type); - void SetRadarSite(const std::string& radarSite); + void SetRadarSite(const std::string& radarSite, + bool checkProductAvailability = false); void UpdateLoadedStyle(); bool UpdateStoredMapParameters(); + void CheckLevel3Availability(); std::string FindMapSymbologyLayer(); @@ -268,6 +271,10 @@ public: std::set activeHotkeys_ {}; std::chrono::system_clock::time_point prevHotkeyTime_ {}; + bool productAvailabilityCheckNeeded_ {false}; + bool productAvailabilityUpdated_ {false}; + bool productAvailabilityProductSelected_ {false}; + public slots: void Update(); }; @@ -429,6 +436,14 @@ void MapWidgetImpl::ConnectSignals() &manager::HotkeyManager::HotkeyReleased, this, &MapWidgetImpl::HandleHotkeyReleased); + connect(widget_, + &MapWidget::RadarSiteUpdated, + widget_, + [this](const std::shared_ptr&) + { + productAvailabilityProductSelected_ = true; + CheckLevel3Availability(); + }); } void MapWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat) @@ -913,7 +928,7 @@ void MapWidget::SelectRadarSite(std::shared_ptr radarSite, p->map_->setCoordinate( {radarSite->latitude(), radarSite->longitude()}); } - p->SetRadarSite(radarSite->id()); + p->SetRadarSite(radarSite->id(), true); p->Update(); // Select products from new site @@ -1772,7 +1787,12 @@ void MapWidgetImpl::RadarProductManagerConnect() connect(radarProductManager_.get(), &manager::RadarProductManager::Level3ProductsChanged, this, - [this]() { Q_EMIT widget_->Level3ProductsChanged(); }); + [this]() + { + productAvailabilityUpdated_ = true; + CheckLevel3Availability(); + Q_EMIT widget_->Level3ProductsChanged(); + }); connect( radarProductManager_.get(), @@ -1990,7 +2010,8 @@ void MapWidgetImpl::SelectNearestRadarSite(double latitude, } } -void MapWidgetImpl::SetRadarSite(const std::string& radarSite) +void MapWidgetImpl::SetRadarSite(const std::string& radarSite, + bool checkProductAvailability) { // Check if radar site has changed if (radarProductManager_ == nullptr || @@ -2009,6 +2030,12 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) // Connect signals to new RadarProductManager RadarProductManagerConnect(); + // Once the available products are loaded, check to make sure the current + // one is available + productAvailabilityCheckNeeded_ = checkProductAvailability; + productAvailabilityUpdated_ = false; + productAvailabilityProductSelected_ = false; + radarProductManager_->UpdateAvailableProducts(); } } @@ -2053,6 +2080,83 @@ bool MapWidgetImpl::UpdateStoredMapParameters() return changed; } +void MapWidgetImpl::CheckLevel3Availability() +{ + /* + * productAvailabilityCheckNeeded_ Only do this when it is indicated that it + * is needed (mostly on radar site change). This is mainly to avoid potential + * recursion with SelectRadarProduct calls. + * + * productAvailabilityUpdated_ Only update once the product availability + * has been updated + * + * productAvailabilityProductSelected_ Only update once the radar site is + * fully selected, including the current product + */ + if (!(productAvailabilityCheckNeeded_ && productAvailabilityUpdated_ && + productAvailabilityProductSelected_)) + { + return; + } + productAvailabilityCheckNeeded_ = false; + + // Only do this for level3 products + if (widget_->GetRadarProductGroup() != common::RadarProductGroup::Level3) + { + return; + } + + const common::Level3ProductCategoryMap& categoryMap = + widget_->GetAvailableLevel3Categories(); + + const std::string& productTilt = context_->radar_product(); + const std::string& productName = + common::GetLevel3ProductByAwipsId(productTilt); + const common::Level3ProductCategory productCategory = + common::GetLevel3CategoryByProduct(productName); + if (productCategory == common::Level3ProductCategory::Unknown) + { + return; + } + + const auto& availableProductsIt = categoryMap.find(productCategory); + // Has no products in this category, do not change categories + if (availableProductsIt == categoryMap.cend()) + { + return; + } + + const auto& availableProducts = availableProductsIt->second; + const auto& availableTiltsIt = availableProducts.find(productName); + // Does not have the same product, but has others in the same category. + // Switch to the default product and tilt in this category. + if (availableTiltsIt == availableProducts.cend()) + { + widget_->SelectRadarProduct( + common::RadarProductGroup::Level3, + common::GetLevel3CategoryDefaultProduct(productCategory, categoryMap), + 0, + widget_->GetSelectedTime()); + return; + } + + const auto& availableTilts = availableTiltsIt->second; + const auto& tilt = std::ranges::find_if( + availableTilts, + [productTilt](const std::string& tilt) { return productTilt == tilt; }); + // Tilt is not available, set it to first tilt + if (tilt == availableTilts.cend() && availableTilts.size() > 0) + { + widget_->SelectRadarProduct(common::RadarProductGroup::Level3, + availableTilts[0], + 0, + widget_->GetSelectedTime()); + return; + } + + // Tilt is available, no change needed +} + } // namespace map } // namespace qt } // namespace scwx From 50006ada379025ab904a98010489ea9695cf1461 Mon Sep 17 00:00:00 2001 From: aware70 <7832566+aware70@users.noreply.github.com> Date: Sun, 16 Mar 2025 15:45:09 -0500 Subject: [PATCH 417/762] Add map API key test button to initial dialog --- scwx-qt/scwx-qt.cmake | 6 +- .../source/scwx/qt/ui/api_key_edit_widget.cpp | 107 ++++++++++++++++++ .../source/scwx/qt/ui/api_key_edit_widget.hpp | 40 +++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 3 + scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 11 +- .../scwx/qt/ui/setup/map_provider_page.cpp | 35 +++--- 6 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/api_key_edit_widget.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 77024a0d..b080ae66 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -281,7 +281,8 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/serial_port_dialog.hpp source/scwx/qt/ui/settings_dialog.hpp source/scwx/qt/ui/update_dialog.hpp - source/scwx/qt/ui/wfo_dialog.hpp) + source/scwx/qt/ui/wfo_dialog.hpp + source/scwx/qt/ui/api_key_edit_widget.hpp) set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/alert_dialog.cpp source/scwx/qt/ui/alert_dock_widget.cpp @@ -312,7 +313,8 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp source/scwx/qt/ui/serial_port_dialog.cpp source/scwx/qt/ui/update_dialog.cpp - source/scwx/qt/ui/wfo_dialog.cpp) + source/scwx/qt/ui/wfo_dialog.cpp + source/scwx/qt/ui/api_key_edit_widget.cpp) set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/alert_dialog.ui source/scwx/qt/ui/alert_dock_widget.ui diff --git a/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp new file mode 100644 index 00000000..15adf525 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp @@ -0,0 +1,107 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace scwx::qt::ui; + +static const std::string logPrefix_ = "scwx::qt::ui::setup::api_key"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +QApiKeyEdit::QApiKeyEdit(QWidget* parent) : + QLineEdit(parent), networkAccessManager_(new QNetworkAccessManager(this)) +{ + const QIcon icon = + QApplication::style()->standardIcon(QStyle::SP_BrowserReload); + testAction_ = addAction(icon, QLineEdit::TrailingPosition); + testAction_->setIconText(tr("Test Key")); + testAction_->setToolTip(tr("Test the API key for this provider")); + + connect(testAction_, &QAction::triggered, this, &QApiKeyEdit::apiTest); + connect(networkAccessManager_, + &QNetworkAccessManager::finished, + this, + &QApiKeyEdit::apiTestFinished); + + // Reset test icon when text changes + connect(this, + &QLineEdit::textChanged, + this, + [this, icon]() { testAction_->setIcon(icon); }); +} + +void QApiKeyEdit::apiTest() +{ + QNetworkRequest req; + req.setTransferTimeout(5000); + + switch (provider_) + { + case map::MapProvider::Mapbox: + { + QUrl url("https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/1/0/0.mvt"); + logger_->debug("Testing MapProvider::Mapbox API key at {}", + url.toString().toStdString()); + QUrlQuery query; + query.addQueryItem("access_token", text()); + url.setQuery(query); + req.setUrl(url); + break; + } + case map::MapProvider::MapTiler: + { + QUrl url("https://api.maptiler.com/maps/streets-v2/"); + logger_->debug("Testing MapProvider::MapTiler API key at {}", + url.toString().toStdString()); + QUrlQuery query; + query.addQueryItem("key", text()); + url.setQuery(query); + req.setUrl(url); + break; + } + default: + { + logger_->warn("Cannot test MapProvider::Unknown API key"); + break; + } + } + + networkAccessManager_->get(req); +} + +void QApiKeyEdit::apiTestFinished(QNetworkReply* reply) +{ + switch (reply->error()) + { + case QNetworkReply::NoError: + { + logger_->info("QApiKeyEdit: test success"); + QToolTip::showText(mapToGlobal(QPoint()), tr("Key was valid")); + testAction_->setIcon( + QApplication::style()->standardIcon(QStyle::SP_DialogApplyButton)); + Q_EMIT apiTestSucceeded(); + break; + } + default: + { + const char* errStr = + QMetaEnum::fromType().valueToKey( + reply->error()); + logger_->warn("QApiKeyEdit: test failed, got {} from {}", + errStr, + reply->url().host().toStdString()); + QToolTip::showText(mapToGlobal(QPoint()), tr("Invalid key")); + testAction_->setIcon( + QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton)); + Q_EMIT apiTestFailed(reply->error()); + break; + } + } +} diff --git a/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.hpp b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.hpp new file mode 100644 index 00000000..074e0b55 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class QNetworkAccessManager; + +namespace scwx::qt::ui +{ + +class QApiKeyEdit : public QLineEdit +{ + Q_OBJECT + +public: + QApiKeyEdit(QWidget* parent = nullptr); + + map::MapProvider getMapProvider() const { return provider_; } + + void setMapProvider(const map::MapProvider provider) + { + provider_ = provider; + } + +signals: + void apiTestSucceeded(); + void apiTestFailed(QNetworkReply::NetworkError error); + +private slots: + void apiTest(); + void apiTestFinished(QNetworkReply* reply); + +protected: + map::MapProvider provider_ {map::MapProvider::Unknown}; + QNetworkAccessManager* networkAccessManager_ {}; + QAction* testAction_ {}; +}; + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 5491bb21..7ce55037 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -669,11 +669,14 @@ void SettingsDialogImpl::SetupGeneralTab() map::GetMapProviderName); mapProvider_.SetResetButton(self_->ui->resetMapProviderButton); + self_->ui->mapboxApiKeyLineEdit->setMapProvider(map::MapProvider::Mapbox); mapboxApiKey_.SetSettingsVariable(generalSettings.mapbox_api_key()); mapboxApiKey_.SetEditWidget(self_->ui->mapboxApiKeyLineEdit); mapboxApiKey_.SetResetButton(self_->ui->resetMapboxApiKeyButton); mapboxApiKey_.EnableTrimming(); + self_->ui->mapTilerApiKeyLineEdit->setMapProvider( + map::MapProvider::MapTiler); mapTilerApiKey_.SetSettingsVariable(generalSettings.maptiler_api_key()); mapTilerApiKey_.SetEditWidget(self_->ui->mapTilerApiKeyLineEdit); mapTilerApiKey_.SetResetButton(self_->ui->resetMapTilerApiKeyButton); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index be599bff..bb2389bc 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -256,14 +256,14 @@ - + QLineEdit::EchoMode::Password - + QLineEdit::EchoMode::Password @@ -1389,6 +1389,13 @@ + + + scwx::qt::ui::QApiKeyEdit + QLineEdit +
scwx/qt/ui/api_key_edit_widget.hpp
+
+
diff --git a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp index 0e5c3f98..b4d7038a 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -18,13 +19,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace ui -{ -namespace setup +namespace scwx::qt::ui::setup { static const std::unordered_map kUrl_ { @@ -33,8 +28,8 @@ static const std::unordered_map kUrl_ { struct MapProviderGroup { - QLabel* apiKeyLabel_ {}; - QLineEdit* apiKeyEdit_ {}; + QLabel* apiKeyLabel_ {}; + QApiKeyEdit* apiKeyEdit_ {}; }; class MapProviderPage::Impl @@ -45,7 +40,9 @@ public: void SelectMapProvider(const QString& text); void SelectMapProvider(map::MapProvider mapProvider); - void SetupMapProviderGroup(MapProviderGroup& group, int row); + void SetupMapProviderGroup(const map::MapProvider mapProvider, + MapProviderGroup& group, + int row); void SetupSettingsInterface(); static void SetGroupVisible(MapProviderGroup& group, bool visible); @@ -122,8 +119,8 @@ MapProviderPage::MapProviderPage(QWidget* parent) : p->buttonLayout_->addItem(p->buttonSpacer_); p->buttonFrame_->setLayout(p->buttonLayout_); - p->SetupMapProviderGroup(p->mapboxGroup_, 1); - p->SetupMapProviderGroup(p->maptilerGroup_, 2); + p->SetupMapProviderGroup(map::MapProvider::Mapbox, p->mapboxGroup_, 1); + p->SetupMapProviderGroup(map::MapProvider::MapTiler, p->maptilerGroup_, 2); // Overall layout p->layout_ = new QVBoxLayout(this); @@ -159,11 +156,12 @@ MapProviderPage::MapProviderPage(QWidget* parent) : MapProviderPage::~MapProviderPage() = default; -void MapProviderPage::Impl::SetupMapProviderGroup(MapProviderGroup& group, - int row) +void MapProviderPage::Impl::SetupMapProviderGroup( + const map::MapProvider provider, MapProviderGroup& group, int row) { group.apiKeyLabel_ = new QLabel(self_); - group.apiKeyEdit_ = new QLineEdit(self_); + group.apiKeyEdit_ = new QApiKeyEdit(self_); + group.apiKeyEdit_->setMapProvider(provider); group.apiKeyLabel_->setText(tr("API Key")); @@ -171,7 +169,7 @@ void MapProviderPage::Impl::SetupMapProviderGroup(MapProviderGroup& group, mapProviderLayout_->addWidget(group.apiKeyEdit_, row, 1, 1, 1); QObject::connect(group.apiKeyEdit_, - &QLineEdit::textChanged, + &QApiKeyEdit::textChanged, self_, &QWizardPage::completeChanged); } @@ -292,7 +290,4 @@ bool MapProviderPage::IsRequired() return (mapboxApiKey.size() <= 1 && maptilerApiKey.size() <= 1); } -} // namespace setup -} // namespace ui -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::ui::setup From 71bfbcc6ced7e9b897d6494783f6100aa999c3ee Mon Sep 17 00:00:00 2001 From: aware70 <7832566+aware70@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:23:02 -0500 Subject: [PATCH 418/762] Update QApiKeyEdit log prefix --- scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp index 15adf525..73f936b5 100644 --- a/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/api_key_edit_widget.cpp @@ -12,7 +12,7 @@ using namespace scwx::qt::ui; -static const std::string logPrefix_ = "scwx::qt::ui::setup::api_key"; +static const std::string logPrefix_ = "scwx::qt::ui::QApiKeyEdit"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); QApiKeyEdit::QApiKeyEdit(QWidget* parent) : From f00b92274add71f42b7c3ed1468e3cb6a50a779d Mon Sep 17 00:00:00 2001 From: aware70 <7832566+aware70@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:03:35 -0500 Subject: [PATCH 419/762] Keep alphabetical file order in scwx-qt.cmake --- scwx-qt/scwx-qt.cmake | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b080ae66..90b9b56f 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -255,6 +255,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/alert_dialog.hpp source/scwx/qt/ui/alert_dock_widget.hpp source/scwx/qt/ui/animation_dock_widget.hpp + source/scwx/qt/ui/api_key_edit_widget.hpp source/scwx/qt/ui/collapsible_group.hpp source/scwx/qt/ui/county_dialog.hpp source/scwx/qt/ui/download_dialog.hpp @@ -281,12 +282,12 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/serial_port_dialog.hpp source/scwx/qt/ui/settings_dialog.hpp source/scwx/qt/ui/update_dialog.hpp - source/scwx/qt/ui/wfo_dialog.hpp - source/scwx/qt/ui/api_key_edit_widget.hpp) + source/scwx/qt/ui/wfo_dialog.hpp) set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/alert_dialog.cpp source/scwx/qt/ui/alert_dock_widget.cpp source/scwx/qt/ui/animation_dock_widget.cpp + source/scwx/qt/ui/api_key_edit_widget.cpp source/scwx/qt/ui/collapsible_group.cpp source/scwx/qt/ui/county_dialog.cpp source/scwx/qt/ui/download_dialog.cpp @@ -313,8 +314,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/settings_dialog.cpp source/scwx/qt/ui/serial_port_dialog.cpp source/scwx/qt/ui/update_dialog.cpp - source/scwx/qt/ui/wfo_dialog.cpp - source/scwx/qt/ui/api_key_edit_widget.cpp) + source/scwx/qt/ui/wfo_dialog.cpp) set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/alert_dialog.ui source/scwx/qt/ui/alert_dock_widget.ui From eb925c2ca186b68f83f8b67feb2c584af3a363fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:53:05 +0000 Subject: [PATCH 420/762] Update dependency cpr to v1.11.2 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index cbfc9896..031228eb 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,7 @@ import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.87.0", - "cpr/1.11.1", + "cpr/1.11.2", "fontconfig/2.15.0", "freetype/2.13.2", "geographiclib/2.4", From fee00b737a739ade3148ff09b1f74676e8720d4c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 16 Mar 2025 15:52:29 -0400 Subject: [PATCH 421/762] Fix up custom map inputs, making it easier for others to use --- scwx-qt/scwx-qt.cmake | 3 + scwx-qt/source/scwx/qt/main/main_window.cpp | 2 +- .../scwx/qt/settings/general_settings.cpp | 4 + .../scwx/qt/settings/settings_interface.cpp | 154 +++++++++++------- .../scwx/qt/settings/settings_interface.hpp | 8 + .../source/scwx/qt/ui/custom_layer_dialog.cpp | 124 ++++++++++++++ .../source/scwx/qt/ui/custom_layer_dialog.hpp | 38 +++++ .../source/scwx/qt/ui/custom_layer_dialog.ui | 96 +++++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 40 ++++- scwx-qt/source/scwx/qt/ui/settings_dialog.hpp | 4 +- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 17 +- 11 files changed, 425 insertions(+), 65 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 90b9b56f..74864ab7 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -258,6 +258,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/api_key_edit_widget.hpp source/scwx/qt/ui/collapsible_group.hpp source/scwx/qt/ui/county_dialog.hpp + source/scwx/qt/ui/custom_layer_dialog.hpp source/scwx/qt/ui/download_dialog.hpp source/scwx/qt/ui/edit_line_dialog.hpp source/scwx/qt/ui/edit_marker_dialog.hpp @@ -290,6 +291,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/api_key_edit_widget.cpp source/scwx/qt/ui/collapsible_group.cpp source/scwx/qt/ui/county_dialog.cpp + source/scwx/qt/ui/custom_layer_dialog.cpp source/scwx/qt/ui/download_dialog.cpp source/scwx/qt/ui/edit_line_dialog.cpp source/scwx/qt/ui/edit_marker_dialog.cpp @@ -321,6 +323,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.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/custom_layer_dialog.ui source/scwx/qt/ui/edit_line_dialog.ui source/scwx/qt/ui/edit_marker_dialog.ui source/scwx/qt/ui/gps_info_dialog.ui diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index ad504cc0..5b25a91a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -316,7 +316,7 @@ MainWindow::MainWindow(QWidget* parent) : p->layerDialog_ = new ui::LayerDialog(this); // Settings Dialog - p->settingsDialog_ = new ui::SettingsDialog(this); + p->settingsDialog_ = new ui::SettingsDialog(p->settings_, this); // Map Settings p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this); diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index c6e93c76..a445da6d 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -107,6 +107,10 @@ public: SCWX_SETTINGS_ENUM_VALIDATOR(scwx::util::ClockFormat, scwx::util::ClockFormatIterator(), scwx::util::GetClockFormatName)); + + customStyleUrl_.SetValidator( + [](const std::string& value) + { return value.find("key=") == std::string::npos; }); customStyleDrawLayer_.SetValidator([](const std::string& value) { return !value.empty(); }); defaultAlertAction_.SetValidator( diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index ab36ebd9..b2089d08 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace scwx::qt::settings @@ -19,6 +20,9 @@ namespace scwx::qt::settings static const std::string logPrefix_ = "scwx::qt::settings::settings_interface"; +static const QString kValidStyleSheet_ = ""; +static const QString kInvalidStyleSheet_ = "border: 2px solid red;"; + template class SettingsInterface::Impl { @@ -59,6 +63,8 @@ public: bool unitEnabled_ {false}; bool trimmingEnabled_ {false}; + + std::optional invalidTooltip_; }; template @@ -167,20 +173,24 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { if constexpr (std::is_same_v) { - QObject::connect(hotkeyEdit, - &ui::HotkeyEdit::KeySequenceChanged, - p->context_.get(), - [this](const QKeySequence& sequence) - { - std::string value { - sequence.toString().toStdString()}; + QObject::connect( + hotkeyEdit, + &ui::HotkeyEdit::KeySequenceChanged, + p->context_.get(), + [this, hotkeyEdit](const QKeySequence& sequence) + { + const std::string value {sequence.toString().toStdString()}; - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); - // TODO: Display invalid status - }); + hotkeyEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + hotkeyEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } } else if (QLineEdit* lineEdit = dynamic_cast(widget)) @@ -189,53 +199,64 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect(lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this](const QString& text) - { - QString trimmedText = - p->trimmingEnabled_ ? text.trimmed() : text; + QObject::connect( + lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this, lineEdit](const QString& text) + { + const QString trimmedText = + p->trimmingEnabled_ ? text.trimmed() : text; - // Map to value if required - std::string value {trimmedText.toStdString()}; - if (p->mapToValue_ != nullptr) - { - value = p->mapToValue_(value); - } + // Map to value if required + std::string value {trimmedText.toStdString()}; + if (p->mapToValue_ != nullptr) + { + value = p->mapToValue_(value); + } - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); - // TODO: Display invalid status - }); + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } else if constexpr (std::is_same_v) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect(lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this](const QString& text) - { - // Convert to a double - bool ok; - double value = text.toDouble(&ok); - if (ok) - { - // Attempt to stage the value - p->stagedValid_ = - p->variable_->StageValue(value); - p->UpdateResetButton(); - } - else - { - p->stagedValid_ = false; - p->UpdateResetButton(); - } - }); + QObject::connect( + lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this, lineEdit](const QString& text) + { + // Convert to a double + bool ok = false; + const 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(); + } + + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); + }); } else if constexpr (std::is_same_v>) { @@ -245,7 +266,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) lineEdit, &QLineEdit::textEdited, p->context_.get(), - [this](const QString& text) + [this, lineEdit](const QString& text) { // Map to value if required T value {}; @@ -280,7 +301,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - // TODO: Display invalid status + lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -343,7 +368,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) spinBox, &QSpinBox::valueChanged, p->context_.get(), - [this](int i) + [this, spinBox](int i) { const T value = p->variable_->GetValue(); const std::optional staged = p->variable_->GetStaged(); @@ -364,6 +389,12 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + spinBox->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + spinBox->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -389,7 +420,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) doubleSpinBox, &QDoubleSpinBox::valueChanged, p->context_.get(), - [this](double d) + [this, doubleSpinBox](double d) { if (p->unitEnabled_) { @@ -415,6 +446,13 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->UpdateResetButton(); } // Otherwise, don't process an unchanged value + + doubleSpinBox->setStyleSheet( + p->stagedValid_ ? kValidStyleSheet_ : kInvalidStyleSheet_); + doubleSpinBox->setToolTip(p->invalidTooltip_ && + !p->stagedValid_ ? + p->invalidTooltip_->c_str() : + ""); }); } } @@ -500,6 +538,12 @@ void SettingsInterface::EnableTrimming(bool trimmingEnabled) p->trimmingEnabled_ = trimmingEnabled; } +template +void SettingsInterface::SetInvalidTooltip(std::optional tooltip) +{ + p->invalidTooltip_ = std::move(tooltip); +} + template template void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 3d7d9d85..0e42aca7 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -130,6 +131,13 @@ public: */ void EnableTrimming(bool trimmingEnabled = true); + /** + * Set a tooltip to be displayed when an invalid input is given. + * + * @param tooltip the tooltip to be displayed + */ + void SetInvalidTooltip(std::optional tooltip); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp new file mode 100644 index 00000000..0bea7ffc --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp @@ -0,0 +1,124 @@ +#include "custom_layer_dialog.hpp" +#include "ui_custom_layer_dialog.h" + +#include +#include +#include +#include +#include + +namespace scwx::qt::ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::custom_layer_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class CustomLayerDialogImpl +{ +public: + explicit CustomLayerDialogImpl(CustomLayerDialog* self, + QMapLibre::Settings settings) : + self_(self), settings_(std::move(settings)) + { + } + + ~CustomLayerDialogImpl() = default; + CustomLayerDialogImpl(const CustomLayerDialogImpl&) = delete; + CustomLayerDialogImpl(CustomLayerDialogImpl&&) = delete; + CustomLayerDialogImpl& operator=(const CustomLayerDialogImpl&) = delete; + CustomLayerDialogImpl& operator=(CustomLayerDialogImpl&&) = delete; + + void handle_mapChanged(QMapLibre::Map::MapChange change); + + CustomLayerDialog* self_; + + QMapLibre::Settings settings_; + std::shared_ptr map_; +}; + +// TODO Duplicated form map_widget, Should probably be moved. +static bool match_layer(const std::string& pattern, const std::string& layer) +{ + // Perform case-insensitive matching + const RE2 re {"(?i)" + pattern}; + if (re.ok()) + { + return RE2::FullMatch(layer, re); + } + else + { + // Fall back to basic comparison if RE + // doesn't compile + return layer == pattern; + } +} + +void CustomLayerDialogImpl::handle_mapChanged(QMapLibre::Map::MapChange change) +{ + if (change == QMapLibre::Map::MapChange::MapChangeDidFinishLoadingStyle) + { + auto& generalSettings = settings::GeneralSettings::Instance(); + const std::string& customStyleDrawLayer = + generalSettings.custom_style_draw_layer().GetValue(); + + const QStringList layerIds = map_->layerIds(); + self_->ui->layerListWidget->clear(); + self_->ui->layerListWidget->addItems(layerIds); + + for (int i = 0; i < self_->ui->layerListWidget->count(); i++) + { + auto* item = self_->ui->layerListWidget->item(i); + + if (match_layer(customStyleDrawLayer, item->text().toStdString())) + { + self_->ui->layerListWidget->setCurrentItem(item); + } + } + } +} + +CustomLayerDialog::CustomLayerDialog(const QMapLibre::Settings& settings, + QWidget* parent) : + QDialog(parent), + p {std::make_unique(this, settings)}, + ui(new Ui::CustomLayerDialog) +{ + ui->setupUi(this); + + auto& generalSettings = settings::GeneralSettings::Instance(); + const auto& customStyleUrl = generalSettings.custom_style_url().GetValue(); + const auto mapProvider = + map::GetMapProvider(generalSettings.map_provider().GetValue()); + + // TODO render the map with a layer to show what they are selecting + p->map_ = std::make_shared( + nullptr, p->settings_, QSize(1, 1), devicePixelRatioF()); + + QString qUrl = QString::fromStdString(customStyleUrl); + + if (mapProvider == map::MapProvider::MapTiler) + { + qUrl.append("?key="); + qUrl.append(map::GetMapProviderApiKey(mapProvider)); + } + + p->map_->setStyleUrl(qUrl); + + QObject::connect(p->map_.get(), + &QMapLibre::Map::mapChanged, + this, + [this](QMapLibre::Map::MapChange change) + { p->handle_mapChanged(change); }); +} + +CustomLayerDialog::~CustomLayerDialog() +{ + delete ui; +} + +std::string CustomLayerDialog::selected_layer() +{ + return ui->layerListWidget->currentItem()->text().toStdString(); +} + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp new file mode 100644 index 00000000..8c03cf71 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace Ui +{ +class CustomLayerDialog; +} + +namespace scwx::qt::ui +{ + +class CustomLayerDialogImpl; + +class CustomLayerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CustomLayerDialog(const QMapLibre::Settings& settings, + QWidget* parent = nullptr); + ~CustomLayerDialog() override; + CustomLayerDialog(const CustomLayerDialog&) = delete; + CustomLayerDialog(CustomLayerDialog&&) = delete; + CustomLayerDialog& operator=(const CustomLayerDialog&) = delete; + CustomLayerDialog& operator=(CustomLayerDialog&&) = delete; + + std::string selected_layer(); + +private: + friend class CustomLayerDialogImpl; + std::unique_ptr p; + Ui::CustomLayerDialog* ui; +}; + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui new file mode 100644 index 00000000..2ce59321 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.ui @@ -0,0 +1,96 @@ + + + CustomLayerDialog + + + + 0 + 0 + 308 + 300 + + + + + 0 + 0 + + + + Custom Map Style Draw Layer + + + + + + + + + 0 + 0 + + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + true + + + + + + + + + + 0 + 0 + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + CustomLayerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CustomLayerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 7ce55037..bf5c9180 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ #include #include #include +#include namespace scwx { @@ -106,7 +108,8 @@ static const std::unordered_map class SettingsDialogImpl { public: - explicit SettingsDialogImpl(SettingsDialog* self) : + explicit SettingsDialogImpl(SettingsDialog* self, + QMapLibre::Settings mapSettings) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, alertAudioRadarSiteDialog_ {new RadarSiteDialog(self)}, @@ -114,6 +117,7 @@ public: countyDialog_ {new CountyDialog(self)}, wfoDialog_ {new WFODialog(self)}, fontDialog_ {new QFontDialog(self)}, + mapSettings_ {std::move(mapSettings)}, fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { &defaultRadarSite_, @@ -219,6 +223,8 @@ public: WFODialog* wfoDialog_; QFontDialog* fontDialog_; + QMapLibre::Settings mapSettings_; + QStandardItemModel* fontCategoryModel_; types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; @@ -292,9 +298,10 @@ public: std::vector settings_; }; -SettingsDialog::SettingsDialog(QWidget* parent) : +SettingsDialog::SettingsDialog(const QMapLibre::Settings& mapSettings, + QWidget* parent) : QDialog(parent), - p {std::make_unique(this)}, + p {std::make_unique(this, mapSettings)}, ui(new Ui::SettingsDialog) { ui->setupUi(this); @@ -685,12 +692,39 @@ void SettingsDialogImpl::SetupGeneralTab() customStyleUrl_.SetSettingsVariable(generalSettings.custom_style_url()); customStyleUrl_.SetEditWidget(self_->ui->customMapUrlLineEdit); customStyleUrl_.SetResetButton(self_->ui->resetCustomMapUrlButton); + customStyleUrl_.SetInvalidTooltip( + "Remove anything following \"?key=\" in the URL"); customStyleUrl_.EnableTrimming(); customStyleDrawLayer_.SetSettingsVariable( generalSettings.custom_style_draw_layer()); customStyleDrawLayer_.SetEditWidget(self_->ui->customMapLayerLineEdit); customStyleDrawLayer_.SetResetButton(self_->ui->resetCustomMapLayerButton); + QObject::connect( + self_->ui->customMapLayerToolButton, + &QAbstractButton::clicked, + self_, + [this]() + { + // WA_DeleteOnClose manages memory + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto* customLayerDialog = new ui::CustomLayerDialog(mapSettings_); + customLayerDialog->setAttribute(Qt::WA_DeleteOnClose); + QObject::connect( + customLayerDialog, + &QDialog::accepted, + self_, + [this, customLayerDialog]() + { + auto newLayer = customLayerDialog->selected_layer(); + self_->ui->customMapLayerLineEdit->setText(newLayer.c_str()); + // setText does not emit the textEdited signal + Q_EMIT + self_->ui->customMapLayerLineEdit->textEdited(newLayer.c_str()); + }); + + customLayerDialog->open(); + }); defaultAlertAction_.SetSettingsVariable( generalSettings.default_alert_action()); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp index 82f00905..b767aa0b 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Ui { @@ -24,7 +25,8 @@ private: Q_DISABLE_COPY(SettingsDialog) public: - explicit SettingsDialog(QWidget* parent = nullptr); + explicit SettingsDialog(const QMapLibre::Settings& mapSettings, + QWidget* parent = nullptr); ~SettingsDialog(); private: diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index bb2389bc..f14c6a96 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 @@ - 3 + 0 @@ -135,9 +135,9 @@ 0 - -303 + -260 511 - 733 + 812 @@ -608,6 +608,13 @@
+ + + + ... + + +
@@ -714,8 +721,8 @@ 0 0 - 98 - 28 + 80 + 18 From a7c6be2bab0ccd290ab15e43573551eefccb0df9 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 25 Mar 2025 10:22:12 -0400 Subject: [PATCH 422/762] Move code from map_widget and custom_layer_dialog to util/maplibre --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 42 +------------------ .../source/scwx/qt/ui/custom_layer_dialog.cpp | 36 +++++----------- scwx-qt/source/scwx/qt/util/maplibre.cpp | 40 ++++++++++++++++++ scwx-qt/source/scwx/qt/util/maplibre.hpp | 12 ++++++ 4 files changed, 64 insertions(+), 66 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index b4882345..ebea2b21 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -185,8 +185,6 @@ public: bool UpdateStoredMapParameters(); void CheckLevel3Availability(); - std::string FindMapSymbologyLayer(); - common::Level2Product GetLevel2ProductOrDefault(const std::string& productName) const; @@ -1146,43 +1144,6 @@ void MapWidget::DumpLayerList() const logger_->info("Layers: {}", p->map_->layerIds().join(", ").toStdString()); } -std::string MapWidgetImpl::FindMapSymbologyLayer() -{ - std::string before = "ferry"; - - for (const QString& qlayer : styleLayers_) - { - const std::string layer = qlayer.toStdString(); - - // Draw below layers defined in map style - auto it = std::find_if(currentStyle_->drawBelow_.cbegin(), - currentStyle_->drawBelow_.cend(), - [&layer](const std::string& styleLayer) -> bool - { - // Perform case-insensitive matching - RE2 re {"(?i)" + styleLayer}; - if (re.ok()) - { - return RE2::FullMatch(layer, re); - } - else - { - // Fall back to basic comparison if RE - // doesn't compile - return layer == styleLayer; - } - }); - - if (it != currentStyle_->drawBelow_.cend()) - { - before = layer; - break; - } - } - - return before; -} - void MapWidgetImpl::AddLayers() { if (styleLayers_.isEmpty()) @@ -1218,7 +1179,8 @@ void MapWidgetImpl::AddLayers() { // Subsequent layers are drawn underneath the map symbology layer case types::MapLayer::MapUnderlay: - before = FindMapSymbologyLayer(); + before = util::maplibre::FindMapSymbologyLayer( + styleLayers_, currentStyle_->drawBelow_); break; // Subsequent layers are drawn after all style-defined layers diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp index 0bea7ffc..b297ab22 100644 --- a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp @@ -1,10 +1,11 @@ #include "custom_layer_dialog.hpp" #include "ui_custom_layer_dialog.h" -#include #include +#include #include #include + #include namespace scwx::qt::ui @@ -36,43 +37,26 @@ public: std::shared_ptr map_; }; -// TODO Duplicated form map_widget, Should probably be moved. -static bool match_layer(const std::string& pattern, const std::string& layer) -{ - // Perform case-insensitive matching - const RE2 re {"(?i)" + pattern}; - if (re.ok()) - { - return RE2::FullMatch(layer, re); - } - else - { - // Fall back to basic comparison if RE - // doesn't compile - return layer == pattern; - } -} - void CustomLayerDialogImpl::handle_mapChanged(QMapLibre::Map::MapChange change) { if (change == QMapLibre::Map::MapChange::MapChangeDidFinishLoadingStyle) { auto& generalSettings = settings::GeneralSettings::Instance(); const std::string& customStyleDrawLayer = - generalSettings.custom_style_draw_layer().GetValue(); + generalSettings.custom_style_draw_layer().GetStagedOrValue(); const QStringList layerIds = map_->layerIds(); self_->ui->layerListWidget->clear(); self_->ui->layerListWidget->addItems(layerIds); - for (int i = 0; i < self_->ui->layerListWidget->count(); i++) - { - auto* item = self_->ui->layerListWidget->item(i); + std::string symbologyLayer = util::maplibre::FindMapSymbologyLayer( + layerIds, {customStyleDrawLayer}); - if (match_layer(customStyleDrawLayer, item->text().toStdString())) - { - self_->ui->layerListWidget->setCurrentItem(item); - } + const auto& symbologyItems = self_->ui->layerListWidget->findItems( + symbologyLayer.c_str(), Qt::MatchExactly); + if (!symbologyItems.isEmpty()) + { + self_->ui->layerListWidget->setCurrentItem(symbologyItems.first()); } } } diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 63f5112e..88ae4cce 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -1,7 +1,9 @@ #include #include +#include #include +#include namespace scwx { @@ -120,6 +122,44 @@ void SetMapStyleUrl(const std::shared_ptr& mapContext, } } +std::string FindMapSymbologyLayer(const QStringList& styleLayers, + const std::vector& drawBelow) +{ + std::string before = "ferry"; + + for (const QString& qlayer : styleLayers) + { + const std::string layer = qlayer.toStdString(); + + // Draw below layers defined in map style + auto it = + std::ranges::find_if(drawBelow, + [&layer](const std::string& styleLayer) -> bool + { + // Perform case-insensitive matching + RE2 re {"(?i)" + styleLayer}; + if (re.ok()) + { + return RE2::FullMatch(layer, re); + } + else + { + // Fall back to basic comparison if RE + // doesn't compile + return layer == styleLayer; + } + }); + + if (it != drawBelow.cend()) + { + before = layer; + break; + } + } + + return before; +} + } // namespace maplibre } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/maplibre.hpp b/scwx-qt/source/scwx/qt/util/maplibre.hpp index 7c2eb58b..c03be113 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.hpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.hpp @@ -37,6 +37,18 @@ glm::vec2 LatLongToScreenCoordinate(const QMapLibre::Coordinate& coordinate); void SetMapStyleUrl(const std::shared_ptr& mapContext, const std::string& url); +/** + * @brief Find the first layer which should be drawn above the radar products + * + * @param [in] styleLayers The layers of the style + * @param [in] drawBelow A list of RE2 compatible regex's describing the layers + * to draw below + * + * @return The first layer to be drawn above the radar products + */ +std::string FindMapSymbologyLayer(const QStringList& styleLayers, + const std::vector& drawBelow); + } // namespace maplibre } // namespace util } // namespace qt From f5ab6f3ef7e76723b4fb0a05c890d85dd1db716d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 25 Mar 2025 10:35:01 -0400 Subject: [PATCH 423/762] clang tidy/format fixes for add_custom_layer_dialog --- scwx-qt/source/scwx/qt/settings/settings_interface.cpp | 3 ++- scwx-qt/source/scwx/qt/settings/settings_interface.hpp | 2 +- scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp | 2 +- scwx-qt/source/scwx/qt/util/maplibre.cpp | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index b2089d08..3fbeccf3 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -539,7 +539,8 @@ void SettingsInterface::EnableTrimming(bool trimmingEnabled) } template -void SettingsInterface::SetInvalidTooltip(std::optional tooltip) +void SettingsInterface::SetInvalidTooltip( + const std::optional& tooltip) { p->invalidTooltip_ = std::move(tooltip); } diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index 0e42aca7..6bf586a7 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -136,7 +136,7 @@ public: * * @param tooltip the tooltip to be displayed */ - void SetInvalidTooltip(std::optional tooltip); + void SetInvalidTooltip(const std::optional& tooltip); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp index b297ab22..d7c963f6 100644 --- a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.cpp @@ -49,7 +49,7 @@ void CustomLayerDialogImpl::handle_mapChanged(QMapLibre::Map::MapChange change) self_->ui->layerListWidget->clear(); self_->ui->layerListWidget->addItems(layerIds); - std::string symbologyLayer = util::maplibre::FindMapSymbologyLayer( + const std::string symbologyLayer = util::maplibre::FindMapSymbologyLayer( layerIds, {customStyleDrawLayer}); const auto& symbologyItems = self_->ui->layerListWidget->findItems( diff --git a/scwx-qt/source/scwx/qt/util/maplibre.cpp b/scwx-qt/source/scwx/qt/util/maplibre.cpp index 88ae4cce..6a4c7d55 100644 --- a/scwx-qt/source/scwx/qt/util/maplibre.cpp +++ b/scwx-qt/source/scwx/qt/util/maplibre.cpp @@ -137,7 +137,7 @@ std::string FindMapSymbologyLayer(const QStringList& styleLayers, [&layer](const std::string& styleLayer) -> bool { // Perform case-insensitive matching - RE2 re {"(?i)" + styleLayer}; + const RE2 re {"(?i)" + styleLayer}; if (re.ok()) { return RE2::FullMatch(layer, re); From 7d2635608df297c52aa3929b5f7c46cd86577835 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 28 Mar 2025 11:28:11 -0400 Subject: [PATCH 424/762] Fixes to add_custom_layer_dialog suggested on GitHub --- .../scwx/qt/settings/settings_interface.cpp | 155 ++++++++---------- .../source/scwx/qt/ui/custom_layer_dialog.hpp | 5 +- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 12 +- scwx-qt/source/scwx/qt/ui/settings_dialog.hpp | 4 +- 4 files changed, 77 insertions(+), 99 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 3fbeccf3..e2c280b2 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -44,6 +44,7 @@ public: void UpdateEditWidget(); void UpdateResetButton(); void UpdateUnitLabel(); + void UpdateValidityDisplay(); SettingsInterface* self_; @@ -173,24 +174,19 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { if constexpr (std::is_same_v) { - QObject::connect( - hotkeyEdit, - &ui::HotkeyEdit::KeySequenceChanged, - p->context_.get(), - [this, hotkeyEdit](const QKeySequence& sequence) - { - const std::string value {sequence.toString().toStdString()}; + QObject::connect(hotkeyEdit, + &ui::HotkeyEdit::KeySequenceChanged, + p->context_.get(), + [this](const QKeySequence& sequence) + { + const std::string value { + sequence.toString().toStdString()}; - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); - - hotkeyEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : - kInvalidStyleSheet_); - hotkeyEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); - }); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); + p->UpdateValidityDisplay(); + }); } } else if (QLineEdit* lineEdit = dynamic_cast(widget)) @@ -199,64 +195,54 @@ void SettingsInterface::SetEditWidget(QWidget* widget) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect( - lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this, lineEdit](const QString& text) - { - const QString trimmedText = - p->trimmingEnabled_ ? text.trimmed() : text; + QObject::connect(lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this](const QString& text) + { + const QString trimmedText = + p->trimmingEnabled_ ? text.trimmed() : text; - // Map to value if required - std::string value {trimmedText.toStdString()}; - if (p->mapToValue_ != nullptr) - { - value = p->mapToValue_(value); - } + // Map to value if required + std::string value {trimmedText.toStdString()}; + if (p->mapToValue_ != nullptr) + { + value = p->mapToValue_(value); + } - // Attempt to stage the value - p->stagedValid_ = p->variable_->StageValue(value); - p->UpdateResetButton(); - - lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : - kInvalidStyleSheet_); - lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); - }); + // Attempt to stage the value + p->stagedValid_ = p->variable_->StageValue(value); + p->UpdateResetButton(); + p->UpdateValidityDisplay(); + }); } else if constexpr (std::is_same_v) { // If the line is edited (not programatically changed), stage the new // value - QObject::connect( - lineEdit, - &QLineEdit::textEdited, - p->context_.get(), - [this, lineEdit](const QString& text) - { - // Convert to a double - bool ok = false; - const 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(); - } + QObject::connect(lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this](const QString& text) + { + // Convert to a double + bool ok = false; + const 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(); + } - lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : - kInvalidStyleSheet_); - lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); - }); + p->UpdateValidityDisplay(); + }); } else if constexpr (std::is_same_v>) { @@ -266,7 +252,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) lineEdit, &QLineEdit::textEdited, p->context_.get(), - [this, lineEdit](const QString& text) + [this](const QString& text) { // Map to value if required T value {}; @@ -300,12 +286,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) // Attempt to stage the value p->stagedValid_ = p->variable_->StageValue(value); p->UpdateResetButton(); - - lineEdit->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : - kInvalidStyleSheet_); - lineEdit->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); + p->UpdateValidityDisplay(); }); } } @@ -368,7 +349,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) spinBox, &QSpinBox::valueChanged, p->context_.get(), - [this, spinBox](int i) + [this](int i) { const T value = p->variable_->GetValue(); const std::optional staged = p->variable_->GetStaged(); @@ -390,11 +371,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) } // Otherwise, don't process an unchanged value - spinBox->setStyleSheet(p->stagedValid_ ? kValidStyleSheet_ : - kInvalidStyleSheet_); - spinBox->setToolTip(p->invalidTooltip_ && !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); + p->UpdateValidityDisplay(); }); } } @@ -420,7 +397,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) doubleSpinBox, &QDoubleSpinBox::valueChanged, p->context_.get(), - [this, doubleSpinBox](double d) + [this](double d) { if (p->unitEnabled_) { @@ -447,12 +424,7 @@ void SettingsInterface::SetEditWidget(QWidget* widget) } // Otherwise, don't process an unchanged value - doubleSpinBox->setStyleSheet( - p->stagedValid_ ? kValidStyleSheet_ : kInvalidStyleSheet_); - doubleSpinBox->setToolTip(p->invalidTooltip_ && - !p->stagedValid_ ? - p->invalidTooltip_->c_str() : - ""); + p->UpdateValidityDisplay(); }); } } @@ -662,6 +634,15 @@ void SettingsInterface::Impl::UpdateUnitLabel() unitLabel_->setText(QString::fromStdString(unitAbbreviation_.value_or(""))); } +template +void SettingsInterface::Impl::UpdateValidityDisplay() +{ + editWidget_->setStyleSheet(stagedValid_ ? kValidStyleSheet_ : + kInvalidStyleSheet_); + editWidget_->setToolTip( + invalidTooltip_ && !stagedValid_ ? invalidTooltip_->c_str() : ""); +} + template void SettingsInterface::Impl::UpdateResetButton() { diff --git a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp index 8c03cf71..ad0a2526 100644 --- a/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/custom_layer_dialog.hpp @@ -17,15 +17,12 @@ class CustomLayerDialogImpl; class CustomLayerDialog : public QDialog { Q_OBJECT + Q_DISABLE_COPY_MOVE(CustomLayerDialog) public: explicit CustomLayerDialog(const QMapLibre::Settings& settings, QWidget* parent = nullptr); ~CustomLayerDialog() override; - CustomLayerDialog(const CustomLayerDialog&) = delete; - CustomLayerDialog(CustomLayerDialog&&) = delete; - CustomLayerDialog& operator=(const CustomLayerDialog&) = delete; - CustomLayerDialog& operator=(CustomLayerDialog&&) = delete; std::string selected_layer(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index bf5c9180..c1e6d501 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -108,8 +108,8 @@ static const std::unordered_map class SettingsDialogImpl { public: - explicit SettingsDialogImpl(SettingsDialog* self, - QMapLibre::Settings mapSettings) : + explicit SettingsDialogImpl(SettingsDialog* self, + QMapLibre::Settings& mapSettings) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, alertAudioRadarSiteDialog_ {new RadarSiteDialog(self)}, @@ -117,7 +117,7 @@ public: countyDialog_ {new CountyDialog(self)}, wfoDialog_ {new WFODialog(self)}, fontDialog_ {new QFontDialog(self)}, - mapSettings_ {std::move(mapSettings)}, + mapSettings_ {mapSettings}, fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { &defaultRadarSite_, @@ -223,7 +223,7 @@ public: WFODialog* wfoDialog_; QFontDialog* fontDialog_; - QMapLibre::Settings mapSettings_; + QMapLibre::Settings& mapSettings_; QStandardItemModel* fontCategoryModel_; @@ -298,8 +298,8 @@ public: std::vector settings_; }; -SettingsDialog::SettingsDialog(const QMapLibre::Settings& mapSettings, - QWidget* parent) : +SettingsDialog::SettingsDialog(QMapLibre::Settings& mapSettings, + QWidget* parent) : QDialog(parent), p {std::make_unique(this, mapSettings)}, ui(new Ui::SettingsDialog) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp index b767aa0b..866e1ea8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.hpp @@ -25,8 +25,8 @@ private: Q_DISABLE_COPY(SettingsDialog) public: - explicit SettingsDialog(const QMapLibre::Settings& mapSettings, - QWidget* parent = nullptr); + explicit SettingsDialog(QMapLibre::Settings& mapSettings, + QWidget* parent = nullptr); ~SettingsDialog(); private: From 7380dbe21ae9e3a344a832e8de041549a847da96 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 3 Apr 2025 10:05:11 -0400 Subject: [PATCH 425/762] Update asw-sdk-cpp and hsluv-c to work with cmake 4.0.0 --- external/aws-sdk-cpp | 2 +- external/hsluv-c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/aws-sdk-cpp b/external/aws-sdk-cpp index 8e41fd05..9c95a05a 160000 --- a/external/aws-sdk-cpp +++ b/external/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 8e41fd058ce06a3f7b67d657a7f644f7a38af509 +Subproject commit 9c95a05a5a99718965c033c6e1fc3f8bb34a1b83 diff --git a/external/hsluv-c b/external/hsluv-c index 59539e04..982217c6 160000 --- a/external/hsluv-c +++ b/external/hsluv-c @@ -1 +1 @@ -Subproject commit 59539e04a6fa648935cbe57c2104041f23136c4a +Subproject commit 982217c65a9ff574302335177d2dc078d9bfa6f5 From 7fdf25f1e7b1eb3246bb390c0d69473f2d95076e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 2 Apr 2025 10:15:59 -0400 Subject: [PATCH 426/762] Disable centering on radar site on click --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ebea2b21..869dd939 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1269,7 +1269,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, &RadarSiteLayer::RadarSiteSelected, this, [this](const std::string& id) - { widget_->RadarSiteRequested(id); }); + { widget_->RadarSiteRequested(id, false); }); break; // Create the location marker layer From 6b2f3dd84f068007859f3b4b10c5faa609d8dbdb Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 2 Apr 2025 11:20:46 -0400 Subject: [PATCH 427/762] Add setting for centering radar on site selection --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 15 ++++++++++----- .../source/scwx/qt/settings/general_settings.cpp | 11 ++++++++++- .../source/scwx/qt/settings/general_settings.hpp | 3 ++- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 7 +++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 11 +++++++++-- test/data | 2 +- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 869dd939..ed288a40 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1265,11 +1265,16 @@ void MapWidgetImpl::AddLayer(types::LayerType type, case types::InformationLayer::RadarSite: radarSiteLayer_ = std::make_shared(context_); AddLayer(layerName, radarSiteLayer_, before); - connect(radarSiteLayer_.get(), - &RadarSiteLayer::RadarSiteSelected, - this, - [this](const std::string& id) - { widget_->RadarSiteRequested(id, false); }); + connect( + radarSiteLayer_.get(), + &RadarSiteLayer::RadarSiteSelected, + this, + [this](const std::string& id) + { + auto& generalSettings = settings::GeneralSettings::Instance(); + widget_->RadarSiteRequested( + id, generalSettings.center_on_radar_selection().GetValue()); + }); break; // Create the location marker layer diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index a445da6d..c0d7ec39 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -47,6 +47,7 @@ public: // SetDefault, SetMinimum, and SetMaximum are descriptive // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) antiAliasingEnabled_.SetDefault(true); + centerOnRadarSelection_.SetDefault(false); clockFormat_.SetDefault(defaultClockFormatValue); customStyleDrawLayer_.SetDefault(".*\\.annotations\\.points"); debugEnabled_.SetDefault(false); @@ -148,7 +149,8 @@ public: Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; - SettingsVariable antiAliasingEnabled_ {"anti_aliasing_enabled"}; + SettingsVariable antiAliasingEnabled_ {"anti_aliasing_enabled"}; + SettingsVariable centerOnRadarSelection_ {"center_on_radar_selection"}; SettingsVariable clockFormat_ {"clock_format"}; SettingsVariable customStyleDrawLayer_ { "custom_style_draw_layer"}; @@ -189,6 +191,7 @@ GeneralSettings::GeneralSettings() : SettingsCategory("general"), p(std::make_unique()) { RegisterVariables({&p->antiAliasingEnabled_, + &p->centerOnRadarSelection_, &p->clockFormat_, &p->customStyleDrawLayer_, &p->customStyleUrl_, @@ -233,6 +236,11 @@ SettingsVariable& GeneralSettings::anti_aliasing_enabled() const return p->antiAliasingEnabled_; } +SettingsVariable& GeneralSettings::center_on_radar_selection() const +{ + return p->centerOnRadarSelection_; +} + SettingsVariable& GeneralSettings::clock_format() const { return p->clockFormat_; @@ -413,6 +421,7 @@ GeneralSettings& GeneralSettings::Instance() bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) { return (lhs.p->antiAliasingEnabled_ == rhs.p->antiAliasingEnabled_ && + lhs.p->centerOnRadarSelection_ == rhs.p->centerOnRadarSelection_ && lhs.p->clockFormat_ == rhs.p->clockFormat_ && lhs.p->customStyleDrawLayer_ == rhs.p->customStyleDrawLayer_ && lhs.p->customStyleUrl_ == rhs.p->customStyleUrl_ && diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index ad49761b..3484461b 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -21,7 +21,8 @@ public: GeneralSettings(GeneralSettings&&) noexcept; GeneralSettings& operator=(GeneralSettings&&) noexcept; - [[nodiscard]] SettingsVariable& anti_aliasing_enabled() const; + [[nodiscard]] SettingsVariable& anti_aliasing_enabled() const; + [[nodiscard]] SettingsVariable& center_on_radar_selection() const; [[nodiscard]] SettingsVariable& clock_format() const; [[nodiscard]] SettingsVariable& custom_style_draw_layer() const; [[nodiscard]] SettingsVariable& custom_style_url() const; diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index c1e6d501..b3a9a381 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -139,6 +139,7 @@ public: &warningsProvider_, &radarSiteThreshold_, &antiAliasingEnabled_, + ¢erOnRadarSelection_, &showMapAttribution_, &showMapCenter_, &showMapLogo_, @@ -258,6 +259,7 @@ public: settings::SettingsInterface warningsProvider_ {}; settings::SettingsInterface radarSiteThreshold_ {}; settings::SettingsInterface antiAliasingEnabled_ {}; + settings::SettingsInterface centerOnRadarSelection_ {}; settings::SettingsInterface showMapAttribution_ {}; settings::SettingsInterface showMapCenter_ {}; settings::SettingsInterface showMapLogo_ {}; @@ -813,6 +815,11 @@ void SettingsDialogImpl::SetupGeneralTab() generalSettings.anti_aliasing_enabled()); antiAliasingEnabled_.SetEditWidget(self_->ui->antiAliasingEnabledCheckBox); + centerOnRadarSelection_.SetSettingsVariable( + generalSettings.center_on_radar_selection()); + centerOnRadarSelection_.SetEditWidget( + self_->ui->centerOnRadarSelectionCheckBox); + showMapAttribution_.SetSettingsVariable( generalSettings.show_map_attribution()); showMapAttribution_.SetEditWidget(self_->ui->showMapAttributionCheckBox); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index f14c6a96..3205e553 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - -260 + -412 511 - 812 + 841 @@ -625,6 +625,13 @@ + + + + Center Map on Radar Selection + + + diff --git a/test/data b/test/data index 1f3e1259..6a76030a 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 1f3e1259130a5eb4a6df37d721fe6c8301213e7e +Subproject commit 6a76030a4c3864cb387033d10ce3de88b292828d From 9c16a88b639c1638f7dfb6ddfd413d7b943599cc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Apr 2025 19:46:05 -0400 Subject: [PATCH 428/762] Use data role to get marker id for location marker dialog. --- scwx-qt/source/scwx/qt/model/marker_model.cpp | 5 +++++ .../scwx/qt/ui/marker_settings_widget.cpp | 21 +++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp index 77fb7ab7..6c232177 100644 --- a/scwx-qt/source/scwx/qt/model/marker_model.cpp +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -100,6 +100,11 @@ QVariant MarkerModel::data(const QModelIndex& index, int role) const return QVariant(); } + if (role == Qt::ItemDataRole::UserRole) + { + return qulonglong(id); + } + switch(index.column()) { case static_cast(Column::Name): diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index b3ba8440..f3e81098 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -85,13 +85,14 @@ void MarkerSettingsWidgetImpl::ConnectSignals() ->selectedRows(static_cast( model::MarkerModel::Column::Name)) .first(); - std::optional id = markerModel_->getId(selected.row()); - if (!id) + + QVariant id = proxyModel_->data(selected, Qt::ItemDataRole::UserRole); + if (!id.isValid()) { return; } - markerManager_->remove_marker(*id); + markerManager_->remove_marker(id.toULongLong()); }); QObject::connect( self_->ui->markerView->selectionModel(), @@ -116,20 +117,14 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this](const QModelIndex& index) { - const int row = index.row(); - if (row < 0) + QVariant id = + proxyModel_->data(index, Qt::ItemDataRole::UserRole); + if (!id.isValid()) { return; } - std::optional id = - markerModel_->getId(row); - if (!id) - { - return; - } - - editMarkerDialog_->setup(*id); + editMarkerDialog_->setup(id.toULongLong()); editMarkerDialog_->show(); }); } From c07e2bca53806b86665a0e70239845cb32e0019c Mon Sep 17 00:00:00 2001 From: Aden Koperczak <38887432+AdenKoperczak@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:58:13 -0400 Subject: [PATCH 429/762] Update scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp with clang-tidy suggestions Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index f3e81098..ef55b990 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -86,7 +86,7 @@ void MarkerSettingsWidgetImpl::ConnectSignals() model::MarkerModel::Column::Name)) .first(); - QVariant id = proxyModel_->data(selected, Qt::ItemDataRole::UserRole); + QVariant const id = proxyModel_->data(selected, Qt::ItemDataRole::UserRole); if (!id.isValid()) { return; From 2ecc49ddb37ed429a84e8bc2a6ff1c3bff94e3b5 Mon Sep 17 00:00:00 2001 From: Aden Koperczak <38887432+AdenKoperczak@users.noreply.github.com> Date: Fri, 4 Apr 2025 19:58:23 -0400 Subject: [PATCH 430/762] Update scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp with clang-tidy suggestions Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index ef55b990..d160e21c 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -117,7 +117,7 @@ void MarkerSettingsWidgetImpl::ConnectSignals() self_, [this](const QModelIndex& index) { - QVariant id = + QVariant const id = proxyModel_->data(index, Qt::ItemDataRole::UserRole); if (!id.isValid()) { From 529291d2a222d6bad933320e1f73f141d82c5c8f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Apr 2025 20:01:21 -0400 Subject: [PATCH 431/762] Clang format changes for radar_self_center_setting --- scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index d160e21c..51d9e40a 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -86,7 +86,8 @@ void MarkerSettingsWidgetImpl::ConnectSignals() model::MarkerModel::Column::Name)) .first(); - QVariant const id = proxyModel_->data(selected, Qt::ItemDataRole::UserRole); + QVariant const id = + proxyModel_->data(selected, Qt::ItemDataRole::UserRole); if (!id.isValid()) { return; From ae8eb3e05eddd65aca26161ec78ba0a9e1b56669 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 21 Mar 2025 23:01:58 -0500 Subject: [PATCH 432/762] Increase maximum loop time to 2 days --- scwx-qt/source/scwx/qt/settings/general_settings.cpp | 2 +- scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index c0d7ec39..4f19dfcc 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -92,7 +92,7 @@ public: loopSpeed_.SetMinimum(1.0); loopSpeed_.SetMaximum(99.99); loopTime_.SetMinimum(1); - loopTime_.SetMaximum(1440); + loopTime_.SetMaximum(2880); nmeaBaudRate_.SetMinimum(1); nmeaBaudRate_.SetMaximum(999999999); radarSiteThreshold_.SetMinimum(-10000); diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui index 1c79eb48..d5cfe3aa 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui @@ -150,7 +150,7 @@ 1 - 1440 + 2880 30 From 77716e8f0b0d6b12ac0a6b64ad9bd6e229edea1f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 07:21:01 -0500 Subject: [PATCH 433/762] Updating tests for maximum loop time --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index 6a76030a..f03a46f3 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 6a76030a4c3864cb387033d10ce3de88b292828d +Subproject commit f03a46f31b0378ca4077372c106d6967bb7ad66f From 1ff686629ba7cd603792335106bf73a023619cbd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Apr 2025 17:36:23 -0400 Subject: [PATCH 434/762] Add setting for cursor icon scale --- scwx-qt/res/textures/images/dot.svg | 9 + scwx-qt/scwx-qt.qrc | 1 + .../scwx/qt/manager/resource_manager.cpp | 11 +- .../scwx/qt/manager/resource_manager.hpp | 8 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 84 ++- .../scwx/qt/settings/general_settings.cpp | 15 +- .../scwx/qt/settings/general_settings.hpp | 1 + .../source/scwx/qt/types/texture_types.cpp | 2 +- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 6 + scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 605 ++++++++++-------- scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 18 +- scwx-qt/source/scwx/qt/util/texture_atlas.hpp | 4 +- test/data | 2 +- 13 files changed, 439 insertions(+), 327 deletions(-) create mode 100644 scwx-qt/res/textures/images/dot.svg diff --git a/scwx-qt/res/textures/images/dot.svg b/scwx-qt/res/textures/images/dot.svg new file mode 100644 index 00000000..8f765035 --- /dev/null +++ b/scwx-qt/res/textures/images/dot.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index e7d8315a..ccb2e42a 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -91,6 +91,7 @@ res/textures/images/cursor-17.png res/textures/images/crosshairs-24.png res/textures/images/dot-3.png + res/textures/images/dot.svg res/textures/images/location-marker.svg res/textures/images/mapbox-logo.svg res/textures/images/maptiler-logo.svg diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index c6c40a26..41558378 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -43,10 +43,17 @@ void Initialize() void Shutdown() {} std::shared_ptr -LoadImageResource(const std::string& urlString) +LoadImageResource(const std::string& urlString, double scale) { util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); - return textureAtlas.CacheTexture(urlString, urlString); + return textureAtlas.CacheTexture(urlString, urlString, scale); +} + +std::shared_ptr LoadImageResource( + const std::string& urlString, const std::string& textureName, double scale) +{ + util::TextureAtlas& textureAtlas = util::TextureAtlas::Instance(); + return textureAtlas.CacheTexture(textureName, urlString, scale); } std::vector> diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp index ec7cf65e..cf0aad03 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.hpp @@ -19,9 +19,13 @@ void Initialize(); void Shutdown(); std::shared_ptr -LoadImageResource(const std::string& urlString); +LoadImageResource(const std::string& urlString, double scale = 1); +std::shared_ptr +LoadImageResource(const std::string& urlString, + const std::string& textureName, + double scale = 1); std::vector> -LoadImageResources(const std::vector& urlStrings); + LoadImageResources(const std::vector& urlStrings); void BuildAtlas(); } // namespace ResourceManager diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 3522ba40..c4f93ef3 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -1,10 +1,11 @@ -#include #include #include #include #include #include +#include #include +#include #include #include #include @@ -88,6 +89,9 @@ public: showMapLogoCallbackUuid_); } + void SetupGeoIcons(); + void SetCusorLocation(common::Coordinate coordinate); + OverlayLayer* self_; boost::uuids::uuid clockFormatCallbackUuid_; @@ -115,11 +119,13 @@ public: types::GetTextureName(types::ImageTexture::CardinalPoint24)}; const std::string& compassIconName_ { types::GetTextureName(types::ImageTexture::Compass24)}; - const std::string& cursorIconName_ { + std::string cursorIconName_ { types::GetTextureName(types::ImageTexture::Dot3)}; const std::string& mapCenterIconName_ { types::GetTextureName(types::ImageTexture::Cursor17)}; + std::shared_ptr cursorIconImage_ {nullptr}; + const std::string& mapboxLogoImageName_ { types::GetTextureName(types::ImageTexture::MapboxLogo)}; const std::string& mapTilerLogoImageName_ { @@ -137,6 +143,8 @@ public: float lastFontSize_ {0.0f}; QMargins lastColorTableMargins_ {}; + double cursorScale_ {1}; + std::string sweepTimeString_ {}; bool sweepTimeNeedsUpdate_ {true}; bool sweepTimePicked_ {false}; @@ -156,6 +164,45 @@ OverlayLayer::OverlayLayer(std::shared_ptr context) : OverlayLayer::~OverlayLayer() = default; +void OverlayLayerImpl::SetCusorLocation(common::Coordinate coordinate) +{ + const double offset = 3 * cursorScale_ / 2; + geoIcons_->SetIconLocation(cursorIcon_, + coordinate.latitude_, + coordinate.longitude_, + -offset, + offset); +} + +void OverlayLayerImpl::SetupGeoIcons() +{ + const std::string& texturePath = + types::GetTexturePath(types::ImageTexture::Dot3); + cursorIconName_ = fmt::format( + "{}x{}", types::GetTextureName(types::ImageTexture::Dot3), cursorScale_); + cursorIconImage_ = manager::ResourceManager::LoadImageResource( + texturePath, cursorIconName_, cursorScale_); + + auto coordinate = currentPosition_.coordinate(); + geoIcons_->StartIconSheets(); + geoIcons_->AddIconSheet(cursorIconName_); + geoIcons_->AddIconSheet(locationIconName_); + geoIcons_->FinishIconSheets(); + + geoIcons_->StartIcons(); + + cursorIcon_ = geoIcons_->AddIcon(); + geoIcons_->SetIconTexture(cursorIcon_, cursorIconName_, 0); + + locationIcon_ = geoIcons_->AddIcon(); + geoIcons_->SetIconTexture(locationIcon_, locationIconName_, 0); + geoIcons_->SetIconAngle(locationIcon_, units::angle::degrees {45.0}); + geoIcons_->SetIconLocation( + locationIcon_, coordinate.latitude(), coordinate.longitude()); + + geoIcons_->FinishIcons(); +} + void OverlayLayer::Initialize() { logger_->debug("Initialize()"); @@ -173,27 +220,18 @@ void OverlayLayer::Initialize() } p->currentPosition_ = p->positionManager_->position(); - auto coordinate = p->currentPosition_.coordinate(); // Geo Icons - p->geoIcons_->StartIconSheets(); - p->geoIcons_->AddIconSheet(p->cursorIconName_); - p->geoIcons_->AddIconSheet(p->locationIconName_); - p->geoIcons_->FinishIconSheets(); - - p->geoIcons_->StartIcons(); - - p->cursorIcon_ = p->geoIcons_->AddIcon(); - p->geoIcons_->SetIconTexture(p->cursorIcon_, p->cursorIconName_, 0); - - p->locationIcon_ = p->geoIcons_->AddIcon(); - p->geoIcons_->SetIconTexture(p->locationIcon_, p->locationIconName_, 0); - p->geoIcons_->SetIconAngle(p->locationIcon_, - units::angle::degrees {45.0}); - p->geoIcons_->SetIconLocation( - p->locationIcon_, coordinate.latitude(), coordinate.longitude()); - - p->geoIcons_->FinishIcons(); + auto& generalSettings = settings::GeneralSettings::Instance(); + p->cursorScale_ = generalSettings.cursor_icon_scale().GetValue(); + p->SetupGeoIcons(); + generalSettings.cursor_icon_scale().RegisterValueChangedCallback( + [this](double value) + { + p->cursorScale_ = value; + p->SetupGeoIcons(); + Q_EMIT NeedsRendering(); + }); // Icons p->icons_->StartIconSheets(); @@ -339,9 +377,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->geoIcons_->SetIconVisible(p->cursorIcon_, cursorIconVisible); if (cursorIconVisible) { - common::Coordinate mouseCoordinate = context()->mouse_coordinate(); - p->geoIcons_->SetIconLocation( - p->cursorIcon_, mouseCoordinate.latitude_, mouseCoordinate.longitude_); + p->SetCusorLocation(context()->mouse_coordinate()); } // Location Icon diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.cpp b/scwx-qt/source/scwx/qt/settings/general_settings.cpp index 4f19dfcc..86ac3d6d 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.cpp @@ -78,7 +78,10 @@ public: cursorIconAlwaysOn_.SetDefault(false); radarSiteThreshold_.SetDefault(0.0); highPrivilegeWarningEnabled_.SetDefault(true); + cursorIconScale_.SetDefault(1.0); + cursorIconScale_.SetMinimum(1.0); + cursorIconScale_.SetMaximum(5.0); fontSizes_.SetElementMinimum(1); fontSizes_.SetElementMaximum(72); fontSizes_.SetValidator([](const std::vector& value) @@ -185,6 +188,7 @@ public: SettingsVariable radarSiteThreshold_ {"radar_site_threshold"}; SettingsVariable highPrivilegeWarningEnabled_ { "high_privilege_warning_enabled"}; + SettingsVariable cursorIconScale_ {"cursor_icon_scale"}; }; GeneralSettings::GeneralSettings() : @@ -222,7 +226,8 @@ GeneralSettings::GeneralSettings() : &p->warningsProvider_, &p->cursorIconAlwaysOn_, &p->radarSiteThreshold_, - &p->highPrivilegeWarningEnabled_}); + &p->highPrivilegeWarningEnabled_, + &p->cursorIconScale_}); SetDefaults(); } GeneralSettings::~GeneralSettings() = default; @@ -397,6 +402,11 @@ SettingsVariable& GeneralSettings::high_privilege_warning_enabled() const return p->highPrivilegeWarningEnabled_; } +SettingsVariable& GeneralSettings::cursor_icon_scale() const +{ + return p->cursorIconScale_; +} + bool GeneralSettings::Shutdown() { bool dataChanged = false; @@ -455,7 +465,8 @@ bool operator==(const GeneralSettings& lhs, const GeneralSettings& rhs) lhs.p->cursorIconAlwaysOn_ == rhs.p->cursorIconAlwaysOn_ && lhs.p->radarSiteThreshold_ == rhs.p->radarSiteThreshold_ && lhs.p->highPrivilegeWarningEnabled_ == - rhs.p->highPrivilegeWarningEnabled_); + rhs.p->highPrivilegeWarningEnabled_ && + lhs.p->cursorIconScale_ == rhs.p->cursorIconScale_); } } // namespace scwx::qt::settings diff --git a/scwx-qt/source/scwx/qt/settings/general_settings.hpp b/scwx-qt/source/scwx/qt/settings/general_settings.hpp index 3484461b..59f29275 100644 --- a/scwx-qt/source/scwx/qt/settings/general_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/general_settings.hpp @@ -56,6 +56,7 @@ public: [[nodiscard]] SettingsVariable& cursor_icon_always_on() const; [[nodiscard]] SettingsVariable& radar_site_threshold() const; [[nodiscard]] SettingsVariable& high_privilege_warning_enabled() const; + [[nodiscard]] SettingsVariable& cursor_icon_scale() const; static GeneralSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/types/texture_types.cpp b/scwx-qt/source/scwx/qt/types/texture_types.cpp index 18efd9b9..2369dc34 100644 --- a/scwx-qt/source/scwx/qt/types/texture_types.cpp +++ b/scwx-qt/source/scwx/qt/types/texture_types.cpp @@ -24,7 +24,7 @@ static const std::unordered_map imageTextureInfo_ { {"images/crosshairs-24", ":/res/textures/images/crosshairs-24.png"}}, {ImageTexture::Cursor17, {"images/cursor-17", ":/res/textures/images/cursor-17.png"}}, - {ImageTexture::Dot3, {"images/dot-3", ":/res/textures/images/dot-3.png"}}, + {ImageTexture::Dot3, {"images/dot-3", ":/res/textures/images/dot.svg"}}, {ImageTexture::LocationBriefcase, {"images/location-briefcase", ":/res/icons/font-awesome-6/briefcase-solid.svg"}}, diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index b3a9a381..fa891795 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -146,6 +146,7 @@ public: &showSmoothedRangeFolding_, &updateNotificationsEnabled_, &cursorIconAlwaysOn_, + &cursorIconScale_, &debugEnabled_, &alertAudioSoundFile_, &alertAudioLocationMethod_, @@ -266,6 +267,7 @@ public: settings::SettingsInterface showSmoothedRangeFolding_ {}; settings::SettingsInterface updateNotificationsEnabled_ {}; settings::SettingsInterface cursorIconAlwaysOn_ {}; + settings::SettingsInterface cursorIconScale_ {}; settings::SettingsInterface debugEnabled_ {}; std::unordered_map> @@ -811,6 +813,10 @@ void SettingsDialogImpl::SetupGeneralTab() radarSiteThresholdUpdateUnits( settings::UnitSettings::Instance().distance_units().GetValue()); + cursorIconScale_.SetSettingsVariable(generalSettings.cursor_icon_scale()); + cursorIconScale_.SetEditWidget(self_->ui->cursorIconScaleSpinBox); + cursorIconScale_.SetResetButton(self_->ui->resetCursorIconScaleButton); + antiAliasingEnabled_.SetSettingsVariable( generalSettings.anti_aliasing_enabled()); antiAliasingEnabled_.SetEditWidget(self_->ui->antiAliasingEnabledCheckBox); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 3205e553..5b9b37fd 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -137,7 +137,7 @@ 0 -412 511 - 841 + 873 @@ -159,31 +159,33 @@ 0 - - + + + + + + + + + + + - Grid Width + Radar Site Threshold - - - - - + + - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + Custom Map URL - - + + - ... + Warnings Provider @@ -198,18 +200,25 @@ - - - - - + + - Mapbox API Key + GPS Source - - + + + + + + + MapTiler API Key + + + + + ... @@ -219,8 +228,54 @@ - - + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Default Radar Site + + + + + + + Custom Map Layer + + + + + + + + + + Mapbox API Key + + + + + ... @@ -237,166 +292,12 @@ - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Default Time Zone - - - - - - - QLineEdit::EchoMode::Password - - - - - - - QLineEdit::EchoMode::Password - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Theme File - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - Default Alert Action - - - - - - - MapTiler API Key - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - - - - - GPS Source - - - - - - - - - - Custom Map URL - - - - - - - 1 - - - 999999999 - - - - - - - - - @@ -419,20 +320,38 @@ - - - - - - - Map Provider + + + + 1 + + + 999999999 - - + + - Theme + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Theme File + + + + + + + Grid Width @@ -447,22 +366,8 @@ - - - - Warnings Provider - - - - - - - Default Radar Site - - - - - + + ... @@ -472,58 +377,6 @@ - - - - GPS Baud Rate - - - - - - - Custom Map Layer - - - - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - ... - - - @@ -531,8 +384,11 @@ - - + + + + + ... @@ -542,31 +398,70 @@ - - + + - GPS Plugin + ... + + + + Default Time Zone + + + + + + + + + - - - - Radar Site Threshold - - - - - + + ... - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + Map Provider + + + + + + + QLineEdit::EchoMode::Password + + + + + + + + + + Theme + + + + + + + + + + + + + @@ -598,21 +493,163 @@ - - - - - + + - + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - + + ... + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + QLineEdit::EchoMode::Password + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + GPS Baud Rate + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Default Alert Action + + + + + + + + + + ... + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + GPS Plugin + + + + + + + ... + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Multi-Pane Cursor Size + + + + + + + 1 + + + 1.000000000000000 + + + 5.000000000000000 + + + 0.100000000000000 + + + QAbstractSpinBox::StepType::DefaultStepType + + + + + + + ... + + + + :/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/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index fe2b2a9a..c069bfb3 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -50,12 +50,12 @@ public: ~Impl() {} static std::shared_ptr - LoadImage(const std::string& imagePath); + LoadImage(const std::string& imagePath, double scale = 1); static std::shared_ptr ReadPngFile(const QString& imagePath); static std::shared_ptr - ReadSvgFile(const QString& imagePath); + ReadSvgFile(const QString& imagePath, double scale = 1); std::vector> registeredTextures_ {}; @@ -92,12 +92,12 @@ void TextureAtlas::RegisterTexture(const std::string& name, p->registeredTextures_.emplace_back(std::move(image)); } -std::shared_ptr -TextureAtlas::CacheTexture(const std::string& name, const std::string& path) +std::shared_ptr TextureAtlas::CacheTexture( + const std::string& name, const std::string& path, double scale) { // Attempt to load the image std::shared_ptr image = - TextureAtlas::Impl::LoadImage(path); + TextureAtlas::Impl::LoadImage(path, scale); // If the image is valid if (image != nullptr && image->width() > 0 && image->height() > 0) @@ -380,7 +380,7 @@ TextureAttributes TextureAtlas::GetTextureAttributes(const std::string& name) } std::shared_ptr -TextureAtlas::Impl::LoadImage(const std::string& imagePath) +TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale) { logger_->debug("Loading image: {}", imagePath); @@ -398,7 +398,7 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath) if (suffix == "svg") { - image = ReadSvgFile(qLocalImagePath); + image = ReadSvgFile(qLocalImagePath, scale); } else { @@ -509,10 +509,10 @@ TextureAtlas::Impl::ReadPngFile(const QString& imagePath) } std::shared_ptr -TextureAtlas::Impl::ReadSvgFile(const QString& imagePath) +TextureAtlas::Impl::ReadSvgFile(const QString& imagePath, double scale) { QSvgRenderer renderer {imagePath}; - QPixmap pixmap {renderer.defaultSize()}; + QPixmap pixmap {renderer.defaultSize() * scale}; pixmap.fill(Qt::GlobalColor::transparent); QPainter painter {&pixmap}; diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp index 64c5a2d7..64e23b43 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp @@ -74,8 +74,8 @@ public: std::uint64_t BuildCount() const; void RegisterTexture(const std::string& name, const std::string& path); - std::shared_ptr - CacheTexture(const std::string& name, const std::string& path); + std::shared_ptr CacheTexture( + const std::string& name, const std::string& path, double scale = 1); void BuildAtlas(std::size_t width, std::size_t height); void BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture); diff --git a/test/data b/test/data index f03a46f3..6115c159 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit f03a46f31b0378ca4077372c106d6967bb7ad66f +Subproject commit 6115c15987fd75dd019db995e6bdc07a05b83dcc From 1c23718654c8f13b2bca6ade6343dca908d4dc16 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Apr 2025 18:57:52 -0400 Subject: [PATCH 435/762] Fix rendering on setting change, and location of icon --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index c4f93ef3..a72c2089 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -166,12 +166,8 @@ OverlayLayer::~OverlayLayer() = default; void OverlayLayerImpl::SetCusorLocation(common::Coordinate coordinate) { - const double offset = 3 * cursorScale_ / 2; - geoIcons_->SetIconLocation(cursorIcon_, - coordinate.latitude_, - coordinate.longitude_, - -offset, - offset); + geoIcons_->SetIconLocation( + cursorIcon_, coordinate.latitude_, coordinate.longitude_); } void OverlayLayerImpl::SetupGeoIcons() @@ -182,6 +178,7 @@ void OverlayLayerImpl::SetupGeoIcons() "{}x{}", types::GetTextureName(types::ImageTexture::Dot3), cursorScale_); cursorIconImage_ = manager::ResourceManager::LoadImageResource( texturePath, cursorIconName_, cursorScale_); + manager::ResourceManager::BuildAtlas(); auto coordinate = currentPosition_.coordinate(); geoIcons_->StartIconSheets(); From 260b7c73a4c3204a526c2d67a32798d01908a85b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Apr 2025 19:03:22 -0400 Subject: [PATCH 436/762] Remove unneeded rotation of dot --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index a72c2089..27bf3de1 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -193,7 +193,6 @@ void OverlayLayerImpl::SetupGeoIcons() locationIcon_ = geoIcons_->AddIcon(); geoIcons_->SetIconTexture(locationIcon_, locationIconName_, 0); - geoIcons_->SetIconAngle(locationIcon_, units::angle::degrees {45.0}); geoIcons_->SetIconLocation( locationIcon_, coordinate.latitude(), coordinate.longitude()); From fe61f31c4034401fbc58f48316d4fa5b80523fb8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 7 Apr 2025 10:15:28 -0400 Subject: [PATCH 437/762] Fix signal usage and possible race condition --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 30 +++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 27bf3de1..90fcb2ed 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -44,7 +44,8 @@ public: activeBoxOuter_ {std::make_shared(context)}, activeBoxInner_ {std::make_shared(context)}, geoIcons_ {std::make_shared(context)}, - icons_ {std::make_shared(context)} + icons_ {std::make_shared(context)}, + renderMutex_ {} { auto& generalSettings = settings::GeneralSettings::Instance(); @@ -143,7 +144,10 @@ public: float lastFontSize_ {0.0f}; QMargins lastColorTableMargins_ {}; - double cursorScale_ {1}; + double cursorScale_ {1}; + boost::signals2::scoped_connection cursorScaleConnection_; + + std::mutex renderMutex_; std::string sweepTimeString_ {}; bool sweepTimeNeedsUpdate_ {true}; @@ -172,6 +176,11 @@ void OverlayLayerImpl::SetCusorLocation(common::Coordinate coordinate) void OverlayLayerImpl::SetupGeoIcons() { + const std::unique_lock lock {renderMutex_}; + + auto& generalSettings = settings::GeneralSettings::Instance(); + cursorScale_ = generalSettings.cursor_icon_scale().GetValue(); + const std::string& texturePath = types::GetTexturePath(types::ImageTexture::Dot3); cursorIconName_ = fmt::format( @@ -219,15 +228,14 @@ void OverlayLayer::Initialize() // Geo Icons auto& generalSettings = settings::GeneralSettings::Instance(); - p->cursorScale_ = generalSettings.cursor_icon_scale().GetValue(); p->SetupGeoIcons(); - generalSettings.cursor_icon_scale().RegisterValueChangedCallback( - [this](double value) - { - p->cursorScale_ = value; - p->SetupGeoIcons(); - Q_EMIT NeedsRendering(); - }); + p->cursorScaleConnection_ = + generalSettings.cursor_icon_scale().changed_signal().connect( + [this]() + { + p->SetupGeoIcons(); + Q_EMIT NeedsRendering(); + }); // Icons p->icons_->StartIconSheets(); @@ -322,6 +330,8 @@ void OverlayLayer::Initialize() void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { + const std::unique_lock lock {p->renderMutex_}; + gl::OpenGLFunctions& gl = context()->gl(); auto radarProductView = context()->radar_product_view(); auto& settings = context()->settings(); From c64cd459532410e3eb3e8fdbff5fab9e594bf8b1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 12:29:06 -0400 Subject: [PATCH 438/762] Disconnect cursor scale connection before anything overlay layer is destroid to avoid race conditions --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 90fcb2ed..36258e75 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -166,7 +166,10 @@ OverlayLayer::OverlayLayer(std::shared_ptr context) : p->activeBoxOuter_->SetPosition(0.0f, 0.0f); } -OverlayLayer::~OverlayLayer() = default; +OverlayLayer::~OverlayLayer() +{ + p->cursorScaleConnection_.disconnect(); +} void OverlayLayerImpl::SetCusorLocation(common::Coordinate coordinate) { From da6fe3a6fb04ff555b213f06e979e2ef2cbdb6f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:55:06 +0000 Subject: [PATCH 439/762] Update ZedThree/clang-tidy-review action to v0.21.0 --- .github/workflows/clang-tidy-comments.yml | 2 +- .github/workflows/clang-tidy-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clang-tidy-comments.yml b/.github/workflows/clang-tidy-comments.yml index 7328430f..9a6df52d 100644 --- a/.github/workflows/clang-tidy-comments.yml +++ b/.github/workflows/clang-tidy-comments.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Post Comments - uses: ZedThree/clang-tidy-review/post@v0.20.1 + uses: ZedThree/clang-tidy-review/post@v0.21.0 with: lgtm_comment_body: '' annotations: false diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index f8c4495e..62e26976 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -138,7 +138,7 @@ jobs: rsync -avzh --ignore-missing-args clang_fixes.json ../ - name: Upload Review - uses: ZedThree/clang-tidy-review/upload@v0.20.1 + uses: ZedThree/clang-tidy-review/upload@v0.21.0 - name: Status Check if: steps.review.outputs.total_comments > 0 From 58e2058efcfba242a2d0891fc2765a057f994117 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 8 Apr 2025 20:52:58 -0500 Subject: [PATCH 440/762] Update ci.yml to use jdpurcell install qt action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6682484..bd6f7bf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: submodules: recursive - name: Install Qt - uses: dpaulat/install-qt-action@b45c67aaa9e0ea77e59a7031ec14a12d5ddf4b35 + uses: jdpurcell/install-qt-action@v5 with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} From bdb73b645ccc0cefab0a729ab9904f9bf435ad69 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 11 Apr 2025 11:45:12 -0400 Subject: [PATCH 441/762] Switch to repo owned by Aden Koperczak, with some fixes --- external/qt6ct | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/qt6ct b/external/qt6ct index 55dba870..2c569c6c 160000 --- a/external/qt6ct +++ b/external/qt6ct @@ -1 +1 @@ -Subproject commit 55dba8704c0a748b0ce9f2d3cc2cf200ca3db464 +Subproject commit 2c569c6c4776ea5a1299030c079b16f70473c9e6 From c29c0e82dd2f177e8aae7a81cea2de3ea51e8e92 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 11 Apr 2025 12:35:23 -0400 Subject: [PATCH 442/762] Add theme editor dialog --- external/qt6ct.cmake | 27 +++++++++-- scwx-qt/scwx-qt.cmake | 1 + scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 45 ++++++++++++------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake index 9c1c177a..665b5368 100644 --- a/external/qt6ct.cmake +++ b/external/qt6ct.cmake @@ -2,10 +2,10 @@ cmake_minimum_required(VERSION 3.16.0) set(PROJECT_NAME scwx-qt6ct) find_package(QT NAMES Qt6 - COMPONENTS Gui + COMPONENTS Gui Widgets REQUIRED) find_package(Qt${QT_VERSION_MAJOR} - COMPONENTS Gui + COMPONENTS Gui Widgets REQUIRED) #extract version from qt6ct.h @@ -24,20 +24,39 @@ else() message(FATAL_ERROR "invalid header") endif() -set(app_SRCS +set(qt6ct-common-source qt6ct/src/qt6ct-common/qt6ct.cpp ) -add_library(qt6ct-common STATIC ${app_SRCS}) +set(qt6ct-widgets-source + qt6ct/src/qt6ct/paletteeditdialog.cpp + qt6ct/src/qt6ct/paletteeditdialog.ui +) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +include_directories(qt6ct/src/qt6ct-common) + +add_library(qt6ct-common STATIC ${qt6ct-common-source}) set_target_properties(qt6ct-common PROPERTIES VERSION ${QT6CT_VERSION}) target_link_libraries(qt6ct-common PRIVATE Qt6::Gui) target_compile_definitions(qt6ct-common PRIVATE QT6CT_LIBRARY) +add_library(qt6ct-widgets STATIC ${qt6ct-widgets-source}) +set_target_properties(qt6ct-widgets PROPERTIES VERSION ${QT6CT_VERSION}) +target_link_libraries(qt6ct-widgets PRIVATE Qt6::Widgets Qt6::WidgetsPrivate qt6ct-common) +target_compile_definitions(qt6ct-widgets PRIVATE QT6CT_LIBRARY) + if (MSVC) # Produce PDB file for debug target_compile_options(qt6ct-common PRIVATE "$<$:/Zi>") + target_compile_options(qt6ct-widgets PRIVATE "$<$:/Zi>") else() target_compile_options(qt6ct-common PRIVATE "$<$:-g>") + target_compile_options(qt6ct-widgets PRIVATE "$<$:-g>") endif() target_include_directories( qt6ct-common INTERFACE qt6ct/src ) +target_include_directories( qt6ct-widgets INTERFACE qt6ct/src ) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 74864ab7..4c0ea7cd 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -703,6 +703,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets glm::glm imgui qt6ct-common + qt6ct-widgets SQLite::SQLite3 wxdata) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index fa891795..a339a79e 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -48,6 +48,11 @@ #include #include +#define QT6CT_LIBRARY +#include +#include +#undef QT6CT_LIBRARY + namespace scwx { namespace qt @@ -578,31 +583,39 @@ void SettingsDialogImpl::SetupGeneralTab() self_, [this]() { - static const std::string themeFilter = "Qt6Ct Theme File (*.conf)"; - static const std::string allFilter = "All Files (*)"; + const settings::GeneralSettings& generalSettings = + settings::GeneralSettings::Instance(); + const QString file = + generalSettings.theme_file().GetStagedOrValue().c_str(); + const QPalette palette = + Qt6CT::loadColorScheme(file, QApplication::palette()); + QStyle* style = QApplication::style(); - QFileDialog* dialog = new QFileDialog(self_); - - dialog->setFileMode(QFileDialog::ExistingFile); - - dialog->setNameFilters( - {QObject::tr(themeFilter.c_str()), QObject::tr(allFilter.c_str())}); + // WA_DeleteOnClose manages memory + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto* dialog = new PaletteEditDialog(palette, style, self_); dialog->setAttribute(Qt::WA_DeleteOnClose); QObject::connect( dialog, - &QFileDialog::fileSelected, + &QDialog::accepted, self_, - [this](const QString& file) + [dialog]() { - QString path = QDir::toNativeSeparators(file); - logger_->info("Selected theme file: {}", path.toStdString()); - self_->ui->themeFileLineEdit->setText(path); + const QPalette palette = dialog->selectedPalette(); + const settings::GeneralSettings& generalSettings = + settings::GeneralSettings::Instance(); + const QString file = + generalSettings.theme_file().GetStagedOrValue().c_str(); + Qt6CT::createColorScheme(file, palette); - // setText does not emit the textEdited signal - Q_EMIT self_->ui->themeFileLineEdit->textEdited(path); + auto uiStyle = scwx::qt::types::GetUiStyle( + generalSettings.theme().GetValue()); + if (uiStyle == scwx::qt::types::UiStyle::FusionCustom) + { + QApplication::setPalette(palette); + } }); - dialog->open(); }); From 6afcf1312f14835933e6cd38ad3ed6fa009b67fd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 11 Apr 2025 12:56:40 -0400 Subject: [PATCH 443/762] Make external/qt6ct track the repository owned by Aden Koperczak --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index afccf304..dc9749dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -39,4 +39,4 @@ url = https://github.com/dpaulat/maplibre-gl-native.git [submodule "external/qt6ct"] path = external/qt6ct - url = https://github.com/trialuser02/qt6ct.git + url = https://github.com/AdenKoperczak/qt6ct.git From 15f906ad5a14422198cf853ded0e00805cc26a8a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 11 Apr 2025 13:11:06 -0400 Subject: [PATCH 444/762] Fill in default path when no path is given for theme file --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index a339a79e..4f63743f 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include @@ -585,8 +586,18 @@ void SettingsDialogImpl::SetupGeneralTab() { const settings::GeneralSettings& generalSettings = settings::GeneralSettings::Instance(); - const QString file = - generalSettings.theme_file().GetStagedOrValue().c_str(); + QString file = generalSettings.theme_file().GetStagedOrValue().c_str(); + + if (file.isEmpty()) + { + const QString appDataPath {QStandardPaths::writableLocation( + QStandardPaths::AppLocalDataLocation)}; + file = appDataPath + "/theme.conf"; + self_->ui->themeFileLineEdit->setText(file); + // setText does not emit the textEdited signal + Q_EMIT self_->ui->themeFileLineEdit->textEdited(file); + } + const QPalette palette = Qt6CT::loadColorScheme(file, QApplication::palette()); QStyle* style = QApplication::style(); From 316da55000a7f1f2dd98d34ebfdaadbbd0381f41 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 11:04:15 -0400 Subject: [PATCH 445/762] Modify how level 3 tilts are selected to keep them more consistent --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 107 ++++++++++++++++------ 1 file changed, 80 insertions(+), 27 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ed288a40..9c768509 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -98,7 +98,8 @@ public: prevLongitude_ {0.0}, prevZoom_ {0.0}, prevBearing_ {0.0}, - prevPitch_ {0.0} + prevPitch_ {0.0}, + tiltsToIndices_ {} { // Create views auto overlayProductView = std::make_shared(); @@ -273,6 +274,9 @@ public: bool productAvailabilityUpdated_ {false}; bool productAvailabilityProductSelected_ {false}; + std::unordered_map tiltsToIndices_; + size_t currentTiltIndex_ {0}; + public slots: void Update(); }; @@ -829,6 +833,17 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, productCode = common::GetLevel3ProductCodeByAwipsId(productName); } + if (group == common::RadarProductGroup::Level3) + { + const auto& tiltIndex = p->tiltsToIndices_.find(productName); + p->currentTiltIndex_ = + tiltIndex != p->tiltsToIndices_.cend() ? tiltIndex->second : 0; + } + else + { + p->currentTiltIndex_ = 0; + } + if (radarProductView == nullptr || radarProductView->GetRadarProductGroup() != group || (radarProductView->GetRadarProductGroup() == @@ -933,11 +948,6 @@ void MapWidget::SelectRadarSite(std::shared_ptr radarSite, if (radarProductView != nullptr) { radarProductView->set_radar_product_manager(p->radarProductManager_); - SelectRadarProduct(radarProductView->GetRadarProductGroup(), - radarProductView->GetRadarProductName(), - 0, - radarProductView->selected_time(), - false); } p->AddLayers(); @@ -1756,6 +1766,24 @@ void MapWidgetImpl::RadarProductManagerConnect() this, [this]() { + const common::Level3ProductCategoryMap& categoryMap = + widget_->GetAvailableLevel3Categories(); + + tiltsToIndices_.clear(); + for (const auto& category : categoryMap) + { + for (const auto& product : category.second) + { + for (size_t tiltIndex = 0; + tiltIndex < product.second.size(); + tiltIndex++) + { + tiltsToIndices_.emplace(product.second[tiltIndex], + tiltIndex); + } + } + } + productAvailabilityUpdated_ = true; CheckLevel3Availability(); Q_EMIT widget_->Level3ProductsChanged(); @@ -2058,7 +2086,7 @@ void MapWidgetImpl::CheckLevel3Availability() * has been updated * * productAvailabilityProductSelected_ Only update once the radar site is - * fully selected, including the current product + * fully selected */ if (!(productAvailabilityCheckNeeded_ && productAvailabilityUpdated_ && productAvailabilityProductSelected_)) @@ -2073,6 +2101,9 @@ void MapWidgetImpl::CheckLevel3Availability() return; } + // Get radar product view for fallback selection + auto radarProductView = context_->radar_product_view(); + const common::Level3ProductCategoryMap& categoryMap = widget_->GetAvailableLevel3Categories(); @@ -2083,6 +2114,12 @@ void MapWidgetImpl::CheckLevel3Availability() common::GetLevel3CategoryByProduct(productName); if (productCategory == common::Level3ProductCategory::Unknown) { + // Default to the same as already selected + widget_->SelectRadarProduct(radarProductView->GetRadarProductGroup(), + radarProductView->GetRadarProductName(), + 0, + radarProductView->selected_time(), + false); return; } @@ -2090,38 +2127,54 @@ void MapWidgetImpl::CheckLevel3Availability() // Has no products in this category, do not change categories if (availableProductsIt == categoryMap.cend()) { + // Default to the same as already selected + widget_->SelectRadarProduct(radarProductView->GetRadarProductGroup(), + radarProductView->GetRadarProductName(), + 0, + radarProductView->selected_time(), + false); return; } const auto& availableProducts = availableProductsIt->second; const auto& availableTiltsIt = availableProducts.find(productName); - // Does not have the same product, but has others in the same category. - // Switch to the default product and tilt in this category. - if (availableTiltsIt == availableProducts.cend()) - { - widget_->SelectRadarProduct( - common::RadarProductGroup::Level3, - common::GetLevel3CategoryDefaultProduct(productCategory, categoryMap), - 0, - widget_->GetSelectedTime()); - return; - } - const auto& availableTilts = availableTiltsIt->second; - const auto& tilt = std::ranges::find_if( - availableTilts, - [productTilt](const std::string& tilt) { return productTilt == tilt; }); - // Tilt is not available, set it to first tilt - if (tilt == availableTilts.cend() && availableTilts.size() > 0) + const auto& availableTilts = + availableTiltsIt == availableProducts.cend() ? + // Does not have the same product, but has others in the same category. + // Switch to the default product and tilt in this category. + availableProducts.at(common::GetLevel3ProductByAwipsId( + common::GetLevel3CategoryDefaultProduct(productCategory, + categoryMap))) : + // Has the same product + availableTiltsIt->second; + + // Try to match the tilt to the last tilt. + if (currentTiltIndex_ < availableTilts.size()) { widget_->SelectRadarProduct(common::RadarProductGroup::Level3, - availableTilts[0], + availableTilts[currentTiltIndex_], 0, widget_->GetSelectedTime()); - return; } + else if (availableTilts.size() > 0) + { + widget_->SelectRadarProduct(common::RadarProductGroup::Level3, + availableTilts[availableTilts.size() - 1], + 0, + widget_->GetSelectedTime()); - // Tilt is available, no change needed + } + else + { + // No tilts available in this case, default to the same as already + // selected + widget_->SelectRadarProduct(radarProductView->GetRadarProductGroup(), + radarProductView->GetRadarProductName(), + 0, + radarProductView->selected_time(), + false); + } } } // namespace map From 5968fd981cd80f96ec6e55a40c608ad018b4c7f1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 11:05:21 -0400 Subject: [PATCH 446/762] Fix parsing of negative elevations in product description block --- wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 4cb525ae..8bd6f331 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -726,7 +726,7 @@ units::angle::degrees ProductDescriptionBlock::elevation() const if (p->elevationNumber_ > 0) { - elevation = p->parameters_[2] * 0.1; + elevation = static_cast(p->parameters_[2]) * 0.1; } return units::angle::degrees {elevation}; From be972cdb7e823c23b94579902761b4b225d962fb Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 11:10:42 -0400 Subject: [PATCH 447/762] Add elevation to product label. Mainly for level 3 products --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 3 +-- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 12 ++++++++++-- scwx-qt/source/scwx/qt/view/level3_radial_view.cpp | 7 +++++++ scwx-qt/source/scwx/qt/view/level3_radial_view.hpp | 10 ++++++---- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 9c768509..55813bb4 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -275,7 +275,7 @@ public: bool productAvailabilityProductSelected_ {false}; std::unordered_map tiltsToIndices_; - size_t currentTiltIndex_ {0}; + size_t currentTiltIndex_ {0}; public slots: void Update(); @@ -2163,7 +2163,6 @@ void MapWidgetImpl::CheckLevel3Availability() availableTilts[availableTilts.size() - 1], 0, widget_->GetSelectedTime()); - } else { diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 36258e75..7c3c22a9 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -426,15 +427,22 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) if (radarProductView != nullptr) { // Render product name - std::string productName = radarProductView->GetRadarProductName(); + const std::string productName = radarProductView->GetRadarProductName(); + const float elevation = radarProductView->elevation(); + if (productName.length() > 0 && !productName.starts_with('?')) { + const std::string elevationString = + (QString::number(elevation, 'f', 1) + common::Characters::DEGREE) + .toStdString(); + ImGui::SetNextWindowPos(ImVec2 {0.0f, 0.0f}); ImGui::Begin("Product Name", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); - ImGui::TextUnformatted(productName.c_str()); + ImGui::TextUnformatted( + fmt::format("{} ({})", productName, elevationString).c_str()); ImGui::End(); } } diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 0b3f42fa..05409e68 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -67,6 +67,7 @@ public: float latitude_; float longitude_; + float elevation_; float range_; std::uint16_t vcp_; @@ -91,6 +92,11 @@ boost::asio::thread_pool& Level3RadialView::thread_pool() return p->threadPool_; } +float Level3RadialView::elevation() const +{ + return p->elevation_; +} + float Level3RadialView::range() const { return p->range_; @@ -306,6 +312,7 @@ void Level3RadialView::ComputeSweep() p->latitude_ = descriptionBlock->latitude_of_radar(); p->longitude_ = descriptionBlock->longitude_of_radar(); p->range_ = descriptionBlock->range(); + p->elevation_ = static_cast(descriptionBlock->elevation().value()); p->sweepTime_ = scwx::util::TimePoint(descriptionBlock->volume_scan_date(), descriptionBlock->volume_scan_start_time() * 1000); diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp index f99f4e63..9f6c0a3d 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp @@ -23,10 +23,12 @@ public: std::shared_ptr radarProductManager); ~Level3RadialView(); - float range() const override; - std::chrono::system_clock::time_point sweep_time() const override; - std::uint16_t vcp() const override; - const std::vector& vertices() const override; + [[nodiscard]] float elevation() const override; + [[nodiscard]] float range() const override; + [[nodiscard]] std::chrono::system_clock::time_point + sweep_time() const override; + [[nodiscard]] std::uint16_t vcp() const override; + [[nodiscard]] const std::vector& vertices() const override; std::tuple GetMomentData() const override; From 443f5a36158f2f81bcd5b9e5873a28262f2dad1f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 11:15:45 -0400 Subject: [PATCH 448/762] clang tidy fixes for modify_tilt_selection --- wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 8bd6f331..1fc80d04 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -726,6 +726,8 @@ units::angle::degrees ProductDescriptionBlock::elevation() const if (p->elevationNumber_ > 0) { + // Elevation is given in tenths of a degree + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) elevation = static_cast(p->parameters_[2]) * 0.1; } From ac3c986568125d00e177b486f36b77b3e0dd3714 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 13:03:02 -0400 Subject: [PATCH 449/762] Add button for setting default radar products --- scwx-qt/source/scwx/qt/main/main_window.cpp | 25 ++++ scwx-qt/source/scwx/qt/main/main_window.ui | 109 ++++++++++-------- .../source/scwx/qt/settings/map_settings.cpp | 4 +- 3 files changed, 86 insertions(+), 52 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 5b25a91a..814ed0fb 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -267,6 +267,7 @@ MainWindow::MainWindow(QWidget* parent) : ui->vcpLabel->setVisible(false); ui->vcpValueLabel->setVisible(false); ui->vcpDescriptionLabel->setVisible(false); + ui->saveRadarProductsButton->setVisible(true); p->radarSitePresetsMenu_ = new QMenu(this); ui->radarSitePresetsButton->setMenu(p->radarSitePresetsMenu_); @@ -326,6 +327,8 @@ MainWindow::MainWindow(QWidget* parent) : ui->smoothRadarDataCheckBox); p->mapSettingsGroup_->GetContentsLayout()->addWidget( ui->trackLocationCheckBox); + p->mapSettingsGroup_->GetContentsLayout()->addWidget( + ui->saveRadarProductsButton); ui->radarToolboxScrollAreaContents->layout()->replaceWidget( ui->mapSettingsGroupBox, p->mapSettingsGroup_); ui->mapSettingsGroupBox->setVisible(false); @@ -1124,6 +1127,22 @@ void MainWindowImpl::ConnectOtherSignals() // Turn on location tracking positionManager_->TrackLocation(trackingEnabled); }); + connect(mainWindow_->ui->saveRadarProductsButton, + &QAbstractButton::clicked, + mainWindow_, + [this]() + { + auto& mapSettings = settings::MapSettings::Instance(); + for (std::size_t i = 0; i < maps_.size(); i++) + { + const auto& map = maps_.at(i); + mapSettings.radar_product_group(i).StageValue( + common::GetRadarProductGroupName( + map->GetRadarProductGroup())); + mapSettings.radar_product(i).StageValue( + map->GetRadarProductName()); + } + }); connect(level2ProductsWidget_, &ui::Level2ProductsWidget::RadarProductSelected, mainWindow_, @@ -1509,6 +1528,8 @@ void MainWindowImpl::UpdateRadarProductSettings() void MainWindowImpl::UpdateRadarSite() { std::shared_ptr radarSite = activeMap_->GetRadarSite(); + const std::string homeRadarSite = + settings::GeneralSettings::Instance().default_radar_site().GetValue(); if (radarSite != nullptr) { @@ -1523,6 +1544,9 @@ void MainWindowImpl::UpdateRadarSite() radarSite->location_name().c_str()); timelineManager_->SetRadarSite(radarSite->id()); + + mainWindow_->ui->saveRadarProductsButton->setVisible( + radarSite->id() == homeRadarSite); } else { @@ -1530,6 +1554,7 @@ void MainWindowImpl::UpdateRadarSite() mainWindow_->ui->radarSiteValueLabel->setVisible(false); mainWindow_->ui->radarLocationLabel->setVisible(false); + mainWindow_->ui->saveRadarProductsButton->setVisible(false); timelineManager_->SetRadarSite("?"); } diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 5d856663..94346c68 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -155,8 +155,8 @@ 0 0 - 190 - 680 + 205 + 701 @@ -181,32 +181,24 @@ QFrame::Shadow::Raised - - - - - 0 - 0 - - + + - KLSX + 35 - - - - - 0 - 0 - - - - Volume Coverage Pattern - + + - VCP + Clear Air Mode + + + + + + + St. Louis, MO @@ -271,34 +263,6 @@ - - - - Radar Site - - - - - - - St. Louis, MO - - - - - - - 35 - - - - - - - Clear Air Mode - - - @@ -312,6 +276,42 @@ + + + + + 0 + 0 + + + + Volume Coverage Pattern + + + VCP + + + + + + + Radar Site + + + + + + + + 0 + 0 + + + + KLSX + + + @@ -345,6 +345,13 @@ + + + + Set As Default Products + + + diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 416c0a6d..76c09d30 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -76,7 +76,7 @@ public: { common::RadarProductGroup radarProductGroup = common::GetRadarProductGroup( - map_.at(i).radarProductGroup_.GetValue()); + map_.at(i).radarProductGroup_.GetStagedOrValue()); if (radarProductGroup == common::RadarProductGroup::Level2) { @@ -193,6 +193,8 @@ bool MapSettings::Shutdown() dataChanged |= mapRecordSettings.mapStyle_.Commit(); dataChanged |= mapRecordSettings.smoothingEnabled_.Commit(); + dataChanged |= mapRecordSettings.radarProductGroup_.Commit(); + dataChanged |= mapRecordSettings.radarProduct_.Commit(); } return dataChanged; From 484c08c4557fde5dfdea205c77e8b81f26d75d18 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Apr 2025 13:07:23 -0400 Subject: [PATCH 450/762] clang format fixes for modify_tilt_selection --- scwx-qt/source/scwx/qt/main/main_window.cpp | 35 ++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 814ed0fb..e951de71 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1127,22 +1127,21 @@ void MainWindowImpl::ConnectOtherSignals() // Turn on location tracking positionManager_->TrackLocation(trackingEnabled); }); - connect(mainWindow_->ui->saveRadarProductsButton, - &QAbstractButton::clicked, - mainWindow_, - [this]() - { - auto& mapSettings = settings::MapSettings::Instance(); - for (std::size_t i = 0; i < maps_.size(); i++) - { - const auto& map = maps_.at(i); - mapSettings.radar_product_group(i).StageValue( - common::GetRadarProductGroupName( - map->GetRadarProductGroup())); - mapSettings.radar_product(i).StageValue( - map->GetRadarProductName()); - } - }); + connect( + mainWindow_->ui->saveRadarProductsButton, + &QAbstractButton::clicked, + mainWindow_, + [this]() + { + auto& mapSettings = settings::MapSettings::Instance(); + for (std::size_t i = 0; i < maps_.size(); i++) + { + const auto& map = maps_.at(i); + mapSettings.radar_product_group(i).StageValue( + common::GetRadarProductGroupName(map->GetRadarProductGroup())); + mapSettings.radar_product(i).StageValue(map->GetRadarProductName()); + } + }); connect(level2ProductsWidget_, &ui::Level2ProductsWidget::RadarProductSelected, mainWindow_, @@ -1545,8 +1544,8 @@ void MainWindowImpl::UpdateRadarSite() timelineManager_->SetRadarSite(radarSite->id()); - mainWindow_->ui->saveRadarProductsButton->setVisible( - radarSite->id() == homeRadarSite); + mainWindow_->ui->saveRadarProductsButton->setVisible(radarSite->id() == + homeRadarSite); } else { From 24f5f0a3e34612f7e53648759c055f5939dbf2a6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Apr 2025 10:59:41 -0400 Subject: [PATCH 451/762] Do not display an elevation number when there is non --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 +- scwx-qt/source/scwx/qt/map/map_widget.hpp | 3 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 23 +++-- .../scwx/qt/ui/level2_settings_widget.cpp | 4 +- .../scwx/qt/view/level2_product_view.cpp | 2 +- .../scwx/qt/view/level2_product_view.hpp | 63 ++++++------ .../scwx/qt/view/level3_radial_view.cpp | 9 +- .../scwx/qt/view/level3_radial_view.hpp | 25 +++-- .../scwx/qt/view/radar_product_view.cpp | 4 +- .../scwx/qt/view/radar_product_view.hpp | 69 ++++++------- .../wsr88d/rpg/product_description_block.hpp | 98 +++++++++---------- .../wsr88d/rpg/product_description_block.cpp | 7 +- 12 files changed, 166 insertions(+), 145 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 55813bb4..60c50a98 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -615,7 +615,7 @@ common::Level3ProductCategoryMap MapWidget::GetAvailableLevel3Categories() } } -float MapWidget::GetElevation() const +std::optional MapWidget::GetElevation() const { auto radarProductView = p->context_->radar_product_view(); @@ -625,7 +625,7 @@ float MapWidget::GetElevation() const } else { - return 0.0f; + return {}; } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 23d38680..6764629e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -9,6 +9,7 @@ #include #include +#include #include @@ -41,7 +42,7 @@ public: [[nodiscard]] common::Level3ProductCategoryMap GetAvailableLevel3Categories(); - [[nodiscard]] float GetElevation() const; + [[nodiscard]] std::optional GetElevation() const; [[nodiscard]] std::vector GetElevationCuts() const; [[nodiscard]] std::vector GetLevel3Products(); [[nodiscard]] std::string GetMapStyle() const; diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 7c3c22a9..ba692aae 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -428,21 +428,30 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { // Render product name const std::string productName = radarProductView->GetRadarProductName(); - const float elevation = radarProductView->elevation(); + const std::optional elevation = radarProductView->elevation(); if (productName.length() > 0 && !productName.starts_with('?')) { - const std::string elevationString = - (QString::number(elevation, 'f', 1) + common::Characters::DEGREE) - .toStdString(); - ImGui::SetNextWindowPos(ImVec2 {0.0f, 0.0f}); ImGui::Begin("Product Name", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); - ImGui::TextUnformatted( - fmt::format("{} ({})", productName, elevationString).c_str()); + + if (elevation.has_value()) + { + const std::string elevationString = + (QString::number(*elevation, 'f', 1) + + common::Characters::DEGREE) + .toStdString(); + ImGui::TextUnformatted( + fmt::format("{} ({})", productName, elevationString).c_str()); + } + else + { + ImGui::TextUnformatted(productName.c_str()); + } + ImGui::End(); } } diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 85b476c9..0289ee54 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -242,7 +242,9 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) { - float currentElevation = activeMap->GetElevation(); + std::optional currentElevationOption = activeMap->GetElevation(); + float currentElevation = + currentElevationOption.has_value() ? *currentElevationOption : 0.0f; std::vector elevationCuts = activeMap->GetElevationCuts(); if (p->elevationCuts_ != elevationCuts) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 4e7181e7..6fcc775c 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -271,7 +271,7 @@ uint16_t Level2ProductView::color_table_max() const } } -float Level2ProductView::elevation() const +std::optional Level2ProductView::elevation() const { return p->elevationCut_; } diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp index db8fc45c..64651bfa 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp @@ -8,11 +8,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { class Level2ProductView : public RadarProductView @@ -23,38 +19,47 @@ public: explicit Level2ProductView( common::Level2Product product, std::shared_ptr radarProductManager); - ~Level2ProductView(); + ~Level2ProductView() override; - std::shared_ptr color_table() const override; - const std::vector& - color_table_lut() const override; - std::uint16_t color_table_min() const override; - std::uint16_t color_table_max() const override; - float elevation() const override; - float range() const override; - std::chrono::system_clock::time_point sweep_time() const override; - float unit_scale() const override; - std::string units() const override; - std::uint16_t vcp() const override; - const std::vector& vertices() const override; + Level2ProductView(const Level2ProductView&) = delete; + Level2ProductView(Level2ProductView&&) = delete; + Level2ProductView& operator=(const Level2ProductView&) = delete; + Level2ProductView& operator=(Level2ProductView&&) = delete; + + [[nodiscard]] std::shared_ptr + color_table() const override; + [[nodiscard]] const std::vector& + color_table_lut() const override; + [[nodiscard]] std::uint16_t color_table_min() const override; + [[nodiscard]] std::uint16_t color_table_max() const override; + [[nodiscard]] std::optional elevation() const override; + [[nodiscard]] float range() const override; + [[nodiscard]] std::chrono::system_clock::time_point + sweep_time() const override; + [[nodiscard]] float unit_scale() const override; + [[nodiscard]] std::string units() const override; + [[nodiscard]] std::uint16_t vcp() const override; + [[nodiscard]] const std::vector& vertices() const override; void LoadColorTable(std::shared_ptr colorTable) override; void SelectElevation(float elevation) override; void SelectProduct(const std::string& productName) override; - common::RadarProductGroup GetRadarProductGroup() const override; - std::string GetRadarProductName() const override; - std::vector GetElevationCuts() const override; - std::tuple + [[nodiscard]] common::RadarProductGroup + GetRadarProductGroup() const override; + [[nodiscard]] std::string GetRadarProductName() const override; + [[nodiscard]] std::vector GetElevationCuts() const override; + [[nodiscard]] std::tuple GetMomentData() const override; - std::tuple + [[nodiscard]] std::tuple GetCfpMomentData() const override; - std::optional + [[nodiscard]] std::optional GetBinLevel(const common::Coordinate& coordinate) const override; - std::optional - GetDataLevelCode(std::uint16_t level) const override; - std::optional GetDataValue(std::uint16_t level) const override; + [[nodiscard]] std::optional + GetDataLevelCode(std::uint16_t level) const override; + [[nodiscard]] std::optional + GetDataValue(std::uint16_t level) const override; static std::shared_ptr Create(common::Level2Product product, @@ -75,6 +80,4 @@ private: std::unique_ptr p; }; -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 05409e68..c01e0cd4 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -67,7 +67,7 @@ public: float latitude_; float longitude_; - float elevation_; + std::optional elevation_ {}; float range_; std::uint16_t vcp_; @@ -92,7 +92,7 @@ boost::asio::thread_pool& Level3RadialView::thread_pool() return p->threadPool_; } -float Level3RadialView::elevation() const +std::optional Level3RadialView::elevation() const { return p->elevation_; } @@ -312,7 +312,10 @@ void Level3RadialView::ComputeSweep() p->latitude_ = descriptionBlock->latitude_of_radar(); p->longitude_ = descriptionBlock->longitude_of_radar(); p->range_ = descriptionBlock->range(); - p->elevation_ = static_cast(descriptionBlock->elevation().value()); + p->elevation_ = + descriptionBlock->has_elevation() ? + static_cast(descriptionBlock->elevation().value()) : + std::optional {}; p->sweepTime_ = scwx::util::TimePoint(descriptionBlock->volume_scan_date(), descriptionBlock->volume_scan_start_time() * 1000); diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp index 9f6c0a3d..86d550e6 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp @@ -6,11 +6,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { class Level3RadialView : public Level3ProductView @@ -21,19 +17,24 @@ public: explicit Level3RadialView( const std::string& product, std::shared_ptr radarProductManager); - ~Level3RadialView(); + ~Level3RadialView() override; - [[nodiscard]] float elevation() const override; - [[nodiscard]] float range() const override; + Level3RadialView(const Level3RadialView&) = delete; + Level3RadialView(Level3RadialView&&) = delete; + Level3RadialView& operator=(const Level3RadialView&) = delete; + Level3RadialView& operator=(Level3RadialView&&) = delete; + + [[nodiscard]] std::optional elevation() const override; + [[nodiscard]] float range() const override; [[nodiscard]] std::chrono::system_clock::time_point sweep_time() const override; [[nodiscard]] std::uint16_t vcp() const override; [[nodiscard]] const std::vector& vertices() const override; - std::tuple + [[nodiscard]] std::tuple GetMomentData() const override; - std::optional + [[nodiscard]] std::optional GetBinLevel(const common::Coordinate& coordinate) const override; static std::shared_ptr @@ -51,6 +52,4 @@ private: std::unique_ptr p; }; -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp index 9c5a84de..dc50383c 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -85,9 +85,9 @@ std::uint16_t RadarProductView::color_table_max() const return kDefaultColorTableMax_; } -float RadarProductView::elevation() const +std::optional RadarProductView::elevation() const { - return 0.0f; + return {}; } std::shared_ptr diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 31d47840..2801b74e 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -16,11 +16,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { class RadarProductViewImpl; @@ -32,20 +28,27 @@ class RadarProductView : public QObject public: explicit RadarProductView( std::shared_ptr radarProductManager); - virtual ~RadarProductView(); + ~RadarProductView() override; - virtual std::shared_ptr color_table() const = 0; - virtual const std::vector& - color_table_lut() const; - virtual std::uint16_t color_table_min() const; - virtual std::uint16_t color_table_max() const; - virtual float elevation() const; - virtual float range() const; - virtual std::chrono::system_clock::time_point sweep_time() const; - virtual float unit_scale() const = 0; - virtual std::string units() const = 0; - virtual std::uint16_t vcp() const = 0; - virtual const std::vector& vertices() const = 0; + RadarProductView(const RadarProductView&) = delete; + RadarProductView(RadarProductView&&) = delete; + RadarProductView& operator=(const RadarProductView&) = delete; + RadarProductView& operator=(RadarProductView&&) = delete; + + [[nodiscard]] virtual std::shared_ptr + color_table() const = 0; + [[nodiscard]] virtual const std::vector& + color_table_lut() const; + [[nodiscard]] virtual std::uint16_t color_table_min() const; + [[nodiscard]] virtual std::uint16_t color_table_max() const; + [[nodiscard]] virtual std::optional elevation() const; + [[nodiscard]] virtual float range() const; + [[nodiscard]] virtual std::chrono::system_clock::time_point + sweep_time() const; + [[nodiscard]] virtual float unit_scale() const = 0; + [[nodiscard]] virtual std::string units() const = 0; + [[nodiscard]] virtual std::uint16_t vcp() const = 0; + [[nodiscard]] virtual const std::vector& vertices() const = 0; [[nodiscard]] std::shared_ptr radar_product_manager() const; @@ -66,24 +69,26 @@ public: void SelectTime(std::chrono::system_clock::time_point time); void Update(); - bool IsInitialized() const; + [[nodiscard]] bool IsInitialized() const; - virtual common::RadarProductGroup GetRadarProductGroup() const = 0; - virtual std::string GetRadarProductName() const = 0; - virtual std::vector GetElevationCuts() const; - virtual std::tuple + [[nodiscard]] virtual common::RadarProductGroup + GetRadarProductGroup() const = 0; + [[nodiscard]] virtual std::string GetRadarProductName() const = 0; + [[nodiscard]] virtual std::vector GetElevationCuts() const; + [[nodiscard]] virtual std::tuple GetMomentData() const = 0; - virtual std::tuple + [[nodiscard]] virtual std::tuple GetCfpMomentData() const; - virtual std::optional + [[nodiscard]] virtual std::optional GetBinLevel(const common::Coordinate& coordinate) const = 0; - virtual std::optional - GetDataLevelCode(std::uint16_t level) const = 0; - virtual std::optional GetDataValue(std::uint16_t level) const = 0; - virtual bool IgnoreUnits() const; + [[nodiscard]] virtual std::optional + GetDataLevelCode(std::uint16_t level) const = 0; + [[nodiscard]] virtual std::optional + GetDataValue(std::uint16_t level) const = 0; + [[nodiscard]] virtual bool IgnoreUnits() const; - virtual std::vector> + [[nodiscard]] virtual std::vector> GetDescriptionFields() const; protected: @@ -105,6 +110,4 @@ private: std::unique_ptr p; }; -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view diff --git a/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp b/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp index 1182828a..30bdfdf2 100644 --- a/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp @@ -9,11 +9,7 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rpg +namespace scwx::wsr88d::rpg { class ProductDescriptionBlockImpl; @@ -22,7 +18,7 @@ class ProductDescriptionBlock : public awips::Message { public: explicit ProductDescriptionBlock(); - ~ProductDescriptionBlock(); + ~ProductDescriptionBlock() override; ProductDescriptionBlock(const ProductDescriptionBlock&) = delete; ProductDescriptionBlock& operator=(const ProductDescriptionBlock&) = delete; @@ -30,57 +26,59 @@ public: ProductDescriptionBlock(ProductDescriptionBlock&&) noexcept; ProductDescriptionBlock& operator=(ProductDescriptionBlock&&) noexcept; - int16_t block_divider() const; - float latitude_of_radar() const; - float longitude_of_radar() const; - int16_t height_of_radar() const; - int16_t product_code() const; - uint16_t operational_mode() const; - uint16_t volume_coverage_pattern() const; - int16_t sequence_number() const; - uint16_t volume_scan_number() const; - uint16_t volume_scan_date() const; - uint32_t volume_scan_start_time() const; - uint16_t generation_date_of_product() const; - uint32_t generation_time_of_product() const; - uint16_t elevation_number() const; - uint16_t data_level_threshold(size_t i) const; - uint8_t version() const; - uint8_t spot_blank() const; - uint32_t offset_to_symbology() const; - uint32_t offset_to_graphic() const; - uint32_t offset_to_tabular() const; + [[nodiscard]] int16_t block_divider() const; + [[nodiscard]] float latitude_of_radar() const; + [[nodiscard]] float longitude_of_radar() const; + [[nodiscard]] int16_t height_of_radar() const; + [[nodiscard]] int16_t product_code() const; + [[nodiscard]] uint16_t operational_mode() const; + [[nodiscard]] uint16_t volume_coverage_pattern() const; + [[nodiscard]] int16_t sequence_number() const; + [[nodiscard]] uint16_t volume_scan_number() const; + [[nodiscard]] uint16_t volume_scan_date() const; + [[nodiscard]] uint32_t volume_scan_start_time() const; + [[nodiscard]] uint16_t generation_date_of_product() const; + [[nodiscard]] uint32_t generation_time_of_product() const; + [[nodiscard]] uint16_t elevation_number() const; + [[nodiscard]] uint16_t data_level_threshold(size_t i) const; + [[nodiscard]] uint8_t version() const; + [[nodiscard]] uint8_t spot_blank() const; + [[nodiscard]] uint32_t offset_to_symbology() const; + [[nodiscard]] uint32_t offset_to_graphic() const; + [[nodiscard]] uint32_t offset_to_tabular() const; - float range() const; - uint16_t range_raw() const; - float x_resolution() const; - uint16_t x_resolution_raw() const; - float y_resolution() const; - uint16_t y_resolution_raw() const; + [[nodiscard]] float range() const; + [[nodiscard]] uint16_t range_raw() const; + [[nodiscard]] float x_resolution() const; + [[nodiscard]] uint16_t x_resolution_raw() const; + [[nodiscard]] float y_resolution() const; + [[nodiscard]] uint16_t y_resolution_raw() const; - uint16_t threshold() const; - float offset() const; - float scale() const; - uint16_t number_of_levels() const; + [[nodiscard]] uint16_t threshold() const; + [[nodiscard]] float offset() const; + [[nodiscard]] float scale() const; + [[nodiscard]] uint16_t number_of_levels() const; - std::optional data_level_code(std::uint8_t level) const; - std::optional data_value(std::uint8_t level) const; + [[nodiscard]] std::optional + data_level_code(std::uint8_t level) const; + [[nodiscard]] std::optional data_value(std::uint8_t level) const; - std::uint16_t log_start() const; - float log_offset() const; - float log_scale() const; + [[nodiscard]] std::uint16_t log_start() const; + [[nodiscard]] float log_offset() const; + [[nodiscard]] float log_scale() const; - float gr_scale() const; + [[nodiscard]] float gr_scale() const; - std::uint8_t data_mask() const; - std::uint8_t topped_mask() const; + [[nodiscard]] std::uint8_t data_mask() const; + [[nodiscard]] std::uint8_t topped_mask() const; - units::angle::degrees elevation() const; + [[nodiscard]] units::angle::degrees elevation() const; + [[nodiscard]] bool has_elevation() const; - bool IsCompressionEnabled() const; - bool IsDataLevelCoded() const; + [[nodiscard]] bool IsCompressionEnabled() const; + [[nodiscard]] bool IsDataLevelCoded() const; - size_t data_size() const override; + [[nodiscard]] size_t data_size() const override; bool Parse(std::istream& is) override; @@ -90,6 +88,4 @@ private: std::unique_ptr p; }; -} // namespace rpg -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 1fc80d04..10fdbe72 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -724,7 +724,7 @@ units::angle::degrees ProductDescriptionBlock::elevation() const { double elevation = 0.0; - if (p->elevationNumber_ > 0) + if (has_elevation()) { // Elevation is given in tenths of a degree // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) @@ -734,6 +734,11 @@ units::angle::degrees ProductDescriptionBlock::elevation() const return units::angle::degrees {elevation}; } +bool ProductDescriptionBlock::has_elevation() const +{ + return p->elevationNumber_ > 0; +} + bool ProductDescriptionBlock::IsCompressionEnabled() const { bool isCompressed = false; From ceb1ca8416135db0e5a5340934562a0f962ffa71 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Apr 2025 11:07:46 -0400 Subject: [PATCH 452/762] Fix level 2 selection on radar site change from modify_tilt_selection --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 60c50a98..e5362905 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -2095,15 +2095,20 @@ void MapWidgetImpl::CheckLevel3Availability() } productAvailabilityCheckNeeded_ = false; + // Get radar product view for fallback and level2 selection + auto radarProductView = context_->radar_product_view(); + // Only do this for level3 products if (widget_->GetRadarProductGroup() != common::RadarProductGroup::Level3) { + widget_->SelectRadarProduct(radarProductView->GetRadarProductGroup(), + radarProductView->GetRadarProductName(), + 0, + radarProductView->selected_time(), + false); return; } - // Get radar product view for fallback selection - auto radarProductView = context_->radar_product_view(); - const common::Level3ProductCategoryMap& categoryMap = widget_->GetAvailableLevel3Categories(); From ddd8977586ffe76aad50a2e3054696a2d01f89bc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Apr 2025 11:15:20 -0400 Subject: [PATCH 453/762] Avoid nullptr dereference when selecting a tilt on radar site change --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index e5362905..abf5d484 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -2097,6 +2097,10 @@ void MapWidgetImpl::CheckLevel3Availability() // Get radar product view for fallback and level2 selection auto radarProductView = context_->radar_product_view(); + if (radarProductView == nullptr) + { + return; + } // Only do this for level3 products if (widget_->GetRadarProductGroup() != common::RadarProductGroup::Level3) From 81ec5f9f7a9163e496249af0f7fe611c7fac3bee Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Apr 2025 11:38:25 -0400 Subject: [PATCH 454/762] Update set default products button on default radar site change --- scwx-qt/source/scwx/qt/main/main_window.cpp | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index e951de71..a9c140be 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -139,6 +139,8 @@ public: } ~MainWindowImpl() { + homeRadarConnection_.disconnect(); + auto& generalSettings = settings::GeneralSettings::Instance(); auto& customStyleUrl = generalSettings.custom_style_url(); @@ -239,6 +241,8 @@ public: layerActions_ {}; bool layerActionsInitialized_ {false}; + boost::signals2::scoped_connection homeRadarConnection_ {}; + std::vector maps_; std::chrono::system_clock::time_point selectedTime_ {}; @@ -1273,6 +1277,29 @@ void MainWindowImpl::ConnectOtherSignals() timeLabel_->setVisible(true); }); clockTimer_.start(1000); + + auto& generalSettings = settings::GeneralSettings::Instance(); + homeRadarConnection_ = + generalSettings.default_radar_site().changed_signal().connect( + [this]() + { + std::shared_ptr radarSite = + activeMap_->GetRadarSite(); + const std::string homeRadarSite = + settings::GeneralSettings::Instance() + .default_radar_site() + .GetValue(); + if (radarSite == nullptr) + { + mainWindow_->ui->saveRadarProductsButton->setVisible( + false); + } + else + { + mainWindow_->ui->saveRadarProductsButton->setVisible( + radarSite->id() == homeRadarSite); + } + }); } void MainWindowImpl::InitializeLayerDisplayActions() From 17b2db64effa68f69113b02a747c8aed1d5481eb Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 13 Apr 2025 11:47:28 -0400 Subject: [PATCH 455/762] clang format/tidy fixes for modify_tilt_selection --- scwx-qt/source/scwx/qt/main/main_window.cpp | 5 ++--- scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index a9c140be..71f4d00c 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1283,7 +1283,7 @@ void MainWindowImpl::ConnectOtherSignals() generalSettings.default_radar_site().changed_signal().connect( [this]() { - std::shared_ptr radarSite = + const std::shared_ptr radarSite = activeMap_->GetRadarSite(); const std::string homeRadarSite = settings::GeneralSettings::Instance() @@ -1291,8 +1291,7 @@ void MainWindowImpl::ConnectOtherSignals() .GetValue(); if (radarSite == nullptr) { - mainWindow_->ui->saveRadarProductsButton->setVisible( - false); + mainWindow_->ui->saveRadarProductsButton->setVisible(false); } else { diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 0289ee54..1b851e72 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -243,7 +243,7 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) { std::optional currentElevationOption = activeMap->GetElevation(); - float currentElevation = + const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; std::vector elevationCuts = activeMap->GetElevationCuts(); From d34224c1351527097d5d24954906cf953769937d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 15 Apr 2025 22:52:33 -0500 Subject: [PATCH 456/762] Configure aqtinstall to use a mirror --- .github/workflows/ci.yml | 2 ++ .github/workflows/clang-tidy-review.yml | 4 +++- tools/aqt-settings.ini | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tools/aqt-settings.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd6f7bf3..ef0d18fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,8 @@ jobs: - name: Install Qt uses: jdpurcell/install-qt-action@v5 + env: + AQT_CONFIG: ${{ github.workspace }}/source/tools/aqt-settings.ini with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 62e26976..c7297103 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -48,7 +48,9 @@ jobs: path: clang-tidy-review - name: Install Qt - uses: jurplel/install-qt-action@v4 + uses: jdpurcell/install-qt-action@v5 + env: + AQT_CONFIG: ${{ github.workspace }}/source/tools/aqt-settings.ini with: version: ${{ matrix.qt_version }} arch: ${{ matrix.qt_arch_aqt }} diff --git a/tools/aqt-settings.ini b/tools/aqt-settings.ini new file mode 100644 index 00000000..9d20e0a2 --- /dev/null +++ b/tools/aqt-settings.ini @@ -0,0 +1,17 @@ +[aqt] +# Using this mirror instead of download.qt.io because of timeouts in CI +# Below is the default URL of the mirror +# baseurl: https://download.qt.io +baseurl: https://qt.mirror.constant.com + +[requests] +# Mirrors require sha1 instead of sha256 +hash_algorithm: sha1 + +[mirrors] +trusted_mirrors: + https://qt.mirror.constant.com +fallbacks: + https://qt.mirror.constant.com + https://mirrors.ocf.berkeley.edu + https://download.qt.io From 97693fdace73711b11b889dca25ab42acd4085cd Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 15 Apr 2025 13:12:49 -0400 Subject: [PATCH 457/762] Add a maximum number of forward/backward time steps that can be queued --- scwx-qt/scwx-qt.cmake | 2 + .../scwx/qt/manager/timeline_manager.cpp | 22 +++++-- scwx-qt/source/scwx/qt/util/queue_counter.cpp | 49 ++++++++++++++ scwx-qt/source/scwx/qt/util/queue_counter.hpp | 64 +++++++++++++++++++ 4 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/util/queue_counter.cpp create mode 100644 scwx-qt/source/scwx/qt/util/queue_counter.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 4c0ea7cd..30e8adb9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -372,6 +372,7 @@ set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/q_color_modulate.hpp source/scwx/qt/util/q_file_buffer.hpp source/scwx/qt/util/q_file_input_stream.hpp + source/scwx/qt/util/queue_counter.hpp source/scwx/qt/util/time.hpp source/scwx/qt/util/tooltip.hpp) set(SRC_UTIL source/scwx/qt/util/color.cpp @@ -385,6 +386,7 @@ set(SRC_UTIL source/scwx/qt/util/color.cpp source/scwx/qt/util/q_color_modulate.cpp source/scwx/qt/util/q_file_buffer.cpp source/scwx/qt/util/q_file_input_stream.cpp + source/scwx/qt/util/queue_counter.cpp source/scwx/qt/util/time.cpp source/scwx/qt/util/tooltip.cpp) set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index f0870f93..f0c95e53 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,8 @@ enum class Direction // Wait up to 5 seconds for radar sweeps to update static constexpr std::chrono::seconds kRadarSweepMonitorTimeout_ {5}; +// Only allow for 3 steps to be queued at any time +static constexpr size_t kMaxQueuedSteps_ {3}; class TimelineManager::Impl { @@ -80,6 +83,8 @@ public: boost::asio::thread_pool playThreadPool_ {1}; boost::asio::thread_pool selectThreadPool_ {1}; + util::QueueCounter stepCounter_ {kMaxQueuedSteps_}; + std::size_t mapCount_ {0}; std::string radarSite_ {"?"}; std::string previousRadarSite_ {"?"}; @@ -256,7 +261,7 @@ void TimelineManager::AnimationStepEnd() if (p->viewType_ == types::MapTime::Live) { // If the selected view type is live, select the current products - p->SelectTime(); + p->SelectTimeAsync(); } else { @@ -395,8 +400,9 @@ void TimelineManager::Impl::UpdateCacheLimit( { // Calculate the number of volume scans in the loop auto [startTime, endTime] = GetLoopStartAndEndTimes(); - auto startIter = util::GetBoundedElementIterator(volumeTimes, startTime); - auto endIter = util::GetBoundedElementIterator(volumeTimes, endTime); + auto startIter = + scwx::util::GetBoundedElementIterator(volumeTimes, startTime); + auto endIter = scwx::util::GetBoundedElementIterator(volumeTimes, endTime); std::size_t numVolumeScans = std::distance(startIter, endIter) + 1; // Dynamically update maximum cached volume scans to the lesser of @@ -571,7 +577,8 @@ std::pair TimelineManager::Impl::SelectTime( UpdateCacheLimit(radarProductManager, volumeTimes); // Find the best match bounded time - auto elementPtr = util::GetBoundedElementPointer(volumeTimes, selectedTime); + auto elementPtr = + scwx::util::GetBoundedElementPointer(volumeTimes, selectedTime); // The timeline is no longer live Q_EMIT self_->LiveStateUpdated(false); @@ -612,6 +619,12 @@ std::pair TimelineManager::Impl::SelectTime( void TimelineManager::Impl::StepAsync(Direction direction) { + // Prevent too many steps from being added to the queue + if (!stepCounter_.add()) + { + return; + } + boost::asio::post(selectThreadPool_, [=, this]() { @@ -623,6 +636,7 @@ void TimelineManager::Impl::StepAsync(Direction direction) { logger_->error(ex.what()); } + stepCounter_.remove(); }); } diff --git a/scwx-qt/source/scwx/qt/util/queue_counter.cpp b/scwx-qt/source/scwx/qt/util/queue_counter.cpp new file mode 100644 index 00000000..929229e2 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/queue_counter.cpp @@ -0,0 +1,49 @@ +#include + +#include + +namespace scwx::qt::util +{ + +class QueueCounter::Impl +{ +public: + explicit Impl(size_t maxCount) : maxCount_ {maxCount} {} + + const size_t maxCount_; + std::atomic count_ {0}; +}; + +QueueCounter::QueueCounter(size_t maxCount) : + p {std::make_unique(maxCount)} +{ +} + +QueueCounter::~QueueCounter() = default; + +bool QueueCounter::add() +{ + const size_t count = p->count_.fetch_add(1); + // Must be >= (not ==) to avoid race conditions + if (count >= p->maxCount_) + { + p->count_.fetch_sub(1); + return false; + } + else + { + return true; + } +} + +void QueueCounter::remove() +{ + p->count_.fetch_sub(1); +} + +bool QueueCounter::is_lock_free() +{ + return p->count_.is_lock_free(); +} + +} // namespace scwx::qt::util diff --git a/scwx-qt/source/scwx/qt/util/queue_counter.hpp b/scwx-qt/source/scwx/qt/util/queue_counter.hpp new file mode 100644 index 00000000..c540ec63 --- /dev/null +++ b/scwx-qt/source/scwx/qt/util/queue_counter.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +namespace scwx::qt::util +{ + +class QueueCounter +{ +public: + /** + * Counts the number of items in a queue, and prevents it from exceeding a + * count in a thread safe manor. This is lock free, assuming + * std::atomic supports lock free fetch_add and fetch_sub. + */ + + /** + * Construct a QueueCounter with a given maximum count + * + * @param maxCount The maximum number of items in the queue + */ + explicit QueueCounter(size_t maxCount); + + ~QueueCounter(); + QueueCounter(const QueueCounter&) = delete; + QueueCounter(QueueCounter&&) = delete; + QueueCounter& operator=(const QueueCounter&) = delete; + QueueCounter& operator=(QueueCounter&&) = delete; + + /** + * Called before adding an item. If it returns true, it is ok to add. If it + * returns false, it should not be added + * + * @return true if it is ok to add, false if the queue is full + */ + bool add(); + + /** + * Called when item is removed from the queue. Should only be called after a + * corresponding and successful call to add. + */ + void remove(); + + /** + * Tells if this instance is lock free + * + * @return true if it is lock free, false otherwise + */ + bool is_lock_free(); + + /** + * Tells if this class is always lock free. True if it is lock free, false + * otherwise + */ + static constexpr bool is_always_lock_free = + std::atomic::is_always_lock_free; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::qt::util From 3f9f5fcb90dadc339fb10b27b32b8016512c94c7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Apr 2025 09:22:11 -0400 Subject: [PATCH 458/762] Explicitly link atomic for max_time_step_queue_size --- scwx-qt/scwx-qt.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 30e8adb9..86606d9e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -692,6 +692,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Positioning Qt${QT_VERSION_MAJOR}::SerialPort Qt${QT_VERSION_MAJOR}::Svg + atomic Boost::json Boost::timer QMapLibre::Core From b4694d637bb37158e601b99a9d0d815654e32f1f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Apr 2025 10:51:27 -0400 Subject: [PATCH 459/762] Ensure atomic is only linked for non-windows OS's --- scwx-qt/scwx-qt.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 86606d9e..674bc661 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -686,13 +686,17 @@ else() target_compile_options(supercell-wx PRIVATE "$<$:-g>") endif() +# link atomic only for Linux +if (!MSVC) + target_link_libraries(scwx-qt PUBLIC atomic) +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 Qt${QT_VERSION_MAJOR}::SerialPort Qt${QT_VERSION_MAJOR}::Svg - atomic Boost::json Boost::timer QMapLibre::Core From 9f5c126b7fc1856de67b594ae2229ba3f80f98c2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 17 Apr 2025 11:28:29 -0400 Subject: [PATCH 460/762] Use boost::atomic for max_time_step_queue_size, for easier linking --- scwx-qt/scwx-qt.cmake | 6 +----- scwx-qt/source/scwx/qt/util/queue_counter.cpp | 6 +++--- scwx-qt/source/scwx/qt/util/queue_counter.hpp | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 674bc661..89b31011 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -686,11 +686,6 @@ else() target_compile_options(supercell-wx PRIVATE "$<$:-g>") endif() -# link atomic only for Linux -if (!MSVC) - target_link_libraries(scwx-qt PUBLIC atomic) -endif() - target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::OpenGLWidgets Qt${QT_VERSION_MAJOR}::Multimedia @@ -699,6 +694,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg Boost::json Boost::timer + Boost::atomic QMapLibre::Core $<$:opengl32> $<$:SetupAPI> diff --git a/scwx-qt/source/scwx/qt/util/queue_counter.cpp b/scwx-qt/source/scwx/qt/util/queue_counter.cpp index 929229e2..39b9fb9d 100644 --- a/scwx-qt/source/scwx/qt/util/queue_counter.cpp +++ b/scwx-qt/source/scwx/qt/util/queue_counter.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace scwx::qt::util { @@ -10,8 +10,8 @@ class QueueCounter::Impl public: explicit Impl(size_t maxCount) : maxCount_ {maxCount} {} - const size_t maxCount_; - std::atomic count_ {0}; + const size_t maxCount_; + boost::atomic count_ {0}; }; QueueCounter::QueueCounter(size_t maxCount) : diff --git a/scwx-qt/source/scwx/qt/util/queue_counter.hpp b/scwx-qt/source/scwx/qt/util/queue_counter.hpp index c540ec63..471bd645 100644 --- a/scwx-qt/source/scwx/qt/util/queue_counter.hpp +++ b/scwx-qt/source/scwx/qt/util/queue_counter.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include namespace scwx::qt::util { @@ -54,7 +54,7 @@ public: * otherwise */ static constexpr bool is_always_lock_free = - std::atomic::is_always_lock_free; + boost::atomic::is_always_lock_free; private: class Impl; From d6e2bfe9abc833819f9ed873938a9bf7939fd115 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 22 Apr 2025 11:04:48 -0400 Subject: [PATCH 461/762] Level 2 azimuth angle is the center of the radial, not the start --- .../scwx/qt/view/level2_product_view.cpp | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 6fcc775c..6344dff0 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -1131,7 +1131,7 @@ void Level2ProductView::Impl::ComputeCoordinates( units::degrees angle {}; auto radialData = radarData->find(radial); - if (radialData != radarData->cend() && !smoothingEnabled) + if (radialData != radarData->cend() && smoothingEnabled) { angle = radialData->second->azimuth_angle(); } @@ -1143,7 +1143,7 @@ void Level2ProductView::Impl::ComputeCoordinates( (radial >= 2) ? radial - 2 : numRadials - (2 - radial)); if (radialData != radarData->cend() && - prevRadial1 != radarData->cend() && smoothingEnabled) + prevRadial1 != radarData->cend() && !smoothingEnabled) { const units::degrees currentAngle = radialData->second->azimuth_angle(); @@ -1154,13 +1154,13 @@ void Level2ProductView::Impl::ComputeCoordinates( const units::degrees deltaAngle = NormalizeAngle(currentAngle - prevAngle); - // Delta scale is half the delta angle to reach the center of the - // bin, because smoothing is enabled + // Delta scale is half the delta angle to reach the end of the + // bin, because smoothing is not enabled constexpr float deltaScale = 0.5f; - angle = currentAngle + deltaAngle * deltaScale; + angle = currentAngle - deltaAngle * deltaScale; } - else if (radialData != radarData->cend() && smoothingEnabled) + else if (radialData != radarData->cend() && !smoothingEnabled) { const units::degrees currentAngle = radialData->second->azimuth_angle(); @@ -1169,11 +1169,11 @@ void Level2ProductView::Impl::ComputeCoordinates( // to determine a delta angle constexpr units::degrees deltaAngle {0.5f}; - // Delta scale is half the delta angle to reach the center of the + // Delta scale is half the delta angle to reach the edge of the // bin, because smoothing is enabled constexpr float deltaScale = 0.5f; - angle = currentAngle + deltaAngle * deltaScale; + angle = currentAngle - deltaAngle * deltaScale; } else if (prevRadial1 != radarData->cend() && prevRadial2 != radarData->cend()) @@ -1189,11 +1189,12 @@ void Level2ProductView::Impl::ComputeCoordinates( const float deltaScale = (smoothingEnabled) ? - // Delta scale is 1.5x the delta angle to reach the center + // Delta scale is 1.0x the delta angle to reach the center // of the next bin, because smoothing is enabled - 1.5f : - // Delta scale is 1.0x the delta angle - 1.0f; + 1.0f : + // Delta scale is 0.5x the delta angle to reach the edge of + // the next bin + 0.5f; angle = prevAngle1 + deltaAngle * deltaScale; } @@ -1208,11 +1209,12 @@ void Level2ProductView::Impl::ComputeCoordinates( const float deltaScale = (smoothingEnabled) ? - // Delta scale is 1.5x the delta angle to reach the center + // Delta scale is 1.0x the delta angle to reach the center // of the next bin, because smoothing is enabled - 1.5f : - // Delta scale is 1.0x the delta angle - 1.0f; + 1.0f : + // Delta scale is 0.5x the delta angle to reach the edge of + // the next bin + 0.5f; angle = prevAngle1 + deltaAngle * deltaScale; } @@ -1368,6 +1370,13 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const if (nextRadial != radarData->cend()) { nextAngle = nextRadial->second->azimuth_angle(); + + // Level 2 angles are the center of the bins. + const units::degrees deltaAngle = + common::GetAngleDelta(startAngle, nextAngle); + startAngle -= deltaAngle / 2; + nextAngle -= deltaAngle / 2; + hasNextAngle = true; } else @@ -1384,7 +1393,9 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const const units::degrees deltaAngle = common::GetAngleDelta(startAngle, prevAngle); - nextAngle = startAngle + deltaAngle; + // Level 2 angles are the center of the bins. + nextAngle = startAngle + deltaAngle / 2; + startAngle -= deltaAngle / 2; hasNextAngle = true; } } From 152d0722420591a9b3e427b58539bda63a729cd6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 22 Apr 2025 13:58:00 -0400 Subject: [PATCH 462/762] Disable compile time selection of certs on Linux. SSL cert location is not standardized --- conanfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 031228eb..e68c9f39 100644 --- a/conanfile.py +++ b/conanfile.py @@ -31,7 +31,9 @@ class SupercellWxConan(ConanFile): if self.settings.os == "Windows": self.options["libcurl"].with_ssl = "schannel" elif self.settings.os == "Linux": - self.options["openssl"].shared = True + self.options["openssl"].shared = True + self.options["libcurl"].ca_bundle = "none" + self.options["libcurl"].ca_path = "none" def requirements(self): if self.settings.os == "Linux": From 27cfabcc660b11271a1f447ea816b828e1bdb31a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 25 Apr 2025 08:22:12 -0500 Subject: [PATCH 463/762] Bump version to v0.4.9 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef0d18fd..7aff49e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.8 + SCWX_VERSION: v0.4.9 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 0184bb65..bda4b47c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} - VERSION 0.4.8 + VERSION 0.4.9 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -27,7 +27,7 @@ set_property(DIRECTORY set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.8") +set(SCWX_VERSION "0.4.9") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) From 071c2f60bf7a333fac9f066c05f6277aaf93d07a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 10:26:35 -0500 Subject: [PATCH 464/762] Bump clang-format to 19 --- .github/workflows/clang-format-check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index acdcd586..aec8cac3 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -30,11 +30,11 @@ jobs: - name: Setup Ubuntu Environment shell: bash run: | - sudo apt-get install clang-format-17 + sudo apt-get install clang-format-19 - name: Check Formatting shell: bash run: | MERGE_BASE=$(git merge-base origin/develop ${{ github.event.pull_request.head.sha || github.ref }}) echo "Comparing against ${MERGE_BASE}" - git clang-format-17 --diff --style=file -v ${MERGE_BASE} + git clang-format-19 --diff --style=file -v ${MERGE_BASE} From 3168725b00d5fe33934d621683110da484690f39 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 10:57:49 -0500 Subject: [PATCH 465/762] Update clang-format symlink to 19 before formatting --- .github/workflows/clang-format-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index aec8cac3..02c5cdd3 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -31,6 +31,8 @@ jobs: shell: bash run: | sudo apt-get install clang-format-19 + sudo rm -f /usr/bin/clang-format + sudo ln -s /usr/bin/clang-format-19 /usr/bin/clang-format - name: Check Formatting shell: bash From 64c2555b4bbffc22cb5a5835e2fb7027a2f2a3a1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 18:36:06 -0500 Subject: [PATCH 466/762] Bump clang-tidy-review to clang-18, include *.ipp in review --- .github/workflows/clang-tidy-review.yml | 13 +++++++------ tools/conan/profiles/scwx-linux_clang-18 | 8 ++++++++ tools/conan/profiles/scwx-linux_clang-18_armv8 | 8 ++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 tools/conan/profiles/scwx-linux_clang-18 create mode 100644 tools/conan/profiles/scwx-linux_clang-18_armv8 diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index c7297103..bd88980d 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -18,15 +18,16 @@ jobs: - name: linux_clang-tidy_x64 os: ubuntu-24.04 build_type: Release - env_cc: clang-17 - env_cxx: clang++-17 + env_cc: clang-18 + env_cxx: clang++-18 qt_version: 6.8.1 qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' conan_package_manager: --conf tools.system.package_manager:mode=install --conf tools.system.package_manager:sudo=True - conan_profile: scwx-linux_clang-17 - compiler_packages: clang-17 clang-tidy-17 + conan_profile: scwx-linux_clang-18 + compiler_packages: clang-18 clang-tidy-18 + clang_tidy_binary: clang-tidy-18 name: ${{ matrix.name }} runs-on: ${{ matrix.os }} env: @@ -118,7 +119,7 @@ jobs: shell: bash run: | cd source - review --clang_tidy_binary=clang-tidy-17 \ + review --clang_tidy_binary=${{ matrix.clang_tidy_binary }} \ --token=${{ github.token }} \ --repo='${{ github.repository }}' \ --pr='${{ github.event.pull_request.number }}' \ @@ -126,7 +127,7 @@ jobs: --base_dir='${{ github.workspace }}/source' \ --clang_tidy_checks='' \ --config_file='.clang-tidy' \ - --include='*.[ch],*.[ch]xx,*.[ch]pp,*.[ch]++,*.cc,*.hh' \ + --include='*.[ch],*.[ch]xx,*.[chi]pp,*.[ch]++,*.cc,*.hh' \ --exclude='' \ --apt-packages='' \ --cmake-command='' \ diff --git a/tools/conan/profiles/scwx-linux_clang-18 b/tools/conan/profiles/scwx-linux_clang-18 new file mode 100644 index 00000000..ea19016b --- /dev/null +++ b/tools/conan/profiles/scwx-linux_clang-18 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=18 +os=Linux diff --git a/tools/conan/profiles/scwx-linux_clang-18_armv8 b/tools/conan/profiles/scwx-linux_clang-18_armv8 new file mode 100644 index 00000000..8c567fd4 --- /dev/null +++ b/tools/conan/profiles/scwx-linux_clang-18_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libstdc++11 +compiler.version=18 +os=Linux From 72f4296e1e2138919ff5eb74722bf1d887a00f51 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 22:49:23 -0500 Subject: [PATCH 467/762] Updating maplibre-native to latest as of 2025-04-27 --- external/maplibre-native | 2 +- external/maplibre-native-qt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/maplibre-native b/external/maplibre-native index a77cb6b2..fc96f28e 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit a77cb6b2d1cde3a5f2c147e25245aa019aacb74b +Subproject commit fc96f28ea8a6a80d400b2f1e89801e6415c4efaa diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt index 3939a82a..527734fd 160000 --- a/external/maplibre-native-qt +++ b/external/maplibre-native-qt @@ -1 +1 @@ -Subproject commit 3939a82ab9c2ff8bc2746e58ab9727cd12caef2c +Subproject commit 527734fdcacf8d85185776f4b020b94a8c937cdd From db20e21ccbe61a15efc4732dac5cc2fa08697299 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 23:10:03 -0500 Subject: [PATCH 468/762] Bump dependency Qt to 6.8.3 --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/clang-tidy-review.yml | 2 +- setup-debug.bat | 2 +- setup-debug.sh | 2 +- setup-multi.bat | 2 +- setup-release.bat | 2 +- setup-release.sh | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7aff49e9..986a5b65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: compiler: msvc msvc_arch: x64 msvc_version: 2022 - qt_version: 6.8.1 + qt_version: 6.8.3 qt_arch_aqt: win64_msvc2022_64 qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -43,7 +43,7 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc - qt_version: 6.8.1 + qt_version: 6.8.3 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -59,7 +59,7 @@ jobs: env_cc: clang-17 env_cxx: clang++-17 compiler: clang - qt_version: 6.8.1 + qt_version: 6.8.3 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -75,7 +75,7 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc - qt_version: 6.8.1 + qt_version: 6.8.3 qt_arch_aqt: linux_gcc_arm64 qt_arch_dir: gcc_arm64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index bd88980d..c37236d6 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -20,7 +20,7 @@ jobs: build_type: Release env_cc: clang-18 env_cxx: clang++-18 - qt_version: 6.8.1 + qt_version: 6.8.3 qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' diff --git a/setup-debug.bat b/setup-debug.bat index afcd52c2..e8076f02 100644 --- a/setup-debug.bat +++ b/setup-debug.bat @@ -3,7 +3,7 @@ call tools\setup-common.bat set build_dir=build-debug set build_type=Debug set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.1 +set qt_version=6.8.3 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-debug.sh b/setup-debug.sh index e48efe9f..689ac617 100755 --- a/setup-debug.sh +++ b/setup-debug.sh @@ -4,7 +4,7 @@ build_dir=${1:-build-debug} build_type=Debug conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.1 +qt_version=6.8.3 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" diff --git a/setup-multi.bat b/setup-multi.bat index 9a51b1f9..b7f0b8df 100644 --- a/setup-multi.bat +++ b/setup-multi.bat @@ -2,7 +2,7 @@ call tools\setup-common.bat set build_dir=build set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.1 +set qt_version=6.8.3 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-release.bat b/setup-release.bat index a5481508..e019e204 100644 --- a/setup-release.bat +++ b/setup-release.bat @@ -3,7 +3,7 @@ call tools\setup-common.bat set build_dir=build-release set build_type=Release set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.1 +set qt_version=6.8.3 set qt_arch=msvc2022_64 conan config install tools/conan/profiles/%conan_profile% -tf profiles diff --git a/setup-release.sh b/setup-release.sh index d242ca1c..8d2b5fe6 100755 --- a/setup-release.sh +++ b/setup-release.sh @@ -4,7 +4,7 @@ build_dir=${1:-build-release} build_type=Release conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.1 +qt_version=6.8.3 qt_arch=gcc_64 script_dir="$(dirname "$(readlink -f "$0")")" From e47a3c28c93c2ba57ca3981a9ac172d1f5c176d5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 28 Apr 2025 22:25:40 -0500 Subject: [PATCH 469/762] Revert "Updating maplibre-native to latest as of 2025-04-27" --- external/maplibre-native | 2 +- external/maplibre-native-qt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/maplibre-native b/external/maplibre-native index fc96f28e..a77cb6b2 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit fc96f28ea8a6a80d400b2f1e89801e6415c4efaa +Subproject commit a77cb6b2d1cde3a5f2c147e25245aa019aacb74b diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt index 527734fd..3939a82a 160000 --- a/external/maplibre-native-qt +++ b/external/maplibre-native-qt @@ -1 +1 @@ -Subproject commit 527734fdcacf8d85185776f4b020b94a8c937cdd +Subproject commit 3939a82ab9c2ff8bc2746e58ab9727cd12caef2c From 2569e3e3d8230978677289f74b0e1e6f61db8771 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 29 Apr 2025 00:16:48 -0500 Subject: [PATCH 470/762] Update maplibre-native to the last legacy renderer commit --- external/maplibre-native | 2 +- external/maplibre-native-qt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/maplibre-native b/external/maplibre-native index a77cb6b2..554e6e9a 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit a77cb6b2d1cde3a5f2c147e25245aa019aacb74b +Subproject commit 554e6e9ac46b6eaf5970a219c88e3df11f1cee30 diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt index 3939a82a..527734fd 160000 --- a/external/maplibre-native-qt +++ b/external/maplibre-native-qt @@ -1 +1 @@ -Subproject commit 3939a82ab9c2ff8bc2746e58ab9727cd12caef2c +Subproject commit 527734fdcacf8d85185776f4b020b94a8c937cdd From 22ed4c36fc1000988c6ed7a501896b898f37c71c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 3 May 2025 10:13:54 -0400 Subject: [PATCH 471/762] Add radar altitude data form config json file into code --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 8 +++++ scwx-qt/source/scwx/qt/config/radar_site.hpp | 34 +++++++++----------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index 5e49847a..5c1dba2e 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -51,6 +51,7 @@ public: std::string state_ {}; std::string place_ {}; std::string tzName_ {}; + double altitude_ {0.0}; const scwx::util::time_zone* timeZone_ {nullptr}; }; @@ -142,6 +143,11 @@ const scwx::util::time_zone* RadarSite::time_zone() const return p->timeZone_; } +units::length::feet RadarSite::altitude() const +{ + return units::length::feet(p->altitude_); +} + std::shared_ptr RadarSite::Get(const std::string& id) { std::shared_lock lock(siteMutex_); @@ -268,6 +274,8 @@ size_t RadarSite::ReadConfig(const std::string& path) site->p->state_ = boost::json::value_to(o.at("state")); site->p->place_ = boost::json::value_to(o.at("place")); site->p->tzName_ = boost::json::value_to(o.at("tz")); + site->p->altitude_ = + boost::json::value_to(o.at("elevation")); try { diff --git a/scwx-qt/source/scwx/qt/config/radar_site.hpp b/scwx-qt/source/scwx/qt/config/radar_site.hpp index 16f6e710..cf622d7a 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.hpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.hpp @@ -6,12 +6,9 @@ #include #include #include +#include -namespace scwx -{ -namespace qt -{ -namespace config +namespace scwx::qt::config { class RadarSiteImpl; @@ -28,18 +25,19 @@ public: RadarSite(RadarSite&&) noexcept; RadarSite& operator=(RadarSite&&) noexcept; - std::string type() const; - std::string type_name() const; - std::string id() const; - double latitude() const; - double longitude() const; - std::string country() const; - std::string state() const; - std::string place() const; - std::string location_name() const; - std::string tz_name() const; + [[nodiscard]] std::string type() const; + [[nodiscard]] std::string type_name() const; + [[nodiscard]] std::string id() const; + [[nodiscard]] double latitude() const; + [[nodiscard]] double longitude() const; + [[nodiscard]] std::string country() const; + [[nodiscard]] std::string state() const; + [[nodiscard]] std::string place() const; + [[nodiscard]] std::string location_name() const; + [[nodiscard]] std::string tz_name() const; + [[nodiscard]] units::length::feet altitude() const; - const scwx::util::time_zone* time_zone() const; + [[nodiscard]] const scwx::util::time_zone* time_zone() const; static std::shared_ptr Get(const std::string& id); static std::vector> GetAll(); @@ -67,6 +65,4 @@ private: std::string GetRadarIdFromSiteId(const std::string& siteId); -} // namespace config -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::config From b84c36c91ab49ec69a9fc28cc165b78fb73a77e3 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 3 May 2025 10:14:53 -0400 Subject: [PATCH 472/762] Add GetRadarBeamAltititude into geographic lib --- .../source/scwx/qt/util/geographic_lib.cpp | 20 +++++++++++++++++++ .../source/scwx/qt/util/geographic_lib.hpp | 15 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index bfaf408f..8181f34c 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -289,6 +290,25 @@ bool AreaInRangeOfPoint(const std::vector& area, return GetDistanceAreaPoint(area, point) <= distance; } +units::length::meters +GetRadarBeamAltititude(units::length::meters range, + units::angle::degrees elevation, + units::length::meters height) +{ + static const units::length::meters earthRadius {6367444 * 4/3}; + + height += earthRadius; + + const double elevationRadians = + units::angle::radians(elevation).value(); + const auto altitudeSquared = + (range * range + height * height + + 2 * range * height * std::sin(elevationRadians)); + + return units::length::meters(std::sqrt(altitudeSquared.value())) - + earthRadius; +} + } // 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 5038d9a9..1d6cc38e 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -121,6 +121,21 @@ bool AreaInRangeOfPoint(const std::vector& area, const common::Coordinate& point, const units::length::meters distance); +/** + * Get the altitude of the radar beam at a given distance, elevation and height + * + * @param [in] range The range to the radar site + * @param [in] elevation The elevation of the radar site + * @param [in] height The height of the radar site + * + * @return The altitude of the radar at that range + */ +units::length::meters +GetRadarBeamAltititude(units::length::meters range, + units::angle::degrees elevation, + units::length::meters height); + + } // namespace GeographicLib } // namespace util } // namespace qt From d21a11963fdbf03d4921ee55f191952373517b71 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 3 May 2025 10:15:28 -0400 Subject: [PATCH 473/762] Add current radar site to map context --- scwx-qt/source/scwx/qt/map/map_context.cpp | 11 +++++++++++ scwx-qt/source/scwx/qt/map/map_context.hpp | 22 +++++++++++----------- scwx-qt/source/scwx/qt/map/map_widget.cpp | 3 +++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 46c2b6fa..a0c1e74a 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -27,6 +27,7 @@ public: common::RadarProductGroup::Unknown}; std::string radarProduct_ {"???"}; int16_t radarProductCode_ {0}; + std::shared_ptr radarSite_ {nullptr}; MapProvider mapProvider_ {MapProvider::Unknown}; std::string mapCopyrights_ {}; @@ -106,6 +107,11 @@ std::string MapContext::radar_product() const return p->radarProduct_; } +std::shared_ptr MapContext::radar_site() const +{ + return p->radarSite_; +} + int16_t MapContext::radar_product_code() const { return p->radarProductCode_; @@ -174,6 +180,11 @@ void MapContext::set_radar_product_code(int16_t radarProductCode) p->radarProductCode_ = radarProductCode; } +void MapContext::set_radar_site(const std::shared_ptr& site) +{ + p->radarSite_ = site; +} + void MapContext::set_widget(QWidget* widget) { p->widget_ = widget; diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 680f9ddd..39a5c1be 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -4,13 +4,12 @@ #include #include #include +#include #include #include -namespace scwx -{ -namespace qt +namespace scwx::qt { namespace view { @@ -30,7 +29,7 @@ class MapContext : public gl::GlContext public: explicit MapContext( std::shared_ptr radarProductView = nullptr); - ~MapContext(); + ~MapContext() override; MapContext(const MapContext&) = delete; MapContext& operator=(const MapContext&) = delete; @@ -48,11 +47,12 @@ public: [[nodiscard]] std::shared_ptr overlay_product_view() const; [[nodiscard]] std::shared_ptr - radar_product_view() const; - [[nodiscard]] common::RadarProductGroup radar_product_group() const; - [[nodiscard]] std::string radar_product() const; - [[nodiscard]] int16_t radar_product_code() const; - [[nodiscard]] QWidget* widget() const; + radar_product_view() const; + [[nodiscard]] common::RadarProductGroup radar_product_group() const; + [[nodiscard]] std::string radar_product() const; + [[nodiscard]] int16_t radar_product_code() const; + [[nodiscard]] std::shared_ptr radar_site() const; + [[nodiscard]] QWidget* widget() const; void set_map(const std::shared_ptr& map); void set_map_copyrights(const std::string& copyrights); @@ -67,6 +67,7 @@ public: void set_radar_product_group(common::RadarProductGroup radarProductGroup); void set_radar_product(const std::string& radarProduct); void set_radar_product_code(int16_t radarProductCode); + void set_radar_site(const std::shared_ptr& site); void set_widget(QWidget* widget); private: @@ -76,5 +77,4 @@ private: }; } // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index abf5d484..f9b898c1 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -2008,6 +2008,9 @@ void MapWidgetImpl::SelectNearestRadarSite(double latitude, void MapWidgetImpl::SetRadarSite(const std::string& radarSite, bool checkProductAvailability) { + // Set the radar site in the context + context_->set_radar_site(config::RadarSite::Get(radarSite)); + // Check if radar site has changed if (radarProductManager_ == nullptr || radarSite != radarProductManager_->radar_site()->id()) From 33f92bcda574fbb0a5646bcea333f6ec5e5393a1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 3 May 2025 10:16:20 -0400 Subject: [PATCH 474/762] Add radar line and radar distance/altitude tooltip --- .../scwx/qt/map/radar_product_layer.cpp | 80 +++++++++++++++++-- .../source/scwx/qt/map/radar_site_layer.cpp | 71 +++++++++++++++- 2 files changed, 141 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 8d243973..f305f4f7 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -1,6 +1,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -353,11 +356,66 @@ bool RadarProductLayer::RunMousePicking( std::shared_ptr radarProductView = context()->radar_product_view(); - if (radarProductView == nullptr) + if (context()->radar_site() == nullptr) { return itemPicked; } + // Get distance and altitude of point + const double radarLatitude = context()->radar_site()->latitude(); + const double radarLongitude = context()->radar_site()->longitude(); + + const auto distanceMeters = util::GeographicLib::GetDistance( + mouseGeoCoords.latitude_, + mouseGeoCoords.longitude_, + radarLatitude, + radarLongitude); + + const std::string distanceUnitName = + settings::UnitSettings::Instance().distance_units().GetValue(); + const types::DistanceUnits distanceUnits = + types::GetDistanceUnitsFromName(distanceUnitName); + const double distanceScale = types::GetDistanceUnitsScale(distanceUnits); + const std::string distanceAbbrev = + types::GetDistanceUnitsAbbreviation(distanceUnits); + + const double distance = distanceMeters.value() * + scwx::common::kKilometersPerMeter * distanceScale; + std::string distanceHeightStr = + fmt::format("{:.2f} {}", distance, distanceAbbrev); + + if (radarProductView == nullptr) + { + util::tooltip::Show(distanceHeightStr, mouseGlobalPos); + itemPicked = true; + return itemPicked; + } + + std::optional elevation = radarProductView->elevation(); + if (elevation.has_value()) + { + const auto altitudeMeters = + util::GeographicLib::GetRadarBeamAltititude( + distanceMeters, + units::angle::degrees(*elevation), + context()->radar_site()->altitude()); + + const std::string heightUnitName = + settings::UnitSettings::Instance().echo_tops_units().GetValue(); + const types::EchoTopsUnits heightUnits = + types::GetEchoTopsUnitsFromName(heightUnitName); + const double heightScale = types::GetEchoTopsUnitsScale(heightUnits); + const std::string heightAbbrev = + types::GetEchoTopsUnitsAbbreviation(heightUnits); + + const double altitude = altitudeMeters.value() * + scwx::common::kKilometersPerMeter * + heightScale; + + distanceHeightStr = fmt::format( + "{}\n{:.2f} {}", distanceHeightStr, altitude, heightAbbrev); + } + std::optional binLevel = radarProductView->GetBinLevel(mouseGeoCoords); @@ -383,12 +441,13 @@ bool RadarProductLayer::RunMousePicking( if (codeName != codeShortName && !codeShortName.empty()) { // There is a unique long and short name for the code - hoverText = fmt::format("{}: {}", codeShortName, codeName); + hoverText = fmt::format( + "{}: {}\n{}", codeShortName, codeName, distanceHeightStr); } else { // Otherwise, only use the long name (always present) - hoverText = codeName; + hoverText = fmt::format("{}\n{}", codeName, distanceHeightStr); } // Show the tooltip @@ -439,17 +498,20 @@ bool RadarProductLayer::RunMousePicking( { // Don't display a units value that wasn't intended to be // displayed - hoverText = fmt::format("{}{}", f, suffix); + hoverText = + fmt::format("{}{}\n{}", f, suffix, distanceHeightStr); } else if (std::isalpha(static_cast(units.at(0)))) { // dBZ, Kts, etc. - hoverText = fmt::format("{} {}{}", f, units, suffix); + hoverText = fmt::format( + "{} {}{}\n{}", f, units, suffix, distanceHeightStr); } else { // %, etc. - hoverText = fmt::format("{}{}{}", f, units, suffix); + hoverText = fmt::format( + "{}{}{}\n{}", f, units, suffix, distanceHeightStr); } // Show the tooltip @@ -458,6 +520,12 @@ bool RadarProductLayer::RunMousePicking( itemPicked = true; } } + else + { + // Always show tooltip for distance and altitude + util::tooltip::Show(distanceHeightStr, mouseGlobalPos); + itemPicked = true; + } } return itemPicked; diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 65b53f14..67c0ed0e 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -10,6 +11,8 @@ #include #include +#include + namespace scwx { namespace qt @@ -23,11 +26,32 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class RadarSiteLayer::Impl { public: - explicit Impl(RadarSiteLayer* self) : self_ {self} {} + explicit Impl(RadarSiteLayer* self, std::shared_ptr& context) : + self_ {self}, + geoLines_ {std::make_shared(context)} + { + geoLines_->StartLines(); + radarSiteLines_[0] = geoLines_->AddLine(); + radarSiteLines_[1] = geoLines_->AddLine(); + geoLines_->FinishLines(); + + static const boost::gil::rgba32f_pixel_t color0 {0.0f, 0.0f, 0.0f, 1.0f}; + static const boost::gil::rgba32f_pixel_t color1 {1.0f, 1.0f, 1.0f, 1.0f}; + static const float width = 1; + geoLines_->SetLineModulate(radarSiteLines_[0], color0); + geoLines_->SetLineWidth(radarSiteLines_[0], width + 2); + + geoLines_->SetLineModulate(radarSiteLines_[1], color1); + geoLines_->SetLineWidth(radarSiteLines_[1], width); + + self_->AddDrawItem(geoLines_); + geoLines_->set_thresholded(false); + } ~Impl() = default; void RenderRadarSite(const QMapLibre::CustomLayerRenderParameters& params, std::shared_ptr& radarSite); + void RenderRadarLine(); RadarSiteLayer* self_; @@ -41,10 +65,13 @@ public: float halfHeight_ {}; std::string hoverText_ {}; + + std::shared_ptr geoLines_; + std::array, 2> radarSiteLines_; }; RadarSiteLayer::RadarSiteLayer(std::shared_ptr context) : - DrawLayer(context, "RadarSiteLayer"), p(std::make_unique(this)) + DrawLayer(context, "RadarSiteLayer"), p(std::make_unique(this, context)) { } @@ -56,7 +83,7 @@ void RadarSiteLayer::Initialize() p->radarSites_ = config::RadarSite::GetAll(); - ImGuiInitialize(); + DrawLayer::Initialize(); } void RadarSiteLayer::Render( @@ -96,8 +123,12 @@ void RadarSiteLayer::Render( } ImGui::PopStyleVar(); - ImGuiFrameEnd(); + p->RenderRadarLine(); + + DrawLayer::RenderWithoutImGui(params); + + ImGuiFrameEnd(); SCWX_GL_CHECK_ERROR(); } @@ -163,6 +194,38 @@ void RadarSiteLayer::Impl::RenderRadarSite( } } +void RadarSiteLayer::Impl::RenderRadarLine() +{ + // TODO check if state is updated. + if ((QGuiApplication::keyboardModifiers() & + Qt::KeyboardModifier::ShiftModifier) && + self_->context()->radar_site() != nullptr) + { + const auto& mouseCoord = self_->context()->mouse_coordinate(); + const double radarLatitude = self_->context()->radar_site()->latitude(); + const double radarLongitude = self_->context()->radar_site()->longitude(); + + geoLines_->SetLineLocation(radarSiteLines_[0], + static_cast(mouseCoord.latitude_), + static_cast(mouseCoord.longitude_), + static_cast(radarLatitude), + static_cast(radarLongitude)); + geoLines_->SetLineVisible(radarSiteLines_[0], true); + + geoLines_->SetLineLocation(radarSiteLines_[1], + static_cast(mouseCoord.latitude_), + static_cast(mouseCoord.longitude_), + static_cast(radarLatitude), + static_cast(radarLongitude)); + geoLines_->SetLineVisible(radarSiteLines_[1], true); + } + else + { + geoLines_->SetLineVisible(radarSiteLines_[0], false); + geoLines_->SetLineVisible(radarSiteLines_[1], false); + } +} + void RadarSiteLayer::Deinitialize() { logger_->debug("Deinitialize()"); From ef197bf57889c3c485f9dcc7bde9a75af9efbfff Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 3 May 2025 10:24:02 -0400 Subject: [PATCH 475/762] Clang format/tidy fixes for radar_site_line --- scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 10 +++++----- scwx-qt/source/scwx/qt/map/radar_site_layer.cpp | 8 ++++---- scwx-qt/source/scwx/qt/util/geographic_lib.cpp | 2 +- scwx-qt/source/scwx/qt/util/geographic_lib.hpp | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index f305f4f7..0d2b7125 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -365,11 +365,11 @@ bool RadarProductLayer::RunMousePicking( const double radarLatitude = context()->radar_site()->latitude(); const double radarLongitude = context()->radar_site()->longitude(); - const auto distanceMeters = util::GeographicLib::GetDistance( - mouseGeoCoords.latitude_, - mouseGeoCoords.longitude_, - radarLatitude, - radarLongitude); + const auto distanceMeters = + util::GeographicLib::GetDistance(mouseGeoCoords.latitude_, + mouseGeoCoords.longitude_, + radarLatitude, + radarLongitude); const std::string distanceUnitName = settings::UnitSettings::Instance().distance_units().GetValue(); diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 67c0ed0e..3e2f3da8 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -27,8 +27,7 @@ class RadarSiteLayer::Impl { public: explicit Impl(RadarSiteLayer* self, std::shared_ptr& context) : - self_ {self}, - geoLines_ {std::make_shared(context)} + self_ {self}, geoLines_ {std::make_shared(context)} { geoLines_->StartLines(); radarSiteLines_[0] = geoLines_->AddLine(); @@ -37,7 +36,7 @@ public: static const boost::gil::rgba32f_pixel_t color0 {0.0f, 0.0f, 0.0f, 1.0f}; static const boost::gil::rgba32f_pixel_t color1 {1.0f, 1.0f, 1.0f, 1.0f}; - static const float width = 1; + static const float width = 1; geoLines_->SetLineModulate(radarSiteLines_[0], color0); geoLines_->SetLineWidth(radarSiteLines_[0], width + 2); @@ -71,7 +70,8 @@ public: }; RadarSiteLayer::RadarSiteLayer(std::shared_ptr context) : - DrawLayer(context, "RadarSiteLayer"), p(std::make_unique(this, context)) + DrawLayer(context, "RadarSiteLayer"), + p(std::make_unique(this, context)) { } diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 8181f34c..1e5fa5e7 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -295,7 +295,7 @@ GetRadarBeamAltititude(units::length::meters range, units::angle::degrees elevation, units::length::meters height) { - static const units::length::meters earthRadius {6367444 * 4/3}; + static const units::length::meters earthRadius {6367444 * 4 / 3}; height += earthRadius; diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index 1d6cc38e..d34f2aeb 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -135,7 +135,6 @@ GetRadarBeamAltititude(units::length::meters range, units::angle::degrees elevation, units::length::meters height); - } // namespace GeographicLib } // namespace util } // namespace qt From 4f58827c96b23e3666282b3f97911ddc108aad8e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 4 May 2025 10:36:00 -0400 Subject: [PATCH 476/762] Move initialization for radar site lines to correct location --- .../source/scwx/qt/map/radar_site_layer.cpp | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 3e2f3da8..6a9f6d1a 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -29,22 +29,6 @@ public: explicit Impl(RadarSiteLayer* self, std::shared_ptr& context) : self_ {self}, geoLines_ {std::make_shared(context)} { - geoLines_->StartLines(); - radarSiteLines_[0] = geoLines_->AddLine(); - radarSiteLines_[1] = geoLines_->AddLine(); - geoLines_->FinishLines(); - - static const boost::gil::rgba32f_pixel_t color0 {0.0f, 0.0f, 0.0f, 1.0f}; - static const boost::gil::rgba32f_pixel_t color1 {1.0f, 1.0f, 1.0f, 1.0f}; - static const float width = 1; - geoLines_->SetLineModulate(radarSiteLines_[0], color0); - geoLines_->SetLineWidth(radarSiteLines_[0], width + 2); - - geoLines_->SetLineModulate(radarSiteLines_[1], color1); - geoLines_->SetLineWidth(radarSiteLines_[1], width); - - self_->AddDrawItem(geoLines_); - geoLines_->set_thresholded(false); } ~Impl() = default; @@ -66,7 +50,8 @@ public: std::string hoverText_ {}; std::shared_ptr geoLines_; - std::array, 2> radarSiteLines_; + std::array, 2> radarSiteLines_ { + nullptr, nullptr}; }; RadarSiteLayer::RadarSiteLayer(std::shared_ptr context) : @@ -83,6 +68,23 @@ void RadarSiteLayer::Initialize() p->radarSites_ = config::RadarSite::GetAll(); + p->geoLines_->StartLines(); + p->radarSiteLines_[0] = p->geoLines_->AddLine(); + p->radarSiteLines_[1] = p->geoLines_->AddLine(); + p->geoLines_->FinishLines(); + + static const boost::gil::rgba32f_pixel_t color0 {0.0f, 0.0f, 0.0f, 1.0f}; + static const boost::gil::rgba32f_pixel_t color1 {1.0f, 1.0f, 1.0f, 1.0f}; + static const float width = 1; + p->geoLines_->SetLineModulate(p->radarSiteLines_[0], color0); + p->geoLines_->SetLineWidth(p->radarSiteLines_[0], width + 2); + + p->geoLines_->SetLineModulate(p->radarSiteLines_[1], color1); + p->geoLines_->SetLineWidth(p->radarSiteLines_[1], width); + + AddDrawItem(p->geoLines_); + p->geoLines_->set_thresholded(false); + DrawLayer::Initialize(); } @@ -196,7 +198,6 @@ void RadarSiteLayer::Impl::RenderRadarSite( void RadarSiteLayer::Impl::RenderRadarLine() { - // TODO check if state is updated. if ((QGuiApplication::keyboardModifiers() & Qt::KeyboardModifier::ShiftModifier) && self_->context()->radar_site() != nullptr) From 87af6479d6bf07189d76bc4e813b6e6b24d207ed Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 00:25:00 -0600 Subject: [PATCH 477/762] Rewrite warnings provider to use HEAD requests instead of directory listing to find recent warnings --- .../scwx/qt/manager/text_event_manager.cpp | 46 ++- .../scwx/provider/warnings_provider.hpp | 4 +- .../scwx/provider/warnings_provider.cpp | 305 ++++++++++-------- 3 files changed, 208 insertions(+), 147 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index e48ecd48..0a7c66f5 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -22,8 +22,10 @@ namespace manager static const std::string logPrefix_ = "scwx::qt::manager::text_event_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static const std::string& kDefaultWarningsProviderUrl { - "https://warnings.allisonhouse.com"}; +static constexpr std::chrono::hours kInitialLoadHistoryDuration_ = + std::chrono::days {3}; +static constexpr std::chrono::hours kDefaultLoadHistoryDuration_ = + std::chrono::hours {1}; class TextEventManager::Impl { @@ -42,7 +44,9 @@ public: warningsProviderChangedCallbackUuid_ = generalSettings.warnings_provider().RegisterValueChangedCallback( - [this](const std::string& value) { + [this](const std::string& value) + { + loadHistoryDuration_ = kInitialLoadHistoryDuration_; warningsProvider_ = std::make_shared(value); }); @@ -94,6 +98,8 @@ public: std::shared_mutex textEventMutex_; std::shared_ptr warningsProvider_ {nullptr}; + std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; + std::chrono::sys_time prevLoadTime_ {}; boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; }; @@ -254,21 +260,33 @@ void TextEventManager::Impl::Refresh() std::shared_ptr warningsProvider = warningsProvider_; - // Update the file listing from the warnings provider - auto [newFiles, totalFiles] = warningsProvider->ListFiles(); + // Load updated files from the warnings provider + // Start time should default to: + // - 3 days of history for the first load + // - 1 hour of history for subsequent loads + // If the time jumps, we should attempt to load from no later than the + // previous load time + auto loadTime = + std::chrono::floor(std::chrono::system_clock::now()); + auto startTime = loadTime - loadHistoryDuration_; - if (newFiles > 0) + if (prevLoadTime_ != std::chrono::sys_time {}) { - // Load new files - auto updatedFiles = warningsProvider->LoadUpdatedFiles(); + startTime = std::min(startTime, prevLoadTime_); + } - // Handle messages - for (auto& file : updatedFiles) + auto updatedFiles = warningsProvider->LoadUpdatedFiles(startTime); + + // Store the load time and reset the load history duration + prevLoadTime_ = loadTime; + loadHistoryDuration_ = kDefaultLoadHistoryDuration_; + + // Handle messages + for (auto& file : updatedFiles) + { + for (auto& message : file->messages()) { - for (auto& message : file->messages()) - { - HandleMessage(message); - } + HandleMessage(message); } } diff --git a/wxdata/include/scwx/provider/warnings_provider.hpp b/wxdata/include/scwx/provider/warnings_provider.hpp index e519ec5d..140b671f 100644 --- a/wxdata/include/scwx/provider/warnings_provider.hpp +++ b/wxdata/include/scwx/provider/warnings_provider.hpp @@ -22,10 +22,8 @@ public: WarningsProvider(WarningsProvider&&) noexcept; WarningsProvider& operator=(WarningsProvider&&) noexcept; - std::pair - ListFiles(std::chrono::system_clock::time_point newerThan = {}); std::vector> - LoadUpdatedFiles(std::chrono::system_clock::time_point newerThan = {}); + LoadUpdatedFiles(std::chrono::sys_time newerThan = {}); private: class Impl; diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 8cfe9b77..d506fc5c 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -1,9 +1,18 @@ -#include -#include -#include +// Prevent redefinition of __cpp_lib_format +#if defined(_MSC_VER) +# include +#endif -#include -#include +// Enable chrono formatters +#ifndef __cpp_lib_format +# define __cpp_lib_format 202110L +#endif + +#include +#include +#include + +#include #if defined(_MSC_VER) # pragma warning(push, 0) @@ -11,8 +20,6 @@ #define LIBXML_HTML_ENABLED #include -#include -#include #if (__cpp_lib_chrono < 201907L) # include @@ -35,13 +42,17 @@ class WarningsProvider::Impl public: struct FileInfoRecord { - std::chrono::system_clock::time_point startTime_ {}; - std::chrono::system_clock::time_point lastModified_ {}; - size_t size_ {}; - bool updated_ {}; + FileInfoRecord(const std::string& contentLength, + const std::string& lastModified) : + contentLengthStr_ {contentLength}, lastModifiedStr_ {lastModified} + { + } + + std::string contentLengthStr_ {}; + std::string lastModifiedStr_ {}; }; - typedef std::map WarningFileMap; + using WarningFileMap = std::map; explicit Impl(const std::string& baseUrl) : baseUrl_ {baseUrl}, files_ {}, filesMutex_ {} @@ -50,10 +61,13 @@ public: ~Impl() {} + bool UpdateFileRecord(const cpr::Response& response, + const std::string& filename); + std::string baseUrl_; - WarningFileMap files_; - std::shared_mutex filesMutex_; + WarningFileMap files_; + std::mutex filesMutex_; }; WarningsProvider::WarningsProvider(const std::string& baseUrl) : @@ -66,145 +80,176 @@ WarningsProvider::WarningsProvider(WarningsProvider&&) noexcept = default; WarningsProvider& WarningsProvider::operator=(WarningsProvider&&) noexcept = default; -std::pair -WarningsProvider::ListFiles(std::chrono::system_clock::time_point newerThan) +std::vector> +WarningsProvider::LoadUpdatedFiles( + std::chrono::sys_time startTime) { using namespace std::chrono; -#if (__cpp_lib_chrono < 201907L) +#if (__cpp_lib_chrono >= 201907L) + namespace date = std::chrono; + namespace df = std; +#else using namespace date; + namespace df = date; #endif - static constexpr LazyRE2 reWarningsFilename = { - "warnings_[0-9]{8}_[0-9]{2}.txt"}; - static const std::string dateTimeFormat {"warnings_%Y%m%d_%H.txt"}; - - logger_->trace("Listing files"); - - size_t updatedObjects = 0; - size_t totalObjects = 0; - - // Perform a directory listing - auto records = network::DirList(p->baseUrl_); - - // Sort records by filename - std::sort(records.begin(), - records.end(), - [](auto& a, auto& b) { return a.filename_ < b.filename_; }); - - // Filter warning records - auto warningRecords = - records | - std::views::filter( - [](auto& record) - { - return record.type_ == std::filesystem::file_type::regular && - RE2::FullMatch(record.filename_, *reWarningsFilename); - }); - - std::unique_lock lock(p->filesMutex_); - - Impl::WarningFileMap warningFileMap; - - // Store records - for (auto& record : warningRecords) - { - // Determine start time - std::chrono::sys_time startTime; - std::istringstream ssFilename {record.filename_}; - - ssFilename >> parse(dateTimeFormat, startTime); - - // If start time is valid - if (!ssFilename.fail()) - { - // Determine if the record should be marked updated - bool updated = true; - auto it = p->files_.find(record.filename_); - if (it != p->files_.cend()) - { - auto& existingRecord = it->second; - - updated = existingRecord.updated_ || - record.size_ != existingRecord.size_ || - record.mtime_ != existingRecord.lastModified_; - } - - // Update object counts, but only if newer than threshold - if (newerThan < startTime) - { - if (updated) - { - ++updatedObjects; - } - ++totalObjects; - } - - // Store record - warningFileMap.emplace( - std::piecewise_construct, - std::forward_as_tuple(record.filename_), - std::forward_as_tuple( - startTime, record.mtime_, record.size_, updated)); - } - } - - p->files_ = std::move(warningFileMap); - - return std::make_pair(updatedObjects, totalObjects); -} - -std::vector> -WarningsProvider::LoadUpdatedFiles( - std::chrono::system_clock::time_point newerThan) -{ - logger_->debug("Loading updated files"); - + std::vector< + std::pair, false>>> + asyncCallbacks; std::vector> updatedFiles; - std::vector> asyncResponses; + std::chrono::sys_time now = + std::chrono::floor(std::chrono::system_clock::now()); + std::chrono::sys_time currentHour = + (startTime != std::chrono::sys_time {}) ? + startTime : + now - std::chrono::hours {1}; - std::unique_lock lock(p->filesMutex_); + logger_->trace("Querying files newer than: {}", util::TimeString(startTime)); - // For each warning file - for (auto& record : p->files_) + while (currentHour <= now) { - // If file is updated, and time is later than the threshold - if (record.second.updated_ && newerThan < record.second.startTime_) - { - // Retrieve warning file - asyncResponses.emplace_back( - record.first, - cpr::GetAsync(cpr::Url {p->baseUrl_ + "/" + record.first})); + static constexpr std::string_view dateTimeFormat { + "warnings_{:%Y%m%d_%H}.txt"}; + const std::string filename = df::format(dateTimeFormat, currentHour); + const std::string url = p->baseUrl_ + "/" + filename; - // Clear updated flag - record.second.updated_ = false; - } + logger_->trace("HEAD request for file: {}", filename); + + asyncCallbacks.emplace_back( + filename, + cpr::HeadCallback( + [url, filename, this]( + cpr::Response headResponse) -> std::optional + { + if (headResponse.status_code == cpr::status::HTTP_OK) + { + bool updated = + p->UpdateFileRecord(headResponse, url); // TODO: filename + + if (updated) + { + logger_->trace("GET request for file: {}", filename); + return cpr::GetAsync(cpr::Url {url}); + } + } + else if (headResponse.status_code != cpr::status::HTTP_NOT_FOUND) + { + logger_->warn("HEAD request for file failed: {} ({})", + url, + headResponse.status_line); + } + + return std::nullopt; + }, + cpr::Url {url})); + + // Query the next hour + currentHour += 1h; } - lock.unlock(); - - // Wait for warning files to load - for (auto& asyncResponse : asyncResponses) + for (auto& asyncCallback : asyncCallbacks) { - cpr::Response response = asyncResponse.second.get(); - if (response.status_code == cpr::status::HTTP_OK) - { - logger_->debug("Loading file: {}", asyncResponse.first); + auto& filename = asyncCallback.first; + auto& callback = asyncCallback.second; - // Load file - std::shared_ptr textProductFile { - std::make_shared()}; - std::istringstream responseBody {response.text}; - if (textProductFile->LoadData(responseBody)) + if (callback.valid()) + { + // Wait for futures to complete + callback.wait(); + auto asyncResponse = callback.get(); + + if (asyncResponse.has_value()) { - updatedFiles.push_back(textProductFile); + auto response = asyncResponse.value().get(); + + if (response.status_code == cpr::status::HTTP_OK) + { + logger_->debug("Loading file: {}", filename); + + // Load file + std::shared_ptr textProductFile { + std::make_shared()}; + std::istringstream responseBody {response.text}; + if (textProductFile->LoadData(responseBody)) + { + updatedFiles.push_back(textProductFile); + } + } + else + { + logger_->warn("Could not load file: {} ({})", + filename, + response.status_line); + } } } + else + { + logger_->error("Invalid future state"); + } } return updatedFiles; } +bool WarningsProvider::Impl::UpdateFileRecord(const cpr::Response& response, + const std::string& filename) +{ + bool updated = false; + + auto contentLengthIt = response.header.find("Content-Length"); + auto lastModifiedIt = response.header.find("Last-Modified"); + + std::string contentLength {}; + std::string lastModified {}; + + if (contentLengthIt != response.header.cend()) + { + contentLength = contentLengthIt->second; + } + if (lastModifiedIt != response.header.cend()) + { + lastModified = lastModifiedIt->second; + } + + std::unique_lock lock(filesMutex_); + + auto it = files_.find(filename); + if (it != files_.cend()) + { + auto& existingRecord = it->second; + + // If the size or last modified changes, request an update + + if (!contentLength.empty() && + contentLength != existingRecord.contentLengthStr_) + { + // Size changed + existingRecord.contentLengthStr_ = contentLengthIt->second; + updated = true; + } + else if (!lastModified.empty() && + lastModified != existingRecord.lastModifiedStr_) + { + // Last modified changed + existingRecord.lastModifiedStr_ = lastModifiedIt->second; + updated = true; + } + } + else + { + // File not found + files_.emplace(std::piecewise_construct, + std::forward_as_tuple(filename), + std::forward_as_tuple(contentLength, lastModified)); + updated = true; + } + + return updated; +} + } // namespace provider } // namespace scwx From 549f7ece6156e9021d12cc27dcd583cb3b3aad55 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 01:24:59 -0600 Subject: [PATCH 478/762] Fixing warnings provider test --- .../scwx/provider/warnings_provider.test.cpp | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/test/source/scwx/provider/warnings_provider.test.cpp b/test/source/scwx/provider/warnings_provider.test.cpp index 78ef9b95..de315b4b 100644 --- a/test/source/scwx/provider/warnings_provider.test.cpp +++ b/test/source/scwx/provider/warnings_provider.test.cpp @@ -13,53 +13,27 @@ static const std::string& kAlternateUrl {"https://warnings.cod.edu"}; class WarningsProviderTest : public testing::TestWithParam { }; -TEST_P(WarningsProviderTest, ListFiles) -{ - WarningsProvider provider(GetParam()); - - auto [newObjects, totalObjects] = provider.ListFiles(); - - // No objects, skip test - if (totalObjects == 0) - { - GTEST_SKIP(); - } - - EXPECT_GT(newObjects, 0); - EXPECT_GT(totalObjects, 0); - EXPECT_EQ(newObjects, totalObjects); -} TEST_P(WarningsProviderTest, LoadUpdatedFiles) { WarningsProvider provider(GetParam()); - auto [newObjects, totalObjects] = provider.ListFiles(); - auto updatedFiles = provider.LoadUpdatedFiles(); + std::chrono::sys_time now = + std::chrono::floor(std::chrono::system_clock::now()); + std::chrono::sys_time startTime = + now - std::chrono::days {3}; + + auto updatedFiles = provider.LoadUpdatedFiles(startTime); // No objects, skip test - if (totalObjects == 0) + if (updatedFiles.empty()) { GTEST_SKIP(); } - EXPECT_GT(newObjects, 0); - EXPECT_GT(totalObjects, 0); - EXPECT_EQ(newObjects, totalObjects); - EXPECT_EQ(updatedFiles.size(), newObjects); + EXPECT_GT(updatedFiles.size(), 0); - auto [newObjects2, totalObjects2] = provider.ListFiles(); - auto updatedFiles2 = provider.LoadUpdatedFiles(); - - // There should be no more than 2 updated warnings files since the last query - // (assumption that the previous newest file was updated, and a new file was - // created on the hour) - EXPECT_LE(newObjects2, 2); - EXPECT_EQ(updatedFiles2.size(), newObjects2); - - // The total number of objects may have changed, since the oldest file could - // have dropped off the list - EXPECT_GT(totalObjects2, 0); + auto updatedFiles2 = provider.LoadUpdatedFiles(); } INSTANTIATE_TEST_SUITE_P(WarningsProvider, From a8da035566fe87af0a526dcf00a48ac11957d63e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 11:33:30 -0600 Subject: [PATCH 479/762] Warnings provider clang-tidy fixes --- .../scwx/provider/warnings_provider.hpp | 7 ++--- .../scwx/provider/warnings_provider.cpp | 26 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/wxdata/include/scwx/provider/warnings_provider.hpp b/wxdata/include/scwx/provider/warnings_provider.hpp index 140b671f..0d14d258 100644 --- a/wxdata/include/scwx/provider/warnings_provider.hpp +++ b/wxdata/include/scwx/provider/warnings_provider.hpp @@ -2,9 +2,7 @@ #include -namespace scwx -{ -namespace provider +namespace scwx::provider { /** @@ -30,5 +28,4 @@ private: std::unique_ptr p; }; -} // namespace provider -} // namespace scwx +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index d506fc5c..a6b1a37d 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -29,9 +29,7 @@ # pragma warning(pop) #endif -namespace scwx -{ -namespace provider +namespace scwx::provider { static const std::string logPrefix_ = "scwx::provider::warnings_provider"; @@ -42,9 +40,9 @@ class WarningsProvider::Impl public: struct FileInfoRecord { - FileInfoRecord(const std::string& contentLength, - const std::string& lastModified) : - contentLengthStr_ {contentLength}, lastModifiedStr_ {lastModified} + FileInfoRecord(std::string contentLength, std::string lastModified) : + contentLengthStr_ {std::move(contentLength)}, + lastModifiedStr_ {std::move(lastModified)} { } @@ -54,12 +52,16 @@ public: using WarningFileMap = std::map; - explicit Impl(const std::string& baseUrl) : - baseUrl_ {baseUrl}, files_ {}, filesMutex_ {} + explicit Impl(std::string baseUrl) : + baseUrl_ {std::move(baseUrl)}, files_ {}, filesMutex_ {} { } - ~Impl() {} + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; bool UpdateFileRecord(const cpr::Response& response, const std::string& filename); @@ -87,8 +89,7 @@ WarningsProvider::LoadUpdatedFiles( using namespace std::chrono; #if (__cpp_lib_chrono >= 201907L) - namespace date = std::chrono; - namespace df = std; + namespace df = std; #else using namespace date; namespace df = date; @@ -251,5 +252,4 @@ bool WarningsProvider::Impl::UpdateFileRecord(const cpr::Response& response, return updated; } -} // namespace provider -} // namespace scwx +} // namespace scwx::provider From d34cd6847103024ddb06e732a5ba3abbef37b3ca Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 11:33:43 -0600 Subject: [PATCH 480/762] Warnings provider gcc fixes --- wxdata/source/scwx/provider/warnings_provider.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index a6b1a37d..560cb307 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -90,9 +90,14 @@ WarningsProvider::LoadUpdatedFiles( #if (__cpp_lib_chrono >= 201907L) namespace df = std; + + static constexpr std::string_view kDateTimeFormat { + "warnings_{:%Y%m%d_%H}.txt"}; #else using namespace date; namespace df = date; + +# define kDateTimeFormat "warnings_%Y%m%d_%H.txt" #endif std::vector< @@ -112,9 +117,7 @@ WarningsProvider::LoadUpdatedFiles( while (currentHour <= now) { - static constexpr std::string_view dateTimeFormat { - "warnings_{:%Y%m%d_%H}.txt"}; - const std::string filename = df::format(dateTimeFormat, currentHour); + const std::string filename = df::format(kDateTimeFormat, currentHour); const std::string url = p->baseUrl_ + "/" + filename; logger_->trace("HEAD request for file: {}", filename); From 895e760fee524d40896ab6bc9ecdee2112c0595b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 15:50:28 -0600 Subject: [PATCH 481/762] Create IemWarningsProvider class for archive warnings --- .../scwx/provider/iem_warnings_provider.hpp | 28 ++++++++++++++++++ .../scwx/provider/iem_warnings_provider.cpp | 29 +++++++++++++++++++ wxdata/wxdata.cmake | 2 ++ 3 files changed, 59 insertions(+) create mode 100644 wxdata/include/scwx/provider/iem_warnings_provider.hpp create mode 100644 wxdata/source/scwx/provider/iem_warnings_provider.cpp diff --git a/wxdata/include/scwx/provider/iem_warnings_provider.hpp b/wxdata/include/scwx/provider/iem_warnings_provider.hpp new file mode 100644 index 00000000..64c5e96f --- /dev/null +++ b/wxdata/include/scwx/provider/iem_warnings_provider.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace scwx::provider +{ + +/** + * @brief Warnings Provider + */ +class IemWarningsProvider +{ +public: + explicit IemWarningsProvider(); + ~IemWarningsProvider(); + + IemWarningsProvider(const IemWarningsProvider&) = delete; + IemWarningsProvider& operator=(const IemWarningsProvider&) = delete; + + IemWarningsProvider(IemWarningsProvider&&) noexcept; + IemWarningsProvider& operator=(IemWarningsProvider&&) noexcept; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/iem_warnings_provider.cpp b/wxdata/source/scwx/provider/iem_warnings_provider.cpp new file mode 100644 index 00000000..7c780816 --- /dev/null +++ b/wxdata/source/scwx/provider/iem_warnings_provider.cpp @@ -0,0 +1,29 @@ +#include +#include + +namespace scwx::provider +{ + +static const std::string logPrefix_ = "scwx::provider::iem_warnings_provider"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +class IemWarningsProvider::Impl +{ +public: + explicit Impl() = default; + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; +}; + +IemWarningsProvider::IemWarningsProvider() : p(std::make_unique()) {} +IemWarningsProvider::~IemWarningsProvider() = default; + +IemWarningsProvider::IemWarningsProvider(IemWarningsProvider&&) noexcept = + default; +IemWarningsProvider& +IemWarningsProvider::operator=(IemWarningsProvider&&) noexcept = default; + +} // namespace scwx::provider diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 94b0e3a7..77e65c58 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -61,12 +61,14 @@ set(SRC_NETWORK source/scwx/network/cpr.cpp set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp + include/scwx/provider/iem_warnings_provider.hpp include/scwx/provider/nexrad_data_provider.hpp include/scwx/provider/nexrad_data_provider_factory.hpp include/scwx/provider/warnings_provider.hpp) set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/aws_level3_data_provider.cpp source/scwx/provider/aws_nexrad_data_provider.cpp + source/scwx/provider/iem_warnings_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) From 9f33189c18f5b277f752b0ca75dd85e52fe6c24c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 18:21:41 -0600 Subject: [PATCH 482/762] Refactor json utility to wxdata, add ReadJsonString function --- scwx-qt/source/scwx/qt/config/radar_site.cpp | 2 +- .../source/scwx/qt/manager/marker_manager.cpp | 26 +-- .../scwx/qt/manager/placefile_manager.cpp | 6 +- .../scwx/qt/manager/settings_manager.cpp | 2 +- .../source/scwx/qt/manager/update_manager.cpp | 28 +-- scwx-qt/source/scwx/qt/model/layer_model.cpp | 2 +- .../source/scwx/qt/model/radar_site_model.cpp | 6 +- scwx-qt/source/scwx/qt/util/json.cpp | 178 +--------------- scwx-qt/source/scwx/qt/util/json.hpp | 7 +- wxdata/include/scwx/util/json.hpp | 15 ++ wxdata/source/scwx/util/json.cpp | 199 ++++++++++++++++++ wxdata/wxdata.cmake | 2 + 12 files changed, 245 insertions(+), 228 deletions(-) create mode 100644 wxdata/include/scwx/util/json.hpp create mode 100644 wxdata/source/scwx/util/json.cpp diff --git a/scwx-qt/source/scwx/qt/config/radar_site.cpp b/scwx-qt/source/scwx/qt/config/radar_site.cpp index 5c1dba2e..69815636 100644 --- a/scwx-qt/source/scwx/qt/config/radar_site.cpp +++ b/scwx-qt/source/scwx/qt/config/radar_site.cpp @@ -245,7 +245,7 @@ size_t RadarSite::ReadConfig(const std::string& path) bool dataValid = true; size_t sitesAdded = 0; - boost::json::value j = util::json::ReadJsonFile(path); + boost::json::value j = util::json::ReadJsonQFile(path); dataValid = j.is_array(); diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index 952dea44..ea21b211 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -1,10 +1,10 @@ #include #include #include -#include #include #include #include +#include #include #include @@ -62,7 +62,7 @@ public: bool markerFileRead_ {false}; - void InitalizeIds(); + void InitalizeIds(); types::MarkerId NewId(); types::MarkerId lastId_ {0}; }; @@ -70,15 +70,9 @@ public: class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(const types::MarkerInfo& info) : - markerInfo_ {info} - { - } + MarkerRecord(const types::MarkerInfo& info) : markerInfo_ {info} {} - const types::MarkerInfo& toMarkerInfo() - { - return markerInfo_; - } + const types::MarkerInfo& toMarkerInfo() { return markerInfo_; } types::MarkerInfo markerInfo_; @@ -175,7 +169,7 @@ void MarkerManager::Impl::ReadMarkerSettings() // Determine if marker settings exists if (std::filesystem::exists(markerSettingsPath_)) { - markerJson = util::json::ReadJsonFile(markerSettingsPath_); + markerJson = scwx::util::json::ReadJsonFile(markerSettingsPath_); } if (markerJson != nullptr && markerJson.is_array()) @@ -224,8 +218,8 @@ void MarkerManager::Impl::WriteMarkerSettings() logger_->info("Saving location marker settings"); const std::shared_lock lock(markerRecordLock_); - auto markerJson = boost::json::value_from(markerRecords_); - util::json::WriteJsonFile(markerSettingsPath_, markerJson); + auto markerJson = boost::json::value_from(markerRecords_); + scwx::util::json::WriteJsonFile(markerSettingsPath_, markerJson); } std::shared_ptr @@ -357,10 +351,11 @@ types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) types::MarkerId id; { const std::unique_lock lock(p->markerRecordLock_); - id = p->NewId(); + id = p->NewId(); size_t index = p->markerRecords_.size(); p->idToIndex_.emplace(id, index); - p->markerRecords_.emplace_back(std::make_shared(marker)); + p->markerRecords_.emplace_back( + std::make_shared(marker)); p->markerRecords_[index]->markerInfo_.id = id; add_icon(marker.iconName); @@ -499,7 +494,6 @@ void MarkerManager::set_marker_settings_path(const std::string& path) p->markerSettingsPath_ = path; } - std::shared_ptr MarkerManager::Instance() { static std::weak_ptr markerManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index a6158773..d85f9e40 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -2,10 +2,10 @@ #include #include #include -#include #include #include #include +#include #include #include @@ -385,7 +385,7 @@ void PlacefileManager::Impl::ReadPlacefileSettings() // Determine if placefile settings exists if (std::filesystem::exists(placefileSettingsPath_)) { - placefileJson = util::json::ReadJsonFile(placefileSettingsPath_); + placefileJson = scwx::util::json::ReadJsonFile(placefileSettingsPath_); } // If placefile settings was successfully read @@ -428,7 +428,7 @@ void PlacefileManager::Impl::WritePlacefileSettings() std::shared_lock lock {placefileRecordLock_}; auto placefileJson = boost::json::value_from(placefileRecords_); - util::json::WriteJsonFile(placefileSettingsPath_, placefileJson); + scwx::util::json::WriteJsonFile(placefileSettingsPath_, placefileJson); } void PlacefileManager::SetRadarSite( diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index 5b2e9cbb..a47428bb 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include diff --git a/scwx-qt/source/scwx/qt/manager/update_manager.cpp b/scwx-qt/source/scwx/qt/manager/update_manager.cpp index d21068cb..5910fcaf 100644 --- a/scwx-qt/source/scwx/qt/manager/update_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/update_manager.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -29,8 +30,7 @@ public: ~Impl() {} - static std::string GetVersionString(const std::string& releaseName); - static boost::json::value ParseResponseText(const std::string& s); + static std::string GetVersionString(const std::string& releaseName); size_t PopulateReleases(); size_t AddReleases(const boost::json::value& json); @@ -70,28 +70,6 @@ UpdateManager::Impl::GetVersionString(const std::string& releaseName) return versionString; } -boost::json::value UpdateManager::Impl::ParseResponseText(const std::string& s) -{ - boost::json::stream_parser p; - boost::system::error_code ec; - - p.write(s, ec); - if (ec) - { - logger_->warn("{}", ec.message()); - return nullptr; - } - - p.finish(ec); - if (ec) - { - logger_->warn("{}", ec.message()); - return nullptr; - } - - return p.release(); -} - bool UpdateManager::CheckForUpdates(const std::string& currentVersion) { std::unique_lock lock(p->updateMutex_); @@ -148,7 +126,7 @@ size_t UpdateManager::Impl::PopulateReleases() // Successful REST API query if (r.status_code == 200) { - boost::json::value json = Impl::ParseResponseText(r.text); + boost::json::value json = util::json::ReadJsonString(r.text); if (json == nullptr) { logger_->warn("Response not JSON: {}", r.header["content-type"]); diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 2f1b8a9d..6be8eb9d 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include #include 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 e1a593d7..f131a1cd 100644 --- a/scwx-qt/source/scwx/qt/model/radar_site_model.cpp +++ b/scwx-qt/source/scwx/qt/model/radar_site_model.cpp @@ -4,8 +4,8 @@ #include #include #include -#include #include +#include #include #include @@ -117,7 +117,7 @@ void RadarSiteModelImpl::ReadPresets() // Determine if presets exists if (std::filesystem::exists(presetsPath_)) { - presetsJson = util::json::ReadJsonFile(presetsPath_); + presetsJson = scwx::util::json::ReadJsonFile(presetsPath_); } // If presets was successfully read @@ -160,7 +160,7 @@ void RadarSiteModelImpl::WritePresets() logger_->info("Saving presets"); auto presetsJson = boost::json::value_from(presets_); - util::json::WriteJsonFile(presetsPath_, presetsJson); + scwx::util::json::WriteJsonFile(presetsPath_, presetsJson); } int RadarSiteModel::rowCount(const QModelIndex& parent) const diff --git a/scwx-qt/source/scwx/qt/util/json.cpp b/scwx-qt/source/scwx/qt/util/json.cpp index 7bf0d23a..d508a224 100644 --- a/scwx-qt/source/scwx/qt/util/json.cpp +++ b/scwx-qt/source/scwx/qt/util/json.cpp @@ -1,41 +1,19 @@ #include +#include #include -#include - -#include -#include #include #include -namespace scwx -{ -namespace qt -{ -namespace util -{ -namespace json +namespace scwx::qt::util::json { static const std::string logPrefix_ = "scwx::qt::util::json"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -/* Adapted from: - * https://www.boost.org/doc/libs/1_77_0/libs/json/doc/html/json/examples.html#json.examples.pretty - * - * Copyright (c) 2019, 2020 Vinnie Falco - * Copyright (c) 2020 Krystian Stasiowski - * Distributed under the Boost Software License, Version 1.0. (See - * http://www.boost.org/LICENSE_1_0.txt) - */ -static void PrettyPrintJson(std::ostream& os, - boost::json::value const& jv, - std::string* indent = nullptr); - static boost::json::value ReadJsonFile(QFile& file); -static boost::json::value ReadJsonStream(std::istream& is); -boost::json::value ReadJsonFile(const std::string& path) +boost::json::value ReadJsonQFile(const std::string& path) { boost::json::value json; @@ -46,8 +24,7 @@ boost::json::value ReadJsonFile(const std::string& path) } else { - std::ifstream ifs {path}; - json = ReadJsonStream(ifs); + json = ::scwx::util::json::ReadJsonFile(path); } return json; @@ -65,7 +42,7 @@ static boost::json::value ReadJsonFile(QFile& file) std::string jsonSource = jsonStream.readAll().toStdString(); std::istringstream is {jsonSource}; - json = ReadJsonStream(is); + json = ::scwx::util::json::ReadJsonStream(is); file.close(); } @@ -78,147 +55,4 @@ static boost::json::value ReadJsonFile(QFile& file) return json; } -static boost::json::value ReadJsonStream(std::istream& is) -{ - std::string line; - - boost::json::stream_parser p; - boost::system::error_code ec; - - while (std::getline(is, line)) - { - p.write(line, ec); - if (ec) - { - logger_->warn("{}", ec.message()); - return nullptr; - } - } - - p.finish(ec); - if (ec) - { - logger_->warn("{}", ec.message()); - return nullptr; - } - - return p.release(); -} - -void WriteJsonFile(const std::string& path, - const boost::json::value& json, - bool prettyPrint) -{ - std::ofstream ofs {path}; - - if (!ofs.is_open()) - { - logger_->warn("Cannot write JSON file: \"{}\"", path); - } - else - { - if (prettyPrint) - { - PrettyPrintJson(ofs, json); - } - else - { - ofs << json; - } - ofs.close(); - } -} - -static void PrettyPrintJson(std::ostream& os, - boost::json::value const& jv, - std::string* indent) -{ - std::string indent_; - if (!indent) - indent = &indent_; - switch (jv.kind()) - { - case boost::json::kind::object: - { - os << "{\n"; - indent->append(4, ' '); - auto const& obj = jv.get_object(); - if (!obj.empty()) - { - auto it = obj.begin(); - for (;;) - { - os << *indent << boost::json::serialize(it->key()) << " : "; - PrettyPrintJson(os, it->value(), indent); - if (++it == obj.end()) - break; - os << ",\n"; - } - } - os << "\n"; - indent->resize(indent->size() - 4); - os << *indent << "}"; - break; - } - - case boost::json::kind::array: - { - os << "[\n"; - indent->append(4, ' '); - auto const& arr = jv.get_array(); - if (!arr.empty()) - { - auto it = arr.begin(); - for (;;) - { - os << *indent; - PrettyPrintJson(os, *it, indent); - if (++it == arr.end()) - break; - os << ",\n"; - } - } - os << "\n"; - indent->resize(indent->size() - 4); - os << *indent << "]"; - break; - } - - case boost::json::kind::string: - { - os << boost::json::serialize(jv.get_string()); - break; - } - - case boost::json::kind::uint64: - os << jv.get_uint64(); - break; - - case boost::json::kind::int64: - os << jv.get_int64(); - break; - - case boost::json::kind::double_: - os << jv.get_double(); - break; - - case boost::json::kind::bool_: - if (jv.get_bool()) - os << "true"; - else - os << "false"; - break; - - case boost::json::kind::null: - os << "null"; - break; - } - - if (indent->empty()) - os << "\n"; -} - -} // namespace json -} // namespace util -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::util::json diff --git a/scwx-qt/source/scwx/qt/util/json.hpp b/scwx-qt/source/scwx/qt/util/json.hpp index bbf497f4..9dd09810 100644 --- a/scwx-qt/source/scwx/qt/util/json.hpp +++ b/scwx-qt/source/scwx/qt/util/json.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include namespace scwx @@ -13,10 +11,7 @@ namespace util namespace json { -boost::json::value ReadJsonFile(const std::string& path); -void WriteJsonFile(const std::string& path, - const boost::json::value& json, - bool prettyPrint = true); +boost::json::value ReadJsonQFile(const std::string& path); } // namespace json } // namespace util diff --git a/wxdata/include/scwx/util/json.hpp b/wxdata/include/scwx/util/json.hpp new file mode 100644 index 00000000..ab836edc --- /dev/null +++ b/wxdata/include/scwx/util/json.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace scwx::util::json +{ + +boost::json::value ReadJsonFile(const std::string& path); +boost::json::value ReadJsonStream(std::istream& is); +boost::json::value ReadJsonString(std::string_view sv); +void WriteJsonFile(const std::string& path, + const boost::json::value& json, + bool prettyPrint = true); + +} // namespace scwx::util::json diff --git a/wxdata/source/scwx/util/json.cpp b/wxdata/source/scwx/util/json.cpp new file mode 100644 index 00000000..b8f51507 --- /dev/null +++ b/wxdata/source/scwx/util/json.cpp @@ -0,0 +1,199 @@ +#include +#include + +#include + +#include +#include + +namespace scwx::util::json +{ + +static const std::string logPrefix_ = "scwx::util::json"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +/* Adapted from: + * https://www.boost.org/doc/libs/1_77_0/libs/json/doc/html/json/examples.html#json.examples.pretty + * + * Copyright (c) 2019, 2020 Vinnie Falco + * Copyright (c) 2020 Krystian Stasiowski + * Distributed under the Boost Software License, Version 1.0. (See + * http://www.boost.org/LICENSE_1_0.txt) + */ +static void PrettyPrintJson(std::ostream& os, + boost::json::value const& jv, + std::string* indent = nullptr); + +boost::json::value ReadJsonFile(const std::string& path) +{ + boost::json::value json; + + std::ifstream ifs {path}; + json = ReadJsonStream(ifs); + + return json; +} + +boost::json::value ReadJsonStream(std::istream& is) +{ + std::string line; + + boost::json::stream_parser p; + boost::system::error_code ec; + + while (std::getline(is, line)) + { + p.write(line, ec); + if (ec) + { + logger_->warn("{}", ec.message()); + return nullptr; + } + } + + p.finish(ec); + if (ec) + { + logger_->warn("{}", ec.message()); + return nullptr; + } + + return p.release(); +} + +boost::json::value ReadJsonString(std::string_view sv) +{ + boost::json::stream_parser p; + boost::system::error_code ec; + + p.write(sv, ec); + if (ec) + { + logger_->warn("{}", ec.message()); + return nullptr; + } + + p.finish(ec); + if (ec) + { + logger_->warn("{}", ec.message()); + return nullptr; + } + + return p.release(); +} + +void WriteJsonFile(const std::string& path, + const boost::json::value& json, + bool prettyPrint) +{ + std::ofstream ofs {path}; + + if (!ofs.is_open()) + { + logger_->warn("Cannot write JSON file: \"{}\"", path); + } + else + { + if (prettyPrint) + { + PrettyPrintJson(ofs, json); + } + else + { + ofs << json; + } + ofs.close(); + } +} + +static void PrettyPrintJson(std::ostream& os, + boost::json::value const& jv, + std::string* indent) +{ + std::string indent_; + if (!indent) + indent = &indent_; + switch (jv.kind()) + { + case boost::json::kind::object: + { + os << "{\n"; + indent->append(4, ' '); + auto const& obj = jv.get_object(); + if (!obj.empty()) + { + auto it = obj.begin(); + for (;;) + { + os << *indent << boost::json::serialize(it->key()) << " : "; + PrettyPrintJson(os, it->value(), indent); + if (++it == obj.end()) + break; + os << ",\n"; + } + } + os << "\n"; + indent->resize(indent->size() - 4); + os << *indent << "}"; + break; + } + + case boost::json::kind::array: + { + os << "[\n"; + indent->append(4, ' '); + auto const& arr = jv.get_array(); + if (!arr.empty()) + { + auto it = arr.begin(); + for (;;) + { + os << *indent; + PrettyPrintJson(os, *it, indent); + if (++it == arr.end()) + break; + os << ",\n"; + } + } + os << "\n"; + indent->resize(indent->size() - 4); + os << *indent << "]"; + break; + } + + case boost::json::kind::string: + { + os << boost::json::serialize(jv.get_string()); + break; + } + + case boost::json::kind::uint64: + os << jv.get_uint64(); + break; + + case boost::json::kind::int64: + os << jv.get_int64(); + break; + + case boost::json::kind::double_: + os << jv.get_double(); + break; + + case boost::json::kind::bool_: + if (jv.get_bool()) + os << "true"; + else + os << "false"; + break; + + case boost::json::kind::null: + os << "null"; + break; + } + + if (indent->empty()) + os << "\n"; +} + +} // namespace scwx::util::json diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 77e65c58..4b08ad19 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -78,6 +78,7 @@ set(HDR_UTIL include/scwx/util/digest.hpp include/scwx/util/float.hpp include/scwx/util/hash.hpp include/scwx/util/iterator.hpp + include/scwx/util/json.hpp include/scwx/util/logger.hpp include/scwx/util/map.hpp include/scwx/util/rangebuf.hpp @@ -90,6 +91,7 @@ set(SRC_UTIL source/scwx/util/digest.cpp source/scwx/util/environment.cpp source/scwx/util/float.cpp source/scwx/util/hash.cpp + source/scwx/util/json.cpp source/scwx/util/logger.cpp source/scwx/util/rangebuf.cpp source/scwx/util/streams.cpp From cd7435a4d5ddfcd7ec01001ade22c9110c2b4cd7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 22:20:33 -0600 Subject: [PATCH 483/762] Add IEM types supporting AFOS list --- wxdata/include/scwx/types/iem_types.hpp | 73 +++++++++++++++++ wxdata/source/scwx/types/iem_types.cpp | 103 ++++++++++++++++++++++++ wxdata/wxdata.cmake | 6 ++ 3 files changed, 182 insertions(+) create mode 100644 wxdata/include/scwx/types/iem_types.hpp create mode 100644 wxdata/source/scwx/types/iem_types.cpp diff --git a/wxdata/include/scwx/types/iem_types.hpp b/wxdata/include/scwx/types/iem_types.hpp new file mode 100644 index 00000000..ee461c36 --- /dev/null +++ b/wxdata/include/scwx/types/iem_types.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include + +namespace scwx::types::iem +{ + +/** + * @brief AFOS Entry object + * + * + */ +struct AfosEntry +{ + std::int64_t index_ {}; + std::string entered_ {}; + std::string pil_ {}; + std::string productId_ {}; + std::string cccc_ {}; + std::int64_t count_ {}; + std::string link_ {}; + std::string textLink_ {}; +}; + +/** + * @brief AFOS List object + * + * + */ +struct AfosList +{ + std::vector data_ {}; +}; + +/** + * @brief Bad Request (400) object + */ +struct BadRequest +{ + std::string detail_ {}; +}; + +/** + * @brief Validation Error (422) object + */ +struct ValidationError +{ + struct Detail + { + std::string type_ {}; + std::vector> loc_ {}; + std::string msg_ {}; + std::string input_ {}; + struct Context + { + std::string error_ {}; + } ctx_; + }; + + std::vector detail_ {}; +}; + +AfosList tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); +BadRequest tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); +ValidationError tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv); + +} // namespace scwx::types::iem diff --git a/wxdata/source/scwx/types/iem_types.cpp b/wxdata/source/scwx/types/iem_types.cpp new file mode 100644 index 00000000..c1921ede --- /dev/null +++ b/wxdata/source/scwx/types/iem_types.cpp @@ -0,0 +1,103 @@ +#include + +#include + +namespace scwx::types::iem +{ + +AfosEntry tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + AfosEntry entry {}; + + // Required parameters + entry.index_ = jo.at("index").as_int64(); + entry.entered_ = jo.at("entered").as_string(); + entry.pil_ = jo.at("pil").as_string(); + entry.productId_ = jo.at("product_id").as_string(); + entry.cccc_ = jo.at("cccc").as_string(); + entry.count_ = jo.at("count").as_int64(); + entry.link_ = jo.at("link").as_string(); + entry.textLink_ = jo.at("text_link").as_string(); + + return entry; +} + +AfosList tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + AfosList list {}; + + // Required parameters + list.data_ = boost::json::value_to>(jo.at("data")); + + return list; +} + +BadRequest tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + BadRequest badRequest {}; + + // Required parameters + badRequest.detail_ = jo.at("detail").as_string(); + + return badRequest; +} + +ValidationError::Detail::Context +tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + ValidationError::Detail::Context ctx {}; + + // Required parameters + ctx.error_ = jo.at("error").as_string(); + + return ctx; +} + +ValidationError::Detail +tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + ValidationError::Detail detail {}; + + // Required parameters + detail.type_ = jo.at("type").as_string(); + detail.loc_ = boost::json::value_to< + std::vector>>(jo.at("loc")); + detail.msg_ = jo.at("msg").as_string(); + detail.input_ = jo.at("input").as_string(); + + detail.ctx_ = + boost::json::value_to(jo.at("ctx")); + + return detail; +} + +ValidationError tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) +{ + auto& jo = jv.as_object(); + + ValidationError error {}; + + // Required parameters + error.detail_ = boost::json::value_to>( + jo.at("detail")); + + return error; +} + +} // namespace scwx::types::iem diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 4b08ad19..92de3bf0 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -72,6 +72,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_TYPES include/scwx/types/iem_types.hpp) +set(SRC_TYPES source/scwx/types/iem_types.cpp) set(HDR_UTIL include/scwx/util/digest.hpp include/scwx/util/enum.hpp include/scwx/util/environment.hpp @@ -228,6 +230,8 @@ add_library(wxdata OBJECT ${HDR_AWIPS} ${SRC_NETWORK} ${HDR_PROVIDER} ${SRC_PROVIDER} + ${HDR_TYPES} + ${SRC_TYPES} ${HDR_UTIL} ${SRC_UTIL} ${HDR_WSR88D} @@ -248,6 +252,8 @@ source_group("Header Files\\network" FILES ${HDR_NETWORK}) source_group("Source Files\\network" FILES ${SRC_NETWORK}) source_group("Header Files\\provider" FILES ${HDR_PROVIDER}) source_group("Source Files\\provider" FILES ${SRC_PROVIDER}) +source_group("Header Files\\types" FILES ${HDR_TYPES}) +source_group("Source Files\\types" FILES ${SRC_TYPES}) source_group("Header Files\\util" FILES ${HDR_UTIL}) source_group("Source Files\\util" FILES ${SRC_UTIL}) source_group("Header Files\\wsr88d" FILES ${HDR_WSR88D}) From 59a8fdbf56463957e5886feb8e78bfbc483a43ed Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 22:32:39 -0600 Subject: [PATCH 484/762] List NWS text products metadata --- .../scwx/provider/iem_warnings_provider.hpp | 6 + .../scwx/provider/iem_warnings_provider.cpp | 123 ++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/wxdata/include/scwx/provider/iem_warnings_provider.hpp b/wxdata/include/scwx/provider/iem_warnings_provider.hpp index 64c5e96f..2061afa4 100644 --- a/wxdata/include/scwx/provider/iem_warnings_provider.hpp +++ b/wxdata/include/scwx/provider/iem_warnings_provider.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace scwx::provider { @@ -20,6 +21,11 @@ public: IemWarningsProvider(IemWarningsProvider&&) noexcept; IemWarningsProvider& operator=(IemWarningsProvider&&) noexcept; + std::vector + ListTextProducts(std::chrono::sys_time date, + std::optional cccc = {}, + std::optional pil = {}); + private: class Impl; std::unique_ptr p; diff --git a/wxdata/source/scwx/provider/iem_warnings_provider.cpp b/wxdata/source/scwx/provider/iem_warnings_provider.cpp index 7c780816..b40955ca 100644 --- a/wxdata/source/scwx/provider/iem_warnings_provider.cpp +++ b/wxdata/source/scwx/provider/iem_warnings_provider.cpp @@ -1,12 +1,23 @@ #include +#include +#include +#include #include +#include +#include + namespace scwx::provider { static const std::string logPrefix_ = "scwx::provider::iem_warnings_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); +static const std::string kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; + +static const std::string kListNwsTextProductsEndpoint_ = "/nws/afos/list.json"; +static const std::string kNwsTextProductEndpoint_ = "/nwstext/"; + class IemWarningsProvider::Impl { public: @@ -16,6 +27,11 @@ public: Impl& operator=(const Impl&) = delete; Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; + + std::vector + ListTextProducts(std::chrono::sys_time date, + std::optional cccc = {}, + std::optional pil = {}); }; IemWarningsProvider::IemWarningsProvider() : p(std::make_unique()) {} @@ -26,4 +42,111 @@ IemWarningsProvider::IemWarningsProvider(IemWarningsProvider&&) noexcept = IemWarningsProvider& IemWarningsProvider::operator=(IemWarningsProvider&&) noexcept = default; +std::vector IemWarningsProvider::ListTextProducts( + std::chrono::sys_time date, + std::optional cccc, + std::optional pil) +{ + return p->ListTextProducts(date, cccc, pil); +} + +std::vector IemWarningsProvider::Impl::ListTextProducts( + std::chrono::sys_time date, + std::optional cccc, + std::optional pil) +{ + using namespace std::chrono; + +#if (__cpp_lib_chrono >= 201907L) + namespace df = std; + + static constexpr std::string_view kDateFormat {"{:%Y-%m-%d}"}; +#else + using namespace date; + namespace df = date; + +# define kDateFormat "%Y-%m-%d" +#endif + + auto parameters = cpr::Parameters {{"date", df::format(kDateFormat, date)}}; + + // WMO Source Code + if (cccc.has_value()) + { + parameters.Add({"cccc", std::string {cccc.value()}}); + } + + // AFOS / AWIPS ID / 3-6 length identifier + if (pil.has_value()) + { + parameters.Add({"pil", std::string {pil.value()}}); + } + + auto response = + cpr::Get(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, + network::cpr::GetHeader(), + parameters); + boost::json::value json = util::json::ReadJsonString(response.text); + + std::vector textProducts {}; + + if (response.status_code == cpr::status::HTTP_OK) + { + try + { + // Get AFOS list from response + auto entries = boost::json::value_to(json); + + for (auto& entry : entries.data_) + { + textProducts.push_back(entry.productId_); + } + + logger_->trace("Found {} products", entries.data_.size()); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing JSON: {}", ex.what()); + } + } + else if (response.status_code == cpr::status::HTTP_BAD_REQUEST && + json != nullptr) + { + try + { + // Log bad request details + auto badRequest = boost::json::value_to(json); + logger_->warn("ListTextProducts bad request: {}", badRequest.detail_); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing bad response: {}", ex.what()); + } + } + else if (response.status_code == cpr::status::HTTP_UNPROCESSABLE_ENTITY && + json != nullptr) + { + try + { + // Log validation error details + auto error = boost::json::value_to(json); + logger_->warn("ListTextProducts validation error: {}", + error.detail_.at(0).msg_); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing validation error: {}", ex.what()); + } + } + else + { + logger_->warn("Could not list text products: {}", response.status_line); + } + + return textProducts; +} + } // namespace scwx::provider From f06191f290c20587502ed7c2db5f9768bf073b85 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 22:33:24 -0600 Subject: [PATCH 485/762] Add IEM warnings provider test --- .../provider/iem_warnings_provider.test.cpp | 34 +++++++++++++++++++ test/test.cmake | 1 + 2 files changed, 35 insertions(+) create mode 100644 test/source/scwx/provider/iem_warnings_provider.test.cpp diff --git a/test/source/scwx/provider/iem_warnings_provider.test.cpp b/test/source/scwx/provider/iem_warnings_provider.test.cpp new file mode 100644 index 00000000..4f66e185 --- /dev/null +++ b/test/source/scwx/provider/iem_warnings_provider.test.cpp @@ -0,0 +1,34 @@ +#include + +#include + +namespace scwx +{ +namespace provider +{ + +TEST(IemWarningsProviderTest, LoadUpdatedFiles) +{ + using namespace std::chrono; + using sys_days = time_point; + + IemWarningsProvider provider {}; + + auto date = sys_days {2023y / March / 25d}; + + auto torProducts = provider.ListTextProducts(date, {}, "TOR"); + + EXPECT_EQ(torProducts.size(), 35); + + if (torProducts.size() >= 1) + { + EXPECT_EQ(torProducts.at(0), "202303250016-KMEG-WFUS54-TORMEG"); + } + if (torProducts.size() >= 35) + { + EXPECT_EQ(torProducts.at(34), "202303252015-KFFC-WFUS52-TORFFC"); + } +} + +} // namespace provider +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index 3ec6ef19..17141e0c 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -19,6 +19,7 @@ set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp source/scwx/provider/aws_level3_data_provider.test.cpp + source/scwx/provider/iem_warnings_provider.test.cpp source/scwx/provider/warnings_provider.test.cpp) set(SRC_QT_CONFIG_TESTS source/scwx/qt/config/county_database.test.cpp source/scwx/qt/config/radar_site.test.cpp) From 2720ad6a38fd9b13ebecc02e09cd20f62cda7907 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 1 Feb 2025 23:40:06 -0600 Subject: [PATCH 486/762] Add IEM load text product API functionality --- .../provider/iem_warnings_provider.test.cpp | 15 ++++- .../scwx/provider/iem_warnings_provider.hpp | 7 ++- wxdata/source/scwx/awips/wmo_header.cpp | 12 +++- .../scwx/provider/iem_warnings_provider.cpp | 60 +++++++++++++++---- wxdata/source/scwx/types/iem_types.cpp | 16 +++-- 5 files changed, 90 insertions(+), 20 deletions(-) diff --git a/test/source/scwx/provider/iem_warnings_provider.test.cpp b/test/source/scwx/provider/iem_warnings_provider.test.cpp index 4f66e185..6b6809e5 100644 --- a/test/source/scwx/provider/iem_warnings_provider.test.cpp +++ b/test/source/scwx/provider/iem_warnings_provider.test.cpp @@ -7,7 +7,7 @@ namespace scwx namespace provider { -TEST(IemWarningsProviderTest, LoadUpdatedFiles) +TEST(IemWarningsProviderTest, ListTextProducts) { using namespace std::chrono; using sys_days = time_point; @@ -30,5 +30,18 @@ TEST(IemWarningsProviderTest, LoadUpdatedFiles) } } +TEST(IemWarningsProviderTest, LoadTextProducts) +{ + static const std::vector productIds { + "202303250016-KMEG-WFUS54-TORMEG", // + "202303252015-KFFC-WFUS52-TORFFC"}; + + IemWarningsProvider provider {}; + + auto textProducts = provider.LoadTextProducts(productIds); + + EXPECT_EQ(textProducts.size(), 2); +} + } // namespace provider } // namespace scwx diff --git a/wxdata/include/scwx/provider/iem_warnings_provider.hpp b/wxdata/include/scwx/provider/iem_warnings_provider.hpp index 2061afa4..35c6bbad 100644 --- a/wxdata/include/scwx/provider/iem_warnings_provider.hpp +++ b/wxdata/include/scwx/provider/iem_warnings_provider.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -21,11 +23,14 @@ public: IemWarningsProvider(IemWarningsProvider&&) noexcept; IemWarningsProvider& operator=(IemWarningsProvider&&) noexcept; - std::vector + static std::vector ListTextProducts(std::chrono::sys_time date, std::optional cccc = {}, std::optional pil = {}); + static std::vector> + LoadTextProducts(const std::vector& textProducts); + private: class Impl; std::unique_ptr p; diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index eb4501e7..4d502604 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -132,9 +132,19 @@ bool WmoHeader::Parse(std::istream& is) { util::getline(is, sohLine); util::getline(is, sequenceLine); + util::getline(is, wmoLine); + } + else + { + // The next line could be the WMO line or the sequence line + util::getline(is, wmoLine); + if (wmoLine.length() < 18) + { + sequenceLine.swap(wmoLine); + util::getline(is, wmoLine); + } } - util::getline(is, wmoLine); util::getline(is, awipsLine); if (is.eof()) diff --git a/wxdata/source/scwx/provider/iem_warnings_provider.cpp b/wxdata/source/scwx/provider/iem_warnings_provider.cpp index b40955ca..47c29547 100644 --- a/wxdata/source/scwx/provider/iem_warnings_provider.cpp +++ b/wxdata/source/scwx/provider/iem_warnings_provider.cpp @@ -27,11 +27,6 @@ public: Impl& operator=(const Impl&) = delete; Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; - - std::vector - ListTextProducts(std::chrono::sys_time date, - std::optional cccc = {}, - std::optional pil = {}); }; IemWarningsProvider::IemWarningsProvider() : p(std::make_unique()) {} @@ -46,14 +41,6 @@ std::vector IemWarningsProvider::ListTextProducts( std::chrono::sys_time date, std::optional cccc, std::optional pil) -{ - return p->ListTextProducts(date, cccc, pil); -} - -std::vector IemWarningsProvider::Impl::ListTextProducts( - std::chrono::sys_time date, - std::optional cccc, - std::optional pil) { using namespace std::chrono; @@ -149,4 +136,51 @@ std::vector IemWarningsProvider::Impl::ListTextProducts( return textProducts; } +std::vector> +IemWarningsProvider::LoadTextProducts( + const std::vector& textProducts) +{ + auto parameters = cpr::Parameters {{"nolimit", "true"}}; + + std::vector> + asyncResponses {}; + + for (auto& productId : textProducts) + { + asyncResponses.emplace_back( + productId, + cpr::GetAsync( + cpr::Url {kBaseUrl_ + kNwsTextProductEndpoint_ + productId}, + network::cpr::GetHeader(), + parameters)); + } + + std::vector> textProductFiles; + + for (auto& asyncResponse : asyncResponses) + { + auto response = asyncResponse.second.get(); + + if (response.status_code == cpr::status::HTTP_OK) + { + // Load file + std::shared_ptr textProductFile { + std::make_shared()}; + std::istringstream responseBody {response.text}; + if (textProductFile->LoadData(responseBody)) + { + textProductFiles.push_back(textProductFile); + } + } + else + { + logger_->warn("Could not load text product: {} ({})", + asyncResponse.first, + response.status_line); + } + } + + return textProductFiles; +} + } // namespace scwx::provider diff --git a/wxdata/source/scwx/types/iem_types.cpp b/wxdata/source/scwx/types/iem_types.cpp index c1921ede..ed3884d3 100644 --- a/wxdata/source/scwx/types/iem_types.cpp +++ b/wxdata/source/scwx/types/iem_types.cpp @@ -77,11 +77,19 @@ tag_invoke(boost::json::value_to_tag, detail.type_ = jo.at("type").as_string(); detail.loc_ = boost::json::value_to< std::vector>>(jo.at("loc")); - detail.msg_ = jo.at("msg").as_string(); - detail.input_ = jo.at("input").as_string(); + detail.msg_ = jo.at("msg").as_string(); - detail.ctx_ = - boost::json::value_to(jo.at("ctx")); + // Optional parameters + if (jo.contains("input")) + { + detail.input_ = jo.at("input").as_string(); + } + + if (jo.contains("ctx")) + { + detail.ctx_ = + boost::json::value_to(jo.at("ctx")); + } return detail; } From e6cfef06a73286ea435d2a04a3c923fb5f739e14 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 2 Feb 2025 01:34:35 -0600 Subject: [PATCH 487/762] Text product message fixes to support IEM --- .../scwx/awips/text_product_message.cpp | 44 ++++++++++--------- wxdata/source/scwx/util/streams.cpp | 18 +++++--- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 54ce7e25..3b571d83 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -11,9 +11,7 @@ #include #include -namespace scwx -{ -namespace awips +namespace scwx::awips { static const std::string logPrefix_ = "scwx::awips::text_product_message"; @@ -49,6 +47,11 @@ public: { } ~TextProductMessageImpl() = default; + + TextProductMessageImpl(const TextProductMessageImpl&) = delete; + TextProductMessageImpl& operator=(const TextProductMessageImpl&) = delete; + TextProductMessageImpl(const TextProductMessageImpl&&) = delete; + TextProductMessageImpl& operator=(const TextProductMessageImpl&&) = delete; std::string messageContent_; std::shared_ptr wmoHeader_; @@ -232,7 +235,7 @@ bool TextProductMessage::Parse(std::istream& is) if (i == 0) { - if (is.peek() != '\r') + if (is.peek() != '\r' && is.peek() != '\n') { segment->header_ = TryParseSegmentHeader(is); } @@ -318,8 +321,8 @@ bool TextProductMessage::Parse(std::istream& is) return dataValid; } -void ParseCodedInformation(std::shared_ptr segment, - const std::string& wfo) +void ParseCodedInformation(const std::shared_ptr& segment, + const std::string& wfo) { typedef std::vector::const_iterator StringIterator; @@ -352,8 +355,8 @@ void ParseCodedInformation(std::shared_ptr segment, codedLocationEnd = it; } - else if (codedMotionBegin == productContent.cend() && - it->starts_with("TIME...MOT...LOC")) + if (codedMotionBegin == productContent.cend() && + it->starts_with("TIME...MOT...LOC")) { codedMotionBegin = it; } @@ -366,8 +369,7 @@ void ParseCodedInformation(std::shared_ptr segment, codedMotionEnd = it; } - else if (!segment->observed_ && - it->find("...OBSERVED") != std::string::npos) + if (!segment->observed_ && it->find("...OBSERVED") != std::string::npos) { segment->observed_ = true; } @@ -378,6 +380,8 @@ void ParseCodedInformation(std::shared_ptr segment, segment->tornadoPossible_ = true; } + // Assignment of an iterator permitted + // NOLINTBEGIN(bugprone-assignment-in-if-condition) else if (segment->threatCategory_ == ibw::ThreatCategory::Base && (threatTagIt = std::find_if(kThreatCategoryTags.cbegin(), kThreatCategoryTags.cend(), @@ -385,6 +389,7 @@ void ParseCodedInformation(std::shared_ptr segment, return it->starts_with(tag); })) != kThreatCategoryTags.cend() && it->length() > threatTagIt->length()) + // NOLINTEND(bugprone-assignment-in-if-condition) { const std::string threatCategoryName = it->substr(threatTagIt->length()); @@ -458,7 +463,7 @@ void SkipBlankLines(std::istream& is) { std::string line; - while (is.peek() == '\r') + while (is.peek() == '\r' || is.peek() == '\n') { util::getline(is, line); } @@ -513,7 +518,7 @@ std::vector TryParseMndHeader(std::istream& is) std::string line; std::streampos isBegin = is.tellg(); - while (!is.eof() && is.peek() != '\r') + while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') { util::getline(is, line); mndHeader.push_back(line); @@ -546,7 +551,7 @@ std::vector TryParseOverviewBlock(std::istream& is) if (is.peek() == '.') { - while (!is.eof() && is.peek() != '\r') + while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') { util::getline(is, line); overviewBlock.push_back(line); @@ -576,7 +581,7 @@ std::optional TryParseSegmentHeader(std::istream& is) header->ugcString_.push_back(line); // If UGC is multi-line, continue parsing - while (!is.eof() && is.peek() != '\r' && + while (!is.eof() && is.peek() != '\r' && is.peek() != '\n' && !RE2::PartialMatch(line, *reUgcExpiration)) { util::getline(is, line); @@ -595,7 +600,7 @@ std::optional TryParseSegmentHeader(std::istream& is) header->vtecString_.push_back(std::move(*vtec)); } - while (!is.eof() && is.peek() != '\r') + while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') { util::getline(is, line); if (!RE2::PartialMatch(line, *reDateTimeString)) @@ -640,10 +645,8 @@ std::optional TryParseVtecString(std::istream& is) if (RE2::PartialMatch(line, *rePVtecString)) { - bool vtecValid; - - vtec = Vtec(); - vtecValid = vtec->pVtec_.Parse(line); + vtec = Vtec(); + bool vtecValid = vtec->pVtec_.Parse(line); isBegin = is.tellg(); @@ -687,5 +690,4 @@ std::shared_ptr TextProductMessage::Create(std::istream& is) return message; } -} // namespace awips -} // namespace scwx +} // namespace scwx::awips diff --git a/wxdata/source/scwx/util/streams.cpp b/wxdata/source/scwx/util/streams.cpp index 9e094f9b..6374ed35 100644 --- a/wxdata/source/scwx/util/streams.cpp +++ b/wxdata/source/scwx/util/streams.cpp @@ -1,8 +1,7 @@ #include +#include -namespace scwx -{ -namespace util +namespace scwx::util { std::istream& getline(std::istream& is, std::string& t) @@ -17,7 +16,8 @@ std::istream& getline(std::istream& is, std::string& t) int c = sb->sbumpc(); switch (c) { - case '\n': return is; + case '\n': + return is; case '\r': while (sb->sgetc() == '\r') @@ -30,6 +30,10 @@ std::istream& getline(std::istream& is, std::string& t) } return is; + case common::Characters::ETX: + sb->sungetc(); + return is; + case std::streambuf::traits_type::eof(): if (t.empty()) { @@ -37,10 +41,10 @@ std::istream& getline(std::istream& is, std::string& t) } return is; - default: t += static_cast(c); + default: + t += static_cast(c); } } } -} // namespace util -} // namespace scwx +} // namespace scwx::util From 8da440ea1f3efa1630727a4b4ddcdcf1cd9c42f6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 2 Feb 2025 01:35:19 -0600 Subject: [PATCH 488/762] General linter cleanup --- wxdata/source/scwx/awips/wmo_header.cpp | 17 +++++++++++------ .../scwx/provider/iem_warnings_provider.cpp | 5 ++++- wxdata/source/scwx/util/json.cpp | 2 ++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index 4d502604..a701e476 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -12,14 +12,14 @@ # include #endif -namespace scwx -{ -namespace awips +namespace scwx::awips { static const std::string logPrefix_ = "scwx::awips::wmo_header"; static const auto logger_ = util::Logger::Create(logPrefix_); +static constexpr std::size_t kWmoHeaderMinLineLength_ = 18; + class WmoHeaderImpl { public: @@ -37,6 +37,11 @@ public: } ~WmoHeaderImpl() = default; + WmoHeaderImpl(const WmoHeaderImpl&) = delete; + WmoHeaderImpl& operator=(const WmoHeaderImpl&) = delete; + WmoHeaderImpl(const WmoHeaderImpl&&) = delete; + WmoHeaderImpl& operator=(const WmoHeaderImpl&&) = delete; + bool operator==(const WmoHeaderImpl& o) const; std::string sequenceNumber_; @@ -138,8 +143,9 @@ bool WmoHeader::Parse(std::istream& is) { // The next line could be the WMO line or the sequence line util::getline(is, wmoLine); - if (wmoLine.length() < 18) + if (wmoLine.length() < kWmoHeaderMinLineLength_) { + // This is likely the sequence line instead sequenceLine.swap(wmoLine); util::getline(is, wmoLine); } @@ -249,5 +255,4 @@ bool WmoHeader::Parse(std::istream& is) return headerValid; } -} // namespace awips -} // namespace scwx +} // namespace scwx::awips diff --git a/wxdata/source/scwx/provider/iem_warnings_provider.cpp b/wxdata/source/scwx/provider/iem_warnings_provider.cpp index 47c29547..4bbd339a 100644 --- a/wxdata/source/scwx/provider/iem_warnings_provider.cpp +++ b/wxdata/source/scwx/provider/iem_warnings_provider.cpp @@ -144,13 +144,16 @@ IemWarningsProvider::LoadTextProducts( std::vector> asyncResponses {}; + asyncResponses.reserve(textProducts.size()); + + const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; for (auto& productId : textProducts) { asyncResponses.emplace_back( productId, cpr::GetAsync( - cpr::Url {kBaseUrl_ + kNwsTextProductEndpoint_ + productId}, + cpr::Url {endpointUrl + productId}, network::cpr::GetHeader(), parameters)); } diff --git a/wxdata/source/scwx/util/json.cpp b/wxdata/source/scwx/util/json.cpp index b8f51507..d5873758 100644 --- a/wxdata/source/scwx/util/json.cpp +++ b/wxdata/source/scwx/util/json.cpp @@ -107,6 +107,8 @@ void WriteJsonFile(const std::string& path, } } +// Allow recursion within the pretty print function +// NOLINTNEXTLINE(misc-no-recursion) static void PrettyPrintJson(std::ostream& os, boost::json::value const& jv, std::string* indent) From cf87cc9bf0ff3871138310fb822d4e84a44ba1f3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 2 Feb 2025 01:36:12 -0600 Subject: [PATCH 489/762] Updating test for IEM provider --- .../provider/iem_warnings_provider.test.cpp | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/source/scwx/provider/iem_warnings_provider.test.cpp b/test/source/scwx/provider/iem_warnings_provider.test.cpp index 6b6809e5..471ac9d0 100644 --- a/test/source/scwx/provider/iem_warnings_provider.test.cpp +++ b/test/source/scwx/provider/iem_warnings_provider.test.cpp @@ -33,14 +33,28 @@ TEST(IemWarningsProviderTest, ListTextProducts) TEST(IemWarningsProviderTest, LoadTextProducts) { static const std::vector productIds { - "202303250016-KMEG-WFUS54-TORMEG", // - "202303252015-KFFC-WFUS52-TORFFC"}; + "202303250016-KMEG-WFUS54-TORMEG", + "202303252015-KFFC-WFUS52-TORFFC", + "202303311942-KLZK-WWUS54-SVSLZK"}; IemWarningsProvider provider {}; auto textProducts = provider.LoadTextProducts(productIds); - EXPECT_EQ(textProducts.size(), 2); + EXPECT_EQ(textProducts.size(), 3); + + if (textProducts.size() >= 1) + { + EXPECT_EQ(textProducts.at(0)->message_count(), 1); + } + if (textProducts.size() >= 2) + { + EXPECT_EQ(textProducts.at(1)->message_count(), 1); + } + if (textProducts.size() >= 3) + { + EXPECT_EQ(textProducts.at(2)->message_count(), 2); + } } } // namespace provider From f9e79b3e407e9ceee279b7b5ab32ebfa251f974c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 2 Feb 2025 01:42:45 -0600 Subject: [PATCH 490/762] Rename IEM warnings provider to IEM API provider --- ...der.test.cpp => iem_api_provider.test.cpp} | 10 +++++----- test/test.cmake | 2 +- ...ings_provider.hpp => iem_api_provider.hpp} | 14 ++++++------- ...ings_provider.cpp => iem_api_provider.cpp} | 20 +++++++++---------- wxdata/wxdata.cmake | 4 ++-- 5 files changed, 25 insertions(+), 25 deletions(-) rename test/source/scwx/provider/{iem_warnings_provider.test.cpp => iem_api_provider.test.cpp} (84%) rename wxdata/include/scwx/provider/{iem_warnings_provider.hpp => iem_api_provider.hpp} (64%) rename wxdata/source/scwx/provider/{iem_warnings_provider.cpp => iem_api_provider.cpp} (89%) diff --git a/test/source/scwx/provider/iem_warnings_provider.test.cpp b/test/source/scwx/provider/iem_api_provider.test.cpp similarity index 84% rename from test/source/scwx/provider/iem_warnings_provider.test.cpp rename to test/source/scwx/provider/iem_api_provider.test.cpp index 471ac9d0..68cb59ae 100644 --- a/test/source/scwx/provider/iem_warnings_provider.test.cpp +++ b/test/source/scwx/provider/iem_api_provider.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include @@ -7,12 +7,12 @@ namespace scwx namespace provider { -TEST(IemWarningsProviderTest, ListTextProducts) +TEST(IemApiProviderTest, ListTextProducts) { using namespace std::chrono; using sys_days = time_point; - IemWarningsProvider provider {}; + IemApiProvider provider {}; auto date = sys_days {2023y / March / 25d}; @@ -30,14 +30,14 @@ TEST(IemWarningsProviderTest, ListTextProducts) } } -TEST(IemWarningsProviderTest, LoadTextProducts) +TEST(IemApiProviderTest, LoadTextProducts) { static const std::vector productIds { "202303250016-KMEG-WFUS54-TORMEG", "202303252015-KFFC-WFUS52-TORFFC", "202303311942-KLZK-WWUS54-SVSLZK"}; - IemWarningsProvider provider {}; + IemApiProvider provider {}; auto textProducts = provider.LoadTextProducts(productIds); diff --git a/test/test.cmake b/test/test.cmake index 17141e0c..64b6c69e 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -19,7 +19,7 @@ set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp source/scwx/provider/aws_level3_data_provider.test.cpp - source/scwx/provider/iem_warnings_provider.test.cpp + source/scwx/provider/iem_api_provider.test.cpp source/scwx/provider/warnings_provider.test.cpp) set(SRC_QT_CONFIG_TESTS source/scwx/qt/config/county_database.test.cpp source/scwx/qt/config/radar_site.test.cpp) diff --git a/wxdata/include/scwx/provider/iem_warnings_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp similarity index 64% rename from wxdata/include/scwx/provider/iem_warnings_provider.hpp rename to wxdata/include/scwx/provider/iem_api_provider.hpp index 35c6bbad..33a82bcf 100644 --- a/wxdata/include/scwx/provider/iem_warnings_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -11,17 +11,17 @@ namespace scwx::provider /** * @brief Warnings Provider */ -class IemWarningsProvider +class IemApiProvider { public: - explicit IemWarningsProvider(); - ~IemWarningsProvider(); + explicit IemApiProvider(); + ~IemApiProvider(); - IemWarningsProvider(const IemWarningsProvider&) = delete; - IemWarningsProvider& operator=(const IemWarningsProvider&) = delete; + IemApiProvider(const IemApiProvider&) = delete; + IemApiProvider& operator=(const IemApiProvider&) = delete; - IemWarningsProvider(IemWarningsProvider&&) noexcept; - IemWarningsProvider& operator=(IemWarningsProvider&&) noexcept; + IemApiProvider(IemApiProvider&&) noexcept; + IemApiProvider& operator=(IemApiProvider&&) noexcept; static std::vector ListTextProducts(std::chrono::sys_time date, diff --git a/wxdata/source/scwx/provider/iem_warnings_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp similarity index 89% rename from wxdata/source/scwx/provider/iem_warnings_provider.cpp rename to wxdata/source/scwx/provider/iem_api_provider.cpp index 4bbd339a..96d1489e 100644 --- a/wxdata/source/scwx/provider/iem_warnings_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -10,7 +10,7 @@ namespace scwx::provider { -static const std::string logPrefix_ = "scwx::provider::iem_warnings_provider"; +static const std::string logPrefix_ = "scwx::provider::iem_api_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); static const std::string kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; @@ -18,7 +18,7 @@ static const std::string kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; static const std::string kListNwsTextProductsEndpoint_ = "/nws/afos/list.json"; static const std::string kNwsTextProductEndpoint_ = "/nwstext/"; -class IemWarningsProvider::Impl +class IemApiProvider::Impl { public: explicit Impl() = default; @@ -29,15 +29,15 @@ public: Impl& operator=(const Impl&&) = delete; }; -IemWarningsProvider::IemWarningsProvider() : p(std::make_unique()) {} -IemWarningsProvider::~IemWarningsProvider() = default; +IemApiProvider::IemApiProvider() : p(std::make_unique()) {} +IemApiProvider::~IemApiProvider() = default; -IemWarningsProvider::IemWarningsProvider(IemWarningsProvider&&) noexcept = +IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = default; -IemWarningsProvider& -IemWarningsProvider::operator=(IemWarningsProvider&&) noexcept = default; +IemApiProvider& +IemApiProvider::operator=(IemApiProvider&&) noexcept = default; -std::vector IemWarningsProvider::ListTextProducts( +std::vector IemApiProvider::ListTextProducts( std::chrono::sys_time date, std::optional cccc, std::optional pil) @@ -137,7 +137,7 @@ std::vector IemWarningsProvider::ListTextProducts( } std::vector> -IemWarningsProvider::LoadTextProducts( +IemApiProvider::LoadTextProducts( const std::vector& textProducts) { auto parameters = cpr::Parameters {{"nolimit", "true"}}; diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 92de3bf0..468f124b 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -61,14 +61,14 @@ set(SRC_NETWORK source/scwx/network/cpr.cpp set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp - include/scwx/provider/iem_warnings_provider.hpp + include/scwx/provider/iem_api_provider.hpp include/scwx/provider/nexrad_data_provider.hpp include/scwx/provider/nexrad_data_provider_factory.hpp include/scwx/provider/warnings_provider.hpp) set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/aws_level3_data_provider.cpp source/scwx/provider/aws_nexrad_data_provider.cpp - source/scwx/provider/iem_warnings_provider.cpp + source/scwx/provider/iem_api_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) From 2eb65defbc60c529ce722a9a17d4aa740f1e8bf8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 2 Feb 2025 01:43:36 -0600 Subject: [PATCH 491/762] Fix broken text product message function signature --- wxdata/source/scwx/awips/text_product_message.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 3b571d83..465b665e 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -25,8 +25,8 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); // Look for hhmm (xM|UTC) to key the date/time string static constexpr LazyRE2 reDateTimeString = {"^[0-9]{3,4} ([AP]M|UTC)"}; -static void ParseCodedInformation(std::shared_ptr segment, - const std::string& wfo); +static void ParseCodedInformation(const std::shared_ptr& segment, + const std::string& wfo); static std::vector ParseProductContent(std::istream& is); static void SkipBlankLines(std::istream& is); static bool TryParseEndOfProduct(std::istream& is); @@ -47,7 +47,7 @@ public: { } ~TextProductMessageImpl() = default; - + TextProductMessageImpl(const TextProductMessageImpl&) = delete; TextProductMessageImpl& operator=(const TextProductMessageImpl&) = delete; TextProductMessageImpl(const TextProductMessageImpl&&) = delete; From c00016cb699b7a07fa1dab0bdce33e32f4759b95 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 17 Feb 2025 23:36:02 -0600 Subject: [PATCH 492/762] Warning file record should use filename, not URL --- wxdata/source/scwx/provider/warnings_provider.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 560cb307..6ef7e237 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -130,8 +130,7 @@ WarningsProvider::LoadUpdatedFiles( { if (headResponse.status_code == cpr::status::HTTP_OK) { - bool updated = - p->UpdateFileRecord(headResponse, url); // TODO: filename + bool updated = p->UpdateFileRecord(headResponse, filename); if (updated) { From 7e9895e0025fafa97b061714605682eceea981e3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 17 Feb 2025 23:39:52 -0600 Subject: [PATCH 493/762] Adding robust date calculation to WMO header --- wxdata/include/scwx/awips/wmo_header.hpp | 36 ++++- .../scwx/awips/text_product_message.cpp | 64 +------- wxdata/source/scwx/awips/wmo_header.cpp | 150 ++++++++++++++++-- 3 files changed, 178 insertions(+), 72 deletions(-) diff --git a/wxdata/include/scwx/awips/wmo_header.hpp b/wxdata/include/scwx/awips/wmo_header.hpp index f3487b6d..f0aed0de 100644 --- a/wxdata/include/scwx/awips/wmo_header.hpp +++ b/wxdata/include/scwx/awips/wmo_header.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -27,7 +28,7 @@ public: explicit WmoHeader(); ~WmoHeader(); - WmoHeader(const WmoHeader&) = delete; + WmoHeader(const WmoHeader&) = delete; WmoHeader& operator=(const WmoHeader&) = delete; WmoHeader(WmoHeader&&) noexcept; @@ -45,8 +46,41 @@ public: std::string product_category() const; std::string product_designator() const; + /** + * @brief Get the WMO date/time + * + * Gets the WMO date/time. Uses the optional date hint provided via + * SetDateHint(std::chrono::year_month). If the date hint has not been + * provided, the endTimeHint parameter is required. + * + * @param [in] endTimeHint The optional end time bounds to provide. This is + * ignored if a date hint has been provided to determine an absolute date. + */ + std::chrono::sys_time GetDateTime( + std::optional endTimeHint = + std::nullopt); + + /** + * @brief Parse a WMO header + * + * @param [in] is The input stream to parse + */ bool Parse(std::istream& is); + /** + * @brief Provide a date hint for the WMO parser + * + * The WMO header contains a date/time in the format DDMMSS. The year and + * month must be derived using another source. The date hint provides the + * additional context required to determine the absolute product time. + * + * This function will update any absolute date/time already calculated, or + * affect the calculation of a subsequent absolute date/time. + * + * @param [in] dateHint The date hint to provide the WMO header parser + */ + void SetDateHint(std::chrono::year_month dateHint); + private: std::unique_ptr p; }; diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 465b665e..9674b325 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -119,71 +119,11 @@ std::chrono::system_clock::time_point Segment::event_begin() const // If event begin is 000000T0000Z if (eventBegin == std::chrono::system_clock::time_point {}) { - using namespace std::chrono; - // Determine event end from P-VTEC string - system_clock::time_point eventEnd = + std::chrono::system_clock::time_point eventEnd = header_->vtecString_[0].pVtec_.event_end(); - auto endDays = floor(eventEnd); - year_month_day endDate {endDays}; - - // Determine WMO date/time - std::string wmoDateTime = wmoHeader_->date_time(); - - bool wmoDateTimeValid = false; - unsigned int dayOfMonth = 0; - unsigned long beginHour = 0; - unsigned long beginMinute = 0; - - try - { - // WMO date time is in the format DDHHMM - dayOfMonth = - static_cast(std::stoul(wmoDateTime.substr(0, 2))); - beginHour = std::stoul(wmoDateTime.substr(2, 2)); - beginMinute = std::stoul(wmoDateTime.substr(4, 2)); - wmoDateTimeValid = true; - } - catch (const std::exception&) - { - logger_->warn("Malformed WMO date/time: {}", wmoDateTime); - } - - if (wmoDateTimeValid) - { - // Combine end date year and month with WMO date time - eventBegin = - sys_days {endDate.year() / endDate.month() / day {dayOfMonth}} + - hours {beginHour} + minutes {beginMinute}; - - // If the begin date is after the end date, assume the start time - // was the previous month (give a 1 day grace period for expiring - // events in the past) - if (eventBegin > eventEnd + 24h) - { - // If the current end month is January - if (endDate.month() == January) - { - // The begin month must be December of last year - eventBegin = - sys_days { - year {static_cast((endDate.year() - 1y).count())} / - December / day {dayOfMonth}} + - hours {beginHour} + minutes {beginMinute}; - } - else - { - // Back up one month - eventBegin = - sys_days {endDate.year() / - month {static_cast( - (endDate.month() - month {1}).count())} / - day {dayOfMonth}} + - hours {beginHour} + minutes {beginMinute}; - } - } - } + eventBegin = wmoHeader_->GetDateTime(eventEnd); } } diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index a701e476..27d169e4 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -42,17 +42,26 @@ public: WmoHeaderImpl(const WmoHeaderImpl&&) = delete; WmoHeaderImpl& operator=(const WmoHeaderImpl&&) = delete; + void CalculateAbsoluteDateTime(); + bool ParseDateTime(unsigned int& dayOfMonth, + unsigned long& hour, + unsigned long& minute); + bool operator==(const WmoHeaderImpl& o) const; - std::string sequenceNumber_; - std::string dataType_; - std::string geographicDesignator_; - std::string bulletinId_; - std::string icao_; - std::string dateTime_; - std::string bbbIndicator_; - std::string productCategory_; - std::string productDesignator_; + std::string sequenceNumber_ {}; + std::string dataType_ {}; + std::string geographicDesignator_ {}; + std::string bulletinId_ {}; + std::string icao_ {}; + std::string dateTime_ {}; + std::string bbbIndicator_ {}; + std::string productCategory_ {}; + std::string productDesignator_ {}; + + std::optional dateHint_ {}; + std::optional> + absoluteDateTime_ {}; }; WmoHeader::WmoHeader() : p(std::make_unique()) {} @@ -124,6 +133,71 @@ std::string WmoHeader::product_designator() const return p->productDesignator_; } +std::chrono::sys_time WmoHeader::GetDateTime( + std::optional endTimeHint) +{ + std::chrono::sys_time wmoDateTime {}; + + if (p->absoluteDateTime_.has_value()) + { + wmoDateTime = p->absoluteDateTime_.value(); + } + else if (endTimeHint.has_value()) + { + bool dateTimeValid = false; + unsigned int dayOfMonth = 0; + unsigned long hour = 0; + unsigned long minute = 0; + + dateTimeValid = p->ParseDateTime(dayOfMonth, hour, minute); + + if (dateTimeValid) + { + using namespace std::chrono; + + auto endDays = floor(endTimeHint.value()); + year_month_day endDate {endDays}; + + // Combine end date year and month with WMO date time + wmoDateTime = + sys_days {endDate.year() / endDate.month() / day {dayOfMonth}} + + hours {hour} + minutes {minute}; + + // If the begin date is after the end date, assume the start time + // was the previous month (give a 1 day grace period for expiring + // events in the past) + if (wmoDateTime > endTimeHint.value() + 24h) + { + // If the current end month is January + if (endDate.month() == January) + { + year_month x = year {2024} / December; + sys_days y; + + // The begin month must be December of last year + wmoDateTime = + sys_days { + year {static_cast((endDate.year() - 1y).count())} / + December / day {dayOfMonth}} + + hours {hour} + minutes {minute}; + } + else + { + // Back up one month + wmoDateTime = + sys_days {endDate.year() / + month {static_cast( + (endDate.month() - month {1}).count())} / + day {dayOfMonth}} + + hours {hour} + minutes {minute}; + } + } + } + } + + return wmoDateTime; +} + bool WmoHeader::Parse(std::istream& is) { bool headerValid = true; @@ -224,6 +298,8 @@ bool WmoHeader::Parse(std::istream& is) p->icao_ = wmoTokenList[1]; p->dateTime_ = wmoTokenList[2]; + p->CalculateAbsoluteDateTime(); + if (wmoTokenList.size() == 4) { p->bbbIndicator_ = wmoTokenList[3]; @@ -255,4 +331,60 @@ bool WmoHeader::Parse(std::istream& is) return headerValid; } +void WmoHeader::SetDateHint(std::chrono::year_month dateHint) +{ + p->dateHint_ = dateHint; + p->CalculateAbsoluteDateTime(); +} + +bool WmoHeaderImpl::ParseDateTime(unsigned int& dayOfMonth, + unsigned long& hour, + unsigned long& minute) +{ + bool dateTimeValid = false; + + try + { + // WMO date time is in the format DDHHMM + dayOfMonth = + static_cast(std::stoul(dateTime_.substr(0, 2))); + hour = std::stoul(dateTime_.substr(2, 2)); + minute = std::stoul(dateTime_.substr(4, 2)); + dateTimeValid = true; + } + catch (const std::exception&) + { + logger_->warn("Malformed WMO date/time: {}", dateTime_); + } + + return dateTimeValid; +} + +void WmoHeaderImpl::CalculateAbsoluteDateTime() +{ + bool dateTimeValid = false; + + if (dateHint_.has_value() && !dateTime_.empty()) + { + unsigned int dayOfMonth = 0; + unsigned long hour = 0; + unsigned long minute = 0; + + dateTimeValid = ParseDateTime(dayOfMonth, hour, minute); + + if (dateTimeValid) + { + using namespace std::chrono; + absoluteDateTime_ = sys_days {dateHint_->year() / dateHint_->month() / + day {dayOfMonth}} + + hours {hour} + minutes {minute}; + } + } + + if (!dateTimeValid) + { + absoluteDateTime_.reset(); + } +} + } // namespace scwx::awips From 3998f0fe3686fc0e6657d12d6ec859f9639440ce Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 12:07:35 -0600 Subject: [PATCH 494/762] Use unique .clang-tidy for test, ignoring checks for magic numbers --- .clang-tidy | 4 ++-- .github/workflows/clang-tidy-review.yml | 2 +- test/.clang-tidy | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 test/.clang-tidy diff --git a/.clang-tidy b/.clang-tidy index 3c98e81d..dbf9fbd7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,10 +6,10 @@ Checks: - 'misc-*' - 'modernize-*' - 'performance-*' + - '-bugprone-easily-swappable-parameters' - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - - '-modernize-use-trailing-return-type' - - '-bugprone-easily-swappable-parameters' - '-modernize-return-braced-init-list' + - '-modernize-use-trailing-return-type' FormatStyle: 'file' diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index c37236d6..a7ec09ff 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -126,7 +126,7 @@ jobs: --build_dir='../build' \ --base_dir='${{ github.workspace }}/source' \ --clang_tidy_checks='' \ - --config_file='.clang-tidy' \ + --config_file='' \ --include='*.[ch],*.[ch]xx,*.[chi]pp,*.[ch]++,*.cc,*.hh' \ --exclude='' \ --apt-packages='' \ diff --git a/test/.clang-tidy b/test/.clang-tidy new file mode 100644 index 00000000..d5079a03 --- /dev/null +++ b/test/.clang-tidy @@ -0,0 +1,16 @@ +Checks: + - '-*' + - 'bugprone-*' + - 'clang-analyzer-*' + - 'cppcoreguidelines-*' + - 'misc-*' + - 'modernize-*' + - 'performance-*' + - '-bugprone-easily-swappable-parameters' + - '-cppcoreguidelines-avoid-magic-numbers' + - '-cppcoreguidelines-pro-type-reinterpret-cast' + - '-misc-include-cleaner' + - '-misc-non-private-member-variables-in-classes' + - '-modernize-return-braced-init-list' + - '-modernize-use-trailing-return-type' +FormatStyle: 'file' From 8646c3da6d5403538b5a53f26993074df4c11ce7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 12:08:57 -0600 Subject: [PATCH 495/762] Add WMO header test --- test/source/scwx/awips/wmo_header.test.cpp | 130 +++++++++++++++++++++ test/test.cmake | 3 +- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 test/source/scwx/awips/wmo_header.test.cpp diff --git a/test/source/scwx/awips/wmo_header.test.cpp b/test/source/scwx/awips/wmo_header.test.cpp new file mode 100644 index 00000000..bdc4406f --- /dev/null +++ b/test/source/scwx/awips/wmo_header.test.cpp @@ -0,0 +1,130 @@ +#include + +#include + +namespace scwx::awips +{ + +static const std::string logPrefix_ = "scwx::awips::wmo_header.test"; + +static const std::string kWmoHeaderSample_ { + "887\n" + "WFUS54 KOUN 280044\n" + "TOROUN"}; + +TEST(WmoHeader, WmoFields) +{ + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + bool valid = header.Parse(ss); + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.sequence_number(), "887"); + EXPECT_EQ(header.data_type(), "WF"); + EXPECT_EQ(header.geographic_designator(), "US"); + EXPECT_EQ(header.bulletin_id(), "54"); + EXPECT_EQ(header.icao(), "KOUN"); + EXPECT_EQ(header.date_time(), "280044"); + EXPECT_EQ(header.bbb_indicator(), ""); + EXPECT_EQ(header.product_category(), "TOR"); + EXPECT_EQ(header.product_designator(), "OUN"); + EXPECT_EQ(header.GetDateTime(), + std::chrono::sys_time {}); +} + +TEST(WmoHeader, DateHintBeforeParse) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + header.SetDateHint(2022y / October); + bool valid = header.Parse(ss); + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(), + sys_days {2022y / October / 28d} + 0h + 44min); +} + +TEST(WmoHeader, DateHintAfterParse) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + bool valid = header.Parse(ss); + header.SetDateHint(2022y / October); + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(), + sys_days {2022y / October / 28d} + 0h + 44min); +} + +TEST(WmoHeader, EndTimeHintSameMonth) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + bool valid = header.Parse(ss); + + auto endTimeHint = sys_days {2022y / October / 29d} + 0h + 0min + 0s; + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(endTimeHint), + sys_days {2022y / October / 28d} + 0h + 44min); +} + +TEST(WmoHeader, EndTimeHintPreviousMonth) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + bool valid = header.Parse(ss); + + auto endTimeHint = sys_days {2022y / October / 27d} + 0h + 0min + 0s; + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(endTimeHint), + sys_days {2022y / September / 28d} + 0h + 44min); +} + +TEST(WmoHeader, EndTimeHintPreviousYear) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + bool valid = header.Parse(ss); + + auto endTimeHint = sys_days {2022y / January / 27d} + 0h + 0min + 0s; + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(endTimeHint), + sys_days {2021y / December / 28d} + 0h + 44min); +} + +TEST(WmoHeader, EndTimeHintIgnored) +{ + using namespace std::chrono; + + std::stringstream ss {kWmoHeaderSample_}; + WmoHeader header; + + header.SetDateHint(2022y / October); + bool valid = header.Parse(ss); + + auto endTimeHint = sys_days {2020y / January / 1d} + 0h + 0min + 0s; + + EXPECT_EQ(valid, true); + EXPECT_EQ(header.GetDateTime(endTimeHint), + sys_days {2022y / October / 28d} + 0h + 44min); +} + +} // namespace scwx::awips diff --git a/test/test.cmake b/test/test.cmake index 64b6c69e..0ae26b53 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -12,7 +12,8 @@ set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp source/scwx/awips/coded_time_motion_location.test.cpp source/scwx/awips/pvtec.test.cpp source/scwx/awips/text_product_file.test.cpp - source/scwx/awips/ugc.test.cpp) + source/scwx/awips/ugc.test.cpp + source/scwx/awips/wmo_header.test.cpp) set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp source/scwx/common/products.test.cpp) set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) From b60318c393caf65972129e76f5594080cc57eba1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 12:16:45 -0600 Subject: [PATCH 496/762] WMO header clang-tidy fixes --- wxdata/include/scwx/awips/wmo_header.hpp | 7 ++----- wxdata/source/scwx/awips/wmo_header.cpp | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/wxdata/include/scwx/awips/wmo_header.hpp b/wxdata/include/scwx/awips/wmo_header.hpp index f0aed0de..6ca63709 100644 --- a/wxdata/include/scwx/awips/wmo_header.hpp +++ b/wxdata/include/scwx/awips/wmo_header.hpp @@ -4,9 +4,7 @@ #include #include -namespace scwx -{ -namespace awips +namespace scwx::awips { class WmoHeaderImpl; @@ -85,5 +83,4 @@ private: std::unique_ptr p; }; -} // namespace awips -} // namespace scwx +} // namespace scwx::awips diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index 27d169e4..ddaf06b2 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -18,7 +18,11 @@ namespace scwx::awips static const std::string logPrefix_ = "scwx::awips::wmo_header"; static const auto logger_ = util::Logger::Create(logPrefix_); -static constexpr std::size_t kWmoHeaderMinLineLength_ = 18; +static constexpr std::size_t kWmoHeaderMinLineLength_ = 18; +static constexpr std::size_t kWmoIdentifierLength_ = 6; +static constexpr std::size_t kIcaoLength_ = 4; +static constexpr std::size_t kDateTimeLength_ = 6; +static constexpr std::size_t kAwipsIdentifierLineLength_ = 6; class WmoHeaderImpl { @@ -166,14 +170,12 @@ std::chrono::sys_time WmoHeader::GetDateTime( // If the begin date is after the end date, assume the start time // was the previous month (give a 1 day grace period for expiring // events in the past) + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) if (wmoDateTime > endTimeHint.value() + 24h) { // If the current end month is January if (endDate.month() == January) { - year_month x = year {2024} / December; - sys_days y; - // The begin month must be December of last year wmoDateTime = sys_days { @@ -269,17 +271,17 @@ bool WmoHeader::Parse(std::istream& is) logger_->warn("Invalid number of WMO tokens"); headerValid = false; } - else if (wmoTokenList[0].size() != 6) + else if (wmoTokenList[0].size() != kWmoIdentifierLength_) { logger_->warn("WMO identifier malformed"); headerValid = false; } - else if (wmoTokenList[1].size() != 4) + else if (wmoTokenList[1].size() != kIcaoLength_) { logger_->warn("ICAO malformed"); headerValid = false; } - else if (wmoTokenList[2].size() != 6) + else if (wmoTokenList[2].size() != kDateTimeLength_) { logger_->warn("Date/time malformed"); headerValid = false; @@ -316,7 +318,7 @@ bool WmoHeader::Parse(std::istream& is) if (headerValid) { - if (awipsLine.size() != 6) + if (awipsLine.size() != kAwipsIdentifierLineLength_) { logger_->warn("AWIPS Identifier Line bad size"); headerValid = false; From 163b7039646a0afdfe33b715e449fe1ea6b6a9db Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 12:28:30 -0600 Subject: [PATCH 497/762] Use constexpr instead of #define where possible in time.cpp --- wxdata/source/scwx/util/time.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 7a706224..20c6121d 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -60,15 +60,19 @@ std::string TimeString(std::chrono::system_clock::time_point time, using namespace std::chrono; #if (__cpp_lib_chrono >= 201907L) -# define FORMAT_STRING_24_HOUR "{:%Y-%m-%d %H:%M:%S %Z}" -# define FORMAT_STRING_12_HOUR "{:%Y-%m-%d %I:%M:%S %p %Z}" namespace date = std::chrono; namespace df = std; + + static constexpr std::string_view kFormatString24Hour = + "{:%Y-%m-%d %H:%M:%S %Z}"; + static constexpr std::string_view kFormatString12Hour = + "{:%Y-%m-%d %I:%M:%S %p %Z}"; #else -# define FORMAT_STRING_24_HOUR "%Y-%m-%d %H:%M:%S %Z" -# define FORMAT_STRING_12_HOUR "%Y-%m-%d %I:%M:%S %p %Z" using namespace date; namespace df = date; + +# define kFormatString24Hour "%Y-%m-%d %H:%M:%S %Z" +# define kFormatString12Hour "%Y-%m-%d %I:%M:%S %p %Z" #endif auto timeInSeconds = time_point_cast(time); @@ -84,11 +88,11 @@ std::string TimeString(std::chrono::system_clock::time_point time, if (clockFormat == ClockFormat::_24Hour) { - os << df::format(FORMAT_STRING_24_HOUR, zt); + os << df::format(kFormatString24Hour, zt); } else { - os << df::format(FORMAT_STRING_12_HOUR, zt); + os << df::format(kFormatString12Hour, zt); } } catch (const std::exception& ex) @@ -110,11 +114,11 @@ std::string TimeString(std::chrono::system_clock::time_point time, { if (clockFormat == ClockFormat::_24Hour) { - os << df::format(FORMAT_STRING_24_HOUR, timeInSeconds); + os << df::format(kFormatString24Hour, timeInSeconds); } else { - os << df::format(FORMAT_STRING_12_HOUR, timeInSeconds); + os << df::format(kFormatString12Hour, timeInSeconds); } } } From d63c2a3ef9a3633eeeaaea4a903084d808788817 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 14:09:22 -0600 Subject: [PATCH 498/762] Insert text product messages in chronological order --- .../scwx/qt/manager/text_event_manager.cpp | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 0a7c66f5..f0fbaa6c 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -80,7 +80,8 @@ public: threadPool_.join(); } - void HandleMessage(std::shared_ptr message); + void + HandleMessage(const std::shared_ptr& message); void RefreshAsync(); void Refresh(); @@ -171,7 +172,7 @@ void TextEventManager::LoadFile(const std::string& filename) } void TextEventManager::Impl::HandleMessage( - std::shared_ptr message) + const std::shared_ptr& message) { auto segments = message->segments(); @@ -220,8 +221,23 @@ void TextEventManager::Impl::HandleMessage( // If there was a matching event, and this message has not been stored // (WMO header equivalence check), add the updated message to the existing // event - messageIndex = it->second.size(); - it->second.push_back(message); + + // Determine the chronological sequence of the message. Note, if there + // were no time hints given to the WMO header, this will place the message + // at the end of the vector. + auto insertionPoint = std::upper_bound( + it->second.begin(), + it->second.end(), + message, + [](const std::shared_ptr& a, + const std::shared_ptr& b) { + return a->wmo_header()->GetDateTime() < + b->wmo_header()->GetDateTime(); + }); + + // Insert the message in chronological order + messageIndex = std::distance(it->second.begin(), insertionPoint); + it->second.insert(insertionPoint, message); updated = true; }; From 46972e87694f9ac5badfa5dd6ff29c705ca0ccbd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 23:24:27 -0600 Subject: [PATCH 499/762] Formatting iem_api_provider.cpp --- .../source/scwx/provider/iem_api_provider.cpp | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 96d1489e..1d5781f0 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -32,15 +32,13 @@ public: IemApiProvider::IemApiProvider() : p(std::make_unique()) {} IemApiProvider::~IemApiProvider() = default; -IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = - default; -IemApiProvider& -IemApiProvider::operator=(IemApiProvider&&) noexcept = default; +IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = default; +IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; -std::vector IemApiProvider::ListTextProducts( - std::chrono::sys_time date, - std::optional cccc, - std::optional pil) +std::vector +IemApiProvider::ListTextProducts(std::chrono::sys_time date, + std::optional cccc, + std::optional pil) { using namespace std::chrono; @@ -137,25 +135,23 @@ std::vector IemApiProvider::ListTextProducts( } std::vector> -IemApiProvider::LoadTextProducts( - const std::vector& textProducts) +IemApiProvider::LoadTextProducts(const std::vector& textProducts) { auto parameters = cpr::Parameters {{"nolimit", "true"}}; std::vector> asyncResponses {}; asyncResponses.reserve(textProducts.size()); - + const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; for (auto& productId : textProducts) { asyncResponses.emplace_back( productId, - cpr::GetAsync( - cpr::Url {endpointUrl + productId}, - network::cpr::GetHeader(), - parameters)); + cpr::GetAsync(cpr::Url {endpointUrl + productId}, + network::cpr::GetHeader(), + parameters)); } std::vector> textProductFiles; From a6ba312f6b461397447fe50ab2b48772ef6ba0ef Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 23:33:58 -0600 Subject: [PATCH 500/762] Provide year/month hint to WMO header parser based on filename --- .../include/scwx/awips/text_product_file.hpp | 2 +- .../source/scwx/awips/text_product_file.cpp | 29 +++++++++++++++++-- .../source/scwx/provider/iem_api_provider.cpp | 3 +- .../scwx/provider/warnings_provider.cpp | 2 +- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/wxdata/include/scwx/awips/text_product_file.hpp b/wxdata/include/scwx/awips/text_product_file.hpp index 478a93b4..80eda9c8 100644 --- a/wxdata/include/scwx/awips/text_product_file.hpp +++ b/wxdata/include/scwx/awips/text_product_file.hpp @@ -29,7 +29,7 @@ public: std::shared_ptr message(size_t i) const; bool LoadFile(const std::string& filename); - bool LoadData(std::istream& is); + bool LoadData(std::string_view filename, std::istream& is); private: std::unique_ptr p; diff --git a/wxdata/source/scwx/awips/text_product_file.cpp b/wxdata/source/scwx/awips/text_product_file.cpp index 3edc7b2d..cd23516c 100644 --- a/wxdata/source/scwx/awips/text_product_file.cpp +++ b/wxdata/source/scwx/awips/text_product_file.cpp @@ -3,6 +3,8 @@ #include +#include + namespace scwx { namespace awips @@ -59,16 +61,34 @@ bool TextProductFile::LoadFile(const std::string& filename) if (fileValid) { - fileValid = LoadData(f); + fileValid = LoadData(filename, f); } return fileValid; } -bool TextProductFile::LoadData(std::istream& is) +bool TextProductFile::LoadData(std::string_view filename, std::istream& is) { + static constexpr LazyRE2 kDateTimePattern_ = { + R"(((?:19|20)\d{2}))" // Year (YYYY) + R"((0[1-9]|1[0-2]))" // Month (MM) + R"((0[1-9]|[12]\d|3[01]))" // Day (DD) + R"(_?)" // Optional separator (not captured) + R"(([01]\d|2[0-3]))" // Hour (HH) + }; + logger_->trace("Loading Data"); + // Attempt to parse the date from the filename + std::optional yearMonth; + int year {}; + unsigned int month {}; + + if (RE2::PartialMatch(filename, *kDateTimePattern_, &year, &month)) + { + yearMonth = std::chrono::year {year} / std::chrono::month {month}; + } + while (!is.eof()) { std::shared_ptr message = @@ -88,6 +108,11 @@ bool TextProductFile::LoadData(std::istream& is) if (!duplicate) { + if (yearMonth.has_value()) + { + message->wmo_header()->SetDateHint(yearMonth.value()); + } + p->messages_.push_back(message); } } diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 1d5781f0..66cdc5d6 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -163,10 +163,11 @@ IemApiProvider::LoadTextProducts(const std::vector& textProducts) if (response.status_code == cpr::status::HTTP_OK) { // Load file + auto productId = asyncResponse.first; std::shared_ptr textProductFile { std::make_shared()}; std::istringstream responseBody {response.text}; - if (textProductFile->LoadData(responseBody)) + if (textProductFile->LoadData(productId, responseBody)) { textProductFiles.push_back(textProductFile); } diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 6ef7e237..f0d6b8dd 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -176,7 +176,7 @@ WarningsProvider::LoadUpdatedFiles( std::shared_ptr textProductFile { std::make_shared()}; std::istringstream responseBody {response.text}; - if (textProductFile->LoadData(responseBody)) + if (textProductFile->LoadData(filename, responseBody)) { updatedFiles.push_back(textProductFile); } From d3d98234593a86796a5d06cbfc6508ba3f10a6df Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 23:34:48 -0600 Subject: [PATCH 501/762] More clang-tidy fixes --- .../include/scwx/awips/text_product_file.hpp | 17 +++++++------ wxdata/include/scwx/awips/wmo_header.hpp | 22 ++++++++--------- .../source/scwx/awips/text_product_file.cpp | 24 +++++++++++-------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/wxdata/include/scwx/awips/text_product_file.hpp b/wxdata/include/scwx/awips/text_product_file.hpp index 80eda9c8..ce164f20 100644 --- a/wxdata/include/scwx/awips/text_product_file.hpp +++ b/wxdata/include/scwx/awips/text_product_file.hpp @@ -5,9 +5,7 @@ #include #include -namespace scwx -{ -namespace awips +namespace scwx::awips { class TextProductFileImpl; @@ -24,16 +22,17 @@ public: TextProductFile(TextProductFile&&) noexcept; TextProductFile& operator=(TextProductFile&&) noexcept; - size_t message_count() const; - std::vector> messages() const; - std::shared_ptr message(size_t i) const; + [[nodiscard]] std::size_t message_count() const; + [[nodiscard]] std::vector> + messages() const; + [[nodiscard]] std::shared_ptr message(size_t i) const; bool LoadFile(const std::string& filename); bool LoadData(std::string_view filename, std::istream& is); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; -} // namespace awips -} // namespace scwx +} // namespace scwx::awips diff --git a/wxdata/include/scwx/awips/wmo_header.hpp b/wxdata/include/scwx/awips/wmo_header.hpp index 6ca63709..f3e24faf 100644 --- a/wxdata/include/scwx/awips/wmo_header.hpp +++ b/wxdata/include/scwx/awips/wmo_header.hpp @@ -34,15 +34,15 @@ public: bool operator==(const WmoHeader& o) const; - std::string sequence_number() const; - std::string data_type() const; - std::string geographic_designator() const; - std::string bulletin_id() const; - std::string icao() const; - std::string date_time() const; - std::string bbb_indicator() const; - std::string product_category() const; - std::string product_designator() const; + [[nodiscard]] std::string sequence_number() const; + [[nodiscard]] std::string data_type() const; + [[nodiscard]] std::string geographic_designator() const; + [[nodiscard]] std::string bulletin_id() const; + [[nodiscard]] std::string icao() const; + [[nodiscard]] std::string date_time() const; + [[nodiscard]] std::string bbb_indicator() const; + [[nodiscard]] std::string product_category() const; + [[nodiscard]] std::string product_designator() const; /** * @brief Get the WMO date/time @@ -54,7 +54,7 @@ public: * @param [in] endTimeHint The optional end time bounds to provide. This is * ignored if a date hint has been provided to determine an absolute date. */ - std::chrono::sys_time GetDateTime( + [[nodiscard]] std::chrono::sys_time GetDateTime( std::optional endTimeHint = std::nullopt); @@ -68,7 +68,7 @@ public: /** * @brief Provide a date hint for the WMO parser * - * The WMO header contains a date/time in the format DDMMSS. The year and + * The WMO header contains a date/time in the format DDHHMM. The year and * month must be derived using another source. The date hint provides the * additional context required to determine the absolute product time. * diff --git a/wxdata/source/scwx/awips/text_product_file.cpp b/wxdata/source/scwx/awips/text_product_file.cpp index cd23516c..9b7dcdb0 100644 --- a/wxdata/source/scwx/awips/text_product_file.cpp +++ b/wxdata/source/scwx/awips/text_product_file.cpp @@ -5,24 +5,29 @@ #include -namespace scwx -{ -namespace awips +namespace scwx::awips { static const std::string logPrefix_ = "scwx::awips::text_product_file"; static const auto logger_ = util::Logger::Create(logPrefix_); -class TextProductFileImpl +class TextProductFile::Impl { public: - explicit TextProductFileImpl() : messages_ {} {}; - ~TextProductFileImpl() = default; + explicit Impl() : messages_ {} {}; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + Impl(Impl&&) = delete; + Impl& operator=(Impl&&) = delete; std::vector> messages_; }; -TextProductFile::TextProductFile() : p(std::make_unique()) +TextProductFile::TextProductFile() : + p(std::make_unique()) { } TextProductFile::~TextProductFile() = default; @@ -97,7 +102,7 @@ bool TextProductFile::LoadData(std::string_view filename, std::istream& is) if (message != nullptr) { - for (auto m : p->messages_) + for (const auto& m : p->messages_) { if (*m->wmo_header().get() == *message->wmo_header().get()) { @@ -125,5 +130,4 @@ bool TextProductFile::LoadData(std::string_view filename, std::istream& is) return !p->messages_.empty(); } -} // namespace awips -} // namespace scwx +} // namespace scwx::awips From d00b2fe06327b3111cd21b17c14ddee0dd863a59 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 18 Feb 2025 23:55:14 -0600 Subject: [PATCH 502/762] Use const std::string& instead of std::string_view with RE2 to avoid abseil issues --- wxdata/include/scwx/awips/text_product_file.hpp | 2 +- wxdata/source/scwx/awips/text_product_file.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wxdata/include/scwx/awips/text_product_file.hpp b/wxdata/include/scwx/awips/text_product_file.hpp index ce164f20..b0d1c965 100644 --- a/wxdata/include/scwx/awips/text_product_file.hpp +++ b/wxdata/include/scwx/awips/text_product_file.hpp @@ -28,7 +28,7 @@ public: [[nodiscard]] std::shared_ptr message(size_t i) const; bool LoadFile(const std::string& filename); - bool LoadData(std::string_view filename, std::istream& is); + bool LoadData(const std::string& filename, std::istream& is); private: class Impl; diff --git a/wxdata/source/scwx/awips/text_product_file.cpp b/wxdata/source/scwx/awips/text_product_file.cpp index 9b7dcdb0..96d5503f 100644 --- a/wxdata/source/scwx/awips/text_product_file.cpp +++ b/wxdata/source/scwx/awips/text_product_file.cpp @@ -72,7 +72,7 @@ bool TextProductFile::LoadFile(const std::string& filename) return fileValid; } -bool TextProductFile::LoadData(std::string_view filename, std::istream& is) +bool TextProductFile::LoadData(const std::string& filename, std::istream& is) { static constexpr LazyRE2 kDateTimePattern_ = { R"(((?:19|20)\d{2}))" // Year (YYYY) From 3f83c8e4a9ee0764dfb8bb27e083a3f72c19bac1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 07:13:49 -0500 Subject: [PATCH 503/762] IEM API provider should use std::string instead of std::string_view for abseil compatibility --- wxdata/source/scwx/provider/iem_api_provider.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 66cdc5d6..817fbc2f 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -139,7 +139,7 @@ IemApiProvider::LoadTextProducts(const std::vector& textProducts) { auto parameters = cpr::Parameters {{"nolimit", "true"}}; - std::vector> + std::vector> asyncResponses {}; asyncResponses.reserve(textProducts.size()); @@ -163,7 +163,7 @@ IemApiProvider::LoadTextProducts(const std::vector& textProducts) if (response.status_code == cpr::status::HTTP_OK) { // Load file - auto productId = asyncResponse.first; + auto& productId = asyncResponse.first; std::shared_ptr textProductFile { std::make_shared()}; std::istringstream responseBody {response.text}; From 16507adbe9ee4e3ae6a0be922d10ebd98b03c782 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 08:14:18 -0500 Subject: [PATCH 504/762] Alert layer should handle alerts by UUID if messages are received out of sequence --- .../scwx/qt/manager/text_event_manager.cpp | 2 +- .../scwx/qt/manager/text_event_manager.hpp | 5 ++- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 35 ++++++++++++++++--- .../scwx/awips/text_product_message.hpp | 3 ++ .../scwx/awips/text_product_message.cpp | 8 +++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index f0fbaa6c..d76212fd 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -245,7 +245,7 @@ void TextEventManager::Impl::HandleMessage( if (updated) { - Q_EMIT self_->AlertUpdated(key, messageIndex); + Q_EMIT self_->AlertUpdated(key, messageIndex, message->uuid()); } } diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp index f97ca223..30748781 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace scwx @@ -32,7 +33,9 @@ public: static std::shared_ptr Instance(); signals: - void AlertUpdated(const types::TextEventKey& key, size_t messageIndex); + void AlertUpdated(const types::TextEventKey& key, + std::size_t messageIndex, + boost::uuids::uuid uuid); private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 7c5c9db2..77a2332f 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -73,8 +73,10 @@ public: connect(textEventManager_.get(), &manager::TextEventManager::AlertUpdated, this, - [this](const types::TextEventKey& key, std::size_t messageIndex) - { HandleAlert(key, messageIndex); }); + [this](const types::TextEventKey& key, + std::size_t messageIndex, + boost::uuids::uuid uuid) + { HandleAlert(key, messageIndex, uuid); }); } ~AlertLayerHandler() { @@ -95,7 +97,9 @@ public: types::TextEventHash> segmentsByKey_ {}; - void HandleAlert(const types::TextEventKey& key, size_t messageIndex); + void HandleAlert(const types::TextEventKey& key, + size_t messageIndex, + boost::uuids::uuid uuid); static AlertLayerHandler& Instance(); @@ -322,7 +326,8 @@ bool IsAlertActive(const std::shared_ptr& segment) } void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, - size_t messageIndex) + size_t messageIndex, + boost::uuids::uuid uuid) { logger_->trace("HandleAlert: {}", key.ToString()); @@ -330,7 +335,27 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, AlertTypeHash>> alertsUpdated {}; - auto message = textEventManager_->message_list(key).at(messageIndex); + const auto& messageList = textEventManager_->message_list(key); + auto message = messageList.at(messageIndex); + + if (message->uuid() != uuid) + { + // Find message by UUID instead of index, as the message index could have + // changed between the signal being emitted and the handler being called + auto it = std::find_if(messageList.cbegin(), + messageList.cend(), + [&uuid](const auto& message) + { return uuid == message->uuid(); }); + + if (it == messageList.cend()) + { + logger_->warn( + "Could not find alert uuid: {} ({})", key.ToString(), messageIndex); + return; + } + + message = *it; + } // Determine start time for first segment std::chrono::system_clock::time_point segmentBegin {}; diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index b043494f..6830f91a 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -13,6 +13,8 @@ #include #include +#include + namespace scwx { namespace awips @@ -94,6 +96,7 @@ public: TextProductMessage(TextProductMessage&&) noexcept; TextProductMessage& operator=(TextProductMessage&&) noexcept; + boost::uuids::uuid uuid() const; std::string message_content() const; std::shared_ptr wmo_header() const; std::vector mnd_header() const; diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index 9674b325..f8716b2b 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -9,6 +9,7 @@ #include #include +#include #include namespace scwx::awips @@ -53,6 +54,8 @@ public: TextProductMessageImpl(const TextProductMessageImpl&&) = delete; TextProductMessageImpl& operator=(const TextProductMessageImpl&&) = delete; + boost::uuids::uuid uuid_ {boost::uuids::random_generator()()}; + std::string messageContent_; std::shared_ptr wmoHeader_; std::vector mndHeader_; @@ -70,6 +73,11 @@ TextProductMessage::TextProductMessage(TextProductMessage&&) noexcept = default; TextProductMessage& TextProductMessage::operator=(TextProductMessage&&) noexcept = default; +boost::uuids::uuid TextProductMessage::uuid() const +{ + return p->uuid_; +} + std::string TextProductMessage::message_content() const { return p->messageContent_; From 1e1422a3dd8ff7172afdb174c808674162d5e6c0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 08:19:49 -0500 Subject: [PATCH 505/762] Only handle the most recent message for each event in the alert model --- scwx-qt/source/scwx/qt/model/alert_model.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index fed7cc17..516dbfae 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -335,8 +335,15 @@ void AlertModel::HandleAlert(const types::TextEventKey& alertKey, double distanceInMeters; - // Get the most recent segment for the event auto alertMessages = p->textEventManager_->message_list(alertKey); + + // Skip alert if this is not the most recent message + if (messageIndex + 1 < alertMessages.size()) + { + return; + } + + // Get the most recent segment for the event std::shared_ptr alertSegment = alertMessages[messageIndex]->segments().back(); From e4fc13aa929cbe8a42918c0f8d3d4b2432fe0583 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 08:37:51 -0500 Subject: [PATCH 506/762] Add an option for enabling/disabling the release console at build time --- scwx-qt/scwx-qt.cmake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 89b31011..1628607a 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -11,6 +11,8 @@ set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +OPTION(SCWX_DISABLE_CONSOLE "Disables the Windows console in release mode" ON) + find_package(Boost) find_package(Fontconfig) find_package(geographiclib) @@ -615,7 +617,9 @@ set_target_properties(scwx-qt_update_radar_sites PROPERTIES FOLDER generate) if (WIN32) set(APP_ICON_RESOURCE_WINDOWS ${RESOURCE_OUTPUT}) qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${APP_ICON_RESOURCE_WINDOWS}) - set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $,TRUE,FALSE>) + if (SCWX_DISABLE_CONSOLE) + set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $,TRUE,FALSE>) + endif() else() qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES}) endif() From cc54e4d834f264955bdc558476b0783a0fdca40b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Apr 2025 20:48:36 -0500 Subject: [PATCH 507/762] Load archived warnings when making a timeline selection --- scwx-qt/source/scwx/qt/main/main_window.cpp | 1 + .../scwx/qt/manager/text_event_manager.cpp | 55 ++++++++++++++++++- .../scwx/qt/manager/text_event_manager.hpp | 2 + 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 71f4d00c..d213b4fd 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1018,6 +1018,7 @@ void MainWindowImpl::ConnectAnimationSignals() for (auto map : maps_) { map->SelectTime(dateTime); + textEventManager_->SelectTime(dateTime); QMetaObject::invokeMethod( map, static_cast(&QWidget::update)); } diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index d76212fd..dffdb6bf 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,9 @@ static constexpr std::chrono::hours kInitialLoadHistoryDuration_ = static constexpr std::chrono::hours kDefaultLoadHistoryDuration_ = std::chrono::hours {1}; +static const std::array kPils_ = { + "TOR", "SVR", "SVS", "FFW", "FFS"}; + class TextEventManager::Impl { public: @@ -82,10 +86,17 @@ public: void HandleMessage(const std::shared_ptr& message); + void LoadArchive(std::chrono::sys_days date, const std::string& pil); + void LoadArchives(std::chrono::sys_days date); void RefreshAsync(); void Refresh(); - boost::asio::thread_pool threadPool_ {1u}; + // Thread pool sized for: + // - Live Refresh (1x) + // - Archive Loading (15x) + // - 3 day window (3x) + // - TOR, SVR, SVS, FFW, FFS (5x) + boost::asio::thread_pool threadPool_ {16u}; TextEventManager* self_; @@ -98,6 +109,8 @@ public: textEventMap_; std::shared_mutex textEventMutex_; + std::unique_ptr iemApiProvider_ { + std::make_unique()}; std::shared_ptr warningsProvider_ {nullptr}; std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; std::chrono::sys_time prevLoadTime_ {}; @@ -171,6 +184,20 @@ void TextEventManager::LoadFile(const std::string& filename) }); } +void TextEventManager::SelectTime( + std::chrono::system_clock::time_point dateTime) +{ + const auto today = std::chrono::floor(dateTime); + const auto yesterday = today - std::chrono::days {1}; + const auto tomorrow = today + std::chrono::days {1}; + const auto dates = {yesterday, today, tomorrow}; + + for (auto& date : dates) + { + p->LoadArchives(date); + } +} + void TextEventManager::Impl::HandleMessage( const std::shared_ptr& message) { @@ -249,6 +276,32 @@ void TextEventManager::Impl::HandleMessage( } } +void TextEventManager::Impl::LoadArchive(std::chrono::sys_days date, + const std::string& pil) +{ + const auto& productIds = iemApiProvider_->ListTextProducts(date, {}, pil); + const auto& products = iemApiProvider_->LoadTextProducts(productIds); + + for (auto& product : products) + { + const auto& messages = product->messages(); + + for (auto& message : messages) + { + HandleMessage(message); + } + } +} + +void TextEventManager::Impl::LoadArchives(std::chrono::sys_days date) +{ + for (auto& pil : kPils_) + { + boost::asio::post(threadPool_, + [this, date, &pil]() { LoadArchive(date, pil); }); + } +} + void TextEventManager::Impl::RefreshAsync() { boost::asio::post(threadPool_, diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp index 30748781..312dece0 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -29,6 +30,7 @@ public: message_list(const types::TextEventKey& key) const; void LoadFile(const std::string& filename); + void SelectTime(std::chrono::system_clock::time_point dateTime); static std::shared_ptr Instance(); From 33cfd4bc0eedd69bd9cb3dbba0e073658209b8d1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 00:11:09 -0500 Subject: [PATCH 508/762] Don't reload archive text products that have already been loaded --- .../scwx/qt/manager/text_event_manager.cpp | 74 +++++++++++++++++-- .../scwx/provider/iem_api_provider.test.cpp | 11 +-- .../scwx/provider/iem_api_provider.hpp | 4 +- .../source/scwx/provider/iem_api_provider.cpp | 13 +++- 4 files changed, 87 insertions(+), 15 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index dffdb6bf..45cf24a6 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -5,7 +5,10 @@ #include #include #include +#include +#include +#include #include #include @@ -90,6 +93,7 @@ public: void LoadArchives(std::chrono::sys_days date); void RefreshAsync(); void Refresh(); + void UpdateArchiveDates(std::chrono::sys_days date); // Thread pool sized for: // - Live Refresh (1x) @@ -112,9 +116,18 @@ public: std::unique_ptr iemApiProvider_ { std::make_unique()}; std::shared_ptr warningsProvider_ {nullptr}; + std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; std::chrono::sys_time prevLoadTime_ {}; + std::mutex archiveMutex_ {}; + std::list archiveDates_ {}; + std::map< + std::chrono::sys_days, + std::unordered_map>>> + archiveMap_; + boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; }; @@ -187,10 +200,12 @@ void TextEventManager::LoadFile(const std::string& filename) void TextEventManager::SelectTime( std::chrono::system_clock::time_point dateTime) { + logger_->trace("Select Time: {}", util::TimeString(dateTime)); + const auto today = std::chrono::floor(dateTime); const auto yesterday = today - std::chrono::days {1}; const auto tomorrow = today + std::chrono::days {1}; - const auto dates = {yesterday, today, tomorrow}; + const auto dates = {today, yesterday, tomorrow}; for (auto& date : dates) { @@ -279,22 +294,56 @@ void TextEventManager::Impl::HandleMessage( void TextEventManager::Impl::LoadArchive(std::chrono::sys_days date, const std::string& pil) { - const auto& productIds = iemApiProvider_->ListTextProducts(date, {}, pil); - const auto& products = iemApiProvider_->LoadTextProducts(productIds); - - for (auto& product : products) + std::unique_lock lock {archiveMutex_}; + auto& dateArchive = archiveMap_[date]; + if (dateArchive.contains(pil)) { - const auto& messages = product->messages(); + // Don't reload data that has already been loaded + return; + } + lock.unlock(); - for (auto& message : messages) + logger_->debug("Load Archive: {}, {}", util::TimeString(date), pil); + + // Query for products + const auto& productIds = iemApiProvider_->ListTextProducts(date, {}, pil); + + if (productIds.has_value()) + { + logger_->debug("Loading {} {} products", productIds.value().size(), pil); + + // Load listed products + auto products = iemApiProvider_->LoadTextProducts(productIds.value()); + + for (auto& product : products) { - HandleMessage(message); + const auto& messages = product->messages(); + + for (auto& message : messages) + { + HandleMessage(message); + } } + + lock.lock(); + + // Ensure the archive map still contains the date, and has not been pruned + if (archiveMap_.contains(date)) + { + // Store the products associated with the PIL in the archive + dateArchive.try_emplace(pil, std::move(products)); + } + + lock.unlock(); } } void TextEventManager::Impl::LoadArchives(std::chrono::sys_days date) { + logger_->trace("Load Archives: {}", util::TimeString(date)); + + UpdateArchiveDates(date); + for (auto& pil : kPils_) { boost::asio::post(threadPool_, @@ -380,6 +429,15 @@ void TextEventManager::Impl::Refresh() }); } +void TextEventManager::Impl::UpdateArchiveDates(std::chrono::sys_days date) +{ + std::unique_lock lock {archiveMutex_}; + + // Remove any existing occurrences of day, and add to the back of the list + archiveDates_.remove(date); + archiveDates_.push_back(date); +} + std::shared_ptr TextEventManager::Instance() { static std::weak_ptr textEventManagerReference_ {}; diff --git a/test/source/scwx/provider/iem_api_provider.test.cpp b/test/source/scwx/provider/iem_api_provider.test.cpp index 68cb59ae..854f0d60 100644 --- a/test/source/scwx/provider/iem_api_provider.test.cpp +++ b/test/source/scwx/provider/iem_api_provider.test.cpp @@ -18,15 +18,16 @@ TEST(IemApiProviderTest, ListTextProducts) auto torProducts = provider.ListTextProducts(date, {}, "TOR"); - EXPECT_EQ(torProducts.size(), 35); + ASSERT_EQ(torProducts.has_value(), true); + EXPECT_EQ(torProducts.value().size(), 35); - if (torProducts.size() >= 1) + if (torProducts.value().size() >= 1) { - EXPECT_EQ(torProducts.at(0), "202303250016-KMEG-WFUS54-TORMEG"); + EXPECT_EQ(torProducts.value().at(0), "202303250016-KMEG-WFUS54-TORMEG"); } - if (torProducts.size() >= 35) + if (torProducts.value().size() >= 35) { - EXPECT_EQ(torProducts.at(34), "202303252015-KFFC-WFUS52-TORFFC"); + EXPECT_EQ(torProducts.value().at(34), "202303252015-KFFC-WFUS52-TORFFC"); } } diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index 33a82bcf..a0183cd4 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -5,6 +5,8 @@ #include #include +#include + namespace scwx::provider { @@ -23,7 +25,7 @@ public: IemApiProvider(IemApiProvider&&) noexcept; IemApiProvider& operator=(IemApiProvider&&) noexcept; - static std::vector + static boost::outcome_v2::result> ListTextProducts(std::chrono::sys_time date, std::optional cccc = {}, std::optional pil = {}); diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 817fbc2f..d5752fcb 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -35,7 +35,7 @@ IemApiProvider::~IemApiProvider() = default; IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = default; IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; -std::vector +boost::outcome_v2::result> IemApiProvider::ListTextProducts(std::chrono::sys_time date, std::optional cccc, std::optional pil) @@ -93,6 +93,8 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, { // Unexpected bad response logger_->warn("Error parsing JSON: {}", ex.what()); + return boost::system::errc::make_error_code( + boost::system::errc::bad_message); } } else if (response.status_code == cpr::status::HTTP_BAD_REQUEST && @@ -109,6 +111,9 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, // Unexpected bad response logger_->warn("Error parsing bad response: {}", ex.what()); } + + return boost::system::errc::make_error_code( + boost::system::errc::invalid_argument); } else if (response.status_code == cpr::status::HTTP_UNPROCESSABLE_ENTITY && json != nullptr) @@ -125,10 +130,16 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, // Unexpected bad response logger_->warn("Error parsing validation error: {}", ex.what()); } + + return boost::system::errc::make_error_code( + boost::system::errc::no_message_available); } else { logger_->warn("Could not list text products: {}", response.status_line); + + return boost::system::errc::make_error_code( + boost::system::errc::no_message); } return textProducts; From 02ec27dd2ff0995c290c0a5850b2ab9299434797 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 00:16:05 -0500 Subject: [PATCH 509/762] Ignore default date/time selections for archive warnings --- scwx-qt/source/scwx/qt/manager/text_event_manager.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 45cf24a6..b3d0fd3a 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -200,6 +200,12 @@ void TextEventManager::LoadFile(const std::string& filename) void TextEventManager::SelectTime( std::chrono::system_clock::time_point dateTime) { + if (dateTime == std::chrono::system_clock::time_point {}) + { + // Ignore a default date/time selection + return; + } + logger_->trace("Select Time: {}", util::TimeString(dateTime)); const auto today = std::chrono::floor(dateTime); From 53ade7fc53031777625ae6e3d39b8cf95a91acb1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 00:33:51 -0500 Subject: [PATCH 510/762] Don't load archived text products for days that have full coverage of live warning data --- .../source/scwx/qt/manager/text_event_manager.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index b3d0fd3a..bac8b0e7 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -119,6 +119,7 @@ public: std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; std::chrono::sys_time prevLoadTime_ {}; + std::chrono::sys_days archiveLimit_ {}; std::mutex archiveMutex_ {}; std::list archiveDates_ {}; @@ -215,7 +216,11 @@ void TextEventManager::SelectTime( for (auto& date : dates) { - p->LoadArchives(date); + if (p->archiveLimit_ == std::chrono::sys_days {} || + date < p->archiveLimit_) + { + p->LoadArchives(date); + } } } @@ -399,6 +404,11 @@ void TextEventManager::Impl::Refresh() startTime = std::min(startTime, prevLoadTime_); } + if (archiveLimit_ == std::chrono::sys_days {}) + { + archiveLimit_ = std::chrono::ceil(startTime); + } + auto updatedFiles = warningsProvider->LoadUpdatedFiles(startTime); // Store the load time and reset the load history duration From 05ff080d789ef717b0b2c6ffd167cc8110978214 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 15:18:36 -0500 Subject: [PATCH 511/762] Allow a 1 character bulletin ID in the WMO header --- wxdata/source/scwx/awips/wmo_header.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index ddaf06b2..d7da0818 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -19,7 +19,8 @@ static const std::string logPrefix_ = "scwx::awips::wmo_header"; static const auto logger_ = util::Logger::Create(logPrefix_); static constexpr std::size_t kWmoHeaderMinLineLength_ = 18; -static constexpr std::size_t kWmoIdentifierLength_ = 6; +static constexpr std::size_t kWmoIdentifierLengthMin_ = 5; +static constexpr std::size_t kWmoIdentifierLengthMax_ = 6; static constexpr std::size_t kIcaoLength_ = 4; static constexpr std::size_t kDateTimeLength_ = 6; static constexpr std::size_t kAwipsIdentifierLineLength_ = 6; @@ -271,7 +272,8 @@ bool WmoHeader::Parse(std::istream& is) logger_->warn("Invalid number of WMO tokens"); headerValid = false; } - else if (wmoTokenList[0].size() != kWmoIdentifierLength_) + else if (wmoTokenList[0].size() < kWmoIdentifierLengthMin_ || + wmoTokenList[0].size() > kWmoIdentifierLengthMax_) { logger_->warn("WMO identifier malformed"); headerValid = false; @@ -296,9 +298,9 @@ bool WmoHeader::Parse(std::istream& is) { p->dataType_ = wmoTokenList[0].substr(0, 2); p->geographicDesignator_ = wmoTokenList[0].substr(2, 2); - p->bulletinId_ = wmoTokenList[0].substr(4, 2); - p->icao_ = wmoTokenList[1]; - p->dateTime_ = wmoTokenList[2]; + p->bulletinId_ = wmoTokenList[0].substr(4, wmoTokenList[0].size() - 4); + p->icao_ = wmoTokenList[1]; + p->dateTime_ = wmoTokenList[2]; p->CalculateAbsoluteDateTime(); From b117d2088a8fd077439df221f99ca06bbe7e16ea Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 15:35:00 -0500 Subject: [PATCH 512/762] Add missing date includes to IEM API provider --- wxdata/source/scwx/provider/iem_api_provider.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index d5752fcb..f2704e54 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -7,6 +7,10 @@ #include #include +#if (__cpp_lib_chrono < 201907L) +# include +#endif + namespace scwx::provider { From 1bdfdcafad0380ae15a8f523193ce75fe4b991e2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 6 Apr 2025 23:05:18 -0500 Subject: [PATCH 513/762] Missing AWIPS Identifier Line in WMO header should not be treated as an error --- wxdata/source/scwx/awips/wmo_header.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index d7da0818..fad1231b 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -228,6 +228,7 @@ bool WmoHeader::Parse(std::istream& is) } } + auto awipsLinePos = is.tellg(); util::getline(is, awipsLine); if (is.eof()) @@ -322,8 +323,12 @@ bool WmoHeader::Parse(std::istream& is) { if (awipsLine.size() != kAwipsIdentifierLineLength_) { - logger_->warn("AWIPS Identifier Line bad size"); - headerValid = false; + // Older products may be missing an AWIPS Identifier Line + logger_->trace("AWIPS Identifier Line bad size"); + + is.seekg(awipsLinePos); + p->productCategory_ = ""; + p->productDesignator_ = ""; } else { From 33c73ef0e22333e42d7dcedae180c0348238c52d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Apr 2025 22:40:36 -0500 Subject: [PATCH 514/762] Add range-v3 dependency --- ACKNOWLEDGEMENTS.md | 1 + conanfile.py | 1 + wxdata/wxdata.cmake | 2 ++ 3 files changed, 4 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 4aec61d2..6f7cd4ef 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -36,6 +36,7 @@ Supercell Wx uses code from the following dependencies: | [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 Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | | [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | +| [range-v3](https://github.com/ericniebler/range-v3) | [Boost Software License 1.0](https://spdx.org/licenses/BSL-1.0.html)
[MIT License](https://spdx.org/licenses/MIT.html)
[Stepanov and McJones, "Elements of Programming" license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file)
[SGI C++ Standard Template Library license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file) | | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | [SQLite](https://www.sqlite.org/) | Public Domain | diff --git a/conanfile.py b/conanfile.py index e68c9f39..ea9c6ab9 100644 --- a/conanfile.py +++ b/conanfile.py @@ -18,6 +18,7 @@ class SupercellWxConan(ConanFile): "libpng/1.6.47", "libxml2/2.13.6", "openssl/3.4.1", + "range-v3/0.12.0", "re2/20240702", "spdlog/1.15.1", "sqlite3/3.49.1", diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 468f124b..32db05ab 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -6,6 +6,7 @@ find_package(Boost) find_package(cpr) find_package(LibXml2) find_package(OpenSSL) +find_package(range-v3) find_package(re2) find_package(spdlog) @@ -303,6 +304,7 @@ target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core cpr::cpr LibXml2::LibXml2 OpenSSL::Crypto + range-v3::range-v3 re2::re2 spdlog::spdlog units::units) From 7a8a0302e0407a3c673f5efcfbe2db9521779b2d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Apr 2025 22:41:23 -0500 Subject: [PATCH 515/762] Provide interface to request multiple text product lists in parallel --- .../scwx/provider/iem_api_provider.hpp | 5 + .../source/scwx/provider/iem_api_provider.cpp | 181 +++++++++++------- 2 files changed, 118 insertions(+), 68 deletions(-) diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index a0183cd4..7b568d83 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -29,6 +30,10 @@ public: ListTextProducts(std::chrono::sys_time date, std::optional cccc = {}, std::optional pil = {}); + static boost::outcome_v2::result> + ListTextProducts(std::vector> dates, + std::vector ccccs = {}, + std::vector pils = {}); static std::vector> LoadTextProducts(const std::vector& textProducts); diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index f2704e54..00fff608 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -6,6 +6,7 @@ #include #include +#include #if (__cpp_lib_chrono < 201907L) # include @@ -41,8 +42,25 @@ IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; boost::outcome_v2::result> IemApiProvider::ListTextProducts(std::chrono::sys_time date, - std::optional cccc, - std::optional pil) + std::optional optionalCccc, + std::optional optionalPil) +{ + std::string_view cccc = + optionalCccc.has_value() ? optionalCccc.value() : std::string_view {}; + std::string_view pil = + optionalPil.has_value() ? optionalPil.value() : std::string_view {}; + + return ListTextProducts( + std::vector> {date}, + {cccc}, + {pil}); +} + +boost::outcome_v2::result> +IemApiProvider::ListTextProducts( + std::vector> dates, + std::vector ccccs, + std::vector pils) { using namespace std::chrono; @@ -57,93 +75,120 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, # define kDateFormat "%Y-%m-%d" #endif - auto parameters = cpr::Parameters {{"date", df::format(kDateFormat, date)}}; - - // WMO Source Code - if (cccc.has_value()) + if (ccccs.empty()) { - parameters.Add({"cccc", std::string {cccc.value()}}); + ccccs.push_back({}); } - // AFOS / AWIPS ID / 3-6 length identifier - if (pil.has_value()) + if (pils.empty()) { - parameters.Add({"pil", std::string {pil.value()}}); + pils.push_back({}); } - auto response = - cpr::Get(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, - network::cpr::GetHeader(), - parameters); - boost::json::value json = util::json::ReadJsonString(response.text); + std::vector responses {}; + + for (const auto& [date, cccc, pil] : + ranges::views::cartesian_product(dates, ccccs, pils)) + { + auto parameters = + cpr::Parameters {{"date", df::format(kDateFormat, date)}}; + + // WMO Source Code + if (!cccc.empty()) + { + parameters.Add({"cccc", std::string {cccc}}); + } + + // AFOS / AWIPS ID / 3-6 length identifier + if (!pil.empty()) + { + parameters.Add({"pil", std::string {pil}}); + } + + responses.emplace_back( + cpr::GetAsync(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, + network::cpr::GetHeader(), + parameters)); + } std::vector textProducts {}; - if (response.status_code == cpr::status::HTTP_OK) + for (auto& asyncResponse : responses) { - try - { - // Get AFOS list from response - auto entries = boost::json::value_to(json); + auto response = asyncResponse.get(); - for (auto& entry : entries.data_) + boost::json::value json = util::json::ReadJsonString(response.text); + + if (response.status_code == cpr::status::HTTP_OK) + { + try { - textProducts.push_back(entry.productId_); + // Get AFOS list from response + auto entries = boost::json::value_to(json); + + for (auto& entry : entries.data_) + { + textProducts.push_back(entry.productId_); + } + + logger_->trace("Found {} products", entries.data_.size()); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing JSON: {}", ex.what()); + return boost::system::errc::make_error_code( + boost::system::errc::bad_message); + } + } + else if (response.status_code == cpr::status::HTTP_BAD_REQUEST && + json != nullptr) + { + try + { + // Log bad request details + auto badRequest = + boost::json::value_to(json); + logger_->warn("ListTextProducts bad request: {}", + badRequest.detail_); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing bad response: {}", ex.what()); } - logger_->trace("Found {} products", entries.data_.size()); - } - catch (const std::exception& ex) - { - // Unexpected bad response - logger_->warn("Error parsing JSON: {}", ex.what()); return boost::system::errc::make_error_code( - boost::system::errc::bad_message); + boost::system::errc::invalid_argument); } - } - else if (response.status_code == cpr::status::HTTP_BAD_REQUEST && - json != nullptr) - { - try + else if (response.status_code == cpr::status::HTTP_UNPROCESSABLE_ENTITY && + json != nullptr) { - // Log bad request details - auto badRequest = boost::json::value_to(json); - logger_->warn("ListTextProducts bad request: {}", badRequest.detail_); - } - catch (const std::exception& ex) - { - // Unexpected bad response - logger_->warn("Error parsing bad response: {}", ex.what()); - } + try + { + // Log validation error details + auto error = + boost::json::value_to(json); + logger_->warn("ListTextProducts validation error: {}", + error.detail_.at(0).msg_); + } + catch (const std::exception& ex) + { + // Unexpected bad response + logger_->warn("Error parsing validation error: {}", ex.what()); + } - return boost::system::errc::make_error_code( - boost::system::errc::invalid_argument); - } - else if (response.status_code == cpr::status::HTTP_UNPROCESSABLE_ENTITY && - json != nullptr) - { - try - { - // Log validation error details - auto error = boost::json::value_to(json); - logger_->warn("ListTextProducts validation error: {}", - error.detail_.at(0).msg_); + return boost::system::errc::make_error_code( + boost::system::errc::no_message_available); } - catch (const std::exception& ex) + else { - // Unexpected bad response - logger_->warn("Error parsing validation error: {}", ex.what()); + logger_->warn("Could not list text products: {}", + response.status_line); + + return boost::system::errc::make_error_code( + boost::system::errc::no_message); } - - return boost::system::errc::make_error_code( - boost::system::errc::no_message_available); - } - else - { - logger_->warn("Could not list text products: {}", response.status_line); - - return boost::system::errc::make_error_code( - boost::system::errc::no_message); } return textProducts; From e82fa93fb0e2ab1fcb72ad66785db8eae899ff30 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Apr 2025 01:20:52 -0500 Subject: [PATCH 516/762] Use ranges instead of vectors for listing text products --- .../scwx/provider/iem_api_provider.hpp | 20 +++++++++--- .../source/scwx/provider/iem_api_provider.cpp | 31 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index 7b568d83..316890df 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -4,10 +4,20 @@ #include #include -#include #include +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4702) +#endif + +#include + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif + namespace scwx::provider { @@ -30,10 +40,10 @@ public: ListTextProducts(std::chrono::sys_time date, std::optional cccc = {}, std::optional pil = {}); - static boost::outcome_v2::result> - ListTextProducts(std::vector> dates, - std::vector ccccs = {}, - std::vector pils = {}); + static boost::outcome_v2::result> ListTextProducts( + ranges::any_view> dates, + ranges::any_view ccccs = {}, + ranges::any_view pils = {}); static std::vector> LoadTextProducts(const std::vector& textProducts); diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 00fff608..b72b3b18 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -6,7 +6,9 @@ #include #include +#include #include +#include #if (__cpp_lib_chrono < 201907L) # include @@ -50,17 +52,18 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, std::string_view pil = optionalPil.has_value() ? optionalPil.value() : std::string_view {}; - return ListTextProducts( - std::vector> {date}, - {cccc}, - {pil}); + const auto dateArray = std::array {date}; + const auto ccccArray = std::array {cccc}; + const auto pilArray = std::array {pil}; + + return ListTextProducts(dateArray, ccccArray, pilArray); } boost::outcome_v2::result> IemApiProvider::ListTextProducts( - std::vector> dates, - std::vector ccccs, - std::vector pils) + ranges::any_view> dates, + ranges::any_view ccccs, + ranges::any_view pils) { using namespace std::chrono; @@ -75,20 +78,24 @@ IemApiProvider::ListTextProducts( # define kDateFormat "%Y-%m-%d" #endif - if (ccccs.empty()) + if (ccccs.begin() == ccccs.end()) { - ccccs.push_back({}); + ccccs = ranges::views::single(std::string_view {}); } - if (pils.empty()) + if (pils.begin() == pils.end()) { - pils.push_back({}); + pils = ranges::views::single(std::string_view {}); } + const auto dv = ranges::to(dates); + const auto cv = ranges::to(ccccs); + const auto pv = ranges::to(pils); + std::vector responses {}; for (const auto& [date, cccc, pil] : - ranges::views::cartesian_product(dates, ccccs, pils)) + ranges::views::cartesian_product(dv, cv, pv)) { auto parameters = cpr::Parameters {{"date", df::format(kDateFormat, date)}}; From e3ccce5d5bbbe522993d65684fc27dd95ac220c9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Apr 2025 01:27:47 -0500 Subject: [PATCH 517/762] Text event manager should use filtered ranges to request archived products --- .../scwx/qt/manager/text_event_manager.cpp | 99 +++++++++---------- 1 file changed, 45 insertions(+), 54 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index bac8b0e7..6574c1cb 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include namespace scwx { @@ -89,18 +91,15 @@ public: void HandleMessage(const std::shared_ptr& message); - void LoadArchive(std::chrono::sys_days date, const std::string& pil); - void LoadArchives(std::chrono::sys_days date); + void LoadArchives(ranges::any_view dates); void RefreshAsync(); void Refresh(); - void UpdateArchiveDates(std::chrono::sys_days date); + void UpdateArchiveDates(ranges::any_view dates); // Thread pool sized for: // - Live Refresh (1x) - // - Archive Loading (15x) - // - 3 day window (3x) - // - TOR, SVR, SVS, FFW, FFS (5x) - boost::asio::thread_pool threadPool_ {16u}; + // - Archive Loading (1x) + boost::asio::thread_pool threadPool_ {2u}; TextEventManager* self_; @@ -123,10 +122,8 @@ public: std::mutex archiveMutex_ {}; std::list archiveDates_ {}; - std::map< - std::chrono::sys_days, - std::unordered_map>>> + std::map>> archiveMap_; boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; @@ -212,16 +209,17 @@ void TextEventManager::SelectTime( const auto today = std::chrono::floor(dateTime); const auto yesterday = today - std::chrono::days {1}; const auto tomorrow = today + std::chrono::days {1}; - const auto dates = {today, yesterday, tomorrow}; + const auto dateArray = std::array {today, yesterday, tomorrow}; - for (auto& date : dates) - { - if (p->archiveLimit_ == std::chrono::sys_days {} || - date < p->archiveLimit_) - { - p->LoadArchives(date); - } - } + const ranges::any_view dates = + dateArray | ranges::views::filter( + [this](const auto& date) + { + return p->archiveLimit_ == std::chrono::sys_days {} || + date < p->archiveLimit_; + }); + + p->LoadArchives(dates); } void TextEventManager::Impl::HandleMessage( @@ -283,7 +281,8 @@ void TextEventManager::Impl::HandleMessage( it->second.end(), message, [](const std::shared_ptr& a, - const std::shared_ptr& b) { + const std::shared_ptr& b) + { return a->wmo_header()->GetDateTime() < b->wmo_header()->GetDateTime(); }); @@ -302,26 +301,27 @@ void TextEventManager::Impl::HandleMessage( } } -void TextEventManager::Impl::LoadArchive(std::chrono::sys_days date, - const std::string& pil) +void TextEventManager::Impl::LoadArchives( + ranges::any_view dates) { + UpdateArchiveDates(dates); + std::unique_lock lock {archiveMutex_}; - auto& dateArchive = archiveMap_[date]; - if (dateArchive.contains(pil)) - { - // Don't reload data that has already been loaded - return; - } + + // Don't reload data that has already been loaded + const ranges::any_view filteredDates = + dates | ranges::views::filter([this](const auto& date) + { return !archiveMap_.contains(date); }); + lock.unlock(); - logger_->debug("Load Archive: {}, {}", util::TimeString(date), pil); - // Query for products - const auto& productIds = iemApiProvider_->ListTextProducts(date, {}, pil); + const auto& productIds = + iemApiProvider_->ListTextProducts(filteredDates, {}, kPils_); if (productIds.has_value()) { - logger_->debug("Loading {} {} products", productIds.value().size(), pil); + logger_->debug("Loading {} products", productIds.value().size()); // Load listed products auto products = iemApiProvider_->LoadTextProducts(productIds.value()); @@ -338,30 +338,17 @@ void TextEventManager::Impl::LoadArchive(std::chrono::sys_days date, lock.lock(); - // Ensure the archive map still contains the date, and has not been pruned - if (archiveMap_.contains(date)) + for (const auto& date : dates) { - // Store the products associated with the PIL in the archive - dateArchive.try_emplace(pil, std::move(products)); + archiveMap_[date]; + + // TODO: Store the products in the archive } lock.unlock(); } } -void TextEventManager::Impl::LoadArchives(std::chrono::sys_days date) -{ - logger_->trace("Load Archives: {}", util::TimeString(date)); - - UpdateArchiveDates(date); - - for (auto& pil : kPils_) - { - boost::asio::post(threadPool_, - [this, date, &pil]() { LoadArchive(date, pil); }); - } -} - void TextEventManager::Impl::RefreshAsync() { boost::asio::post(threadPool_, @@ -445,13 +432,17 @@ void TextEventManager::Impl::Refresh() }); } -void TextEventManager::Impl::UpdateArchiveDates(std::chrono::sys_days date) +void TextEventManager::Impl::UpdateArchiveDates( + ranges::any_view dates) { std::unique_lock lock {archiveMutex_}; - // Remove any existing occurrences of day, and add to the back of the list - archiveDates_.remove(date); - archiveDates_.push_back(date); + for (const auto& date : dates) + { + // Remove any existing occurrences of day, and add to the back of the list + archiveDates_.remove(date); + archiveDates_.push_back(date); + } } std::shared_ptr TextEventManager::Instance() From 33e18765b7316af6beecf466acab726fde3a2464 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 15 Apr 2025 00:16:39 -0500 Subject: [PATCH 518/762] Start of implementation to load a window of archive warning data, currently broken --- .../scwx/qt/manager/text_event_manager.cpp | 162 ++++++++++++++++-- .../scwx/provider/iem_api_provider.test.cpp | 6 +- .../scwx/provider/iem_api_provider.hpp | 6 +- .../source/scwx/provider/iem_api_provider.cpp | 19 +- 4 files changed, 163 insertions(+), 30 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 6574c1cb..ef56496b 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -15,9 +16,16 @@ #include #include #include +#include +#include +#include #include #include +#if (__cpp_lib_chrono < 201907L) +# include +#endif + namespace scwx { namespace qt @@ -25,6 +33,8 @@ namespace qt namespace manager { +using namespace std::chrono_literals; + static const std::string logPrefix_ = "scwx::qt::manager::text_event_manager"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); @@ -33,8 +43,23 @@ static constexpr std::chrono::hours kInitialLoadHistoryDuration_ = static constexpr std::chrono::hours kDefaultLoadHistoryDuration_ = std::chrono::hours {1}; -static const std::array kPils_ = { - "TOR", "SVR", "SVS", "FFW", "FFS"}; +static const std::array kPils_ = { + "FFS", "FFW", "MWS", "SMW", "SQW", "SVR", "SVS", "TOR"}; + +static const std:: + unordered_map> + kPilLoadWindows_ {{"FFS", {-24h, 1h}}, + {"FFW", {-24h, 1h}}, + {"MWS", {-4h, 1h}}, + {"SMW", {-4h, 1h}}, + {"SQW", {-4h, 1h}}, + {"SVR", {-4h, 1h}}, + {"SVS", {-4h, 1h}}, + {"TOR", {-4h, 1h}}}; + +// Widest load window provided by kPilLoadWindows_ +static const std::pair + kArchiveLoadWindow_ {-24h, 1h}; class TextEventManager::Impl { @@ -91,7 +116,8 @@ public: void HandleMessage(const std::shared_ptr& message); - void LoadArchives(ranges::any_view dates); + void ListArchives(ranges::any_view dates); + void LoadArchives(std::chrono::system_clock::time_point dateTime); void RefreshAsync(); void Refresh(); void UpdateArchiveDates(ranges::any_view dates); @@ -125,6 +151,9 @@ public: std::map>> archiveMap_; + std::map> + unloadedProductMap_; boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; }; @@ -219,7 +248,8 @@ void TextEventManager::SelectTime( date < p->archiveLimit_; }); - p->LoadArchives(dates); + p->ListArchives(dates); + p->LoadArchives(dateTime); } void TextEventManager::Impl::HandleMessage( @@ -301,23 +331,127 @@ void TextEventManager::Impl::HandleMessage( } } -void TextEventManager::Impl::LoadArchives( +void TextEventManager::Impl::ListArchives( ranges::any_view dates) { - UpdateArchiveDates(dates); - std::unique_lock lock {archiveMutex_}; + UpdateArchiveDates(dates); + // Don't reload data that has already been loaded - const ranges::any_view filteredDates = - dates | ranges::views::filter([this](const auto& date) - { return !archiveMap_.contains(date); }); + ranges::any_view filteredDates = + dates | + ranges::views::filter([this](const auto& date) + { return !unloadedProductMap_.contains(date); }); lock.unlock(); - // Query for products - const auto& productIds = - iemApiProvider_->ListTextProducts(filteredDates, {}, kPils_); + const auto dv = ranges::to(filteredDates); + + std::for_each( + std::execution::par, + dv.begin(), + dv.end(), + [this](const auto& date) + { + const auto dateArray = std::array {date}; + + auto productEntries = + iemApiProvider_->ListTextProducts(dateArray, {}, kPils_); + + std::unique_lock lock {archiveMutex_}; + + if (productEntries.has_value()) + { + unloadedProductMap_.try_emplace( + date, + {std::make_move_iterator(productEntries.value().begin()), + std::make_move_iterator(productEntries.value().end())}); + } + }); +} + +void TextEventManager::Impl::LoadArchives( + std::chrono::system_clock::time_point dateTime) +{ + using namespace std::chrono; + +#if (__cpp_lib_chrono >= 201907L) + namespace df = std; + + static constexpr std::string_view kDateFormat {"{:%Y%m%d%H%M}"}; +#else + using namespace date; + namespace df = date; + +# define kDateFormat "%Y%m%d%H%M" +#endif + + // Search unloaded products in the widest archive load window + const std::chrono::sys_days startDate = + std::chrono::floor(dateTime + + kArchiveLoadWindow_.first); + const std::chrono::sys_days endDate = std::chrono::floor( + dateTime + kArchiveLoadWindow_.second + std::chrono::days {1}); + + // Determine load windows for each PIL + std::unordered_map> + pilLoadWindowStrings; + + for (auto& loadWindow : kPilLoadWindows_) + { + const std::string& pil = loadWindow.first; + + pilLoadWindowStrings.insert_or_assign( + pil, + std::pair { + df::format(kDateFormat, (dateTime + loadWindow.second.first)), + df::format(kDateFormat, (dateTime + loadWindow.second.second))}); + } + + std::vector loadList {}; + + std::unique_lock lock {archiveMutex_}; + + for (auto date : boost::irange(startDate, endDate)) + { + auto mapIt = unloadedProductMap_.find(date); + if (mapIt == unloadedProductMap_.cend()) + { + continue; + } + + for (auto it = mapIt->second.begin(); it != mapIt->second.end();) + { + const auto& pil = it->pil_; + + // Check PIL + if (pil.size() >= 3) + { + auto pilPrefix = pil.substr(0, 3); + auto windowIt = pilLoadWindowStrings.find(pilPrefix); + + // Check Window + if (windowIt != pilLoadWindowStrings.cend()) + { + const auto& productId = it->productId_; + const auto& windowStart = windowIt->second.first; + const auto& windowEnd = windowIt->second.second; + + if (windowStart <= productId && productId <= windowEnd) + { + // Product matches, move it to the load list + loadList.emplace_back(std::move(*it)); + it = mapIt->second.erase(it); + continue; + } + } + } + + // Current iterator was not matched + ++it; + } + } if (productIds.has_value()) { @@ -435,8 +569,6 @@ void TextEventManager::Impl::Refresh() void TextEventManager::Impl::UpdateArchiveDates( ranges::any_view dates) { - std::unique_lock lock {archiveMutex_}; - for (const auto& date : dates) { // Remove any existing occurrences of day, and add to the back of the list diff --git a/test/source/scwx/provider/iem_api_provider.test.cpp b/test/source/scwx/provider/iem_api_provider.test.cpp index 854f0d60..4f964b81 100644 --- a/test/source/scwx/provider/iem_api_provider.test.cpp +++ b/test/source/scwx/provider/iem_api_provider.test.cpp @@ -23,11 +23,13 @@ TEST(IemApiProviderTest, ListTextProducts) if (torProducts.value().size() >= 1) { - EXPECT_EQ(torProducts.value().at(0), "202303250016-KMEG-WFUS54-TORMEG"); + EXPECT_EQ(torProducts.value().at(0).productId_, + "202303250016-KMEG-WFUS54-TORMEG"); } if (torProducts.value().size() >= 35) { - EXPECT_EQ(torProducts.value().at(34), "202303252015-KFFC-WFUS52-TORFFC"); + EXPECT_EQ(torProducts.value().at(34).productId_, + "202303252015-KFFC-WFUS52-TORFFC"); } } diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index 316890df..b862f73d 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -36,11 +37,12 @@ public: IemApiProvider(IemApiProvider&&) noexcept; IemApiProvider& operator=(IemApiProvider&&) noexcept; - static boost::outcome_v2::result> + static boost::outcome_v2::result> ListTextProducts(std::chrono::sys_time date, std::optional cccc = {}, std::optional pil = {}); - static boost::outcome_v2::result> ListTextProducts( + static boost::outcome_v2::result> + ListTextProducts( ranges::any_view> dates, ranges::any_view ccccs = {}, ranges::any_view pils = {}); diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index b72b3b18..bab18864 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include @@ -42,7 +41,7 @@ IemApiProvider::~IemApiProvider() = default; IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = default; IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; -boost::outcome_v2::result> +boost::outcome_v2::result> IemApiProvider::ListTextProducts(std::chrono::sys_time date, std::optional optionalCccc, std::optional optionalPil) @@ -59,7 +58,7 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, return ListTextProducts(dateArray, ccccArray, pilArray); } -boost::outcome_v2::result> +boost::outcome_v2::result> IemApiProvider::ListTextProducts( ranges::any_view> dates, ranges::any_view ccccs, @@ -118,7 +117,7 @@ IemApiProvider::ListTextProducts( parameters)); } - std::vector textProducts {}; + std::vector textProducts {}; for (auto& asyncResponse : responses) { @@ -132,13 +131,9 @@ IemApiProvider::ListTextProducts( { // Get AFOS list from response auto entries = boost::json::value_to(json); - - for (auto& entry : entries.data_) - { - textProducts.push_back(entry.productId_); - } - - logger_->trace("Found {} products", entries.data_.size()); + textProducts.insert(textProducts.end(), + std::make_move_iterator(entries.data_.begin()), + std::make_move_iterator(entries.data_.end())); } catch (const std::exception& ex) { @@ -198,6 +193,8 @@ IemApiProvider::ListTextProducts( } } + logger_->trace("Found {} products", textProducts.size()); + return textProducts; } From 1a1c668d62b857e82d67b44229920d1ee3938f2a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 16 Apr 2025 23:29:40 -0500 Subject: [PATCH 519/762] Finish windowed load. Not all polygon updates are shown on the map. --- .../scwx/qt/manager/text_event_manager.cpp | 62 +++++++------------ .../scwx/provider/iem_api_provider.hpp | 35 ++++++++++- .../source/scwx/provider/iem_api_provider.cpp | 30 +++------ 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index ef56496b..153696ff 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -148,9 +149,8 @@ public: std::mutex archiveMutex_ {}; std::list archiveDates_ {}; - std::map>> - archiveMap_; + + std::mutex unloadedProductMapMutex_ {}; std::map> unloadedProductMap_; @@ -248,6 +248,9 @@ void TextEventManager::SelectTime( date < p->archiveLimit_; }); + std::unique_lock lock {p->archiveMutex_}; + + p->UpdateArchiveDates(dates); p->ListArchives(dates); p->LoadArchives(dateTime); } @@ -334,18 +337,12 @@ void TextEventManager::Impl::HandleMessage( void TextEventManager::Impl::ListArchives( ranges::any_view dates) { - std::unique_lock lock {archiveMutex_}; - - UpdateArchiveDates(dates); - // Don't reload data that has already been loaded ranges::any_view filteredDates = dates | ranges::views::filter([this](const auto& date) { return !unloadedProductMap_.contains(date); }); - lock.unlock(); - const auto dv = ranges::to(filteredDates); std::for_each( @@ -359,14 +356,15 @@ void TextEventManager::Impl::ListArchives( auto productEntries = iemApiProvider_->ListTextProducts(dateArray, {}, kPils_); - std::unique_lock lock {archiveMutex_}; + std::unique_lock lock {unloadedProductMapMutex_}; if (productEntries.has_value()) { unloadedProductMap_.try_emplace( date, - {std::make_move_iterator(productEntries.value().begin()), - std::make_move_iterator(productEntries.value().end())}); + boost::container::stable_vector { + std::make_move_iterator(productEntries.value().begin()), + std::make_move_iterator(productEntries.value().end())}); } }); } @@ -409,9 +407,7 @@ void TextEventManager::Impl::LoadArchives( df::format(kDateFormat, (dateTime + loadWindow.second.second))}); } - std::vector loadList {}; - - std::unique_lock lock {archiveMutex_}; + std::vector loadListEntries {}; for (auto date : boost::irange(startDate, endDate)) { @@ -441,7 +437,7 @@ void TextEventManager::Impl::LoadArchives( if (windowStart <= productId && productId <= windowEnd) { // Product matches, move it to the load list - loadList.emplace_back(std::move(*it)); + loadListEntries.emplace_back(std::move(*it)); it = mapIt->second.erase(it); continue; } @@ -453,33 +449,21 @@ void TextEventManager::Impl::LoadArchives( } } - if (productIds.has_value()) + // Load the load list + auto loadView = loadListEntries | + std::ranges::views::transform([](const auto& entry) + { return entry.productId_; }); + auto products = iemApiProvider_->LoadTextProducts(loadView); + + // Process loaded products + for (auto& product : products) { - logger_->debug("Loading {} products", productIds.value().size()); + const auto& messages = product->messages(); - // Load listed products - auto products = iemApiProvider_->LoadTextProducts(productIds.value()); - - for (auto& product : products) + for (auto& message : messages) { - const auto& messages = product->messages(); - - for (auto& message : messages) - { - HandleMessage(message); - } + HandleMessage(message); } - - lock.lock(); - - for (const auto& date : dates) - { - archiveMap_[date]; - - // TODO: Store the products in the archive - } - - lock.unlock(); } } diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index b862f73d..7d697919 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -1,12 +1,16 @@ #pragma once #include +#include #include #include +#include #include +#include #include +#include #if defined(_MSC_VER) # pragma warning(push) @@ -47,12 +51,41 @@ public: ranges::any_view ccccs = {}, ranges::any_view pils = {}); + template + requires std::same_as, std::string> static std::vector> - LoadTextProducts(const std::vector& textProducts); + LoadTextProducts(const Range& textProducts) + { + auto parameters = cpr::Parameters {{"nolimit", "true"}}; + + std::vector> asyncResponses {}; + asyncResponses.reserve(textProducts.size()); + + const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; + + for (const auto& productId : textProducts) + { + asyncResponses.emplace_back( + productId, + cpr::GetAsync(cpr::Url {endpointUrl + productId}, + network::cpr::GetHeader(), + parameters)); + } + + return ProcessTextProductResponses(asyncResponses); + } private: class Impl; std::unique_ptr p; + + static const std::string kBaseUrl_; + static const std::string kListNwsTextProductsEndpoint_; + static const std::string kNwsTextProductEndpoint_; + + static std::vector> + ProcessTextProductResponses( + std::vector>& asyncResponses); }; } // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index bab18864..7e3ff2c4 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -1,10 +1,10 @@ #include -#include #include #include #include #include +#include #include #include #include @@ -19,10 +19,12 @@ namespace scwx::provider static const std::string logPrefix_ = "scwx::provider::iem_api_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); -static const std::string kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; +const std::string IemApiProvider::kBaseUrl_ = + "https://mesonet.agron.iastate.edu/api/1"; -static const std::string kListNwsTextProductsEndpoint_ = "/nws/afos/list.json"; -static const std::string kNwsTextProductEndpoint_ = "/nwstext/"; +const std::string IemApiProvider::kListNwsTextProductsEndpoint_ = + "/nws/afos/list.json"; +const std::string IemApiProvider::kNwsTextProductEndpoint_ = "/nwstext/"; class IemApiProvider::Impl { @@ -199,25 +201,9 @@ IemApiProvider::ListTextProducts( } std::vector> -IemApiProvider::LoadTextProducts(const std::vector& textProducts) +IemApiProvider::ProcessTextProductResponses( + std::vector>& asyncResponses) { - auto parameters = cpr::Parameters {{"nolimit", "true"}}; - - std::vector> - asyncResponses {}; - asyncResponses.reserve(textProducts.size()); - - const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; - - for (auto& productId : textProducts) - { - asyncResponses.emplace_back( - productId, - cpr::GetAsync(cpr::Url {endpointUrl + productId}, - network::cpr::GetHeader(), - parameters)); - } - std::vector> textProductFiles; for (auto& asyncResponse : asyncResponses) From 65e3a667500e7073aeb4bf09bef33015a545173d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 25 Apr 2025 00:15:05 -0500 Subject: [PATCH 520/762] Update IemApiProvider to use template functions --- .../scwx/qt/manager/text_event_manager.cpp | 49 ++++++---- .../scwx/provider/iem_api_provider.hpp | 64 +++++-------- .../scwx/provider/iem_api_provider.ipp | 95 +++++++++++++++++++ .../source/scwx/provider/iem_api_provider.cpp | 65 +------------ wxdata/wxdata.cmake | 1 + 5 files changed, 159 insertions(+), 115 deletions(-) create mode 100644 wxdata/include/scwx/provider/iem_api_provider.ipp diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 153696ff..95d86354 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -20,8 +19,9 @@ #include #include #include -#include #include +#include +#include #if (__cpp_lib_chrono < 201907L) # include @@ -117,11 +117,17 @@ public: void HandleMessage(const std::shared_ptr& message); - void ListArchives(ranges::any_view dates); + template + requires std::same_as, + std::chrono::sys_days> + void ListArchives(DateRange dates); void LoadArchives(std::chrono::system_clock::time_point dateTime); void RefreshAsync(); void Refresh(); - void UpdateArchiveDates(ranges::any_view dates); + template + requires std::same_as, + std::chrono::sys_days> + void UpdateArchiveDates(DateRange dates); // Thread pool sized for: // - Live Refresh (1x) @@ -139,8 +145,6 @@ public: textEventMap_; std::shared_mutex textEventMutex_; - std::unique_ptr iemApiProvider_ { - std::make_unique()}; std::shared_ptr warningsProvider_ {nullptr}; std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; @@ -240,7 +244,7 @@ void TextEventManager::SelectTime( const auto tomorrow = today + std::chrono::days {1}; const auto dateArray = std::array {today, yesterday, tomorrow}; - const ranges::any_view dates = + const auto dates = dateArray | ranges::views::filter( [this](const auto& date) { @@ -334,11 +338,13 @@ void TextEventManager::Impl::HandleMessage( } } -void TextEventManager::Impl::ListArchives( - ranges::any_view dates) +template + requires std::same_as, + std::chrono::sys_days> +void TextEventManager::Impl::ListArchives(DateRange dates) { // Don't reload data that has already been loaded - ranges::any_view filteredDates = + auto filteredDates = dates | ranges::views::filter([this](const auto& date) { return !unloadedProductMap_.contains(date); }); @@ -351,10 +357,17 @@ void TextEventManager::Impl::ListArchives( dv.end(), [this](const auto& date) { + static const auto kEmptyRange_ = + ranges::views::single(std::string_view {}); + static const auto kPilsView_ = + kPils_ | + ranges::views::transform([](const std::string& pil) + { return std::string_view {pil}; }); + const auto dateArray = std::array {date}; - auto productEntries = - iemApiProvider_->ListTextProducts(dateArray, {}, kPils_); + auto productEntries = provider::IemApiProvider::ListTextProducts( + dateArray | ranges::views::all, kEmptyRange_, kPilsView_); std::unique_lock lock {unloadedProductMapMutex_}; @@ -450,10 +463,10 @@ void TextEventManager::Impl::LoadArchives( } // Load the load list - auto loadView = loadListEntries | - std::ranges::views::transform([](const auto& entry) + auto loadView = + loadListEntries | ranges::views::transform([](const auto& entry) { return entry.productId_; }); - auto products = iemApiProvider_->LoadTextProducts(loadView); + auto products = provider::IemApiProvider::LoadTextProducts(loadView); // Process loaded products for (auto& product : products) @@ -550,8 +563,10 @@ void TextEventManager::Impl::Refresh() }); } -void TextEventManager::Impl::UpdateArchiveDates( - ranges::any_view dates) +template + requires std::same_as, + std::chrono::sys_days> +void TextEventManager::Impl::UpdateArchiveDates(DateRange dates) { for (const auto& date : dates) { diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index 7d697919..3af9f545 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -1,24 +1,21 @@ #pragma once #include -#include #include #include -#include #include #include #include -#include +#include +#include #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4702) #endif -#include - #if defined(_MSC_VER) # pragma warning(pop) #endif @@ -42,50 +39,41 @@ public: IemApiProvider& operator=(IemApiProvider&&) noexcept; static boost::outcome_v2::result> - ListTextProducts(std::chrono::sys_time date, - std::optional cccc = {}, - std::optional pil = {}); + ListTextProducts(std::chrono::sys_days date, + std::optional cccc = {}, + std::optional pil = {}); + + template + requires std::same_as, + std::chrono::sys_days> && + std::same_as, + std::string_view> && + std::same_as, std::string_view> static boost::outcome_v2::result> - ListTextProducts( - ranges::any_view> dates, - ranges::any_view ccccs = {}, - ranges::any_view pils = {}); + ListTextProducts(DateRange dates, CcccRange ccccs, PilRange pils); - template - requires std::same_as, std::string> + template + requires std::same_as, std::string> static std::vector> - LoadTextProducts(const Range& textProducts) - { - auto parameters = cpr::Parameters {{"nolimit", "true"}}; - - std::vector> asyncResponses {}; - asyncResponses.reserve(textProducts.size()); - - const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; - - for (const auto& productId : textProducts) - { - asyncResponses.emplace_back( - productId, - cpr::GetAsync(cpr::Url {endpointUrl + productId}, - network::cpr::GetHeader(), - parameters)); - } - - return ProcessTextProductResponses(asyncResponses); - } + LoadTextProducts(const Range& textProducts); private: class Impl; std::unique_ptr p; + static boost::outcome_v2::result> + ProcessTextProductLists(std::vector& asyncResponses); + static std::vector> + ProcessTextProductFiles( + std::vector>& asyncResponses); + static const std::string kBaseUrl_; static const std::string kListNwsTextProductsEndpoint_; static const std::string kNwsTextProductEndpoint_; - - static std::vector> - ProcessTextProductResponses( - std::vector>& asyncResponses); }; } // namespace scwx::provider + +#include diff --git a/wxdata/include/scwx/provider/iem_api_provider.ipp b/wxdata/include/scwx/provider/iem_api_provider.ipp new file mode 100644 index 00000000..3fc94dad --- /dev/null +++ b/wxdata/include/scwx/provider/iem_api_provider.ipp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#if (__cpp_lib_chrono < 201907L) +# include +#endif + +namespace scwx::provider +{ + +template + requires std::same_as, + std::chrono::sys_days> && + std::same_as, std::string_view> && + std::same_as, std::string_view> +boost::outcome_v2::result> +IemApiProvider::ListTextProducts(DateRange dates, + CcccRange ccccs, + PilRange pils) +{ + using namespace std::chrono; + +#if (__cpp_lib_chrono >= 201907L) + namespace df = std; + + static constexpr std::string_view kDateFormat {"{:%Y-%m-%d}"}; +#else + using namespace date; + namespace df = date; + +# define kDateFormat "%Y-%m-%d" +#endif + + std::vector asyncResponses {}; + + for (const auto& [date, cccc, pil] : + ranges::views::cartesian_product(dates, ccccs, pils)) + { + auto parameters = + cpr::Parameters {{"date", df::format(kDateFormat, date)}}; + + // WMO Source Code + if (!cccc.empty()) + { + parameters.Add({"cccc", std::string {cccc}}); + } + + // AFOS / AWIPS ID / 3-6 length identifier + if (!pil.empty()) + { + parameters.Add({"pil", std::string {pil}}); + } + + asyncResponses.emplace_back( + cpr::GetAsync(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, + network::cpr::GetHeader(), + parameters)); + } + + return ProcessTextProductLists(asyncResponses); +} + +template + requires std::same_as, std::string> +std::vector> +IemApiProvider::LoadTextProducts(const Range& textProducts) +{ + auto parameters = cpr::Parameters {{"nolimit", "true"}}; + + std::vector> asyncResponses {}; + asyncResponses.reserve(textProducts.size()); + + const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; + + for (const auto& productId : textProducts) + { + asyncResponses.emplace_back( + productId, + cpr::GetAsync(cpr::Url {endpointUrl + productId}, + network::cpr::GetHeader(), + parameters)); + } + + return ProcessTextProductFiles(asyncResponses); +} + +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 7e3ff2c4..c4fb1ccd 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -44,7 +44,7 @@ IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept = default; IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; boost::outcome_v2::result> -IemApiProvider::ListTextProducts(std::chrono::sys_time date, +IemApiProvider::ListTextProducts(std::chrono::sys_days date, std::optional optionalCccc, std::optional optionalPil) { @@ -61,67 +61,12 @@ IemApiProvider::ListTextProducts(std::chrono::sys_time date, } boost::outcome_v2::result> -IemApiProvider::ListTextProducts( - ranges::any_view> dates, - ranges::any_view ccccs, - ranges::any_view pils) +IemApiProvider::ProcessTextProductLists( + std::vector& asyncResponses) { - using namespace std::chrono; - -#if (__cpp_lib_chrono >= 201907L) - namespace df = std; - - static constexpr std::string_view kDateFormat {"{:%Y-%m-%d}"}; -#else - using namespace date; - namespace df = date; - -# define kDateFormat "%Y-%m-%d" -#endif - - if (ccccs.begin() == ccccs.end()) - { - ccccs = ranges::views::single(std::string_view {}); - } - - if (pils.begin() == pils.end()) - { - pils = ranges::views::single(std::string_view {}); - } - - const auto dv = ranges::to(dates); - const auto cv = ranges::to(ccccs); - const auto pv = ranges::to(pils); - - std::vector responses {}; - - for (const auto& [date, cccc, pil] : - ranges::views::cartesian_product(dv, cv, pv)) - { - auto parameters = - cpr::Parameters {{"date", df::format(kDateFormat, date)}}; - - // WMO Source Code - if (!cccc.empty()) - { - parameters.Add({"cccc", std::string {cccc}}); - } - - // AFOS / AWIPS ID / 3-6 length identifier - if (!pil.empty()) - { - parameters.Add({"pil", std::string {pil}}); - } - - responses.emplace_back( - cpr::GetAsync(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, - network::cpr::GetHeader(), - parameters)); - } - std::vector textProducts {}; - for (auto& asyncResponse : responses) + for (auto& asyncResponse : asyncResponses) { auto response = asyncResponse.get(); @@ -201,7 +146,7 @@ IemApiProvider::ListTextProducts( } std::vector> -IemApiProvider::ProcessTextProductResponses( +IemApiProvider::ProcessTextProductFiles( std::vector>& asyncResponses) { std::vector> textProductFiles; diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 32db05ab..8d2e15b4 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -63,6 +63,7 @@ set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp include/scwx/provider/iem_api_provider.hpp + include/scwx/provider/iem_api_provider.ipp include/scwx/provider/nexrad_data_provider.hpp include/scwx/provider/nexrad_data_provider_factory.hpp include/scwx/provider/warnings_provider.hpp) From 8dde98d2a9117410784c55f19f09b2e1fd08a483 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 26 Apr 2025 09:47:50 -0500 Subject: [PATCH 521/762] Add debug log statements to archive warning loading --- .../source/scwx/qt/manager/text_event_manager.cpp | 13 +++++++++---- wxdata/include/scwx/provider/iem_api_provider.hpp | 3 +++ wxdata/include/scwx/provider/iem_api_provider.ipp | 12 ++++++++++++ wxdata/source/scwx/provider/iem_api_provider.cpp | 7 +++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 95d86354..72c7531f 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -462,11 +462,16 @@ void TextEventManager::Impl::LoadArchives( } } + std::vector> products {}; + // Load the load list - auto loadView = - loadListEntries | ranges::views::transform([](const auto& entry) - { return entry.productId_; }); - auto products = provider::IemApiProvider::LoadTextProducts(loadView); + if (!loadListEntries.empty()) + { + auto loadView = loadListEntries | + ranges::views::transform([](const auto& entry) + { return entry.productId_; }); + products = provider::IemApiProvider::LoadTextProducts(loadView); + } // Process loaded products for (auto& product : products) diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index 3af9f545..a3a65614 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -69,6 +70,8 @@ private: ProcessTextProductFiles( std::vector>& asyncResponses); + static const std::shared_ptr logger_; + static const std::string kBaseUrl_; static const std::string kListNwsTextProductsEndpoint_; static const std::string kNwsTextProductEndpoint_; diff --git a/wxdata/include/scwx/provider/iem_api_provider.ipp b/wxdata/include/scwx/provider/iem_api_provider.ipp index 3fc94dad..08ffbd34 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.ipp +++ b/wxdata/include/scwx/provider/iem_api_provider.ipp @@ -2,10 +2,13 @@ #include #include +#include +#include #include #include #include +#include #if (__cpp_lib_chrono < 201907L) # include @@ -39,6 +42,13 @@ IemApiProvider::ListTextProducts(DateRange dates, # define kDateFormat "%Y-%m-%d" #endif + auto formattedDates = dates | ranges::views::transform( + [](const std::chrono::sys_days& date) + { return df::format(kDateFormat, date); }); + + logger_->debug("Listing text products for: {}", + boost::algorithm::join(formattedDates, ", ")); + std::vector asyncResponses {}; for (const auto& [date, cccc, pil] : @@ -75,6 +85,8 @@ IemApiProvider::LoadTextProducts(const Range& textProducts) { auto parameters = cpr::Parameters {{"nolimit", "true"}}; + logger_->debug("Loading {} text products", textProducts.size()); + std::vector> asyncResponses {}; asyncResponses.reserve(textProducts.size()); diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index c4fb1ccd..e9b8ed71 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -17,7 +17,8 @@ namespace scwx::provider { static const std::string logPrefix_ = "scwx::provider::iem_api_provider"; -static const auto logger_ = util::Logger::Create(logPrefix_); + +const auto IemApiProvider::logger_ = util::Logger::Create(logPrefix_); const std::string IemApiProvider::kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; @@ -140,7 +141,7 @@ IemApiProvider::ProcessTextProductLists( } } - logger_->trace("Found {} products", textProducts.size()); + logger_->debug("Found {} products", textProducts.size()); return textProducts; } @@ -175,6 +176,8 @@ IemApiProvider::ProcessTextProductFiles( } } + logger_->debug("Loaded {} text products", textProductFiles.size()); + return textProductFiles; } From ae24991432dc0495fb10d5333625d8f799e48d77 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 26 Apr 2025 15:05:15 -0500 Subject: [PATCH 522/762] Load archive warnings in a dedicated thread --- .../scwx/qt/manager/text_event_manager.cpp | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 72c7531f..2300a7b1 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -239,24 +239,37 @@ void TextEventManager::SelectTime( logger_->trace("Select Time: {}", util::TimeString(dateTime)); - const auto today = std::chrono::floor(dateTime); - const auto yesterday = today - std::chrono::days {1}; - const auto tomorrow = today + std::chrono::days {1}; - const auto dateArray = std::array {today, yesterday, tomorrow}; + boost::asio::post( + p->threadPool_, + [=, this]() + { + try + { + const auto today = std::chrono::floor(dateTime); + const auto yesterday = today - std::chrono::days {1}; + const auto tomorrow = today + std::chrono::days {1}; + const auto dateArray = std::array {today, yesterday, tomorrow}; - const auto dates = - dateArray | ranges::views::filter( - [this](const auto& date) - { - return p->archiveLimit_ == std::chrono::sys_days {} || - date < p->archiveLimit_; - }); + const auto dates = + dateArray | + ranges::views::filter( + [this](const auto& date) + { + return p->archiveLimit_ == std::chrono::sys_days {} || + date < p->archiveLimit_; + }); - std::unique_lock lock {p->archiveMutex_}; + std::unique_lock lock {p->archiveMutex_}; - p->UpdateArchiveDates(dates); - p->ListArchives(dates); - p->LoadArchives(dateTime); + p->UpdateArchiveDates(dates); + p->ListArchives(dates); + p->LoadArchives(dateTime); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); } void TextEventManager::Impl::HandleMessage( From 104fe790fbaa1cda232bb5e5011aef695d3ca596 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 26 Apr 2025 22:31:23 -0500 Subject: [PATCH 523/762] Update segment end time logic for alert layer - Only earlier segments should have their end time updated - The current message should end when the next message begins --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 64 +++++++++++++++------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 77a2332f..9188cc4c 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -336,27 +336,28 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, alertsUpdated {}; const auto& messageList = textEventManager_->message_list(key); - auto message = messageList.at(messageIndex); - if (message->uuid() != uuid) + // Find message by UUID instead of index, as the message index could have + // changed between the signal being emitted and the handler being called + auto messageIt = std::find_if(messageList.cbegin(), + messageList.cend(), + [&uuid](const auto& message) + { return uuid == message->uuid(); }); + + if (messageIt == messageList.cend()) { - // Find message by UUID instead of index, as the message index could have - // changed between the signal being emitted and the handler being called - auto it = std::find_if(messageList.cbegin(), - messageList.cend(), - [&uuid](const auto& message) - { return uuid == message->uuid(); }); - - if (it == messageList.cend()) - { - logger_->warn( - "Could not find alert uuid: {} ({})", key.ToString(), messageIndex); - return; - } - - message = *it; + logger_->warn( + "Could not find alert uuid: {} ({})", key.ToString(), messageIndex); + return; } + auto& message = *messageIt; + auto nextMessageIt = std::next(messageIt); + + // Store the current message index + messageIndex = + static_cast(std::distance(messageList.cbegin(), messageIt)); + // Determine start time for first segment std::chrono::system_clock::time_point segmentBegin {}; if (message->segment_count() > 0) @@ -364,14 +365,31 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, segmentBegin = message->segment(0)->event_begin(); } + // Determine the start time for the first segment of the next message + std::optional nextMessageBegin {}; + if (nextMessageIt != messageList.cend()) + { + nextMessageBegin = + (*nextMessageIt) + ->wmo_header() + ->GetDateTime((*nextMessageIt)->segment(0)->event_begin()); + } + // Take a unique mutex before modifying segments std::unique_lock lock {alertMutex_}; - // Update any existing segments with new end time + // Update any existing earlier segments with new end time auto& segmentsForKey = segmentsByKey_[key]; for (auto& segmentRecord : segmentsForKey) { - if (segmentRecord->segmentEnd_ > segmentBegin) + // Determine if the segment is earlier than the current message + auto it = std::find( + messageList.cbegin(), messageList.cend(), segmentRecord->message_); + auto segmentIndex = + static_cast(std::distance(messageList.cbegin(), it)); + + if (segmentIndex < messageIndex && + segmentRecord->segmentEnd_ > segmentBegin) { segmentRecord->segmentEnd_ = segmentBegin; @@ -398,6 +416,14 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, std::shared_ptr segmentRecord = std::make_shared(segment, key, message); + // Update segment end time to be no later than the begin time of the next + // message (if present) + if (nextMessageBegin.has_value() && + segmentRecord->segmentEnd_ > nextMessageBegin) + { + segmentRecord->segmentEnd_ = nextMessageBegin.value(); + } + segmentsForKey.push_back(segmentRecord); segmentsForType.push_back(segmentRecord); From 3ba569354e7314dc54918dcb5f1800b4f4a9accc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 26 Apr 2025 22:43:44 -0500 Subject: [PATCH 524/762] Ensure the alert layer schedules a render when an alert is added or updated --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 9188cc4c..b0dafcfc 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -598,6 +598,8 @@ void AlertLayer::Impl::AddAlert( lineHover, drawItems.first->second); } + + Q_EMIT self_->NeedsRendering(); } void AlertLayer::Impl::UpdateAlert( @@ -621,6 +623,8 @@ void AlertLayer::Impl::UpdateAlert( geoLines->SetLineEndTime(line, segmentRecord->segmentEnd_); } } + + Q_EMIT self_->NeedsRendering(); } void AlertLayer::Impl::AddLines( From 2f2516b998d68e2e1799c9192c45e24c68b6d811 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 26 Apr 2025 23:03:20 -0500 Subject: [PATCH 525/762] Fix: selected time is uninitialized on layer initialization --- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 5 +++++ scwx-qt/source/scwx/qt/manager/timeline_manager.hpp | 2 ++ scwx-qt/source/scwx/qt/map/alert_layer.cpp | 2 ++ scwx-qt/source/scwx/qt/map/placefile_layer.cpp | 2 ++ 4 files changed, 11 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index f0c95e53..0bcf4f68 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -112,6 +112,11 @@ public: TimelineManager::TimelineManager() : p(std::make_unique(this)) {} TimelineManager::~TimelineManager() = default; +std::chrono::system_clock::time_point TimelineManager::GetSelectedTime() const +{ + return p->selectedTime_; +} + void TimelineManager::SetMapCount(std::size_t mapCount) { p->mapCount_ = mapCount; diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp index 1a4154ac..bbff19f4 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp @@ -24,6 +24,8 @@ public: static std::shared_ptr Instance(); + std::chrono::system_clock::time_point GetSelectedTime() const; + void SetMapCount(std::size_t mapCount); public slots: diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index b0dafcfc..e6d731be 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -268,6 +268,8 @@ void AlertLayer::Initialize() auto& alertLayerHandler = AlertLayerHandler::Instance(); + p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); + // Take a shared lock to prevent handling additional alerts while populating // initial lists std::shared_lock lock {alertLayerHandler.alertMutex_}; diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index df9828eb..dcead2a1 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -122,6 +122,8 @@ void PlacefileLayer::Initialize() logger_->debug("Initialize()"); DrawLayer::Initialize(); + + p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); } void PlacefileLayer::Render( From 8cdd8526ebc31861262afe04b27ec986e87a9ac0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 10:40:17 -0500 Subject: [PATCH 526/762] Archive warning fixes for gcc --- wxdata/include/scwx/awips/wmo_header.hpp | 1 + wxdata/include/scwx/provider/iem_api_provider.ipp | 4 ++++ wxdata/source/scwx/provider/iem_api_provider.cpp | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/wxdata/include/scwx/awips/wmo_header.hpp b/wxdata/include/scwx/awips/wmo_header.hpp index f3e24faf..889c955e 100644 --- a/wxdata/include/scwx/awips/wmo_header.hpp +++ b/wxdata/include/scwx/awips/wmo_header.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace scwx::awips diff --git a/wxdata/include/scwx/provider/iem_api_provider.ipp b/wxdata/include/scwx/provider/iem_api_provider.ipp index 08ffbd34..25187376 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.ipp +++ b/wxdata/include/scwx/provider/iem_api_provider.ipp @@ -104,4 +104,8 @@ IemApiProvider::LoadTextProducts(const Range& textProducts) return ProcessTextProductFiles(asyncResponses); } +#ifdef kDateFormat +# undef kDateFormat +#endif + } // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index e9b8ed71..c3d7dce0 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -18,7 +18,8 @@ namespace scwx::provider static const std::string logPrefix_ = "scwx::provider::iem_api_provider"; -const auto IemApiProvider::logger_ = util::Logger::Create(logPrefix_); +const std::shared_ptr IemApiProvider::logger_ = + util::Logger::Create(logPrefix_); const std::string IemApiProvider::kBaseUrl_ = "https://mesonet.agron.iastate.edu/api/1"; From 81f09e07f0cbebe6bf1a008e17772331875ce9bf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 13:40:04 -0500 Subject: [PATCH 527/762] Archive warnings clang-tidy fixes --- .clang-tidy | 1 + scwx-qt/source/scwx/qt/manager/marker_manager.cpp | 3 ++- .../source/scwx/qt/manager/text_event_manager.cpp | 6 +++--- .../source/scwx/qt/manager/timeline_manager.hpp | 2 +- scwx-qt/source/scwx/qt/manager/update_manager.cpp | 2 +- test/source/scwx/awips/wmo_header.test.cpp | 14 +++++++------- .../scwx/provider/iem_api_provider.test.cpp | 15 ++++----------- .../scwx/provider/warnings_provider.test.cpp | 4 ++-- .../include/scwx/awips/text_product_message.hpp | 2 +- wxdata/source/scwx/awips/text_product_message.cpp | 4 ++-- wxdata/source/scwx/awips/wmo_header.cpp | 10 ++++++---- wxdata/source/scwx/provider/iem_api_provider.cpp | 8 ++++---- wxdata/source/scwx/provider/warnings_provider.cpp | 10 ++++++---- wxdata/source/scwx/util/time.cpp | 8 +++----- 14 files changed, 43 insertions(+), 46 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index dbf9fbd7..645c9c05 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,6 +10,7 @@ Checks: - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' + - '-misc-use-anonymous-namespace' - '-modernize-return-braced-init-list' - '-modernize-use-trailing-return-type' FormatStyle: 'file' diff --git a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp index ea21b211..8af310ec 100644 --- a/scwx-qt/source/scwx/qt/manager/marker_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -70,7 +71,7 @@ public: class MarkerManager::Impl::MarkerRecord { public: - MarkerRecord(const types::MarkerInfo& info) : markerInfo_ {info} {} + MarkerRecord(types::MarkerInfo info) : markerInfo_ {std::move(info)} {} const types::MarkerInfo& toMarkerInfo() { return markerInfo_; } diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 2300a7b1..77b8143b 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -241,7 +241,7 @@ void TextEventManager::SelectTime( boost::asio::post( p->threadPool_, - [=, this]() + [this]() { try { @@ -259,7 +259,7 @@ void TextEventManager::SelectTime( date < p->archiveLimit_; }); - std::unique_lock lock {p->archiveMutex_}; + const std::unique_lock lock {p->archiveMutex_}; p->UpdateArchiveDates(dates); p->ListArchives(dates); @@ -382,7 +382,7 @@ void TextEventManager::Impl::ListArchives(DateRange dates) auto productEntries = provider::IemApiProvider::ListTextProducts( dateArray | ranges::views::all, kEmptyRange_, kPilsView_); - std::unique_lock lock {unloadedProductMapMutex_}; + const std::unique_lock lock {unloadedProductMapMutex_}; if (productEntries.has_value()) { diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp index bbff19f4..054a8201 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.hpp @@ -24,7 +24,7 @@ public: static std::shared_ptr Instance(); - std::chrono::system_clock::time_point GetSelectedTime() const; + [[nodiscard]] std::chrono::system_clock::time_point GetSelectedTime() const; void SetMapCount(std::size_t mapCount); diff --git a/scwx-qt/source/scwx/qt/manager/update_manager.cpp b/scwx-qt/source/scwx/qt/manager/update_manager.cpp index 5910fcaf..05a9c0d1 100644 --- a/scwx-qt/source/scwx/qt/manager/update_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/update_manager.cpp @@ -126,7 +126,7 @@ size_t UpdateManager::Impl::PopulateReleases() // Successful REST API query if (r.status_code == 200) { - boost::json::value json = util::json::ReadJsonString(r.text); + const boost::json::value json = util::json::ReadJsonString(r.text); if (json == nullptr) { logger_->warn("Response not JSON: {}", r.header["content-type"]); diff --git a/test/source/scwx/awips/wmo_header.test.cpp b/test/source/scwx/awips/wmo_header.test.cpp index bdc4406f..17cd9706 100644 --- a/test/source/scwx/awips/wmo_header.test.cpp +++ b/test/source/scwx/awips/wmo_header.test.cpp @@ -16,7 +16,7 @@ TEST(WmoHeader, WmoFields) { std::stringstream ss {kWmoHeaderSample_}; WmoHeader header; - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); EXPECT_EQ(valid, true); EXPECT_EQ(header.sequence_number(), "887"); @@ -40,7 +40,7 @@ TEST(WmoHeader, DateHintBeforeParse) WmoHeader header; header.SetDateHint(2022y / October); - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); EXPECT_EQ(valid, true); EXPECT_EQ(header.GetDateTime(), @@ -54,7 +54,7 @@ TEST(WmoHeader, DateHintAfterParse) std::stringstream ss {kWmoHeaderSample_}; WmoHeader header; - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); header.SetDateHint(2022y / October); EXPECT_EQ(valid, true); @@ -69,7 +69,7 @@ TEST(WmoHeader, EndTimeHintSameMonth) std::stringstream ss {kWmoHeaderSample_}; WmoHeader header; - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); auto endTimeHint = sys_days {2022y / October / 29d} + 0h + 0min + 0s; @@ -85,7 +85,7 @@ TEST(WmoHeader, EndTimeHintPreviousMonth) std::stringstream ss {kWmoHeaderSample_}; WmoHeader header; - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); auto endTimeHint = sys_days {2022y / October / 27d} + 0h + 0min + 0s; @@ -101,7 +101,7 @@ TEST(WmoHeader, EndTimeHintPreviousYear) std::stringstream ss {kWmoHeaderSample_}; WmoHeader header; - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); auto endTimeHint = sys_days {2022y / January / 27d} + 0h + 0min + 0s; @@ -118,7 +118,7 @@ TEST(WmoHeader, EndTimeHintIgnored) WmoHeader header; header.SetDateHint(2022y / October); - bool valid = header.Parse(ss); + const bool valid = header.Parse(ss); auto endTimeHint = sys_days {2020y / January / 1d} + 0h + 0min + 0s; diff --git a/test/source/scwx/provider/iem_api_provider.test.cpp b/test/source/scwx/provider/iem_api_provider.test.cpp index 4f964b81..0cf133e0 100644 --- a/test/source/scwx/provider/iem_api_provider.test.cpp +++ b/test/source/scwx/provider/iem_api_provider.test.cpp @@ -2,9 +2,7 @@ #include -namespace scwx -{ -namespace provider +namespace scwx::provider { TEST(IemApiProviderTest, ListTextProducts) @@ -12,11 +10,9 @@ TEST(IemApiProviderTest, ListTextProducts) using namespace std::chrono; using sys_days = time_point; - IemApiProvider provider {}; - auto date = sys_days {2023y / March / 25d}; - auto torProducts = provider.ListTextProducts(date, {}, "TOR"); + auto torProducts = IemApiProvider::ListTextProducts(date, {}, "TOR"); ASSERT_EQ(torProducts.has_value(), true); EXPECT_EQ(torProducts.value().size(), 35); @@ -40,9 +36,7 @@ TEST(IemApiProviderTest, LoadTextProducts) "202303252015-KFFC-WFUS52-TORFFC", "202303311942-KLZK-WWUS54-SVSLZK"}; - IemApiProvider provider {}; - - auto textProducts = provider.LoadTextProducts(productIds); + auto textProducts = IemApiProvider::LoadTextProducts(productIds); EXPECT_EQ(textProducts.size(), 3); @@ -60,5 +54,4 @@ TEST(IemApiProviderTest, LoadTextProducts) } } -} // namespace provider -} // namespace scwx +} // namespace scwx::provider diff --git a/test/source/scwx/provider/warnings_provider.test.cpp b/test/source/scwx/provider/warnings_provider.test.cpp index de315b4b..c1c824da 100644 --- a/test/source/scwx/provider/warnings_provider.test.cpp +++ b/test/source/scwx/provider/warnings_provider.test.cpp @@ -18,9 +18,9 @@ TEST_P(WarningsProviderTest, LoadUpdatedFiles) { WarningsProvider provider(GetParam()); - std::chrono::sys_time now = + const std::chrono::sys_time now = std::chrono::floor(std::chrono::system_clock::now()); - std::chrono::sys_time startTime = + const std::chrono::sys_time startTime = now - std::chrono::days {3}; auto updatedFiles = provider.LoadUpdatedFiles(startTime); diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index 6830f91a..373dc223 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -96,7 +96,7 @@ public: TextProductMessage(TextProductMessage&&) noexcept; TextProductMessage& operator=(TextProductMessage&&) noexcept; - boost::uuids::uuid uuid() const; + [[nodiscard]] boost::uuids::uuid uuid() const; std::string message_content() const; std::shared_ptr wmo_header() const; std::vector mnd_header() const; diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index f8716b2b..53ec502d 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -593,8 +593,8 @@ std::optional TryParseVtecString(std::istream& is) if (RE2::PartialMatch(line, *rePVtecString)) { - vtec = Vtec(); - bool vtecValid = vtec->pVtec_.Parse(line); + vtec = Vtec(); + const bool vtecValid = vtec->pVtec_.Parse(line); isBegin = is.tellg(); diff --git a/wxdata/source/scwx/awips/wmo_header.cpp b/wxdata/source/scwx/awips/wmo_header.cpp index fad1231b..f89db609 100644 --- a/wxdata/source/scwx/awips/wmo_header.cpp +++ b/wxdata/source/scwx/awips/wmo_header.cpp @@ -143,9 +143,11 @@ std::chrono::sys_time WmoHeader::GetDateTime( { std::chrono::sys_time wmoDateTime {}; - if (p->absoluteDateTime_.has_value()) + const auto absoluteDateTime = p->absoluteDateTime_; + + if (absoluteDateTime.has_value()) { - wmoDateTime = p->absoluteDateTime_.value(); + wmoDateTime = absoluteDateTime.value(); } else if (endTimeHint.has_value()) { @@ -160,8 +162,8 @@ std::chrono::sys_time WmoHeader::GetDateTime( { using namespace std::chrono; - auto endDays = floor(endTimeHint.value()); - year_month_day endDate {endDays}; + const auto endDays = floor(endTimeHint.value()); + const year_month_day endDate {endDays}; // Combine end date year and month with WMO date time wmoDateTime = diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index c3d7dce0..85c37ec5 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -50,9 +50,9 @@ IemApiProvider::ListTextProducts(std::chrono::sys_days date, std::optional optionalCccc, std::optional optionalPil) { - std::string_view cccc = + const std::string_view cccc = optionalCccc.has_value() ? optionalCccc.value() : std::string_view {}; - std::string_view pil = + const std::string_view pil = optionalPil.has_value() ? optionalPil.value() : std::string_view {}; const auto dateArray = std::array {date}; @@ -72,7 +72,7 @@ IemApiProvider::ProcessTextProductLists( { auto response = asyncResponse.get(); - boost::json::value json = util::json::ReadJsonString(response.text); + const boost::json::value json = util::json::ReadJsonString(response.text); if (response.status_code == cpr::status::HTTP_OK) { @@ -161,7 +161,7 @@ IemApiProvider::ProcessTextProductFiles( { // Load file auto& productId = asyncResponse.first; - std::shared_ptr textProductFile { + const std::shared_ptr textProductFile { std::make_shared()}; std::istringstream responseBody {response.text}; if (textProductFile->LoadData(productId, responseBody)) diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index f0d6b8dd..78eb687c 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -5,6 +5,7 @@ // Enable chrono formatters #ifndef __cpp_lib_format +// NOLINTNEXTLINE(bugprone-reserved-identifier, cppcoreguidelines-macro-usage) # define __cpp_lib_format 202110L #endif @@ -106,7 +107,7 @@ WarningsProvider::LoadUpdatedFiles( asyncCallbacks; std::vector> updatedFiles; - std::chrono::sys_time now = + const std::chrono::sys_time now = std::chrono::floor(std::chrono::system_clock::now()); std::chrono::sys_time currentHour = (startTime != std::chrono::sys_time {}) ? @@ -130,7 +131,8 @@ WarningsProvider::LoadUpdatedFiles( { if (headResponse.status_code == cpr::status::HTTP_OK) { - bool updated = p->UpdateFileRecord(headResponse, filename); + const bool updated = + p->UpdateFileRecord(headResponse, filename); if (updated) { @@ -173,7 +175,7 @@ WarningsProvider::LoadUpdatedFiles( logger_->debug("Loading file: {}", filename); // Load file - std::shared_ptr textProductFile { + const std::shared_ptr textProductFile { std::make_shared()}; std::istringstream responseBody {response.text}; if (textProductFile->LoadData(filename, responseBody)) @@ -218,7 +220,7 @@ bool WarningsProvider::Impl::UpdateFileRecord(const cpr::Response& response, lastModified = lastModifiedIt->second; } - std::unique_lock lock(filesMutex_); + const std::unique_lock lock(filesMutex_); auto it = files_.find(filename); if (it != files_.cend()) diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 20c6121d..563aea1b 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -21,9 +21,7 @@ # include #endif -namespace scwx -{ -namespace util +namespace scwx::util { static const std::string logPrefix_ = "scwx::util::time"; @@ -48,6 +46,7 @@ std::chrono::system_clock::time_point TimePoint(uint32_t modifiedJulianDate, using sys_days = time_point; constexpr auto epoch = sys_days {1969y / December / 31d}; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): literals are used return epoch + (modifiedJulianDate * 24h) + std::chrono::milliseconds {milliseconds}; } @@ -154,5 +153,4 @@ template std::optional> TryParseDateTime(const std::string& dateTimeFormat, const std::string& str); -} // namespace util -} // namespace scwx +} // namespace scwx::util From 56961efe769bd1447244867cecd5b47c142ba117 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 14:30:29 -0500 Subject: [PATCH 528/762] Correcting clang-tidy fix compile error --- scwx-qt/source/scwx/qt/manager/text_event_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 77b8143b..7572beb7 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -241,7 +241,7 @@ void TextEventManager::SelectTime( boost::asio::post( p->threadPool_, - [this]() + [dateTime, this]() { try { From 82ba51909ef193f878aca0784b809bcbcbb087c0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 15:09:58 -0500 Subject: [PATCH 529/762] Breaking circular header dependency --- scwx-qt/source/scwx/qt/manager/text_event_manager.cpp | 2 +- wxdata/include/scwx/provider/iem_api_provider.hpp | 2 -- wxdata/source/scwx/provider/iem_api_provider.cpp | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 7572beb7..4c17e819 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/wxdata/include/scwx/provider/iem_api_provider.hpp b/wxdata/include/scwx/provider/iem_api_provider.hpp index a3a65614..1aac31b2 100644 --- a/wxdata/include/scwx/provider/iem_api_provider.hpp +++ b/wxdata/include/scwx/provider/iem_api_provider.hpp @@ -78,5 +78,3 @@ private: }; } // namespace scwx::provider - -#include diff --git a/wxdata/source/scwx/provider/iem_api_provider.cpp b/wxdata/source/scwx/provider/iem_api_provider.cpp index 85c37ec5..ef0576e7 100644 --- a/wxdata/source/scwx/provider/iem_api_provider.cpp +++ b/wxdata/source/scwx/provider/iem_api_provider.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include From 34fc6d584f469dd28991c193c999bf0eee92c95d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 27 Apr 2025 18:34:46 -0500 Subject: [PATCH 530/762] Updating include for IEM API Provider test --- test/source/scwx/provider/iem_api_provider.test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/scwx/provider/iem_api_provider.test.cpp b/test/source/scwx/provider/iem_api_provider.test.cpp index 0cf133e0..e3e25669 100644 --- a/test/source/scwx/provider/iem_api_provider.test.cpp +++ b/test/source/scwx/provider/iem_api_provider.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include From 228ec191f6da72db08ee63195d716887bc55c40c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 2 May 2025 22:56:41 -0500 Subject: [PATCH 531/762] Add year to Text Event Key --- .../scwx/qt/manager/text_event_manager.cpp | 24 ++++++++++++++++- .../source/scwx/qt/types/text_event_key.cpp | 14 ++++++---- .../source/scwx/qt/types/text_event_key.hpp | 26 +++++++++++++++++-- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 4c17e819..6337e97e 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -275,6 +275,8 @@ void TextEventManager::SelectTime( void TextEventManager::Impl::HandleMessage( const std::shared_ptr& message) { + using namespace std::chrono_literals; + auto segments = message->segments(); // If there are no segments, skip this message @@ -295,15 +297,35 @@ void TextEventManager::Impl::HandleMessage( } } + // Determine year + std::chrono::year_month_day wmoDate = std::chrono::floor( + message->wmo_header()->GetDateTime()); + std::chrono::year wmoYear = wmoDate.year(); + std::unique_lock lock(textEventMutex_); // Find a matching event in the event map auto& vtecString = segments[0]->header_->vtecString_; - types::TextEventKey key {vtecString[0].pVtec_}; + types::TextEventKey key {vtecString[0].pVtec_, wmoYear}; size_t messageIndex = 0; auto it = textEventMap_.find(key); bool updated = false; + if ( + // If there was no matching event + it == textEventMap_.cend() && + // The event is not new + vtecString[0].pVtec_.action() != awips::PVtec::Action::New && + // The message was on January 1 + wmoDate.month() == std::chrono::January && wmoDate.day() == 1d && + // This is at least the 10th ETN of the year + vtecString[0].pVtec_.event_tracking_number() > 10) + { + // Attempt to find a matching event from last year + key = {vtecString[0].pVtec_, wmoYear - std::chrono::years {1}}; + it = textEventMap_.find(key); + } + if (it == textEventMap_.cend()) { // If there was no matching event, add the message to a new event diff --git a/scwx-qt/source/scwx/qt/types/text_event_key.cpp b/scwx-qt/source/scwx/qt/types/text_event_key.cpp index bebf6f63..be5d0443 100644 --- a/scwx-qt/source/scwx/qt/types/text_event_key.cpp +++ b/scwx-qt/source/scwx/qt/types/text_event_key.cpp @@ -14,26 +14,29 @@ static const std::string logPrefix_ = "scwx::qt::types::text_event_key"; std::string TextEventKey::ToFullString() const { - return fmt::format("{} {} {} {:04}", + return fmt::format("{} {} {} {:04} ({:04})", officeId_, awips::GetPhenomenonText(phenomenon_), awips::GetSignificanceText(significance_), - etn_); + etn_, + static_cast(year_)); } std::string TextEventKey::ToString() const { - return fmt::format("{}.{}.{}.{:04}", + return fmt::format("{}.{}.{}.{:04}.{:04}", officeId_, awips::GetPhenomenonCode(phenomenon_), awips::GetSignificanceCode(significance_), - etn_); + etn_, + static_cast(year_)); } bool TextEventKey::operator==(const TextEventKey& o) const { return (officeId_ == o.officeId_ && phenomenon_ == o.phenomenon_ && - significance_ == o.significance_ && etn_ == o.etn_); + significance_ == o.significance_ && etn_ == o.etn_ && + year_ == o.year_); } size_t TextEventHash::operator()(const TextEventKey& x) const @@ -43,6 +46,7 @@ size_t TextEventHash::operator()(const TextEventKey& x) const boost::hash_combine(seed, x.phenomenon_); boost::hash_combine(seed, x.significance_); boost::hash_combine(seed, x.etn_); + boost::hash_combine(seed, static_cast(x.year_)); return seed; } diff --git a/scwx-qt/source/scwx/qt/types/text_event_key.hpp b/scwx-qt/source/scwx/qt/types/text_event_key.hpp index f962bcdf..4cc57dca 100644 --- a/scwx-qt/source/scwx/qt/types/text_event_key.hpp +++ b/scwx-qt/source/scwx/qt/types/text_event_key.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace scwx { @@ -12,12 +13,32 @@ namespace types struct TextEventKey { TextEventKey() : TextEventKey(awips::PVtec {}) {} - TextEventKey(const awips::PVtec& pvtec) : + TextEventKey(const awips::PVtec& pvtec, std::chrono::year yearHint = {}) : officeId_ {pvtec.office_id()}, phenomenon_ {pvtec.phenomenon()}, significance_ {pvtec.significance()}, etn_ {pvtec.event_tracking_number()} { + using namespace std::chrono_literals; + + std::chrono::year_month_day ymd = + std::chrono::floor(pvtec.event_begin()); + if (ymd.year() > 1970y) + { + // Prefer the year from the event begin + year_ = ymd.year(); + } + else if (yearHint > 1970y) + { + // Otherwise, use the year hint + year_ = yearHint; + } + else + { + // If there was no year hint, use the event end + ymd = std::chrono::floor(pvtec.event_end()); + year_ = ymd.year(); + } } std::string ToFullString() const; @@ -27,7 +48,8 @@ struct TextEventKey std::string officeId_; awips::Phenomenon phenomenon_; awips::Significance significance_; - int16_t etn_; + std::int16_t etn_; + std::chrono::year year_; }; template From 4719badc5425568d99ef25aff65c25867d29150e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 3 May 2025 09:27:58 -0500 Subject: [PATCH 532/762] clang-tidy fixes --- .../scwx/qt/manager/text_event_manager.cpp | 23 ++++++++++--------- .../source/scwx/qt/types/text_event_key.hpp | 8 ++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 6337e97e..c6da90f7 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -27,11 +27,7 @@ # include #endif -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { using namespace std::chrono_literals; @@ -115,6 +111,11 @@ public: threadPool_.join(); } + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void HandleMessage(const std::shared_ptr& message); template @@ -298,9 +299,10 @@ void TextEventManager::Impl::HandleMessage( } // Determine year - std::chrono::year_month_day wmoDate = std::chrono::floor( - message->wmo_header()->GetDateTime()); - std::chrono::year wmoYear = wmoDate.year(); + const std::chrono::year_month_day wmoDate = + std::chrono::floor( + message->wmo_header()->GetDateTime()); + const std::chrono::year wmoYear = wmoDate.year(); std::unique_lock lock(textEventMutex_); @@ -319,6 +321,7 @@ void TextEventManager::Impl::HandleMessage( // The message was on January 1 wmoDate.month() == std::chrono::January && wmoDate.day() == 1d && // This is at least the 10th ETN of the year + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability vtecString[0].pVtec_.event_tracking_number() > 10) { // Attempt to find a matching event from last year @@ -635,6 +638,4 @@ std::shared_ptr TextEventManager::Instance() return textEventManager; } -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager diff --git a/scwx-qt/source/scwx/qt/types/text_event_key.hpp b/scwx-qt/source/scwx/qt/types/text_event_key.hpp index 4cc57dca..15eec31c 100644 --- a/scwx-qt/source/scwx/qt/types/text_event_key.hpp +++ b/scwx-qt/source/scwx/qt/types/text_event_key.hpp @@ -21,14 +21,16 @@ struct TextEventKey { using namespace std::chrono_literals; + static constexpr std::chrono::year kMinYear_ = 1970y; + std::chrono::year_month_day ymd = std::chrono::floor(pvtec.event_begin()); - if (ymd.year() > 1970y) + if (ymd.year() > kMinYear_) { // Prefer the year from the event begin year_ = ymd.year(); } - else if (yearHint > 1970y) + else if (yearHint > kMinYear_) { // Otherwise, use the year hint year_ = yearHint; @@ -49,7 +51,7 @@ struct TextEventKey awips::Phenomenon phenomenon_; awips::Significance significance_; std::int16_t etn_; - std::chrono::year year_; + std::chrono::year year_ {}; }; template From f37a77a9f7222aab3e1bdb7c7d9826808145759f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 3 May 2025 23:20:26 -0500 Subject: [PATCH 533/762] Add text event pruning - Still need to prune AlertLayer - Still need to test alerts reload after being pruned --- .../source/scwx/qt/manager/alert_manager.cpp | 6 +- .../scwx/qt/manager/text_event_manager.cpp | 114 +++++++++++++++++- .../scwx/qt/manager/text_event_manager.hpp | 5 + scwx-qt/source/scwx/qt/model/alert_model.cpp | 31 ++++- scwx-qt/source/scwx/qt/model/alert_model.hpp | 5 + .../source/scwx/qt/ui/alert_dock_widget.cpp | 5 + 6 files changed, 158 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index 757754a9..748e0943 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -138,8 +138,10 @@ common::Coordinate AlertManager::Impl::CurrentCoordinate( void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, size_t messageIndex) const { + auto messages = textEventManager_->message_list(key); + // Skip alert if there are more messages to be processed - if (messageIndex + 1 < textEventManager_->message_count(key)) + if (messages.empty() || messageIndex + 1 < messages.size()) { return; } @@ -153,7 +155,7 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, audioSettings.alert_radius().GetValue()); std::string alertWFO = audioSettings.alert_wfo().GetValue(); - auto message = textEventManager_->message_list(key).at(messageIndex); + auto message = messages.at(messageIndex); for (auto& segment : message->segments()) { diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index c6da90f7..70505d44 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -116,13 +116,14 @@ public: Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; - void - HandleMessage(const std::shared_ptr& message); + void HandleMessage(const std::shared_ptr& message, + bool archiveEvent = false); template requires std::same_as, std::chrono::sys_days> void ListArchives(DateRange dates); void LoadArchives(std::chrono::system_clock::time_point dateTime); + void PruneArchives(); void RefreshAsync(); void Refresh(); template @@ -155,6 +156,15 @@ public: std::mutex archiveMutex_ {}; std::list archiveDates_ {}; + std::mutex archiveEventKeyMutex_ {}; + std::map>> + archiveEventKeys_ {}; + std::unordered_set> + liveEventKeys_ {}; + std::mutex unloadedProductMapMutex_ {}; std::map> @@ -249,7 +259,7 @@ void TextEventManager::SelectTime( const auto today = std::chrono::floor(dateTime); const auto yesterday = today - std::chrono::days {1}; const auto tomorrow = today + std::chrono::days {1}; - const auto dateArray = std::array {today, yesterday, tomorrow}; + const auto dateArray = std::array {yesterday, today, tomorrow}; const auto dates = dateArray | @@ -265,6 +275,7 @@ void TextEventManager::SelectTime( p->UpdateArchiveDates(dates); p->ListArchives(dates); p->LoadArchives(dateTime); + p->PruneArchives(); } catch (const std::exception& ex) { @@ -274,7 +285,7 @@ void TextEventManager::SelectTime( } void TextEventManager::Impl::HandleMessage( - const std::shared_ptr& message) + const std::shared_ptr& message, bool archiveEvent) { using namespace std::chrono_literals; @@ -335,6 +346,12 @@ void TextEventManager::Impl::HandleMessage( textEventMap_.emplace(key, std::vector {message}); messageIndex = 0; updated = true; + + if (!archiveEvent) + { + // Add the Text Event Key to the list of live events to prevent pruning + liveEventKeys_.insert(key); + } } else if (std::find_if(it->second.cbegin(), it->second.cend(), @@ -368,6 +385,17 @@ void TextEventManager::Impl::HandleMessage( updated = true; }; + // If this is an archive event, and the key does not exist in the live events + // Assumption: A live event will always be loaded before a duplicate archive + // event + if (archiveEvent && !liveEventKeys_.contains(key)) + { + // Add the Text Event Key to the current date's archive + const std::unique_lock archiveEventKeyLock {archiveEventKeyMutex_}; + auto& archiveKeys = archiveEventKeys_[wmoDate]; + archiveKeys.insert(key); + } + lock.unlock(); if (updated) @@ -518,11 +546,87 @@ void TextEventManager::Impl::LoadArchives( for (auto& message : messages) { - HandleMessage(message); + HandleMessage(message, true); } } } +void TextEventManager::Impl::PruneArchives() +{ + static constexpr std::size_t kMaxArchiveDates_ = 5; + + std::unordered_set> + eventKeysToKeep {}; + std::unordered_set> + eventKeysToPrune {}; + + // Remove oldest dates from the archive + while (archiveDates_.size() > kMaxArchiveDates_) + { + archiveDates_.pop_front(); + } + + const std::unique_lock archiveEventKeyLock {archiveEventKeyMutex_}; + + // If there are the same number of dates in both archiveEventKeys_ and + // archiveDates_, there is nothing to prune + if (archiveEventKeys_.size() == archiveDates_.size()) + { + // Nothing to prune + return; + } + + const std::unique_lock unloadedProductMapLock {unloadedProductMapMutex_}; + + for (auto it = archiveEventKeys_.begin(); it != archiveEventKeys_.end();) + { + const auto& date = it->first; + const auto& eventKeys = it->second; + + // If date is not in recent days map + if (std::find(archiveDates_.cbegin(), archiveDates_.cend(), date) == + archiveDates_.cend()) + { + // Prune these keys (unless they are in the eventKeysToKeep set) + eventKeysToPrune.insert(eventKeys.begin(), eventKeys.end()); + + // The date is not in the list of recent dates, remove it + it = archiveEventKeys_.erase(it); + unloadedProductMap_.erase(date); + } + else + { + // Make sure these keys don't get pruned + eventKeysToKeep.insert(eventKeys.begin(), eventKeys.end()); + + // The date is recent, keep it + ++it; + } + } + + // Remove elements from eventKeysToPrune if they are in eventKeysToKeep + for (const auto& eventKey : eventKeysToKeep) + { + eventKeysToPrune.erase(eventKey); + } + + // Remove eventKeysToPrune from textEventMap + for (const auto& eventKey : eventKeysToPrune) + { + textEventMap_.erase(eventKey); + } + + // If event keys were pruned, emit a signal + if (!eventKeysToPrune.empty()) + { + logger_->debug("Pruned {} archive events", eventKeysToPrune.size()); + + Q_EMIT self_->AlertsRemoved(eventKeysToPrune); + } +} + void TextEventManager::Impl::RefreshAsync() { boost::asio::post(threadPool_, diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp index 312dece0..61affe6c 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,10 @@ public: static std::shared_ptr Instance(); signals: + void AlertsRemoved( + const std::unordered_set>& + keys); void AlertUpdated(const types::TextEventKey& key, std::size_t messageIndex, boost::uuids::uuid uuid); diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 516dbfae..9baaf211 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -338,7 +338,7 @@ void AlertModel::HandleAlert(const types::TextEventKey& alertKey, auto alertMessages = p->textEventManager_->message_list(alertKey); // Skip alert if this is not the most recent message - if (messageIndex + 1 < alertMessages.size()) + if (alertMessages.empty() || messageIndex + 1 < alertMessages.size()) { return; } @@ -393,6 +393,35 @@ void AlertModel::HandleAlert(const types::TextEventKey& alertKey, } } +void AlertModel::HandleAlertsRemoved( + const std::unordered_set>& + alertKeys) +{ + logger_->trace("Handle alerts removed"); + + for (const auto& alertKey : alertKeys) + { + // Remove from the list of text event keys + auto it = std::find( + p->textEventKeys_.begin(), p->textEventKeys_.end(), alertKey); + if (it != p->textEventKeys_.end()) + { + int row = std::distance(p->textEventKeys_.begin(), it); + beginRemoveRows(QModelIndex(), row, row); + p->textEventKeys_.erase(it); + endRemoveRows(); + } + + // Remove from internal maps + p->observedMap_.erase(alertKey); + p->threatCategoryMap_.erase(alertKey); + p->tornadoPossibleMap_.erase(alertKey); + p->centroidMap_.erase(alertKey); + p->distanceMap_.erase(alertKey); + } +} + void AlertModel::HandleMapUpdate(double latitude, double longitude) { logger_->trace("Handle map update: {}, {}", latitude, longitude); diff --git a/scwx-qt/source/scwx/qt/model/alert_model.hpp b/scwx-qt/source/scwx/qt/model/alert_model.hpp index df6d561e..02781b6b 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.hpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.hpp @@ -4,6 +4,7 @@ #include #include +#include #include @@ -51,6 +52,10 @@ public: public slots: void HandleAlert(const types::TextEventKey& alertKey, size_t messageIndex); + void HandleAlertsRemoved( + const std::unordered_set>& + alertKeys); void HandleMapUpdate(double latitude, double longitude); private: diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp index 61fd160a..5e22071a 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.cpp @@ -131,6 +131,11 @@ void AlertDockWidgetImpl::ConnectSignals() &QAction::toggled, proxyModel_.get(), &model::AlertProxyModel::SetAlertActiveFilter); + connect(textEventManager_.get(), + &manager::TextEventManager::AlertsRemoved, + alertModel_.get(), + &model::AlertModel::HandleAlertsRemoved, + Qt::QueuedConnection); connect(textEventManager_.get(), &manager::TextEventManager::AlertUpdated, alertModel_.get(), From 671ec1d6583f671575d433cc3c1b8ff6abd60b43 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 4 May 2025 23:05:21 -0500 Subject: [PATCH 534/762] Handle removed alerts from alert layer --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 128 +++++++++++++++++---- 1 file changed, 108 insertions(+), 20 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index e6d731be..811f3b88 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -73,10 +73,11 @@ public: connect(textEventManager_.get(), &manager::TextEventManager::AlertUpdated, this, - [this](const types::TextEventKey& key, - std::size_t messageIndex, - boost::uuids::uuid uuid) - { HandleAlert(key, messageIndex, uuid); }); + &AlertLayerHandler::HandleAlert); + connect(textEventManager_.get(), + &manager::TextEventManager::AlertsRemoved, + this, + &AlertLayerHandler::HandleAlertsRemoved); } ~AlertLayerHandler() { @@ -100,6 +101,10 @@ public: void HandleAlert(const types::TextEventKey& key, size_t messageIndex, boost::uuids::uuid uuid); + void HandleAlertsRemoved( + const std::unordered_set>& + keys); static AlertLayerHandler& Instance(); @@ -112,6 +117,7 @@ signals: void AlertAdded(const std::shared_ptr& segmentRecord, awips::Phenomenon phenomenon); void AlertUpdated(const std::shared_ptr& segmentRecord); + void AlertsRemoved(awips::Phenomenon phenomenon, bool alertActive); void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); }; @@ -190,6 +196,7 @@ public: bool enableHover, boost::container::stable_vector< std::shared_ptr>& drawItems); + void PopulateLines(bool alertActive); void UpdateLines(); static LineData CreateLineData(const settings::LineSettings& lineSettings); @@ -276,22 +283,7 @@ void AlertLayer::Initialize() for (auto alertActive : {false, true}) { - auto& geoLines = p->geoLines_.at(alertActive); - - geoLines->StartLines(); - - // Populate initial segments - auto segmentsIt = - alertLayerHandler.segmentsByType_.find({p->phenomenon_, alertActive}); - if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) - { - for (auto& segment : segmentsIt->second) - { - p->AddAlert(segment); - } - } - - geoLines->FinishLines(); + p->PopulateLines(alertActive); } p->ConnectAlertHandlerSignals(); @@ -444,6 +436,65 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, } } +void AlertLayerHandler::HandleAlertsRemoved( + const std::unordered_set>& keys) +{ + logger_->trace("HandleAlertsRemoved: {} keys", keys.size()); + + std::unordered_set, + AlertTypeHash>> + alertsRemoved {}; + + // Take a unique lock before modifying segments + std::unique_lock lock {alertMutex_}; + + for (const auto& key : keys) + { + // Remove segments associated with the key + auto segmentsIt = segmentsByKey_.find(key); + if (segmentsIt != segmentsByKey_.end()) + { + for (const auto& segmentRecord : segmentsIt->second) + { + auto& segment = segmentRecord->segment_; + bool alertActive = IsAlertActive(segment); + + // Remove from segmentsByType_ + auto typeIt = segmentsByType_.find({key.phenomenon_, alertActive}); + if (typeIt != segmentsByType_.end()) + { + auto& segmentsForType = typeIt->second; + segmentsForType.erase(std::remove(segmentsForType.begin(), + segmentsForType.end(), + segmentRecord), + segmentsForType.end()); + + // If no segments remain for this type, erase the entry + if (segmentsForType.empty()) + { + segmentsByType_.erase(typeIt); + } + } + + alertsRemoved.emplace(key.phenomenon_, alertActive); + } + + // Remove the key from segmentsByKey_ + segmentsByKey_.erase(segmentsIt); + } + } + + // Release the lock after completing segment updates + lock.unlock(); + + // Emit signal to notify that alerts have been removed + for (auto& alert : alertsRemoved) + { + Q_EMIT AlertsRemoved(alert.first, alert.second); + } +} + void AlertLayer::Impl::ConnectAlertHandlerSignals() { auto& alertLayerHandler = AlertLayerHandler::Instance(); @@ -473,6 +524,22 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() UpdateAlert(segmentRecord); } }); + QObject::connect( + &alertLayerHandler, + &AlertLayerHandler::AlertsRemoved, + receiver_.get(), + [&alertLayerHandler, this](awips::Phenomenon phenomenon, bool alertActive) + { + if (phenomenon == phenomenon_) + { + // Take a shared lock to prevent handling additional alerts while + // populating initial lists + const std::shared_lock lock {alertLayerHandler.alertMutex_}; + + // Re-populate the lines if multiple alerts were removed + PopulateLines(alertActive); + } + }); } void AlertLayer::Impl::ConnectSignals() @@ -704,6 +771,27 @@ void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, } } +void AlertLayer::Impl::PopulateLines(bool alertActive) +{ + auto& alertLayerHandler = AlertLayerHandler::Instance(); + auto& geoLines = geoLines_.at(alertActive); + + geoLines->StartLines(); + + // Populate initial segments + auto segmentsIt = + alertLayerHandler.segmentsByType_.find({phenomenon_, alertActive}); + if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) + { + for (auto& segment : segmentsIt->second) + { + AddAlert(segment); + } + } + + geoLines->FinishLines(); +} + void AlertLayer::Impl::UpdateLines() { std::unique_lock lock {linesMutex_}; From f40c24ce6fa5665461c00e4c8c6ae24a24ce99ff Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 4 May 2025 23:05:36 -0500 Subject: [PATCH 535/762] Alert layer warning cleanup --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 53 ++++++++++++---------- scwx-qt/source/scwx/qt/map/alert_layer.hpp | 4 +- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 811f3b88..e1e43965 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -19,12 +19,9 @@ #include #include #include +#include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::alert_layer"; @@ -46,6 +43,8 @@ static bool IsAlertActive(const std::shared_ptr& segment); class AlertLayerHandler : public QObject { Q_OBJECT + Q_DISABLE_COPY_MOVE(AlertLayerHandler) + public: struct SegmentRecord { @@ -57,10 +56,10 @@ public: SegmentRecord( const std::shared_ptr& segment, - const types::TextEventKey& key, + types::TextEventKey key, const std::shared_ptr& message) : segment_ {segment}, - key_ {key}, + key_ {std::move(key)}, message_ {message}, segmentBegin_ {segment->event_begin()}, segmentEnd_ {segment->event_end()} @@ -161,6 +160,11 @@ public: std::unique_lock lock(linesMutex_); }; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void AddAlert( const std::shared_ptr& segmentRecord); void UpdateAlert( @@ -182,14 +186,14 @@ public: std::shared_ptr& di, const common::Coordinate& p1, const common::Coordinate& p2, - boost::gil::rgba32f_pixel_t color, + const boost::gil::rgba32f_pixel_t& color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover); void AddLines(std::shared_ptr& geoLines, const std::vector& coordinates, - boost::gil::rgba32f_pixel_t color, + const boost::gil::rgba32f_pixel_t& color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, @@ -238,8 +242,8 @@ public: std::vector connections_ {}; }; -AlertLayer::AlertLayer(std::shared_ptr context, - awips::Phenomenon phenomenon) : +AlertLayer::AlertLayer(const std::shared_ptr& context, + awips::Phenomenon phenomenon) : DrawLayer( context, fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), @@ -620,9 +624,9 @@ void AlertLayer::Impl::AddAlert( // If draw items were added if (drawItems.second) { - const float borderWidth = lineData.borderWidth_; - const float highlightWidth = lineData.highlightWidth_; - const float lineWidth = lineData.lineWidth_; + const float borderWidth = static_cast(lineData.borderWidth_); + const float highlightWidth = static_cast(lineData.highlightWidth_); + const float lineWidth = static_cast(lineData.lineWidth_); const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); @@ -699,7 +703,7 @@ void AlertLayer::Impl::UpdateAlert( void AlertLayer::Impl::AddLines( std::shared_ptr& geoLines, const std::vector& coordinates, - boost::gil::rgba32f_pixel_t color, + const boost::gil::rgba32f_pixel_t& color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, @@ -739,14 +743,17 @@ void AlertLayer::Impl::AddLine(std::shared_ptr& geoLines, std::shared_ptr& di, const common::Coordinate& p1, const common::Coordinate& p2, - boost::gil::rgba32f_pixel_t color, + const boost::gil::rgba32f_pixel_t& color, float width, std::chrono::system_clock::time_point startTime, std::chrono::system_clock::time_point endTime, bool enableHover) { - geoLines->SetLineLocation( - di, p1.latitude_, p1.longitude_, p2.latitude_, p2.longitude_); + geoLines->SetLineLocation(di, + static_cast(p1.latitude_), + static_cast(p1.longitude_), + static_cast(p2.latitude_), + static_cast(p2.longitude_)); geoLines->SetLineModulate(di, color); geoLines->SetLineWidth(di, width); geoLines->SetLineStartTime(di, startTime); @@ -805,9 +812,9 @@ void AlertLayer::Impl::UpdateLines() auto& lineData = GetLineData(segment, alertActive); auto& geoLines = geoLines_.at(alertActive); - const float borderWidth = lineData.borderWidth_; - const float highlightWidth = lineData.highlightWidth_; - const float lineWidth = lineData.lineWidth_; + const float borderWidth = static_cast(lineData.borderWidth_); + const float highlightWidth = static_cast(lineData.highlightWidth_); + const float lineWidth = static_cast(lineData.lineWidth_); const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); @@ -982,8 +989,6 @@ size_t AlertTypeHash>::operator()( return seed; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map #include "alert_layer.moc" diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index d51391e3..60905680 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -22,8 +22,8 @@ class AlertLayer : public DrawLayer Q_DISABLE_COPY_MOVE(AlertLayer) public: - explicit AlertLayer(std::shared_ptr context, - scwx::awips::Phenomenon phenomenon); + explicit AlertLayer(const std::shared_ptr& context, + scwx::awips::Phenomenon phenomenon); ~AlertLayer(); void Initialize() override final; From 0c59a0d4d27952ce2f8cc3616b473cf278b37ef3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 4 May 2025 23:09:57 -0500 Subject: [PATCH 536/762] Alert model clang-tidy cleanupp --- scwx-qt/source/scwx/qt/model/alert_model.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 9baaf211..a41a033f 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -10,16 +10,10 @@ #include #include -#include - #include #include -namespace scwx -{ -namespace qt -{ -namespace model +namespace scwx::qt::model { static const std::string logPrefix_ = "scwx::qt::model::alert_model"; @@ -407,7 +401,8 @@ void AlertModel::HandleAlertsRemoved( p->textEventKeys_.begin(), p->textEventKeys_.end(), alertKey); if (it != p->textEventKeys_.end()) { - int row = std::distance(p->textEventKeys_.begin(), it); + const int row = + static_cast(std::distance(p->textEventKeys_.begin(), it)); beginRemoveRows(QModelIndex(), row, row); p->textEventKeys_.erase(it); endRemoveRows(); @@ -597,6 +592,4 @@ std::string AlertModelImpl::GetEndTimeString(const types::TextEventKey& key) return scwx::util::TimeString(GetEndTime(key)); } -} // namespace model -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::model From 490989ac2ad5345eb46ec63e14279ca5b508f31d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 00:25:28 -0500 Subject: [PATCH 537/762] Make archive event pruning more robust --- .../scwx/qt/manager/text_event_manager.cpp | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 70505d44..8aa4c611 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -570,9 +570,10 @@ void TextEventManager::Impl::PruneArchives() const std::unique_lock archiveEventKeyLock {archiveEventKeyMutex_}; - // If there are the same number of dates in both archiveEventKeys_ and - // archiveDates_, there is nothing to prune - if (archiveEventKeys_.size() == archiveDates_.size()) + // If there are the same number of dates in archiveEventKeys_, archiveDates_ + // and unloadedProductMap_, there is nothing to prune + if (archiveEventKeys_.size() == archiveDates_.size() && + unloadedProductMap_.size() == archiveDates_.size()) { // Nothing to prune return; @@ -594,7 +595,6 @@ void TextEventManager::Impl::PruneArchives() // The date is not in the list of recent dates, remove it it = archiveEventKeys_.erase(it); - unloadedProductMap_.erase(date); } else { @@ -606,6 +606,24 @@ void TextEventManager::Impl::PruneArchives() } } + for (auto it = unloadedProductMap_.begin(); it != unloadedProductMap_.end();) + { + const auto& date = it->first; + + // If date is not in recent days map + if (std::find(archiveDates_.cbegin(), archiveDates_.cend(), date) == + archiveDates_.cend()) + { + // The date is not in the list of recent dates, remove it + it = unloadedProductMap_.erase(it); + } + else + { + // The date is recent, keep it + ++it; + } + } + // Remove elements from eventKeysToPrune if they are in eventKeysToKeep for (const auto& eventKey : eventKeysToKeep) { From 1fdefe83de9563a7a4dfa546406a6731f6bdd465 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 00:30:23 -0500 Subject: [PATCH 538/762] invalidateRowsFilter must be called from UI thread --- .../scwx/qt/model/alert_proxy_model.cpp | 37 ++++++++++--------- .../scwx/qt/model/alert_proxy_model.hpp | 22 ++++------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp index a2afee55..224f7400 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp @@ -9,21 +9,22 @@ #include -namespace scwx -{ -namespace qt -{ -namespace model +namespace scwx::qt::model { static const std::string logPrefix_ = "scwx::qt::model::alert_proxy_model"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class AlertProxyModelImpl +class AlertProxyModel::Impl { public: - explicit AlertProxyModelImpl(AlertProxyModel* self); - ~AlertProxyModelImpl(); + explicit Impl(AlertProxyModel* self); + ~Impl(); + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void UpdateAlerts(); @@ -36,8 +37,7 @@ public: }; AlertProxyModel::AlertProxyModel(QObject* parent) : - QSortFilterProxyModel(parent), - p(std::make_unique(this)) + QSortFilterProxyModel(parent), p(std::make_unique(this)) { } AlertProxyModel::~AlertProxyModel() = default; @@ -77,7 +77,7 @@ bool AlertProxyModel::filterAcceptsRow(int sourceRow, QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } -AlertProxyModelImpl::AlertProxyModelImpl(AlertProxyModel* self) : +AlertProxyModel::Impl::Impl(AlertProxyModel* self) : self_ {self}, alertActiveFilterEnabled_ {false}, alertUpdateTimer_ {scwx::util::io_context()} @@ -86,13 +86,13 @@ AlertProxyModelImpl::AlertProxyModelImpl(AlertProxyModel* self) : UpdateAlerts(); } -AlertProxyModelImpl::~AlertProxyModelImpl() +AlertProxyModel::Impl::~Impl() { std::unique_lock lock(alertMutex_); alertUpdateTimer_.cancel(); } -void AlertProxyModelImpl::UpdateAlerts() +void AlertProxyModel::Impl::UpdateAlerts() { logger_->trace("UpdateAlerts"); @@ -102,10 +102,15 @@ void AlertProxyModelImpl::UpdateAlerts() // Re-evaluate for expired alerts if (alertActiveFilterEnabled_) { - self_->invalidateRowsFilter(); + QMetaObject::invokeMethod( + self_, + static_cast( + &QSortFilterProxyModel::invalidateRowsFilter)); } using namespace std::chrono; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability alertUpdateTimer_.expires_after(15s); alertUpdateTimer_.async_wait( [this](const boost::system::error_code& e) @@ -132,6 +137,4 @@ void AlertProxyModelImpl::UpdateAlerts() }); } -} // namespace model -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::model diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.hpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.hpp index ee8b81c1..1ee6a138 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.hpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.hpp @@ -4,11 +4,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace model +namespace scwx::qt::model { class AlertProxyModelImpl; @@ -16,7 +12,7 @@ class AlertProxyModelImpl; class AlertProxyModel : public QSortFilterProxyModel { private: - Q_DISABLE_COPY(AlertProxyModel) + Q_DISABLE_COPY_MOVE(AlertProxyModel) public: explicit AlertProxyModel(QObject* parent = nullptr); @@ -24,15 +20,13 @@ public: void SetAlertActiveFilter(bool enabled); - bool filterAcceptsRow(int sourceRow, - const QModelIndex& sourceParent) const override; + [[nodiscard]] bool + filterAcceptsRow(int sourceRow, + const QModelIndex& sourceParent) const override; private: - std::unique_ptr p; - - friend class AlertProxyModelImpl; + class Impl; + std::unique_ptr p; }; -} // namespace model -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::model From 86926178dfae13916993a263fd8688062e575cee Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 21:49:41 -0500 Subject: [PATCH 539/762] Call QSortFilterProxyModel::invalidate instead of invalidateRowsFilter (public vs. protected API) --- scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp index 224f7400..dff55202 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp @@ -102,10 +102,9 @@ void AlertProxyModel::Impl::UpdateAlerts() // Re-evaluate for expired alerts if (alertActiveFilterEnabled_) { - QMetaObject::invokeMethod( - self_, - static_cast( - &QSortFilterProxyModel::invalidateRowsFilter)); + QMetaObject::invokeMethod(self_, + static_cast( + &QSortFilterProxyModel::invalidate)); } using namespace std::chrono; From dc074b0262ed447b64c935a9e527105f4b2dd7ae Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 21:50:01 -0500 Subject: [PATCH 540/762] More clang-tidy fixes --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 16 ++++++++-------- .../source/scwx/qt/model/alert_proxy_model.cpp | 11 +++++++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index e1e43965..786d80e0 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -461,8 +461,8 @@ void AlertLayerHandler::HandleAlertsRemoved( { for (const auto& segmentRecord : segmentsIt->second) { - auto& segment = segmentRecord->segment_; - bool alertActive = IsAlertActive(segment); + auto& segment = segmentRecord->segment_; + const bool alertActive = IsAlertActive(segment); // Remove from segmentsByType_ auto typeIt = segmentsByType_.find({key.phenomenon_, alertActive}); @@ -624,9 +624,9 @@ void AlertLayer::Impl::AddAlert( // If draw items were added if (drawItems.second) { - const float borderWidth = static_cast(lineData.borderWidth_); - const float highlightWidth = static_cast(lineData.highlightWidth_); - const float lineWidth = static_cast(lineData.lineWidth_); + const auto borderWidth = static_cast(lineData.borderWidth_); + const auto highlightWidth = static_cast(lineData.highlightWidth_); + const auto lineWidth = static_cast(lineData.lineWidth_); const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); @@ -812,9 +812,9 @@ void AlertLayer::Impl::UpdateLines() auto& lineData = GetLineData(segment, alertActive); auto& geoLines = geoLines_.at(alertActive); - const float borderWidth = static_cast(lineData.borderWidth_); - const float highlightWidth = static_cast(lineData.highlightWidth_); - const float lineWidth = static_cast(lineData.lineWidth_); + const auto borderWidth = static_cast(lineData.borderWidth_); + const auto highlightWidth = static_cast(lineData.highlightWidth_); + const auto lineWidth = static_cast(lineData.lineWidth_); const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp index dff55202..fd5ffc69 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp @@ -88,8 +88,15 @@ AlertProxyModel::Impl::Impl(AlertProxyModel* self) : AlertProxyModel::Impl::~Impl() { - std::unique_lock lock(alertMutex_); - alertUpdateTimer_.cancel(); + try + { + std::unique_lock lock(alertMutex_); + alertUpdateTimer_.cancel(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } } void AlertProxyModel::Impl::UpdateAlerts() From 4532327f50f1aeb170125a90f70fc0269f4e5c84 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 22:13:18 -0500 Subject: [PATCH 541/762] AlertModel::HandleAlert should find the alert index from the UUID --- scwx-qt/source/scwx/qt/model/alert_model.cpp | 48 ++++++++++++++------ scwx-qt/source/scwx/qt/model/alert_model.hpp | 5 +- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index a41a033f..20af05f8 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -323,23 +323,45 @@ AlertModel::headerData(int section, Qt::Orientation orientation, int role) const } void AlertModel::HandleAlert(const types::TextEventKey& alertKey, - size_t messageIndex) + std::size_t messageIndex, + boost::uuids::uuid uuid) { logger_->trace("Handle alert: {}", alertKey.ToString()); double distanceInMeters; - auto alertMessages = p->textEventManager_->message_list(alertKey); + const auto& alertMessages = p->textEventManager_->message_list(alertKey); + + // Find message by UUID instead of index, as the message index could have + // changed between the signal being emitted and the handler being called + auto messageIt = std::find_if(alertMessages.cbegin(), + alertMessages.cend(), + [&uuid](const auto& message) + { return uuid == message->uuid(); }); + + if (messageIt == alertMessages.cend()) + { + logger_->warn("Could not find alert uuid: {} ({})", + alertKey.ToString(), + messageIndex); + return; + } + + auto& message = *messageIt; + + // Store the current message index + messageIndex = static_cast( + std::distance(alertMessages.cbegin(), messageIt)); // Skip alert if this is not the most recent message - if (alertMessages.empty() || messageIndex + 1 < alertMessages.size()) + if (messageIndex + 1 < alertMessages.size()) { return; } // Get the most recent segment for the event - std::shared_ptr alertSegment = - alertMessages[messageIndex]->segments().back(); + const std::shared_ptr alertSegment = + message->segments().back(); p->observedMap_.insert_or_assign(alertKey, alertSegment->observed_); p->threatCategoryMap_.insert_or_assign(alertKey, @@ -519,8 +541,8 @@ std::string AlertModelImpl::GetCounties(const types::TextEventKey& key) } else { - logger_->warn("GetCounties(): No message associated with key: {}", - key.ToString()); + logger_->trace("GetCounties(): No message associated with key: {}", + key.ToString()); return {}; } } @@ -538,8 +560,8 @@ std::string AlertModelImpl::GetState(const types::TextEventKey& key) } else { - logger_->warn("GetState(): No message associated with key: {}", - key.ToString()); + logger_->trace("GetState(): No message associated with key: {}", + key.ToString()); return {}; } } @@ -556,8 +578,8 @@ AlertModelImpl::GetStartTime(const types::TextEventKey& key) } else { - logger_->warn("GetStartTime(): No message associated with key: {}", - key.ToString()); + logger_->trace("GetStartTime(): No message associated with key: {}", + key.ToString()); return {}; } } @@ -581,8 +603,8 @@ AlertModelImpl::GetEndTime(const types::TextEventKey& key) } else { - logger_->warn("GetEndTime(): No message associated with key: {}", - key.ToString()); + logger_->trace("GetEndTime(): No message associated with key: {}", + key.ToString()); return {}; } } diff --git a/scwx-qt/source/scwx/qt/model/alert_model.hpp b/scwx-qt/source/scwx/qt/model/alert_model.hpp index 02781b6b..443ca9bb 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.hpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.hpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace scwx @@ -51,7 +52,9 @@ public: int role = Qt::DisplayRole) const override; public slots: - void HandleAlert(const types::TextEventKey& alertKey, size_t messageIndex); + void HandleAlert(const types::TextEventKey& alertKey, + std::size_t messageIndex, + boost::uuids::uuid uuid); void HandleAlertsRemoved( const std::unordered_set>& From 73355c94247a5e6fafa999b7e3b53f0cf31af661 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 22:49:43 -0500 Subject: [PATCH 542/762] Fix line repopulation on alert removal --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 67 +++++++++++++++------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 786d80e0..495be87c 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -116,7 +117,7 @@ signals: void AlertAdded(const std::shared_ptr& segmentRecord, awips::Phenomenon phenomenon); void AlertUpdated(const std::shared_ptr& segmentRecord); - void AlertsRemoved(awips::Phenomenon phenomenon, bool alertActive); + void AlertsRemoved(awips::Phenomenon phenomenon); void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); }; @@ -201,6 +202,7 @@ public: boost::container::stable_vector< std::shared_ptr>& drawItems); void PopulateLines(bool alertActive); + void RepopulateLines(); void UpdateLines(); static LineData CreateLineData(const settings::LineSettings& lineSettings); @@ -216,6 +218,7 @@ public: const awips::ibw::ImpactBasedWarningInfo& ibw_; std::unique_ptr receiver_ {std::make_unique()}; + std::mutex receiverMutex_ {}; std::unordered_map> geoLines_; @@ -446,9 +449,7 @@ void AlertLayerHandler::HandleAlertsRemoved( { logger_->trace("HandleAlertsRemoved: {} keys", keys.size()); - std::unordered_set, - AlertTypeHash>> - alertsRemoved {}; + std::set alertsRemoved {}; // Take a unique lock before modifying segments std::unique_lock lock {alertMutex_}; @@ -481,7 +482,7 @@ void AlertLayerHandler::HandleAlertsRemoved( } } - alertsRemoved.emplace(key.phenomenon_, alertActive); + alertsRemoved.emplace(key.phenomenon_); } // Remove the key from segmentsByKey_ @@ -495,7 +496,7 @@ void AlertLayerHandler::HandleAlertsRemoved( // Emit signal to notify that alerts have been removed for (auto& alert : alertsRemoved) { - Q_EMIT AlertsRemoved(alert.first, alert.second); + Q_EMIT AlertsRemoved(alert); } } @@ -513,6 +514,9 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() { if (phenomenon == phenomenon_) { + // Only process one signal at a time + const std::unique_lock lock {receiverMutex_}; + AddAlert(segmentRecord); } }); @@ -525,25 +529,27 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() { if (segmentRecord->key_.phenomenon_ == phenomenon_) { + // Only process one signal at a time + const std::unique_lock lock {receiverMutex_}; + UpdateAlert(segmentRecord); } }); - QObject::connect( - &alertLayerHandler, - &AlertLayerHandler::AlertsRemoved, - receiver_.get(), - [&alertLayerHandler, this](awips::Phenomenon phenomenon, bool alertActive) - { - if (phenomenon == phenomenon_) - { - // Take a shared lock to prevent handling additional alerts while - // populating initial lists - const std::shared_lock lock {alertLayerHandler.alertMutex_}; + QObject::connect(&alertLayerHandler, + &AlertLayerHandler::AlertsRemoved, + receiver_.get(), + [this](awips::Phenomenon phenomenon) + { + if (phenomenon == phenomenon_) + { + // Only process one signal at a time + const std::unique_lock lock {receiverMutex_}; - // Re-populate the lines if multiple alerts were removed - PopulateLines(alertActive); - } - }); + // Re-populate the lines if multiple alerts were + // removed + RepopulateLines(); + } + }); } void AlertLayer::Impl::ConnectSignals() @@ -799,6 +805,25 @@ void AlertLayer::Impl::PopulateLines(bool alertActive) geoLines->FinishLines(); } +void AlertLayer::Impl::RepopulateLines() +{ + auto& alertLayerHandler = AlertLayerHandler::Instance(); + + // Take a shared lock to prevent handling additional alerts while populating + // initial lists + const std::shared_lock alertLock {alertLayerHandler.alertMutex_}; + + linesBySegment_.clear(); + segmentsByLine_.clear(); + + for (auto alertActive : {false, true}) + { + PopulateLines(alertActive); + } + + Q_EMIT self_->NeedsRendering(); +} + void AlertLayer::Impl::UpdateLines() { std::unique_lock lock {linesMutex_}; From 3c5b126c67c34976e3b496454f34496ab99662c3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 5 May 2025 23:24:01 -0500 Subject: [PATCH 543/762] Adding const to locks in AlertProxyModel --- scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp index fd5ffc69..4dfeca41 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp @@ -90,7 +90,7 @@ AlertProxyModel::Impl::~Impl() { try { - std::unique_lock lock(alertMutex_); + const std::unique_lock lock(alertMutex_); alertUpdateTimer_.cancel(); } catch (const std::exception& ex) @@ -104,7 +104,7 @@ void AlertProxyModel::Impl::UpdateAlerts() logger_->trace("UpdateAlerts"); // Take a unique lock before modifying feature lists - std::unique_lock lock(alertMutex_); + const std::unique_lock lock(alertMutex_); // Re-evaluate for expired alerts if (alertActiveFilterEnabled_) From 01dbb96f1fdf9f5a19fd1d0c5379ec4387af04e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 13:01:03 +0000 Subject: [PATCH 544/762] Update dependency libpng to v1.6.48 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index ea9c6ab9..d4ed7bd7 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "glm/1.0.1", "gtest/1.16.0", "libcurl/8.12.1", - "libpng/1.6.47", + "libpng/1.6.48", "libxml2/2.13.6", "openssl/3.4.1", "range-v3/0.12.0", From b169f46f5fe514625d69375a0b247e3d42cb6abc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 9 May 2025 19:25:15 -0500 Subject: [PATCH 545/762] Pinch to zoom functionality --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 31 +++++++++++++++++++++++ scwx-qt/source/scwx/qt/map/map_widget.hpp | 16 +++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index f9b898c1..df9f118d 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -169,6 +170,7 @@ public: void HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat); void HandleHotkeyReleased(types::Hotkey hotkey); void HandleHotkeyUpdates(); + void HandlePinchGesture(QPinchGesture* gesture); void ImGuiCheckFonts(); void InitializeCustomStyles(); void InitializeNewRadarProductView(const std::string& colorPalette); @@ -293,6 +295,8 @@ MapWidget::MapWidget(std::size_t id, const QMapLibre::Settings& settings) : setFocusPolicy(Qt::StrongFocus); + grabGesture(Qt::GestureType::PinchGesture); + ImGui_ImplQt_RegisterWidget(this); // Qt parent deals with memory management @@ -603,6 +607,15 @@ void MapWidgetImpl::HandleHotkeyUpdates() } } +void MapWidgetImpl::HandlePinchGesture(QPinchGesture* gesture) +{ + if (gesture->changeFlags() & QPinchGesture::ChangeFlag::ScaleFactorChanged) + { + double scale = gesture->scaleFactor(); + map_->scaleBy(scale, widget_->mapFromGlobal(gesture->centerPoint())); + } +} + common::Level3ProductCategoryMap MapWidget::GetAvailableLevel3Categories() { if (p->radarProductManager_ != nullptr) @@ -1396,6 +1409,16 @@ bool MapWidget::event(QEvent* e) } pickedEventHandler.reset(); + switch (e->type()) + { + case QEvent::Type::Gesture: + gestureEvent(static_cast(e)); + break; + + default: + break; + } + return QOpenGLWidget::event(e); } @@ -1425,6 +1448,14 @@ void MapWidget::keyReleaseEvent(QKeyEvent* ev) } } +void MapWidget::gestureEvent(QGestureEvent* ev) +{ + if (QGesture* pinch = ev->gesture(Qt::PinchGesture)) + { + p->HandlePinchGesture(static_cast(pinch)); + } +} + void MapWidget::mousePressEvent(QMouseEvent* ev) { p->lastPos_ = ev->position(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 6764629e..845832e4 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -137,13 +138,14 @@ private: // QWidget implementation. bool event(QEvent* e) override; - void enterEvent(QEnterEvent* ev) override final; - void keyPressEvent(QKeyEvent* ev) override final; - void keyReleaseEvent(QKeyEvent* ev) override final; - void leaveEvent(QEvent* ev) override final; - void mousePressEvent(QMouseEvent* ev) override final; - void mouseMoveEvent(QMouseEvent* ev) override final; - void wheelEvent(QWheelEvent* ev) override final; + void enterEvent(QEnterEvent* ev) final; + void keyPressEvent(QKeyEvent* ev) final; + void keyReleaseEvent(QKeyEvent* ev) final; + void gestureEvent(QGestureEvent* ev); + void leaveEvent(QEvent* ev) final; + void mousePressEvent(QMouseEvent* ev) final; + void mouseMoveEvent(QMouseEvent* ev) final; + void wheelEvent(QWheelEvent* ev) final; // QOpenGLWidget implementation. void initializeGL() override final; From dcc7a1f637d73e7ce3662e110c900f0e64ea3ce3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 9 May 2025 20:16:13 -0500 Subject: [PATCH 546/762] Pinch to zoom clang-tidy fixes --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index df9f118d..dafa801c 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -611,8 +611,8 @@ void MapWidgetImpl::HandlePinchGesture(QPinchGesture* gesture) { if (gesture->changeFlags() & QPinchGesture::ChangeFlag::ScaleFactorChanged) { - double scale = gesture->scaleFactor(); - map_->scaleBy(scale, widget_->mapFromGlobal(gesture->centerPoint())); + map_->scaleBy(gesture->scaleFactor(), + widget_->mapFromGlobal(gesture->centerPoint())); } } @@ -1412,6 +1412,8 @@ bool MapWidget::event(QEvent* e) switch (e->type()) { case QEvent::Type::Gesture: + // QEvent is always a QGestureEvent + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) gestureEvent(static_cast(e)); break; @@ -1452,6 +1454,8 @@ void MapWidget::gestureEvent(QGestureEvent* ev) { if (QGesture* pinch = ev->gesture(Qt::PinchGesture)) { + // QGesture is always a QPinchGesture + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast) p->HandlePinchGesture(static_cast(pinch)); } } From f0ef6b35dd60c3ef20385f626100f0d40ae8402a Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 27 Mar 2025 11:20:01 -0400 Subject: [PATCH 547/762] Slight rework to nexrad data provider interface --- .../scwx/qt/manager/radar_product_manager.cpp | 13 +++------- .../provider/aws_nexrad_data_provider.hpp | 4 +++ .../scwx/provider/nexrad_data_provider.hpp | 25 +++++++++++++++++++ .../provider/aws_nexrad_data_provider.cpp | 24 ++++++++++++++++++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 48584a2d..c93dd78a 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -769,9 +769,7 @@ void RadarProductManagerImpl::RefreshDataSync( if (totalObjects > 0) { - std::string key = providerManager->provider_->FindLatestKey(); - auto latestTime = providerManager->provider_->GetTimePointByKey(key); - + auto latestTime = providerManager->provider_->FindLatestTime(); auto updatePeriod = providerManager->provider_->update_period(); auto lastModified = providerManager->provider_->last_modified(); auto sinceLastModified = std::chrono::system_clock::now() - lastModified; @@ -951,13 +949,8 @@ void RadarProductManagerImpl::LoadProviderData( if (existingRecord == nullptr) { - std::string key = providerManager->provider_->FindKey(time); - - if (!key.empty()) - { - nexradFile = providerManager->provider_->LoadObjectByKey(key); - } - else + nexradFile = providerManager->provider_->LoadObjectByTime(time); + if (nexradFile == nullptr) { logger_->warn("Attempting to load object without key: {}", scwx::util::TimeString(time)); diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index 462d293d..cb71f27b 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -39,12 +39,16 @@ public: std::string FindKey(std::chrono::system_clock::time_point time) override; std::string FindLatestKey() override; + std::chrono::system_clock::time_point FindLatestTime() override; std::vector GetTimePointsByDate(std::chrono::system_clock::time_point date) override; std::tuple ListObjects(std::chrono::system_clock::time_point date) override; std::shared_ptr LoadObjectByKey(const std::string& key) override; + std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) override; + std::shared_ptr LoadLatestObject() override; std::pair Refresh() override; protected: diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index 14a75815..81edc1eb 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -59,6 +59,13 @@ public: */ virtual std::string FindLatestKey() = 0; + /** + * Finds the most recent time in the cache. + * + * @return NEXRAD data key + */ + virtual std::chrono::system_clock::time_point FindLatestTime() = 0; + /** * Lists NEXRAD objects for the date supplied, and adds them to the cache. * @@ -81,6 +88,24 @@ public: virtual std::shared_ptr LoadObjectByKey(const std::string& key) = 0; + /** + * Loads a NEXRAD file object at the given time + * + * @param time NEXRAD time + * + * @return NEXRAD data + */ + virtual std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) = 0; + + /** + * Loads the latest NEXRAD file object + * + * @return NEXRAD data + */ + virtual std::shared_ptr + LoadLatestObject() = 0; + /** * Lists NEXRAD objects for the current date, and adds them to the cache. If * no objects have been added to the cache for the current date, the previous diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index c4ac523b..74740be0 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -170,6 +170,11 @@ std::string AwsNexradDataProvider::FindLatestKey() return key; } +std::chrono::system_clock::time_point AwsNexradDataProvider::FindLatestTime() +{ + return GetTimePointByKey(FindLatestKey()); +} + std::vector AwsNexradDataProvider::GetTimePointsByDate( std::chrono::system_clock::time_point date) @@ -327,6 +332,25 @@ AwsNexradDataProvider::LoadObjectByKey(const std::string& key) return nexradFile; } +std::shared_ptr AwsNexradDataProvider::LoadObjectByTime( + std::chrono::system_clock::time_point time) +{ + const std::string key = FindKey(time); + if (key.empty()) + { + return nullptr; + } + else + { + return LoadObjectByKey(key); + } +} + +std::shared_ptr AwsNexradDataProvider::LoadLatestObject() +{ + return LoadObjectByKey(FindLatestKey()); +} + std::pair AwsNexradDataProvider::Refresh() { using namespace std::chrono; From 05335fad8473b342bf3ff313f6828c8e521ea67c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 27 Mar 2025 11:21:08 -0400 Subject: [PATCH 548/762] Add ability to load new LDM records into preexisting Ar2vFile objects for L2 chunks --- wxdata/include/scwx/wsr88d/ar2v_file.hpp | 3 +++ wxdata/source/scwx/wsr88d/ar2v_file.cpp | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/wxdata/include/scwx/wsr88d/ar2v_file.hpp b/wxdata/include/scwx/wsr88d/ar2v_file.hpp index 1f3ab0cc..34d50b32 100644 --- a/wxdata/include/scwx/wsr88d/ar2v_file.hpp +++ b/wxdata/include/scwx/wsr88d/ar2v_file.hpp @@ -53,6 +53,9 @@ public: bool LoadFile(const std::string& filename); bool LoadData(std::istream& is); + bool LoadLDMRecords(std::istream& is); + bool IndexFile(); + private: std::unique_ptr p; }; diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index ed976c24..db04feba 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -519,5 +519,25 @@ void Ar2vFileImpl::IndexFile() } } +bool Ar2vFile::LoadLDMRecords(std::istream& is) { + size_t decompressedRecords = p->DecompressLDMRecords(is); + if (decompressedRecords == 0) + { + p->ParseLDMRecord(is); + } + else + { + p->ParseLDMRecords(); + } + + return true; +} + +bool Ar2vFile::IndexFile() +{ + p->IndexFile(); + return true; +} + } // namespace wsr88d } // namespace scwx From 9570dcf20e26bfd2db576ff871f9a2121684cd23 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 27 Mar 2025 11:22:44 -0400 Subject: [PATCH 549/762] Initial working level2 chunks data provider --- .../aws_level2_chunks_data_provider.hpp | 62 +++ .../aws_level2_chunks_data_provider.cpp | 490 ++++++++++++++++++ wxdata/wxdata.cmake | 2 + 3 files changed, 554 insertions(+) create mode 100644 wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp create mode 100644 wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp new file mode 100644 index 00000000..247ff346 --- /dev/null +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +namespace Aws::S3 +{ +class S3Client; +} // namespace Aws::S3 + +namespace scwx::provider +{ + +/** + * @brief AWS Level 2 Data Provider + */ +class AwsLevel2ChunksDataProvider : public NexradDataProvider +{ +public: + explicit AwsLevel2ChunksDataProvider(const std::string& radarSite); + explicit AwsLevel2ChunksDataProvider(const std::string& radarSite, + const std::string& bucketName, + const std::string& region); + ~AwsLevel2ChunksDataProvider() override; + + AwsLevel2ChunksDataProvider(const AwsLevel2ChunksDataProvider&) = delete; + AwsLevel2ChunksDataProvider& operator=(const AwsLevel2ChunksDataProvider&) = delete; + + AwsLevel2ChunksDataProvider(AwsLevel2ChunksDataProvider&&) noexcept; + AwsLevel2ChunksDataProvider& operator=(AwsLevel2ChunksDataProvider&&) noexcept; + + [[nodiscard]] std::chrono::system_clock::time_point + GetTimePointByKey(const std::string& key) const override; + + [[nodiscard]] size_t cache_size() const override; + + [[nodiscard]] std::chrono::system_clock::time_point + last_modified() const override; + [[nodiscard]] std::chrono::seconds update_period() const override; + + std::string FindKey(std::chrono::system_clock::time_point time) override; + std::string FindLatestKey() override; + std::chrono::system_clock::time_point FindLatestTime() override; + std::vector + GetTimePointsByDate(std::chrono::system_clock::time_point date) override; + std::tuple + ListObjects(std::chrono::system_clock::time_point date) override; + std::shared_ptr + LoadObjectByKey(const std::string& key) override; + std::shared_ptr + LoadObjectByTime(std::chrono::system_clock::time_point time) override; + std::shared_ptr LoadLatestObject() override; + std::pair Refresh() override; + + void RequestAvailableProducts() override; + std::vector GetAvailableProducts() override; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp new file mode 100644 index 00000000..4d641d8a --- /dev/null +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -0,0 +1,490 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#if (__cpp_lib_chrono < 201907L) +# include +#endif + +namespace scwx::provider +{ + +static const std::string logPrefix_ = + "scwx::provider::aws_level2_chunks_data_provider"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +static const std::string kDefaultBucketName_ = "unidata-nexrad-level2-chunks"; +static const std::string kDefaultRegion_ = "us-east-1"; + +class AwsLevel2ChunksDataProvider::Impl +{ +public: + struct ScanRecord + { + explicit ScanRecord(std::string prefix) : + prefix_ {std::move(prefix)}, + nexradFile_ {}, + lastModified_ {}, + secondLastModified_ {} + { + } + ~ScanRecord() = default; + ScanRecord(const ScanRecord&) = default; + ScanRecord(ScanRecord&&) = default; + ScanRecord& operator=(const ScanRecord&) = default; + ScanRecord& operator=(ScanRecord&&) = default; + + std::string prefix_; + std::shared_ptr nexradFile_; + std::chrono::system_clock::time_point lastModified_; + std::chrono::system_clock::time_point secondLastModified_; + int nextFile_{1}; + bool hasAllFiles_{false}; + }; + + explicit Impl(AwsLevel2ChunksDataProvider* self, + std::string radarSite, + std::string bucketName, + std::string region) : + radarSite_ {std::move(radarSite)}, + bucketName_ {std::move(bucketName)}, + region_ {std::move(region)}, + client_ {nullptr}, + scans_ {}, + scansMutex_ {}, + lastModified_ {}, + updatePeriod_ {}, + self_ {self} + { + // Disable HTTP request for region + util::SetEnvironment("AWS_EC2_METADATA_DISABLED", "true"); + + // Use anonymous credentials + Aws::Auth::AWSCredentials credentials {}; + + Aws::Client::ClientConfiguration config; + config.region = region_; + config.connectTimeoutMs = 10000; + + client_ = std::make_shared( + credentials, + Aws::MakeShared( + Aws::S3::S3Client::GetAllocationTag()), + config); + } + ~Impl() = default; + Impl(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(const Impl&) = delete; + Impl& operator=(Impl&&) = delete; + + std::chrono::system_clock::time_point GetScanTime(const std::string& prefix); + std::string GetScanKey(const std::string& prefix, + const std::chrono::system_clock::time_point& time, + int last); + std::shared_ptr LoadScan(Impl::ScanRecord& scanRecord); + + std::string radarSite_; + std::string bucketName_; + std::string region_; + std::shared_ptr client_; + + std::mutex refreshMutex_; + + std::map scans_; + std::shared_mutex scansMutex_; + + std::chrono::system_clock::time_point lastModified_; + std::chrono::seconds updatePeriod_; + + AwsLevel2ChunksDataProvider* self_; + }; + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + const std::string& radarSite) : + AwsLevel2ChunksDataProvider(radarSite, kDefaultBucketName_, kDefaultRegion_) +{ +} + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + const std::string& radarSite, + const std::string& bucketName, + const std::string& region) : + p(std::make_unique(this, radarSite, bucketName, region)) +{ +} + +AwsLevel2ChunksDataProvider::~AwsLevel2ChunksDataProvider() = default; + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::GetTimePointByKey(const std::string& key) const +{ + std::chrono::system_clock::time_point time {}; + + const size_t lastSeparator = key.rfind('/'); + const size_t offset = + (lastSeparator == std::string::npos) ? 0 : lastSeparator + 1; + + // Filename format is YYYYMMDD-TTTTTT-AAA-B + static const size_t formatSize = std::string("YYYYMMDD-TTTTTT").size(); + + if (key.size() >= offset + formatSize) + { + using namespace std::chrono; + +#if (__cpp_lib_chrono < 201907L) + using namespace date; +#endif + + static const std::string timeFormat {"%Y%m%d-%H%M%S"}; + + std::string timeStr {key.substr(offset, formatSize)}; + std::istringstream in {timeStr}; + in >> parse(timeFormat, time); + + if (in.fail()) + { + logger_->warn("Invalid time: \"{}\"", timeStr); + } + } + else + { + logger_->warn("Time not parsable from key: \"{}\"", key); + } + + return time; +} + +size_t AwsLevel2ChunksDataProvider::cache_size() const +{ + return p->scans_.size(); +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::last_modified() const +{ + return p->lastModified_; +} +std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const +{ + return p->updatePeriod_; +} + +std::string +AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) +{ + logger_->debug("FindKey: {}", util::TimeString(time)); + + std::shared_lock lock(p->scansMutex_); + + auto element = util::GetBoundedElement(p->scans_, time); + + if (element.has_value()) + { + return element->prefix_; + } + + return {}; +} + +std::string AwsLevel2ChunksDataProvider::FindLatestKey() +{ + std::shared_lock lock(p->scansMutex_); + if (p->scans_.empty()) + { + return ""; + } + + return p->scans_.crbegin()->second.prefix_; +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::FindLatestTime() +{ + std::shared_lock lock(p->scansMutex_); + if (p->scans_.empty()) + { + return {}; + } + + return p->scans_.crbegin()->first; +} + +std::vector +AwsLevel2ChunksDataProvider::GetTimePointsByDate( + std::chrono::system_clock::time_point /*date*/) +{ + return {}; +} + +std::chrono::system_clock::time_point +AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) +{ + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + request.SetMaxKeys(1); + + auto outcome = client_->ListObjectsV2(request); + if (outcome.IsSuccess()) + { + return self_->GetTimePointByKey( + outcome.GetResult().GetContents().at(0).GetKey()); + } + + return {}; +} + +std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( + const std::string& prefix, + const std::chrono::system_clock::time_point& time, + int last) +{ + + static const std::string timeFormat {"%Y%m%d-%H%M%S"}; + + //TODO + return fmt::format( + "{0}/{1:%Y%m%d-%H%M%S}-{2}", prefix, fmt::gmtime(time), last - 1); +} + +std::tuple +AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) +{ + // TODO this is slow. It could probably be speed up by not reloading every + // scan every time. + const std::string prefix = p->radarSite_ + "/"; + + logger_->debug("ListObjects: {}", prefix); + + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(p->bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + + auto outcome = p->client_->ListObjectsV2(request); + + size_t newObjects = 0; + size_t totalObjects = 0; + + if (outcome.IsSuccess()) + { + auto& scans = outcome.GetResult().GetCommonPrefixes(); + logger_->debug("Found {} scans", scans.size()); + + for (const auto& scan : scans) + { + const std::string& prefix = scan.GetPrefix(); + + auto time = p->GetScanTime(prefix); + + if (!p->scans_.contains(time)) + { + p->scans_.insert_or_assign(time, Impl::ScanRecord {prefix}); + newObjects++; + } + + totalObjects++; + } + } + + return {outcome.IsSuccess(), newObjects, totalObjects}; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) +{ + return nullptr; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) +{ + if (scanRecord.hasAllFiles_) + { + return scanRecord.nexradFile_; + } + + // TODO can get only new records using scanRecords last + Aws::S3::Model::ListObjectsV2Request listRequest; + listRequest.SetBucket(bucketName_); + listRequest.SetPrefix(scanRecord.prefix_); + listRequest.SetDelimiter("/"); + + auto listOutcome = client_->ListObjectsV2(listRequest); + if (!listOutcome.IsSuccess()) + { + logger_->warn("Could not find scan at {}", scanRecord.prefix_); + return nullptr; + } + + auto& chunks = listOutcome.GetResult().GetContents(); + for (const auto& chunk : chunks) + { + const std::string& key = chunk.GetKey(); + + // We just want the number of this chunk for now + // KIND/585/20250324-134727-001-S + constexpr size_t startNumberPos = + std::string("KIND/585/20250324-134727-").size(); + const std::string& keyNumberStr = key.substr(startNumberPos, 3); + const int keyNumber = std::stoi(keyNumberStr); + if (keyNumber != scanRecord.nextFile_) + { + continue; + } + + // Now we want the ending char + // KIND/585/20250324-134727-001-S + constexpr size_t charPos = + std::string("KIND/585/20250324-134727-001-").size(); + const char keyChar = key[charPos]; + + Aws::S3::Model::GetObjectRequest objectRequest; + objectRequest.SetBucket(bucketName_); + objectRequest.SetKey(key); + + auto outcome = client_->GetObject(objectRequest); + + if (!outcome.IsSuccess()) + { + logger_->warn("Could not get object: {}", + outcome.GetError().GetMessage()); + return nullptr; + } + + auto& body = outcome.GetResultWithOwnership().GetBody(); + + switch (keyChar) { + case 'S': + { // First chunk + scanRecord.nexradFile_ = std::make_shared(); + if (!scanRecord.nexradFile_->LoadData(body)) + { + logger_->warn("Failed to load first chunk"); + return nullptr; + } + break; + } + case 'I': + { // Middle chunk + if (!scanRecord.nexradFile_->LoadLDMRecords(body)) + { + logger_->warn("Failed to load middle chunk"); + return nullptr; + } + break; + } + case 'E': + { // Last chunk + if (!scanRecord.nexradFile_->LoadLDMRecords(body)) + { + logger_->warn("Failed to load last chunk"); + return nullptr; + } + scanRecord.hasAllFiles_ = true; + break; + } + default: + return nullptr; + } + + std::chrono::seconds lastModifiedSeconds { + outcome.GetResult().GetLastModified().Seconds()}; + std::chrono::system_clock::time_point lastModified { + lastModifiedSeconds}; + + scanRecord.secondLastModified_ = scanRecord.lastModified_; + scanRecord.lastModified_ = lastModified; + + scanRecord.nextFile_ += 1; + } + scanRecord.nexradFile_->IndexFile(); + + if (!scans_.empty()) + { + auto& lastScan = scans_.crend()->second; + lastModified_ = lastScan.lastModified_; + if (lastScan.secondLastModified_ != + std::chrono::system_clock::time_point()) + { + auto delta = lastScan.lastModified_ - lastScan.secondLastModified_; + updatePeriod_ = + std::chrono::duration_cast(delta); + } + } + + return scanRecord.nexradFile_; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadObjectByTime( + std::chrono::system_clock::time_point time) +{ + std::shared_lock lock(p->scansMutex_); + + logger_->error("LoadObjectByTime({})", time); + + auto scanRecord = util::GetBoundedElementPointer(p->scans_, time); + if (scanRecord == nullptr) + { + logger_->warn("Could not find object at time {}", time); + return nullptr; + } + + // The scanRecord must be a reference + return p->LoadScan(p->scans_.at(scanRecord->first)); +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadLatestObject() +{ + return LoadObjectByTime(FindLatestTime()); +} + +std::pair AwsLevel2ChunksDataProvider::Refresh() +{ + using namespace std::chrono; + + std::unique_lock lock(p->refreshMutex_); + + auto [success, newObjects, totalObjects] = ListObjects({}); + + for (auto& scanRecord : p->scans_) + { + if (scanRecord.second.nexradFile_ != nullptr) + { + p->LoadScan(scanRecord.second); + newObjects += 1; + } + } + + return std::make_pair(newObjects, totalObjects); +} + +void AwsLevel2ChunksDataProvider::RequestAvailableProducts() {} +std::vector AwsLevel2ChunksDataProvider::GetAvailableProducts() +{ + return {}; +} + +AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( + AwsLevel2ChunksDataProvider&&) noexcept = default; +AwsLevel2ChunksDataProvider& AwsLevel2ChunksDataProvider::operator=( + AwsLevel2ChunksDataProvider&&) noexcept = default; + +} // namespace scwx::provider diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 8d2e15b4..2c062f4b 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -60,6 +60,7 @@ set(HDR_NETWORK include/scwx/network/cpr.hpp set(SRC_NETWORK source/scwx/network/cpr.cpp source/scwx/network/dir_list.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp + include/scwx/provider/aws_level2_chunks_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp include/scwx/provider/aws_nexrad_data_provider.hpp include/scwx/provider/iem_api_provider.hpp @@ -68,6 +69,7 @@ set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/nexrad_data_provider_factory.hpp include/scwx/provider/warnings_provider.hpp) set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp + source/scwx/provider/aws_level2_chunks_data_provider.cpp source/scwx/provider/aws_level3_data_provider.cpp source/scwx/provider/aws_nexrad_data_provider.cpp source/scwx/provider/iem_api_provider.cpp From 7fef5789de4e5343912eb447dd4e826d45d51cf8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 27 Mar 2025 11:23:25 -0400 Subject: [PATCH 550/762] Temporarly use only the L2 Chunks data provider for L2 data --- wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp index 5e75fd96..83ccb1a3 100644 --- a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp +++ b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp @@ -1,5 +1,6 @@ #include -#include +//#include +#include #include namespace scwx @@ -14,7 +15,7 @@ std::shared_ptr NexradDataProviderFactory::CreateLevel2DataProvider( const std::string& radarSite) { - return std::make_unique(radarSite); + return std::make_unique(radarSite); } std::shared_ptr From ac12cce5f24d0aa1cf2294fb2ea291a3dbe08df7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 27 Mar 2025 12:07:49 -0400 Subject: [PATCH 551/762] fix compilation errors with level_2_chunks for not gcc-13/clang-17 --- .../aws_level2_chunks_data_provider.hpp | 12 +++--- .../aws_level2_chunks_data_provider.cpp | 43 +++++++++---------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 247ff346..57e8e301 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -23,10 +23,12 @@ public: ~AwsLevel2ChunksDataProvider() override; AwsLevel2ChunksDataProvider(const AwsLevel2ChunksDataProvider&) = delete; - AwsLevel2ChunksDataProvider& operator=(const AwsLevel2ChunksDataProvider&) = delete; + AwsLevel2ChunksDataProvider& + operator=(const AwsLevel2ChunksDataProvider&) = delete; AwsLevel2ChunksDataProvider(AwsLevel2ChunksDataProvider&&) noexcept; - AwsLevel2ChunksDataProvider& operator=(AwsLevel2ChunksDataProvider&&) noexcept; + AwsLevel2ChunksDataProvider& + operator=(AwsLevel2ChunksDataProvider&&) noexcept; [[nodiscard]] std::chrono::system_clock::time_point GetTimePointByKey(const std::string& key) const override; @@ -45,13 +47,13 @@ public: std::tuple ListObjects(std::chrono::system_clock::time_point date) override; std::shared_ptr - LoadObjectByKey(const std::string& key) override; + LoadObjectByKey(const std::string& key) override; std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; std::shared_ptr LoadLatestObject() override; - std::pair Refresh() override; + std::pair Refresh() override; - void RequestAvailableProducts() override; + void RequestAvailableProducts() override; std::vector GetAvailableProducts() override; private: diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 4d641d8a..5dccf149 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -21,7 +21,6 @@ namespace scwx::provider { - static const std::string logPrefix_ = "scwx::provider::aws_level2_chunks_data_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); @@ -47,12 +46,12 @@ public: ScanRecord& operator=(const ScanRecord&) = default; ScanRecord& operator=(ScanRecord&&) = default; - std::string prefix_; - std::shared_ptr nexradFile_; + std::string prefix_; + std::shared_ptr nexradFile_; std::chrono::system_clock::time_point lastModified_; std::chrono::system_clock::time_point secondLastModified_; - int nextFile_{1}; - bool hasAllFiles_{false}; + int nextFile_ {1}; + bool hasAllFiles_ {false}; }; explicit Impl(AwsLevel2ChunksDataProvider* self, @@ -92,9 +91,9 @@ public: Impl& operator=(Impl&&) = delete; std::chrono::system_clock::time_point GetScanTime(const std::string& prefix); - std::string GetScanKey(const std::string& prefix, - const std::chrono::system_clock::time_point& time, - int last); + std::string GetScanKey(const std::string& prefix, + const std::chrono::system_clock::time_point& time, + int last); std::shared_ptr LoadScan(Impl::ScanRecord& scanRecord); std::string radarSite_; @@ -111,7 +110,7 @@ public: std::chrono::seconds updatePeriod_; AwsLevel2ChunksDataProvider* self_; - }; +}; AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( const std::string& radarSite) : @@ -225,7 +224,7 @@ AwsLevel2ChunksDataProvider::FindLatestTime() std::vector AwsLevel2ChunksDataProvider::GetTimePointsByDate( - std::chrono::system_clock::time_point /*date*/) + std::chrono::system_clock::time_point /*date*/) { return {}; } @@ -254,10 +253,10 @@ std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( const std::chrono::system_clock::time_point& time, int last) { - + static const std::string timeFormat {"%Y%m%d-%H%M%S"}; - //TODO + // TODO return fmt::format( "{0}/{1:%Y%m%d-%H%M%S}-{2}", prefix, fmt::gmtime(time), last - 1); } @@ -288,13 +287,13 @@ AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) for (const auto& scan : scans) { - const std::string& prefix = scan.GetPrefix(); + const std::string& prefixScan = scan.GetPrefix(); - auto time = p->GetScanTime(prefix); + auto time = p->GetScanTime(prefixScan); if (!p->scans_.contains(time)) { - p->scans_.insert_or_assign(time, Impl::ScanRecord {prefix}); + p->scans_.insert_or_assign(time, Impl::ScanRecord {prefixScan}); newObjects++; } @@ -306,7 +305,7 @@ AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) } std::shared_ptr -AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) +AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) { return nullptr; } @@ -339,10 +338,10 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) // We just want the number of this chunk for now // KIND/585/20250324-134727-001-S - constexpr size_t startNumberPos = + static const size_t startNumberPos = std::string("KIND/585/20250324-134727-").size(); const std::string& keyNumberStr = key.substr(startNumberPos, 3); - const int keyNumber = std::stoi(keyNumberStr); + const int keyNumber = std::stoi(keyNumberStr); if (keyNumber != scanRecord.nextFile_) { continue; @@ -350,7 +349,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) // Now we want the ending char // KIND/585/20250324-134727-001-S - constexpr size_t charPos = + static const size_t charPos = std::string("KIND/585/20250324-134727-001-").size(); const char keyChar = key[charPos]; @@ -369,7 +368,8 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) auto& body = outcome.GetResultWithOwnership().GetBody(); - switch (keyChar) { + switch (keyChar) + { case 'S': { // First chunk scanRecord.nexradFile_ = std::make_shared(); @@ -405,8 +405,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) std::chrono::seconds lastModifiedSeconds { outcome.GetResult().GetLastModified().Seconds()}; - std::chrono::system_clock::time_point lastModified { - lastModifiedSeconds}; + std::chrono::system_clock::time_point lastModified {lastModifiedSeconds}; scanRecord.secondLastModified_ = scanRecord.lastModified_; scanRecord.lastModified_ = lastModified; From 7c99bbc185cf92a54dcc538151adc734de23694b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 28 Mar 2025 11:06:11 -0400 Subject: [PATCH 552/762] Begin work on moving over to only storing last 2 scans in chunks --- .../aws_level2_chunks_data_provider.cpp | 255 ++++++++++++------ 1 file changed, 166 insertions(+), 89 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 5dccf149..236b97ee 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -33,11 +34,12 @@ class AwsLevel2ChunksDataProvider::Impl public: struct ScanRecord { - explicit ScanRecord(std::string prefix) : + explicit ScanRecord(std::string prefix, bool valid = true) : + valid_ {valid}, prefix_ {std::move(prefix)}, nexradFile_ {}, lastModified_ {}, - secondLastModified_ {} + lastKey_ {""} { } ~ScanRecord() = default; @@ -46,10 +48,12 @@ public: ScanRecord& operator=(const ScanRecord&) = default; ScanRecord& operator=(ScanRecord&&) = default; + bool valid_; std::string prefix_; std::shared_ptr nexradFile_; + std::chrono::system_clock::time_point time_; std::chrono::system_clock::time_point lastModified_; - std::chrono::system_clock::time_point secondLastModified_; + std::string lastKey_; int nextFile_ {1}; bool hasAllFiles_ {false}; }; @@ -62,10 +66,11 @@ public: bucketName_ {std::move(bucketName)}, region_ {std::move(region)}, client_ {nullptr}, - scans_ {}, + scanTimes_ {}, + lastScan_ {"", false}, + currentScan_ {"", false}, scansMutex_ {}, - lastModified_ {}, - updatePeriod_ {}, + updatePeriod_ {15}, self_ {self} { // Disable HTTP request for region @@ -95,6 +100,7 @@ public: const std::chrono::system_clock::time_point& time, int last); std::shared_ptr LoadScan(Impl::ScanRecord& scanRecord); + int GetScanNumber(const std::string& prefix); std::string radarSite_; std::string bucketName_; @@ -103,11 +109,13 @@ public: std::mutex refreshMutex_; - std::map scans_; - std::shared_mutex scansMutex_; + std::unordered_map + scanTimes_; + ScanRecord lastScan_; + ScanRecord currentScan_; + std::shared_mutex scansMutex_; - std::chrono::system_clock::time_point lastModified_; - std::chrono::seconds updatePeriod_; + std::chrono::seconds updatePeriod_; AwsLevel2ChunksDataProvider* self_; }; @@ -169,13 +177,13 @@ AwsLevel2ChunksDataProvider::GetTimePointByKey(const std::string& key) const size_t AwsLevel2ChunksDataProvider::cache_size() const { - return p->scans_.size(); + return 2; } std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::last_modified() const { - return p->lastModified_; + return p->currentScan_.lastModified_; } std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const { @@ -188,12 +196,13 @@ AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) logger_->debug("FindKey: {}", util::TimeString(time)); std::shared_lock lock(p->scansMutex_); - - auto element = util::GetBoundedElement(p->scans_, time); - - if (element.has_value()) + if (p->currentScan_.valid_ && time >= p->currentScan_.time_) { - return element->prefix_; + return p->currentScan_.prefix_; + } + else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) + { + return p->lastScan_.prefix_; } return {}; @@ -202,24 +211,24 @@ AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) std::string AwsLevel2ChunksDataProvider::FindLatestKey() { std::shared_lock lock(p->scansMutex_); - if (p->scans_.empty()) + if (!p->currentScan_.valid_) { return ""; } - return p->scans_.crbegin()->second.prefix_; + return p->currentScan_.prefix_; } std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::FindLatestTime() { std::shared_lock lock(p->scansMutex_); - if (p->scans_.empty()) + if (!p->currentScan_.valid_) { return {}; } - return p->scans_.crbegin()->first; + return p->currentScan_.time_; } std::vector @@ -232,6 +241,12 @@ AwsLevel2ChunksDataProvider::GetTimePointsByDate( std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) { + const auto& scanTimeIt = scanTimes_.find(prefix); // O(log(n)) + if (scanTimeIt != scanTimes_.cend()) + { + return scanTimeIt->second; + } + Aws::S3::Model::ListObjectsV2Request request; request.SetBucket(bucketName_); request.SetPrefix(prefix); @@ -241,8 +256,9 @@ AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) auto outcome = client_->ListObjectsV2(request); if (outcome.IsSuccess()) { - return self_->GetTimePointByKey( + auto timePoint = self_->GetTimePointByKey( outcome.GetResult().GetContents().at(0).GetKey()); + return timePoint; } return {}; @@ -264,44 +280,7 @@ std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( std::tuple AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) { - // TODO this is slow. It could probably be speed up by not reloading every - // scan every time. - const std::string prefix = p->radarSite_ + "/"; - - logger_->debug("ListObjects: {}", prefix); - - Aws::S3::Model::ListObjectsV2Request request; - request.SetBucket(p->bucketName_); - request.SetPrefix(prefix); - request.SetDelimiter("/"); - - auto outcome = p->client_->ListObjectsV2(request); - - size_t newObjects = 0; - size_t totalObjects = 0; - - if (outcome.IsSuccess()) - { - auto& scans = outcome.GetResult().GetCommonPrefixes(); - logger_->debug("Found {} scans", scans.size()); - - for (const auto& scan : scans) - { - const std::string& prefixScan = scan.GetPrefix(); - - auto time = p->GetScanTime(prefixScan); - - if (!p->scans_.contains(time)) - { - p->scans_.insert_or_assign(time, Impl::ScanRecord {prefixScan}); - newObjects++; - } - - totalObjects++; - } - } - - return {outcome.IsSuccess(), newObjects, totalObjects}; + return {true, 0, 0}; } std::shared_ptr @@ -313,16 +292,23 @@ AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) std::shared_ptr AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) { - if (scanRecord.hasAllFiles_) + if (!scanRecord.valid_) + { + return nullptr; + } + else if (scanRecord.hasAllFiles_) { return scanRecord.nexradFile_; } - // TODO can get only new records using scanRecords last Aws::S3::Model::ListObjectsV2Request listRequest; listRequest.SetBucket(bucketName_); listRequest.SetPrefix(scanRecord.prefix_); listRequest.SetDelimiter("/"); + if (!scanRecord.lastKey_.empty()) + { + listRequest.SetStartAfter(scanRecord.lastKey_); + } auto listOutcome = client_->ListObjectsV2(listRequest); if (!listOutcome.IsSuccess()) @@ -336,6 +322,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) { const std::string& key = chunk.GetKey(); + // TODO this is wrong, 1st number can be 1-3 digits // We just want the number of this chunk for now // KIND/585/20250324-134727-001-S static const size_t startNumberPos = @@ -347,6 +334,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) continue; } + // TODO this is wrong, 1st number can be 1-3 digits // Now we want the ending char // KIND/585/20250324-134727-001-S static const size_t charPos = @@ -407,24 +395,15 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) outcome.GetResult().GetLastModified().Seconds()}; std::chrono::system_clock::time_point lastModified {lastModifiedSeconds}; - scanRecord.secondLastModified_ = scanRecord.lastModified_; - scanRecord.lastModified_ = lastModified; + scanRecord.lastModified_ = lastModified; scanRecord.nextFile_ += 1; + scanRecord.lastKey_ = key; } - scanRecord.nexradFile_->IndexFile(); - if (!scans_.empty()) + if (scanRecord.nexradFile_ != nullptr) { - auto& lastScan = scans_.crend()->second; - lastModified_ = lastScan.lastModified_; - if (lastScan.secondLastModified_ != - std::chrono::system_clock::time_point()) - { - auto delta = lastScan.lastModified_ - lastScan.secondLastModified_; - updatePeriod_ = - std::chrono::duration_cast(delta); - } + scanRecord.nexradFile_->IndexFile(); } return scanRecord.nexradFile_; @@ -434,19 +413,20 @@ std::shared_ptr AwsLevel2ChunksDataProvider::LoadObjectByTime( std::chrono::system_clock::time_point time) { - std::shared_lock lock(p->scansMutex_); + std::unique_lock lock(p->scansMutex_); - logger_->error("LoadObjectByTime({})", time); - - auto scanRecord = util::GetBoundedElementPointer(p->scans_, time); - if (scanRecord == nullptr) + if (p->currentScan_.valid_ && time >= p->currentScan_.time_) + { + return p->LoadScan(p->currentScan_); + } + else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) + { + return p->LoadScan(p->lastScan_); + } + else { - logger_->warn("Could not find object at time {}", time); return nullptr; } - - // The scanRecord must be a reference - return p->LoadScan(p->scans_.at(scanRecord->first)); } std::shared_ptr @@ -455,23 +435,120 @@ AwsLevel2ChunksDataProvider::LoadLatestObject() return LoadObjectByTime(FindLatestTime()); } +int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) +{ + + // We just want the number of this chunk for now + // KIND/585/20250324-134727-001-S + static const size_t startNumberPos = std::string("KIND/").size(); + const std::string& prefixNumberStr = prefix.substr(startNumberPos, 3); + return std::stoi(prefixNumberStr); +} + std::pair AwsLevel2ChunksDataProvider::Refresh() { using namespace std::chrono; std::unique_lock lock(p->refreshMutex_); + std::unique_lock scanLock(p->scansMutex_); - auto [success, newObjects, totalObjects] = ListObjects({}); - for (auto& scanRecord : p->scans_) + size_t newObjects = 0; + size_t totalObjects = 0; + + const std::string prefix = p->radarSite_ + "/"; + + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(p->bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + + auto outcome = p->client_->ListObjectsV2(request); + + + if (outcome.IsSuccess()) { - if (scanRecord.second.nexradFile_ != nullptr) + auto& scans = outcome.GetResult().GetCommonPrefixes(); + logger_->debug("Found {} scans", scans.size()); + + boost::timer::cpu_timer timer {}; + timer.start(); + if (scans.size() > 0) { - p->LoadScan(scanRecord.second); - newObjects += 1; + + // TODO this cannot be done by getting things form the network. + // Use index number instead. + + // find latest scan + std::chrono::system_clock::time_point latestTime = {}; + std::chrono::system_clock::time_point secondLatestTime = {}; + size_t latestIndex = 0; + size_t secondLatestIndex = 0; + + + for (size_t i = 0; i < scans.size(); i++) // O(n log(n)) n <= 999 + { + auto time = p->GetScanTime(scans[i].GetPrefix()); + if (time > latestTime) + { + secondLatestTime = latestTime; + latestTime = time; + secondLatestIndex = latestIndex; + latestIndex = i; + } + } + + const auto& last = scans.at(secondLatestIndex).GetPrefix(); + if (secondLatestTime != std::chrono::system_clock::time_point {}) + { + p->lastScan_ = p->currentScan_; + } + else if (!p->lastScan_.valid_ || p->lastScan_.prefix_ != last) + { + p->lastScan_.valid_ = true; + p->lastScan_.prefix_ = last; + p->lastScan_.nexradFile_ = nullptr; + p->lastScan_.time_ = secondLatestTime; + p->lastScan_.lastModified_ = {}; + p->lastScan_.lastKey_ = ""; + p->lastScan_.nextFile_ = 1; + p->lastScan_.hasAllFiles_ = false; + newObjects += 1; + } + + const auto& current = scans.at(latestIndex).GetPrefix(); + if (!p->currentScan_.valid_ || p->currentScan_.prefix_ != current) + { + p->currentScan_.valid_ = true; + p->currentScan_.prefix_ = current; + p->currentScan_.nexradFile_ = nullptr; + p->currentScan_.time_ = latestTime; + p->currentScan_.lastModified_ = {}; + p->currentScan_.lastKey_ = ""; + p->currentScan_.nextFile_ = 1; + p->currentScan_.hasAllFiles_ = false; + newObjects += 1; + } } + + timer.stop(); + logger_->debug("Updated current scans in {}", timer.format(6, "%ws")); } + logger_->debug("Loading scans"); + + if (p->currentScan_.valid_) + { + p->LoadScan(p->currentScan_); + totalObjects += 1; + } + if (p->lastScan_.valid_) + { + p->LoadScan(p->lastScan_); + totalObjects += 1; + } + + return std::make_pair(newObjects, totalObjects); } From fc83a7a36f35862378a16384fb241f311cca052b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 30 Mar 2025 11:24:42 -0400 Subject: [PATCH 553/762] working level2 chunks with auto rerendering --- .../scwx/qt/manager/radar_product_manager.cpp | 12 +- .../scwx/qt/view/level2_product_view.cpp | 3 +- .../aws_level2_chunks_data_provider.cpp | 342 +++++++++++------- 3 files changed, 232 insertions(+), 125 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index c93dd78a..9bd5dbd8 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -66,6 +66,7 @@ static const std::string kDefaultLevel3Product_ {"N0B"}; static constexpr std::size_t kTimerPlaces_ {6u}; static constexpr std::chrono::seconds kFastRetryInterval_ {15}; +static constexpr std::chrono::seconds kFastRetryIntervalChunks_ {3}; static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; static std::unordered_map> @@ -765,7 +766,12 @@ void RadarProductManagerImpl::RefreshDataSync( auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); - std::chrono::milliseconds interval = kFastRetryInterval_; + // Level2 chunked data is updated quickly and uses a fater interval + const std::chrono::milliseconds fastRetryInterval = + providerManager->group_ == common::RadarProductGroup::Level2 ? + kFastRetryIntervalChunks_ : + kFastRetryInterval_; + std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) { @@ -786,10 +792,10 @@ void RadarProductManagerImpl::RefreshDataSync( // been last modified, slow the retry period interval = kSlowRetryInterval_; } - else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) + else if (interval < std::chrono::milliseconds {fastRetryInterval}) { // The interval should be no quicker than the fast retry interval - interval = kFastRetryInterval_; + interval = fastRetryInterval; } if (newObjects > 0) diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 6344dff0..69f0dbf2 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -561,7 +561,8 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } - if (radarData == p->elevationScan_ && + // TODO do not do this when updating from live data + if (false && (radarData == p->elevationScan_) && smoothingEnabled == p->lastSmoothingEnabled_ && (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || !smoothingEnabled)) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 236b97ee..3536c16c 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -53,6 +53,7 @@ public: std::shared_ptr nexradFile_; std::chrono::system_clock::time_point time_; std::chrono::system_clock::time_point lastModified_; + std::chrono::system_clock::time_point secondLastModified_; std::string lastKey_; int nextFile_ {1}; bool hasAllFiles_ {false}; @@ -70,7 +71,8 @@ public: lastScan_ {"", false}, currentScan_ {"", false}, scansMutex_ {}, - updatePeriod_ {15}, + lastTimeListed_ {}, + updatePeriod_ {7}, self_ {self} { // Disable HTTP request for region @@ -96,11 +98,14 @@ public: Impl& operator=(Impl&&) = delete; std::chrono::system_clock::time_point GetScanTime(const std::string& prefix); - std::string GetScanKey(const std::string& prefix, - const std::chrono::system_clock::time_point& time, - int last); - std::shared_ptr LoadScan(Impl::ScanRecord& scanRecord); - int GetScanNumber(const std::string& prefix); + int GetScanNumber(const std::string& prefix); + std::string GetScanKey(const std::string& prefix, + const std::chrono::system_clock::time_point& time, + int last); + + bool LoadScan(Impl::ScanRecord& scanRecord); + std::tuple ListObjects(); + std::string radarSite_; std::string bucketName_; @@ -110,10 +115,11 @@ public: std::mutex refreshMutex_; std::unordered_map - scanTimes_; - ScanRecord lastScan_; - ScanRecord currentScan_; - std::shared_mutex scansMutex_; + scanTimes_; + ScanRecord lastScan_; + ScanRecord currentScan_; + std::shared_mutex scansMutex_; + std::chrono::system_clock::time_point lastTimeListed_; std::chrono::seconds updatePeriod_; @@ -187,6 +193,24 @@ AwsLevel2ChunksDataProvider::last_modified() const } std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const { + std::shared_lock lock(p->scansMutex_); + // Add an extra second of delay + static const auto extra = std::chrono::seconds(1); + // get update period from time between chunks + if (p->currentScan_.valid_ && p->currentScan_.nextFile_ > 2) + { + auto delta = + p->currentScan_.lastModified_ - p->currentScan_.secondLastModified_; + return std::chrono::duration_cast(delta) + extra; + } + else if (p->lastScan_.valid_ && p->lastScan_.nextFile_ > 2) + { + auto delta = + p->lastScan_.lastModified_ - p->lastScan_.secondLastModified_; + return std::chrono::duration_cast(delta) + extra; + } + + // default to a set update period return p->updatePeriod_; } @@ -241,10 +265,16 @@ AwsLevel2ChunksDataProvider::GetTimePointsByDate( std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) { + using namespace std::chrono; const auto& scanTimeIt = scanTimes_.find(prefix); // O(log(n)) if (scanTimeIt != scanTimes_.cend()) { - return scanTimeIt->second; + // If the time is greater than 2 hours ago, it may be a new scan + auto replaceBy = system_clock::now() - hours {2}; + if (scanTimeIt->second > replaceBy) + { + return scanTimeIt->second; + } } Aws::S3::Model::ListObjectsV2Request request; @@ -258,6 +288,7 @@ AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) { auto timePoint = self_->GetTimePointByKey( outcome.GetResult().GetContents().at(0).GetKey()); + scanTimes_.insert_or_assign(prefix, timePoint); return timePoint; } @@ -277,6 +308,135 @@ std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( "{0}/{1:%Y%m%d-%H%M%S}-{2}", prefix, fmt::gmtime(time), last - 1); } +std::tuple +AwsLevel2ChunksDataProvider::Impl::ListObjects() +{ + size_t newObjects = 0; + size_t totalObjects = 0; + + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + + if (currentScan_.valid_ && !currentScan_.hasAllFiles_ && + lastTimeListed_ + std::chrono::minutes(7) > now) + { + return {true, newObjects, totalObjects}; + } + logger_->debug("ListObjects"); + lastTimeListed_ = now; + + const std::string prefix = radarSite_ + "/"; + + Aws::S3::Model::ListObjectsV2Request request; + request.SetBucket(bucketName_); + request.SetPrefix(prefix); + request.SetDelimiter("/"); + + auto outcome = client_->ListObjectsV2(request); + + if (outcome.IsSuccess()) + { + auto& scans = outcome.GetResult().GetCommonPrefixes(); + logger_->debug("Found {} scans", scans.size()); + + if (scans.size() > 0) + { + // find latest scan + auto scanNumberMap = std::map(); + + for (auto& scan : scans) // O(n log(n)) n <= 999 + { + const std::string& scanPrefix = scan.GetPrefix(); + scanNumberMap.insert_or_assign(GetScanNumber(scanPrefix), + scanPrefix); + } + + // TODO ensure not out of range + int lastScanNumber = -1; + // Start with last scan + int previousScanNumber = scanNumberMap.crbegin()->first; + const int firstScanNumber = scanNumberMap.cbegin()->first; + + // This indicates that highest number scan is the last scan + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + if (previousScanNumber != 999 || firstScanNumber != 1) + { + lastScanNumber = previousScanNumber; + } + else + { + // have already checked scan with highest number, so skip first + previousScanNumber = firstScanNumber; + bool first = true; + for (const auto& scan : scanNumberMap) + { + if (first) + { + first = false; + continue; + } + if (scan.first != previousScanNumber + 1) + { + lastScanNumber = previousScanNumber; + break; + } + previousScanNumber = scan.first; + } + } + + if (lastScanNumber == -1) + { + logger_->warn("Could not find last scan"); + // TODO make sure this makes sence + return {false, 0, 0}; + } + + std::string& lastScanPrefix = scanNumberMap.at(lastScanNumber); + int secondLastScanNumber = + lastScanNumber == 1 ? 999 : lastScanNumber - 1; + + const auto& secondLastScanPrefix = + scanNumberMap.find(secondLastScanNumber); + + if (!currentScan_.valid_ || + currentScan_.prefix_ != lastScanPrefix) + { + if (currentScan_.valid_ && + (secondLastScanPrefix == scanNumberMap.cend() || + currentScan_.prefix_ == secondLastScanPrefix->second)) + { + lastScan_ = currentScan_; + } + else if (secondLastScanPrefix != scanNumberMap.cend()) + { + lastScan_.valid_ = true; + lastScan_.prefix_ = secondLastScanPrefix->second; + lastScan_.nexradFile_ = nullptr; + lastScan_.time_ = GetScanTime(secondLastScanPrefix->second); + lastScan_.lastModified_ = {}; + lastScan_.secondLastModified_ = {}; + lastScan_.lastKey_ = ""; + lastScan_.nextFile_ = 1; + lastScan_.hasAllFiles_ = false; + newObjects += 1; + } + + currentScan_.valid_ = true; + currentScan_.prefix_ = lastScanPrefix; + currentScan_.nexradFile_ = nullptr; + currentScan_.time_ = GetScanTime(lastScanPrefix); + currentScan_.lastModified_ = {}; + currentScan_.secondLastModified_ = {}; + currentScan_.lastKey_ = ""; + currentScan_.nextFile_ = 1; + currentScan_.hasAllFiles_ = false; + newObjects += 1; + } + } + } + + return {true, newObjects, totalObjects}; +} + std::tuple AwsLevel2ChunksDataProvider::ListObjects(std::chrono::system_clock::time_point) { @@ -289,16 +449,16 @@ AwsLevel2ChunksDataProvider::LoadObjectByKey(const std::string& /*prefix*/) return nullptr; } -std::shared_ptr -AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) +bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) { if (!scanRecord.valid_) { - return nullptr; + logger_->warn("Tried to load scan which was not listed yet"); + return false; } else if (scanRecord.hasAllFiles_) { - return scanRecord.nexradFile_; + return false; } Aws::S3::Model::ListObjectsV2Request listRequest; @@ -314,19 +474,23 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) if (!listOutcome.IsSuccess()) { logger_->warn("Could not find scan at {}", scanRecord.prefix_); - return nullptr; + return false; } + bool hasNew = false; auto& chunks = listOutcome.GetResult().GetContents(); for (const auto& chunk : chunks) { const std::string& key = chunk.GetKey(); - // TODO this is wrong, 1st number can be 1-3 digits - // We just want the number of this chunk for now // KIND/585/20250324-134727-001-S - static const size_t startNumberPos = - std::string("KIND/585/20250324-134727-").size(); + // KIND/5/20250324-134727-001-S + static const size_t firstSlash = std::string("KIND/").size(); + const size_t secondSlash = key.find('/', firstSlash); + static const size_t startNumberPosOffset = + std::string("/20250324-134727-").size(); + const size_t startNumberPos = + secondSlash + startNumberPosOffset; const std::string& keyNumberStr = key.substr(startNumberPos, 3); const int keyNumber = std::stoi(keyNumberStr); if (keyNumber != scanRecord.nextFile_) @@ -334,12 +498,11 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) continue; } - // TODO this is wrong, 1st number can be 1-3 digits // Now we want the ending char // KIND/585/20250324-134727-001-S static const size_t charPos = - std::string("KIND/585/20250324-134727-001-").size(); - const char keyChar = key[charPos]; + std::string("/20250324-134727-001-").size(); + const char keyChar = key[secondSlash + charPos]; Aws::S3::Model::GetObjectRequest objectRequest; objectRequest.SetBucket(bucketName_); @@ -351,7 +514,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) { logger_->warn("Could not get object: {}", outcome.GetError().GetMessage()); - return nullptr; + return hasNew; } auto& body = outcome.GetResultWithOwnership().GetBody(); @@ -364,7 +527,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) if (!scanRecord.nexradFile_->LoadData(body)) { logger_->warn("Failed to load first chunk"); - return nullptr; + return hasNew; } break; } @@ -373,7 +536,7 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) if (!scanRecord.nexradFile_->LoadLDMRecords(body)) { logger_->warn("Failed to load middle chunk"); - return nullptr; + return hasNew; } break; } @@ -382,31 +545,38 @@ AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) if (!scanRecord.nexradFile_->LoadLDMRecords(body)) { logger_->warn("Failed to load last chunk"); - return nullptr; + return hasNew; } scanRecord.hasAllFiles_ = true; break; } default: - return nullptr; + logger_->warn("Could not load chunk with unknown char"); + return hasNew; } + hasNew = true; std::chrono::seconds lastModifiedSeconds { outcome.GetResult().GetLastModified().Seconds()}; std::chrono::system_clock::time_point lastModified {lastModifiedSeconds}; + scanRecord.secondLastModified_ = scanRecord.lastModified_; scanRecord.lastModified_ = lastModified; scanRecord.nextFile_ += 1; scanRecord.lastKey_ = key; } - if (scanRecord.nexradFile_ != nullptr) + if (scanRecord.nexradFile_ == nullptr) + { + logger_->warn("Could not load file"); + } + else { scanRecord.nexradFile_->IndexFile(); } - return scanRecord.nexradFile_; + return hasNew; } std::shared_ptr @@ -417,14 +587,15 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( if (p->currentScan_.valid_ && time >= p->currentScan_.time_) { - return p->LoadScan(p->currentScan_); + return p->currentScan_.nexradFile_; } else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) { - return p->LoadScan(p->lastScan_); + return p->lastScan_.nexradFile_; } else { + logger_->warn("Could not find scan with time"); return nullptr; } } @@ -440,8 +611,8 @@ int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) // We just want the number of this chunk for now // KIND/585/20250324-134727-001-S - static const size_t startNumberPos = std::string("KIND/").size(); - const std::string& prefixNumberStr = prefix.substr(startNumberPos, 3); + static const size_t firstSlash = std::string("KIND/").size(); + const std::string& prefixNumberStr = prefix.substr(firstSlash, 3); return std::stoi(prefixNumberStr); } @@ -449,106 +620,35 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() { using namespace std::chrono; + boost::timer::cpu_timer timer {}; + timer.start(); + std::unique_lock lock(p->refreshMutex_); std::unique_lock scanLock(p->scansMutex_); - - size_t newObjects = 0; - size_t totalObjects = 0; - - const std::string prefix = p->radarSite_ + "/"; - - Aws::S3::Model::ListObjectsV2Request request; - request.SetBucket(p->bucketName_); - request.SetPrefix(prefix); - request.SetDelimiter("/"); - - auto outcome = p->client_->ListObjectsV2(request); - - - if (outcome.IsSuccess()) - { - auto& scans = outcome.GetResult().GetCommonPrefixes(); - logger_->debug("Found {} scans", scans.size()); - - boost::timer::cpu_timer timer {}; - timer.start(); - if (scans.size() > 0) - { - - // TODO this cannot be done by getting things form the network. - // Use index number instead. - - // find latest scan - std::chrono::system_clock::time_point latestTime = {}; - std::chrono::system_clock::time_point secondLatestTime = {}; - size_t latestIndex = 0; - size_t secondLatestIndex = 0; - - - for (size_t i = 0; i < scans.size(); i++) // O(n log(n)) n <= 999 - { - auto time = p->GetScanTime(scans[i].GetPrefix()); - if (time > latestTime) - { - secondLatestTime = latestTime; - latestTime = time; - secondLatestIndex = latestIndex; - latestIndex = i; - } - } - - const auto& last = scans.at(secondLatestIndex).GetPrefix(); - if (secondLatestTime != std::chrono::system_clock::time_point {}) - { - p->lastScan_ = p->currentScan_; - } - else if (!p->lastScan_.valid_ || p->lastScan_.prefix_ != last) - { - p->lastScan_.valid_ = true; - p->lastScan_.prefix_ = last; - p->lastScan_.nexradFile_ = nullptr; - p->lastScan_.time_ = secondLatestTime; - p->lastScan_.lastModified_ = {}; - p->lastScan_.lastKey_ = ""; - p->lastScan_.nextFile_ = 1; - p->lastScan_.hasAllFiles_ = false; - newObjects += 1; - } - - const auto& current = scans.at(latestIndex).GetPrefix(); - if (!p->currentScan_.valid_ || p->currentScan_.prefix_ != current) - { - p->currentScan_.valid_ = true; - p->currentScan_.prefix_ = current; - p->currentScan_.nexradFile_ = nullptr; - p->currentScan_.time_ = latestTime; - p->currentScan_.lastModified_ = {}; - p->currentScan_.lastKey_ = ""; - p->currentScan_.nextFile_ = 1; - p->currentScan_.hasAllFiles_ = false; - newObjects += 1; - } - } - - timer.stop(); - logger_->debug("Updated current scans in {}", timer.format(6, "%ws")); - } - - logger_->debug("Loading scans"); + auto [success, newObjects, totalObjects] = p->ListObjects(); if (p->currentScan_.valid_) { - p->LoadScan(p->currentScan_); + if (p->LoadScan(p->currentScan_)) + { + newObjects += 1; + } totalObjects += 1; } if (p->lastScan_.valid_) { - p->LoadScan(p->lastScan_); + /* + if (p->LoadScan(p->lastScan_)) + { + newObjects += 1; + } + */ totalObjects += 1; } - + timer.stop(); + logger_->debug("Refresh() in {}", timer.format(6, "%ws")); return std::make_pair(newObjects, totalObjects); } From a754d6684492a1e1e215d37ca3d73b9659d89c4d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 30 Mar 2025 13:18:04 -0400 Subject: [PATCH 554/762] Setting up for merging last and current scan's, and having archive and chunks --- .../scwx/qt/manager/radar_product_manager.cpp | 129 +++++++++++++++--- .../aws_level2_chunks_data_provider.hpp | 1 + .../provider/aws_nexrad_data_provider.hpp | 1 + .../scwx/provider/nexrad_data_provider.hpp | 9 ++ .../provider/nexrad_data_provider_factory.hpp | 3 + .../aws_level2_chunks_data_provider.cpp | 8 +- .../provider/aws_nexrad_data_provider.cpp | 6 + .../provider/nexrad_data_provider_factory.cpp | 9 +- 8 files changed, 143 insertions(+), 23 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 9bd5dbd8..0c7f10ea 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -134,6 +134,8 @@ public: level3ProductsInitialized_ {false}, radarSite_ {config::RadarSite::Get(radarId)}, level2ProviderManager_ {std::make_shared( + self_, radarId_, common::RadarProductGroup::Level2)}, + level2ChunksProviderManager_ {std::make_shared( self_, radarId_, common::RadarProductGroup::Level2)} { if (radarSite_ == nullptr) @@ -144,10 +146,14 @@ public: level2ProviderManager_->provider_ = provider::NexradDataProviderFactory::CreateLevel2DataProvider(radarId); + level2ChunksProviderManager_->provider_ = + provider::NexradDataProviderFactory::CreateLevel2ChunksDataProvider( + radarId); } ~RadarProductManagerImpl() { level2ProviderManager_->Disable(); + level2ChunksProviderManager_->Disable(); std::shared_lock lock(level3ProviderManagerMutex_); std::for_each(std::execution::par_unseq, @@ -251,6 +257,7 @@ public: std::shared_mutex level3ProductRecordMutex_ {}; std::shared_ptr level2ProviderManager_; + std::shared_ptr level2ChunksProviderManager_; std::unordered_map> level3ProviderManagerMap_ {}; std::shared_mutex level3ProviderManagerMutex_ {}; @@ -639,6 +646,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, if (group == common::RadarProductGroup::Level2) { p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); + p->EnableRefresh(uuid, p->level2ChunksProviderManager_, enabled); } else { @@ -986,6 +994,12 @@ void RadarProductManager::LoadLevel2Data( p->level2ProductRecordMutex_, p->loadLevel2DataMutex_, request); + p->LoadProviderData(time, + p->level2ChunksProviderManager_, + p->level2ProductRecords_, + p->level2ProductRecordMutex_, + p->loadLevel2DataMutex_, + request); } void RadarProductManager::LoadLevel3Data( @@ -1162,6 +1176,10 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes( level2ProductRecords_, level2ProductRecordMutex_, time); + PopulateProductTimes(level2ChunksProviderManager_, + level2ProductRecords_, + level2ProductRecordMutex_, + time); } void RadarProductManagerImpl::PopulateLevel3ProductTimes( @@ -1504,35 +1522,104 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, auto records = p->GetLevel2ProductRecords(time); - for (auto& recordPair : records) + //TODO decide when to use chunked vs archived data. + if (true) { - auto& record = recordPair.second; - - if (record != nullptr) + auto currentFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadLatestObject()); + std::shared_ptr currentRadarData = nullptr; + float currentElevationCut = 0.0f; + std::vector currentElevationCuts; + if (currentFile != nullptr) { - std::shared_ptr recordRadarData = nullptr; - float recordElevationCut = 0.0f; - std::vector recordElevationCuts; + std::tie(currentRadarData, currentElevationCut, currentElevationCuts) = + currentFile->GetElevationScan(dataBlockType, elevation, time); + } - std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = - record->level2_file()->GetElevationScan( - dataBlockType, elevation, time); + std::shared_ptr lastRadarData = nullptr; + float lastElevationCut = 0.0f; + std::vector lastElevationCuts; + auto lastFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadSecondLatestObject()); + if (lastFile != nullptr) + { + std::tie(lastRadarData, lastElevationCut, lastElevationCuts) = + lastFile->GetElevationScan(dataBlockType, elevation, time); + } - if (recordRadarData != nullptr) + if (currentRadarData != nullptr) + { + if (lastRadarData != nullptr) { - auto& radarData0 = (*recordRadarData)[0]; + auto& radarData0 = (*currentRadarData)[0]; auto collectionTime = std::chrono::floor( - scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time())); + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); - // Find the newest radar data, not newer than the selected time - if (radarData == nullptr || - (collectionTime <= time && foundTime < collectionTime)) + // TODO merge data + radarData = currentRadarData; + elevationCut = currentElevationCut; + elevationCuts = std::move(currentElevationCuts); + foundTime = collectionTime; + } + else + { + auto& radarData0 = (*currentRadarData)[0]; + auto collectionTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); + + radarData = currentRadarData; + elevationCut = currentElevationCut; + elevationCuts = std::move(currentElevationCuts); + foundTime = collectionTime; + } + } + else if (lastRadarData != nullptr) + { + auto& radarData0 = (*lastRadarData)[0]; + auto collectionTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); + + radarData = lastRadarData; + elevationCut = lastElevationCut; + elevationCuts = std::move(lastElevationCuts); + foundTime = collectionTime; + } + } + else + { + for (auto& recordPair : records) + { + auto& record = recordPair.second; + + if (record != nullptr) + { + std::shared_ptr recordRadarData = nullptr; + float recordElevationCut = 0.0f; + std::vector recordElevationCuts; + + std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = + record->level2_file()->GetElevationScan( + dataBlockType, elevation, time); + + if (recordRadarData != nullptr) { - radarData = recordRadarData; - elevationCut = recordElevationCut; - elevationCuts = std::move(recordElevationCuts); - foundTime = collectionTime; + auto& radarData0 = (*recordRadarData)[0]; + auto collectionTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); + + // Find the newest radar data, not newer than the selected time + if (radarData == nullptr || + (collectionTime <= time && foundTime < collectionTime)) + { + radarData = recordRadarData; + elevationCut = recordElevationCut; + elevationCuts = std::move(recordElevationCuts); + foundTime = collectionTime; + } } } } diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 57e8e301..106b6605 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -51,6 +51,7 @@ public: std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; std::shared_ptr LoadLatestObject() override; + std::shared_ptr LoadSecondLatestObject() override; std::pair Refresh() override; void RequestAvailableProducts() override; diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index cb71f27b..b2946f38 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -49,6 +49,7 @@ public: std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; std::shared_ptr LoadLatestObject() override; + std::shared_ptr LoadSecondLatestObject() override; std::pair Refresh() override; protected: diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index 81edc1eb..a0e09f04 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -106,6 +106,15 @@ public: virtual std::shared_ptr LoadLatestObject() = 0; + /** + * Loads the second NEXRAD file object + * + * @return NEXRAD data + */ + virtual std::shared_ptr + LoadSecondLatestObject() = 0; + + /** * Lists NEXRAD objects for the current date, and adds them to the cache. If * no objects have been added to the cache for the current date, the previous diff --git a/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp b/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp index bdd51b9e..510ee14c 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider_factory.hpp @@ -27,6 +27,9 @@ public: static std::shared_ptr CreateLevel2DataProvider(const std::string& radarSite); + static std::shared_ptr + CreateLevel2ChunksDataProvider(const std::string& radarSite); + static std::shared_ptr CreateLevel3DataProvider(const std::string& radarSite, const std::string& product); diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 3536c16c..7c692f7f 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -603,7 +603,13 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( std::shared_ptr AwsLevel2ChunksDataProvider::LoadLatestObject() { - return LoadObjectByTime(FindLatestTime()); + return p->currentScan_.nexradFile_; +} + +std::shared_ptr +AwsLevel2ChunksDataProvider::LoadSecondLatestObject() +{ + return p->lastScan_.nexradFile_; } int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index 74740be0..c0611804 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -351,6 +351,12 @@ std::shared_ptr AwsNexradDataProvider::LoadLatestObject() return LoadObjectByKey(FindLatestKey()); } +std::shared_ptr +AwsNexradDataProvider::LoadSecondLatestObject() +{ + return nullptr; +} + std::pair AwsNexradDataProvider::Refresh() { using namespace std::chrono; diff --git a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp index 83ccb1a3..dcaf7c9f 100644 --- a/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp +++ b/wxdata/source/scwx/provider/nexrad_data_provider_factory.cpp @@ -1,5 +1,5 @@ #include -//#include +#include #include #include @@ -14,6 +14,13 @@ static const std::string logPrefix_ = std::shared_ptr NexradDataProviderFactory::CreateLevel2DataProvider( const std::string& radarSite) +{ + return std::make_unique(radarSite); +} + +std::shared_ptr +NexradDataProviderFactory::CreateLevel2ChunksDataProvider( + const std::string& radarSite) { return std::make_unique(radarSite); } From add57ff26fb723e21cf1ec522ad07f55e3f1bef0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Apr 2025 12:04:05 -0400 Subject: [PATCH 555/762] Minor updates to level2 chunks --- .../scwx/qt/manager/radar_product_manager.cpp | 2 +- .../aws_level2_chunks_data_provider.cpp | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 0c7f10ea..bc29ed9a 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -645,7 +645,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, { if (group == common::RadarProductGroup::Level2) { - p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); + //p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); p->EnableRefresh(uuid, p->level2ChunksProviderManager_, enabled); } else diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 7c692f7f..19783965 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -189,13 +189,26 @@ size_t AwsLevel2ChunksDataProvider::cache_size() const std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::last_modified() const { - return p->currentScan_.lastModified_; + if (p->currentScan_.valid_ && p->currentScan_.lastModified_ != + std::chrono::system_clock::time_point {}) + { + return p->currentScan_.lastModified_; + } + else if (p->lastScan_.valid_ && p->lastScan_.lastModified_ != + std::chrono::system_clock::time_point {}) + { + return p->lastScan_.lastModified_; + } + else + { + return {}; + } } std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const { std::shared_lock lock(p->scansMutex_); // Add an extra second of delay - static const auto extra = std::chrono::seconds(1); + static const auto extra = std::chrono::seconds(2); // get update period from time between chunks if (p->currentScan_.valid_ && p->currentScan_.nextFile_ > 2) { @@ -317,7 +330,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); if (currentScan_.valid_ && !currentScan_.hasAllFiles_ && - lastTimeListed_ + std::chrono::minutes(7) > now) + lastTimeListed_ + std::chrono::minutes(2) > now) { return {true, newObjects, totalObjects}; } @@ -350,13 +363,13 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() scanPrefix); } - // TODO ensure not out of range int lastScanNumber = -1; // Start with last scan int previousScanNumber = scanNumberMap.crbegin()->first; const int firstScanNumber = scanNumberMap.cbegin()->first; // This indicates that highest number scan is the last scan + // (including if there is only 1 scan) // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) if (previousScanNumber != 999 || firstScanNumber != 1) { @@ -431,6 +444,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() currentScan_.hasAllFiles_ = false; newObjects += 1; } + logger_->error("{}", currentScan_.prefix_); } } @@ -479,6 +493,7 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) bool hasNew = false; auto& chunks = listOutcome.GetResult().GetContents(); + logger_->debug("Found {} new chunks.", chunks.size()); for (const auto& chunk : chunks) { const std::string& key = chunk.GetKey(); @@ -502,6 +517,11 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) // KIND/585/20250324-134727-001-S static const size_t charPos = std::string("/20250324-134727-001-").size(); + if (secondSlash + charPos >= key.size()) + { + logger_->warn("Chunk key was not long enough"); + continue; + } const char keyChar = key[secondSlash + charPos]; Aws::S3::Model::GetObjectRequest objectRequest; @@ -571,7 +591,7 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) { logger_->warn("Could not load file"); } - else + else if (hasNew) { scanRecord.nexradFile_->IndexFile(); } @@ -614,8 +634,6 @@ AwsLevel2ChunksDataProvider::LoadSecondLatestObject() int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) { - - // We just want the number of this chunk for now // KIND/585/20250324-134727-001-S static const size_t firstSlash = std::string("KIND/").size(); const std::string& prefixNumberStr = prefix.substr(firstSlash, 3); From ac6d6093ec5df81bac95789bc492065cd96cf6a7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 4 Apr 2025 19:35:12 -0400 Subject: [PATCH 556/762] updated how the most recent scan was determined to ensure correctness --- .../aws_level2_chunks_data_provider.cpp | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 19783965..09436bbe 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -368,16 +368,19 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() int previousScanNumber = scanNumberMap.crbegin()->first; const int firstScanNumber = scanNumberMap.cbegin()->first; + // Look for a gap in scan numbers. This indicates that is the latest + // scan. + // This indicates that highest number scan is the last scan // (including if there is only 1 scan) // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - if (previousScanNumber != 999 || firstScanNumber != 1) + if (previousScanNumber != 999 || scans.size() == 1) { lastScanNumber = previousScanNumber; } else { - // have already checked scan with highest number, so skip first + // Have already checked scan with highest number, so skip first previousScanNumber = firstScanNumber; bool first = true; for (const auto& scan : scanNumberMap) @@ -398,9 +401,17 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() if (lastScanNumber == -1) { - logger_->warn("Could not find last scan"); - // TODO make sure this makes sence - return {false, 0, 0}; + // 999 is the last scan + if (firstScanNumber != 1) + { + lastScanNumber = previousScanNumber; + } + else + { + logger_->warn("Could not find last scan"); + // TODO make sure this makes sence + return {false, 0, 0}; + } } std::string& lastScanPrefix = scanNumberMap.at(lastScanNumber); @@ -444,7 +455,6 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() currentScan_.hasAllFiles_ = false; newObjects += 1; } - logger_->error("{}", currentScan_.prefix_); } } @@ -510,6 +520,10 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) const int keyNumber = std::stoi(keyNumberStr); if (keyNumber != scanRecord.nextFile_) { + logger_->warn("Chunk found that was not in order {} {} {}", + key, + scanRecord.nextFile_, + keyNumber); continue; } From 8b7a3e978126673fe326255f4605b24524c0c4c7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 6 Apr 2025 16:09:48 -0400 Subject: [PATCH 557/762] partiallaly complete merging of radar data --- .../scwx/qt/manager/radar_product_manager.cpp | 11 ++ wxdata/include/scwx/wsr88d/ar2v_file.hpp | 2 + .../aws_level2_chunks_data_provider.cpp | 2 - wxdata/source/scwx/wsr88d/ar2v_file.cpp | 144 ++++++++++++++++++ 4 files changed, 157 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index bc29ed9a..e6ede7b4 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1525,6 +1525,16 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, //TODO decide when to use chunked vs archived data. if (true) { + auto currentFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadLatestObject()); + auto lastFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadSecondLatestObject()); + auto radarFile = + std::make_shared(currentFile, lastFile); + std::tie(radarData, elevationCut, elevationCuts) = + radarFile->GetElevationScan(dataBlockType, elevation, time); + + /* auto currentFile = std::dynamic_pointer_cast( p->level2ChunksProviderManager_->provider_->LoadLatestObject()); std::shared_ptr currentRadarData = nullptr; @@ -1587,6 +1597,7 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, elevationCuts = std::move(lastElevationCuts); foundTime = collectionTime; } + */ } else { diff --git a/wxdata/include/scwx/wsr88d/ar2v_file.hpp b/wxdata/include/scwx/wsr88d/ar2v_file.hpp index 34d50b32..9afca516 100644 --- a/wxdata/include/scwx/wsr88d/ar2v_file.hpp +++ b/wxdata/include/scwx/wsr88d/ar2v_file.hpp @@ -32,6 +32,8 @@ public: Ar2vFile(Ar2vFile&&) noexcept; Ar2vFile& operator=(Ar2vFile&&) noexcept; + Ar2vFile(std::shared_ptr current, std::shared_ptr last); + std::uint32_t julian_date() const; std::uint32_t milliseconds() const; std::string icao() const; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 09436bbe..ef5c392e 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -676,12 +676,10 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() } if (p->lastScan_.valid_) { - /* if (p->LoadScan(p->lastScan_)) { newObjects += 1; } - */ totalObjects += 1; } diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index db04feba..a5f46731 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include +#include #if defined(_MSC_VER) # pragma warning(push) @@ -539,5 +541,147 @@ bool Ar2vFile::IndexFile() return true; } +// TODO not good +bool IsRadarDataIncomplete( + const std::shared_ptr& radarData) +{ + // Assume the data is incomplete when the delta between the first and last + // angles is greater than 2.5 degrees. + constexpr units::degrees kIncompleteDataAngleThreshold_ {2.5}; + + const units::degrees firstAngle = + radarData->cbegin()->second->azimuth_angle(); + const units::degrees lastAngle = + radarData->crbegin()->second->azimuth_angle(); + const units::degrees angleDelta = + common::GetAngleDelta(firstAngle, lastAngle); + + return angleDelta > kIncompleteDataAngleThreshold_; +} + +Ar2vFile::Ar2vFile(std::shared_ptr current, + std::shared_ptr last) : + Ar2vFile() +{ + /*p->vcpData_ = std::make_shared( + *current->vcp_data());*/ + p->vcpData_ = nullptr; // TODO + /* + use index_ to go through each block type, and elevation. + get the latest time. + if the latest time is not complete, get the previous time (possibly in + last), and merge + */ + + if (current == nullptr) + { + return; + } + + for (const auto& type : current->p->index_) + { + for (const auto& elevation : type.second) + { + const auto& mostRecent = elevation.second.crbegin(); + if (mostRecent == elevation.second.crend()) + { + continue; + } + + if (IsRadarDataIncomplete(mostRecent->second)) + { + std::shared_ptr secondMostRecent = + nullptr; + auto maybe = elevation.second.rbegin(); + ++maybe; + + if (maybe == elevation.second.rend()) + { + if (last == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + auto elevationScan = + std::get>( + last->GetElevationScan(type.first, elevation.first, {})); + if (elevationScan == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + secondMostRecent = elevationScan; + } + else + { + secondMostRecent = maybe->second; + } + + auto newScan = std::make_shared(); + + // Convert old into new coords + logger_->error( + "old {}, new {}", + secondMostRecent->cbegin()->second->azimuth_angle().value(), + mostRecent->second->cbegin()->second->azimuth_angle().value()); + // TODO Ordering these correctly + for (const auto& radial : *secondMostRecent) + { + (*newScan)[radial.first] = radial.second; + } + for (const auto& radial : *(mostRecent->second)) + { + (*newScan)[radial.first] = radial.second; + } + + p->index_[type.first][elevation.first][mostRecent->first] = + newScan; + } + else + { + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + } + } + } + + // Go though last, adding other elevations TODO + if (last != nullptr) + { + for (const auto& type : last->p->index_) + { + float highestCurrentElevation = -90; + const auto& maybe1 = p->index_.find(type.first); + if (maybe1 != p->index_.cend()) + { + const auto& maybe2 = maybe1->second.crbegin(); + if (maybe2 != maybe1->second.crend()) { + highestCurrentElevation = maybe2->first + 0.01; + } + } + for (const auto& elevation : type.second) + { + if (elevation.first > highestCurrentElevation) + { + const auto& mostRecent = elevation.second.crbegin(); + if (mostRecent == elevation.second.crend()) + { + continue; + } + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + } + } + } + } + +} + } // namespace wsr88d } // namespace scwx From 094d286b418afbf3cd85f11b18b527551e033adf Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 7 Apr 2025 12:36:06 -0400 Subject: [PATCH 558/762] fully working merging of data from last and current scan --- .../scwx/qt/manager/radar_product_manager.cpp | 76 +------ .../scwx/qt/view/level2_product_view.cpp | 3 +- wxdata/include/scwx/wsr88d/ar2v_file.hpp | 3 +- .../aws_level2_chunks_data_provider.cpp | 4 +- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 207 +++++++++++------- 5 files changed, 140 insertions(+), 153 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index e6ede7b4..12cf9d61 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1520,87 +1520,17 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; - auto records = p->GetLevel2ProductRecords(time); - //TODO decide when to use chunked vs archived data. - if (true) + if constexpr (true) { auto currentFile = std::dynamic_pointer_cast( p->level2ChunksProviderManager_->provider_->LoadLatestObject()); - auto lastFile = std::dynamic_pointer_cast( - p->level2ChunksProviderManager_->provider_->LoadSecondLatestObject()); - auto radarFile = - std::make_shared(currentFile, lastFile); std::tie(radarData, elevationCut, elevationCuts) = - radarFile->GetElevationScan(dataBlockType, elevation, time); - - /* - auto currentFile = std::dynamic_pointer_cast( - p->level2ChunksProviderManager_->provider_->LoadLatestObject()); - std::shared_ptr currentRadarData = nullptr; - float currentElevationCut = 0.0f; - std::vector currentElevationCuts; - if (currentFile != nullptr) - { - std::tie(currentRadarData, currentElevationCut, currentElevationCuts) = - currentFile->GetElevationScan(dataBlockType, elevation, time); - } - - std::shared_ptr lastRadarData = nullptr; - float lastElevationCut = 0.0f; - std::vector lastElevationCuts; - auto lastFile = std::dynamic_pointer_cast( - p->level2ChunksProviderManager_->provider_->LoadSecondLatestObject()); - if (lastFile != nullptr) - { - std::tie(lastRadarData, lastElevationCut, lastElevationCuts) = - lastFile->GetElevationScan(dataBlockType, elevation, time); - } - - if (currentRadarData != nullptr) - { - if (lastRadarData != nullptr) - { - auto& radarData0 = (*currentRadarData)[0]; - auto collectionTime = std::chrono::floor( - scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time())); - - // TODO merge data - radarData = currentRadarData; - elevationCut = currentElevationCut; - elevationCuts = std::move(currentElevationCuts); - foundTime = collectionTime; - } - else - { - auto& radarData0 = (*currentRadarData)[0]; - auto collectionTime = std::chrono::floor( - scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time())); - - radarData = currentRadarData; - elevationCut = currentElevationCut; - elevationCuts = std::move(currentElevationCuts); - foundTime = collectionTime; - } - } - else if (lastRadarData != nullptr) - { - auto& radarData0 = (*lastRadarData)[0]; - auto collectionTime = std::chrono::floor( - scwx::util::TimePoint(radarData0->modified_julian_date(), - radarData0->collection_time())); - - radarData = lastRadarData; - elevationCut = lastElevationCut; - elevationCuts = std::move(lastElevationCuts); - foundTime = collectionTime; - } - */ + currentFile->GetElevationScan(dataBlockType, elevation, time); } else { + auto records = p->GetLevel2ProductRecords(time); for (auto& recordPair : records) { auto& record = recordPair.second; diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 69f0dbf2..9ead358b 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -561,8 +561,7 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } - // TODO do not do this when updating from live data - if (false && (radarData == p->elevationScan_) && + if ((radarData == p->elevationScan_) && smoothingEnabled == p->lastSmoothingEnabled_ && (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || !smoothingEnabled)) diff --git a/wxdata/include/scwx/wsr88d/ar2v_file.hpp b/wxdata/include/scwx/wsr88d/ar2v_file.hpp index 9afca516..64319283 100644 --- a/wxdata/include/scwx/wsr88d/ar2v_file.hpp +++ b/wxdata/include/scwx/wsr88d/ar2v_file.hpp @@ -32,7 +32,8 @@ public: Ar2vFile(Ar2vFile&&) noexcept; Ar2vFile& operator=(Ar2vFile&&) noexcept; - Ar2vFile(std::shared_ptr current, std::shared_ptr last); + Ar2vFile(const std::shared_ptr& current, + const std::shared_ptr& last); std::uint32_t julian_date() const; std::uint32_t milliseconds() const; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index ef5c392e..0b18113f 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -637,7 +637,9 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( std::shared_ptr AwsLevel2ChunksDataProvider::LoadLatestObject() { - return p->currentScan_.nexradFile_; + return std::make_shared(p->currentScan_.nexradFile_, + p->lastScan_.nexradFile_); + //return p->currentScan_.nexradFile_; } std::shared_ptr diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index a5f46731..7e7e14e9 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -9,7 +9,6 @@ #include #include -#include #if defined(_MSC_VER) # pragma warning(push) @@ -559,114 +558,171 @@ bool IsRadarDataIncomplete( return angleDelta > kIncompleteDataAngleThreshold_; } -Ar2vFile::Ar2vFile(std::shared_ptr current, - std::shared_ptr last) : - Ar2vFile() +Ar2vFile::Ar2vFile(const std::shared_ptr& current, + const std::shared_ptr& last) : + Ar2vFile() { - /*p->vcpData_ = std::make_shared( - *current->vcp_data());*/ - p->vcpData_ = nullptr; // TODO - /* - use index_ to go through each block type, and elevation. - get the latest time. - if the latest time is not complete, get the previous time (possibly in - last), and merge - */ + // This is only used to index right now, so not a huge deal + p->vcpData_ = nullptr; - if (current == nullptr) + // Reconstruct index from the other's indexes + if (current != nullptr) { - return; - } - - for (const auto& type : current->p->index_) - { - for (const auto& elevation : type.second) + for (const auto& type : current->p->index_) { - const auto& mostRecent = elevation.second.crbegin(); - if (mostRecent == elevation.second.crend()) + for (const auto& elevation : type.second) { - continue; - } - - if (IsRadarDataIncomplete(mostRecent->second)) - { - std::shared_ptr secondMostRecent = - nullptr; - auto maybe = elevation.second.rbegin(); - ++maybe; - - if (maybe == elevation.second.rend()) + // Get the most recent scan + const auto& mostRecent = elevation.second.crbegin(); + if (mostRecent == elevation.second.crend()) { - if (last == nullptr) + continue; + } + + // Merge this scan with the last one if it is incomplete + if (IsRadarDataIncomplete(mostRecent->second)) + { + std::shared_ptr secondMostRecent = nullptr; + + // check if this volume scan has an earlier elevation scan + auto maybe = elevation.second.rbegin(); // TODO name + ++maybe; + + if (maybe == elevation.second.rend()) { - // Nothing to merge with - p->index_[type.first][elevation.first][mostRecent->first] = - mostRecent->second; - continue; + if (last == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + // get the scan from the last scan + auto elevationScan = + std::get>( + last->GetElevationScan( + type.first, elevation.first, {})); + if (elevationScan == nullptr) + { + // Nothing to merge with + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; + continue; + } + + secondMostRecent = elevationScan; + } + else + { + secondMostRecent = maybe->second; } - auto elevationScan = - std::get>( - last->GetElevationScan(type.first, elevation.first, {})); - if (elevationScan == nullptr) + // Make the new scan + auto newScan = std::make_shared(); + + // Copy over the new radials + for (const auto& radial : *(mostRecent->second)) { - // Nothing to merge with - p->index_[type.first][elevation.first][mostRecent->first] = - mostRecent->second; - continue; + (*newScan)[radial.first] = radial.second; } - secondMostRecent = elevationScan; + /* Correctly order the old radials. The radials need to be in + * order for the rendering to work, and the index needs to start + * at 0 and increase by one from there. Since the new radial + * should have index 0, the old radial needs to be reshaped to + * match the new radials indexing. + */ + + const double lowestAzm = + mostRecent->second->cbegin()->second->azimuth_angle().value(); + const double heighestAzm = mostRecent->second->crbegin() + ->second->azimuth_angle() + .value(); + std::uint16_t index = mostRecent->second->crbegin()->first + 1; + + // Sort by the azimuth. Makes the rest of this way easier + auto secondMostRecentAzmMap = + std::map>(); + for (const auto& radial : *secondMostRecent) + { + secondMostRecentAzmMap[radial.second->azimuth_angle() + .value()] = radial.second; + } + + if (lowestAzm <= heighestAzm) // New scan does not contain 0/360 + { + // Get the radials following the new radials + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first > heighestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + } + // Get the radials before the new radials + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first < lowestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + else + { + break; + } + } + } + else // New scan includes 0/360 + { + // The radials will already be in the right order + for (const auto& radial : secondMostRecentAzmMap) + { + if (radial.first > heighestAzm && radial.first < lowestAzm) + { + (*newScan)[index] = radial.second; + ++index; + } + } + } + + p->index_[type.first][elevation.first][mostRecent->first] = + newScan; } else { - secondMostRecent = maybe->second; + p->index_[type.first][elevation.first][mostRecent->first] = + mostRecent->second; } - - auto newScan = std::make_shared(); - - // Convert old into new coords - logger_->error( - "old {}, new {}", - secondMostRecent->cbegin()->second->azimuth_angle().value(), - mostRecent->second->cbegin()->second->azimuth_angle().value()); - // TODO Ordering these correctly - for (const auto& radial : *secondMostRecent) - { - (*newScan)[radial.first] = radial.second; - } - for (const auto& radial : *(mostRecent->second)) - { - (*newScan)[radial.first] = radial.second; - } - - p->index_[type.first][elevation.first][mostRecent->first] = - newScan; - } - else - { - p->index_[type.first][elevation.first][mostRecent->first] = - mostRecent->second; } } } - // Go though last, adding other elevations TODO + // Go though last, adding other elevations if (last != nullptr) { for (const auto& type : last->p->index_) { + // Find the highest elevation this type has for the current scan + // Start below any reasonable elevation + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) float highestCurrentElevation = -90; const auto& maybe1 = p->index_.find(type.first); if (maybe1 != p->index_.cend()) { const auto& maybe2 = maybe1->second.crbegin(); if (maybe2 != maybe1->second.crend()) { - highestCurrentElevation = maybe2->first + 0.01; + // Add a slight offset to ensure good floating point compare. + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + highestCurrentElevation = maybe2->first + 0.01f; } } + for (const auto& elevation : type.second) { + // Only add elevations above the current scan's elevation if (elevation.first > highestCurrentElevation) { const auto& mostRecent = elevation.second.crbegin(); @@ -680,7 +736,6 @@ Ar2vFile::Ar2vFile(std::shared_ptr current, } } } - } } // namespace wsr88d From 63585af26d8b399531708fb86287798e9959d79f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 7 Apr 2025 18:23:29 -0400 Subject: [PATCH 559/762] Get level2 chunks and archive working together, reduce logging of level2 chunks --- .../scwx/qt/manager/radar_product_manager.cpp | 40 +++++++++++++------ .../scwx/qt/map/radar_product_layer.cpp | 5 +-- .../aws_level2_chunks_data_provider.cpp | 9 +++-- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 14 +++---- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 12cf9d61..ec54b4cd 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -68,6 +68,7 @@ static constexpr std::size_t kTimerPlaces_ {6u}; static constexpr std::chrono::seconds kFastRetryInterval_ {15}; static constexpr std::chrono::seconds kFastRetryIntervalChunks_ {3}; static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; +static constexpr std::chrono::seconds kSlowRetryIntervalChunks_ {20}; static std::unordered_map> instanceMap_; @@ -774,11 +775,15 @@ void RadarProductManagerImpl::RefreshDataSync( auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); - // Level2 chunked data is updated quickly and uses a fater interval + // Level2 chunked data is updated quickly and uses a faster interval const std::chrono::milliseconds fastRetryInterval = - providerManager->group_ == common::RadarProductGroup::Level2 ? + providerManager == level2ChunksProviderManager_ ? kFastRetryIntervalChunks_ : kFastRetryInterval_; + const std::chrono::milliseconds slowRetryInterval = + providerManager == level2ChunksProviderManager_ ? + kSlowRetryIntervalChunks_ : + kSlowRetryInterval_; std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) @@ -798,7 +803,7 @@ void RadarProductManagerImpl::RefreshDataSync( { // If it has been at least 5 update periods since the file has // been last modified, slow the retry period - interval = kSlowRetryInterval_; + interval = slowRetryInterval; } else if (interval < std::chrono::milliseconds {fastRetryInterval}) { @@ -817,7 +822,7 @@ void RadarProductManagerImpl::RefreshDataSync( logger_->info("[{}] No data found", providerManager->name()); // If no data is found, retry at the slow retry interval - interval = kSlowRetryInterval_; + interval = slowRetryInterval; } std::unique_lock const lock(providerManager->refreshTimerMutex_); @@ -953,11 +958,13 @@ void RadarProductManagerImpl::LoadProviderData( { existingRecord = it->second.lock(); + /* if (existingRecord != nullptr) { logger_->debug( "Data previously loaded, loading from data cache"); } + */ } } @@ -1416,7 +1423,7 @@ std::shared_ptr RadarProductManagerImpl::StoreRadarProductRecord( std::shared_ptr record) { - logger_->debug("StoreRadarProductRecord()"); + //logger_->debug("StoreRadarProductRecord()"); std::shared_ptr storedRecord = nullptr; @@ -1433,11 +1440,12 @@ RadarProductManagerImpl::StoreRadarProductRecord( { storedRecord = it->second.lock(); + /* if (storedRecord != nullptr) { logger_->debug( "Level 2 product previously loaded, loading from cache"); - } + }*/ } if (storedRecord == nullptr) @@ -1520,15 +1528,23 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; - //TODO decide when to use chunked vs archived data. - if constexpr (true) + // See if we have this one in the chunk provider. + auto chunkFile = std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_->LoadObjectByTime(time)); + if (chunkFile != nullptr) { - auto currentFile = std::dynamic_pointer_cast( - p->level2ChunksProviderManager_->provider_->LoadLatestObject()); std::tie(radarData, elevationCut, elevationCuts) = - currentFile->GetElevationScan(dataBlockType, elevation, time); + chunkFile->GetElevationScan(dataBlockType, elevation, time); + + if (radarData != nullptr) + { + auto& radarData0 = (*radarData)[0]; + foundTime = std::chrono::floor( + scwx::util::TimePoint(radarData0->modified_julian_date(), + radarData0->collection_time())); + } } - else + else // It is not in the chunk provider, so get it from the archive { auto records = p->GetLevel2ProductRecords(time); for (auto& recordPair : records) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 0d2b7125..8640ba03 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -159,8 +159,6 @@ void RadarProductLayer::Initialize() void RadarProductLayer::UpdateSweep() { - logger_->debug("UpdateSweep()"); - gl::OpenGLFunctions& gl = context()->gl(); boost::timer::cpu_timer timer; @@ -172,9 +170,10 @@ void RadarProductLayer::UpdateSweep() std::try_to_lock); if (!sweepLock.owns_lock()) { - logger_->debug("Sweep locked, deferring update"); + //logger_->debug("Sweep locked, deferring update"); return; } + logger_->debug("UpdateSweep()"); p->sweepNeedsUpdate_ = false; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 0b18113f..02fdbca8 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -618,10 +618,13 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( std::chrono::system_clock::time_point time) { std::unique_lock lock(p->scansMutex_); + static const std::chrono::system_clock::time_point epoch {}; - if (p->currentScan_.valid_ && time >= p->currentScan_.time_) + if (p->currentScan_.valid_ && + (time == epoch || time >= p->currentScan_.time_)) { - return p->currentScan_.nexradFile_; + return std::make_shared(p->currentScan_.nexradFile_, + p->lastScan_.nexradFile_); } else if (p->lastScan_.valid_ && time >= p->lastScan_.time_) { @@ -629,7 +632,6 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( } else { - logger_->warn("Could not find scan with time"); return nullptr; } } @@ -637,6 +639,7 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( std::shared_ptr AwsLevel2ChunksDataProvider::LoadLatestObject() { + std::unique_lock lock(p->scansMutex_); return std::make_shared(p->currentScan_.nexradFile_, p->lastScan_.nexradFile_); //return p->currentScan_.nexradFile_; diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 7e7e14e9..250341a0 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -138,7 +138,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) const { - logger_->debug("GetElevationScan: {} degrees", elevation); + //logger_->debug("GetElevationScan: {} degrees", elevation); std::shared_ptr elevationScan = nullptr; float elevationCut = 0.0f; @@ -273,7 +273,7 @@ bool Ar2vFile::LoadData(std::istream& is) std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) { - logger_->debug("Decompressing LDM Records"); + //logger_->debug("Decompressing LDM Records"); std::size_t numRecords = 0; @@ -321,22 +321,22 @@ std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) ++numRecords; } - logger_->debug("Decompressed {} LDM Records", numRecords); + //logger_->debug("Decompressed {} LDM Records", numRecords); return numRecords; } void Ar2vFileImpl::ParseLDMRecords() { - logger_->debug("Parsing LDM Records"); + //logger_->debug("Parsing LDM Records"); - std::size_t count = 0; + //std::size_t count = 0; for (auto it = rawRecords_.begin(); it != rawRecords_.end(); it++) { std::stringstream& ss = *it; - logger_->trace("Record {}", count++); + //logger_->trace("Record {}", count++); ParseLDMRecord(ss); } @@ -445,7 +445,7 @@ void Ar2vFileImpl::ProcessRadarData( void Ar2vFileImpl::IndexFile() { - logger_->debug("Indexing file"); + //logger_->debug("Indexing file"); constexpr float scaleFactor = 8.0f / 0.043945f; From 6ca76b9ecaea5edfd8554ee7b04e2694ecbe8409 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 10:41:44 -0400 Subject: [PATCH 560/762] Move elevation conversion code into VCP and DRD code --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 21 ++++--------------- .../scwx/wsr88d/rda/digital_radar_data.cpp | 13 +++++++++++- .../rda/volume_coverage_pattern_data.cpp | 15 ++++++++++++- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 250341a0..f804685b 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -447,11 +447,9 @@ void Ar2vFileImpl::IndexFile() { //logger_->debug("Indexing file"); - constexpr float scaleFactor = 8.0f / 0.043945f; - for (auto& elevationCut : radarData_) { - std::uint16_t elevationAngle {}; + float elevationAngle {}; rda::WaveformType waveformType = rda::WaveformType::Unknown; std::shared_ptr& radial0 = @@ -467,14 +465,14 @@ void Ar2vFileImpl::IndexFile() if (vcpData_ != nullptr) { - elevationAngle = vcpData_->elevation_angle_raw(elevationCut.first); + elevationAngle = vcpData_->elevation_angle(elevationCut.first); waveformType = vcpData_->waveform_type(elevationCut.first); } else if ((digitalRadarData0 = std::dynamic_pointer_cast(radial0)) != nullptr) { - elevationAngle = digitalRadarData0->elevation_angle_raw(); + elevationAngle = digitalRadarData0->elevation_angle().value(); } else { @@ -502,18 +500,7 @@ void Ar2vFileImpl::IndexFile() auto time = util::TimePoint(radial0->modified_julian_date(), radial0->collection_time()); - // NOLINTNEXTLINE This conversion is accurate - float elevationAngleConverted = elevationAngle / scaleFactor; - // Any elevation above 90 degrees should be interpreted as a - // negative angle - // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) - if (elevationAngleConverted > 90) - { - elevationAngleConverted -= 360; - } - // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) - - index_[dataBlockType][elevationAngleConverted][time] = + index_[dataBlockType][elevationAngle][time] = elevationCut.second; } } diff --git a/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp b/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp index ebadf4f5..8f2643af 100644 --- a/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/digital_radar_data.cpp @@ -154,7 +154,18 @@ std::uint16_t DigitalRadarData::elevation_angle_raw() const units::degrees DigitalRadarData::elevation_angle() const { - return units::degrees {p->elevationAngle_ * kAngleDataScale}; + // NOLINTNEXTLINE This conversion is accurate + float elevationAngleConverted = p->elevationAngle_ * kAngleDataScale; + // Any elevation above 90 degrees should be interpreted as a + // negative angle + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (elevationAngleConverted > 90) + { + elevationAngleConverted -= 360; + } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + return units::degrees {elevationAngleConverted}; } std::uint16_t DigitalRadarData::elevation_number() const diff --git a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp index d2f3dec2..42ed3685 100644 --- a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp @@ -220,7 +220,20 @@ uint16_t VolumeCoveragePatternData::number_of_base_tilts() const double VolumeCoveragePatternData::elevation_angle(uint16_t e) const { - return p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; + + // NOLINTNEXTLINE This conversion is accurate + float elevationAngleConverted = + p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; + // Any elevation above 90 degrees should be interpreted as a + // negative angle + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (elevationAngleConverted > 90) + { + elevationAngleConverted -= 360; + } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + return elevationAngleConverted; } uint16_t VolumeCoveragePatternData::elevation_angle_raw(uint16_t e) const From 0bda6296c0982b1f65bf820477754d79717c61d8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 12:22:47 -0400 Subject: [PATCH 561/762] Add indicator of what level is currently being updated with level 2 chunks. --- scwx-qt/source/scwx/qt/main/main_window.cpp | 7 ++++ .../scwx/qt/manager/radar_product_manager.cpp | 24 ++++++++++++ .../scwx/qt/manager/radar_product_manager.hpp | 10 +++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 18 +++++++++ scwx-qt/source/scwx/qt/map/map_widget.hpp | 2 + .../scwx/qt/ui/level2_settings_widget.cpp | 15 +++++++ .../scwx/qt/ui/level2_settings_widget.hpp | 1 + .../aws_level2_chunks_data_provider.hpp | 2 + .../aws_level2_chunks_data_provider.cpp | 39 +++++++++++++++++++ 9 files changed, 114 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d213b4fd..089d6749 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -963,6 +963,13 @@ void MainWindowImpl::ConnectMapSignals() } }, Qt::QueuedConnection); + connect( + mapWidget, + &map::MapWidget::IncomingLevel2ElevationChanged, + this, + [this](float incomingElevation) + { level2SettingsWidget_->UpdateIncomingElevation(incomingElevation); }, + Qt::QueuedConnection); } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index ec54b4cd..ed952ed7 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -271,6 +272,8 @@ public: common::Level3ProductCategoryMap availableCategoryMap_ {}; std::shared_mutex availableCategoryMutex_ {}; + float incomingLevel2Elevation_ {-90}; + std::unordered_map, boost::hash> @@ -450,6 +453,11 @@ float RadarProductManager::gate_size() const return (is_tdwr()) ? 150.0f : 250.0f; } +float RadarProductManager::incoming_level_2_elevation() const +{ + return p->incomingLevel2Elevation_; +} + std::string RadarProductManager::radar_id() const { return p->radarId_; @@ -1542,6 +1550,16 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, foundTime = std::chrono::floor( scwx::util::TimePoint(radarData0->modified_julian_date(), radarData0->collection_time())); + + const float incomingElevation = + std::dynamic_pointer_cast( + p->level2ChunksProviderManager_->provider_) + ->GetCurrentElevation(); + if (incomingElevation != p->incomingLevel2Elevation_) + { + p->incomingLevel2Elevation_ = incomingElevation; + Q_EMIT IncomingLevel2ElevationChanged(incomingElevation); + } } } else // It is not in the chunk provider, so get it from the archive @@ -1576,6 +1594,12 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, elevationCut = recordElevationCut; elevationCuts = std::move(recordElevationCuts); foundTime = collectionTime; + + if (p->incomingLevel2Elevation_ != -90) + { + p->incomingLevel2Elevation_ = -90; + Q_EMIT IncomingLevel2ElevationChanged(-90); + } } } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index ee54f147..6efd125d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -43,10 +43,11 @@ public: [[nodiscard]] const std::vector& coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; - [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; - [[nodiscard]] bool is_tdwr() const; - [[nodiscard]] float gate_size() const; - [[nodiscard]] std::string radar_id() const; + [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] float gate_size() const; + [[nodiscard]] float incoming_level_2_elevation() const; + [[nodiscard]] bool is_tdwr() const; + [[nodiscard]] std::string radar_id() const; [[nodiscard]] std::shared_ptr radar_site() const; void Initialize(); @@ -148,6 +149,7 @@ signals: void NewDataAvailable(common::RadarProductGroup group, const std::string& product, std::chrono::system_clock::time_point latestTime); + void IncomingLevel2ElevationChanged(float incomingElevation); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index dafa801c..edb4c3d2 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -656,6 +656,12 @@ std::vector MapWidget::GetElevationCuts() const } } +float MapWidget::GetIncomingLevel2Elevation() const +{ + return p->radarProductManager_->incoming_level_2_elevation(); +} + + common::Level2Product MapWidgetImpl::GetLevel2ProductOrDefault(const std::string& productName) const { @@ -1796,6 +1802,14 @@ void MapWidgetImpl::RadarProductManagerConnect() { if (radarProductManager_ != nullptr) { + connect(radarProductManager_.get(), + &manager::RadarProductManager::IncomingLevel2ElevationChanged, + this, + [this](float incomingElevation) + { + Q_EMIT widget_->IncomingLevel2ElevationChanged( + incomingElevation); + }); connect(radarProductManager_.get(), &manager::RadarProductManager::Level3ProductsChanged, this, @@ -1916,6 +1930,10 @@ void MapWidgetImpl::RadarProductManagerDisconnect() &manager::RadarProductManager::NewDataAvailable, this, nullptr); + disconnect(radarProductManager_.get(), + &manager::RadarProductManager::IncomingLevel2ElevationChanged, + this, + nullptr); } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 845832e4..5cc2e0a1 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -45,6 +45,7 @@ public: GetAvailableLevel3Categories(); [[nodiscard]] std::optional GetElevation() const; [[nodiscard]] std::vector GetElevationCuts() const; + [[nodiscard]] float GetIncomingLevel2Elevation() const; [[nodiscard]] std::vector GetLevel3Products(); [[nodiscard]] std::string GetMapStyle() const; [[nodiscard]] common::RadarProductGroup GetRadarProductGroup() const; @@ -184,6 +185,7 @@ signals: void RadarSweepUpdated(); void RadarSweepNotUpdated(types::NoUpdateReason reason); void WidgetPainted(); + void IncomingLevel2ElevationChanged(float incomingElevation); }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 1b851e72..4a3d0967 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -30,6 +31,7 @@ public: self_ {self}, layout_ {new QVBoxLayout(self)}, elevationGroupBox_ {}, + incomingElevationLabel_ {}, elevationButtons_ {}, elevationCuts_ {}, elevationButtonsChanged_ {false}, @@ -39,10 +41,14 @@ public: { layout_->setContentsMargins(0, 0, 0, 0); + incomingElevationLabel_ = new QLabel("", self); + layout_->addWidget(incomingElevationLabel_); + elevationGroupBox_ = new QGroupBox(tr("Elevation"), self); new ui::FlowLayout(elevationGroupBox_); layout_->addWidget(elevationGroupBox_); + settingsGroupBox_ = new QGroupBox(tr("Settings"), self); QLayout* settingsLayout = new QVBoxLayout(settingsGroupBox_); layout_->addWidget(settingsGroupBox_); @@ -67,6 +73,7 @@ public: QLayout* layout_; QGroupBox* elevationGroupBox_; + QLabel* incomingElevationLabel_; std::list elevationButtons_; std::vector elevationCuts_; bool elevationButtonsChanged_; @@ -240,12 +247,19 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) p->currentElevationButton_ = newElevationButton; } +void Level2SettingsWidget::UpdateIncomingElevation(float incomingElevation) +{ + p->incomingElevationLabel_->setText("Incoming Elevation: " + + QString::number(incomingElevation, 'f', 1) + common::Characters::DEGREE); +} + void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) { std::optional currentElevationOption = activeMap->GetElevation(); const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; std::vector elevationCuts = activeMap->GetElevationCuts(); + float incomingElevation = activeMap->GetIncomingLevel2Elevation(); if (p->elevationCuts_ != elevationCuts) { @@ -279,6 +293,7 @@ void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) } UpdateElevationSelection(currentElevation); + UpdateIncomingElevation(incomingElevation); } } // namespace ui diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp index ce2e443f..796b1ade 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp @@ -23,6 +23,7 @@ public: void showEvent(QShowEvent* event) override; void UpdateElevationSelection(float elevation); + void UpdateIncomingElevation(float incomingElevation); void UpdateSettings(map::MapWidget* activeMap); signals: diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 106b6605..a82dffb2 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -57,6 +57,8 @@ public: void RequestAvailableProducts() override; std::vector GetAvailableProducts() override; + float GetCurrentElevation(); + private: class Impl; std::unique_ptr p; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 02fdbca8..0d97c7b8 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -1,3 +1,4 @@ +#include "scwx/wsr88d/rda/digital_radar_data.hpp" #include #include #include @@ -704,4 +705,42 @@ AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( AwsLevel2ChunksDataProvider& AwsLevel2ChunksDataProvider::operator=( AwsLevel2ChunksDataProvider&&) noexcept = default; +float AwsLevel2ChunksDataProvider::GetCurrentElevation() +{ + if (!p->currentScan_.valid_ || p->currentScan_.nexradFile_ == nullptr) + { + // Does not have any scan elevation. -90 is beyond what is possible + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return -90; + } + + auto vcpData = p->currentScan_.nexradFile_->vcp_data(); + auto radarData = p->currentScan_.nexradFile_->radar_data(); + if (radarData.size() == 0) + { + // Does not have any scan elevation. -90 is beyond what is possible + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return -90; + } + + const auto& lastElevation = radarData.crbegin(); + std::shared_ptr digitalRadarData0 = + std::dynamic_pointer_cast( + lastElevation->second->cbegin()->second); + + if (vcpData != nullptr) + { + // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions) Float is plenty + return vcpData->elevation_angle(lastElevation->first); + } + else if (digitalRadarData0 != nullptr) + { + return digitalRadarData0->elevation_angle().value(); + } + + // Does not have any scan elevation. -90 is beyond what is possible + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return -90; +} + } // namespace scwx::provider From 0f95439b61621e5a20a472edfeaefc950612434e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 13:49:41 -0400 Subject: [PATCH 562/762] Initial clang format/tidy fixes for level_2_chunks --- .../scwx/qt/manager/radar_product_manager.cpp | 24 ++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 1 - .../scwx/qt/map/radar_product_layer.cpp | 2 +- .../scwx/qt/ui/level2_settings_widget.cpp | 32 +++-- .../aws_level2_chunks_data_provider.hpp | 1 + .../scwx/provider/nexrad_data_provider.hpp | 7 +- .../aws_level2_chunks_data_provider.cpp | 111 +++++++++--------- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 31 ++--- .../rda/volume_coverage_pattern_data.cpp | 2 +- 9 files changed, 108 insertions(+), 103 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index ed952ed7..39501b29 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -272,7 +272,8 @@ public: common::Level3ProductCategoryMap availableCategoryMap_ {}; std::shared_mutex availableCategoryMutex_ {}; - float incomingLevel2Elevation_ {-90}; + float incomingLevel2Elevation_ { + provider::AwsLevel2ChunksDataProvider::INVALID_ELEVATION}; std::unordered_map, @@ -654,7 +655,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, { if (group == common::RadarProductGroup::Level2) { - //p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); + // p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); p->EnableRefresh(uuid, p->level2ChunksProviderManager_, enabled); } else @@ -1431,7 +1432,7 @@ std::shared_ptr RadarProductManagerImpl::StoreRadarProductRecord( std::shared_ptr record) { - //logger_->debug("StoreRadarProductRecord()"); + // logger_->debug("StoreRadarProductRecord()"); std::shared_ptr storedRecord = nullptr; @@ -1571,9 +1572,10 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, if (record != nullptr) { - std::shared_ptr recordRadarData = nullptr; - float recordElevationCut = 0.0f; - std::vector recordElevationCuts; + std::shared_ptr recordRadarData = + nullptr; + float recordElevationCut = 0.0f; + std::vector recordElevationCuts; std::tie(recordRadarData, recordElevationCut, recordElevationCuts) = record->level2_file()->GetElevationScan( @@ -1595,10 +1597,14 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, elevationCuts = std::move(recordElevationCuts); foundTime = collectionTime; - if (p->incomingLevel2Elevation_ != -90) + if (p->incomingLevel2Elevation_ != + provider::AwsLevel2ChunksDataProvider::INVALID_ELEVATION) { - p->incomingLevel2Elevation_ = -90; - Q_EMIT IncomingLevel2ElevationChanged(-90); + p->incomingLevel2Elevation_ = provider:: + AwsLevel2ChunksDataProvider::INVALID_ELEVATION; + Q_EMIT IncomingLevel2ElevationChanged( + provider::AwsLevel2ChunksDataProvider:: + INVALID_ELEVATION); } } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index edb4c3d2..845e0a51 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -661,7 +661,6 @@ float MapWidget::GetIncomingLevel2Elevation() const return p->radarProductManager_->incoming_level_2_elevation(); } - common::Level2Product MapWidgetImpl::GetLevel2ProductOrDefault(const std::string& productName) const { diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 8640ba03..ee6b3c3c 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -170,7 +170,7 @@ void RadarProductLayer::UpdateSweep() std::try_to_lock); if (!sweepLock.owns_lock()) { - //logger_->debug("Sweep locked, deferring update"); + // logger_->debug("Sweep locked, deferring update"); return; } logger_->debug("UpdateSweep()"); diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 4a3d0967..78e4de37 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -30,15 +30,10 @@ public: explicit Level2SettingsWidgetImpl(Level2SettingsWidget* self) : self_ {self}, layout_ {new QVBoxLayout(self)}, - elevationGroupBox_ {}, - incomingElevationLabel_ {}, elevationButtons_ {}, - elevationCuts_ {}, - elevationButtonsChanged_ {false}, - resizeElevationButtons_ {false}, - settingsGroupBox_ {}, - declutterCheckBox_ {} + elevationCuts_ {} { + // NOLINTBEGIN(cppcoreguidelines-owning-memory) Qt takes care of this layout_->setContentsMargins(0, 0, 0, 0); incomingElevationLabel_ = new QLabel("", self); @@ -48,7 +43,6 @@ public: new ui::FlowLayout(elevationGroupBox_); layout_->addWidget(elevationGroupBox_); - settingsGroupBox_ = new QGroupBox(tr("Settings"), self); QLayout* settingsLayout = new QVBoxLayout(settingsGroupBox_); layout_->addWidget(settingsGroupBox_); @@ -57,6 +51,7 @@ public: settingsLayout->addWidget(declutterCheckBox_); settingsGroupBox_->setVisible(false); + // NOLINTEND(cppcoreguidelines-owning-memory) Qt takes care of this QObject::connect(hotkeyManager_.get(), &manager::HotkeyManager::HotkeyPressed, @@ -72,15 +67,15 @@ public: Level2SettingsWidget* self_; QLayout* layout_; - QGroupBox* elevationGroupBox_; - QLabel* incomingElevationLabel_; + QGroupBox* elevationGroupBox_ {}; + QLabel* incomingElevationLabel_ {}; std::list elevationButtons_; std::vector elevationCuts_; - bool elevationButtonsChanged_; - bool resizeElevationButtons_; + bool elevationButtonsChanged_ {}; + bool resizeElevationButtons_ {}; - QGroupBox* settingsGroupBox_; - QCheckBox* declutterCheckBox_; + QGroupBox* settingsGroupBox_ {}; + QCheckBox* declutterCheckBox_ {}; float currentElevation_ {}; QToolButton* currentElevationButton_ {nullptr}; @@ -249,8 +244,9 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) void Level2SettingsWidget::UpdateIncomingElevation(float incomingElevation) { - p->incomingElevationLabel_->setText("Incoming Elevation: " + - QString::number(incomingElevation, 'f', 1) + common::Characters::DEGREE); + p->incomingElevationLabel_->setText( + "Incoming Elevation: " + QString::number(incomingElevation, 'f', 1) + + common::Characters::DEGREE); } void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) @@ -258,8 +254,8 @@ void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) std::optional currentElevationOption = activeMap->GetElevation(); const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; - std::vector elevationCuts = activeMap->GetElevationCuts(); - float incomingElevation = activeMap->GetIncomingLevel2Elevation(); + std::vector elevationCuts = activeMap->GetElevationCuts(); + const float incomingElevation = activeMap->GetIncomingLevel2Elevation(); if (p->elevationCuts_ != elevationCuts) { diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index a82dffb2..052f639b 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -16,6 +16,7 @@ namespace scwx::provider class AwsLevel2ChunksDataProvider : public NexradDataProvider { public: + constexpr static const float INVALID_ELEVATION = -90.0; explicit AwsLevel2ChunksDataProvider(const std::string& radarSite); explicit AwsLevel2ChunksDataProvider(const std::string& radarSite, const std::string& bucketName, diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index a0e09f04..dc490be0 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -103,17 +103,14 @@ public: * * @return NEXRAD data */ - virtual std::shared_ptr - LoadLatestObject() = 0; + virtual std::shared_ptr LoadLatestObject() = 0; /** * Loads the second NEXRAD file object * * @return NEXRAD data */ - virtual std::shared_ptr - LoadSecondLatestObject() = 0; - + virtual std::shared_ptr LoadSecondLatestObject() = 0; /** * Lists NEXRAD objects for the current date, and adds them to the cache. If diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 0d97c7b8..16fb0e44 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -73,6 +73,7 @@ public: currentScan_ {"", false}, scansMutex_ {}, lastTimeListed_ {}, + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) about average updatePeriod_ {7}, self_ {self} { @@ -80,10 +81,11 @@ public: util::SetEnvironment("AWS_EC2_METADATA_DISABLED", "true"); // Use anonymous credentials - Aws::Auth::AWSCredentials credentials {}; + const Aws::Auth::AWSCredentials credentials {}; Aws::Client::ClientConfiguration config; - config.region = region_; + config.region = region_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) arbitrary config.connectTimeoutMs = 10000; client_ = std::make_shared( @@ -104,10 +106,9 @@ public: const std::chrono::system_clock::time_point& time, int last); - bool LoadScan(Impl::ScanRecord& scanRecord); + bool LoadScan(Impl::ScanRecord& scanRecord); std::tuple ListObjects(); - std::string radarSite_; std::string bucketName_; std::string region_; @@ -190,6 +191,7 @@ size_t AwsLevel2ChunksDataProvider::cache_size() const std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::last_modified() const { + const std::shared_lock lock(p->scansMutex_); if (p->currentScan_.valid_ && p->currentScan_.lastModified_ != std::chrono::system_clock::time_point {}) { @@ -207,7 +209,7 @@ AwsLevel2ChunksDataProvider::last_modified() const } std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const { - std::shared_lock lock(p->scansMutex_); + const std::shared_lock lock(p->scansMutex_); // Add an extra second of delay static const auto extra = std::chrono::seconds(2); // get update period from time between chunks @@ -233,7 +235,7 @@ AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) { logger_->debug("FindKey: {}", util::TimeString(time)); - std::shared_lock lock(p->scansMutex_); + const std::shared_lock lock(p->scansMutex_); if (p->currentScan_.valid_ && time >= p->currentScan_.time_) { return p->currentScan_.prefix_; @@ -248,7 +250,7 @@ AwsLevel2ChunksDataProvider::FindKey(std::chrono::system_clock::time_point time) std::string AwsLevel2ChunksDataProvider::FindLatestKey() { - std::shared_lock lock(p->scansMutex_); + const std::shared_lock lock(p->scansMutex_); if (!p->currentScan_.valid_) { return ""; @@ -260,7 +262,7 @@ std::string AwsLevel2ChunksDataProvider::FindLatestKey() std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::FindLatestTime() { - std::shared_lock lock(p->scansMutex_); + const std::shared_lock lock(p->scansMutex_); if (!p->currentScan_.valid_) { return {}; @@ -325,10 +327,11 @@ std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( std::tuple AwsLevel2ChunksDataProvider::Impl::ListObjects() { - size_t newObjects = 0; - size_t totalObjects = 0; + size_t newObjects = 0; + const size_t totalObjects = 0; - std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + const std::chrono::system_clock::time_point now = + std::chrono::system_clock::now(); if (currentScan_.valid_ && !currentScan_.hasAllFiles_ && lastTimeListed_ + std::chrono::minutes(2) > now) @@ -366,8 +369,8 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() int lastScanNumber = -1; // Start with last scan - int previousScanNumber = scanNumberMap.crbegin()->first; - const int firstScanNumber = scanNumberMap.cbegin()->first; + int previousScanNumber = scanNumberMap.crbegin()->first; + const int firstScanNumber = scanNumberMap.cbegin()->first; // Look for a gap in scan numbers. This indicates that is the latest // scan. @@ -383,7 +386,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() { // Have already checked scan with highest number, so skip first previousScanNumber = firstScanNumber; - bool first = true; + bool first = true; for (const auto& scan : scanNumberMap) { if (first) @@ -415,15 +418,16 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() } } - std::string& lastScanPrefix = scanNumberMap.at(lastScanNumber); - int secondLastScanNumber = + const std::string& lastScanPrefix = scanNumberMap.at(lastScanNumber); + const int secondLastScanNumber = + // 999 is the last file possible + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) lastScanNumber == 1 ? 999 : lastScanNumber - 1; const auto& secondLastScanPrefix = scanNumberMap.find(secondLastScanNumber); - if (!currentScan_.valid_ || - currentScan_.prefix_ != lastScanPrefix) + if (!currentScan_.valid_ || currentScan_.prefix_ != lastScanPrefix) { if (currentScan_.valid_ && (secondLastScanPrefix == scanNumberMap.cend() || @@ -445,9 +449,9 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() newObjects += 1; } - currentScan_.valid_ = true; - currentScan_.prefix_ = lastScanPrefix; - currentScan_.nexradFile_ = nullptr; + currentScan_.valid_ = true; + currentScan_.prefix_ = lastScanPrefix; + currentScan_.nexradFile_ = nullptr; currentScan_.time_ = GetScanTime(lastScanPrefix); currentScan_.lastModified_ = {}; currentScan_.secondLastModified_ = {}; @@ -502,7 +506,7 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) return false; } - bool hasNew = false; + bool hasNew = false; auto& chunks = listOutcome.GetResult().GetContents(); logger_->debug("Found {} new chunks.", chunks.size()); for (const auto& chunk : chunks) @@ -511,27 +515,26 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) // KIND/585/20250324-134727-001-S // KIND/5/20250324-134727-001-S - static const size_t firstSlash = std::string("KIND/").size(); - const size_t secondSlash = key.find('/', firstSlash); + static const size_t firstSlash = std::string("KIND/").size(); + const size_t secondSlash = key.find('/', firstSlash); static const size_t startNumberPosOffset = std::string("/20250324-134727-").size(); - const size_t startNumberPos = - secondSlash + startNumberPosOffset; - const std::string& keyNumberStr = key.substr(startNumberPos, 3); - const int keyNumber = std::stoi(keyNumberStr); - if (keyNumber != scanRecord.nextFile_) + const size_t startNumberPos = secondSlash + startNumberPosOffset; + const std::string& keyNumberStr = key.substr(startNumberPos, 3); + const int keyNumber = std::stoi(keyNumberStr); + // As far as order goes, only the first one matters. This may cause some + // issues if keys come in out of order, but usually they just skip chunks + if (scanRecord.nextFile_ == 1 && keyNumber != scanRecord.nextFile_) { - logger_->warn("Chunk found that was not in order {} {} {}", - key, - scanRecord.nextFile_, - keyNumber); + logger_->warn("Chunk found that was not in order {} {}", + scanRecord.lastKey_, + key); continue; } // Now we want the ending char // KIND/585/20250324-134727-001-S - static const size_t charPos = - std::string("/20250324-134727-001-").size(); + static const size_t charPos = std::string("/20250324-134727-001-").size(); if (secondSlash + charPos >= key.size()) { logger_->warn("Chunk key was not long enough"); @@ -591,15 +594,16 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) } hasNew = true; - std::chrono::seconds lastModifiedSeconds { + const std::chrono::seconds lastModifiedSeconds { outcome.GetResult().GetLastModified().Seconds()}; - std::chrono::system_clock::time_point lastModified {lastModifiedSeconds}; + const std::chrono::system_clock::time_point lastModified { + lastModifiedSeconds}; scanRecord.secondLastModified_ = scanRecord.lastModified_; - scanRecord.lastModified_ = lastModified; + scanRecord.lastModified_ = lastModified; - scanRecord.nextFile_ += 1; - scanRecord.lastKey_ = key; + scanRecord.nextFile_ = keyNumber + 1; + scanRecord.lastKey_ = key; } if (scanRecord.nexradFile_ == nullptr) @@ -618,7 +622,7 @@ std::shared_ptr AwsLevel2ChunksDataProvider::LoadObjectByTime( std::chrono::system_clock::time_point time) { - std::unique_lock lock(p->scansMutex_); + const std::unique_lock lock(p->scansMutex_); static const std::chrono::system_clock::time_point epoch {}; if (p->currentScan_.valid_ && @@ -640,10 +644,10 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( std::shared_ptr AwsLevel2ChunksDataProvider::LoadLatestObject() { - std::unique_lock lock(p->scansMutex_); + const std::unique_lock lock(p->scansMutex_); return std::make_shared(p->currentScan_.nexradFile_, p->lastScan_.nexradFile_); - //return p->currentScan_.nexradFile_; + // return p->currentScan_.nexradFile_; } std::shared_ptr @@ -667,8 +671,8 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() boost::timer::cpu_timer timer {}; timer.start(); - std::unique_lock lock(p->refreshMutex_); - std::unique_lock scanLock(p->scansMutex_); + const std::unique_lock lock(p->refreshMutex_); + const std::unique_lock scanLock(p->scansMutex_); auto [success, newObjects, totalObjects] = p->ListObjects(); @@ -682,6 +686,8 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() } if (p->lastScan_.valid_) { + // TODO this is slow when initially loading data. If possible, loading + // this from the archive may speed it up a lot. if (p->LoadScan(p->lastScan_)) { newObjects += 1; @@ -690,6 +696,7 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() } timer.stop(); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) format to 6 digits logger_->debug("Refresh() in {}", timer.format(6, "%ws")); return std::make_pair(newObjects, totalObjects); } @@ -710,8 +717,7 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() if (!p->currentScan_.valid_ || p->currentScan_.nexradFile_ == nullptr) { // Does not have any scan elevation. -90 is beyond what is possible - // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - return -90; + return INVALID_ELEVATION; } auto vcpData = p->currentScan_.nexradFile_->vcp_data(); @@ -719,18 +725,17 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() if (radarData.size() == 0) { // Does not have any scan elevation. -90 is beyond what is possible - // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - return -90; + return INVALID_ELEVATION; } const auto& lastElevation = radarData.crbegin(); - std::shared_ptr digitalRadarData0 = + const std::shared_ptr digitalRadarData0 = std::dynamic_pointer_cast( lastElevation->second->cbegin()->second); if (vcpData != nullptr) { - // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions) Float is plenty + // NOLINTNEXTLINE(*-narrowing-conversions) Float is plenty return vcpData->elevation_angle(lastElevation->first); } else if (digitalRadarData0 != nullptr) @@ -738,9 +743,7 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() return digitalRadarData0->elevation_angle().value(); } - // Does not have any scan elevation. -90 is beyond what is possible - // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - return -90; + return INVALID_ELEVATION; } } // namespace scwx::provider diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index f804685b..d9335b50 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -138,7 +138,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) const { - //logger_->debug("GetElevationScan: {} degrees", elevation); + // logger_->debug("GetElevationScan: {} degrees", elevation); std::shared_ptr elevationScan = nullptr; float elevationCut = 0.0f; @@ -273,7 +273,7 @@ bool Ar2vFile::LoadData(std::istream& is) std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) { - //logger_->debug("Decompressing LDM Records"); + // logger_->debug("Decompressing LDM Records"); std::size_t numRecords = 0; @@ -321,22 +321,22 @@ std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) ++numRecords; } - //logger_->debug("Decompressed {} LDM Records", numRecords); + // logger_->debug("Decompressed {} LDM Records", numRecords); return numRecords; } void Ar2vFileImpl::ParseLDMRecords() { - //logger_->debug("Parsing LDM Records"); + // logger_->debug("Parsing LDM Records"); - //std::size_t count = 0; + // std::size_t count = 0; for (auto it = rawRecords_.begin(); it != rawRecords_.end(); it++) { std::stringstream& ss = *it; - //logger_->trace("Record {}", count++); + // logger_->trace("Record {}", count++); ParseLDMRecord(ss); } @@ -445,7 +445,7 @@ void Ar2vFileImpl::ProcessRadarData( void Ar2vFileImpl::IndexFile() { - //logger_->debug("Indexing file"); + // logger_->debug("Indexing file"); for (auto& elevationCut : radarData_) { @@ -465,6 +465,7 @@ void Ar2vFileImpl::IndexFile() if (vcpData_ != nullptr) { + // NOLINTNEXTLINE(*-narrowing-conversions) Float is plenty elevationAngle = vcpData_->elevation_angle(elevationCut.first); waveformType = vcpData_->waveform_type(elevationCut.first); } @@ -500,15 +501,15 @@ void Ar2vFileImpl::IndexFile() auto time = util::TimePoint(radial0->modified_julian_date(), radial0->collection_time()); - index_[dataBlockType][elevationAngle][time] = - elevationCut.second; + index_[dataBlockType][elevationAngle][time] = elevationCut.second; } } } } -bool Ar2vFile::LoadLDMRecords(std::istream& is) { - size_t decompressedRecords = p->DecompressLDMRecords(is); +bool Ar2vFile::LoadLDMRecords(std::istream& is) +{ + const size_t decompressedRecords = p->DecompressLDMRecords(is); if (decompressedRecords == 0) { p->ParseLDMRecord(is); @@ -528,6 +529,7 @@ bool Ar2vFile::IndexFile() } // TODO not good +// NOLINTNEXTLINE bool IsRadarDataIncomplete( const std::shared_ptr& radarData) { @@ -695,12 +697,13 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, // Find the highest elevation this type has for the current scan // Start below any reasonable elevation // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - float highestCurrentElevation = -90; - const auto& maybe1 = p->index_.find(type.first); + float highestCurrentElevation = -90; + const auto& maybe1 = p->index_.find(type.first); if (maybe1 != p->index_.cend()) { const auto& maybe2 = maybe1->second.crbegin(); - if (maybe2 != maybe1->second.crend()) { + if (maybe2 != maybe1->second.crend()) + { // Add a slight offset to ensure good floating point compare. // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) highestCurrentElevation = maybe2->first + 0.01f; diff --git a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp index 42ed3685..a30a2df1 100644 --- a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp @@ -221,8 +221,8 @@ uint16_t VolumeCoveragePatternData::number_of_base_tilts() const double VolumeCoveragePatternData::elevation_angle(uint16_t e) const { - // NOLINTNEXTLINE This conversion is accurate float elevationAngleConverted = + // NOLINTNEXTLINE This conversion is accurate p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; // Any elevation above 90 degrees should be interpreted as a // negative angle From 0ac0e03ff88458b78b51b79964437e6e1600ef86 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 13:55:23 -0400 Subject: [PATCH 563/762] Relaod all the settings, just to make sure everything is updated --- scwx-qt/source/scwx/qt/main/main_window.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 089d6749..8308be9b 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -967,8 +967,7 @@ void MainWindowImpl::ConnectMapSignals() mapWidget, &map::MapWidget::IncomingLevel2ElevationChanged, this, - [this](float incomingElevation) - { level2SettingsWidget_->UpdateIncomingElevation(incomingElevation); }, + [this](float) { level2SettingsWidget_->UpdateSettings(activeMap_); }, Qt::QueuedConnection); } } From 34eb3af69806dd6e8e91e0baa67b077e67068128 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 14:32:09 -0400 Subject: [PATCH 564/762] Use static cast when getting elevations to convert from double to float --- .../source/scwx/provider/aws_level2_chunks_data_provider.cpp | 3 +-- wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 16fb0e44..77de4593 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -735,8 +735,7 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() if (vcpData != nullptr) { - // NOLINTNEXTLINE(*-narrowing-conversions) Float is plenty - return vcpData->elevation_angle(lastElevation->first); + return static_cast(vcpData->elevation_angle(lastElevation->first)); } else if (digitalRadarData0 != nullptr) { diff --git a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp index a30a2df1..b2159648 100644 --- a/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp @@ -221,8 +221,7 @@ uint16_t VolumeCoveragePatternData::number_of_base_tilts() const double VolumeCoveragePatternData::elevation_angle(uint16_t e) const { - float elevationAngleConverted = - // NOLINTNEXTLINE This conversion is accurate + double elevationAngleConverted = p->elevationCuts_[e].elevationAngle_ * ANGLE_DATA_SCALE; // Any elevation above 90 degrees should be interpreted as a // negative angle From 314d3f5b9b9fb6f8c3812369f112bb54cee150ef Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 15:17:36 -0400 Subject: [PATCH 565/762] Use static cast when getting elevations to convert from double to float take 2 --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index d9335b50..6d0e5c02 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -465,8 +465,8 @@ void Ar2vFileImpl::IndexFile() if (vcpData_ != nullptr) { - // NOLINTNEXTLINE(*-narrowing-conversions) Float is plenty - elevationAngle = vcpData_->elevation_angle(elevationCut.first); + elevationAngle = + static_cast(vcpData_->elevation_angle(elevationCut.first)); waveformType = vcpData_->waveform_type(elevationCut.first); } else if ((digitalRadarData0 = From 1f0d2a7a66a80c3dc2df4667d7d9f19bf0c673f2 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Apr 2025 16:18:45 -0400 Subject: [PATCH 566/762] Change the selection of the most recent level 2 scan to avoid certain improper removal from causing issues. --- .../aws_level2_chunks_data_provider.cpp | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 77de4593..fc6c022a 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -367,7 +367,6 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() scanPrefix); } - int lastScanNumber = -1; // Start with last scan int previousScanNumber = scanNumberMap.crbegin()->first; const int firstScanNumber = scanNumberMap.cbegin()->first; @@ -375,50 +374,54 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() // Look for a gap in scan numbers. This indicates that is the latest // scan. - // This indicates that highest number scan is the last scan + auto possibleLastNumbers = std::unordered_set(); + // This indicates that highest number scan may be the last scan // (including if there is only 1 scan) // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - if (previousScanNumber != 999 || scans.size() == 1) + if (previousScanNumber != 999 || firstScanNumber != 1) { - lastScanNumber = previousScanNumber; + possibleLastNumbers.emplace(previousScanNumber); } - else + // Have already checked scan with highest number, so skip first + previousScanNumber = firstScanNumber; + bool first = true; + for (const auto& scan : scanNumberMap) { - // Have already checked scan with highest number, so skip first - previousScanNumber = firstScanNumber; - bool first = true; - for (const auto& scan : scanNumberMap) + if (first) { - if (first) - { - first = false; - continue; - } - if (scan.first != previousScanNumber + 1) - { - lastScanNumber = previousScanNumber; - break; - } - previousScanNumber = scan.first; + first = false; + continue; + } + if (scan.first != previousScanNumber + 1) + { + possibleLastNumbers.emplace(previousScanNumber); + } + previousScanNumber = scan.first; + } + + if (possibleLastNumbers.empty()) + { + logger_->warn("Could not find last scan"); + // TODO make sure this makes sence + return {false, 0, 0}; + } + + int lastScanNumber = -1; + std::chrono::system_clock::time_point lastScanTime = {}; + std::string lastScanPrefix; + + for (const int scanNumber : possibleLastNumbers) + { + const std::string& scanPrefix = scanNumberMap.at(scanNumber); + auto scanTime = GetScanTime(scanPrefix); + if (scanTime > lastScanTime) + { + lastScanTime = scanTime; + lastScanPrefix = scanPrefix; + lastScanNumber = scanNumber; } } - if (lastScanNumber == -1) - { - // 999 is the last scan - if (firstScanNumber != 1) - { - lastScanNumber = previousScanNumber; - } - else - { - logger_->warn("Could not find last scan"); - // TODO make sure this makes sence - return {false, 0, 0}; - } - } - - const std::string& lastScanPrefix = scanNumberMap.at(lastScanNumber); const int secondLastScanNumber = // 999 is the last file possible // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) @@ -452,7 +455,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() currentScan_.valid_ = true; currentScan_.prefix_ = lastScanPrefix; currentScan_.nexradFile_ = nullptr; - currentScan_.time_ = GetScanTime(lastScanPrefix); + currentScan_.time_ = lastScanTime; currentScan_.lastModified_ = {}; currentScan_.secondLastModified_ = {}; currentScan_.lastKey_ = ""; From 309a5ed25e796bc08dd0f9e06ed3404dccb524d7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Thu, 10 Apr 2025 13:28:06 -0400 Subject: [PATCH 567/762] Clean up some functions in chunks data provider --- .../aws_level2_chunks_data_provider.cpp | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index fc6c022a..228426c2 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -101,10 +101,7 @@ public: Impl& operator=(Impl&&) = delete; std::chrono::system_clock::time_point GetScanTime(const std::string& prefix); - int GetScanNumber(const std::string& prefix); - std::string GetScanKey(const std::string& prefix, - const std::chrono::system_clock::time_point& time, - int last); + int GetScanNumber(const std::string& prefix); bool LoadScan(Impl::ScanRecord& scanRecord); std::tuple ListObjects(); @@ -311,19 +308,6 @@ AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) return {}; } -std::string AwsLevel2ChunksDataProvider::Impl::GetScanKey( - const std::string& prefix, - const std::chrono::system_clock::time_point& time, - int last) -{ - - static const std::string timeFormat {"%Y%m%d-%H%M%S"}; - - // TODO - return fmt::format( - "{0}/{1:%Y%m%d-%H%M%S}-{2}", prefix, fmt::gmtime(time), last - 1); -} - std::tuple AwsLevel2ChunksDataProvider::Impl::ListObjects() { @@ -406,23 +390,23 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() return {false, 0, 0}; } - int lastScanNumber = -1; - std::chrono::system_clock::time_point lastScanTime = {}; - std::string lastScanPrefix; + int lastScanNumber = -1; + std::chrono::system_clock::time_point lastScanTime = {}; + std::string lastScanPrefix; for (const int scanNumber : possibleLastNumbers) { const std::string& scanPrefix = scanNumberMap.at(scanNumber); - auto scanTime = GetScanTime(scanPrefix); + auto scanTime = GetScanTime(scanPrefix); if (scanTime > lastScanTime) { - lastScanTime = scanTime; + lastScanTime = scanTime; lastScanPrefix = scanPrefix; lastScanNumber = scanNumber; } } - const int secondLastScanNumber = + const int secondLastScanNumber = // 999 is the last file possible // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) lastScanNumber == 1 ? 999 : lastScanNumber - 1; From 16a73ed872a9a4fd9c19077ce37463298d6d6f36 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 18 Apr 2025 10:59:19 -0400 Subject: [PATCH 568/762] Add previous scans for stepping back in time when merging level2 files --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 6d0e5c02..b4dc6248 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -568,6 +568,15 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, continue; } + // Add previous scans for stepping back in time + for (auto scan = ++(elevation.second.rbegin()); + scan != elevation.second.rend(); + ++scan) + { + p->index_[type.first][elevation.first][scan->first] = + scan->second; + } + // Merge this scan with the last one if it is incomplete if (IsRadarDataIncomplete(mostRecent->second)) { From f481d57ed1a53dcd4e7905fab5ca3c7fad97b4bf Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 18 Apr 2025 11:12:39 -0400 Subject: [PATCH 569/762] clang format/tidy fixes for level2_chunks --- scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 78e4de37..582cf64b 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -254,8 +254,8 @@ void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) std::optional currentElevationOption = activeMap->GetElevation(); const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; - std::vector elevationCuts = activeMap->GetElevationCuts(); - const float incomingElevation = activeMap->GetIncomingLevel2Elevation(); + const std::vector elevationCuts = activeMap->GetElevationCuts(); + const float incomingElevation = activeMap->GetIncomingLevel2Elevation(); if (p->elevationCuts_ != elevationCuts) { From e10ebdeb5e5424270be3347fbd3e4305cf80798c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 20 Apr 2025 10:56:50 -0400 Subject: [PATCH 570/762] switch level2 incoming elevation to optional --- scwx-qt/source/scwx/qt/main/main_window.cpp | 3 ++- .../scwx/qt/manager/radar_product_manager.cpp | 16 ++++++-------- .../scwx/qt/manager/radar_product_manager.hpp | 8 +++---- scwx-qt/source/scwx/qt/map/map_widget.cpp | 4 ++-- scwx-qt/source/scwx/qt/map/map_widget.hpp | 4 ++-- .../scwx/qt/ui/level2_settings_widget.cpp | 21 +++++++++++++------ .../scwx/qt/ui/level2_settings_widget.hpp | 4 +++- .../aws_level2_chunks_data_provider.hpp | 5 +++-- .../aws_level2_chunks_data_provider.cpp | 8 +++---- 9 files changed, 41 insertions(+), 32 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 8308be9b..30e3b699 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -967,7 +967,8 @@ void MainWindowImpl::ConnectMapSignals() mapWidget, &map::MapWidget::IncomingLevel2ElevationChanged, this, - [this](float) { level2SettingsWidget_->UpdateSettings(activeMap_); }, + [this](std::optional) + { level2SettingsWidget_->UpdateSettings(activeMap_); }, Qt::QueuedConnection); } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 39501b29..1814905d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -272,8 +272,7 @@ public: common::Level3ProductCategoryMap availableCategoryMap_ {}; std::shared_mutex availableCategoryMutex_ {}; - float incomingLevel2Elevation_ { - provider::AwsLevel2ChunksDataProvider::INVALID_ELEVATION}; + std::optional incomingLevel2Elevation_ {}; std::unordered_map, @@ -454,7 +453,7 @@ float RadarProductManager::gate_size() const return (is_tdwr()) ? 150.0f : 250.0f; } -float RadarProductManager::incoming_level_2_elevation() const +std::optional RadarProductManager::incoming_level_2_elevation() const { return p->incomingLevel2Elevation_; } @@ -1552,7 +1551,7 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, scwx::util::TimePoint(radarData0->modified_julian_date(), radarData0->collection_time())); - const float incomingElevation = + const std::optional incomingElevation = std::dynamic_pointer_cast( p->level2ChunksProviderManager_->provider_) ->GetCurrentElevation(); @@ -1597,14 +1596,11 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, elevationCuts = std::move(recordElevationCuts); foundTime = collectionTime; - if (p->incomingLevel2Elevation_ != - provider::AwsLevel2ChunksDataProvider::INVALID_ELEVATION) + if (!p->incomingLevel2Elevation_.has_value()) { - p->incomingLevel2Elevation_ = provider:: - AwsLevel2ChunksDataProvider::INVALID_ELEVATION; + p->incomingLevel2Elevation_ = {}; Q_EMIT IncomingLevel2ElevationChanged( - provider::AwsLevel2ChunksDataProvider:: - INVALID_ELEVATION); + p->incomingLevel2Elevation_); } } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 6efd125d..c0a49dff 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -45,9 +45,9 @@ public: coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; [[nodiscard]] float gate_size() const; - [[nodiscard]] float incoming_level_2_elevation() const; - [[nodiscard]] bool is_tdwr() const; - [[nodiscard]] std::string radar_id() const; + [[nodiscard]] std::optional incoming_level_2_elevation() const; + [[nodiscard]] bool is_tdwr() const; + [[nodiscard]] std::string radar_id() const; [[nodiscard]] std::shared_ptr radar_site() const; void Initialize(); @@ -149,7 +149,7 @@ signals: void NewDataAvailable(common::RadarProductGroup group, const std::string& product, std::chrono::system_clock::time_point latestTime); - void IncomingLevel2ElevationChanged(float incomingElevation); + void IncomingLevel2ElevationChanged(std::optional incomingElevation); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 845e0a51..ad808626 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -656,7 +656,7 @@ std::vector MapWidget::GetElevationCuts() const } } -float MapWidget::GetIncomingLevel2Elevation() const +std::optional MapWidget::GetIncomingLevel2Elevation() const { return p->radarProductManager_->incoming_level_2_elevation(); } @@ -1804,7 +1804,7 @@ void MapWidgetImpl::RadarProductManagerConnect() connect(radarProductManager_.get(), &manager::RadarProductManager::IncomingLevel2ElevationChanged, this, - [this](float incomingElevation) + [this](std::optional incomingElevation) { Q_EMIT widget_->IncomingLevel2ElevationChanged( incomingElevation); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 5cc2e0a1..d474cd2e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -45,7 +45,7 @@ public: GetAvailableLevel3Categories(); [[nodiscard]] std::optional GetElevation() const; [[nodiscard]] std::vector GetElevationCuts() const; - [[nodiscard]] float GetIncomingLevel2Elevation() const; + [[nodiscard]] std::optional GetIncomingLevel2Elevation() const; [[nodiscard]] std::vector GetLevel3Products(); [[nodiscard]] std::string GetMapStyle() const; [[nodiscard]] common::RadarProductGroup GetRadarProductGroup() const; @@ -185,7 +185,7 @@ signals: void RadarSweepUpdated(); void RadarSweepNotUpdated(types::NoUpdateReason reason); void WidgetPainted(); - void IncomingLevel2ElevationChanged(float incomingElevation); + void IncomingLevel2ElevationChanged(std::optional incomingElevation); }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index 582cf64b..3d530733 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -242,11 +242,19 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) p->currentElevationButton_ = newElevationButton; } -void Level2SettingsWidget::UpdateIncomingElevation(float incomingElevation) +void Level2SettingsWidget::UpdateIncomingElevation( + std::optional incomingElevation) { - p->incomingElevationLabel_->setText( - "Incoming Elevation: " + QString::number(incomingElevation, 'f', 1) + - common::Characters::DEGREE); + if (incomingElevation.has_value()) + { + p->incomingElevationLabel_->setText( + "Incoming Elevation: " + QString::number(*incomingElevation, 'f', 1) + + common::Characters::DEGREE); + } + else + { + p->incomingElevationLabel_->setText("Incoming Elevation: None"); + } } void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) @@ -254,8 +262,9 @@ void Level2SettingsWidget::UpdateSettings(map::MapWidget* activeMap) std::optional currentElevationOption = activeMap->GetElevation(); const float currentElevation = currentElevationOption.has_value() ? *currentElevationOption : 0.0f; - const std::vector elevationCuts = activeMap->GetElevationCuts(); - const float incomingElevation = activeMap->GetIncomingLevel2Elevation(); + const std::vector elevationCuts = activeMap->GetElevationCuts(); + const std::optional incomingElevation = + activeMap->GetIncomingLevel2Elevation(); if (p->elevationCuts_ != elevationCuts) { diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp index 796b1ade..32f788bb 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.hpp @@ -2,6 +2,8 @@ #include +#include + namespace scwx { namespace qt @@ -23,7 +25,7 @@ public: void showEvent(QShowEvent* event) override; void UpdateElevationSelection(float elevation); - void UpdateIncomingElevation(float incomingElevation); + void UpdateIncomingElevation(std::optional incomingElevation); void UpdateSettings(map::MapWidget* activeMap); signals: diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 052f639b..976f0663 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -2,6 +2,8 @@ #include +#include + namespace Aws::S3 { class S3Client; @@ -16,7 +18,6 @@ namespace scwx::provider class AwsLevel2ChunksDataProvider : public NexradDataProvider { public: - constexpr static const float INVALID_ELEVATION = -90.0; explicit AwsLevel2ChunksDataProvider(const std::string& radarSite); explicit AwsLevel2ChunksDataProvider(const std::string& radarSite, const std::string& bucketName, @@ -58,7 +59,7 @@ public: void RequestAvailableProducts() override; std::vector GetAvailableProducts() override; - float GetCurrentElevation(); + std::optional GetCurrentElevation(); private: class Impl; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 228426c2..ddac82e5 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -699,12 +699,12 @@ AwsLevel2ChunksDataProvider::AwsLevel2ChunksDataProvider( AwsLevel2ChunksDataProvider& AwsLevel2ChunksDataProvider::operator=( AwsLevel2ChunksDataProvider&&) noexcept = default; -float AwsLevel2ChunksDataProvider::GetCurrentElevation() +std::optional AwsLevel2ChunksDataProvider::GetCurrentElevation() { if (!p->currentScan_.valid_ || p->currentScan_.nexradFile_ == nullptr) { // Does not have any scan elevation. -90 is beyond what is possible - return INVALID_ELEVATION; + return {}; } auto vcpData = p->currentScan_.nexradFile_->vcp_data(); @@ -712,7 +712,7 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() if (radarData.size() == 0) { // Does not have any scan elevation. -90 is beyond what is possible - return INVALID_ELEVATION; + return {}; } const auto& lastElevation = radarData.crbegin(); @@ -729,7 +729,7 @@ float AwsLevel2ChunksDataProvider::GetCurrentElevation() return digitalRadarData0->elevation_angle().value(); } - return INVALID_ELEVATION; + return {}; } } // namespace scwx::provider From 759a9e43798b98d7b0ef8c514e58f1e3254a36a3 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 20 Apr 2025 13:02:02 -0400 Subject: [PATCH 571/762] Parallelize the chunks loading and load from archive when possible --- .../scwx/qt/manager/radar_product_manager.cpp | 10 +++ .../aws_level2_chunks_data_provider.hpp | 4 + .../aws_level2_chunks_data_provider.cpp | 86 ++++++++++++++++--- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 1814905d..e56be177 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -151,6 +151,16 @@ public: level2ChunksProviderManager_->provider_ = provider::NexradDataProviderFactory::CreateLevel2ChunksDataProvider( radarId); + + auto level2ChunksProvider = + std::dynamic_pointer_cast( + level2ChunksProviderManager_->provider_); + if (level2ChunksProvider != nullptr) + { + level2ChunksProvider->SetLevel2DataProvider( + std::dynamic_pointer_cast( + level2ProviderManager_->provider_)); + } } ~RadarProductManagerImpl() { diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 976f0663..476ff111 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -61,6 +62,9 @@ public: std::optional GetCurrentElevation(); + void SetLevel2DataProvider( + const std::shared_ptr& provider); + private: class Impl; std::unique_ptr p; diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index ddac82e5..bb1b31c1 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -13,10 +13,24 @@ #include #include #include -#include #include #include +// Avoid circular refrence errors in boost +// NOLINTBEGIN(misc-header-include-cycle) +#if defined(_MSC_VER) +# pragma warning(push, 0) +#endif + +#include +#include +#include + +#if defined(_MSC_VER) +# pragma warning(pop) +#endif +// NOLINTEND(misc-header-include-cycle) + #if (__cpp_lib_chrono < 201907L) # include #endif @@ -75,6 +89,7 @@ public: lastTimeListed_ {}, // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) about average updatePeriod_ {7}, + level2DataProvider_ {}, self_ {self} { // Disable HTTP request for region @@ -122,6 +137,8 @@ public: std::chrono::seconds updatePeriod_; + std::weak_ptr level2DataProvider_; + AwsLevel2ChunksDataProvider* self_; }; @@ -634,7 +651,6 @@ AwsLevel2ChunksDataProvider::LoadLatestObject() const std::unique_lock lock(p->scansMutex_); return std::make_shared(p->currentScan_.nexradFile_, p->lastScan_.nexradFile_); - // return p->currentScan_.nexradFile_; } std::shared_ptr @@ -663,23 +679,63 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() auto [success, newObjects, totalObjects] = p->ListObjects(); + auto threadPool = boost::asio::thread_pool(3); + bool newCurrent = false; + bool newLast = false; if (p->currentScan_.valid_) { - if (p->LoadScan(p->currentScan_)) - { - newObjects += 1; - } + boost::asio::post(threadPool, + [this, &newCurrent]() + { newCurrent = p->LoadScan(p->currentScan_); }); totalObjects += 1; } + if (p->lastScan_.valid_) { - // TODO this is slow when initially loading data. If possible, loading - // this from the archive may speed it up a lot. - if (p->LoadScan(p->lastScan_)) - { - newObjects += 1; - } totalObjects += 1; + boost::asio::post( + threadPool, + [this, &newLast]() + { + if (!p->lastScan_.hasAllFiles_) + { + // If we have chunks, use chunks + if (p->lastScan_.nextFile_ != 1) + { + newLast = p->LoadScan(p->lastScan_); + } + else + { + auto level2DataProvider = p->level2DataProvider_.lock(); + if (level2DataProvider != nullptr) + { + level2DataProvider->ListObjects(p->lastScan_.time_); + p->lastScan_.nexradFile_ = + std::dynamic_pointer_cast( + level2DataProvider->LoadObjectByTime( + p->lastScan_.time_)); + if (p->lastScan_.nexradFile_ != nullptr) + { + p->lastScan_.hasAllFiles_ = true; + // TODO maybe set lastModified for timing + } + } + // Fall back to chunks if files did not load + newLast = p->lastScan_.nexradFile_ != nullptr || + p->LoadScan(p->lastScan_); + } + } + }); + } + + threadPool.wait(); + if (newCurrent) + { + newObjects += 1; + } + if (newLast) + { + newObjects += 1; } timer.stop(); @@ -732,4 +788,10 @@ std::optional AwsLevel2ChunksDataProvider::GetCurrentElevation() return {}; } +void AwsLevel2ChunksDataProvider::SetLevel2DataProvider( + const std::shared_ptr& provider) +{ + p->level2DataProvider_ = provider; +} + } // namespace scwx::provider From 2daf4d8ba4b6ef59cad88eb01fefdb681e747c7b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Mon, 21 Apr 2025 19:44:31 -0400 Subject: [PATCH 572/762] Remove some unneded methods added in level_2_chunks --- .../aws_level2_chunks_data_provider.hpp | 4 +-- .../provider/aws_nexrad_data_provider.hpp | 2 -- .../scwx/provider/nexrad_data_provider.hpp | 14 -------- .../aws_level2_chunks_data_provider.cpp | 34 ++++++------------- .../provider/aws_nexrad_data_provider.cpp | 11 ------ 5 files changed, 12 insertions(+), 53 deletions(-) diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index 476ff111..abd70787 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -53,9 +53,7 @@ public: LoadObjectByKey(const std::string& key) override; std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; - std::shared_ptr LoadLatestObject() override; - std::shared_ptr LoadSecondLatestObject() override; - std::pair Refresh() override; + std::pair Refresh() override; void RequestAvailableProducts() override; std::vector GetAvailableProducts() override; diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index b2946f38..d6ddcd7c 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -48,8 +48,6 @@ public: LoadObjectByKey(const std::string& key) override; std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; - std::shared_ptr LoadLatestObject() override; - std::shared_ptr LoadSecondLatestObject() override; std::pair Refresh() override; protected: diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index dc490be0..2a7320d2 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -98,20 +98,6 @@ public: virtual std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) = 0; - /** - * Loads the latest NEXRAD file object - * - * @return NEXRAD data - */ - virtual std::shared_ptr LoadLatestObject() = 0; - - /** - * Loads the second NEXRAD file object - * - * @return NEXRAD data - */ - virtual std::shared_ptr LoadSecondLatestObject() = 0; - /** * Lists NEXRAD objects for the current date, and adds them to the cache. If * no objects have been added to the cache for the current date, the previous diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index bb1b31c1..672ee582 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -205,16 +205,20 @@ size_t AwsLevel2ChunksDataProvider::cache_size() const std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::last_modified() const { + // There is a slight delay between the "modified time" and when it is + // actually available. Radar product manager uses this as available time + static const auto extra = std::chrono::seconds(2); + const std::shared_lock lock(p->scansMutex_); if (p->currentScan_.valid_ && p->currentScan_.lastModified_ != std::chrono::system_clock::time_point {}) { - return p->currentScan_.lastModified_; + return p->currentScan_.lastModified_ + extra; } else if (p->lastScan_.valid_ && p->lastScan_.lastModified_ != std::chrono::system_clock::time_point {}) { - return p->lastScan_.lastModified_; + return p->lastScan_.lastModified_ + extra; } else { @@ -224,20 +228,18 @@ AwsLevel2ChunksDataProvider::last_modified() const std::chrono::seconds AwsLevel2ChunksDataProvider::update_period() const { const std::shared_lock lock(p->scansMutex_); - // Add an extra second of delay - static const auto extra = std::chrono::seconds(2); // get update period from time between chunks if (p->currentScan_.valid_ && p->currentScan_.nextFile_ > 2) { auto delta = p->currentScan_.lastModified_ - p->currentScan_.secondLastModified_; - return std::chrono::duration_cast(delta) + extra; + return std::chrono::duration_cast(delta); } else if (p->lastScan_.valid_ && p->lastScan_.nextFile_ > 2) { auto delta = p->lastScan_.lastModified_ - p->lastScan_.secondLastModified_; - return std::chrono::duration_cast(delta) + extra; + return std::chrono::duration_cast(delta); } // default to a set update period @@ -645,20 +647,6 @@ AwsLevel2ChunksDataProvider::LoadObjectByTime( } } -std::shared_ptr -AwsLevel2ChunksDataProvider::LoadLatestObject() -{ - const std::unique_lock lock(p->scansMutex_); - return std::make_shared(p->currentScan_.nexradFile_, - p->lastScan_.nexradFile_); -} - -std::shared_ptr -AwsLevel2ChunksDataProvider::LoadSecondLatestObject() -{ - return p->lastScan_.nexradFile_; -} - int AwsLevel2ChunksDataProvider::Impl::GetScanNumber(const std::string& prefix) { // KIND/585/20250324-134727-001-S @@ -728,7 +716,7 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() }); } - threadPool.wait(); + threadPool.join(); if (newCurrent) { newObjects += 1; @@ -759,7 +747,7 @@ std::optional AwsLevel2ChunksDataProvider::GetCurrentElevation() { if (!p->currentScan_.valid_ || p->currentScan_.nexradFile_ == nullptr) { - // Does not have any scan elevation. -90 is beyond what is possible + // Does not have any scan elevation. return {}; } @@ -767,7 +755,7 @@ std::optional AwsLevel2ChunksDataProvider::GetCurrentElevation() auto radarData = p->currentScan_.nexradFile_->radar_data(); if (radarData.size() == 0) { - // Does not have any scan elevation. -90 is beyond what is possible + // Does not have any scan elevation. return {}; } diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index c0611804..dceb45d8 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -346,17 +346,6 @@ std::shared_ptr AwsNexradDataProvider::LoadObjectByTime( } } -std::shared_ptr AwsNexradDataProvider::LoadLatestObject() -{ - return LoadObjectByKey(FindLatestKey()); -} - -std::shared_ptr -AwsNexradDataProvider::LoadSecondLatestObject() -{ - return nullptr; -} - std::pair AwsNexradDataProvider::Refresh() { using namespace std::chrono; From 3288ba30ecd28a118d1d979827380d7bfe7141d5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 25 Apr 2025 12:24:45 -0400 Subject: [PATCH 573/762] Rework refreshing in RadarProductManager to allow for multiple refreshes at once. --- .../scwx/qt/manager/radar_product_manager.cpp | 128 +++++++++--------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index e56be177..86f31cd4 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(push, 0) @@ -87,16 +88,14 @@ class ProviderManager : public QObject Q_OBJECT public: explicit ProviderManager(RadarProductManager* self, - const std::string& radarId, - common::RadarProductGroup group) : - ProviderManager(self, radarId, group, "???") - { - } - explicit ProviderManager(RadarProductManager* self, - const std::string& radarId, + std::string radarId, common::RadarProductGroup group, - const std::string& product) : - radarId_ {radarId}, group_ {group}, product_ {product} + std::string product = "???", + bool fastRefresh = false) : + radarId_ {std::move(radarId)}, + group_ {group}, + product_ {std::move(product)}, + fastRefresh_ {fastRefresh} { connect(this, &ProviderManager::NewDataAvailable, @@ -114,10 +113,12 @@ public: const std::string radarId_; const common::RadarProductGroup group_; const std::string product_; + const bool fastRefresh_; bool refreshEnabled_ {false}; boost::asio::steady_timer refreshTimer_ {threadPool_}; std::mutex refreshTimerMutex_ {}; std::shared_ptr provider_ {nullptr}; + size_t refreshCount_ {0}; signals: void NewDataAvailable(common::RadarProductGroup group, @@ -138,7 +139,7 @@ public: level2ProviderManager_ {std::make_shared( self_, radarId_, common::RadarProductGroup::Level2)}, level2ChunksProviderManager_ {std::make_shared( - self_, radarId_, common::RadarProductGroup::Level2)} + self_, radarId_, common::RadarProductGroup::Level2, "???", true)} { if (radarSite_ == nullptr) { @@ -191,9 +192,10 @@ public: std::shared_ptr GetLevel3ProviderManager(const std::string& product); - void EnableRefresh(boost::uuids::uuid uuid, - std::shared_ptr providerManager, - bool enabled); + void EnableRefresh( + boost::uuids::uuid uuid, + const std::set>& providerManagers, + bool enabled); void RefreshData(std::shared_ptr providerManager); void RefreshDataSync(std::shared_ptr providerManager); @@ -285,7 +287,7 @@ public: std::optional incomingLevel2Elevation_ {}; std::unordered_map, + std::set>, boost::hash> refreshMap_ {}; std::shared_mutex refreshMapMutex_ {}; @@ -664,8 +666,10 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, { if (group == common::RadarProductGroup::Level2) { - // p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); - p->EnableRefresh(uuid, p->level2ChunksProviderManager_, enabled); + p->EnableRefresh( + uuid, + {p->level2ProviderManager_, p->level2ChunksProviderManager_}, + enabled); } else { @@ -688,7 +692,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, availableProducts.cend(), product) != availableProducts.cend()) { - p->EnableRefresh(uuid, providerManager, enabled); + p->EnableRefresh(uuid, {providerManager}, enabled); } } catch (const std::exception& ex) @@ -700,50 +704,45 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, } void RadarProductManagerImpl::EnableRefresh( - boost::uuids::uuid uuid, - std::shared_ptr providerManager, - bool enabled) + boost::uuids::uuid uuid, + const std::set>& providerManagers, + bool enabled) { // Lock the refresh map std::unique_lock lock {refreshMapMutex_}; - auto currentProviderManager = refreshMap_.find(uuid); - if (currentProviderManager != refreshMap_.cend()) + auto currentProviderManagers = refreshMap_.find(uuid); + if (currentProviderManagers != refreshMap_.cend()) { - // If the enabling refresh for a different product, or disabling refresh - if (currentProviderManager->second != providerManager || !enabled) + for (const auto& currentProviderManager : currentProviderManagers->second) { - // Determine number of entries in the map for the current provider - // manager - auto currentProviderManagerCount = std::count_if( - refreshMap_.cbegin(), - refreshMap_.cend(), - [&](const auto& provider) - { return provider.second == currentProviderManager->second; }); - - // If this is the last reference to the provider in the refresh map - if (currentProviderManagerCount == 1) + currentProviderManager->refreshCount_ -= 1; + // If the enabling refresh for a different product, or disabling + // refresh + if (!providerManagers.contains(currentProviderManager) || !enabled) { - // Disable current provider - currentProviderManager->second->Disable(); - } - - // Dissociate uuid from current provider manager - refreshMap_.erase(currentProviderManager); - - // If we are enabling a new provider manager - if (enabled) - { - // Associate uuid to providerManager - refreshMap_.emplace(uuid, providerManager); + // If this is the last reference to the provider in the refresh map + if (currentProviderManager->refreshCount_ == 0) + { + // Disable current provider + currentProviderManager->Disable(); + } } } + + // Dissociate uuid from current provider managers + refreshMap_.erase(currentProviderManagers); } - else if (enabled) + + if (enabled) { - // We are enabling a new provider manager + // We are enabling provider managers // Associate uuid to provider manager - refreshMap_.emplace(uuid, providerManager); + refreshMap_.emplace(uuid, providerManagers); + for (const auto& providerManager : providerManagers) + { + providerManager->refreshCount_ += 1; + } } // Release the refresh map mutex @@ -751,13 +750,15 @@ void RadarProductManagerImpl::EnableRefresh( // We have already handled a disable request by this point. If enabling, and // the provider manager refresh isn't already enabled, enable it. - if (enabled && providerManager->refreshEnabled_ != enabled) + if (enabled) { - providerManager->refreshEnabled_ = enabled; - - if (enabled) + for (const auto& providerManager : providerManagers) { - RefreshData(providerManager); + if (providerManager->refreshEnabled_ != enabled) + { + providerManager->refreshEnabled_ = enabled; + RefreshData(providerManager); + } } } } @@ -795,13 +796,11 @@ void RadarProductManagerImpl::RefreshDataSync( // Level2 chunked data is updated quickly and uses a faster interval const std::chrono::milliseconds fastRetryInterval = - providerManager == level2ChunksProviderManager_ ? - kFastRetryIntervalChunks_ : - kFastRetryInterval_; + providerManager->fastRefresh_ ? kFastRetryIntervalChunks_ : + kFastRetryInterval_; const std::chrono::milliseconds slowRetryInterval = - providerManager == level2ChunksProviderManager_ ? - kSlowRetryIntervalChunks_ : - kSlowRetryInterval_; + providerManager->fastRefresh_ ? kSlowRetryIntervalChunks_ : + kSlowRetryInterval_; std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) @@ -896,10 +895,13 @@ RadarProductManager::GetActiveVolumeTimes( std::shared_lock refreshLock {p->refreshMapMutex_}; // For each entry in the refresh map (refresh is enabled) - for (auto& refreshEntry : p->refreshMap_) + for (auto& refreshSet : p->refreshMap_) { - // Add the provider for the current entry - providers.insert(refreshEntry.second->provider_); + for (const auto& refreshEntry : refreshSet.second) + { + // Add the provider for the current entry + providers.insert(refreshEntry->provider_); + } } // Unlock the refresh map From 2821eff71f7df099db3d03e46dcd4c874ffe1ce0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 25 Apr 2025 13:11:57 -0400 Subject: [PATCH 574/762] Fall back to archive if chunks get too old --- .../scwx/qt/manager/radar_product_manager.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 86f31cd4..ea8546f2 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1548,6 +1548,12 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; + const bool isEpox = time == std::chrono::system_clock::time_point{}; + bool needArchive = true; + static const auto maxChunkDelay = std::chrono::minutes(10); + const std::chrono::system_clock::time_point firstValidChunkTime = + (isEpox ? std::chrono::system_clock::now() : time) - maxChunkDelay; + // See if we have this one in the chunk provider. auto chunkFile = std::dynamic_pointer_cast( p->level2ChunksProviderManager_->provider_->LoadObjectByTime(time)); @@ -1572,9 +1578,16 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, p->incomingLevel2Elevation_ = incomingElevation; Q_EMIT IncomingLevel2ElevationChanged(incomingElevation); } + + if (foundTime >= firstValidChunkTime) + { + needArchive = false; + } } } - else // It is not in the chunk provider, so get it from the archive + + // It is not in the chunk provider, so get it from the archive + if (needArchive) { auto records = p->GetLevel2ProductRecords(time); for (auto& recordPair : records) @@ -1601,7 +1614,8 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, // Find the newest radar data, not newer than the selected time if (radarData == nullptr || - (collectionTime <= time && foundTime < collectionTime)) + (collectionTime <= time && foundTime < collectionTime) || + (isEpox && foundTime < collectionTime)) { radarData = recordRadarData; elevationCut = recordElevationCut; From 781aa40e8ce4dcd71637f3897d73faf8973b8a9f Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 25 Apr 2025 13:31:19 -0400 Subject: [PATCH 575/762] Make radar data fall back if it ends up being too old --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index ea8546f2..8cd3f837 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1548,8 +1548,8 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; - const bool isEpox = time == std::chrono::system_clock::time_point{}; - bool needArchive = true; + const bool isEpox = time == std::chrono::system_clock::time_point {}; + bool needArchive = true; static const auto maxChunkDelay = std::chrono::minutes(10); const std::chrono::system_clock::time_point firstValidChunkTime = (isEpox ? std::chrono::system_clock::now() : time) - maxChunkDelay; From 3d7da7d9710da90f55c58bb7fee0940ff12923d0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 26 Apr 2025 18:51:11 -0400 Subject: [PATCH 576/762] Disable logging for level 2 chunks --- .../source/scwx/qt/manager/radar_product_manager.cpp | 10 ++++++---- .../scwx/provider/aws_level2_chunks_data_provider.cpp | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 8cd3f837..b7d99467 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -766,7 +766,7 @@ void RadarProductManagerImpl::EnableRefresh( void RadarProductManagerImpl::RefreshData( std::shared_ptr providerManager) { - logger_->debug("RefreshData: {}", providerManager->name()); + // logger_->debug("RefreshData: {}", providerManager->name()); { std::unique_lock lock(providerManager->refreshTimerMutex_); @@ -846,10 +846,12 @@ void RadarProductManagerImpl::RefreshDataSync( if (providerManager->refreshEnabled_) { + /* logger_->debug( "[{}] Scheduled refresh in {:%M:%S}", providerManager->name(), std::chrono::duration_cast(interval)); + */ { providerManager->refreshTimer_.expires_after(interval); @@ -960,9 +962,9 @@ void RadarProductManagerImpl::LoadProviderData( std::mutex& loadDataMutex, const std::shared_ptr& request) { - logger_->debug("LoadProviderData: {}, {}", + /*logger_->debug("LoadProviderData: {}, {}", providerManager->name(), - scwx::util::TimeString(time)); + scwx::util::TimeString(time));*/ LoadNexradFileAsync( [=, &recordMap, &recordMutex]() -> std::shared_ptr @@ -1013,7 +1015,7 @@ void RadarProductManager::LoadLevel2Data( std::chrono::system_clock::time_point time, const std::shared_ptr& request) { - logger_->debug("LoadLevel2Data: {}", scwx::util::TimeString(time)); + // logger_->debug("LoadLevel2Data: {}", scwx::util::TimeString(time)); p->LoadProviderData(time, p->level2ProviderManager_, diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 672ee582..27b062b7 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -356,7 +356,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() if (outcome.IsSuccess()) { auto& scans = outcome.GetResult().GetCommonPrefixes(); - logger_->debug("Found {} scans", scans.size()); + // logger_->debug("Found {} scans", scans.size()); if (scans.size() > 0) { @@ -514,7 +514,7 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) bool hasNew = false; auto& chunks = listOutcome.GetResult().GetContents(); - logger_->debug("Found {} new chunks.", chunks.size()); + // logger_->debug("Found {} new chunks.", chunks.size()); for (const auto& chunk : chunks) { const std::string& key = chunk.GetKey(); @@ -728,7 +728,7 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() timer.stop(); // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) format to 6 digits - logger_->debug("Refresh() in {}", timer.format(6, "%ws")); + // logger_->debug("Refresh() in {}", timer.format(6, "%ws")); return std::make_pair(newObjects, totalObjects); } From 4906800a22be312dc5facb2181fb1afb7883ab54 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 7 May 2025 17:16:42 -0400 Subject: [PATCH 577/762] Resolve TODOs in level_2_chunks --- .../aws_level2_chunks_data_provider.cpp | 2 -- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 21 +++++++++---------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 27b062b7..64ce04b0 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -405,7 +405,6 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() if (possibleLastNumbers.empty()) { logger_->warn("Could not find last scan"); - // TODO make sure this makes sence return {false, 0, 0}; } @@ -705,7 +704,6 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() if (p->lastScan_.nexradFile_ != nullptr) { p->lastScan_.hasAllFiles_ = true; - // TODO maybe set lastModified for timing } } // Fall back to chunks if files did not load diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index b4dc6248..7772ea44 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -528,7 +528,6 @@ bool Ar2vFile::IndexFile() return true; } -// TODO not good // NOLINTNEXTLINE bool IsRadarDataIncomplete( const std::shared_ptr& radarData) @@ -583,10 +582,10 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, std::shared_ptr secondMostRecent = nullptr; // check if this volume scan has an earlier elevation scan - auto maybe = elevation.second.rbegin(); // TODO name - ++maybe; + auto possibleSecondMostRecent = elevation.second.rbegin(); + ++possibleSecondMostRecent; - if (maybe == elevation.second.rend()) + if (possibleSecondMostRecent == elevation.second.rend()) { if (last == nullptr) { @@ -613,7 +612,7 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, } else { - secondMostRecent = maybe->second; + secondMostRecent = possibleSecondMostRecent->second; } // Make the new scan @@ -706,16 +705,16 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, // Find the highest elevation this type has for the current scan // Start below any reasonable elevation // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - float highestCurrentElevation = -90; - const auto& maybe1 = p->index_.find(type.first); - if (maybe1 != p->index_.cend()) + float highestCurrentElevation = -90; + const auto& elevationScans = p->index_.find(type.first); + if (elevationScans != p->index_.cend()) { - const auto& maybe2 = maybe1->second.crbegin(); - if (maybe2 != maybe1->second.crend()) + const auto& highestElevation = elevationScans->second.crbegin(); + if (highestElevation != elevationScans->second.crend()) { // Add a slight offset to ensure good floating point compare. // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - highestCurrentElevation = maybe2->first + 0.01f; + highestCurrentElevation = highestElevation->first + 0.01f; } } From 969267b661c838bb94b61e99f59064d9adc65cf4 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 7 May 2025 17:33:44 -0400 Subject: [PATCH 578/762] Added back logging as traces for level_2_chunks --- .../scwx/qt/manager/radar_product_manager.cpp | 23 ++++++++----------- .../scwx/qt/map/radar_product_layer.cpp | 2 +- .../aws_level2_chunks_data_provider.cpp | 8 +++---- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 14 +++++------ 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index b7d99467..d6b85685 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -766,7 +766,7 @@ void RadarProductManagerImpl::EnableRefresh( void RadarProductManagerImpl::RefreshData( std::shared_ptr providerManager) { - // logger_->debug("RefreshData: {}", providerManager->name()); + logger_->trace("RefreshData: {}", providerManager->name()); { std::unique_lock lock(providerManager->refreshTimerMutex_); @@ -846,12 +846,10 @@ void RadarProductManagerImpl::RefreshDataSync( if (providerManager->refreshEnabled_) { - /* - logger_->debug( + logger_->trace( "[{}] Scheduled refresh in {:%M:%S}", providerManager->name(), std::chrono::duration_cast(interval)); - */ { providerManager->refreshTimer_.expires_after(interval); @@ -962,9 +960,9 @@ void RadarProductManagerImpl::LoadProviderData( std::mutex& loadDataMutex, const std::shared_ptr& request) { - /*logger_->debug("LoadProviderData: {}, {}", + logger_->trace("LoadProviderData: {}, {}", providerManager->name(), - scwx::util::TimeString(time));*/ + scwx::util::TimeString(time)); LoadNexradFileAsync( [=, &recordMap, &recordMutex]() -> std::shared_ptr @@ -980,13 +978,11 @@ void RadarProductManagerImpl::LoadProviderData( { existingRecord = it->second.lock(); - /* if (existingRecord != nullptr) { - logger_->debug( + logger_->trace( "Data previously loaded, loading from data cache"); } - */ } } @@ -1015,7 +1011,7 @@ void RadarProductManager::LoadLevel2Data( std::chrono::system_clock::time_point time, const std::shared_ptr& request) { - // logger_->debug("LoadLevel2Data: {}", scwx::util::TimeString(time)); + logger_->trace("LoadLevel2Data: {}", scwx::util::TimeString(time)); p->LoadProviderData(time, p->level2ProviderManager_, @@ -1445,7 +1441,7 @@ std::shared_ptr RadarProductManagerImpl::StoreRadarProductRecord( std::shared_ptr record) { - // logger_->debug("StoreRadarProductRecord()"); + logger_->trace("StoreRadarProductRecord()"); std::shared_ptr storedRecord = nullptr; @@ -1462,12 +1458,11 @@ RadarProductManagerImpl::StoreRadarProductRecord( { storedRecord = it->second.lock(); - /* if (storedRecord != nullptr) { - logger_->debug( + logger_->trace( "Level 2 product previously loaded, loading from cache"); - }*/ + } } if (storedRecord == nullptr) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index ee6b3c3c..7bda264f 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -170,7 +170,7 @@ void RadarProductLayer::UpdateSweep() std::try_to_lock); if (!sweepLock.owns_lock()) { - // logger_->debug("Sweep locked, deferring update"); + logger_->trace("Sweep locked, deferring update"); return; } logger_->debug("UpdateSweep()"); diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 64ce04b0..74a9afd7 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -341,7 +341,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() { return {true, newObjects, totalObjects}; } - logger_->debug("ListObjects"); + logger_->trace("ListObjects"); lastTimeListed_ = now; const std::string prefix = radarSite_ + "/"; @@ -356,7 +356,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() if (outcome.IsSuccess()) { auto& scans = outcome.GetResult().GetCommonPrefixes(); - // logger_->debug("Found {} scans", scans.size()); + logger_->trace("Found {} scans", scans.size()); if (scans.size() > 0) { @@ -513,7 +513,7 @@ bool AwsLevel2ChunksDataProvider::Impl::LoadScan(Impl::ScanRecord& scanRecord) bool hasNew = false; auto& chunks = listOutcome.GetResult().GetContents(); - // logger_->debug("Found {} new chunks.", chunks.size()); + logger_->trace("Found {} new chunks.", chunks.size()); for (const auto& chunk : chunks) { const std::string& key = chunk.GetKey(); @@ -726,7 +726,7 @@ std::pair AwsLevel2ChunksDataProvider::Refresh() timer.stop(); // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) format to 6 digits - // logger_->debug("Refresh() in {}", timer.format(6, "%ws")); + logger_->debug("Refresh() in {}", timer.format(6, "%ws")); return std::make_pair(newObjects, totalObjects); } diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 7772ea44..3ee99fe1 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -138,7 +138,7 @@ Ar2vFile::GetElevationScan(rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) const { - // logger_->debug("GetElevationScan: {} degrees", elevation); + logger_->trace("GetElevationScan: {} degrees", elevation); std::shared_ptr elevationScan = nullptr; float elevationCut = 0.0f; @@ -273,7 +273,7 @@ bool Ar2vFile::LoadData(std::istream& is) std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) { - // logger_->debug("Decompressing LDM Records"); + logger_->trace("Decompressing LDM Records"); std::size_t numRecords = 0; @@ -321,22 +321,22 @@ std::size_t Ar2vFileImpl::DecompressLDMRecords(std::istream& is) ++numRecords; } - // logger_->debug("Decompressed {} LDM Records", numRecords); + logger_->trace("Decompressed {} LDM Records", numRecords); return numRecords; } void Ar2vFileImpl::ParseLDMRecords() { - // logger_->debug("Parsing LDM Records"); + logger_->trace("Parsing LDM Records"); - // std::size_t count = 0; + std::size_t count = 0; for (auto it = rawRecords_.begin(); it != rawRecords_.end(); it++) { std::stringstream& ss = *it; - // logger_->trace("Record {}", count++); + logger_->trace("Record {}", count++); ParseLDMRecord(ss); } @@ -445,7 +445,7 @@ void Ar2vFileImpl::ProcessRadarData( void Ar2vFileImpl::IndexFile() { - // logger_->debug("Indexing file"); + logger_->trace("Indexing file"); for (auto& elevationCut : radarData_) { From 0438b65208d73003ec218c077df16c23deab2906 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 7 May 2025 17:44:28 -0400 Subject: [PATCH 579/762] Fix clang format errors for level_2_chunks --- wxdata/source/scwx/wsr88d/ar2v_file.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wxdata/source/scwx/wsr88d/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/ar2v_file.cpp index 3ee99fe1..f4a71c15 100644 --- a/wxdata/source/scwx/wsr88d/ar2v_file.cpp +++ b/wxdata/source/scwx/wsr88d/ar2v_file.cpp @@ -467,7 +467,7 @@ void Ar2vFileImpl::IndexFile() { elevationAngle = static_cast(vcpData_->elevation_angle(elevationCut.first)); - waveformType = vcpData_->waveform_type(elevationCut.first); + waveformType = vcpData_->waveform_type(elevationCut.first); } else if ((digitalRadarData0 = std::dynamic_pointer_cast(radial0)) != @@ -705,8 +705,8 @@ Ar2vFile::Ar2vFile(const std::shared_ptr& current, // Find the highest elevation this type has for the current scan // Start below any reasonable elevation // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - float highestCurrentElevation = -90; - const auto& elevationScans = p->index_.find(type.first); + float highestCurrentElevation = -90; + const auto& elevationScans = p->index_.find(type.first); if (elevationScans != p->index_.cend()) { const auto& highestElevation = elevationScans->second.crbegin(); From 8989c0e88ce044560c5b5aa7c9c21c18d7bf34e7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 10 May 2025 15:06:46 -0400 Subject: [PATCH 580/762] Fix issue where level 2 archive files where put in a cache at times of level 2 chunk files --- .../scwx/qt/manager/radar_product_manager.cpp | 32 ++--- .../scwx/qt/manager/radar_product_manager.hpp | 1 + scwx-qt/source/scwx/qt/map/map_widget.cpp | 117 ++++++++++-------- .../scwx/qt/view/overlay_product_view.cpp | 5 +- 4 files changed, 84 insertions(+), 71 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index d6b85685..81f0fc52 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -90,17 +90,23 @@ public: explicit ProviderManager(RadarProductManager* self, std::string radarId, common::RadarProductGroup group, - std::string product = "???", - bool fastRefresh = false) : + std::string product = "???", + bool isChunks = false) : radarId_ {std::move(radarId)}, group_ {group}, product_ {std::move(product)}, - fastRefresh_ {fastRefresh} + isChunks_ {isChunks} { connect(this, &ProviderManager::NewDataAvailable, self, - &RadarProductManager::NewDataAvailable); + [this, self](common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point latestTime) + { + Q_EMIT self->NewDataAvailable( + group, product, isChunks_, latestTime); + }); } ~ProviderManager() { threadPool_.join(); }; @@ -113,7 +119,7 @@ public: const std::string radarId_; const common::RadarProductGroup group_; const std::string product_; - const bool fastRefresh_; + const bool isChunks_; bool refreshEnabled_ {false}; boost::asio::steady_timer refreshTimer_ {threadPool_}; std::mutex refreshTimerMutex_ {}; @@ -796,11 +802,11 @@ void RadarProductManagerImpl::RefreshDataSync( // Level2 chunked data is updated quickly and uses a faster interval const std::chrono::milliseconds fastRetryInterval = - providerManager->fastRefresh_ ? kFastRetryIntervalChunks_ : - kFastRetryInterval_; + providerManager->isChunks_ ? kFastRetryIntervalChunks_ : + kFastRetryInterval_; const std::chrono::milliseconds slowRetryInterval = - providerManager->fastRefresh_ ? kSlowRetryIntervalChunks_ : - kSlowRetryInterval_; + providerManager->isChunks_ ? kSlowRetryIntervalChunks_ : + kSlowRetryInterval_; std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) @@ -1019,12 +1025,6 @@ void RadarProductManager::LoadLevel2Data( p->level2ProductRecordMutex_, p->loadLevel2DataMutex_, request); - p->LoadProviderData(time, - p->level2ChunksProviderManager_, - p->level2ProductRecords_, - p->level2ProductRecordMutex_, - p->loadLevel2DataMutex_, - request); } void RadarProductManager::LoadLevel3Data( @@ -1460,7 +1460,7 @@ RadarProductManagerImpl::StoreRadarProductRecord( if (storedRecord != nullptr) { - logger_->trace( + logger_->error( "Level 2 product previously loaded, loading from cache"); } } diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index c0a49dff..e8c72193 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -148,6 +148,7 @@ signals: void Level3ProductsChanged(); void NewDataAvailable(common::RadarProductGroup group, const std::string& product, + bool isChunks, std::chrono::system_clock::time_point latestTime); void IncomingLevel2ElevationChanged(std::optional incomingElevation); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index ad808626..e8d0e380 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1843,6 +1843,7 @@ void MapWidgetImpl::RadarProductManagerConnect() this, [this](common::RadarProductGroup group, const std::string& product, + bool isChunks, std::chrono::system_clock::time_point latestTime) { if (autoRefreshEnabled_ && @@ -1850,71 +1851,81 @@ void MapWidgetImpl::RadarProductManagerConnect() (group == common::RadarProductGroup::Level2 || context_->radar_product() == product)) { - // Create file request - std::shared_ptr request = - std::make_shared( - radarProductManager_->radar_id()); - - // File request callback - if (autoUpdateEnabled_) + if (isChunks && autoUpdateEnabled_) { - connect( - request.get(), - &request::NexradFileRequest::RequestComplete, - this, - [=, - this](std::shared_ptr request) - { - // Select loaded record - auto record = request->radar_product_record(); + // Level 2 products may have multiple time points, + // ensure the latest is selected + widget_->SelectRadarProduct(group, product); + } + else + { + // Create file request + const std::shared_ptr request = + std::make_shared( + radarProductManager_->radar_id()); - // Validate record, and verify current map context - // still displays site and product - if (record != nullptr && - radarProductManager_ != nullptr && - radarProductManager_->radar_id() == - request->current_radar_site() && - context_->radar_product_group() == group && - (group == common::RadarProductGroup::Level2 || - context_->radar_product() == product)) + // File request callback + if (autoUpdateEnabled_) + { + connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + this, + [group, product, this]( + const std::shared_ptr& + request) + { + // Select loaded record + auto record = request->radar_product_record(); + + // Validate record, and verify current map context + // still displays site and product + if (record != nullptr && + radarProductManager_ != nullptr && + radarProductManager_->radar_id() == + request->current_radar_site() && + context_->radar_product_group() == group && + (group == common::RadarProductGroup::Level2 || + context_->radar_product() == product)) + { + if (group == common::RadarProductGroup::Level2) + { + // Level 2 products may have multiple time + // points, ensure the latest is selected + widget_->SelectRadarProduct(group, product); + } + else + { + widget_->SelectRadarProduct(record); + } + } + }); + } + + // Load file + boost::asio::post( + threadPool_, + [group, latestTime, request, product, this]() + { + try { if (group == common::RadarProductGroup::Level2) { - // Level 2 products may have multiple time points, - // ensure the latest is selected - widget_->SelectRadarProduct(group, product); + radarProductManager_->LoadLevel2Data(latestTime, + request); } else { - widget_->SelectRadarProduct(record); + radarProductManager_->LoadLevel3Data( + product, latestTime, request); } } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } }); } - - // Load file - boost::asio::post( - threadPool_, - [=, this]() - { - try - { - if (group == common::RadarProductGroup::Level2) - { - radarProductManager_->LoadLevel2Data(latestTime, - request); - } - else - { - radarProductManager_->LoadLevel3Data( - product, latestTime, request); - } - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } - }); } }, Qt::QueuedConnection); diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index c33494c2..3200dcca 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -116,8 +116,9 @@ void OverlayProductView::Impl::ConnectRadarProductManager() radarProductManager_.get(), &manager::RadarProductManager::NewDataAvailable, self_, - [this](common::RadarProductGroup group, - const std::string& product, + [this](common::RadarProductGroup group, + const std::string& product, + bool /*isChunks*/, std::chrono::system_clock::time_point latestTime) { if (autoRefreshEnabled_ && From 2d4ad2737ea54dae15e2c3f43b07b1190eb1539c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 7 May 2025 22:37:45 -0500 Subject: [PATCH 581/762] MapContext has a GlContext instead of MapContext is a GlContext --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 10 +++---- scwx-qt/source/scwx/qt/map/alert_layer.hpp | 12 ++------- .../source/scwx/qt/map/color_table_layer.cpp | 22 +++++++-------- .../source/scwx/qt/map/color_table_layer.hpp | 12 +++------ scwx-qt/source/scwx/qt/map/draw_layer.cpp | 13 ++++----- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 10 ++----- scwx-qt/source/scwx/qt/map/generic_layer.cpp | 14 +++------- scwx-qt/source/scwx/qt/map/generic_layer.hpp | 12 +++------ scwx-qt/source/scwx/qt/map/layer_wrapper.cpp | 14 +++------- scwx-qt/source/scwx/qt/map/layer_wrapper.hpp | 12 +++------ scwx-qt/source/scwx/qt/map/map_context.cpp | 24 +++++++++-------- scwx-qt/source/scwx/qt/map/map_context.hpp | 17 ++++++------ scwx-qt/source/scwx/qt/map/map_provider.cpp | 10 ++----- scwx-qt/source/scwx/qt/map/map_provider.hpp | 17 ++++-------- scwx-qt/source/scwx/qt/map/map_settings.hpp | 10 ++----- scwx-qt/source/scwx/qt/map/map_widget.cpp | 17 +++++------- scwx-qt/source/scwx/qt/map/map_widget.hpp | 10 ++----- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 18 +++++-------- scwx-qt/source/scwx/qt/map/marker_layer.hpp | 12 ++------- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 20 +++++--------- scwx-qt/source/scwx/qt/map/overlay_layer.hpp | 12 +++------ .../scwx/qt/map/overlay_product_layer.cpp | 21 ++++++--------- .../scwx/qt/map/overlay_product_layer.hpp | 12 +++------ .../source/scwx/qt/map/placefile_layer.cpp | 21 ++++++--------- .../source/scwx/qt/map/placefile_layer.hpp | 10 ++----- .../scwx/qt/map/radar_product_layer.cpp | 27 +++++++++---------- .../scwx/qt/map/radar_product_layer.hpp | 12 +++------ .../source/scwx/qt/map/radar_range_layer.cpp | 10 ++----- .../source/scwx/qt/map/radar_range_layer.hpp | 13 ++------- .../source/scwx/qt/map/radar_site_layer.cpp | 18 +++++-------- .../source/scwx/qt/map/radar_site_layer.hpp | 12 +++------ 31 files changed, 147 insertions(+), 307 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 495be87c..18efbc81 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -135,9 +135,9 @@ public: std::size_t lineWidth_ {}; }; - explicit Impl(AlertLayer* self, - std::shared_ptr context, - awips::Phenomenon phenomenon) : + explicit Impl(AlertLayer* self, + std::shared_ptr context, + awips::Phenomenon phenomenon) : self_ {self}, phenomenon_ {phenomenon}, ibw_ {awips::ibw::GetImpactBasedWarningInfo(phenomenon)}, @@ -250,7 +250,7 @@ AlertLayer::AlertLayer(const std::shared_ptr& context, DrawLayer( context, fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), - p(std::make_unique(this, context, phenomenon)) + p(std::make_unique(this, context->gl_context(), phenomenon)) { for (auto alertActive : {false, true}) { @@ -298,7 +298,7 @@ void AlertLayer::Initialize() void AlertLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); for (auto alertActive : {false, true}) { diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index 60905680..e14b5d1a 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -6,14 +6,8 @@ #include #include -#include -#include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class AlertLayer : public DrawLayer @@ -40,6 +34,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index bdafce3f..bed9ec23 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -14,11 +14,7 @@ # pragma warning(pop) #endif -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::color_table_layer"; @@ -51,7 +47,7 @@ public: bool colorTableNeedsUpdate_; }; -ColorTableLayer::ColorTableLayer(std::shared_ptr context) : +ColorTableLayer::ColorTableLayer(const std::shared_ptr& context) : GenericLayer(context), p(std::make_unique()) { } @@ -61,11 +57,13 @@ void ColorTableLayer::Initialize() { logger_->debug("Initialize()"); - gl::OpenGLFunctions& gl = context()->gl(); + auto glContext = context()->gl_context(); + + gl::OpenGLFunctions& gl = glContext->gl(); // Load and configure overlay shader p->shaderProgram_ = - context()->GetShaderProgram(":/gl/texture1d.vert", ":/gl/texture1d.frag"); + glContext->GetShaderProgram(":/gl/texture1d.vert", ":/gl/texture1d.frag"); p->uMVPMatrixLocation_ = gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); @@ -118,7 +116,7 @@ void ColorTableLayer::Initialize() void ColorTableLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); auto radarProductView = context()->radar_product_view(); if (context()->radar_product_view() == nullptr || @@ -196,7 +194,7 @@ void ColorTableLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); gl.glDeleteVertexArrays(1, &p->vao_); gl.glDeleteBuffers(2, p->vbo_.data()); @@ -210,6 +208,4 @@ void ColorTableLayer::Deinitialize() context()->set_color_table_margins(QMargins {}); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.hpp b/scwx-qt/source/scwx/qt/map/color_table_layer.hpp index c23dc2b8..ce72f358 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class ColorTableLayerImpl; @@ -14,7 +10,7 @@ class ColorTableLayerImpl; class ColorTableLayer : public GenericLayer { public: - explicit ColorTableLayer(std::shared_ptr context); + explicit ColorTableLayer(const std::shared_ptr& context); ~ColorTableLayer(); void Initialize() override final; @@ -25,6 +21,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 13d06780..8372829c 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -77,7 +77,7 @@ DrawLayer::~DrawLayer() = default; void DrawLayer::Initialize() { - p->textureAtlas_ = p->context_->GetTextureAtlas(); + p->textureAtlas_ = p->context_->gl_context()->GetTextureAtlas(); for (auto& item : p->drawList_) { @@ -123,13 +123,14 @@ void DrawLayer::ImGuiInitialize() void DrawLayer::RenderWithoutImGui( const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->textureAtlas_ = p->context_->GetTextureAtlas(); + auto glContext = p->context_->gl_context(); + + gl::OpenGLFunctions& gl = glContext->gl(); + p->textureAtlas_ = glContext->GetTextureAtlas(); // Determine if the texture atlas changed since last render - std::uint64_t newTextureAtlasBuildCount = - p->context_->texture_buffer_count(); - bool textureAtlasChanged = + std::uint64_t newTextureAtlasBuildCount = glContext->texture_buffer_count(); + bool textureAtlasChanged = newTextureAtlasBuildCount != p->textureAtlasBuildCount_; // Set OpenGL blend mode for transparency diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index 6cfa5aae..93d8a54a 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -3,11 +3,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class DrawLayerImpl; @@ -44,6 +40,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.cpp b/scwx-qt/source/scwx/qt/map/generic_layer.cpp index 97f22097..45ebea00 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.cpp @@ -1,17 +1,13 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class GenericLayerImpl { public: explicit GenericLayerImpl(std::shared_ptr context) : - context_ {context} + context_ {std::move(context)} { } @@ -20,7 +16,7 @@ public: std::shared_ptr context_; }; -GenericLayer::GenericLayer(std::shared_ptr context) : +GenericLayer::GenericLayer(const std::shared_ptr& context) : p(std::make_unique(context)) { } @@ -43,6 +39,4 @@ std::shared_ptr GenericLayer::context() const return p->context_; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp index 0fee92ab..c9abdecb 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp @@ -10,11 +10,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class GenericLayerImpl; @@ -24,7 +20,7 @@ class GenericLayer : public QObject Q_OBJECT public: - explicit GenericLayer(std::shared_ptr context); + explicit GenericLayer(const std::shared_ptr& context); virtual ~GenericLayer(); virtual void Initialize() = 0; @@ -61,6 +57,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp b/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp index 88fc7df9..8bb5e878 100644 --- a/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp +++ b/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp @@ -1,17 +1,13 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class LayerWrapperImpl { public: explicit LayerWrapperImpl(std::shared_ptr layer) : - layer_ {layer} + layer_ {std::move(layer)} { } @@ -20,7 +16,7 @@ public: std::shared_ptr layer_; }; -LayerWrapper::LayerWrapper(std::shared_ptr layer) : +LayerWrapper::LayerWrapper(const std::shared_ptr& layer) : p(std::make_unique(layer)) { } @@ -58,6 +54,4 @@ void LayerWrapper::deinitialize() } } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp b/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp index 6e0e44ee..6e4cc62e 100644 --- a/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp +++ b/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class LayerWrapperImpl; @@ -14,7 +10,7 @@ class LayerWrapperImpl; class LayerWrapper : public QMapLibre::CustomLayerHostInterface { public: - explicit LayerWrapper(std::shared_ptr layer); + explicit LayerWrapper(const std::shared_ptr& layer); ~LayerWrapper(); LayerWrapper(const LayerWrapper&) = delete; @@ -31,6 +27,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index a0c1e74a..48cf4afc 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -3,11 +3,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class MapContext::Impl @@ -20,14 +16,17 @@ public: ~Impl() {} + std::shared_ptr glContext_ { + std::make_shared()}; + std::weak_ptr map_ {}; MapSettings settings_ {}; float pixelRatio_ {1.0f}; common::RadarProductGroup radarProductGroup_ { common::RadarProductGroup::Unknown}; - std::string radarProduct_ {"???"}; - int16_t radarProductCode_ {0}; - std::shared_ptr radarSite_ {nullptr}; + std::string radarProduct_ {"???"}; + int16_t radarProductCode_ {0}; + std::shared_ptr radarSite_ {nullptr}; MapProvider mapProvider_ {MapProvider::Unknown}; std::string mapCopyrights_ {}; @@ -51,6 +50,11 @@ MapContext::~MapContext() = default; MapContext::MapContext(MapContext&&) noexcept = default; MapContext& MapContext::operator=(MapContext&&) noexcept = default; +std::shared_ptr MapContext::gl_context() const +{ + return p->glContext_; +} + std::weak_ptr MapContext::map() const { return p->map_; @@ -190,6 +194,4 @@ void MapContext::set_widget(QWidget* widget) p->widget_ = widget; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 39a5c1be..9cf611a6 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -9,27 +9,25 @@ #include #include -namespace scwx::qt -{ -namespace view +namespace scwx::qt::view { class OverlayProductView; class RadarProductView; -} // namespace view +} // namespace scwx::qt::view -namespace map +namespace scwx::qt::map { struct MapSettings; -class MapContext : public gl::GlContext +class MapContext { public: explicit MapContext( std::shared_ptr radarProductView = nullptr); - ~MapContext() override; + ~MapContext(); MapContext(const MapContext&) = delete; MapContext& operator=(const MapContext&) = delete; @@ -37,6 +35,8 @@ public: MapContext(MapContext&&) noexcept; MapContext& operator=(MapContext&&) noexcept; + [[nodiscard]] std::shared_ptr gl_context() const; + [[nodiscard]] std::weak_ptr map() const; [[nodiscard]] std::string map_copyrights() const; [[nodiscard]] MapProvider map_provider() const; @@ -76,5 +76,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace scwx::qt +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index 4a6b7fb7..b1b5979d 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -5,11 +5,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::unordered_map mapProviderName_ { @@ -243,6 +239,4 @@ const MapProviderInfo& GetMapProviderInfo(MapProvider mapProvider) return mapProviderInfo_.at(mapProvider); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_provider.hpp b/scwx-qt/source/scwx/qt/map/map_provider.hpp index 5bdd67fc..1ef6fe80 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.hpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.hpp @@ -6,11 +6,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { enum class MapProvider @@ -19,9 +15,8 @@ enum class MapProvider MapTiler, Unknown }; -typedef scwx::util:: - Iterator - MapProviderIterator; +using MapProviderIterator = scwx::util:: + Iterator; struct MapStyle { @@ -29,7 +24,7 @@ struct MapStyle std::string url_; std::vector drawBelow_; - bool IsValid() const; + [[nodiscard]] bool IsValid() const; }; struct MapProviderInfo @@ -45,6 +40,4 @@ std::string GetMapProviderName(MapProvider mapProvider); std::string GetMapProviderApiKey(MapProvider mapProvider); const MapProviderInfo& GetMapProviderInfo(MapProvider mapProvider); -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_settings.hpp b/scwx-qt/source/scwx/qt/map/map_settings.hpp index a015aca3..05a69b42 100644 --- a/scwx-qt/source/scwx/qt/map/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/map/map_settings.hpp @@ -1,10 +1,6 @@ #pragma once -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { struct MapSettings @@ -22,6 +18,4 @@ struct MapSettings bool radarWireframeEnabled_ {false}; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index e8d0e380..dd839d73 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -32,6 +31,7 @@ #include #include +#include #include #include @@ -57,11 +57,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::map_widget"; @@ -1545,7 +1541,8 @@ void MapWidget::initializeGL() logger_->debug("initializeGL()"); makeCurrent(); - p->context_->Initialize(); + + p->context_->gl_context()->Initialize(); // Lock ImGui font atlas prior to new ImGui frame std::shared_lock imguiFontAtlasLock { @@ -1599,7 +1596,7 @@ void MapWidget::paintGL() p->frameDraws_++; - p->context_->StartFrame(); + p->context_->gl_context()->StartFrame(); // Handle hotkey updates p->HandleHotkeyUpdates(); @@ -2251,8 +2248,6 @@ void MapWidgetImpl::CheckLevel3Availability() } } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map #include "map_widget.moc" diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index d474cd2e..2035b517 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -22,11 +22,7 @@ class QKeyEvent; class QMouseEvent; class QWheelEvent; -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class MapWidgetImpl; @@ -188,6 +184,4 @@ signals: void IncomingLevel2ElevationChanged(std::optional incomingElevation); }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index aec23f84..a40e2835 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -10,11 +10,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::marker_layer"; @@ -23,7 +19,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class MarkerLayer::Impl { public: - explicit Impl(MarkerLayer* self, std::shared_ptr context) : + explicit Impl(MarkerLayer* self, std::shared_ptr context) : self_ {self}, geoIcons_ {std::make_shared(context)}, editMarkerDialog_ {std::make_shared()} @@ -42,7 +38,7 @@ public: MarkerLayer* self_; - std::shared_ptr geoIcons_; + std::shared_ptr geoIcons_; std::shared_ptr editMarkerDialog_; }; @@ -130,7 +126,7 @@ void MarkerLayer::Impl::ReloadMarkers() MarkerLayer::MarkerLayer(const std::shared_ptr& context) : DrawLayer(context, "MarkerLayer"), - p(std::make_unique(this, context)) + p(std::make_unique(this, context->gl_context())) { AddDrawItem(p->geoIcons_); } @@ -162,7 +158,7 @@ void MarkerLayer::Impl::set_icon_sheets() void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); DrawLayer::Render(params); @@ -176,6 +172,4 @@ void MarkerLayer::Deinitialize() DrawLayer::Deinitialize(); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.hpp b/scwx-qt/source/scwx/qt/map/marker_layer.hpp index 9cd0674c..94d49f78 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.hpp @@ -2,13 +2,7 @@ #include -#include - -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class MarkerLayer : public DrawLayer @@ -28,6 +22,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index ba692aae..aaff42e9 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -26,11 +26,7 @@ # pragma warning(pop) #endif -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::overlay_layer"; @@ -39,8 +35,8 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class OverlayLayerImpl { public: - explicit OverlayLayerImpl(OverlayLayer* self, - std::shared_ptr context) : + explicit OverlayLayerImpl(OverlayLayer* self, + std::shared_ptr context) : self_ {self}, activeBoxOuter_ {std::make_shared(context)}, activeBoxInner_ {std::make_shared(context)}, @@ -155,9 +151,9 @@ public: bool sweepTimePicked_ {false}; }; -OverlayLayer::OverlayLayer(std::shared_ptr context) : +OverlayLayer::OverlayLayer(const std::shared_ptr& context) : DrawLayer(context, "OverlayLayer"), - p(std::make_unique(this, context)) + p(std::make_unique(this, context->gl_context())) { AddDrawItem(p->activeBoxOuter_); AddDrawItem(p->activeBoxInner_); @@ -336,7 +332,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) { const std::unique_lock lock {p->renderMutex_}; - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); auto radarProductView = context()->radar_product_view(); auto& settings = context()->settings(); const float pixelRatio = context()->pixel_ratio(); @@ -616,6 +612,4 @@ void OverlayLayer::UpdateSweepTimeNextFrame() p->sweepTimeNeedsUpdate_ = true; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.hpp b/scwx-qt/source/scwx/qt/map/overlay_layer.hpp index f842e81b..731cf766 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class OverlayLayerImpl; @@ -16,7 +12,7 @@ class OverlayLayer : public DrawLayer Q_DISABLE_COPY_MOVE(OverlayLayer) public: - explicit OverlayLayer(std::shared_ptr context); + explicit OverlayLayer(const std::shared_ptr& context); ~OverlayLayer(); void Initialize() override final; @@ -38,6 +34,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 76eeaa8b..00e2f1bd 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -11,11 +11,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::overlay_product_layer"; @@ -24,8 +20,8 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class OverlayProductLayer::Impl { public: - explicit Impl(OverlayProductLayer* self, - const std::shared_ptr& context) : + explicit Impl(OverlayProductLayer* self, + std::shared_ptr context) : self_ {self}, linkedVectors_ {std::make_shared(context)} { @@ -108,9 +104,10 @@ public: std::shared_ptr linkedVectors_; }; -OverlayProductLayer::OverlayProductLayer(std::shared_ptr context) : +OverlayProductLayer::OverlayProductLayer( + const std::shared_ptr& context) : DrawLayer(context, "OverlayProductLayer"), - p(std::make_unique(this, context)) + p(std::make_unique(this, context->gl_context())) { auto overlayProductView = context->overlay_product_view(); connect(overlayProductView.get(), @@ -142,7 +139,7 @@ void OverlayProductLayer::Initialize() void OverlayProductLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); if (p->stiNeedsUpdate_) { @@ -449,6 +446,4 @@ bool OverlayProductLayer::RunMousePicking( eventHandler); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp index 8f65c2d6..9db8c02b 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp @@ -2,17 +2,13 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class OverlayProductLayer : public DrawLayer { public: - explicit OverlayProductLayer(std::shared_ptr context); + explicit OverlayProductLayer(const std::shared_ptr& context); ~OverlayProductLayer(); void Initialize() override final; @@ -32,6 +28,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index dcead2a1..7da00a1b 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -12,11 +12,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::placefile_layer"; @@ -25,9 +21,9 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class PlacefileLayer::Impl { public: - explicit Impl(PlacefileLayer* self, - const std::shared_ptr& context, - const std::string& placefileName) : + explicit Impl(PlacefileLayer* self, + std::shared_ptr context, + const std::string& placefileName) : self_ {self}, placefileName_ {placefileName}, placefileIcons_ {std::make_shared(context)}, @@ -67,7 +63,8 @@ public: PlacefileLayer::PlacefileLayer(const std::shared_ptr& context, const std::string& placefileName) : DrawLayer(context, fmt::format("PlacefileLayer {}", placefileName)), - p(std::make_unique(this, context, placefileName)) + p(std::make_unique( + this, context->gl_context(), placefileName)) { AddDrawItem(p->placefileImages_); AddDrawItem(p->placefilePolygons_); @@ -129,7 +126,7 @@ void PlacefileLayer::Initialize() void PlacefileLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); std::shared_ptr placefileManager = manager::PlacefileManager::Instance(); @@ -261,6 +258,4 @@ void PlacefileLayer::Impl::ReloadDataSync() Q_EMIT self_->DataReloaded(); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.hpp b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp index 981c3c12..9a6d49d1 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp @@ -4,11 +4,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class PlacefileLayer : public DrawLayer @@ -38,6 +34,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 7bda264f..fbafd997 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -26,11 +26,7 @@ # pragma warning(pop) #endif -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::radar_product_layer"; @@ -76,7 +72,8 @@ public: bool sweepNeedsUpdate_; }; -RadarProductLayer::RadarProductLayer(std::shared_ptr context) : +RadarProductLayer::RadarProductLayer( + const std::shared_ptr& context) : GenericLayer(context), p(std::make_unique()) { auto radarProductView = context->radar_product_view(); @@ -95,11 +92,13 @@ void RadarProductLayer::Initialize() { logger_->debug("Initialize()"); - gl::OpenGLFunctions& gl = context()->gl(); + auto glContext = context()->gl_context(); + + gl::OpenGLFunctions& gl = glContext->gl(); // Load and configure radar shader p->shaderProgram_ = - context()->GetShaderProgram(":/gl/radar.vert", ":/gl/radar.frag"); + glContext->GetShaderProgram(":/gl/radar.vert", ":/gl/radar.frag"); p->uMVPMatrixLocation_ = gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); @@ -159,7 +158,7 @@ void RadarProductLayer::Initialize() void RadarProductLayer::UpdateSweep() { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); boost::timer::cpu_timer timer; @@ -261,7 +260,7 @@ void RadarProductLayer::UpdateSweep() void RadarProductLayer::Render( const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); p->shaderProgram_->Use(); @@ -324,7 +323,7 @@ void RadarProductLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); gl.glDeleteVertexArrays(1, &p->vao_); gl.glDeleteBuffers(3, p->vbo_.data()); @@ -536,7 +535,7 @@ void RadarProductLayer::UpdateColorTable() p->colorTableNeedsUpdate_ = false; - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); std::shared_ptr radarProductView = context()->radar_product_view(); @@ -563,6 +562,4 @@ void RadarProductLayer::UpdateColorTable() gl.glUniform1f(p->uDataMomentScaleLocation_, scale); } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp index 1e53eba8..4491062b 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class RadarProductLayerImpl; @@ -14,7 +10,7 @@ class RadarProductLayerImpl; class RadarProductLayer : public GenericLayer { public: - explicit RadarProductLayer(std::shared_ptr context); + explicit RadarProductLayer(const std::shared_ptr& context); ~RadarProductLayer(); void Initialize() override final; @@ -37,6 +33,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp index e660c266..b23232a3 100644 --- a/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_range_layer.cpp @@ -5,11 +5,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::radar_range_layer"; @@ -98,6 +94,4 @@ GetRangeCircle(float range, QMapLibre::Coordinate center) return rangeCircle; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_range_layer.hpp b/scwx-qt/source/scwx/qt/map/radar_range_layer.hpp index d900f01e..e11ea6fc 100644 --- a/scwx-qt/source/scwx/qt/map/radar_range_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/radar_range_layer.hpp @@ -2,13 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map -{ -namespace RadarRangeLayer +namespace scwx::qt::map::RadarRangeLayer { void Add(std::shared_ptr map, @@ -19,7 +13,4 @@ void Update(std::shared_ptr map, float range, QMapLibre::Coordinate center); -} // namespace RadarRangeLayer -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map::RadarRangeLayer diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 6a9f6d1a..ba748230 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -13,11 +13,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { static const std::string logPrefix_ = "scwx::qt::map::radar_site_layer"; @@ -26,7 +22,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class RadarSiteLayer::Impl { public: - explicit Impl(RadarSiteLayer* self, std::shared_ptr& context) : + explicit Impl(RadarSiteLayer* self, std::shared_ptr context) : self_ {self}, geoLines_ {std::make_shared(context)} { } @@ -54,9 +50,9 @@ public: nullptr, nullptr}; }; -RadarSiteLayer::RadarSiteLayer(std::shared_ptr context) : +RadarSiteLayer::RadarSiteLayer(const std::shared_ptr& context) : DrawLayer(context, "RadarSiteLayer"), - p(std::make_unique(this, context)) + p(std::make_unique(this, context->gl_context())) { } @@ -103,7 +99,7 @@ void RadarSiteLayer::Render( return; } - gl::OpenGLFunctions& gl = context()->gl(); + gl::OpenGLFunctions& gl = context()->gl_context()->gl(); // Update map screen coordinate and scale information p->mapScreenCoordLocation_ = util::maplibre::LatLongToScreenCoordinate( @@ -251,6 +247,4 @@ bool RadarSiteLayer::RunMousePicking( return false; } -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp index f88786f4..e7d2a535 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace map +namespace scwx::qt::map { class RadarSiteLayer : public DrawLayer @@ -15,7 +11,7 @@ class RadarSiteLayer : public DrawLayer Q_DISABLE_COPY_MOVE(RadarSiteLayer) public: - explicit RadarSiteLayer(std::shared_ptr context); + explicit RadarSiteLayer(const std::shared_ptr& context); ~RadarSiteLayer(); void Initialize() override final; @@ -38,6 +34,4 @@ private: std::unique_ptr p; }; -} // namespace map -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::map From 44a864f50fa21e349dde7ebc7513d09b25213f61 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 8 May 2025 23:15:46 -0500 Subject: [PATCH 582/762] Remove GlContext from MapContext, layers receive MapContext from Initialize/Render --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 29 +++-- scwx-qt/source/scwx/qt/map/alert_layer.hpp | 11 +- .../source/scwx/qt/map/color_table_layer.cpp | 60 ++++----- .../source/scwx/qt/map/color_table_layer.hpp | 16 ++- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 57 +++++---- scwx-qt/source/scwx/qt/map/draw_layer.hpp | 27 ++-- scwx-qt/source/scwx/qt/map/generic_layer.cpp | 24 ++-- scwx-qt/source/scwx/qt/map/generic_layer.hpp | 22 ++-- scwx-qt/source/scwx/qt/map/layer_wrapper.cpp | 24 ++-- scwx-qt/source/scwx/qt/map/layer_wrapper.hpp | 15 ++- scwx-qt/source/scwx/qt/map/map_context.cpp | 8 -- scwx-qt/source/scwx/qt/map/map_context.hpp | 3 - scwx-qt/source/scwx/qt/map/map_widget.cpp | 42 +++--- scwx-qt/source/scwx/qt/map/marker_layer.cpp | 31 +++-- scwx-qt/source/scwx/qt/map/marker_layer.hpp | 10 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 87 ++++++------- scwx-qt/source/scwx/qt/map/overlay_layer.hpp | 29 +++-- .../scwx/qt/map/overlay_product_layer.cpp | 58 +++++---- .../scwx/qt/map/overlay_product_layer.hpp | 27 ++-- .../source/scwx/qt/map/placefile_layer.cpp | 40 +++--- .../source/scwx/qt/map/placefile_layer.hpp | 12 +- .../scwx/qt/map/radar_product_layer.cpp | 121 +++++++++--------- .../scwx/qt/map/radar_product_layer.hpp | 25 ++-- .../source/scwx/qt/map/radar_site_layer.cpp | 42 +++--- .../source/scwx/qt/map/radar_site_layer.hpp | 24 ++-- 25 files changed, 446 insertions(+), 398 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 18efbc81..93a6d2d1 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -135,14 +135,14 @@ public: std::size_t lineWidth_ {}; }; - explicit Impl(AlertLayer* self, - std::shared_ptr context, - awips::Phenomenon phenomenon) : + explicit Impl(AlertLayer* self, + const std::shared_ptr& glContext, + awips::Phenomenon phenomenon) : self_ {self}, phenomenon_ {phenomenon}, ibw_ {awips::ibw::GetImpactBasedWarningInfo(phenomenon)}, - geoLines_ {{false, std::make_shared(context)}, - {true, std::make_shared(context)}} + geoLines_ {{false, std::make_shared(glContext)}, + {true, std::make_shared(glContext)}} { UpdateLineData(); ConnectSignals(); @@ -245,12 +245,12 @@ public: std::vector connections_ {}; }; -AlertLayer::AlertLayer(const std::shared_ptr& context, - awips::Phenomenon phenomenon) : +AlertLayer::AlertLayer(const std::shared_ptr& glContext, + awips::Phenomenon phenomenon) : DrawLayer( - context, + glContext, fmt::format("AlertLayer {}", awips::GetPhenomenonText(phenomenon))), - p(std::make_unique(this, context->gl_context(), phenomenon)) + p(std::make_unique(this, glContext, phenomenon)) { for (auto alertActive : {false, true}) { @@ -274,11 +274,11 @@ void AlertLayer::InitializeHandler() } } -void AlertLayer::Initialize() +void AlertLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize: {}", awips::GetPhenomenonText(p->phenomenon_)); - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); auto& alertLayerHandler = AlertLayerHandler::Instance(); @@ -296,16 +296,17 @@ void AlertLayer::Initialize() p->ConnectAlertHandlerSignals(); } -void AlertLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void AlertLayer::Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); for (auto alertActive : {false, true}) { p->geoLines_.at(alertActive)->set_selected_time(p->selectedTime_); } - DrawLayer::Render(params); + DrawLayer::Render(mapContext, params); SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.hpp b/scwx-qt/source/scwx/qt/map/alert_layer.hpp index e14b5d1a..0416ff04 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.hpp @@ -16,13 +16,14 @@ class AlertLayer : public DrawLayer Q_DISABLE_COPY_MOVE(AlertLayer) public: - explicit AlertLayer(const std::shared_ptr& context, - scwx::awips::Phenomenon phenomenon); + explicit AlertLayer(const std::shared_ptr& glContext, + scwx::awips::Phenomenon phenomenon); ~AlertLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; static void InitializeHandler(); diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index bed9ec23..f9046120 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -20,44 +20,40 @@ namespace scwx::qt::map static const std::string logPrefix_ = "scwx::qt::map::color_table_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class ColorTableLayerImpl +class ColorTableLayer::Impl { public: - explicit ColorTableLayerImpl() : - shaderProgram_(nullptr), - uMVPMatrixLocation_(GL_INVALID_INDEX), - vbo_ {GL_INVALID_INDEX}, - vao_ {GL_INVALID_INDEX}, - texture_ {GL_INVALID_INDEX}, - colorTable_ {}, - colorTableNeedsUpdate_ {true} - { - } - ~ColorTableLayerImpl() = default; + explicit Impl() = default; + ~Impl() = default; - std::shared_ptr shaderProgram_; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; - GLint uMVPMatrixLocation_; - std::array vbo_; - GLuint vao_; - GLuint texture_; + std::shared_ptr shaderProgram_ {nullptr}; - std::vector colorTable_; + GLint uMVPMatrixLocation_ {static_cast(GL_INVALID_INDEX)}; + std::array vbo_ {GL_INVALID_INDEX}; + GLuint vao_ {GL_INVALID_INDEX}; + GLuint texture_ {GL_INVALID_INDEX}; - bool colorTableNeedsUpdate_; + std::vector colorTable_ {}; + + bool colorTableNeedsUpdate_ {true}; }; -ColorTableLayer::ColorTableLayer(const std::shared_ptr& context) : - GenericLayer(context), p(std::make_unique()) +ColorTableLayer::ColorTableLayer(std::shared_ptr glContext) : + GenericLayer(std::move(glContext)), p(std::make_unique()) { } ColorTableLayer::~ColorTableLayer() = default; -void ColorTableLayer::Initialize() +void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); - auto glContext = context()->gl_context(); + auto glContext = gl_context(); gl::OpenGLFunctions& gl = glContext->gl(); @@ -107,20 +103,20 @@ void ColorTableLayer::Initialize() gl.glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, static_cast(0)); gl.glEnableVertexAttribArray(1); - connect(context()->radar_product_view().get(), + connect(mapContext->radar_product_view().get(), &view::RadarProductView::ColorTableLutUpdated, this, [this]() { p->colorTableNeedsUpdate_ = true; }); } void ColorTableLayer::Render( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); - auto radarProductView = context()->radar_product_view(); + gl::OpenGLFunctions& gl = gl_context()->gl(); + auto radarProductView = mapContext->radar_product_view(); - if (context()->radar_product_view() == nullptr || - !context()->radar_product_view()->IsInitialized()) + if (radarProductView == nullptr || !radarProductView->IsInitialized()) { // Defer rendering until view is initialized return; @@ -180,11 +176,11 @@ void ColorTableLayer::Render( gl.glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); gl.glDrawArrays(GL_TRIANGLES, 0, 6); - context()->set_color_table_margins(QMargins {0, 0, 0, 10}); + mapContext->set_color_table_margins(QMargins {0, 0, 0, 10}); } else { - context()->set_color_table_margins(QMargins {}); + mapContext->set_color_table_margins(QMargins {}); } SCWX_GL_CHECK_ERROR(); @@ -194,7 +190,7 @@ void ColorTableLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); gl.glDeleteVertexArrays(1, &p->vao_); gl.glDeleteBuffers(2, p->vbo_.data()); @@ -204,8 +200,6 @@ void ColorTableLayer::Deinitialize() p->vao_ = GL_INVALID_INDEX; p->vbo_ = {GL_INVALID_INDEX}; p->texture_ = GL_INVALID_INDEX; - - context()->set_color_table_margins(QMargins {}); } } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.hpp b/scwx-qt/source/scwx/qt/map/color_table_layer.hpp index ce72f358..17cb505c 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.hpp @@ -5,20 +5,22 @@ namespace scwx::qt::map { -class ColorTableLayerImpl; - class ColorTableLayer : public GenericLayer { + Q_DISABLE_COPY_MOVE(ColorTableLayer) + public: - explicit ColorTableLayer(const std::shared_ptr& context); + explicit ColorTableLayer(std::shared_ptr glContext); ~ColorTableLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 8372829c..fbe9a087 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -1,10 +1,11 @@ -#include #include #include #include #include #include +#include + #include #include #include @@ -17,12 +18,12 @@ namespace scwx::qt::map static const std::string logPrefix_ = "scwx::qt::map::draw_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class DrawLayerImpl +class DrawLayer::Impl { public: - explicit DrawLayerImpl(std::shared_ptr context, - const std::string& imGuiContextName) : - context_ {std::move(context)}, drawList_ {} + explicit Impl(std::shared_ptr glContext, + const std::string& imGuiContextName) : + glContext_ {std::move(glContext)} { static size_t currentLayerId_ {0u}; imGuiContextName_ = @@ -35,7 +36,7 @@ public: // Initialize ImGui Qt backend ImGui_ImplQt_Init(); } - ~DrawLayerImpl() + ~Impl() { // Set ImGui Context ImGui::SetCurrentContext(imGuiContext_); @@ -51,13 +52,14 @@ public: model::ImGuiContextModel::Instance().DestroyContext(imGuiContextName_); } - DrawLayerImpl(const DrawLayerImpl&) = delete; - DrawLayerImpl& operator=(const DrawLayerImpl&) = delete; - DrawLayerImpl(const DrawLayerImpl&&) = delete; - DrawLayerImpl& operator=(const DrawLayerImpl&&) = delete; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; - std::shared_ptr context_; - std::vector> drawList_; + std::shared_ptr glContext_; + + std::vector> drawList_ {}; GLuint textureAtlas_ {GL_INVALID_INDEX}; std::uint64_t textureAtlasBuildCount_ {}; @@ -67,27 +69,27 @@ public: bool imGuiRendererInitialized_ {}; }; -DrawLayer::DrawLayer(const std::shared_ptr& context, - const std::string& imGuiContextName) : - GenericLayer(context), - p(std::make_unique(context, imGuiContextName)) +DrawLayer::DrawLayer(std::shared_ptr glContext, + const std::string& imGuiContextName) : + GenericLayer(glContext), + p(std::make_unique(std::move(glContext), imGuiContextName)) { } DrawLayer::~DrawLayer() = default; -void DrawLayer::Initialize() +void DrawLayer::Initialize(const std::shared_ptr& mapContext) { - p->textureAtlas_ = p->context_->gl_context()->GetTextureAtlas(); + p->textureAtlas_ = p->glContext_->GetTextureAtlas(); for (auto& item : p->drawList_) { item->Initialize(); } - ImGuiInitialize(); + ImGuiInitialize(mapContext); } -void DrawLayer::ImGuiFrameStart() +void DrawLayer::ImGuiFrameStart(const std::shared_ptr& mapContext) { auto defaultFont = manager::FontManager::Instance().GetImGuiFont( types::FontCategory::Default); @@ -96,7 +98,7 @@ void DrawLayer::ImGuiFrameStart() ImGui::SetCurrentContext(p->imGuiContext_); // Start ImGui Frame - ImGui_ImplQt_NewFrame(p->context_->widget()); + ImGui_ImplQt_NewFrame(mapContext->widget()); ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); ImGui::PushFont(defaultFont->font()); @@ -112,10 +114,10 @@ void DrawLayer::ImGuiFrameEnd() ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } -void DrawLayer::ImGuiInitialize() +void DrawLayer::ImGuiInitialize(const std::shared_ptr& mapContext) { ImGui::SetCurrentContext(p->imGuiContext_); - ImGui_ImplQt_RegisterWidget(p->context_->widget()); + ImGui_ImplQt_RegisterWidget(mapContext->widget()); ImGui_ImplOpenGL3_Init(); p->imGuiRendererInitialized_ = true; } @@ -123,7 +125,7 @@ void DrawLayer::ImGuiInitialize() void DrawLayer::RenderWithoutImGui( const QMapLibre::CustomLayerRenderParameters& params) { - auto glContext = p->context_->gl_context(); + auto& glContext = p->glContext_; gl::OpenGLFunctions& gl = glContext->gl(); p->textureAtlas_ = glContext->GetTextureAtlas(); @@ -146,14 +148,16 @@ void DrawLayer::RenderWithoutImGui( p->textureAtlasBuildCount_ = newTextureAtlasBuildCount; } + void DrawLayer::ImGuiSelectContext() { ImGui::SetCurrentContext(p->imGuiContext_); } -void DrawLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void DrawLayer::Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) { - ImGuiFrameStart(); + ImGuiFrameStart(mapContext); RenderWithoutImGui(params); ImGuiFrameEnd(); } @@ -169,6 +173,7 @@ void DrawLayer::Deinitialize() } bool DrawLayer::RunMousePicking( + const std::shared_ptr& /* mapContext */, const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.hpp b/scwx-qt/source/scwx/qt/map/draw_layer.hpp index 93d8a54a..f0589eef 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.hpp @@ -6,21 +6,23 @@ namespace scwx::qt::map { -class DrawLayerImpl; - class DrawLayer : public GenericLayer { + Q_DISABLE_COPY_MOVE(DrawLayer) + public: - explicit DrawLayer(const std::shared_ptr& context, - const std::string& imGuiContextName); + explicit DrawLayer(std::shared_ptr glContext, + const std::string& imGuiContextName); virtual ~DrawLayer(); - virtual void Initialize() override; - virtual void Render(const QMapLibre::CustomLayerRenderParameters&) override; - virtual void Deinitialize() override; + void Initialize(const std::shared_ptr& mapContext) override; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) override; + void Deinitialize() override; - virtual bool - RunMousePicking(const QMapLibre::CustomLayerRenderParameters& params, + bool + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, @@ -29,15 +31,16 @@ public: protected: void AddDrawItem(const std::shared_ptr& drawItem); - void ImGuiFrameStart(); + void ImGuiFrameStart(const std::shared_ptr& mapContext); void ImGuiFrameEnd(); - void ImGuiInitialize(); + void ImGuiInitialize(const std::shared_ptr& mapContext); void RenderWithoutImGui(const QMapLibre::CustomLayerRenderParameters& params); void ImGuiSelectContext(); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.cpp b/scwx-qt/source/scwx/qt/map/generic_layer.cpp index 45ebea00..ec88d1f1 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.cpp @@ -3,26 +3,32 @@ namespace scwx::qt::map { -class GenericLayerImpl +class GenericLayer::Impl { public: - explicit GenericLayerImpl(std::shared_ptr context) : - context_ {std::move(context)} + explicit Impl(std::shared_ptr glContext) : + glContext_ {std::move(glContext)} { } - ~GenericLayerImpl() {} + ~Impl() = default; - std::shared_ptr context_; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + std::shared_ptr glContext_; }; -GenericLayer::GenericLayer(const std::shared_ptr& context) : - p(std::make_unique(context)) +GenericLayer::GenericLayer(std::shared_ptr glContext) : + p(std::make_unique(std::move(glContext))) { } GenericLayer::~GenericLayer() = default; bool GenericLayer::RunMousePicking( + const std::shared_ptr& /* mapContext */, const QMapLibre::CustomLayerRenderParameters& /* params */, const QPointF& /* mouseLocalPos */, const QPointF& /* mouseGlobalPos */, @@ -34,9 +40,9 @@ bool GenericLayer::RunMousePicking( return false; } -std::shared_ptr GenericLayer::context() const +std::shared_ptr GenericLayer::gl_context() const { - return p->context_; + return p->glContext_; } } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp index c9abdecb..32c95ffc 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,23 +14,24 @@ namespace scwx::qt::map { -class GenericLayerImpl; - class GenericLayer : public QObject { Q_OBJECT + Q_DISABLE_COPY_MOVE(GenericLayer) public: - explicit GenericLayer(const std::shared_ptr& context); + explicit GenericLayer(std::shared_ptr glContext); virtual ~GenericLayer(); - virtual void Initialize() = 0; - virtual void Render(const QMapLibre::CustomLayerRenderParameters&) = 0; - virtual void Deinitialize() = 0; + virtual void Initialize(const std::shared_ptr& mapContext) = 0; + virtual void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) = 0; + virtual void Deinitialize() = 0; /** * @brief Run mouse picking on the layer. * + * @param [in] mapContext Map context * @param [in] params Custom layer render parameters * @param [in] mouseLocalPos Mouse cursor widget position * @param [in] mouseGlobalPos Mouse cursor screen position @@ -40,7 +42,8 @@ public: * @return true if a draw item was picked, otherwise false */ virtual bool - RunMousePicking(const QMapLibre::CustomLayerRenderParameters& params, + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, @@ -51,10 +54,11 @@ signals: void NeedsRendering(); protected: - std::shared_ptr context() const; + [[nodiscard]] std::shared_ptr gl_context() const; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp b/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp index 8bb5e878..5a60f90b 100644 --- a/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp +++ b/scwx-qt/source/scwx/qt/map/layer_wrapper.cpp @@ -3,21 +3,29 @@ namespace scwx::qt::map { -class LayerWrapperImpl +class LayerWrapper::Impl { public: - explicit LayerWrapperImpl(std::shared_ptr layer) : - layer_ {std::move(layer)} + explicit Impl(std::shared_ptr layer, + std::shared_ptr mapContext) : + layer_ {std::move(layer)}, mapContext_ {std::move(mapContext)} { } - ~LayerWrapperImpl() {} + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::shared_ptr layer_; + std::shared_ptr mapContext_; }; -LayerWrapper::LayerWrapper(const std::shared_ptr& layer) : - p(std::make_unique(layer)) +LayerWrapper::LayerWrapper(std::shared_ptr layer, + std::shared_ptr mapContext) : + p(std::make_unique(std::move(layer), std::move(mapContext))) { } LayerWrapper::~LayerWrapper() = default; @@ -30,7 +38,7 @@ void LayerWrapper::initialize() auto& layer = p->layer_; if (layer != nullptr) { - layer->Initialize(); + layer->Initialize(p->mapContext_); } } @@ -39,7 +47,7 @@ void LayerWrapper::render(const QMapLibre::CustomLayerRenderParameters& params) auto& layer = p->layer_; if (layer != nullptr) { - layer->Render(params); + layer->Render(p->mapContext_, params); } } diff --git a/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp b/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp index 6e4cc62e..ae133c29 100644 --- a/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp +++ b/scwx-qt/source/scwx/qt/map/layer_wrapper.hpp @@ -1,16 +1,16 @@ #pragma once #include +#include namespace scwx::qt::map { -class LayerWrapperImpl; - class LayerWrapper : public QMapLibre::CustomLayerHostInterface { public: - explicit LayerWrapper(const std::shared_ptr& layer); + explicit LayerWrapper(std::shared_ptr layer, + std::shared_ptr mapContext); ~LayerWrapper(); LayerWrapper(const LayerWrapper&) = delete; @@ -19,12 +19,13 @@ public: LayerWrapper(LayerWrapper&&) noexcept; LayerWrapper& operator=(LayerWrapper&&) noexcept; - void initialize() override final; - void render(const QMapLibre::CustomLayerRenderParameters&) override final; - void deinitialize() override final; + void initialize() final; + void render(const QMapLibre::CustomLayerRenderParameters&) final; + void deinitialize() final; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 48cf4afc..1d1b5c4d 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -16,9 +16,6 @@ public: ~Impl() {} - std::shared_ptr glContext_ { - std::make_shared()}; - std::weak_ptr map_ {}; MapSettings settings_ {}; float pixelRatio_ {1.0f}; @@ -50,11 +47,6 @@ MapContext::~MapContext() = default; MapContext::MapContext(MapContext&&) noexcept = default; MapContext& MapContext::operator=(MapContext&&) noexcept = default; -std::shared_ptr MapContext::gl_context() const -{ - return p->glContext_; -} - std::weak_ptr MapContext::map() const { return p->map_; diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index 9cf611a6..ba319b95 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -35,8 +34,6 @@ public: MapContext(MapContext&&) noexcept; MapContext& operator=(MapContext&&) noexcept; - [[nodiscard]] std::shared_ptr gl_context() const; - [[nodiscard]] std::weak_ptr map() const; [[nodiscard]] std::string map_copyrights() const; [[nodiscard]] MapProvider map_provider() const; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index dd839d73..2d408627 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -73,7 +73,6 @@ public: const QMapLibre::Settings& settings) : id_ {id}, uuid_ {boost::uuids::random_generator()()}, - context_ {std::make_shared()}, widget_ {widget}, settings_(settings), map_(), @@ -154,9 +153,9 @@ public: void AddLayer(types::LayerType type, types::LayerDescription description, const std::string& before = {}); - void AddLayer(const std::string& id, - std::shared_ptr layer, - const std::string& before = {}); + void AddLayer(const std::string& id, + const std::shared_ptr& layer, + const std::string& before = {}); void AddLayers(); void AddPlacefileLayer(const std::string& placefileName, const std::string& before); @@ -194,7 +193,9 @@ public: std::size_t id_; boost::uuids::uuid uuid_; - std::shared_ptr context_; + std::shared_ptr context_ {std::make_shared()}; + std::shared_ptr glContext_ { + std::make_shared()}; MapWidget* widget_; QMapLibre::Settings settings_; @@ -1239,7 +1240,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, // If there is a radar product view, create the radar product layer if (radarProductView != nullptr) { - radarProductLayer_ = std::make_shared(context_); + radarProductLayer_ = std::make_shared(glContext_); AddLayer(layerName, radarProductLayer_, before); } } @@ -1248,7 +1249,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, auto phenomenon = std::get(description); std::shared_ptr alertLayer = - std::make_shared(context_, phenomenon); + std::make_shared(glContext_, phenomenon); AddLayer(fmt::format("alert.{}", awips::GetPhenomenonCode(phenomenon)), alertLayer, before); @@ -1272,7 +1273,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, { // Create the map overlay layer case types::InformationLayer::MapOverlay: - overlayLayer_ = std::make_shared(context_); + overlayLayer_ = std::make_shared(glContext_); AddLayer(layerName, overlayLayer_, before); break; @@ -1280,14 +1281,14 @@ void MapWidgetImpl::AddLayer(types::LayerType type, case types::InformationLayer::ColorTable: if (radarProductView != nullptr) { - colorTableLayer_ = std::make_shared(context_); + colorTableLayer_ = std::make_shared(glContext_); AddLayer(layerName, colorTableLayer_, before); } break; // Create the radar site layer case types::InformationLayer::RadarSite: - radarSiteLayer_ = std::make_shared(context_); + radarSiteLayer_ = std::make_shared(glContext_); AddLayer(layerName, radarSiteLayer_, before); connect( radarSiteLayer_.get(), @@ -1303,7 +1304,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, // Create the location marker layer case types::InformationLayer::Markers: - markerLayer_ = std::make_shared(context_); + markerLayer_ = std::make_shared(glContext_); AddLayer(layerName, markerLayer_, before); break; @@ -1320,7 +1321,7 @@ void MapWidgetImpl::AddLayer(types::LayerType type, if (radarProductView != nullptr) { overlayProductLayer_ = - std::make_shared(context_); + std::make_shared(glContext_); AddLayer(layerName, overlayProductLayer_, before); } break; @@ -1350,7 +1351,7 @@ void MapWidgetImpl::AddPlacefileLayer(const std::string& placefileName, const std::string& before) { std::shared_ptr placefileLayer = - std::make_shared(context_, placefileName); + std::make_shared(glContext_, placefileName); placefileLayers_.push_back(placefileLayer); AddLayer(GetPlacefileLayerName(placefileName), placefileLayer, before); @@ -1367,13 +1368,13 @@ MapWidgetImpl::GetPlacefileLayerName(const std::string& placefileName) return types::GetLayerName(types::LayerType::Placefile, placefileName); } -void MapWidgetImpl::AddLayer(const std::string& id, - std::shared_ptr layer, - const std::string& before) +void MapWidgetImpl::AddLayer(const std::string& id, + const std::shared_ptr& layer, + const std::string& before) { // QMapLibre::addCustomLayer will take ownership of the std::unique_ptr std::unique_ptr pHost = - std::make_unique(layer); + std::make_unique(layer, context_); try { @@ -1542,7 +1543,7 @@ void MapWidget::initializeGL() makeCurrent(); - p->context_->gl_context()->Initialize(); + p->glContext_->Initialize(); // Lock ImGui font atlas prior to new ImGui frame std::shared_lock imguiFontAtlasLock { @@ -1596,7 +1597,7 @@ void MapWidget::paintGL() p->frameDraws_++; - p->context_->gl_context()->StartFrame(); + p->glContext_->StartFrame(); // Handle hotkey updates p->HandleHotkeyUpdates(); @@ -1705,7 +1706,8 @@ void MapWidgetImpl::RunMousePicking() for (auto it = genericLayers_.rbegin(); it != genericLayers_.rend(); ++it) { // Run mouse picking for each layer - if ((*it)->RunMousePicking(params, + if ((*it)->RunMousePicking(context_, + params, lastPos_, lastGlobalPos_, mouseScreenCoordinate, diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index a40e2835..69a22e91 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -5,11 +5,11 @@ #include #include +#include + #include #include -#include - namespace scwx::qt::map { @@ -19,15 +19,21 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class MarkerLayer::Impl { public: - explicit Impl(MarkerLayer* self, std::shared_ptr context) : + explicit Impl(MarkerLayer* self, + const std::shared_ptr& glContext) : self_ {self}, - geoIcons_ {std::make_shared(context)}, + geoIcons_ {std::make_shared(glContext)}, editMarkerDialog_ {std::make_shared()} { ConnectSignals(); } ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void ReloadMarkers(); void ConnectSignals(); @@ -124,19 +130,19 @@ void MarkerLayer::Impl::ReloadMarkers() Q_EMIT self_->NeedsRendering(); } -MarkerLayer::MarkerLayer(const std::shared_ptr& context) : - DrawLayer(context, "MarkerLayer"), - p(std::make_unique(this, context->gl_context())) +MarkerLayer::MarkerLayer(const std::shared_ptr& glContext) : + DrawLayer(glContext, "MarkerLayer"), + p(std::make_unique(this, glContext)) { AddDrawItem(p->geoIcons_); } MarkerLayer::~MarkerLayer() = default; -void MarkerLayer::Initialize() +void MarkerLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); p->set_icon_sheets(); p->ReloadMarkers(); @@ -156,11 +162,12 @@ void MarkerLayer::Impl::set_icon_sheets() geoIcons_->FinishIconSheets(); } -void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void MarkerLayer::Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); - DrawLayer::Render(params); + DrawLayer::Render(mapContext, params); SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.hpp b/scwx-qt/source/scwx/qt/map/marker_layer.hpp index 94d49f78..a5f67d2b 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.hpp @@ -8,14 +8,16 @@ namespace scwx::qt::map class MarkerLayer : public DrawLayer { Q_OBJECT + Q_DISABLE_COPY_MOVE(MarkerLayer) public: - explicit MarkerLayer(const std::shared_ptr& context); + explicit MarkerLayer(const std::shared_ptr& context); ~MarkerLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index aaff42e9..b7853738 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -32,16 +32,16 @@ namespace scwx::qt::map static const std::string logPrefix_ = "scwx::qt::map::overlay_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class OverlayLayerImpl +class OverlayLayer::Impl { public: - explicit OverlayLayerImpl(OverlayLayer* self, - std::shared_ptr context) : + explicit Impl(OverlayLayer* self, + const std::shared_ptr& glContext) : self_ {self}, - activeBoxOuter_ {std::make_shared(context)}, - activeBoxInner_ {std::make_shared(context)}, - geoIcons_ {std::make_shared(context)}, - icons_ {std::make_shared(context)}, + activeBoxOuter_ {std::make_shared(glContext)}, + activeBoxInner_ {std::make_shared(glContext)}, + geoIcons_ {std::make_shared(glContext)}, + icons_ {std::make_shared(glContext)}, renderMutex_ {} { auto& generalSettings = settings::GeneralSettings::Instance(); @@ -71,7 +71,7 @@ public: [this](const bool&) { Q_EMIT self_->NeedsRendering(); }); } - ~OverlayLayerImpl() + ~Impl() { auto& generalSettings = settings::GeneralSettings::Instance(); @@ -87,6 +87,11 @@ public: showMapLogoCallbackUuid_); } + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void SetupGeoIcons(); void SetCusorLocation(common::Coordinate coordinate); @@ -151,9 +156,9 @@ public: bool sweepTimePicked_ {false}; }; -OverlayLayer::OverlayLayer(const std::shared_ptr& context) : - DrawLayer(context, "OverlayLayer"), - p(std::make_unique(this, context->gl_context())) +OverlayLayer::OverlayLayer(const std::shared_ptr& glContext) : + DrawLayer(glContext, "OverlayLayer"), + p(std::make_unique(this, glContext)) { AddDrawItem(p->activeBoxOuter_); AddDrawItem(p->activeBoxInner_); @@ -168,13 +173,13 @@ OverlayLayer::~OverlayLayer() p->cursorScaleConnection_.disconnect(); } -void OverlayLayerImpl::SetCusorLocation(common::Coordinate coordinate) +void OverlayLayer::Impl::SetCusorLocation(common::Coordinate coordinate) { geoIcons_->SetIconLocation( cursorIcon_, coordinate.latitude_, coordinate.longitude_); } -void OverlayLayerImpl::SetupGeoIcons() +void OverlayLayer::Impl::SetupGeoIcons() { const std::unique_lock lock {renderMutex_}; @@ -208,13 +213,13 @@ void OverlayLayerImpl::SetupGeoIcons() geoIcons_->FinishIcons(); } -void OverlayLayer::Initialize() +void OverlayLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); - auto radarProductView = context()->radar_product_view(); + auto radarProductView = mapContext->radar_product_view(); if (radarProductView != nullptr) { @@ -251,7 +256,7 @@ void OverlayLayer::Initialize() p->icons_->SetIconTexture(p->compassIcon_, p->cardinalPointIconName_, 0); gl::draw::Icons::RegisterEventHandler( p->compassIcon_, - [this](QEvent* ev) + [this, mapContext](QEvent* ev) { switch (ev->type()) { @@ -276,7 +281,7 @@ void OverlayLayer::Initialize() if (mouseEvent->buttons() == Qt::MouseButton::LeftButton && p->lastBearing_ != 0.0) { - auto map = context()->map().lock(); + auto map = mapContext->map().lock(); if (map != nullptr) { map->setBearing(0.0); @@ -295,11 +300,11 @@ void OverlayLayer::Initialize() p->icons_->SetIconTexture(p->mapCenterIcon_, p->mapCenterIconName_, 0); p->mapLogoIcon_ = p->icons_->AddIcon(); - if (context()->map_provider() == MapProvider::Mapbox) + if (mapContext->map_provider() == MapProvider::Mapbox) { p->icons_->SetIconTexture(p->mapLogoIcon_, p->mapboxLogoImageName_, 0); } - else if (context()->map_provider() == MapProvider::MapTiler) + else if (mapContext->map_provider() == MapProvider::MapTiler) { p->icons_->SetIconTexture(p->mapLogoIcon_, p->mapTilerLogoImageName_, 0); } @@ -328,16 +333,17 @@ void OverlayLayer::Initialize() }); } -void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +void OverlayLayer::Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) { const std::unique_lock lock {p->renderMutex_}; - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); - auto radarProductView = context()->radar_product_view(); - auto& settings = context()->settings(); - const float pixelRatio = context()->pixel_ratio(); + gl::OpenGLFunctions& gl = gl_context()->gl(); + auto radarProductView = mapContext->radar_product_view(); + auto& settings = mapContext->settings(); + const float pixelRatio = mapContext->pixel_ratio(); - ImGuiFrameStart(); + ImGuiFrameStart(mapContext); p->sweepTimePicked_ = false; @@ -383,7 +389,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->geoIcons_->SetIconVisible(p->cursorIcon_, cursorIconVisible); if (cursorIconVisible) { - p->SetCusorLocation(context()->mouse_coordinate()); + p->SetCusorLocation(mapContext->mouse_coordinate()); } // Location Icon @@ -507,7 +513,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) p->icons_->SetIconVisible(p->mapCenterIcon_, generalSettings.show_map_center().GetValue()); - QMargins colorTableMargins = context()->color_table_margins(); + QMargins colorTableMargins = mapContext->color_table_margins(); if (colorTableMargins != p->lastColorTableMargins_ || p->firstRender_) { // Draw map logo with a 10x10 indent from the bottom left @@ -520,7 +526,7 @@ void OverlayLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) DrawLayer::RenderWithoutImGui(params); - auto mapCopyrights = context()->map_copyrights(); + auto mapCopyrights = mapContext->map_copyrights(); if (mapCopyrights.length() > 0 && generalSettings.show_map_attribution().GetValue()) { @@ -563,29 +569,13 @@ void OverlayLayer::Deinitialize() DrawLayer::Deinitialize(); - auto radarProductView = context()->radar_product_view(); - - if (radarProductView != nullptr) - { - disconnect(radarProductView.get(), - &view::RadarProductView::SweepComputed, - this, - &OverlayLayer::UpdateSweepTimeNextFrame); - } - - disconnect(p->positionManager_.get(), - &manager::PositionManager::LocationTrackingChanged, - this, - nullptr); - disconnect(p->positionManager_.get(), - &manager::PositionManager::PositionUpdated, - this, - nullptr); + disconnect(this); p->locationIcon_ = nullptr; } bool OverlayLayer::RunMousePicking( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, @@ -599,7 +589,8 @@ bool OverlayLayer::RunMousePicking( return true; } - return DrawLayer::RunMousePicking(params, + return DrawLayer::RunMousePicking(mapContext, + params, mouseLocalPos, mouseGlobalPos, mouseCoords, diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.hpp b/scwx-qt/source/scwx/qt/map/overlay_layer.hpp index 731cf766..89e49b6b 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.hpp @@ -5,33 +5,34 @@ namespace scwx::qt::map { -class OverlayLayerImpl; - class OverlayLayer : public DrawLayer { Q_DISABLE_COPY_MOVE(OverlayLayer) public: - explicit OverlayLayer(const std::shared_ptr& context); + explicit OverlayLayer(const std::shared_ptr& glContext); ~OverlayLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; - bool RunMousePicking( - const QMapLibre::CustomLayerRenderParameters& params, - const QPointF& mouseLocalPos, - const QPointF& mouseGlobalPos, - const glm::vec2& mouseCoords, - const common::Coordinate& mouseGeoCoords, - std::shared_ptr& eventHandler) override final; + bool + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) final; public slots: void UpdateSweepTimeNextFrame(); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 00e2f1bd..4684d227 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -20,10 +20,10 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class OverlayProductLayer::Impl { public: - explicit Impl(OverlayProductLayer* self, - std::shared_ptr context) : + explicit Impl(OverlayProductLayer* self, + const std::shared_ptr& glContext) : self_ {self}, - linkedVectors_ {std::make_shared(context)} + linkedVectors_ {std::make_shared(glContext)} { auto& productSettings = settings::ProductSettings::Instance(); @@ -60,7 +60,8 @@ public: stiPastEnabledCallbackUuid_); } - void UpdateStormTrackingInformation(); + void UpdateStormTrackingInformation( + const std::shared_ptr& mapContext); static void HandleLinkedVectorPacket( const std::shared_ptr& packet, @@ -105,11 +106,21 @@ public: }; OverlayProductLayer::OverlayProductLayer( - const std::shared_ptr& context) : - DrawLayer(context, "OverlayProductLayer"), - p(std::make_unique(this, context->gl_context())) + const std::shared_ptr& glContext) : + DrawLayer(glContext, "OverlayProductLayer"), + p(std::make_unique(this, glContext)) { - auto overlayProductView = context->overlay_product_view(); + AddDrawItem(p->linkedVectors_); +} + +OverlayProductLayer::~OverlayProductLayer() = default; + +void OverlayProductLayer::Initialize( + const std::shared_ptr& mapContext) +{ + logger_->debug("Initialize()"); + + auto overlayProductView = mapContext->overlay_product_view(); connect(overlayProductView.get(), &view::OverlayProductView::ProductUpdated, this, @@ -122,31 +133,23 @@ OverlayProductLayer::OverlayProductLayer( } }); - AddDrawItem(p->linkedVectors_); -} + p->UpdateStormTrackingInformation(mapContext); -OverlayProductLayer::~OverlayProductLayer() = default; - -void OverlayProductLayer::Initialize() -{ - logger_->debug("Initialize()"); - - p->UpdateStormTrackingInformation(); - - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); } void OverlayProductLayer::Render( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); if (p->stiNeedsUpdate_) { - p->UpdateStormTrackingInformation(); + p->UpdateStormTrackingInformation(mapContext); } - DrawLayer::Render(params); + DrawLayer::Render(mapContext, params); SCWX_GL_CHECK_ERROR(); } @@ -155,16 +158,19 @@ void OverlayProductLayer::Deinitialize() { logger_->debug("Deinitialize()"); + disconnect(this); + DrawLayer::Deinitialize(); } -void OverlayProductLayer::Impl::UpdateStormTrackingInformation() +void OverlayProductLayer::Impl::UpdateStormTrackingInformation( + const std::shared_ptr& mapContext) { logger_->debug("Update Storm Tracking Information"); stiNeedsUpdate_ = false; - auto overlayProductView = self_->context()->overlay_product_view(); + auto overlayProductView = mapContext->overlay_product_view(); auto radarProductManager = overlayProductView->radar_product_manager(); auto message = overlayProductView->radar_product_message("NST"); @@ -431,6 +437,7 @@ std::string OverlayProductLayer::Impl::BuildHoverText( } bool OverlayProductLayer::RunMousePicking( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, @@ -438,7 +445,8 @@ bool OverlayProductLayer::RunMousePicking( const common::Coordinate& mouseGeoCoords, std::shared_ptr& eventHandler) { - return DrawLayer::RunMousePicking(params, + return DrawLayer::RunMousePicking(mapContext, + params, mouseLocalPos, mouseGlobalPos, mouseCoords, diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp index 9db8c02b..5c2fd73e 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp @@ -7,21 +7,26 @@ namespace scwx::qt::map class OverlayProductLayer : public DrawLayer { + Q_DISABLE_COPY_MOVE(OverlayProductLayer) + public: - explicit OverlayProductLayer(const std::shared_ptr& context); + explicit OverlayProductLayer( + const std::shared_ptr& glContext); ~OverlayProductLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; - bool RunMousePicking( - const QMapLibre::CustomLayerRenderParameters& params, - const QPointF& mouseLocalPos, - const QPointF& mouseGlobalPos, - const glm::vec2& mouseCoords, - const common::Coordinate& mouseGeoCoords, - std::shared_ptr& eventHandler) override final; + bool + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) final; private: class Impl; diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index 7da00a1b..d725850e 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -21,25 +21,31 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class PlacefileLayer::Impl { public: - explicit Impl(PlacefileLayer* self, - std::shared_ptr context, - const std::string& placefileName) : + explicit Impl(PlacefileLayer* self, + const std::shared_ptr& glContext, + const std::string& placefileName) : self_ {self}, placefileName_ {placefileName}, - placefileIcons_ {std::make_shared(context)}, - placefileImages_ {std::make_shared(context)}, - placefileLines_ {std::make_shared(context)}, + placefileIcons_ {std::make_shared(glContext)}, + placefileImages_ { + std::make_shared(glContext)}, + placefileLines_ {std::make_shared(glContext)}, placefilePolygons_ { - std::make_shared(context)}, + std::make_shared(glContext)}, placefileTriangles_ { - std::make_shared(context)}, + std::make_shared(glContext)}, placefileText_ { - std::make_shared(context, placefileName)} + std::make_shared(glContext, placefileName)} { ConnectSignals(); } ~Impl() { threadPool_.join(); } + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void ConnectSignals(); void ReloadDataSync(); @@ -60,11 +66,10 @@ public: std::chrono::system_clock::time_point selectedTime_ {}; }; -PlacefileLayer::PlacefileLayer(const std::shared_ptr& context, +PlacefileLayer::PlacefileLayer(const std::shared_ptr& glContext, const std::string& placefileName) : - DrawLayer(context, fmt::format("PlacefileLayer {}", placefileName)), - p(std::make_unique( - this, context->gl_context(), placefileName)) + DrawLayer(glContext, fmt::format("PlacefileLayer {}", placefileName)), + p(std::make_unique(this, glContext, placefileName)) { AddDrawItem(p->placefileImages_); AddDrawItem(p->placefilePolygons_); @@ -114,19 +119,20 @@ void PlacefileLayer::set_placefile_name(const std::string& placefileName) ReloadData(); } -void PlacefileLayer::Initialize() +void PlacefileLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); } void PlacefileLayer::Render( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); std::shared_ptr placefileManager = manager::PlacefileManager::Instance(); @@ -153,7 +159,7 @@ void PlacefileLayer::Render( p->placefileText_->set_selected_time(p->selectedTime_); } - DrawLayer::Render(params); + DrawLayer::Render(mapContext, params); SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.hpp b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp index 9a6d49d1..35f5a81b 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.hpp @@ -10,19 +10,21 @@ namespace scwx::qt::map class PlacefileLayer : public DrawLayer { Q_OBJECT + Q_DISABLE_COPY_MOVE(PlacefileLayer) public: - explicit PlacefileLayer(const std::shared_ptr& context, - const std::string& placefileName); + explicit PlacefileLayer(const std::shared_ptr& glContext, + const std::string& placefileName); ~PlacefileLayer(); std::string placefile_name() const; void set_placefile_name(const std::string& placefileName); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; void ReloadData(); diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index fbafd997..c1fbd6c4 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -32,67 +32,48 @@ namespace scwx::qt::map static const std::string logPrefix_ = "scwx::qt::map::radar_product_layer"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -class RadarProductLayerImpl +class RadarProductLayer::Impl { public: - explicit RadarProductLayerImpl() : - shaderProgram_(nullptr), - uMVPMatrixLocation_(GL_INVALID_INDEX), - uMapScreenCoordLocation_(GL_INVALID_INDEX), - uDataMomentOffsetLocation_(GL_INVALID_INDEX), - uDataMomentScaleLocation_(GL_INVALID_INDEX), - uCFPEnabledLocation_(GL_INVALID_INDEX), - vbo_ {GL_INVALID_INDEX}, - vao_ {GL_INVALID_INDEX}, - texture_ {GL_INVALID_INDEX}, - numVertices_ {0}, - cfpEnabled_ {false}, - colorTableNeedsUpdate_ {false}, - sweepNeedsUpdate_ {false} - { - } - ~RadarProductLayerImpl() = default; + explicit Impl() = default; + ~Impl() = default; - std::shared_ptr shaderProgram_; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; - GLint uMVPMatrixLocation_; - GLint uMapScreenCoordLocation_; - GLint uDataMomentOffsetLocation_; - GLint uDataMomentScaleLocation_; - GLint uCFPEnabledLocation_; - std::array vbo_; - GLuint vao_; - GLuint texture_; + std::shared_ptr shaderProgram_ {nullptr}; - GLsizeiptr numVertices_; + GLint uMVPMatrixLocation_ {static_cast(GL_INVALID_INDEX)}; + GLint uMapScreenCoordLocation_ {static_cast(GL_INVALID_INDEX)}; + GLint uDataMomentOffsetLocation_ {static_cast(GL_INVALID_INDEX)}; + GLint uDataMomentScaleLocation_ {static_cast(GL_INVALID_INDEX)}; + GLint uCFPEnabledLocation_ {static_cast(GL_INVALID_INDEX)}; + std::array vbo_ {GL_INVALID_INDEX}; + GLuint vao_ {GL_INVALID_INDEX}; + GLuint texture_ {GL_INVALID_INDEX}; - bool cfpEnabled_; + GLsizeiptr numVertices_ {0}; - bool colorTableNeedsUpdate_; - bool sweepNeedsUpdate_; + bool cfpEnabled_ {false}; + + bool colorTableNeedsUpdate_ {false}; + bool sweepNeedsUpdate_ {false}; }; -RadarProductLayer::RadarProductLayer( - const std::shared_ptr& context) : - GenericLayer(context), p(std::make_unique()) +RadarProductLayer::RadarProductLayer(std::shared_ptr glContext) : + GenericLayer(std::move(glContext)), p(std::make_unique()) { - auto radarProductView = context->radar_product_view(); - connect(radarProductView.get(), - &view::RadarProductView::ColorTableLutUpdated, - this, - [this]() { p->colorTableNeedsUpdate_ = true; }); - connect(radarProductView.get(), - &view::RadarProductView::SweepComputed, - this, - [this]() { p->sweepNeedsUpdate_ = true; }); } RadarProductLayer::~RadarProductLayer() = default; -void RadarProductLayer::Initialize() +void RadarProductLayer::Initialize( + const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); - auto glContext = context()->gl_context(); + auto glContext = gl_context(); gl::OpenGLFunctions& gl = glContext->gl(); @@ -145,25 +126,36 @@ void RadarProductLayer::Initialize() // Update radar sweep p->sweepNeedsUpdate_ = true; - UpdateSweep(); + UpdateSweep(mapContext); // Create color table gl.glGenTextures(1, &p->texture_); p->colorTableNeedsUpdate_ = true; - UpdateColorTable(); + UpdateColorTable(mapContext); gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + + auto radarProductView = mapContext->radar_product_view(); + connect(radarProductView.get(), + &view::RadarProductView::ColorTableLutUpdated, + this, + [this]() { p->colorTableNeedsUpdate_ = true; }); + connect(radarProductView.get(), + &view::RadarProductView::SweepComputed, + this, + [this]() { p->sweepNeedsUpdate_ = true; }); } -void RadarProductLayer::UpdateSweep() +void RadarProductLayer::UpdateSweep( + const std::shared_ptr& mapContext) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); boost::timer::cpu_timer timer; std::shared_ptr radarProductView = - context()->radar_product_view(); + mapContext->radar_product_view(); std::unique_lock sweepLock(radarProductView->sweep_mutex(), std::try_to_lock); @@ -258,16 +250,17 @@ void RadarProductLayer::UpdateSweep() } void RadarProductLayer::Render( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); p->shaderProgram_->Use(); // Set OpenGL blend mode for transparency gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - const bool wireframeEnabled = context()->settings().radarWireframeEnabled_; + const bool wireframeEnabled = mapContext->settings().radarWireframeEnabled_; if (wireframeEnabled) { // Set polygon mode to draw wireframe @@ -276,12 +269,12 @@ void RadarProductLayer::Render( if (p->colorTableNeedsUpdate_) { - UpdateColorTable(); + UpdateColorTable(mapContext); } if (p->sweepNeedsUpdate_) { - UpdateSweep(); + UpdateSweep(mapContext); } const float scale = std::pow(2.0, params.zoom) * 2.0f * @@ -323,7 +316,7 @@ void RadarProductLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); gl.glDeleteVertexArrays(1, &p->vao_); gl.glDeleteBuffers(3, p->vbo_.data()); @@ -339,6 +332,7 @@ void RadarProductLayer::Deinitialize() } bool RadarProductLayer::RunMousePicking( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& /* params */, const QPointF& /* mouseLocalPos */, const QPointF& mouseGlobalPos, @@ -352,16 +346,16 @@ bool RadarProductLayer::RunMousePicking( Qt::KeyboardModifier::ShiftModifier) { std::shared_ptr radarProductView = - context()->radar_product_view(); + mapContext->radar_product_view(); - if (context()->radar_site() == nullptr) + if (mapContext->radar_site() == nullptr) { return itemPicked; } // Get distance and altitude of point - const double radarLatitude = context()->radar_site()->latitude(); - const double radarLongitude = context()->radar_site()->longitude(); + const double radarLatitude = mapContext->radar_site()->latitude(); + const double radarLongitude = mapContext->radar_site()->longitude(); const auto distanceMeters = util::GeographicLib::GetDistance(mouseGeoCoords.latitude_, @@ -396,7 +390,7 @@ bool RadarProductLayer::RunMousePicking( util::GeographicLib::GetRadarBeamAltititude( distanceMeters, units::angle::degrees(*elevation), - context()->radar_site()->altitude()); + mapContext->radar_site()->altitude()); const std::string heightUnitName = settings::UnitSettings::Instance().echo_tops_units().GetValue(); @@ -529,15 +523,16 @@ bool RadarProductLayer::RunMousePicking( return itemPicked; } -void RadarProductLayer::UpdateColorTable() +void RadarProductLayer::UpdateColorTable( + const std::shared_ptr& mapContext) { logger_->debug("UpdateColorTable()"); p->colorTableNeedsUpdate_ = false; - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); std::shared_ptr radarProductView = - context()->radar_product_view(); + mapContext->radar_product_view(); const std::vector& colorTable = radarProductView->color_table_lut(); diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp index 4491062b..df5adcc9 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.hpp @@ -5,20 +5,22 @@ namespace scwx::qt::map { -class RadarProductLayerImpl; - class RadarProductLayer : public GenericLayer { + Q_DISABLE_COPY_MOVE(RadarProductLayer) + public: - explicit RadarProductLayer(const std::shared_ptr& context); + explicit RadarProductLayer(std::shared_ptr glContext); ~RadarProductLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; - virtual bool - RunMousePicking(const QMapLibre::CustomLayerRenderParameters& params, + bool + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, const QPointF& mouseGlobalPos, const glm::vec2& mouseCoords, @@ -26,11 +28,12 @@ public: std::shared_ptr& eventHandler) override; private: - void UpdateColorTable(); - void UpdateSweep(); + void UpdateColorTable(const std::shared_ptr& mapContext); + void UpdateSweep(const std::shared_ptr& mapContext); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::qt::map diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index ba748230..418fa2ff 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -22,15 +22,21 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class RadarSiteLayer::Impl { public: - explicit Impl(RadarSiteLayer* self, std::shared_ptr context) : - self_ {self}, geoLines_ {std::make_shared(context)} + explicit Impl(RadarSiteLayer* self, + const std::shared_ptr& glContext) : + self_ {self}, geoLines_ {std::make_shared(glContext)} { } ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + void RenderRadarSite(const QMapLibre::CustomLayerRenderParameters& params, std::shared_ptr& radarSite); - void RenderRadarLine(); + void RenderRadarLine(const std::shared_ptr& mapContext); RadarSiteLayer* self_; @@ -50,15 +56,16 @@ public: nullptr, nullptr}; }; -RadarSiteLayer::RadarSiteLayer(const std::shared_ptr& context) : - DrawLayer(context, "RadarSiteLayer"), - p(std::make_unique(this, context->gl_context())) +RadarSiteLayer::RadarSiteLayer( + const std::shared_ptr& glContext) : + DrawLayer(glContext, "RadarSiteLayer"), + p(std::make_unique(this, glContext)) { } RadarSiteLayer::~RadarSiteLayer() = default; -void RadarSiteLayer::Initialize() +void RadarSiteLayer::Initialize(const std::shared_ptr& mapContext) { logger_->debug("Initialize()"); @@ -81,10 +88,11 @@ void RadarSiteLayer::Initialize() AddDrawItem(p->geoLines_); p->geoLines_->set_thresholded(false); - DrawLayer::Initialize(); + DrawLayer::Initialize(mapContext); } void RadarSiteLayer::Render( + const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { p->hoverText_.clear(); @@ -99,7 +107,7 @@ void RadarSiteLayer::Render( return; } - gl::OpenGLFunctions& gl = context()->gl_context()->gl(); + gl::OpenGLFunctions& gl = gl_context()->gl(); // Update map screen coordinate and scale information p->mapScreenCoordLocation_ = util::maplibre::LatLongToScreenCoordinate( @@ -111,7 +119,7 @@ void RadarSiteLayer::Render( p->halfWidth_ = params.width * 0.5f; p->halfHeight_ = params.height * 0.5f; - ImGuiFrameStart(); + ImGuiFrameStart(mapContext); // Radar site ImGui windows shouldn't have padding ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {0.0f, 0.0f}); @@ -122,7 +130,7 @@ void RadarSiteLayer::Render( ImGui::PopStyleVar(); - p->RenderRadarLine(); + p->RenderRadarLine(mapContext); DrawLayer::RenderWithoutImGui(params); @@ -192,15 +200,16 @@ void RadarSiteLayer::Impl::RenderRadarSite( } } -void RadarSiteLayer::Impl::RenderRadarLine() +void RadarSiteLayer::Impl::RenderRadarLine( + const std::shared_ptr& mapContext) { if ((QGuiApplication::keyboardModifiers() & Qt::KeyboardModifier::ShiftModifier) && - self_->context()->radar_site() != nullptr) + mapContext->radar_site() != nullptr) { - const auto& mouseCoord = self_->context()->mouse_coordinate(); - const double radarLatitude = self_->context()->radar_site()->latitude(); - const double radarLongitude = self_->context()->radar_site()->longitude(); + const auto& mouseCoord = mapContext->mouse_coordinate(); + const double radarLatitude = mapContext->radar_site()->latitude(); + const double radarLongitude = mapContext->radar_site()->longitude(); geoLines_->SetLineLocation(radarSiteLines_[0], static_cast(mouseCoord.latitude_), @@ -231,6 +240,7 @@ void RadarSiteLayer::Deinitialize() } bool RadarSiteLayer::RunMousePicking( + const std::shared_ptr& /* mapContext */, const QMapLibre::CustomLayerRenderParameters& /* params */, const QPointF& /* mouseLocalPos */, const QPointF& mouseGlobalPos, diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp index e7d2a535..74fc6398 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.hpp @@ -11,20 +11,22 @@ class RadarSiteLayer : public DrawLayer Q_DISABLE_COPY_MOVE(RadarSiteLayer) public: - explicit RadarSiteLayer(const std::shared_ptr& context); + explicit RadarSiteLayer(const std::shared_ptr& glContext); ~RadarSiteLayer(); - void Initialize() override final; - void Render(const QMapLibre::CustomLayerRenderParameters&) override final; - void Deinitialize() override final; + void Initialize(const std::shared_ptr& mapContext) final; + void Render(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters&) final; + void Deinitialize() final; - bool RunMousePicking( - const QMapLibre::CustomLayerRenderParameters& params, - const QPointF& mouseLocalPos, - const QPointF& mouseGlobalPos, - const glm::vec2& mouseCoords, - const common::Coordinate& mouseGeoCoords, - std::shared_ptr& eventHandler) override final; + bool + RunMousePicking(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) final; signals: void RadarSiteSelected(const std::string& id); From 21e56970736c855a8ae123775e2f94054ffb4dee Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 8 May 2025 23:27:02 -0500 Subject: [PATCH 583/762] Use a shared GlContext for all MapWidgets --- scwx-qt/source/scwx/qt/main/main_window.cpp | 44 +++++++++++---------- scwx-qt/source/scwx/qt/map/map_widget.cpp | 17 ++++---- scwx-qt/source/scwx/qt/map/map_widget.hpp | 9 ++++- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 30e3b699..a209de28 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1,6 +1,7 @@ #include "main_window.hpp" #include "./ui_main_window.h" +#include #include #include #include @@ -776,6 +777,8 @@ void MainWindowImpl::ConfigureMapLayout() } }; + auto glContext = std::make_shared(); + for (int64_t y = 0; y < gridHeight; y++) { QSplitter* hs = new QSplitter(vs); @@ -785,7 +788,8 @@ void MainWindowImpl::ConfigureMapLayout() { if (maps_.at(mapIndex) == nullptr) { - maps_[mapIndex] = new map::MapWidget(mapIndex, settings_); + maps_[mapIndex] = + new map::MapWidget(mapIndex, settings_, glContext); } hs->addWidget(maps_[mapIndex]); @@ -818,9 +822,9 @@ void MainWindowImpl::ConfigureMapStyles() if ((customStyleAvailable_ && styleName == "Custom") || std::find_if(mapProviderInfo.mapStyles_.cbegin(), mapProviderInfo.mapStyles_.cend(), - [&](const auto& mapStyle) { - return mapStyle.name_ == styleName; - }) != mapProviderInfo.mapStyles_.cend()) + [&](const auto& mapStyle) + { return mapStyle.name_ == styleName; }) != + mapProviderInfo.mapStyles_.cend()) { // Initialize map style from settings maps_.at(i)->SetInitialMapStyle(styleName); @@ -1154,22 +1158,22 @@ void MainWindowImpl::ConnectOtherSignals() mapSettings.radar_product(i).StageValue(map->GetRadarProductName()); } }); - connect(level2ProductsWidget_, - &ui::Level2ProductsWidget::RadarProductSelected, - mainWindow_, - [&](common::RadarProductGroup group, - const std::string& productName, - int16_t productCode) { - SelectRadarProduct(activeMap_, group, productName, productCode); - }); - connect(level3ProductsWidget_, - &ui::Level3ProductsWidget::RadarProductSelected, - mainWindow_, - [&](common::RadarProductGroup group, - const std::string& productName, - int16_t productCode) { - SelectRadarProduct(activeMap_, group, productName, productCode); - }); + connect( + level2ProductsWidget_, + &ui::Level2ProductsWidget::RadarProductSelected, + mainWindow_, + [&](common::RadarProductGroup group, + const std::string& productName, + int16_t productCode) + { SelectRadarProduct(activeMap_, group, productName, productCode); }); + connect( + level3ProductsWidget_, + &ui::Level3ProductsWidget::RadarProductSelected, + mainWindow_, + [&](common::RadarProductGroup group, + const std::string& productName, + int16_t productCode) + { SelectRadarProduct(activeMap_, group, productName, productCode); }); connect(level2SettingsWidget_, &ui::Level2SettingsWidget::ElevationSelected, mainWindow_, diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 2d408627..cfeba407 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -68,11 +68,13 @@ class MapWidgetImpl : public QObject Q_OBJECT public: - explicit MapWidgetImpl(MapWidget* widget, - std::size_t id, - const QMapLibre::Settings& settings) : + explicit MapWidgetImpl(MapWidget* widget, + std::size_t id, + const QMapLibre::Settings& settings, + std::shared_ptr glContext) : id_ {id}, uuid_ {boost::uuids::random_generator()()}, + glContext_ {std::move(glContext)}, widget_ {widget}, settings_(settings), map_(), @@ -194,8 +196,7 @@ public: boost::uuids::uuid uuid_; std::shared_ptr context_ {std::make_shared()}; - std::shared_ptr glContext_ { - std::make_shared()}; + std::shared_ptr glContext_; MapWidget* widget_; QMapLibre::Settings settings_; @@ -280,8 +281,10 @@ public slots: void Update(); }; -MapWidget::MapWidget(std::size_t id, const QMapLibre::Settings& settings) : - p(std::make_unique(this, id, settings)) +MapWidget::MapWidget(std::size_t id, + const QMapLibre::Settings& settings, + std::shared_ptr glContext) : + p(std::make_unique(this, id, settings, std::move(glContext))) { if (settings::GeneralSettings::Instance().anti_aliasing_enabled().GetValue()) { diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 2035b517..348e9113 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -22,6 +22,11 @@ class QKeyEvent; class QMouseEvent; class QWheelEvent; +namespace scwx::qt::gl +{ +class GlContext; +} + namespace scwx::qt::map { @@ -32,7 +37,9 @@ class MapWidget : public QOpenGLWidget Q_OBJECT public: - explicit MapWidget(std::size_t id, const QMapLibre::Settings&); + explicit MapWidget(std::size_t id, + const QMapLibre::Settings&, + std::shared_ptr glContext); ~MapWidget(); void DumpLayerList() const; From d5e50198113f1c521392c23825560e92d164b3a2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 8 May 2025 23:32:28 -0500 Subject: [PATCH 584/762] Fix GenericLayer formatting --- scwx-qt/source/scwx/qt/map/generic_layer.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/generic_layer.hpp b/scwx-qt/source/scwx/qt/map/generic_layer.hpp index 32c95ffc..2b713c9c 100644 --- a/scwx-qt/source/scwx/qt/map/generic_layer.hpp +++ b/scwx-qt/source/scwx/qt/map/generic_layer.hpp @@ -23,10 +23,10 @@ public: explicit GenericLayer(std::shared_ptr glContext); virtual ~GenericLayer(); - virtual void Initialize(const std::shared_ptr& mapContext) = 0; + virtual void Initialize(const std::shared_ptr& mapContext) = 0; virtual void Render(const std::shared_ptr& mapContext, - const QMapLibre::CustomLayerRenderParameters&) = 0; - virtual void Deinitialize() = 0; + const QMapLibre::CustomLayerRenderParameters&) = 0; + virtual void Deinitialize() = 0; /** * @brief Run mouse picking on the layer. From 4955dcd2c9dcc036ca5042fed242c9aae4a5ed78 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 9 May 2025 18:11:49 -0500 Subject: [PATCH 585/762] Gl cleanup clang-tidy fixes --- scwx-qt/source/scwx/qt/main/main_window.cpp | 1 + scwx-qt/source/scwx/qt/map/color_table_layer.cpp | 8 +++++++- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 5 +++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 5 +++-- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index a209de28..d85a5a84 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -788,6 +788,7 @@ void MainWindowImpl::ConfigureMapLayout() { if (maps_.at(mapIndex) == nullptr) { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Owned by parent maps_[mapIndex] = new map::MapWidget(mapIndex, settings_, glContext); } diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index f9046120..30b8a11e 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -176,7 +176,13 @@ void ColorTableLayer::Render( gl.glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); gl.glDrawArrays(GL_TRIANGLES, 0, 6); - mapContext->set_color_table_margins(QMargins {0, 0, 0, 10}); + static constexpr int kLeftMargin_ = 0; + static constexpr int kTopMargin_ = 0; + static constexpr int kRightMargin_ = 0; + static constexpr int kBottomMargin_ = 10; + + mapContext->set_color_table_margins( + QMargins {kLeftMargin_, kTopMargin_, kRightMargin_, kBottomMargin_}); } else { diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index fbe9a087..4128f893 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -131,8 +131,9 @@ void DrawLayer::RenderWithoutImGui( p->textureAtlas_ = glContext->GetTextureAtlas(); // Determine if the texture atlas changed since last render - std::uint64_t newTextureAtlasBuildCount = glContext->texture_buffer_count(); - bool textureAtlasChanged = + const std::uint64_t newTextureAtlasBuildCount = + glContext->texture_buffer_count(); + const bool textureAtlasChanged = newTextureAtlasBuildCount != p->textureAtlasBuildCount_; // Set OpenGL blend mode for transparency diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index cfeba407..78362a28 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -70,13 +71,13 @@ class MapWidgetImpl : public QObject public: explicit MapWidgetImpl(MapWidget* widget, std::size_t id, - const QMapLibre::Settings& settings, + QMapLibre::Settings settings, std::shared_ptr glContext) : id_ {id}, uuid_ {boost::uuids::random_generator()()}, glContext_ {std::move(glContext)}, widget_ {widget}, - settings_(settings), + settings_(std::move(settings)), map_(), layerList_ {}, imGuiRendererInitialized_ {false}, diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index b7853738..2af24d43 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -513,7 +513,7 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, p->icons_->SetIconVisible(p->mapCenterIcon_, generalSettings.show_map_center().GetValue()); - QMargins colorTableMargins = mapContext->color_table_margins(); + const QMargins colorTableMargins = mapContext->color_table_margins(); if (colorTableMargins != p->lastColorTableMargins_ || p->firstRender_) { // Draw map logo with a 10x10 indent from the bottom left From 8e0c95d6ea98f059073cb1075c9973afd0bf2a27 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 13:13:32 +0000 Subject: [PATCH 586/762] Update dependency libxml2 to v2.13.8 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index d4ed7bd7..b6eb2af5 100644 --- a/conanfile.py +++ b/conanfile.py @@ -16,7 +16,7 @@ class SupercellWxConan(ConanFile): "gtest/1.16.0", "libcurl/8.12.1", "libpng/1.6.48", - "libxml2/2.13.6", + "libxml2/2.13.8", "openssl/3.4.1", "range-v3/0.12.0", "re2/20240702", From 46cd75cff48037f662b2a533cc9a8e7201ca4b78 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 13 May 2025 22:43:56 -0500 Subject: [PATCH 587/762] Fix GlContext destruction of OpenGLFunctions causing crash --- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 57 ++++++++++++--------- scwx-qt/source/scwx/qt/main/main_window.cpp | 6 ++- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index d927aef3..66296a70 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -16,50 +16,48 @@ static const std::string logPrefix_ = "scwx::qt::gl::gl_context"; class GlContext::Impl { public: - explicit Impl() : - gl_ {}, - shaderProgramMap_ {}, - shaderProgramMutex_ {}, - textureAtlas_ {GL_INVALID_INDEX}, - textureMutex_ {} - { - } - ~Impl() {} + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void InitializeGL(); static std::size_t GetShaderKey(std::initializer_list> shaders); - gl::OpenGLFunctions gl_; - QOpenGLFunctions_3_0 gl30_; + gl::OpenGLFunctions* gl_ {nullptr}; + QOpenGLFunctions_3_0* gl30_ {nullptr}; bool glInitialized_ {false}; std::unordered_map> - shaderProgramMap_; - std::mutex shaderProgramMutex_; + shaderProgramMap_ {}; + std::mutex shaderProgramMutex_ {}; - GLuint textureAtlas_; - std::mutex textureMutex_; + GLuint textureAtlas_ {GL_INVALID_INDEX}; + std::mutex textureMutex_ {}; std::uint64_t textureBufferCount_ {}; }; GlContext::GlContext() : p(std::make_unique()) {} -GlContext::~GlContext() = default; +GlContext::~GlContext() {}; GlContext::GlContext(GlContext&&) noexcept = default; GlContext& GlContext::operator=(GlContext&&) noexcept = default; gl::OpenGLFunctions& GlContext::gl() { - return p->gl_; + return *p->gl_; } QOpenGLFunctions_3_0& GlContext::gl30() { - return p->gl30_; + return *p->gl30_; } std::uint64_t GlContext::texture_buffer_count() const @@ -74,10 +72,19 @@ void GlContext::Impl::InitializeGL() return; } - gl_.initializeOpenGLFunctions(); - gl30_.initializeOpenGLFunctions(); + // QOpenGLFunctions objects will not be freed. Since "destruction" takes + // place at the end of program execution, it is OK to intentionally leak + // these. - gl_.glGenTextures(1, &textureAtlas_); + // NOLINTBEGIN(cppcoreguidelines-owning-memory) + gl_ = new gl::OpenGLFunctions(); + gl30_ = new QOpenGLFunctions_3_0(); + // NOLINTEND(cppcoreguidelines-owning-memory) + + gl_->initializeOpenGLFunctions(); + gl30_->initializeOpenGLFunctions(); + + gl_->glGenTextures(1, &textureAtlas_); glInitialized_ = true; } @@ -102,7 +109,7 @@ std::shared_ptr GlContext::GetShaderProgram( if (it == p->shaderProgramMap_.end()) { - shaderProgram = std::make_shared(p->gl_); + shaderProgram = std::make_shared(*p->gl_); shaderProgram->Load(shaders); p->shaderProgramMap_[key] = shaderProgram; } @@ -125,7 +132,7 @@ GLuint GlContext::GetTextureAtlas() if (p->textureBufferCount_ != textureAtlas.BuildCount()) { p->textureBufferCount_ = textureAtlas.BuildCount(); - textureAtlas.BufferAtlas(p->gl_, p->textureAtlas_); + textureAtlas.BufferAtlas(*p->gl_, p->textureAtlas_); } return p->textureAtlas_; @@ -140,8 +147,8 @@ void GlContext::StartFrame() { auto& gl = p->gl_; - gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - gl.glClear(GL_COLOR_BUFFER_BIT); + gl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + gl->glClear(GL_COLOR_BUFFER_BIT); } std::size_t GlContext::Impl::GetShaderKey( diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d85a5a84..dec713ed 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -190,6 +190,8 @@ public: map::MapProvider mapProvider_; map::MapWidget* activeMap_; + std::shared_ptr glContext_ {nullptr}; + ui::CollapsibleGroup* mapSettingsGroup_; ui::CollapsibleGroup* level2ProductsGroup_; ui::CollapsibleGroup* level2SettingsGroup_; @@ -777,7 +779,7 @@ void MainWindowImpl::ConfigureMapLayout() } }; - auto glContext = std::make_shared(); + glContext_ = std::make_shared(); for (int64_t y = 0; y < gridHeight; y++) { @@ -790,7 +792,7 @@ void MainWindowImpl::ConfigureMapLayout() { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): Owned by parent maps_[mapIndex] = - new map::MapWidget(mapIndex, settings_, glContext); + new map::MapWidget(mapIndex, settings_, glContext_); } hs->addWidget(maps_[mapIndex]); From 9e24a781a66028a23c753870aff3dd078144d7ad Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 13 May 2025 23:22:08 -0500 Subject: [PATCH 588/762] Update scwx-qt/source/scwx/qt/gl/gl_context.cpp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 66296a70..4cc42879 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -45,7 +45,7 @@ public: }; GlContext::GlContext() : p(std::make_unique()) {} -GlContext::~GlContext() {}; +GlContext::~GlContext() = default; GlContext::GlContext(GlContext&&) noexcept = default; GlContext& GlContext::operator=(GlContext&&) noexcept = default; From 4c8f0b4df6c1f9423219b85ff982c8cae74aa746 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 13 May 2025 18:34:50 -0400 Subject: [PATCH 589/762] Add wayland files --- .github/workflows/ci.yml | 2 ++ scwx-qt/scwx-qt.cmake | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 986a5b65..992bb42e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,6 +205,8 @@ jobs: cd plugins/ mkdir -p sqldrivers/ cp "${RUNNER_WORKSPACE}/Qt/${{ matrix.qt_version }}/${{ matrix.qt_arch_dir }}/plugins/sqldrivers/libqsqlite.so" sqldrivers/ + mkdir -p platforms/ + cp ${RUNNER_WORKSPACE}/Qt/${{ matrix.qt_version }}/${{ matrix.qt_arch_dir }}/plugins/platforms/libqwayland* platforms/ cd .. popd tar -czf supercell-wx-${{ matrix.artifact_suffix }}.tar.gz supercell-wx/ diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1628607a..028ee0aa 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -34,6 +34,7 @@ find_package(QT NAMES Qt6 Svg Widgets Sql + WaylandClient REQUIRED) find_package(Qt${QT_VERSION_MAJOR} @@ -48,6 +49,7 @@ find_package(Qt${QT_VERSION_MAJOR} Svg Widgets Sql + WaylandClient REQUIRED) set(SRC_EXE_MAIN source/scwx/qt/main/main.cpp) @@ -696,6 +698,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Positioning Qt${QT_VERSION_MAJOR}::SerialPort Qt${QT_VERSION_MAJOR}::Svg + Qt${QT_VERSION_MAJOR}::WaylandClient Boost::json Boost::timer Boost::atomic From 953975a8511095aae6d3e8d44f5407b76b2f1cec Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 13 May 2025 18:46:56 -0400 Subject: [PATCH 590/762] add wayland-protocols to ubuntu install --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 992bb42e..aca9ffeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,7 @@ jobs: sudo apt-get install doxygen \ libfuse2 \ ninja-build \ + wayland-protocols \ ${{ matrix.compiler_packages }} - name: Setup Python Environment From fbc135103107d6f598b78472005202d5af34f80d Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 13 May 2025 18:56:26 -0400 Subject: [PATCH 591/762] Add libwayland to installed apt --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aca9ffeb..ad80a89f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,6 +127,8 @@ jobs: libfuse2 \ ninja-build \ wayland-protocols \ + libwayland-dev \ + libwayland-egl-backend-dev \ ${{ matrix.compiler_packages }} - name: Setup Python Environment From b90deae897ac8be52a7b04cc9c289790763f7683 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 14 May 2025 17:02:02 -0400 Subject: [PATCH 592/762] Remove unneded WaylandClient libraries --- scwx-qt/scwx-qt.cmake | 3 --- 1 file changed, 3 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 028ee0aa..1628607a 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -34,7 +34,6 @@ find_package(QT NAMES Qt6 Svg Widgets Sql - WaylandClient REQUIRED) find_package(Qt${QT_VERSION_MAJOR} @@ -49,7 +48,6 @@ find_package(Qt${QT_VERSION_MAJOR} Svg Widgets Sql - WaylandClient REQUIRED) set(SRC_EXE_MAIN source/scwx/qt/main/main.cpp) @@ -698,7 +696,6 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Positioning Qt${QT_VERSION_MAJOR}::SerialPort Qt${QT_VERSION_MAJOR}::Svg - Qt${QT_VERSION_MAJOR}::WaylandClient Boost::json Boost::timer Boost::atomic From 4ad3e927305edd2def9c706770a6672796042f04 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 14 May 2025 18:19:31 -0400 Subject: [PATCH 593/762] Add wayland client libraries conditionally --- scwx-qt/scwx-qt.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 1628607a..0e8b2ef1 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -688,6 +688,16 @@ if (MSVC) else() target_compile_options(scwx-qt PRIVATE "$<$:-g>") target_compile_options(supercell-wx PRIVATE "$<$:-g>") + + # Add wayland client packages + find_package(QT NAMES Qt6 + COMPONENTS WaylandClient + REQUIRED) + + find_package(Qt${QT_VERSION_MAJOR} + COMPONENTS WaylandClient + REQUIRED) + target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::WaylandClient) endif() target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets From dd76fbf7484975b3d52f59362650132bdd3b10b5 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 14 May 2025 21:03:52 -0400 Subject: [PATCH 594/762] Add wayland libs to clang tidy review github action --- .github/workflows/clang-tidy-review.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index a7ec09ff..2137b451 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -65,6 +65,9 @@ jobs: sudo apt-get install doxygen \ libfuse2 \ ninja-build \ + wayland-protocols \ + libwayland-dev \ + libwayland-egl-backend-dev \ ${{ matrix.compiler_packages }} - name: Setup Python Environment From 2025698d88355acfff86a7fb7e27bdf96d1756b4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 11 May 2025 01:59:42 -0500 Subject: [PATCH 595/762] Updating modified messages for RDA Build 23.0 --- wxdata/include/scwx/awips/message.hpp | 9 + .../rda/performance_maintenance_data.hpp | 20 +- .../scwx/wsr88d/rda/rda_adaptation_data.hpp | 13 +- .../scwx/wsr88d/rda/rda_status_data.hpp | 5 +- .../wsr88d/rda/digital_radar_data_generic.cpp | 13 + .../rda/performance_maintenance_data.cpp | 889 ++++++------------ .../scwx/wsr88d/rda/rda_adaptation_data.cpp | 581 +++++------- .../scwx/wsr88d/rda/rda_status_data.cpp | 138 ++- 8 files changed, 639 insertions(+), 1029 deletions(-) diff --git a/wxdata/include/scwx/awips/message.hpp b/wxdata/include/scwx/awips/message.hpp index 6a386065..4f2388d0 100644 --- a/wxdata/include/scwx/awips/message.hpp +++ b/wxdata/include/scwx/awips/message.hpp @@ -63,6 +63,15 @@ public: return f; } + static double SwapDouble(double d) + { + std::uint64_t temp; + std::memcpy(&temp, &d, sizeof(std::uint64_t)); + temp = ntohll(temp); + std::memcpy(&d, &temp, sizeof(float)); + return d; + } + template static void SwapArray(std::array& arr, std::size_t size = _Size) diff --git a/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp b/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp index 8e1c91ea..c362af78 100644 --- a/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp @@ -31,10 +31,9 @@ public: uint32_t router_memory_free() const; uint16_t router_memory_utilization() const; uint16_t route_to_rpg() const; - uint32_t csu_loss_of_signal() const; - uint32_t csu_loss_of_frames() const; - uint32_t csu_yellow_alarms() const; - uint32_t csu_blue_alarms() const; + uint16_t t1_port_status() const; + uint16_t router_dedicated_ethernet_port_status() const; + uint16_t router_commercial_ethernet_port_status() const; uint32_t csu_24hr_errored_seconds() const; uint32_t csu_24hr_severely_errored_seconds() const; uint32_t csu_24hr_severely_errored_framing_seconds() const; @@ -48,7 +47,7 @@ public: uint16_t lan_switch_memory_utilization() const; uint16_t ifdr_chasis_temperature() const; uint16_t ifdr_fpga_temperature() const; - int32_t gps_satellites() const; + uint16_t ntp_status() const; uint16_t ipc_status() const; uint16_t commanded_channel_control() const; uint16_t polarization() const; @@ -237,7 +236,7 @@ public: float long_pulse_vertical_dbz0() const; float horizontal_power_sense() const; float vertical_power_sense() const; - float zdr_bias() const; + float zdr_offset() const; float clutter_suppression_delta() const; float clutter_suppression_unfiltered_power() const; float clutter_suppression_filtered_power() const; @@ -256,16 +255,11 @@ public: uint16_t read_status_of_prf_sets() const; uint16_t clutter_filter_map_file_read_status() const; uint16_t clutter_filter_map_file_write_status() const; - uint16_t generatl_disk_io_error() const; + uint16_t general_disk_io_error() const; uint8_t rsp_status() const; - uint8_t motherboard_temperature() const; uint8_t cpu1_temperature() const; uint8_t cpu2_temperature() const; - uint16_t cpu1_fan_speed() const; - uint16_t cpu2_fan_speed() const; - uint16_t rsp_fan1_speed() const; - uint16_t rsp_fan2_speed() const; - uint16_t rsp_fan3_speed() const; + uint16_t rsp_motherboard_power() const; uint16_t spip_comm_status() const; uint16_t hci_comm_status() const; uint16_t signal_processor_command_status() const; diff --git a/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp index 38d519a1..be4f8067 100644 --- a/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp @@ -17,7 +17,7 @@ public: explicit RdaAdaptationData(); ~RdaAdaptationData(); - RdaAdaptationData(const RdaAdaptationData&) = delete; + RdaAdaptationData(const RdaAdaptationData&) = delete; RdaAdaptationData& operator=(const RdaAdaptationData&) = delete; RdaAdaptationData(RdaAdaptationData&&) noexcept; @@ -121,6 +121,8 @@ public: uint32_t slonmin() const; char slatdir() const; char slondir() const; + double dig_rcvr_clock_freq() const; + double coho_freq() const; float az_correction_factor() const; float el_correction_factor() const; std::string site_name() const; @@ -147,6 +149,11 @@ public: float el_off_neutral_drive() const; float az_intertia() const; float el_inertia() const; + float az_stow_angle() const; + float el_stow_angle() const; + float az_encoder_alignment() const; + float el_encoder_alignment() const; + std::string refined_park() const; uint32_t rvp8nv_iwaveguide_length() const; float v_rnscale(unsigned i) const; float vel_data_tover() const; @@ -166,8 +173,8 @@ public: bool gen_exercise() const; float v_noise_tolerance() const; float min_v_dyn_range() const; - float zdr_bias_dgrad_lim() const; - float baseline_zdr_bias() const; + float zdr_offset_dgrad_lim() const; + float baseline_zdr_offset() const; float v_noise_long() const; float v_noise_short() const; float zdr_data_tover() const; diff --git a/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp index 7dd51edf..a4e7702a 100644 --- a/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp @@ -17,7 +17,7 @@ public: explicit RdaStatusData(); ~RdaStatusData(); - RdaStatusData(const RdaStatusData&) = delete; + RdaStatusData(const RdaStatusData&) = delete; RdaStatusData& operator=(const RdaStatusData&) = delete; RdaStatusData(RdaStatusData&&) noexcept; @@ -36,7 +36,7 @@ public: uint16_t operational_mode() const; uint16_t super_resolution_status() const; uint16_t clutter_mitigation_decision_status() const; - uint16_t avset_ebc_rda_log_data_status() const; + uint16_t rda_scan_and_data_flags() const; uint16_t rda_alarm_summary() const; uint16_t command_acknowledgement() const; uint16_t channel_control_status() const; @@ -51,6 +51,7 @@ public: uint16_t performance_check_status() const; uint16_t alarm_codes(unsigned i) const; uint16_t signal_processing_options() const; + uint16_t downloaded_pattern_number() const; uint16_t status_version() const; bool Parse(std::istream& is); diff --git a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp index 54c0ba2c..79705a70 100644 --- a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp +++ b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp @@ -247,6 +247,7 @@ public: float initialSystemDifferentialPhase_ {0.0f}; std::uint16_t volumeCoveragePatternNumber_ {0}; std::uint16_t processingStatus_ {0}; + std::uint16_t zdrBiasEstimateWeightedMean_ {0}; }; DigitalRadarDataGeneric::VolumeDataBlock::VolumeDataBlock( @@ -332,6 +333,18 @@ bool DigitalRadarDataGeneric::VolumeDataBlock::Parse(std::istream& is) p->volumeCoveragePatternNumber_ = ntohs(p->volumeCoveragePatternNumber_); p->processingStatus_ = ntohs(p->processingStatus_); + if (p->lrtup_ >= 46) + { + is.read(reinterpret_cast(&p->zdrBiasEstimateWeightedMean_), + 2); // 44-45 + p->zdrBiasEstimateWeightedMean_ = ntohs(p->zdrBiasEstimateWeightedMean_); + } + + if (p->lrtup_ >= 52) + { + is.seekg(6, std::ios_base::cur); // 46-51 + } + return dataBlockValid; } diff --git a/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp b/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp index 662ab506..fcdd19c8 100644 --- a/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp @@ -17,536 +17,277 @@ static const auto logger_ = util::Logger::Create(logPrefix_); class PerformanceMaintenanceDataImpl { public: - explicit PerformanceMaintenanceDataImpl() : - loopBackSetStatus_ {0}, - t1OutputFrames_ {0}, - t1InputFrames_ {0}, - routerMemoryUsed_ {0}, - routerMemoryFree_ {0}, - routerMemoryUtilization_ {0}, - routeToRpg_ {0}, - csuLossOfSignal_ {0}, - csuLossOfFrames_ {0}, - csuYellowAlarms_ {0}, - csuBlueAlarms_ {0}, - csu24HrErroredSeconds_ {0}, - csu24HrSeverelyErroredSeconds_ {0}, - csu24HrSeverelyErroredFramingSeconds_ {0}, - csu24HrUnavailableSeconds_ {0}, - csu24HrControlledSlipSeconds_ {0}, - csu24HrPathCodingViolations_ {0}, - csu24HrLineErroredSeconds_ {0}, - csu24HrBurstyErroredSeconds_ {0}, - csu24HrDegradedMinutes_ {0}, - lanSwitchCpuUtilization_ {0}, - lanSwitchMemoryUtilization_ {0}, - ifdrChasisTemperature_ {0}, - ifdrFpgaTemperature_ {0}, - gpsSatellites_ {0}, - ipcStatus_ {0}, - commandedChannelControl_ {0}, - polarization_ {0}, - ameInternalTemperature_ {0.0f}, - ameReceiverModuleTemperature_ {0.0f}, - ameBiteCalModuleTemperature_ {0.0f}, - amePeltierPulseWidthModulation_ {0}, - amePeltierStatus_ {0}, - ameADConverterStatus_ {0}, - ameState_ {0}, - ame3_3VPsVoltage_ {0.0f}, - ame5VPsVoltage_ {0.0f}, - ame6_5VPsVoltage_ {0.0f}, - ame15VPsVoltage_ {0.0f}, - ame48VPsVoltage_ {0.0f}, - ameStaloPower_ {0.0f}, - peltierCurrent_ {0.0f}, - adcCalibrationReferenceVoltage_ {0.0f}, - ameMode_ {0}, - amePeltierMode_ {0}, - amePeltierInsideFanCurrent_ {0.0f}, - amePeltierOutsideFanCurrent_ {0.0f}, - horizontalTrLimiterVoltage_ {0.0f}, - verticalTrLimiterVoltage_ {0.0f}, - adcCalibrationOffsetVoltage_ {0.0f}, - adcCalibrationGainCorrection_ {0.0f}, - rcpStatus_ {0}, - rcpString_ {}, - spipPowerButtons_ {0}, - masterPowerAdministratorLoad_ {0.0f}, - expansionPowerAdministratorLoad_ {0.0f}, - _5VdcPs_ {0}, - _15VdcPs_ {0}, - _28VdcPs_ {0}, - neg15VdcPs_ {0}, - _45VdcPs_ {0}, - filamentPsVoltage_ {0}, - vacuumPumpPsVoltage_ {0}, - focusCoilPsVoltage_ {0}, - filamentPs_ {0}, - klystronWarmup_ {0}, - transmitterAvailable_ {0}, - wgSwitchPosition_ {0}, - wgPfnTransferInterlock_ {0}, - maintenanceMode_ {0}, - maintenanceRequired_ {0}, - pfnSwitchPosition_ {0}, - modulatorOverload_ {0}, - modulatorInvCurrent_ {0}, - modulatorSwitchFail_ {0}, - mainPowerVoltage_ {0}, - chargingSystemFail_ {0}, - inverseDiodeCurrent_ {0}, - triggerAmplifier_ {0}, - circulatorTemperature_ {0}, - spectrumFilterPressure_ {0}, - wgArcVswr_ {0}, - cabinetInterlock_ {0}, - cabinetAirTemperature_ {0}, - cabinetAirflow_ {0}, - klystronCurrent_ {0}, - klystronFilamentCurrent_ {0}, - klystronVacionCurrent_ {0}, - klystronAirTemperature_ {0}, - klystronAirflow_ {0}, - modulatorSwitchMaintenance_ {0}, - postChargeRegulatorMaintenance_ {0}, - wgPressureHumidity_ {0}, - transmitterOvervoltage_ {0}, - transmitterOvercurrent_ {0}, - focusCoilCurrent_ {0}, - focusCoilAirflow_ {0}, - oilTemperature_ {0}, - prfLimit_ {0}, - transmitterOilLevel_ {0}, - transmitterBatteryCharging_ {0}, - highVoltageStatus_ {0}, - transmitterRecyclingSummary_ {0}, - transmitterInoperable_ {0}, - transmitterAirFilter_ {0}, - zeroTestBit_ {0}, - oneTestBit_ {0}, - xmtrSpipInterface_ {0}, - transmitterSummaryStatus_ {0}, - transmitterRfPower_ {0.0f}, - horizontalXmtrPeakPower_ {0.0f}, - xmtrPeakPower_ {0.0f}, - verticalXmtrPeakPower_ {0.0f}, - xmtrRfAvgPower_ {0.0f}, - xmtrRecycleCount_ {0}, - receiverBias_ {0.0f}, - transmitImbalance_ {0.0f}, - xmtrPowerMeterZero_ {0.0f}, - acUnit1CompressorShutOff_ {0}, - acUnit2CompressorShutOff_ {0}, - generatorMaintenanceRequired_ {0}, - generatorBatteryVoltage_ {0}, - generatorEngine_ {0}, - generatorVoltFrequency_ {0}, - powerSource_ {0}, - transitionalPowerSource_ {0}, - generatorAutoRunOffSwitch_ {0}, - aircraftHazardLighting_ {0}, - equipmentShelterFireDetectionSystem_ {0}, - equipmentShelterFireSmoke_ {0}, - generatorShelterFireSmoke_ {0}, - utilityVoltageFrequency_ {0}, - siteSecurityAlarm_ {0}, - securityEquipment_ {0}, - securitySystem_ {0}, - receiverConnectedToAntenna_ {0}, - radomeHatch_ {0}, - acUnit1FilterDirty_ {0}, - acUnit2FilterDirty_ {0}, - equipmentShelterTemperature_ {0.0f}, - outsideAmbientTemperature_ {0.0f}, - transmitterLeavingAirTemp_ {0.0f}, - acUnit1DischargeAirTemp_ {0.0f}, - generatorShelterTemperature_ {0.0f}, - radomeAirTemperature_ {0.0f}, - acUnit2DischargeAirTemp_ {0.0f}, - spip15VPs_ {0.0f}, - spipNeg15VPs_ {0.0f}, - spip28VPsStatus_ {0}, - spip5VPs_ {0.0f}, - convertedGeneratorFuelLevel_ {0}, - elevationPosDeadLimit_ {0}, - _150VOvervoltage_ {0}, - _150VUndervoltage_ {0}, - elevationServoAmpInhibit_ {0}, - elevationServoAmpShortCircuit_ {0}, - elevationServoAmpOvertemp_ {0}, - elevationMotorOvertemp_ {0}, - elevationStowPin_ {0}, - elevationHousing5VPs_ {0}, - elevationNegDeadLimit_ {0}, - elevationPosNormalLimit_ {0}, - elevationNegNormalLimit_ {0}, - elevationEncoderLight_ {0}, - elevationGearboxOil_ {0}, - elevationHandwheel_ {0}, - elevationAmpPs_ {0}, - azimuthServoAmpInhibit_ {0}, - azimuthServoAmpShortCircuit_ {0}, - azimuthServoAmpOvertemp_ {0}, - azimuthMotorOvertemp_ {0}, - azimuthStowPin_ {0}, - azimuthHousing5VPs_ {0}, - azimuthEncoderLight_ {0}, - azimuthGearboxOil_ {0}, - azimuthBullGearOil_ {0}, - azimuthHandwheel_ {0}, - azimuthServoAmpPs_ {0}, - servo_ {0}, - pedestalInterlockSwitch_ {0}, - cohoClock_ {0}, - rfGeneratorFrequencySelectOscillator_ {0}, - rfGeneratorRfStalo_ {0}, - rfGeneratorPhaseShiftedCoho_ {0}, - _9VReceiverPs_ {0}, - _5VReceiverPs_ {0}, - _18VReceiverPs_ {0}, - neg9VReceiverPs_ {0}, - _5VSingleChannelRdaiuPs_ {0}, - horizontalShortPulseNoise_ {0.0f}, - horizontalLongPulseNoise_ {0.0f}, - horizontalNoiseTemperature_ {0.0f}, - verticalShortPulseNoise_ {0.0f}, - verticalLongPulseNoise_ {0.0f}, - verticalNoiseTemperature_ {0.0f}, - horizontalLinearity_ {0.0f}, - horizontalDynamicRange_ {0.0f}, - horizontalDeltaDbz0_ {0.0f}, - verticalDeltaDbz0_ {0.0f}, - kdPeakMeasured_ {0.0f}, - shortPulseHorizontalDbz0_ {0.0f}, - longPulseHorizontalDbz0_ {0.0f}, - velocityProcessed_ {0}, - widthProcessed_ {0}, - velocityRfGen_ {0}, - widthRfGen_ {0}, - horizontalI0_ {0.0f}, - verticalI0_ {0.0f}, - verticalDynamicRange_ {0.0f}, - shortPulseVerticalDbz0_ {0.0f}, - longPulseVerticalDbz0_ {0.0f}, - horizontalPowerSense_ {0.0f}, - verticalPowerSense_ {0.0f}, - zdrBias_ {0.0f}, - clutterSuppressionDelta_ {0.0f}, - clutterSuppressionUnfilteredPower_ {0.0f}, - clutterSuppressionFilteredPower_ {0.0f}, - verticalLinearity_ {0.0f}, - stateFileReadStatus_ {0}, - stateFileWriteStatus_ {0}, - bypassMapFileReadStatus_ {0}, - bypassMapFileWriteStatus_ {0}, - currentAdaptationFileReadStatus_ {0}, - currentAdaptationFileWriteStatus_ {0}, - censorZoneFileReadStatus_ {0}, - censorZoneFileWriteStatus_ {0}, - remoteVcpFileReadStatus_ {0}, - remoteVcpFileWriteStatus_ {0}, - baselineAdaptationFileReadStatus_ {0}, - readStatusOfPrfSets_ {0}, - clutterFilterMapFileReadStatus_ {0}, - clutterFilterMapFileWriteStatus_ {0}, - generatlDiskIoError_ {0}, - rspStatus_ {0}, - motherboardTemperature_ {0}, - cpu1Temperature_ {0}, - cpu2Temperature_ {0}, - cpu1FanSpeed_ {0}, - cpu2FanSpeed_ {0}, - rspFan1Speed_ {0}, - rspFan2Speed_ {0}, - rspFan3Speed_ {0}, - spipCommStatus_ {0}, - hciCommStatus_ {0}, - signalProcessorCommandStatus_ {0}, - ameCommunicationStatus_ {0}, - rmsLinkStatus_ {0}, - rpgLinkStatus_ {0}, - interpanelLinkStatus_ {0}, - performanceCheckTime_ {0}, - version_ {0} - { - } - ~PerformanceMaintenanceDataImpl() = default; + explicit PerformanceMaintenanceDataImpl() = default; + ~PerformanceMaintenanceDataImpl() = default; // Communications - uint16_t loopBackSetStatus_; - uint32_t t1OutputFrames_; - uint32_t t1InputFrames_; - uint32_t routerMemoryUsed_; - uint32_t routerMemoryFree_; - uint16_t routerMemoryUtilization_; - uint16_t routeToRpg_; - uint32_t csuLossOfSignal_; - uint32_t csuLossOfFrames_; - uint32_t csuYellowAlarms_; - uint32_t csuBlueAlarms_; - uint32_t csu24HrErroredSeconds_; - uint32_t csu24HrSeverelyErroredSeconds_; - uint32_t csu24HrSeverelyErroredFramingSeconds_; - uint32_t csu24HrUnavailableSeconds_; - uint32_t csu24HrControlledSlipSeconds_; - uint32_t csu24HrPathCodingViolations_; - uint32_t csu24HrLineErroredSeconds_; - uint32_t csu24HrBurstyErroredSeconds_; - uint32_t csu24HrDegradedMinutes_; - uint32_t lanSwitchCpuUtilization_; - uint16_t lanSwitchMemoryUtilization_; - uint16_t ifdrChasisTemperature_; - uint16_t ifdrFpgaTemperature_; - int32_t gpsSatellites_; - uint16_t ipcStatus_; - uint16_t commandedChannelControl_; + uint16_t loopBackSetStatus_ {0}; + uint32_t t1OutputFrames_ {0}; + uint32_t t1InputFrames_ {0}; + uint32_t routerMemoryUsed_ {0}; + uint32_t routerMemoryFree_ {0}; + uint16_t routerMemoryUtilization_ {0}; + uint16_t routeToRpg_ {0}; + uint16_t t1PortStatus_ {0}; + uint16_t routerDedicatedEthernetPortStatus_ {0}; + uint16_t routerCommercialEthernetPortStatus_ {0}; + uint32_t csu24HrErroredSeconds_ {0}; + uint32_t csu24HrSeverelyErroredSeconds_ {0}; + uint32_t csu24HrSeverelyErroredFramingSeconds_ {0}; + uint32_t csu24HrUnavailableSeconds_ {0}; + uint32_t csu24HrControlledSlipSeconds_ {0}; + uint32_t csu24HrPathCodingViolations_ {0}; + uint32_t csu24HrLineErroredSeconds_ {0}; + uint32_t csu24HrBurstyErroredSeconds_ {0}; + uint32_t csu24HrDegradedMinutes_ {0}; + uint32_t lanSwitchCpuUtilization_ {0}; + uint16_t lanSwitchMemoryUtilization_ {0}; + uint16_t ifdrChasisTemperature_ {0}; + uint16_t ifdrFpgaTemperature_ {0}; + uint16_t ntpStatus_ {0}; + uint16_t ipcStatus_ {0}; + uint16_t commandedChannelControl_ {0}; // AME - uint16_t polarization_; - float ameInternalTemperature_; - float ameReceiverModuleTemperature_; - float ameBiteCalModuleTemperature_; - uint16_t amePeltierPulseWidthModulation_; - uint16_t amePeltierStatus_; - uint16_t ameADConverterStatus_; - uint16_t ameState_; - float ame3_3VPsVoltage_; - float ame5VPsVoltage_; - float ame6_5VPsVoltage_; - float ame15VPsVoltage_; - float ame48VPsVoltage_; - float ameStaloPower_; - float peltierCurrent_; - float adcCalibrationReferenceVoltage_; - uint16_t ameMode_; - uint16_t amePeltierMode_; - float amePeltierInsideFanCurrent_; - float amePeltierOutsideFanCurrent_; - float horizontalTrLimiterVoltage_; - float verticalTrLimiterVoltage_; - float adcCalibrationOffsetVoltage_; - float adcCalibrationGainCorrection_; + uint16_t polarization_ {0}; + float ameInternalTemperature_ {0.0f}; + float ameReceiverModuleTemperature_ {0.0f}; + float ameBiteCalModuleTemperature_ {0.0f}; + uint16_t amePeltierPulseWidthModulation_ {0}; + uint16_t amePeltierStatus_ {0}; + uint16_t ameADConverterStatus_ {0}; + uint16_t ameState_ {0}; + float ame3_3VPsVoltage_ {0.0f}; + float ame5VPsVoltage_ {0.0f}; + float ame6_5VPsVoltage_ {0.0f}; + float ame15VPsVoltage_ {0.0f}; + float ame48VPsVoltage_ {0.0f}; + float ameStaloPower_ {0.0f}; + float peltierCurrent_ {0.0f}; + float adcCalibrationReferenceVoltage_ {0.0f}; + uint16_t ameMode_ {0}; + uint16_t amePeltierMode_ {0}; + float amePeltierInsideFanCurrent_ {0.0f}; + float amePeltierOutsideFanCurrent_ {0.0f}; + float horizontalTrLimiterVoltage_ {0.0f}; + float verticalTrLimiterVoltage_ {0.0f}; + float adcCalibrationOffsetVoltage_ {0.0f}; + float adcCalibrationGainCorrection_ {0.0f}; // RCP/SPIP Power Button Status - uint16_t rcpStatus_; - std::string rcpString_; - uint16_t spipPowerButtons_; + uint16_t rcpStatus_ {0}; + std::string rcpString_ {}; + uint16_t spipPowerButtons_ {0}; // Power - float masterPowerAdministratorLoad_; - float expansionPowerAdministratorLoad_; + float masterPowerAdministratorLoad_ {0.0f}; + float expansionPowerAdministratorLoad_ {0.0f}; // Transmitter - uint16_t _5VdcPs_; - uint16_t _15VdcPs_; - uint16_t _28VdcPs_; - uint16_t neg15VdcPs_; - uint16_t _45VdcPs_; - uint16_t filamentPsVoltage_; - uint16_t vacuumPumpPsVoltage_; - uint16_t focusCoilPsVoltage_; - uint16_t filamentPs_; - uint16_t klystronWarmup_; - uint16_t transmitterAvailable_; - uint16_t wgSwitchPosition_; - uint16_t wgPfnTransferInterlock_; - uint16_t maintenanceMode_; - uint16_t maintenanceRequired_; - uint16_t pfnSwitchPosition_; - uint16_t modulatorOverload_; - uint16_t modulatorInvCurrent_; - uint16_t modulatorSwitchFail_; - uint16_t mainPowerVoltage_; - uint16_t chargingSystemFail_; - uint16_t inverseDiodeCurrent_; - uint16_t triggerAmplifier_; - uint16_t circulatorTemperature_; - uint16_t spectrumFilterPressure_; - uint16_t wgArcVswr_; - uint16_t cabinetInterlock_; - uint16_t cabinetAirTemperature_; - uint16_t cabinetAirflow_; - uint16_t klystronCurrent_; - uint16_t klystronFilamentCurrent_; - uint16_t klystronVacionCurrent_; - uint16_t klystronAirTemperature_; - uint16_t klystronAirflow_; - uint16_t modulatorSwitchMaintenance_; - uint16_t postChargeRegulatorMaintenance_; - uint16_t wgPressureHumidity_; - uint16_t transmitterOvervoltage_; - uint16_t transmitterOvercurrent_; - uint16_t focusCoilCurrent_; - uint16_t focusCoilAirflow_; - uint16_t oilTemperature_; - uint16_t prfLimit_; - uint16_t transmitterOilLevel_; - uint16_t transmitterBatteryCharging_; - uint16_t highVoltageStatus_; - uint16_t transmitterRecyclingSummary_; - uint16_t transmitterInoperable_; - uint16_t transmitterAirFilter_; - std::array zeroTestBit_; - std::array oneTestBit_; - uint16_t xmtrSpipInterface_; - uint16_t transmitterSummaryStatus_; - float transmitterRfPower_; - float horizontalXmtrPeakPower_; - float xmtrPeakPower_; - float verticalXmtrPeakPower_; - float xmtrRfAvgPower_; - uint32_t xmtrRecycleCount_; - float receiverBias_; - float transmitImbalance_; - float xmtrPowerMeterZero_; + uint16_t _5VdcPs_ {0}; + uint16_t _15VdcPs_ {0}; + uint16_t _28VdcPs_ {0}; + uint16_t neg15VdcPs_ {0}; + uint16_t _45VdcPs_ {0}; + uint16_t filamentPsVoltage_ {0}; + uint16_t vacuumPumpPsVoltage_ {0}; + uint16_t focusCoilPsVoltage_ {0}; + uint16_t filamentPs_ {0}; + uint16_t klystronWarmup_ {0}; + uint16_t transmitterAvailable_ {0}; + uint16_t wgSwitchPosition_ {0}; + uint16_t wgPfnTransferInterlock_ {0}; + uint16_t maintenanceMode_ {0}; + uint16_t maintenanceRequired_ {0}; + uint16_t pfnSwitchPosition_ {0}; + uint16_t modulatorOverload_ {0}; + uint16_t modulatorInvCurrent_ {0}; + uint16_t modulatorSwitchFail_ {0}; + uint16_t mainPowerVoltage_ {0}; + uint16_t chargingSystemFail_ {0}; + uint16_t inverseDiodeCurrent_ {0}; + uint16_t triggerAmplifier_ {0}; + uint16_t circulatorTemperature_ {0}; + uint16_t spectrumFilterPressure_ {0}; + uint16_t wgArcVswr_ {0}; + uint16_t cabinetInterlock_ {0}; + uint16_t cabinetAirTemperature_ {0}; + uint16_t cabinetAirflow_ {0}; + uint16_t klystronCurrent_ {0}; + uint16_t klystronFilamentCurrent_ {0}; + uint16_t klystronVacionCurrent_ {0}; + uint16_t klystronAirTemperature_ {0}; + uint16_t klystronAirflow_ {0}; + uint16_t modulatorSwitchMaintenance_ {0}; + uint16_t postChargeRegulatorMaintenance_ {0}; + uint16_t wgPressureHumidity_ {0}; + uint16_t transmitterOvervoltage_ {0}; + uint16_t transmitterOvercurrent_ {0}; + uint16_t focusCoilCurrent_ {0}; + uint16_t focusCoilAirflow_ {0}; + uint16_t oilTemperature_ {0}; + uint16_t prfLimit_ {0}; + uint16_t transmitterOilLevel_ {0}; + uint16_t transmitterBatteryCharging_ {0}; + uint16_t highVoltageStatus_ {0}; + uint16_t transmitterRecyclingSummary_ {0}; + uint16_t transmitterInoperable_ {0}; + uint16_t transmitterAirFilter_ {0}; + std::array zeroTestBit_ {0}; + std::array oneTestBit_ {0}; + uint16_t xmtrSpipInterface_ {0}; + uint16_t transmitterSummaryStatus_ {0}; + float transmitterRfPower_ {0.0f}; + float horizontalXmtrPeakPower_ {0.0f}; + float xmtrPeakPower_ {0.0f}; + float verticalXmtrPeakPower_ {0.0f}; + float xmtrRfAvgPower_ {0.0f}; + uint32_t xmtrRecycleCount_ {0}; + float receiverBias_ {0.0f}; + float transmitImbalance_ {0.0f}; + float xmtrPowerMeterZero_ {0.0f}; // Tower/Utilities - uint16_t acUnit1CompressorShutOff_; - uint16_t acUnit2CompressorShutOff_; - uint16_t generatorMaintenanceRequired_; - uint16_t generatorBatteryVoltage_; - uint16_t generatorEngine_; - uint16_t generatorVoltFrequency_; - uint16_t powerSource_; - uint16_t transitionalPowerSource_; - uint16_t generatorAutoRunOffSwitch_; - uint16_t aircraftHazardLighting_; + uint16_t acUnit1CompressorShutOff_ {0}; + uint16_t acUnit2CompressorShutOff_ {0}; + uint16_t generatorMaintenanceRequired_ {0}; + uint16_t generatorBatteryVoltage_ {0}; + uint16_t generatorEngine_ {0}; + uint16_t generatorVoltFrequency_ {0}; + uint16_t powerSource_ {0}; + uint16_t transitionalPowerSource_ {0}; + uint16_t generatorAutoRunOffSwitch_ {0}; + uint16_t aircraftHazardLighting_ {0}; // Equipment Shelter - uint16_t equipmentShelterFireDetectionSystem_; - uint16_t equipmentShelterFireSmoke_; - uint16_t generatorShelterFireSmoke_; - uint16_t utilityVoltageFrequency_; - uint16_t siteSecurityAlarm_; - uint16_t securityEquipment_; - uint16_t securitySystem_; - uint16_t receiverConnectedToAntenna_; - uint16_t radomeHatch_; - uint16_t acUnit1FilterDirty_; - uint16_t acUnit2FilterDirty_; - float equipmentShelterTemperature_; - float outsideAmbientTemperature_; - float transmitterLeavingAirTemp_; - float acUnit1DischargeAirTemp_; - float generatorShelterTemperature_; - float radomeAirTemperature_; - float acUnit2DischargeAirTemp_; - float spip15VPs_; - float spipNeg15VPs_; - uint16_t spip28VPsStatus_; - float spip5VPs_; - uint16_t convertedGeneratorFuelLevel_; + uint16_t equipmentShelterFireDetectionSystem_ {0}; + uint16_t equipmentShelterFireSmoke_ {0}; + uint16_t generatorShelterFireSmoke_ {0}; + uint16_t utilityVoltageFrequency_ {0}; + uint16_t siteSecurityAlarm_ {0}; + uint16_t securityEquipment_ {0}; + uint16_t securitySystem_ {0}; + uint16_t receiverConnectedToAntenna_ {0}; + uint16_t radomeHatch_ {0}; + uint16_t acUnit1FilterDirty_ {0}; + uint16_t acUnit2FilterDirty_ {0}; + float equipmentShelterTemperature_ {0.0f}; + float outsideAmbientTemperature_ {0.0f}; + float transmitterLeavingAirTemp_ {0.0f}; + float acUnit1DischargeAirTemp_ {0.0f}; + float generatorShelterTemperature_ {0.0f}; + float radomeAirTemperature_ {0.0f}; + float acUnit2DischargeAirTemp_ {0.0f}; + float spip15VPs_ {0.0f}; + float spipNeg15VPs_ {0.0f}; + uint16_t spip28VPsStatus_ {0}; + float spip5VPs_ {0.0f}; + uint16_t convertedGeneratorFuelLevel_ {0}; // Antenna/Pedestal - uint16_t elevationPosDeadLimit_; - uint16_t _150VOvervoltage_; - uint16_t _150VUndervoltage_; - uint16_t elevationServoAmpInhibit_; - uint16_t elevationServoAmpShortCircuit_; - uint16_t elevationServoAmpOvertemp_; - uint16_t elevationMotorOvertemp_; - uint16_t elevationStowPin_; - uint16_t elevationHousing5VPs_; - uint16_t elevationNegDeadLimit_; - uint16_t elevationPosNormalLimit_; - uint16_t elevationNegNormalLimit_; - uint16_t elevationEncoderLight_; - uint16_t elevationGearboxOil_; - uint16_t elevationHandwheel_; - uint16_t elevationAmpPs_; - uint16_t azimuthServoAmpInhibit_; - uint16_t azimuthServoAmpShortCircuit_; - uint16_t azimuthServoAmpOvertemp_; - uint16_t azimuthMotorOvertemp_; - uint16_t azimuthStowPin_; - uint16_t azimuthHousing5VPs_; - uint16_t azimuthEncoderLight_; - uint16_t azimuthGearboxOil_; - uint16_t azimuthBullGearOil_; - uint16_t azimuthHandwheel_; - uint16_t azimuthServoAmpPs_; - uint16_t servo_; - uint16_t pedestalInterlockSwitch_; + uint16_t elevationPosDeadLimit_ {0}; + uint16_t _150VOvervoltage_ {0}; + uint16_t _150VUndervoltage_ {0}; + uint16_t elevationServoAmpInhibit_ {0}; + uint16_t elevationServoAmpShortCircuit_ {0}; + uint16_t elevationServoAmpOvertemp_ {0}; + uint16_t elevationMotorOvertemp_ {0}; + uint16_t elevationStowPin_ {0}; + uint16_t elevationHousing5VPs_ {0}; + uint16_t elevationNegDeadLimit_ {0}; + uint16_t elevationPosNormalLimit_ {0}; + uint16_t elevationNegNormalLimit_ {0}; + uint16_t elevationEncoderLight_ {0}; + uint16_t elevationGearboxOil_ {0}; + uint16_t elevationHandwheel_ {0}; + uint16_t elevationAmpPs_ {0}; + uint16_t azimuthServoAmpInhibit_ {0}; + uint16_t azimuthServoAmpShortCircuit_ {0}; + uint16_t azimuthServoAmpOvertemp_ {0}; + uint16_t azimuthMotorOvertemp_ {0}; + uint16_t azimuthStowPin_ {0}; + uint16_t azimuthHousing5VPs_ {0}; + uint16_t azimuthEncoderLight_ {0}; + uint16_t azimuthGearboxOil_ {0}; + uint16_t azimuthBullGearOil_ {0}; + uint16_t azimuthHandwheel_ {0}; + uint16_t azimuthServoAmpPs_ {0}; + uint16_t servo_ {0}; + uint16_t pedestalInterlockSwitch_ {0}; // RF Generator/Receiver - uint16_t cohoClock_; - uint16_t rfGeneratorFrequencySelectOscillator_; - uint16_t rfGeneratorRfStalo_; - uint16_t rfGeneratorPhaseShiftedCoho_; - uint16_t _9VReceiverPs_; - uint16_t _5VReceiverPs_; - uint16_t _18VReceiverPs_; - uint16_t neg9VReceiverPs_; - uint16_t _5VSingleChannelRdaiuPs_; - float horizontalShortPulseNoise_; - float horizontalLongPulseNoise_; - float horizontalNoiseTemperature_; - float verticalShortPulseNoise_; - float verticalLongPulseNoise_; - float verticalNoiseTemperature_; + uint16_t cohoClock_ {0}; + uint16_t rfGeneratorFrequencySelectOscillator_ {0}; + uint16_t rfGeneratorRfStalo_ {0}; + uint16_t rfGeneratorPhaseShiftedCoho_ {0}; + uint16_t _9VReceiverPs_ {0}; + uint16_t _5VReceiverPs_ {0}; + uint16_t _18VReceiverPs_ {0}; + uint16_t neg9VReceiverPs_ {0}; + uint16_t _5VSingleChannelRdaiuPs_ {0}; + float horizontalShortPulseNoise_ {0.0f}; + float horizontalLongPulseNoise_ {0.0f}; + float horizontalNoiseTemperature_ {0.0f}; + float verticalShortPulseNoise_ {0.0f}; + float verticalLongPulseNoise_ {0.0f}; + float verticalNoiseTemperature_ {0.0f}; // Calibration - float horizontalLinearity_; - float horizontalDynamicRange_; - float horizontalDeltaDbz0_; - float verticalDeltaDbz0_; - float kdPeakMeasured_; - float shortPulseHorizontalDbz0_; - float longPulseHorizontalDbz0_; - uint16_t velocityProcessed_; - uint16_t widthProcessed_; - uint16_t velocityRfGen_; - uint16_t widthRfGen_; - float horizontalI0_; - float verticalI0_; - float verticalDynamicRange_; - float shortPulseVerticalDbz0_; - float longPulseVerticalDbz0_; - float horizontalPowerSense_; - float verticalPowerSense_; - float zdrBias_; - float clutterSuppressionDelta_; - float clutterSuppressionUnfilteredPower_; - float clutterSuppressionFilteredPower_; - float verticalLinearity_; + float horizontalLinearity_ {0.0f}; + float horizontalDynamicRange_ {0.0f}; + float horizontalDeltaDbz0_ {0.0f}; + float verticalDeltaDbz0_ {0.0f}; + float kdPeakMeasured_ {0.0f}; + float shortPulseHorizontalDbz0_ {0.0f}; + float longPulseHorizontalDbz0_ {0.0f}; + uint16_t velocityProcessed_ {0}; + uint16_t widthProcessed_ {0}; + uint16_t velocityRfGen_ {0}; + uint16_t widthRfGen_ {0}; + float horizontalI0_ {0.0f}; + float verticalI0_ {0.0f}; + float verticalDynamicRange_ {0.0f}; + float shortPulseVerticalDbz0_ {0.0f}; + float longPulseVerticalDbz0_ {0.0f}; + float horizontalPowerSense_ {0.0f}; + float verticalPowerSense_ {0.0f}; + float zdrOffset_ {0.0f}; + float clutterSuppressionDelta_ {0.0f}; + float clutterSuppressionUnfilteredPower_ {0.0f}; + float clutterSuppressionFilteredPower_ {0.0f}; + float verticalLinearity_ {0.0f}; // File Status - uint16_t stateFileReadStatus_; - uint16_t stateFileWriteStatus_; - uint16_t bypassMapFileReadStatus_; - uint16_t bypassMapFileWriteStatus_; - uint16_t currentAdaptationFileReadStatus_; - uint16_t currentAdaptationFileWriteStatus_; - uint16_t censorZoneFileReadStatus_; - uint16_t censorZoneFileWriteStatus_; - uint16_t remoteVcpFileReadStatus_; - uint16_t remoteVcpFileWriteStatus_; - uint16_t baselineAdaptationFileReadStatus_; - uint16_t readStatusOfPrfSets_; - uint16_t clutterFilterMapFileReadStatus_; - uint16_t clutterFilterMapFileWriteStatus_; - uint16_t generatlDiskIoError_; - uint8_t rspStatus_; - uint8_t motherboardTemperature_; - uint8_t cpu1Temperature_; - uint8_t cpu2Temperature_; - uint16_t cpu1FanSpeed_; - uint16_t cpu2FanSpeed_; - uint16_t rspFan1Speed_; - uint16_t rspFan2Speed_; - uint16_t rspFan3Speed_; + uint16_t stateFileReadStatus_ {0}; + uint16_t stateFileWriteStatus_ {0}; + uint16_t bypassMapFileReadStatus_ {0}; + uint16_t bypassMapFileWriteStatus_ {0}; + uint16_t currentAdaptationFileReadStatus_ {0}; + uint16_t currentAdaptationFileWriteStatus_ {0}; + uint16_t censorZoneFileReadStatus_ {0}; + uint16_t censorZoneFileWriteStatus_ {0}; + uint16_t remoteVcpFileReadStatus_ {0}; + uint16_t remoteVcpFileWriteStatus_ {0}; + uint16_t baselineAdaptationFileReadStatus_ {0}; + uint16_t readStatusOfPrfSets_ {0}; + uint16_t clutterFilterMapFileReadStatus_ {0}; + uint16_t clutterFilterMapFileWriteStatus_ {0}; + uint16_t generalDiskIoError_ {0}; + uint8_t rspStatus_ {0}; + uint8_t cpu1Temperature_ {0}; + uint8_t cpu2Temperature_ {0}; + uint16_t rspMotherboardPower_ {0}; // Device Status - uint16_t spipCommStatus_; - uint16_t hciCommStatus_; - uint16_t signalProcessorCommandStatus_; - uint16_t ameCommunicationStatus_; - uint16_t rmsLinkStatus_; - uint16_t rpgLinkStatus_; - uint16_t interpanelLinkStatus_; - uint32_t performanceCheckTime_; - uint16_t version_; + uint16_t spipCommStatus_ {0}; + uint16_t hciCommStatus_ {0}; + uint16_t signalProcessorCommandStatus_ {0}; + uint16_t ameCommunicationStatus_ {0}; + uint16_t rmsLinkStatus_ {0}; + uint16_t rpgLinkStatus_ {0}; + uint16_t interpanelLinkStatus_ {0}; + uint32_t performanceCheckTime_ {0}; + uint16_t version_ {0}; }; PerformanceMaintenanceData::PerformanceMaintenanceData() : @@ -556,7 +297,7 @@ PerformanceMaintenanceData::PerformanceMaintenanceData() : PerformanceMaintenanceData::~PerformanceMaintenanceData() = default; PerformanceMaintenanceData::PerformanceMaintenanceData( - PerformanceMaintenanceData&&) noexcept = default; + PerformanceMaintenanceData&&) noexcept = default; PerformanceMaintenanceData& PerformanceMaintenanceData::operator=( PerformanceMaintenanceData&&) noexcept = default; @@ -595,24 +336,21 @@ uint16_t PerformanceMaintenanceData::route_to_rpg() const return p->routeToRpg_; } -uint32_t PerformanceMaintenanceData::csu_loss_of_signal() const +uint16_t PerformanceMaintenanceData::t1_port_status() const { - return p->csuLossOfSignal_; + return p->t1PortStatus_; } -uint32_t PerformanceMaintenanceData::csu_loss_of_frames() const +uint16_t +PerformanceMaintenanceData::router_dedicated_ethernet_port_status() const { - return p->csuLossOfFrames_; + return p->routerDedicatedEthernetPortStatus_; } -uint32_t PerformanceMaintenanceData::csu_yellow_alarms() const +uint16_t +PerformanceMaintenanceData::router_commercial_ethernet_port_status() const { - return p->csuYellowAlarms_; -} - -uint32_t PerformanceMaintenanceData::csu_blue_alarms() const -{ - return p->csuBlueAlarms_; + return p->routerCommercialEthernetPortStatus_; } uint32_t PerformanceMaintenanceData::csu_24hr_errored_seconds() const @@ -681,9 +419,9 @@ uint16_t PerformanceMaintenanceData::ifdr_fpga_temperature() const return p->ifdrFpgaTemperature_; } -int32_t PerformanceMaintenanceData::gps_satellites() const +uint16_t PerformanceMaintenanceData::ntp_status() const { - return p->gpsSatellites_; + return p->ntpStatus_; } uint16_t PerformanceMaintenanceData::ipc_status() const @@ -1628,9 +1366,9 @@ float PerformanceMaintenanceData::vertical_power_sense() const return p->verticalPowerSense_; } -float PerformanceMaintenanceData::zdr_bias() const +float PerformanceMaintenanceData::zdr_offset() const { - return p->zdrBias_; + return p->zdrOffset_; } float PerformanceMaintenanceData::clutter_suppression_delta() const @@ -1726,9 +1464,9 @@ PerformanceMaintenanceData::clutter_filter_map_file_write_status() const return p->clutterFilterMapFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::generatl_disk_io_error() const +uint16_t PerformanceMaintenanceData::general_disk_io_error() const { - return p->generatlDiskIoError_; + return p->generalDiskIoError_; } uint8_t PerformanceMaintenanceData::rsp_status() const @@ -1736,11 +1474,6 @@ uint8_t PerformanceMaintenanceData::rsp_status() const return p->rspStatus_; } -uint8_t PerformanceMaintenanceData::motherboard_temperature() const -{ - return p->motherboardTemperature_; -} - uint8_t PerformanceMaintenanceData::cpu1_temperature() const { return p->cpu1Temperature_; @@ -1751,29 +1484,9 @@ uint8_t PerformanceMaintenanceData::cpu2_temperature() const return p->cpu2Temperature_; } -uint16_t PerformanceMaintenanceData::cpu1_fan_speed() const +uint16_t PerformanceMaintenanceData::rsp_motherboard_power() const { - return p->cpu1FanSpeed_; -} - -uint16_t PerformanceMaintenanceData::cpu2_fan_speed() const -{ - return p->cpu2FanSpeed_; -} - -uint16_t PerformanceMaintenanceData::rsp_fan1_speed() const -{ - return p->rspFan1Speed_; -} - -uint16_t PerformanceMaintenanceData::rsp_fan2_speed() const -{ - return p->rspFan2Speed_; -} - -uint16_t PerformanceMaintenanceData::rsp_fan3_speed() const -{ - return p->rspFan3Speed_; + return p->rspMotherboardPower_; } uint16_t PerformanceMaintenanceData::spip_comm_status() const @@ -1839,11 +1552,13 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) is.read(reinterpret_cast(&p->routerMemoryFree_), 4); // 9-10 is.read(reinterpret_cast(&p->routerMemoryUtilization_), 2); // 11 is.read(reinterpret_cast(&p->routeToRpg_), 2); // 12 - is.read(reinterpret_cast(&p->csuLossOfSignal_), 4); // 13-14 - is.read(reinterpret_cast(&p->csuLossOfFrames_), 4); // 15-16 - is.read(reinterpret_cast(&p->csuYellowAlarms_), 4); // 17-18 - is.read(reinterpret_cast(&p->csuBlueAlarms_), 4); // 19-20 - is.read(reinterpret_cast(&p->csu24HrErroredSeconds_), 4); // 21-22 + is.read(reinterpret_cast(&p->t1PortStatus_), 2); // 13 + is.read(reinterpret_cast(&p->routerDedicatedEthernetPortStatus_), + 2); // 14 + is.read(reinterpret_cast(&p->routerCommercialEthernetPortStatus_), + 2); // 15 + is.seekg(10, std::ios_base::cur); // 16-20 + is.read(reinterpret_cast(&p->csu24HrErroredSeconds_), 4); // 21-22 is.read(reinterpret_cast(&p->csu24HrSeverelyErroredSeconds_), 4); // 23-24 is.read(reinterpret_cast(&p->csu24HrSeverelyErroredFramingSeconds_), @@ -1863,9 +1578,8 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) is.seekg(2, std::ios_base::cur); // 44 is.read(reinterpret_cast(&p->ifdrChasisTemperature_), 2); // 45 is.read(reinterpret_cast(&p->ifdrFpgaTemperature_), 2); // 46 - is.seekg(4, std::ios_base::cur); // 47-48 - is.read(reinterpret_cast(&p->gpsSatellites_), 4); // 49-50 - is.seekg(4, std::ios_base::cur); // 51-52 + is.read(reinterpret_cast(&p->ntpStatus_), 2); // 47 + is.seekg(10, std::ios_base::cur); // 48-52 is.read(reinterpret_cast(&p->ipcStatus_), 2); // 53 is.read(reinterpret_cast(&p->commandedChannelControl_), 2); // 54 is.seekg(6, std::ios_base::cur); // 55-57 @@ -2112,7 +1826,7 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) is.seekg(8, std::ios_base::cur); // 393-396 is.read(reinterpret_cast(&p->horizontalPowerSense_), 4); // 397-398 is.read(reinterpret_cast(&p->verticalPowerSense_), 4); // 399-400 - is.read(reinterpret_cast(&p->zdrBias_), 4); // 401-402 + is.read(reinterpret_cast(&p->zdrOffset_), 4); // 401-402 is.seekg(12, std::ios_base::cur); // 403-408 is.read(reinterpret_cast(&p->clutterSuppressionDelta_), 4); // 409-410 is.read(reinterpret_cast(&p->clutterSuppressionUnfilteredPower_), @@ -2143,18 +1857,14 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) is.read(reinterpret_cast(&p->clutterFilterMapFileReadStatus_), 2); // 445 is.read(reinterpret_cast(&p->clutterFilterMapFileWriteStatus_), - 2); // 446 - is.read(reinterpret_cast(&p->generatlDiskIoError_), 2); // 447 - is.read(reinterpret_cast(&p->rspStatus_), 1); // 448 - is.read(reinterpret_cast(&p->motherboardTemperature_), 1); // 448 - is.read(reinterpret_cast(&p->cpu1Temperature_), 1); // 449 - is.read(reinterpret_cast(&p->cpu2Temperature_), 1); // 449 - is.read(reinterpret_cast(&p->cpu1FanSpeed_), 2); // 450 - is.read(reinterpret_cast(&p->cpu2FanSpeed_), 2); // 451 - is.read(reinterpret_cast(&p->rspFan1Speed_), 2); // 452 - is.read(reinterpret_cast(&p->rspFan2Speed_), 2); // 453 - is.read(reinterpret_cast(&p->rspFan3Speed_), 2); // 454 - is.seekg(12, std::ios_base::cur); // 455-460 + 2); // 446 + is.read(reinterpret_cast(&p->generalDiskIoError_), 2); // 447 + is.read(reinterpret_cast(&p->rspStatus_), 1); // 448 + is.seekg(1, std::ios_base::cur); // 448 + is.read(reinterpret_cast(&p->cpu1Temperature_), 1); // 449 + is.read(reinterpret_cast(&p->cpu2Temperature_), 1); // 449 + is.read(reinterpret_cast(&p->rspMotherboardPower_), 2); // 450 + is.seekg(20, std::ios_base::cur); // 451-460 // Device Status is.read(reinterpret_cast(&p->spipCommStatus_), 2); // 461 @@ -2173,17 +1883,18 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) bytesRead += 960; // Communications - p->loopBackSetStatus_ = ntohs(p->loopBackSetStatus_); - p->t1OutputFrames_ = ntohl(p->t1OutputFrames_); - p->t1InputFrames_ = ntohl(p->t1InputFrames_); - p->routerMemoryUsed_ = ntohl(p->routerMemoryUsed_); - p->routerMemoryFree_ = ntohl(p->routerMemoryFree_); - p->routerMemoryUtilization_ = ntohs(p->routerMemoryUtilization_); - p->routeToRpg_ = ntohs(p->routeToRpg_); - p->csuLossOfSignal_ = ntohl(p->csuLossOfSignal_); - p->csuLossOfFrames_ = ntohl(p->csuLossOfFrames_); - p->csuYellowAlarms_ = ntohl(p->csuYellowAlarms_); - p->csuBlueAlarms_ = ntohl(p->csuBlueAlarms_); + p->loopBackSetStatus_ = ntohs(p->loopBackSetStatus_); + p->t1OutputFrames_ = ntohl(p->t1OutputFrames_); + p->t1InputFrames_ = ntohl(p->t1InputFrames_); + p->routerMemoryUsed_ = ntohl(p->routerMemoryUsed_); + p->routerMemoryFree_ = ntohl(p->routerMemoryFree_); + p->routerMemoryUtilization_ = ntohs(p->routerMemoryUtilization_); + p->routeToRpg_ = ntohs(p->routeToRpg_); + p->t1PortStatus_ = ntohs(p->t1PortStatus_); + p->routerDedicatedEthernetPortStatus_ = + ntohs(p->routerDedicatedEthernetPortStatus_); + p->routerCommercialEthernetPortStatus_ = + ntohs(p->routerCommercialEthernetPortStatus_); p->csu24HrErroredSeconds_ = ntohl(p->csu24HrErroredSeconds_); p->csu24HrSeverelyErroredSeconds_ = ntohl(p->csu24HrSeverelyErroredSeconds_); p->csu24HrSeverelyErroredFramingSeconds_ = @@ -2198,7 +1909,7 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) p->lanSwitchMemoryUtilization_ = ntohs(p->lanSwitchMemoryUtilization_); p->ifdrChasisTemperature_ = ntohs(p->ifdrChasisTemperature_); p->ifdrFpgaTemperature_ = ntohs(p->ifdrFpgaTemperature_); - p->gpsSatellites_ = ntohl(p->gpsSatellites_); + p->ntpStatus_ = ntohs(p->ntpStatus_); p->ipcStatus_ = ntohs(p->ipcStatus_); p->commandedChannelControl_ = ntohs(p->commandedChannelControl_); @@ -2413,7 +2124,7 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) p->longPulseVerticalDbz0_ = SwapFloat(p->longPulseVerticalDbz0_); p->horizontalPowerSense_ = SwapFloat(p->horizontalPowerSense_); p->verticalPowerSense_ = SwapFloat(p->verticalPowerSense_); - p->zdrBias_ = SwapFloat(p->zdrBias_); + p->zdrOffset_ = SwapFloat(p->zdrOffset_); p->clutterSuppressionDelta_ = SwapFloat(p->clutterSuppressionDelta_); p->clutterSuppressionUnfilteredPower_ = SwapFloat(p->clutterSuppressionUnfilteredPower_); @@ -2441,12 +2152,8 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) ntohs(p->clutterFilterMapFileReadStatus_); p->clutterFilterMapFileWriteStatus_ = ntohs(p->clutterFilterMapFileWriteStatus_); - p->generatlDiskIoError_ = ntohs(p->generatlDiskIoError_); - p->cpu1FanSpeed_ = ntohs(p->cpu1FanSpeed_); - p->cpu2FanSpeed_ = ntohs(p->cpu2FanSpeed_); - p->rspFan1Speed_ = ntohs(p->rspFan1Speed_); - p->rspFan2Speed_ = ntohs(p->rspFan2Speed_); - p->rspFan3Speed_ = ntohs(p->rspFan3Speed_); + p->generalDiskIoError_ = ntohs(p->generalDiskIoError_); + p->rspMotherboardPower_ = ntohs(p->rspMotherboardPower_); // Device Status p->spipCommStatus_ = ntohs(p->spipCommStatus_); diff --git a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp index 94a96283..aff8664a 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp @@ -34,337 +34,180 @@ struct AntManualSetup class RdaAdaptationDataImpl { public: - explicit RdaAdaptationDataImpl() : - adapFileName_ {}, - adapFormat_ {}, - adapRevision_ {}, - adapDate_ {}, - adapTime_ {}, - lowerPreLimit_ {0.0f}, - azLat_ {0.0f}, - upperPreLimit_ {0.0f}, - elLat_ {0.0f}, - parkaz_ {0.0f}, - parkel_ {0.0f}, - aFuelConv_ {0.0f}, - aMinShelterTemp_ {0.0f}, - aMaxShelterTemp_ {0.0f}, - aMinShelterAcTempDiff_ {0.0f}, - aMaxXmtrAirTemp_ {0.0f}, - aMaxRadTemp_ {0.0f}, - aMaxRadTempRise_ {0.0f}, - lowerDeadLimit_ {0.0f}, - upperDeadLimit_ {0.0f}, - aMinGenRoomTemp_ {0.0f}, - aMaxGenRoomTemp_ {0.0f}, - spip5VRegLim_ {0.0f}, - spip15VRegLim_ {0.0f}, - rpgCoLocated_ {false}, - specFilterInstalled_ {false}, - tpsInstalled_ {false}, - rmsInstalled_ {false}, - aHvdlTstInt_ {0}, - aRpgLtInt_ {0}, - aMinStabUtilPwrTime_ {0}, - aGenAutoExerInterval_ {0}, - aUtilPwrSwReqInterval_ {0}, - aLowFuelLevel_ {0.0f}, - configChanNumber_ {0}, - redundantChanConfig_ {0}, - attenTable_ {0.0f}, - pathLosses_ {}, - vTsCw_ {0.0f}, - hRnscale_ {0.0f}, - atmos_ {0.0f}, - elIndex_ {0.0f}, - tfreqMhz_ {0}, - baseDataTcn_ {0.0f}, - reflDataTover_ {0.0f}, - tarHDbz0Lp_ {0.0f}, - tarVDbz0Lp_ {0.0f}, - initPhiDp_ {0}, - normInitPhiDp_ {0}, - lxLp_ {0.0f}, - lxSp_ {0.0f}, - meteorParam_ {0.0f}, - antennaGain_ {0.0f}, - velDegradLimit_ {0.0f}, - wthDegradLimit_ {0.0f}, - hNoisetempDgradLimit_ {0.0f}, - hMinNoisetemp_ {0}, - vNoisetempDgradLimit_ {0.0f}, - vMinNoisetemp_ {0}, - klyDegradeLimit_ {0.0f}, - tsCoho_ {0.0f}, - hTsCw_ {0.0f}, - tsStalo_ {0.0f}, - ameHNoiseEnr_ {0.0f}, - xmtrPeakPwrHighLimit_ {0.0f}, - xmtrPeakPwrLowLimit_ {0.0f}, - hDbz0DeltaLimit_ {0.0f}, - threshold1_ {0.0f}, - threshold2_ {0.0f}, - clutSuppDgradLim_ {0.0f}, - range0Value_ {0.0f}, - xmtrPwrMtrScale_ {0.0f}, - vDbz0DeltaLimit_ {0.0f}, - tarHDbz0Sp_ {0.0f}, - tarVDbz0Sp_ {0.0f}, - deltaprf_ {0}, - tauSp_ {0}, - tauLp_ {0}, - ncDeadValue_ {0}, - tauRfSp_ {0}, - tauRfLp_ {0}, - seg1Lim_ {0.0f}, - slatsec_ {0.0f}, - slonsec_ {0.0f}, - slatdeg_ {0}, - slatmin_ {0}, - slondeg_ {0}, - slonmin_ {0}, - slatdir_ {0}, - slondir_ {0}, - azCorrectionFactor_ {0.0f}, - elCorrectionFactor_ {0.0f}, - siteName_ {}, - antManualSetup_(), - azPosSustainDrive_ {0.0f}, - azNegSustainDrive_ {0.0f}, - azNomPosDriveSlope_ {0.0f}, - azNomNegDriveSlope_ {0.0f}, - azFeedbackSlope_ {0.0f}, - elPosSustainDrive_ {0.0f}, - elNegSustainDrive_ {0.0f}, - elNomPosDriveSlope_ {0.0f}, - elNomNegDriveSlope_ {0.0f}, - elFeedbackSlope_ {0.0f}, - elFirstSlope_ {0.0f}, - elSecondSlope_ {0.0f}, - elThirdSlope_ {0.0f}, - elDroopPos_ {0.0f}, - elOffNeutralDrive_ {0.0f}, - azIntertia_ {0.0f}, - elInertia_ {0.0f}, - rvp8nvIwaveguideLength_ {0}, - vRnscale_ {0.0f}, - velDataTover_ {0.0f}, - widthDataTover_ {0.0f}, - dopplerRangeStart_ {0.0f}, - maxElIndex_ {0}, - seg2Lim_ {0.0f}, - seg3Lim_ {0.0f}, - seg4Lim_ {0.0f}, - nbrElSegments_ {0}, - hNoiseLong_ {0.0f}, - antNoiseTemp_ {0.0f}, - hNoiseShort_ {0.0f}, - hNoiseTolerance_ {0.0f}, - minHDynRange_ {0.0f}, - genInstalled_ {false}, - genExercise_ {false}, - vNoiseTolerance_ {0.0f}, - minVDynRange_ {0.0f}, - zdrBiasDgradLim_ {0.0f}, - baselineZdrBias_ {0.0f}, - vNoiseLong_ {0.0f}, - vNoiseShort_ {0.0f}, - zdrDataTover_ {0.0f}, - phiDataTover_ {0.0f}, - rhoDataTover_ {0.0f}, - staloPowerDgradLimit_ {0.0f}, - staloPowerMaintLimit_ {0.0f}, - minHPwrSense_ {0.0f}, - minVPwrSense_ {0.0f}, - hPwrSenseOffset_ {0.0f}, - vPwrSenseOffset_ {0.0f}, - psGainRef_ {0.0f}, - rfPalletBroadLoss_ {0.0f}, - amePsTolerance_ {0.0f}, - ameMaxTemp_ {0.0f}, - ameMinTemp_ {0.0f}, - rcvrModMaxTemp_ {0.0f}, - rcvrModMinTemp_ {0.0f}, - biteModMaxTemp_ {0.0f}, - biteModMinTemp_ {0.0f}, - defaultPolarization_ {0}, - trLimitDgradLimit_ {0.0f}, - trLimitFailLimit_ {0.0f}, - rfpStepperEnabled_ {false}, - ameCurrentTolerance_ {0.0f}, - hOnlyPolarization_ {0}, - vOnlyPolarization_ {0}, - sunBias_ {0.0f}, - aMinShelterTempWarn_ {0.0f}, - powerMeterZero_ {0.0f}, - txbBaseline_ {0.0f}, - txbAlarmThresh_ {0.0f} {}; - ~RdaAdaptationDataImpl() = default; + explicit RdaAdaptationDataImpl() = default; + ~RdaAdaptationDataImpl() = default; - std::string adapFileName_; - std::string adapFormat_; - std::string adapRevision_; - std::string adapDate_; - std::string adapTime_; - float lowerPreLimit_; - float azLat_; - float upperPreLimit_; - float elLat_; - float parkaz_; - float parkel_; - std::array aFuelConv_; - float aMinShelterTemp_; - float aMaxShelterTemp_; - float aMinShelterAcTempDiff_; - float aMaxXmtrAirTemp_; - float aMaxRadTemp_; - float aMaxRadTempRise_; - float lowerDeadLimit_; - float upperDeadLimit_; - float aMinGenRoomTemp_; - float aMaxGenRoomTemp_; - float spip5VRegLim_; - float spip15VRegLim_; - bool rpgCoLocated_; - bool specFilterInstalled_; - bool tpsInstalled_; - bool rmsInstalled_; - uint32_t aHvdlTstInt_; - uint32_t aRpgLtInt_; - uint32_t aMinStabUtilPwrTime_; - uint32_t aGenAutoExerInterval_; - uint32_t aUtilPwrSwReqInterval_; - float aLowFuelLevel_; - uint32_t configChanNumber_; - uint32_t redundantChanConfig_; - std::array attenTable_; - std::map pathLosses_; - float vTsCw_; - std::array hRnscale_; - std::array atmos_; - std::array elIndex_; - uint32_t tfreqMhz_; - float baseDataTcn_; - float reflDataTover_; - float tarHDbz0Lp_; - float tarVDbz0Lp_; - uint32_t initPhiDp_; - uint32_t normInitPhiDp_; - float lxLp_; - float lxSp_; - float meteorParam_; - float antennaGain_; - float velDegradLimit_; - float wthDegradLimit_; - float hNoisetempDgradLimit_; - uint32_t hMinNoisetemp_; - float vNoisetempDgradLimit_; - uint32_t vMinNoisetemp_; - float klyDegradeLimit_; - float tsCoho_; - float hTsCw_; - float tsStalo_; - float ameHNoiseEnr_; - float xmtrPeakPwrHighLimit_; - float xmtrPeakPwrLowLimit_; - float hDbz0DeltaLimit_; - float threshold1_; - float threshold2_; - float clutSuppDgradLim_; - float range0Value_; - float xmtrPwrMtrScale_; - float vDbz0DeltaLimit_; - float tarHDbz0Sp_; - float tarVDbz0Sp_; - uint32_t deltaprf_; - uint32_t tauSp_; - uint32_t tauLp_; - uint32_t ncDeadValue_; - uint32_t tauRfSp_; - uint32_t tauRfLp_; - float seg1Lim_; - float slatsec_; - float slonsec_; - uint32_t slatdeg_; - uint32_t slatmin_; - uint32_t slondeg_; - uint32_t slonmin_; - char slatdir_; - char slondir_; - float azCorrectionFactor_; - float elCorrectionFactor_; - std::string siteName_; - AntManualSetup antManualSetup_; - float azPosSustainDrive_; - float azNegSustainDrive_; - float azNomPosDriveSlope_; - float azNomNegDriveSlope_; - float azFeedbackSlope_; - float elPosSustainDrive_; - float elNegSustainDrive_; - float elNomPosDriveSlope_; - float elNomNegDriveSlope_; - float elFeedbackSlope_; - float elFirstSlope_; - float elSecondSlope_; - float elThirdSlope_; - float elDroopPos_; - float elOffNeutralDrive_; - float azIntertia_; - float elInertia_; - uint32_t rvp8nvIwaveguideLength_; - std::array vRnscale_; - float velDataTover_; - float widthDataTover_; - float dopplerRangeStart_; - uint32_t maxElIndex_; - float seg2Lim_; - float seg3Lim_; - float seg4Lim_; - uint32_t nbrElSegments_; - float hNoiseLong_; - float antNoiseTemp_; - float hNoiseShort_; - float hNoiseTolerance_; - float minHDynRange_; - bool genInstalled_; - bool genExercise_; - float vNoiseTolerance_; - float minVDynRange_; - float zdrBiasDgradLim_; - float baselineZdrBias_; - float vNoiseLong_; - float vNoiseShort_; - float zdrDataTover_; - float phiDataTover_; - float rhoDataTover_; - float staloPowerDgradLimit_; - float staloPowerMaintLimit_; - float minHPwrSense_; - float minVPwrSense_; - float hPwrSenseOffset_; - float vPwrSenseOffset_; - float psGainRef_; - float rfPalletBroadLoss_; - float amePsTolerance_; - float ameMaxTemp_; - float ameMinTemp_; - float rcvrModMaxTemp_; - float rcvrModMinTemp_; - float biteModMaxTemp_; - float biteModMinTemp_; - uint32_t defaultPolarization_; - float trLimitDgradLimit_; - float trLimitFailLimit_; - bool rfpStepperEnabled_; - float ameCurrentTolerance_; - uint32_t hOnlyPolarization_; - uint32_t vOnlyPolarization_; - float sunBias_; - float aMinShelterTempWarn_; - float powerMeterZero_; - float txbBaseline_; - float txbAlarmThresh_; + std::string adapFileName_ {}; + std::string adapFormat_ {}; + std::string adapRevision_ {}; + std::string adapDate_ {}; + std::string adapTime_ {}; + float lowerPreLimit_ {0.0f}; + float azLat_ {0.0f}; + float upperPreLimit_ {0.0f}; + float elLat_ {0.0f}; + float parkaz_ {0.0f}; + float parkel_ {0.0f}; + std::array aFuelConv_ {0.0f}; + float aMinShelterTemp_ {0.0f}; + float aMaxShelterTemp_ {0.0f}; + float aMinShelterAcTempDiff_ {0.0f}; + float aMaxXmtrAirTemp_ {0.0f}; + float aMaxRadTemp_ {0.0f}; + float aMaxRadTempRise_ {0.0f}; + float lowerDeadLimit_ {0.0f}; + float upperDeadLimit_ {0.0f}; + float aMinGenRoomTemp_ {0.0f}; + float aMaxGenRoomTemp_ {0.0f}; + float spip5VRegLim_ {0.0f}; + float spip15VRegLim_ {0.0f}; + bool rpgCoLocated_ {false}; + bool specFilterInstalled_ {false}; + bool tpsInstalled_ {false}; + bool rmsInstalled_ {false}; + uint32_t aHvdlTstInt_ {0}; + uint32_t aRpgLtInt_ {0}; + uint32_t aMinStabUtilPwrTime_ {0}; + uint32_t aGenAutoExerInterval_ {0}; + uint32_t aUtilPwrSwReqInterval_ {0}; + float aLowFuelLevel_ {0.0f}; + uint32_t configChanNumber_ {0}; + uint32_t redundantChanConfig_ {0}; + std::array attenTable_ {0.0f}; + std::map pathLosses_ {}; + float vTsCw_ {0.0f}; + std::array hRnscale_ {0.0f}; + std::array atmos_ {0.0f}; + std::array elIndex_ {0.0f}; + uint32_t tfreqMhz_ {0}; + float baseDataTcn_ {0.0f}; + float reflDataTover_ {0.0f}; + float tarHDbz0Lp_ {0.0f}; + float tarVDbz0Lp_ {0.0f}; + uint32_t initPhiDp_ {0}; + uint32_t normInitPhiDp_ {0}; + float lxLp_ {0.0f}; + float lxSp_ {0.0f}; + float meteorParam_ {0.0f}; + float antennaGain_ {0.0f}; + float velDegradLimit_ {0.0f}; + float wthDegradLimit_ {0.0f}; + float hNoisetempDgradLimit_ {0.0f}; + uint32_t hMinNoisetemp_ {0}; + float vNoisetempDgradLimit_ {0.0f}; + uint32_t vMinNoisetemp_ {0}; + float klyDegradeLimit_ {0.0f}; + float tsCoho_ {0.0f}; + float hTsCw_ {0.0f}; + float tsStalo_ {0.0f}; + float ameHNoiseEnr_ {0.0f}; + float xmtrPeakPwrHighLimit_ {0.0f}; + float xmtrPeakPwrLowLimit_ {0.0f}; + float hDbz0DeltaLimit_ {0.0f}; + float threshold1_ {0.0f}; + float threshold2_ {0.0f}; + float clutSuppDgradLim_ {0.0f}; + float range0Value_ {0.0f}; + float xmtrPwrMtrScale_ {0.0f}; + float vDbz0DeltaLimit_ {0.0f}; + float tarHDbz0Sp_ {0.0f}; + float tarVDbz0Sp_ {0.0f}; + uint32_t deltaprf_ {0}; + uint32_t tauSp_ {0}; + uint32_t tauLp_ {0}; + uint32_t ncDeadValue_ {0}; + uint32_t tauRfSp_ {0}; + uint32_t tauRfLp_ {0}; + float seg1Lim_ {0.0f}; + float slatsec_ {0.0f}; + float slonsec_ {0.0f}; + uint32_t slatdeg_ {0}; + uint32_t slatmin_ {0}; + uint32_t slondeg_ {0}; + uint32_t slonmin_ {0}; + char slatdir_ {0}; + char slondir_ {0}; + double digRcvrClockFreq_ {0.0}; + double cohoFreq_ {0.0}; + float azCorrectionFactor_ {0.0f}; + float elCorrectionFactor_ {0.0f}; + std::string siteName_ {}; + AntManualSetup antManualSetup_ {}; + float azPosSustainDrive_ {0.0f}; + float azNegSustainDrive_ {0.0f}; + float azNomPosDriveSlope_ {0.0f}; + float azNomNegDriveSlope_ {0.0f}; + float azFeedbackSlope_ {0.0f}; + float elPosSustainDrive_ {0.0f}; + float elNegSustainDrive_ {0.0f}; + float elNomPosDriveSlope_ {0.0f}; + float elNomNegDriveSlope_ {0.0f}; + float elFeedbackSlope_ {0.0f}; + float elFirstSlope_ {0.0f}; + float elSecondSlope_ {0.0f}; + float elThirdSlope_ {0.0f}; + float elDroopPos_ {0.0f}; + float elOffNeutralDrive_ {0.0f}; + float azIntertia_ {0.0f}; + float elInertia_ {0.0f}; + float azStowAngle_ {0.0f}; + float elStowAngle_ {0.0f}; + float azEncoderAlignment_ {0.0f}; + float elEncoderAlignment_ {0.0f}; + std::string refinedPark_ {}; + uint32_t rvp8nvIwaveguideLength_ {0}; + std::array vRnscale_ {0.0f}; + float velDataTover_ {0.0f}; + float widthDataTover_ {0.0f}; + float dopplerRangeStart_ {0.0f}; + uint32_t maxElIndex_ {0}; + float seg2Lim_ {0.0f}; + float seg3Lim_ {0.0f}; + float seg4Lim_ {0.0f}; + uint32_t nbrElSegments_ {0}; + float hNoiseLong_ {0.0f}; + float antNoiseTemp_ {0.0f}; + float hNoiseShort_ {0.0f}; + float hNoiseTolerance_ {0.0f}; + float minHDynRange_ {0.0f}; + bool genInstalled_ {false}; + bool genExercise_ {false}; + float vNoiseTolerance_ {0.0f}; + float minVDynRange_ {0.0f}; + float zdrOffsetDgradLim_ {0.0f}; + float baselineZdrOffset_ {0.0f}; + float vNoiseLong_ {0.0f}; + float vNoiseShort_ {0.0f}; + float zdrDataTover_ {0.0f}; + float phiDataTover_ {0.0f}; + float rhoDataTover_ {0.0f}; + float staloPowerDgradLimit_ {0.0f}; + float staloPowerMaintLimit_ {0.0f}; + float minHPwrSense_ {0.0f}; + float minVPwrSense_ {0.0f}; + float hPwrSenseOffset_ {0.0f}; + float vPwrSenseOffset_ {0.0f}; + float psGainRef_ {0.0f}; + float rfPalletBroadLoss_ {0.0f}; + float amePsTolerance_ {0.0f}; + float ameMaxTemp_ {0.0f}; + float ameMinTemp_ {0.0f}; + float rcvrModMaxTemp_ {0.0f}; + float rcvrModMinTemp_ {0.0f}; + float biteModMaxTemp_ {0.0f}; + float biteModMinTemp_ {0.0f}; + uint32_t defaultPolarization_ {0}; + float trLimitDgradLimit_ {0.0f}; + float trLimitFailLimit_ {0.0f}; + bool rfpStepperEnabled_ {false}; + float ameCurrentTolerance_ {0.0f}; + uint32_t hOnlyPolarization_ {0}; + uint32_t vOnlyPolarization_ {0}; + float sunBias_ {0.0f}; + float aMinShelterTempWarn_ {0.0f}; + float powerMeterZero_ {0.0f}; + float txbBaseline_ {0.0f}; + float txbAlarmThresh_ {0.0f}; }; RdaAdaptationData::RdaAdaptationData() : @@ -867,6 +710,16 @@ char RdaAdaptationData::slondir() const return p->slondir_; } +double RdaAdaptationData::dig_rcvr_clock_freq() const +{ + return p->digRcvrClockFreq_; +} + +double RdaAdaptationData::coho_freq() const +{ + return p->cohoFreq_; +} + float RdaAdaptationData::az_correction_factor() const { return p->azCorrectionFactor_; @@ -999,6 +852,31 @@ float RdaAdaptationData::el_inertia() const return p->elInertia_; } +float RdaAdaptationData::az_stow_angle() const +{ + return p->azStowAngle_; +} + +float RdaAdaptationData::el_stow_angle() const +{ + return p->elStowAngle_; +} + +float RdaAdaptationData::az_encoder_alignment() const +{ + return p->azEncoderAlignment_; +} + +float RdaAdaptationData::el_encoder_alignment() const +{ + return p->elEncoderAlignment_; +} + +std::string RdaAdaptationData::refined_park() const +{ + return p->refinedPark_; +} + uint32_t RdaAdaptationData::rvp8nv_iwaveguide_length() const { return p->rvp8nvIwaveguideLength_; @@ -1094,14 +972,14 @@ float RdaAdaptationData::min_v_dyn_range() const return p->minVDynRange_; } -float RdaAdaptationData::zdr_bias_dgrad_lim() const +float RdaAdaptationData::zdr_offset_dgrad_lim() const { - return p->zdrBiasDgradLim_; + return p->zdrOffsetDgradLim_; } -float RdaAdaptationData::baseline_zdr_bias() const +float RdaAdaptationData::baseline_zdr_offset() const { - return p->baselineZdrBias_; + return p->baselineZdrOffset_; } float RdaAdaptationData::v_noise_long() const @@ -1277,6 +1155,7 @@ bool RdaAdaptationData::Parse(std::istream& is) p->adapDate_.resize(12); p->adapTime_.resize(12); p->siteName_.resize(4); + p->refinedPark_.resize(4); is.read(&p->adapFileName_[0], 12); // 0-11 is.read(&p->adapFormat_[0], 4); // 12-15 @@ -1458,7 +1337,12 @@ bool RdaAdaptationData::Parse(std::istream& is) ReadChar(is, p->slatdir_); // 1316-1319 ReadChar(is, p->slondir_); // 1320-1323 - is.seekg(7036, std::ios_base::cur); // 1324-8359 + is.seekg(3824, std::ios_base::cur); // 1324-2499 + + is.read(reinterpret_cast(&p->digRcvrClockFreq_), 8); // 2500-2507 + is.read(reinterpret_cast(&p->cohoFreq_), 8); // 2508-2515 + + is.seekg(5844, std::ios_base::cur); // 2516-8359 is.read(reinterpret_cast(&p->azCorrectionFactor_), 4); // 8360-8363 is.read(reinterpret_cast(&p->elCorrectionFactor_), 4); // 8364-8367 @@ -1493,7 +1377,18 @@ bool RdaAdaptationData::Parse(std::istream& is) is.read(reinterpret_cast(&p->azIntertia_), 4); // 8456-8459 is.read(reinterpret_cast(&p->elInertia_), 4); // 8460-8463 - is.seekg(232, std::ios_base::cur); // 8464-8695 + is.seekg(32, std::ios_base::cur); // 8464-8495 + + is.read(reinterpret_cast(&p->azStowAngle_), 4); // 8496-8499 + is.read(reinterpret_cast(&p->elStowAngle_), 4); // 8500-8503 + is.read(reinterpret_cast(&p->azEncoderAlignment_), 4); // 8504-8507 + is.read(reinterpret_cast(&p->elEncoderAlignment_), 4); // 8508-8511 + + is.seekg(176, std::ios_base::cur); // 8512-8687 + + is.read(&p->refinedPark_[0], 4); // 8688-8691 + + is.seekg(4, std::ios_base::cur); // 8692-8695 is.read(reinterpret_cast(&p->rvp8nvIwaveguideLength_), 4); // 8696-8699 @@ -1522,8 +1417,8 @@ bool RdaAdaptationData::Parse(std::istream& is) ReadBoolean(is, p->genExercise_); // 8812-8815 is.read(reinterpret_cast(&p->vNoiseTolerance_), 4); // 8816-8819 is.read(reinterpret_cast(&p->minVDynRange_), 4); // 8820-8823 - is.read(reinterpret_cast(&p->zdrBiasDgradLim_), 4); // 8824-8827 - is.read(reinterpret_cast(&p->baselineZdrBias_), 4); // 8828-8831 + is.read(reinterpret_cast(&p->zdrOffsetDgradLim_), 4); // 8824-8827 + is.read(reinterpret_cast(&p->baselineZdrOffset_), 4); // 8828-8831 is.seekg(12, std::ios_base::cur); // 8832-8843 @@ -1658,6 +1553,8 @@ bool RdaAdaptationData::Parse(std::istream& is) p->slatmin_ = ntohl(p->slatmin_); p->slondeg_ = ntohl(p->slondeg_); p->slonmin_ = ntohl(p->slonmin_); + p->digRcvrClockFreq_ = SwapDouble(p->digRcvrClockFreq_); + p->cohoFreq_ = SwapDouble(p->cohoFreq_); p->azCorrectionFactor_ = SwapFloat(p->azCorrectionFactor_); p->elCorrectionFactor_ = SwapFloat(p->elCorrectionFactor_); p->antManualSetup_.ielmin_ = ntohl(p->antManualSetup_.ielmin_); @@ -1683,6 +1580,10 @@ bool RdaAdaptationData::Parse(std::istream& is) p->elOffNeutralDrive_ = SwapFloat(p->elOffNeutralDrive_); p->azIntertia_ = SwapFloat(p->azIntertia_); p->elInertia_ = SwapFloat(p->elInertia_); + p->azStowAngle_ = SwapFloat(p->azStowAngle_); + p->elStowAngle_ = SwapFloat(p->elStowAngle_); + p->azEncoderAlignment_ = SwapFloat(p->azEncoderAlignment_); + p->elEncoderAlignment_ = SwapFloat(p->elEncoderAlignment_); p->rvp8nvIwaveguideLength_ = ntohl(p->rvp8nvIwaveguideLength_); SwapArray(p->vRnscale_); @@ -1702,8 +1603,8 @@ bool RdaAdaptationData::Parse(std::istream& is) p->minHDynRange_ = SwapFloat(p->minHDynRange_); p->vNoiseTolerance_ = SwapFloat(p->vNoiseTolerance_); p->minVDynRange_ = SwapFloat(p->minVDynRange_); - p->zdrBiasDgradLim_ = SwapFloat(p->zdrBiasDgradLim_); - p->baselineZdrBias_ = SwapFloat(p->baselineZdrBias_); + p->zdrOffsetDgradLim_ = SwapFloat(p->zdrOffsetDgradLim_); + p->baselineZdrOffset_ = SwapFloat(p->baselineZdrOffset_); p->vNoiseLong_ = SwapFloat(p->vNoiseLong_); p->vNoiseShort_ = SwapFloat(p->vNoiseShort_); p->zdrDataTover_ = SwapFloat(p->zdrDataTover_); diff --git a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp index ced8ff59..50ebd596 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp @@ -14,67 +14,39 @@ static const auto logger_ = util::Logger::Create(logPrefix_); class RdaStatusDataImpl { public: - explicit RdaStatusDataImpl() : - rdaStatus_ {0}, - operabilityStatus_ {0}, - controlStatus_ {0}, - auxiliaryPowerGeneratorState_ {0}, - averageTransmitterPower_ {0}, - horizontalReflectivityCalibrationCorrection_ {0}, - dataTransmissionEnabled_ {0}, - volumeCoveragePatternNumber_ {0}, - rdaControlAuthorization_ {0}, - rdaBuildNumber_ {0}, - operationalMode_ {0}, - superResolutionStatus_ {0}, - clutterMitigationDecisionStatus_ {0}, - avsetEbcRdaLogDataStatus_ {0}, - rdaAlarmSummary_ {0}, - commandAcknowledgement_ {0}, - channelControlStatus_ {0}, - spotBlankingStatus_ {0}, - bypassMapGenerationDate_ {0}, - bypassMapGenerationTime_ {0}, - clutterFilterMapGenerationDate_ {0}, - clutterFilterMapGenerationTime_ {0}, - verticalReflectivityCalibrationCorrection_ {0}, - transitionPowerSourceStatus_ {0}, - rmsControlStatus_ {0}, - performanceCheckStatus_ {0}, - alarmCodes_ {0}, - signalProcessingOptions_ {0}, - statusVersion_ {0} {}; - ~RdaStatusDataImpl() = default; + explicit RdaStatusDataImpl() = default; + ~RdaStatusDataImpl() = default; - uint16_t rdaStatus_; - uint16_t operabilityStatus_; - uint16_t controlStatus_; - uint16_t auxiliaryPowerGeneratorState_; - uint16_t averageTransmitterPower_; - int16_t horizontalReflectivityCalibrationCorrection_; - uint16_t dataTransmissionEnabled_; - uint16_t volumeCoveragePatternNumber_; - uint16_t rdaControlAuthorization_; - uint16_t rdaBuildNumber_; - uint16_t operationalMode_; - uint16_t superResolutionStatus_; - uint16_t clutterMitigationDecisionStatus_; - uint16_t avsetEbcRdaLogDataStatus_; - uint16_t rdaAlarmSummary_; - uint16_t commandAcknowledgement_; - uint16_t channelControlStatus_; - uint16_t spotBlankingStatus_; - uint16_t bypassMapGenerationDate_; - uint16_t bypassMapGenerationTime_; - uint16_t clutterFilterMapGenerationDate_; - uint16_t clutterFilterMapGenerationTime_; - int16_t verticalReflectivityCalibrationCorrection_; - uint16_t transitionPowerSourceStatus_; - uint16_t rmsControlStatus_; - uint16_t performanceCheckStatus_; - std::array alarmCodes_; - uint16_t signalProcessingOptions_; - uint16_t statusVersion_; + uint16_t rdaStatus_ {0}; + uint16_t operabilityStatus_ {0}; + uint16_t controlStatus_ {0}; + uint16_t auxiliaryPowerGeneratorState_ {0}; + uint16_t averageTransmitterPower_ {0}; + int16_t horizontalReflectivityCalibrationCorrection_ {0}; + uint16_t dataTransmissionEnabled_ {0}; + uint16_t volumeCoveragePatternNumber_ {0}; + uint16_t rdaControlAuthorization_ {0}; + uint16_t rdaBuildNumber_ {0}; + uint16_t operationalMode_ {0}; + uint16_t superResolutionStatus_ {0}; + uint16_t clutterMitigationDecisionStatus_ {0}; + uint16_t rdaScanAndDataFlags_ {0}; + uint16_t rdaAlarmSummary_ {0}; + uint16_t commandAcknowledgement_ {0}; + uint16_t channelControlStatus_ {0}; + uint16_t spotBlankingStatus_ {0}; + uint16_t bypassMapGenerationDate_ {0}; + uint16_t bypassMapGenerationTime_ {0}; + uint16_t clutterFilterMapGenerationDate_ {0}; + uint16_t clutterFilterMapGenerationTime_ {0}; + int16_t verticalReflectivityCalibrationCorrection_ {0}; + uint16_t transitionPowerSourceStatus_ {0}; + uint16_t rmsControlStatus_ {0}; + uint16_t performanceCheckStatus_ {0}; + std::array alarmCodes_ {0}; + uint16_t signalProcessingOptions_ {0}; + uint16_t downloadedPatternNumber_ {0}; + uint16_t statusVersion_ {0}; }; RdaStatusData::RdaStatusData() : @@ -83,7 +55,7 @@ RdaStatusData::RdaStatusData() : } RdaStatusData::~RdaStatusData() = default; -RdaStatusData::RdaStatusData(RdaStatusData&&) noexcept = default; +RdaStatusData::RdaStatusData(RdaStatusData&&) noexcept = default; RdaStatusData& RdaStatusData::operator=(RdaStatusData&&) noexcept = default; uint16_t RdaStatusData::rda_status() const @@ -151,9 +123,9 @@ uint16_t RdaStatusData::clutter_mitigation_decision_status() const return p->clutterMitigationDecisionStatus_; } -uint16_t RdaStatusData::avset_ebc_rda_log_data_status() const +uint16_t RdaStatusData::rda_scan_and_data_flags() const { - return p->avsetEbcRdaLogDataStatus_; + return p->rdaScanAndDataFlags_; } uint16_t RdaStatusData::rda_alarm_summary() const @@ -226,6 +198,11 @@ uint16_t RdaStatusData::signal_processing_options() const return p->signalProcessingOptions_; } +uint16_t RdaStatusData::downloaded_pattern_number() const +{ + return p->downloadedPatternNumber_; +} + uint16_t RdaStatusData::status_version() const { return p->statusVersion_; @@ -253,14 +230,14 @@ bool RdaStatusData::Parse(std::istream& is) is.read(reinterpret_cast(&p->operationalMode_), 2); // 11 is.read(reinterpret_cast(&p->superResolutionStatus_), 2); // 12 is.read(reinterpret_cast(&p->clutterMitigationDecisionStatus_), - 2); // 13 - is.read(reinterpret_cast(&p->avsetEbcRdaLogDataStatus_), 2); // 14 - is.read(reinterpret_cast(&p->rdaAlarmSummary_), 2); // 15 - is.read(reinterpret_cast(&p->commandAcknowledgement_), 2); // 16 - is.read(reinterpret_cast(&p->channelControlStatus_), 2); // 17 - is.read(reinterpret_cast(&p->spotBlankingStatus_), 2); // 18 - is.read(reinterpret_cast(&p->bypassMapGenerationDate_), 2); // 19 - is.read(reinterpret_cast(&p->bypassMapGenerationTime_), 2); // 20 + 2); // 13 + is.read(reinterpret_cast(&p->rdaScanAndDataFlags_), 2); // 14 + is.read(reinterpret_cast(&p->rdaAlarmSummary_), 2); // 15 + is.read(reinterpret_cast(&p->commandAcknowledgement_), 2); // 16 + is.read(reinterpret_cast(&p->channelControlStatus_), 2); // 17 + is.read(reinterpret_cast(&p->spotBlankingStatus_), 2); // 18 + is.read(reinterpret_cast(&p->bypassMapGenerationDate_), 2); // 19 + is.read(reinterpret_cast(&p->bypassMapGenerationTime_), 2); // 20 is.read(reinterpret_cast(&p->clutterFilterMapGenerationDate_), 2); // 21 is.read(reinterpret_cast(&p->clutterFilterMapGenerationTime_), @@ -290,13 +267,13 @@ bool RdaStatusData::Parse(std::istream& is) p->superResolutionStatus_ = ntohs(p->superResolutionStatus_); p->clutterMitigationDecisionStatus_ = ntohs(p->clutterMitigationDecisionStatus_); - p->avsetEbcRdaLogDataStatus_ = ntohs(p->avsetEbcRdaLogDataStatus_); - p->rdaAlarmSummary_ = ntohs(p->rdaAlarmSummary_); - p->commandAcknowledgement_ = ntohs(p->commandAcknowledgement_); - p->channelControlStatus_ = ntohs(p->channelControlStatus_); - p->spotBlankingStatus_ = ntohs(p->spotBlankingStatus_); - p->bypassMapGenerationDate_ = ntohs(p->bypassMapGenerationDate_); - p->bypassMapGenerationTime_ = ntohs(p->bypassMapGenerationTime_); + p->rdaScanAndDataFlags_ = ntohs(p->rdaScanAndDataFlags_); + p->rdaAlarmSummary_ = ntohs(p->rdaAlarmSummary_); + p->commandAcknowledgement_ = ntohs(p->commandAcknowledgement_); + p->channelControlStatus_ = ntohs(p->channelControlStatus_); + p->spotBlankingStatus_ = ntohs(p->spotBlankingStatus_); + p->bypassMapGenerationDate_ = ntohs(p->bypassMapGenerationDate_); + p->bypassMapGenerationTime_ = ntohs(p->bypassMapGenerationTime_); p->clutterFilterMapGenerationDate_ = ntohs(p->clutterFilterMapGenerationDate_); p->clutterFilterMapGenerationTime_ = @@ -312,8 +289,9 @@ bool RdaStatusData::Parse(std::istream& is) if (header().message_size() * 2 > Level2MessageHeader::SIZE + 80) { is.read(reinterpret_cast(&p->signalProcessingOptions_), 2); // 41 - is.seekg(36, std::ios_base::cur); // 42-59 - is.read(reinterpret_cast(&p->statusVersion_), 2); // 60 + is.seekg(34, std::ios_base::cur); // 42-58 + is.read(reinterpret_cast(&p->downloadedPatternNumber_), 2); // 59 + is.read(reinterpret_cast(&p->statusVersion_), 2); // 60 bytesRead += 40; p->signalProcessingOptions_ = ntohs(p->signalProcessingOptions_); From fd6c224fc2bed617b92971cb4564f7cb82454d31 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 12 May 2025 23:24:49 -0500 Subject: [PATCH 596/762] Modified RDA message clang-tidy cleanup --- wxdata/include/scwx/awips/message.hpp | 12 +- .../wsr88d/rda/digital_radar_data_generic.hpp | 88 +- .../rda/performance_maintenance_data.hpp | 508 +++++----- .../scwx/wsr88d/rda/rda_adaptation_data.hpp | 385 ++++---- .../scwx/wsr88d/rda/rda_status_data.hpp | 77 +- wxdata/source/scwx/awips/message.cpp | 15 +- .../wsr88d/rda/digital_radar_data_generic.cpp | 141 ++- .../rda/performance_maintenance_data.cpp | 906 +++++++++--------- .../scwx/wsr88d/rda/rda_adaptation_data.cpp | 372 +++---- .../scwx/wsr88d/rda/rda_status_data.cpp | 171 ++-- 10 files changed, 1365 insertions(+), 1310 deletions(-) diff --git a/wxdata/include/scwx/awips/message.hpp b/wxdata/include/scwx/awips/message.hpp index 4f2388d0..f13a2a90 100644 --- a/wxdata/include/scwx/awips/message.hpp +++ b/wxdata/include/scwx/awips/message.hpp @@ -13,12 +13,8 @@ # include #endif -namespace scwx +namespace scwx::awips { -namespace awips -{ - -class MessageImpl; class Message { @@ -135,8 +131,8 @@ public: } private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; -} // namespace awips -} // namespace scwx +} // namespace scwx::awips diff --git a/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp b/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp index ba911898..07f3c111 100644 --- a/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp +++ b/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp @@ -2,11 +2,7 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { class DigitalRadarDataGeneric : public GenericRadarData @@ -27,30 +23,31 @@ public: DigitalRadarDataGeneric(DigitalRadarDataGeneric&&) noexcept; DigitalRadarDataGeneric& operator=(DigitalRadarDataGeneric&&) noexcept; - std::string radar_identifier() const; - std::uint32_t collection_time() const; - std::uint16_t modified_julian_date() const; - std::uint16_t azimuth_number() const; - units::degrees azimuth_angle() const; - std::uint8_t compression_indicator() const; - std::uint16_t radial_length() const; - std::uint8_t azimuth_resolution_spacing() const; - std::uint8_t radial_status() const; - std::uint16_t elevation_number() const; - std::uint8_t cut_sector_number() const; - units::degrees elevation_angle() const; - std::uint8_t radial_spot_blanking_status() const; - std::uint8_t azimuth_indexing_mode() const; - std::uint16_t data_block_count() const; - std::uint16_t volume_coverage_pattern_number() const; + [[nodiscard]] std::string radar_identifier() const; + [[nodiscard]] std::uint32_t collection_time() const override; + [[nodiscard]] std::uint16_t modified_julian_date() const override; + [[nodiscard]] std::uint16_t azimuth_number() const override; + [[nodiscard]] units::degrees azimuth_angle() const override; + [[nodiscard]] std::uint8_t compression_indicator() const; + [[nodiscard]] std::uint16_t radial_length() const; + [[nodiscard]] std::uint8_t azimuth_resolution_spacing() const; + [[nodiscard]] std::uint8_t radial_status() const; + [[nodiscard]] std::uint16_t elevation_number() const override; + [[nodiscard]] std::uint8_t cut_sector_number() const; + [[nodiscard]] units::degrees elevation_angle() const; + [[nodiscard]] std::uint8_t radial_spot_blanking_status() const; + [[nodiscard]] std::uint8_t azimuth_indexing_mode() const; + [[nodiscard]] std::uint16_t data_block_count() const; + [[nodiscard]] std::uint16_t volume_coverage_pattern_number() const override; - std::shared_ptr elevation_data_block() const; - std::shared_ptr radial_data_block() const; - std::shared_ptr volume_data_block() const; - std::shared_ptr - moment_data_block(DataBlockType type) const; + [[nodiscard]] std::shared_ptr + elevation_data_block() const; + [[nodiscard]] std::shared_ptr radial_data_block() const; + [[nodiscard]] std::shared_ptr volume_data_block() const; + [[nodiscard]] std::shared_ptr + moment_data_block(DataBlockType type) const override; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static std::shared_ptr Create(Level2MessageHeader&& header, std::istream& is); @@ -65,11 +62,14 @@ class DigitalRadarDataGeneric::DataBlock protected: explicit DataBlock(const std::string& dataBlockType, const std::string& dataName); + +public: virtual ~DataBlock(); DataBlock(const DataBlock&) = delete; DataBlock& operator=(const DataBlock&) = delete; +protected: DataBlock(DataBlock&&) noexcept; DataBlock& operator=(DataBlock&&) noexcept; @@ -118,17 +118,17 @@ public: MomentDataBlock(MomentDataBlock&&) noexcept; MomentDataBlock& operator=(MomentDataBlock&&) noexcept; - std::uint16_t number_of_data_moment_gates() const; - units::kilometers data_moment_range() const; - std::int16_t data_moment_range_raw() const; - units::kilometers data_moment_range_sample_interval() const; - std::uint16_t data_moment_range_sample_interval_raw() const; - float snr_threshold() const; - std::int16_t snr_threshold_raw() const; - std::uint8_t data_word_size() const; - float scale() const; - float offset() const; - const void* data_moments() const; + [[nodiscard]] std::uint16_t number_of_data_moment_gates() const override; + [[nodiscard]] units::kilometers data_moment_range() const override; + [[nodiscard]] std::int16_t data_moment_range_raw() const override; + [[nodiscard]] units::kilometers data_moment_range_sample_interval() const override; + [[nodiscard]] std::uint16_t data_moment_range_sample_interval_raw() const override; + [[nodiscard]] float snr_threshold() const; + [[nodiscard]] std::int16_t snr_threshold_raw() const override; + [[nodiscard]] std::uint8_t data_word_size() const override; + [[nodiscard]] float scale() const override; + [[nodiscard]] float offset() const override; + [[nodiscard]] const void* data_moments() const override; static std::shared_ptr Create(const std::string& dataBlockType, @@ -155,7 +155,7 @@ public: RadialDataBlock(RadialDataBlock&&) noexcept; RadialDataBlock& operator=(RadialDataBlock&&) noexcept; - float unambiguous_range() const; + [[nodiscard]] float unambiguous_range() const; static std::shared_ptr Create(const std::string& dataBlockType, @@ -182,9 +182,9 @@ public: VolumeDataBlock(VolumeDataBlock&&) noexcept; VolumeDataBlock& operator=(VolumeDataBlock&&) noexcept; - float latitude() const; - float longitude() const; - std::uint16_t volume_coverage_pattern_number() const; + [[nodiscard]] float latitude() const; + [[nodiscard]] float longitude() const; + [[nodiscard]] std::uint16_t volume_coverage_pattern_number() const; static std::shared_ptr Create(const std::string& dataBlockType, @@ -198,6 +198,4 @@ private: bool Parse(std::istream& is); }; -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp b/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp index c362af78..c1bf3869 100644 --- a/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/performance_maintenance_data.hpp @@ -2,14 +2,8 @@ #include -namespace scwx +namespace scwx::wsr88d::rda { -namespace wsr88d -{ -namespace rda -{ - -class PerformanceMaintenanceDataImpl; class PerformanceMaintenanceData : public Level2Message { @@ -24,261 +18,261 @@ public: PerformanceMaintenanceData(PerformanceMaintenanceData&&) noexcept; PerformanceMaintenanceData& operator=(PerformanceMaintenanceData&&) noexcept; - uint16_t loop_back_set_status() const; - uint32_t t1_output_frames() const; - uint32_t t1_input_frames() const; - uint32_t router_memory_used() const; - uint32_t router_memory_free() const; - uint16_t router_memory_utilization() const; - uint16_t route_to_rpg() const; - uint16_t t1_port_status() const; - uint16_t router_dedicated_ethernet_port_status() const; - uint16_t router_commercial_ethernet_port_status() const; - uint32_t csu_24hr_errored_seconds() const; - uint32_t csu_24hr_severely_errored_seconds() const; - uint32_t csu_24hr_severely_errored_framing_seconds() const; - uint32_t csu_24hr_unavailable_seconds() const; - uint32_t csu_24hr_controlled_slip_seconds() const; - uint32_t csu_24hr_path_coding_violations() const; - uint32_t csu_24hr_line_errored_seconds() const; - uint32_t csu_24hr_bursty_errored_seconds() const; - uint32_t csu_24hr_degraded_minutes() const; - uint32_t lan_switch_cpu_utilization() const; - uint16_t lan_switch_memory_utilization() const; - uint16_t ifdr_chasis_temperature() const; - uint16_t ifdr_fpga_temperature() const; - uint16_t ntp_status() const; - uint16_t ipc_status() const; - uint16_t commanded_channel_control() const; - uint16_t polarization() const; - float ame_internal_temperature() const; - float ame_receiver_module_temperature() const; - float ame_bite_cal_module_temperature() const; - uint16_t ame_peltier_pulse_width_modulation() const; - uint16_t ame_peltier_status() const; - uint16_t ame_a_d_converter_status() const; - uint16_t ame_state() const; - float ame_3_3v_ps_voltage() const; - float ame_5v_ps_voltage() const; - float ame_6_5v_ps_voltage() const; - float ame_15v_ps_voltage() const; - float ame_48v_ps_voltage() const; - float ame_stalo_power() const; - float peltier_current() const; - float adc_calibration_reference_voltage() const; - uint16_t ame_mode() const; - uint16_t ame_peltier_mode() const; - float ame_peltier_inside_fan_current() const; - float ame_peltier_outside_fan_current() const; - float horizontal_tr_limiter_voltage() const; - float vertical_tr_limiter_voltage() const; - float adc_calibration_offset_voltage() const; - float adc_calibration_gain_correction() const; - uint16_t rcp_status() const; - std::string rcp_string() const; - uint16_t spip_power_buttons() const; - float master_power_administrator_load() const; - float expansion_power_administrator_load() const; - uint16_t _5vdc_ps() const; - uint16_t _15vdc_ps() const; - uint16_t _28vdc_ps() const; - uint16_t neg_15vdc_ps() const; - uint16_t _45vdc_ps() const; - uint16_t filament_ps_voltage() const; - uint16_t vacuum_pump_ps_voltage() const; - uint16_t focus_coil_ps_voltage() const; - uint16_t filament_ps() const; - uint16_t klystron_warmup() const; - uint16_t transmitter_available() const; - uint16_t wg_switch_position() const; - uint16_t wg_pfn_transfer_interlock() const; - uint16_t maintenance_mode() const; - uint16_t maintenance_required() const; - uint16_t pfn_switch_position() const; - uint16_t modulator_overload() const; - uint16_t modulator_inv_current() const; - uint16_t modulator_switch_fail() const; - uint16_t main_power_voltage() const; - uint16_t charging_system_fail() const; - uint16_t inverse_diode_current() const; - uint16_t trigger_amplifier() const; - uint16_t circulator_temperature() const; - uint16_t spectrum_filter_pressure() const; - uint16_t wg_arc_vswr() const; - uint16_t cabinet_interlock() const; - uint16_t cabinet_air_temperature() const; - uint16_t cabinet_airflow() const; - uint16_t klystron_current() const; - uint16_t klystron_filament_current() const; - uint16_t klystron_vacion_current() const; - uint16_t klystron_air_temperature() const; - uint16_t klystron_airflow() const; - uint16_t modulator_switch_maintenance() const; - uint16_t post_charge_regulator_maintenance() const; - uint16_t wg_pressure_humidity() const; - uint16_t transmitter_overvoltage() const; - uint16_t transmitter_overcurrent() const; - uint16_t focus_coil_current() const; - uint16_t focus_coil_airflow() const; - uint16_t oil_temperature() const; - uint16_t prf_limit() const; - uint16_t transmitter_oil_level() const; - uint16_t transmitter_battery_charging() const; - uint16_t high_voltage_status() const; - uint16_t transmitter_recycling_summary() const; - uint16_t transmitter_inoperable() const; - uint16_t transmitter_air_filter() const; - uint16_t zero_test_bit(unsigned i) const; - uint16_t one_test_bit(unsigned i) const; - uint16_t xmtr_spip_interface() const; - uint16_t transmitter_summary_status() const; - float transmitter_rf_power() const; - float horizontal_xmtr_peak_power() const; - float xmtr_peak_power() const; - float vertical_xmtr_peak_power() const; - float xmtr_rf_avg_power() const; - uint32_t xmtr_recycle_count() const; - float receiver_bias() const; - float transmit_imbalance() const; - float xmtr_power_meter_zero() const; - uint16_t ac_unit1_compressor_shut_off() const; - uint16_t ac_unit2_compressor_shut_off() const; - uint16_t generator_maintenance_required() const; - uint16_t generator_battery_voltage() const; - uint16_t generator_engine() const; - uint16_t generator_volt_frequency() const; - uint16_t power_source() const; - uint16_t transitional_power_source() const; - uint16_t generator_auto_run_off_switch() const; - uint16_t aircraft_hazard_lighting() const; - uint16_t equipment_shelter_fire_detection_system() const; - uint16_t equipment_shelter_fire_smoke() const; - uint16_t generator_shelter_fire_smoke() const; - uint16_t utility_voltage_frequency() const; - uint16_t site_security_alarm() const; - uint16_t security_equipment() const; - uint16_t security_system() const; - uint16_t receiver_connected_to_antenna() const; - uint16_t radome_hatch() const; - uint16_t ac_unit1_filter_dirty() const; - uint16_t ac_unit2_filter_dirty() const; - float equipment_shelter_temperature() const; - float outside_ambient_temperature() const; - float transmitter_leaving_air_temp() const; - float ac_unit1_discharge_air_temp() const; - float generator_shelter_temperature() const; - float radome_air_temperature() const; - float ac_unit2_discharge_air_temp() const; - float spip_15v_ps() const; - float spip_neg_15v_ps() const; - uint16_t spip_28v_ps_status() const; - float spip_5v_ps() const; - uint16_t converted_generator_fuel_level() const; - uint16_t elevation_pos_dead_limit() const; - uint16_t _150v_overvoltage() const; - uint16_t _150v_undervoltage() const; - uint16_t elevation_servo_amp_inhibit() const; - uint16_t elevation_servo_amp_short_circuit() const; - uint16_t elevation_servo_amp_overtemp() const; - uint16_t elevation_motor_overtemp() const; - uint16_t elevation_stow_pin() const; - uint16_t elevation_housing_5v_ps() const; - uint16_t elevation_neg_dead_limit() const; - uint16_t elevation_pos_normal_limit() const; - uint16_t elevation_neg_normal_limit() const; - uint16_t elevation_encoder_light() const; - uint16_t elevation_gearbox_oil() const; - uint16_t elevation_handwheel() const; - uint16_t elevation_amp_ps() const; - uint16_t azimuth_servo_amp_inhibit() const; - uint16_t azimuth_servo_amp_short_circuit() const; - uint16_t azimuth_servo_amp_overtemp() const; - uint16_t azimuth_motor_overtemp() const; - uint16_t azimuth_stow_pin() const; - uint16_t azimuth_housing_5v_ps() const; - uint16_t azimuth_encoder_light() const; - uint16_t azimuth_gearbox_oil() const; - uint16_t azimuth_bull_gear_oil() const; - uint16_t azimuth_handwheel() const; - uint16_t azimuth_servo_amp_ps() const; - uint16_t servo() const; - uint16_t pedestal_interlock_switch() const; - uint16_t coho_clock() const; - uint16_t rf_generator_frequency_select_oscillator() const; - uint16_t rf_generator_rf_stalo() const; - uint16_t rf_generator_phase_shifted_coho() const; - uint16_t _9v_receiver_ps() const; - uint16_t _5v_receiver_ps() const; - uint16_t _18v_receiver_ps() const; - uint16_t neg_9v_receiver_ps() const; - uint16_t _5v_single_channel_rdaiu_ps() const; - float horizontal_short_pulse_noise() const; - float horizontal_long_pulse_noise() const; - float horizontal_noise_temperature() const; - float vertical_short_pulse_noise() const; - float vertical_long_pulse_noise() const; - float vertical_noise_temperature() const; - float horizontal_linearity() const; - float horizontal_dynamic_range() const; - float horizontal_delta_dbz0() const; - float vertical_delta_dbz0() const; - float kd_peak_measured() const; - float short_pulse_horizontal_dbz0() const; - float long_pulse_horizontal_dbz0() const; - uint16_t velocity_processed() const; - uint16_t width_processed() const; - uint16_t velocity_rf_gen() const; - uint16_t width_rf_gen() const; - float horizontal_i0() const; - float vertical_i0() const; - float vertical_dynamic_range() const; - float short_pulse_vertical_dbz0() const; - float long_pulse_vertical_dbz0() const; - float horizontal_power_sense() const; - float vertical_power_sense() const; - float zdr_offset() const; - float clutter_suppression_delta() const; - float clutter_suppression_unfiltered_power() const; - float clutter_suppression_filtered_power() const; - float vertical_linearity() const; - uint16_t state_file_read_status() const; - uint16_t state_file_write_status() const; - uint16_t bypass_map_file_read_status() const; - uint16_t bypass_map_file_write_status() const; - uint16_t current_adaptation_file_read_status() const; - uint16_t current_adaptation_file_write_status() const; - uint16_t censor_zone_file_read_status() const; - uint16_t censor_zone_file_write_status() const; - uint16_t remote_vcp_file_read_status() const; - uint16_t remote_vcp_file_write_status() const; - uint16_t baseline_adaptation_file_read_status() const; - uint16_t read_status_of_prf_sets() const; - uint16_t clutter_filter_map_file_read_status() const; - uint16_t clutter_filter_map_file_write_status() const; - uint16_t general_disk_io_error() const; - uint8_t rsp_status() const; - uint8_t cpu1_temperature() const; - uint8_t cpu2_temperature() const; - uint16_t rsp_motherboard_power() const; - uint16_t spip_comm_status() const; - uint16_t hci_comm_status() const; - uint16_t signal_processor_command_status() const; - uint16_t ame_communication_status() const; - uint16_t rms_link_status() const; - uint16_t rpg_link_status() const; - uint16_t interpanel_link_status() const; - uint32_t performance_check_time() const; - uint16_t version() const; + [[nodiscard]] std::uint16_t loop_back_set_status() const; + [[nodiscard]] std::uint32_t t1_output_frames() const; + [[nodiscard]] std::uint32_t t1_input_frames() const; + [[nodiscard]] std::uint32_t router_memory_used() const; + [[nodiscard]] std::uint32_t router_memory_free() const; + [[nodiscard]] std::uint16_t router_memory_utilization() const; + [[nodiscard]] std::uint16_t route_to_rpg() const; + [[nodiscard]] std::uint16_t t1_port_status() const; + [[nodiscard]] std::uint16_t router_dedicated_ethernet_port_status() const; + [[nodiscard]] std::uint16_t router_commercial_ethernet_port_status() const; + [[nodiscard]] std::uint32_t csu_24hr_errored_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_severely_errored_seconds() const; + [[nodiscard]] std::uint32_t + csu_24hr_severely_errored_framing_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_unavailable_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_controlled_slip_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_path_coding_violations() const; + [[nodiscard]] std::uint32_t csu_24hr_line_errored_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_bursty_errored_seconds() const; + [[nodiscard]] std::uint32_t csu_24hr_degraded_minutes() const; + [[nodiscard]] std::uint32_t lan_switch_cpu_utilization() const; + [[nodiscard]] std::uint16_t lan_switch_memory_utilization() const; + [[nodiscard]] std::uint16_t ifdr_chasis_temperature() const; + [[nodiscard]] std::uint16_t ifdr_fpga_temperature() const; + [[nodiscard]] std::uint16_t ntp_status() const; + [[nodiscard]] std::uint16_t ipc_status() const; + [[nodiscard]] std::uint16_t commanded_channel_control() const; + [[nodiscard]] std::uint16_t polarization() const; + [[nodiscard]] float ame_internal_temperature() const; + [[nodiscard]] float ame_receiver_module_temperature() const; + [[nodiscard]] float ame_bite_cal_module_temperature() const; + [[nodiscard]] std::uint16_t ame_peltier_pulse_width_modulation() const; + [[nodiscard]] std::uint16_t ame_peltier_status() const; + [[nodiscard]] std::uint16_t ame_a_d_converter_status() const; + [[nodiscard]] std::uint16_t ame_state() const; + [[nodiscard]] float ame_3_3v_ps_voltage() const; + [[nodiscard]] float ame_5v_ps_voltage() const; + [[nodiscard]] float ame_6_5v_ps_voltage() const; + [[nodiscard]] float ame_15v_ps_voltage() const; + [[nodiscard]] float ame_48v_ps_voltage() const; + [[nodiscard]] float ame_stalo_power() const; + [[nodiscard]] float peltier_current() const; + [[nodiscard]] float adc_calibration_reference_voltage() const; + [[nodiscard]] std::uint16_t ame_mode() const; + [[nodiscard]] std::uint16_t ame_peltier_mode() const; + [[nodiscard]] float ame_peltier_inside_fan_current() const; + [[nodiscard]] float ame_peltier_outside_fan_current() const; + [[nodiscard]] float horizontal_tr_limiter_voltage() const; + [[nodiscard]] float vertical_tr_limiter_voltage() const; + [[nodiscard]] float adc_calibration_offset_voltage() const; + [[nodiscard]] float adc_calibration_gain_correction() const; + [[nodiscard]] std::uint16_t rcp_status() const; + [[nodiscard]] std::string rcp_string() const; + [[nodiscard]] std::uint16_t spip_power_buttons() const; + [[nodiscard]] float master_power_administrator_load() const; + [[nodiscard]] float expansion_power_administrator_load() const; + [[nodiscard]] std::uint16_t _5vdc_ps() const; + [[nodiscard]] std::uint16_t _15vdc_ps() const; + [[nodiscard]] std::uint16_t _28vdc_ps() const; + [[nodiscard]] std::uint16_t neg_15vdc_ps() const; + [[nodiscard]] std::uint16_t _45vdc_ps() const; + [[nodiscard]] std::uint16_t filament_ps_voltage() const; + [[nodiscard]] std::uint16_t vacuum_pump_ps_voltage() const; + [[nodiscard]] std::uint16_t focus_coil_ps_voltage() const; + [[nodiscard]] std::uint16_t filament_ps() const; + [[nodiscard]] std::uint16_t klystron_warmup() const; + [[nodiscard]] std::uint16_t transmitter_available() const; + [[nodiscard]] std::uint16_t wg_switch_position() const; + [[nodiscard]] std::uint16_t wg_pfn_transfer_interlock() const; + [[nodiscard]] std::uint16_t maintenance_mode() const; + [[nodiscard]] std::uint16_t maintenance_required() const; + [[nodiscard]] std::uint16_t pfn_switch_position() const; + [[nodiscard]] std::uint16_t modulator_overload() const; + [[nodiscard]] std::uint16_t modulator_inv_current() const; + [[nodiscard]] std::uint16_t modulator_switch_fail() const; + [[nodiscard]] std::uint16_t main_power_voltage() const; + [[nodiscard]] std::uint16_t charging_system_fail() const; + [[nodiscard]] std::uint16_t inverse_diode_current() const; + [[nodiscard]] std::uint16_t trigger_amplifier() const; + [[nodiscard]] std::uint16_t circulator_temperature() const; + [[nodiscard]] std::uint16_t spectrum_filter_pressure() const; + [[nodiscard]] std::uint16_t wg_arc_vswr() const; + [[nodiscard]] std::uint16_t cabinet_interlock() const; + [[nodiscard]] std::uint16_t cabinet_air_temperature() const; + [[nodiscard]] std::uint16_t cabinet_airflow() const; + [[nodiscard]] std::uint16_t klystron_current() const; + [[nodiscard]] std::uint16_t klystron_filament_current() const; + [[nodiscard]] std::uint16_t klystron_vacion_current() const; + [[nodiscard]] std::uint16_t klystron_air_temperature() const; + [[nodiscard]] std::uint16_t klystron_airflow() const; + [[nodiscard]] std::uint16_t modulator_switch_maintenance() const; + [[nodiscard]] std::uint16_t post_charge_regulator_maintenance() const; + [[nodiscard]] std::uint16_t wg_pressure_humidity() const; + [[nodiscard]] std::uint16_t transmitter_overvoltage() const; + [[nodiscard]] std::uint16_t transmitter_overcurrent() const; + [[nodiscard]] std::uint16_t focus_coil_current() const; + [[nodiscard]] std::uint16_t focus_coil_airflow() const; + [[nodiscard]] std::uint16_t oil_temperature() const; + [[nodiscard]] std::uint16_t prf_limit() const; + [[nodiscard]] std::uint16_t transmitter_oil_level() const; + [[nodiscard]] std::uint16_t transmitter_battery_charging() const; + [[nodiscard]] std::uint16_t high_voltage_status() const; + [[nodiscard]] std::uint16_t transmitter_recycling_summary() const; + [[nodiscard]] std::uint16_t transmitter_inoperable() const; + [[nodiscard]] std::uint16_t transmitter_air_filter() const; + [[nodiscard]] std::uint16_t zero_test_bit(unsigned i) const; + [[nodiscard]] std::uint16_t one_test_bit(unsigned i) const; + [[nodiscard]] std::uint16_t xmtr_spip_interface() const; + [[nodiscard]] std::uint16_t transmitter_summary_status() const; + [[nodiscard]] float transmitter_rf_power() const; + [[nodiscard]] float horizontal_xmtr_peak_power() const; + [[nodiscard]] float xmtr_peak_power() const; + [[nodiscard]] float vertical_xmtr_peak_power() const; + [[nodiscard]] float xmtr_rf_avg_power() const; + [[nodiscard]] std::uint32_t xmtr_recycle_count() const; + [[nodiscard]] float receiver_bias() const; + [[nodiscard]] float transmit_imbalance() const; + [[nodiscard]] float xmtr_power_meter_zero() const; + [[nodiscard]] std::uint16_t ac_unit1_compressor_shut_off() const; + [[nodiscard]] std::uint16_t ac_unit2_compressor_shut_off() const; + [[nodiscard]] std::uint16_t generator_maintenance_required() const; + [[nodiscard]] std::uint16_t generator_battery_voltage() const; + [[nodiscard]] std::uint16_t generator_engine() const; + [[nodiscard]] std::uint16_t generator_volt_frequency() const; + [[nodiscard]] std::uint16_t power_source() const; + [[nodiscard]] std::uint16_t transitional_power_source() const; + [[nodiscard]] std::uint16_t generator_auto_run_off_switch() const; + [[nodiscard]] std::uint16_t aircraft_hazard_lighting() const; + [[nodiscard]] std::uint16_t equipment_shelter_fire_detection_system() const; + [[nodiscard]] std::uint16_t equipment_shelter_fire_smoke() const; + [[nodiscard]] std::uint16_t generator_shelter_fire_smoke() const; + [[nodiscard]] std::uint16_t utility_voltage_frequency() const; + [[nodiscard]] std::uint16_t site_security_alarm() const; + [[nodiscard]] std::uint16_t security_equipment() const; + [[nodiscard]] std::uint16_t security_system() const; + [[nodiscard]] std::uint16_t receiver_connected_to_antenna() const; + [[nodiscard]] std::uint16_t radome_hatch() const; + [[nodiscard]] std::uint16_t ac_unit1_filter_dirty() const; + [[nodiscard]] std::uint16_t ac_unit2_filter_dirty() const; + [[nodiscard]] float equipment_shelter_temperature() const; + [[nodiscard]] float outside_ambient_temperature() const; + [[nodiscard]] float transmitter_leaving_air_temp() const; + [[nodiscard]] float ac_unit1_discharge_air_temp() const; + [[nodiscard]] float generator_shelter_temperature() const; + [[nodiscard]] float radome_air_temperature() const; + [[nodiscard]] float ac_unit2_discharge_air_temp() const; + [[nodiscard]] float spip_15v_ps() const; + [[nodiscard]] float spip_neg_15v_ps() const; + [[nodiscard]] std::uint16_t spip_28v_ps_status() const; + [[nodiscard]] float spip_5v_ps() const; + [[nodiscard]] std::uint16_t converted_generator_fuel_level() const; + [[nodiscard]] std::uint16_t elevation_pos_dead_limit() const; + [[nodiscard]] std::uint16_t _150v_overvoltage() const; + [[nodiscard]] std::uint16_t _150v_undervoltage() const; + [[nodiscard]] std::uint16_t elevation_servo_amp_inhibit() const; + [[nodiscard]] std::uint16_t elevation_servo_amp_short_circuit() const; + [[nodiscard]] std::uint16_t elevation_servo_amp_overtemp() const; + [[nodiscard]] std::uint16_t elevation_motor_overtemp() const; + [[nodiscard]] std::uint16_t elevation_stow_pin() const; + [[nodiscard]] std::uint16_t elevation_housing_5v_ps() const; + [[nodiscard]] std::uint16_t elevation_neg_dead_limit() const; + [[nodiscard]] std::uint16_t elevation_pos_normal_limit() const; + [[nodiscard]] std::uint16_t elevation_neg_normal_limit() const; + [[nodiscard]] std::uint16_t elevation_encoder_light() const; + [[nodiscard]] std::uint16_t elevation_gearbox_oil() const; + [[nodiscard]] std::uint16_t elevation_handwheel() const; + [[nodiscard]] std::uint16_t elevation_amp_ps() const; + [[nodiscard]] std::uint16_t azimuth_servo_amp_inhibit() const; + [[nodiscard]] std::uint16_t azimuth_servo_amp_short_circuit() const; + [[nodiscard]] std::uint16_t azimuth_servo_amp_overtemp() const; + [[nodiscard]] std::uint16_t azimuth_motor_overtemp() const; + [[nodiscard]] std::uint16_t azimuth_stow_pin() const; + [[nodiscard]] std::uint16_t azimuth_housing_5v_ps() const; + [[nodiscard]] std::uint16_t azimuth_encoder_light() const; + [[nodiscard]] std::uint16_t azimuth_gearbox_oil() const; + [[nodiscard]] std::uint16_t azimuth_bull_gear_oil() const; + [[nodiscard]] std::uint16_t azimuth_handwheel() const; + [[nodiscard]] std::uint16_t azimuth_servo_amp_ps() const; + [[nodiscard]] std::uint16_t servo() const; + [[nodiscard]] std::uint16_t pedestal_interlock_switch() const; + [[nodiscard]] std::uint16_t coho_clock() const; + [[nodiscard]] std::uint16_t rf_generator_frequency_select_oscillator() const; + [[nodiscard]] std::uint16_t rf_generator_rf_stalo() const; + [[nodiscard]] std::uint16_t rf_generator_phase_shifted_coho() const; + [[nodiscard]] std::uint16_t _9v_receiver_ps() const; + [[nodiscard]] std::uint16_t _5v_receiver_ps() const; + [[nodiscard]] std::uint16_t _18v_receiver_ps() const; + [[nodiscard]] std::uint16_t neg_9v_receiver_ps() const; + [[nodiscard]] std::uint16_t _5v_single_channel_rdaiu_ps() const; + [[nodiscard]] float horizontal_short_pulse_noise() const; + [[nodiscard]] float horizontal_long_pulse_noise() const; + [[nodiscard]] float horizontal_noise_temperature() const; + [[nodiscard]] float vertical_short_pulse_noise() const; + [[nodiscard]] float vertical_long_pulse_noise() const; + [[nodiscard]] float vertical_noise_temperature() const; + [[nodiscard]] float horizontal_linearity() const; + [[nodiscard]] float horizontal_dynamic_range() const; + [[nodiscard]] float horizontal_delta_dbz0() const; + [[nodiscard]] float vertical_delta_dbz0() const; + [[nodiscard]] float kd_peak_measured() const; + [[nodiscard]] float short_pulse_horizontal_dbz0() const; + [[nodiscard]] float long_pulse_horizontal_dbz0() const; + [[nodiscard]] std::uint16_t velocity_processed() const; + [[nodiscard]] std::uint16_t width_processed() const; + [[nodiscard]] std::uint16_t velocity_rf_gen() const; + [[nodiscard]] std::uint16_t width_rf_gen() const; + [[nodiscard]] float horizontal_i0() const; + [[nodiscard]] float vertical_i0() const; + [[nodiscard]] float vertical_dynamic_range() const; + [[nodiscard]] float short_pulse_vertical_dbz0() const; + [[nodiscard]] float long_pulse_vertical_dbz0() const; + [[nodiscard]] float horizontal_power_sense() const; + [[nodiscard]] float vertical_power_sense() const; + [[nodiscard]] float zdr_offset() const; + [[nodiscard]] float clutter_suppression_delta() const; + [[nodiscard]] float clutter_suppression_unfiltered_power() const; + [[nodiscard]] float clutter_suppression_filtered_power() const; + [[nodiscard]] float vertical_linearity() const; + [[nodiscard]] std::uint16_t state_file_read_status() const; + [[nodiscard]] std::uint16_t state_file_write_status() const; + [[nodiscard]] std::uint16_t bypass_map_file_read_status() const; + [[nodiscard]] std::uint16_t bypass_map_file_write_status() const; + [[nodiscard]] std::uint16_t current_adaptation_file_read_status() const; + [[nodiscard]] std::uint16_t current_adaptation_file_write_status() const; + [[nodiscard]] std::uint16_t censor_zone_file_read_status() const; + [[nodiscard]] std::uint16_t censor_zone_file_write_status() const; + [[nodiscard]] std::uint16_t remote_vcp_file_read_status() const; + [[nodiscard]] std::uint16_t remote_vcp_file_write_status() const; + [[nodiscard]] std::uint16_t baseline_adaptation_file_read_status() const; + [[nodiscard]] std::uint16_t read_status_of_prf_sets() const; + [[nodiscard]] std::uint16_t clutter_filter_map_file_read_status() const; + [[nodiscard]] std::uint16_t clutter_filter_map_file_write_status() const; + [[nodiscard]] std::uint16_t general_disk_io_error() const; + [[nodiscard]] std::uint8_t rsp_status() const; + [[nodiscard]] std::uint8_t cpu1_temperature() const; + [[nodiscard]] std::uint8_t cpu2_temperature() const; + [[nodiscard]] std::uint16_t rsp_motherboard_power() const; + [[nodiscard]] std::uint16_t spip_comm_status() const; + [[nodiscard]] std::uint16_t hci_comm_status() const; + [[nodiscard]] std::uint16_t signal_processor_command_status() const; + [[nodiscard]] std::uint16_t ame_communication_status() const; + [[nodiscard]] std::uint16_t rms_link_status() const; + [[nodiscard]] std::uint16_t rpg_link_status() const; + [[nodiscard]] std::uint16_t interpanel_link_status() const; + [[nodiscard]] std::uint32_t performance_check_time() const; + [[nodiscard]] std::uint16_t version() const; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static std::shared_ptr Create(Level2MessageHeader&& header, std::istream& is); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp index be4f8067..60520e83 100644 --- a/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/rda_adaptation_data.hpp @@ -2,14 +2,8 @@ #include -namespace scwx +namespace scwx::wsr88d::rda { -namespace wsr88d -{ -namespace rda -{ - -class RdaAdaptationDataImpl; class RdaAdaptationData : public Level2Message { @@ -23,200 +17,199 @@ public: RdaAdaptationData(RdaAdaptationData&&) noexcept; RdaAdaptationData& operator=(RdaAdaptationData&&) noexcept; - std::string adap_file_name() const; - std::string adap_format() const; - std::string adap_revision() const; - std::string adap_date() const; - std::string adap_time() const; - float lower_pre_limit() const; - float az_lat() const; - float upper_pre_limit() const; - float el_lat() const; - float parkaz() const; - float parkel() const; - float a_fuel_conv(unsigned i) const; - float a_min_shelter_temp() const; - float a_max_shelter_temp() const; - float a_min_shelter_ac_temp_diff() const; - float a_max_xmtr_air_temp() const; - float a_max_rad_temp() const; - float a_max_rad_temp_rise() const; - float lower_dead_limit() const; - float upper_dead_limit() const; - float a_min_gen_room_temp() const; - float a_max_gen_room_temp() const; - float spip_5v_reg_lim() const; - float spip_15v_reg_lim() const; - bool rpg_co_located() const; - bool spec_filter_installed() const; - bool tps_installed() const; - bool rms_installed() const; - uint32_t a_hvdl_tst_int() const; - uint32_t a_rpg_lt_int() const; - uint32_t a_min_stab_util_pwr_time() const; - uint32_t a_gen_auto_exer_interval() const; - uint32_t a_util_pwr_sw_req_interval() const; - float a_low_fuel_level() const; - uint32_t config_chan_number() const; - uint32_t redundant_chan_config() const; - float atten_table(unsigned i) const; - float path_losses(unsigned i) const; - float h_coupler_xmt_loss() const; - float h_coupler_cw_loss() const; - float v_coupler_xmt_loss() const; - float ame_ts_bias() const; - float v_coupler_cw_loss() const; - float pwr_sense_bias() const; - float ame_v_noise_enr() const; - float chan_cal_diff() const; - float v_ts_cw() const; - float h_rnscale(unsigned i) const; - float atmos(unsigned i) const; - float el_index(unsigned i) const; - uint32_t tfreq_mhz() const; - float base_data_tcn() const; - float refl_data_tover() const; - float tar_h_dbz0_lp() const; - float tar_v_dbz0_lp() const; - uint32_t init_phi_dp() const; - uint32_t norm_init_phi_dp() const; - float lx_lp() const; - float lx_sp() const; - float meteor_param() const; - float antenna_gain() const; - float vel_degrad_limit() const; - float wth_degrad_limit() const; - float h_noisetemp_dgrad_limit() const; - uint32_t h_min_noisetemp() const; - float v_noisetemp_dgrad_limit() const; - uint32_t v_min_noisetemp() const; - float kly_degrade_limit() const; - float ts_coho() const; - float h_ts_cw() const; - float ts_stalo() const; - float ame_h_noise_enr() const; - float xmtr_peak_pwr_high_limit() const; - float xmtr_peak_pwr_low_limit() const; - float h_dbz0_delta_limit() const; - float threshold1() const; - float threshold2() const; - float clut_supp_dgrad_lim() const; - float range0_value() const; - float xmtr_pwr_mtr_scale() const; - float v_dbz0_delta_limit() const; - float tar_h_dbz0_sp() const; - float tar_v_dbz0_sp() const; - uint32_t deltaprf() const; - uint32_t tau_sp() const; - uint32_t tau_lp() const; - uint32_t nc_dead_value() const; - uint32_t tau_rf_sp() const; - uint32_t tau_rf_lp() const; - float seg1_lim() const; - float slatsec() const; - float slonsec() const; - uint32_t slatdeg() const; - uint32_t slatmin() const; - uint32_t slondeg() const; - uint32_t slonmin() const; - char slatdir() const; - char slondir() const; - double dig_rcvr_clock_freq() const; - double coho_freq() const; - float az_correction_factor() const; - float el_correction_factor() const; - std::string site_name() const; - float ant_manual_setup_ielmin() const; - float ant_manual_setup_ielmax() const; - uint32_t ant_manual_setup_fazvelmax() const; - uint32_t ant_manual_setup_felvelmax() const; - int32_t ant_manual_setup_ignd_hgt() const; - uint32_t ant_manual_setup_irad_hgt() const; - float az_pos_sustain_drive() const; - float az_neg_sustain_drive() const; - float az_nom_pos_drive_slope() const; - float az_nom_neg_drive_slope() const; - float az_feedback_slope() const; - float el_pos_sustain_drive() const; - float el_neg_sustain_drive() const; - float el_nom_pos_drive_slope() const; - float el_nom_neg_drive_slope() const; - float el_feedback_slope() const; - float el_first_slope() const; - float el_second_slope() const; - float el_third_slope() const; - float el_droop_pos() const; - float el_off_neutral_drive() const; - float az_intertia() const; - float el_inertia() const; - float az_stow_angle() const; - float el_stow_angle() const; - float az_encoder_alignment() const; - float el_encoder_alignment() const; - std::string refined_park() const; - uint32_t rvp8nv_iwaveguide_length() const; - float v_rnscale(unsigned i) const; - float vel_data_tover() const; - float width_data_tover() const; - float doppler_range_start() const; - uint32_t max_el_index() const; - float seg2_lim() const; - float seg3_lim() const; - float seg4_lim() const; - uint32_t nbr_el_segments() const; - float h_noise_long() const; - float ant_noise_temp() const; - float h_noise_short() const; - float h_noise_tolerance() const; - float min_h_dyn_range() const; - bool gen_installed() const; - bool gen_exercise() const; - float v_noise_tolerance() const; - float min_v_dyn_range() const; - float zdr_offset_dgrad_lim() const; - float baseline_zdr_offset() const; - float v_noise_long() const; - float v_noise_short() const; - float zdr_data_tover() const; - float phi_data_tover() const; - float rho_data_tover() const; - float stalo_power_dgrad_limit() const; - float stalo_power_maint_limit() const; - float min_h_pwr_sense() const; - float min_v_pwr_sense() const; - float h_pwr_sense_offset() const; - float v_pwr_sense_offset() const; - float ps_gain_ref() const; - float rf_pallet_broad_loss() const; - float ame_ps_tolerance() const; - float ame_max_temp() const; - float ame_min_temp() const; - float rcvr_mod_max_temp() const; - float rcvr_mod_min_temp() const; - float bite_mod_max_temp() const; - float bite_mod_min_temp() const; - uint32_t default_polarization() const; - float tr_limit_dgrad_limit() const; - float tr_limit_fail_limit() const; - bool rfp_stepper_enabled() const; - float ame_current_tolerance() const; - uint32_t h_only_polarization() const; - uint32_t v_only_polarization() const; - float sun_bias() const; - float a_min_shelter_temp_warn() const; - float power_meter_zero() const; - float txb_baseline() const; - float txb_alarm_thresh() const; + [[nodiscard]] std::string adap_file_name() const; + [[nodiscard]] std::string adap_format() const; + [[nodiscard]] std::string adap_revision() const; + [[nodiscard]] std::string adap_date() const; + [[nodiscard]] std::string adap_time() const; + [[nodiscard]] float lower_pre_limit() const; + [[nodiscard]] float az_lat() const; + [[nodiscard]] float upper_pre_limit() const; + [[nodiscard]] float el_lat() const; + [[nodiscard]] float parkaz() const; + [[nodiscard]] float parkel() const; + [[nodiscard]] float a_fuel_conv(unsigned i) const; + [[nodiscard]] float a_min_shelter_temp() const; + [[nodiscard]] float a_max_shelter_temp() const; + [[nodiscard]] float a_min_shelter_ac_temp_diff() const; + [[nodiscard]] float a_max_xmtr_air_temp() const; + [[nodiscard]] float a_max_rad_temp() const; + [[nodiscard]] float a_max_rad_temp_rise() const; + [[nodiscard]] float lower_dead_limit() const; + [[nodiscard]] float upper_dead_limit() const; + [[nodiscard]] float a_min_gen_room_temp() const; + [[nodiscard]] float a_max_gen_room_temp() const; + [[nodiscard]] float spip_5v_reg_lim() const; + [[nodiscard]] float spip_15v_reg_lim() const; + [[nodiscard]] bool rpg_co_located() const; + [[nodiscard]] bool spec_filter_installed() const; + [[nodiscard]] bool tps_installed() const; + [[nodiscard]] bool rms_installed() const; + [[nodiscard]] std::uint32_t a_hvdl_tst_int() const; + [[nodiscard]] std::uint32_t a_rpg_lt_int() const; + [[nodiscard]] std::uint32_t a_min_stab_util_pwr_time() const; + [[nodiscard]] std::uint32_t a_gen_auto_exer_interval() const; + [[nodiscard]] std::uint32_t a_util_pwr_sw_req_interval() const; + [[nodiscard]] float a_low_fuel_level() const; + [[nodiscard]] std::uint32_t config_chan_number() const; + [[nodiscard]] std::uint32_t redundant_chan_config() const; + [[nodiscard]] float atten_table(unsigned i) const; + [[nodiscard]] float path_losses(unsigned i) const; + [[nodiscard]] float h_coupler_xmt_loss() const; + [[nodiscard]] float h_coupler_cw_loss() const; + [[nodiscard]] float v_coupler_xmt_loss() const; + [[nodiscard]] float ame_ts_bias() const; + [[nodiscard]] float v_coupler_cw_loss() const; + [[nodiscard]] float pwr_sense_bias() const; + [[nodiscard]] float ame_v_noise_enr() const; + [[nodiscard]] float chan_cal_diff() const; + [[nodiscard]] float v_ts_cw() const; + [[nodiscard]] float h_rnscale(unsigned i) const; + [[nodiscard]] float atmos(unsigned i) const; + [[nodiscard]] float el_index(unsigned i) const; + [[nodiscard]] std::uint32_t tfreq_mhz() const; + [[nodiscard]] float base_data_tcn() const; + [[nodiscard]] float refl_data_tover() const; + [[nodiscard]] float tar_h_dbz0_lp() const; + [[nodiscard]] float tar_v_dbz0_lp() const; + [[nodiscard]] std::uint32_t init_phi_dp() const; + [[nodiscard]] std::uint32_t norm_init_phi_dp() const; + [[nodiscard]] float lx_lp() const; + [[nodiscard]] float lx_sp() const; + [[nodiscard]] float meteor_param() const; + [[nodiscard]] float antenna_gain() const; + [[nodiscard]] float vel_degrad_limit() const; + [[nodiscard]] float wth_degrad_limit() const; + [[nodiscard]] float h_noisetemp_dgrad_limit() const; + [[nodiscard]] std::uint32_t h_min_noisetemp() const; + [[nodiscard]] float v_noisetemp_dgrad_limit() const; + [[nodiscard]] std::uint32_t v_min_noisetemp() const; + [[nodiscard]] float kly_degrade_limit() const; + [[nodiscard]] float ts_coho() const; + [[nodiscard]] float h_ts_cw() const; + [[nodiscard]] float ts_stalo() const; + [[nodiscard]] float ame_h_noise_enr() const; + [[nodiscard]] float xmtr_peak_pwr_high_limit() const; + [[nodiscard]] float xmtr_peak_pwr_low_limit() const; + [[nodiscard]] float h_dbz0_delta_limit() const; + [[nodiscard]] float threshold1() const; + [[nodiscard]] float threshold2() const; + [[nodiscard]] float clut_supp_dgrad_lim() const; + [[nodiscard]] float range0_value() const; + [[nodiscard]] float xmtr_pwr_mtr_scale() const; + [[nodiscard]] float v_dbz0_delta_limit() const; + [[nodiscard]] float tar_h_dbz0_sp() const; + [[nodiscard]] float tar_v_dbz0_sp() const; + [[nodiscard]] std::uint32_t deltaprf() const; + [[nodiscard]] std::uint32_t tau_sp() const; + [[nodiscard]] std::uint32_t tau_lp() const; + [[nodiscard]] std::uint32_t nc_dead_value() const; + [[nodiscard]] std::uint32_t tau_rf_sp() const; + [[nodiscard]] std::uint32_t tau_rf_lp() const; + [[nodiscard]] float seg1_lim() const; + [[nodiscard]] float slatsec() const; + [[nodiscard]] float slonsec() const; + [[nodiscard]] std::uint32_t slatdeg() const; + [[nodiscard]] std::uint32_t slatmin() const; + [[nodiscard]] std::uint32_t slondeg() const; + [[nodiscard]] std::uint32_t slonmin() const; + [[nodiscard]] char slatdir() const; + [[nodiscard]] char slondir() const; + [[nodiscard]] double dig_rcvr_clock_freq() const; + [[nodiscard]] double coho_freq() const; + [[nodiscard]] float az_correction_factor() const; + [[nodiscard]] float el_correction_factor() const; + [[nodiscard]] std::string site_name() const; + [[nodiscard]] float ant_manual_setup_ielmin() const; + [[nodiscard]] float ant_manual_setup_ielmax() const; + [[nodiscard]] std::uint32_t ant_manual_setup_fazvelmax() const; + [[nodiscard]] std::uint32_t ant_manual_setup_felvelmax() const; + [[nodiscard]] std::int32_t ant_manual_setup_ignd_hgt() const; + [[nodiscard]] std::uint32_t ant_manual_setup_irad_hgt() const; + [[nodiscard]] float az_pos_sustain_drive() const; + [[nodiscard]] float az_neg_sustain_drive() const; + [[nodiscard]] float az_nom_pos_drive_slope() const; + [[nodiscard]] float az_nom_neg_drive_slope() const; + [[nodiscard]] float az_feedback_slope() const; + [[nodiscard]] float el_pos_sustain_drive() const; + [[nodiscard]] float el_neg_sustain_drive() const; + [[nodiscard]] float el_nom_pos_drive_slope() const; + [[nodiscard]] float el_nom_neg_drive_slope() const; + [[nodiscard]] float el_feedback_slope() const; + [[nodiscard]] float el_first_slope() const; + [[nodiscard]] float el_second_slope() const; + [[nodiscard]] float el_third_slope() const; + [[nodiscard]] float el_droop_pos() const; + [[nodiscard]] float el_off_neutral_drive() const; + [[nodiscard]] float az_intertia() const; + [[nodiscard]] float el_inertia() const; + [[nodiscard]] float az_stow_angle() const; + [[nodiscard]] float el_stow_angle() const; + [[nodiscard]] float az_encoder_alignment() const; + [[nodiscard]] float el_encoder_alignment() const; + [[nodiscard]] std::string refined_park() const; + [[nodiscard]] std::uint32_t rvp8nv_iwaveguide_length() const; + [[nodiscard]] float v_rnscale(unsigned i) const; + [[nodiscard]] float vel_data_tover() const; + [[nodiscard]] float width_data_tover() const; + [[nodiscard]] float doppler_range_start() const; + [[nodiscard]] std::uint32_t max_el_index() const; + [[nodiscard]] float seg2_lim() const; + [[nodiscard]] float seg3_lim() const; + [[nodiscard]] float seg4_lim() const; + [[nodiscard]] std::uint32_t nbr_el_segments() const; + [[nodiscard]] float h_noise_long() const; + [[nodiscard]] float ant_noise_temp() const; + [[nodiscard]] float h_noise_short() const; + [[nodiscard]] float h_noise_tolerance() const; + [[nodiscard]] float min_h_dyn_range() const; + [[nodiscard]] bool gen_installed() const; + [[nodiscard]] bool gen_exercise() const; + [[nodiscard]] float v_noise_tolerance() const; + [[nodiscard]] float min_v_dyn_range() const; + [[nodiscard]] float zdr_offset_dgrad_lim() const; + [[nodiscard]] float baseline_zdr_offset() const; + [[nodiscard]] float v_noise_long() const; + [[nodiscard]] float v_noise_short() const; + [[nodiscard]] float zdr_data_tover() const; + [[nodiscard]] float phi_data_tover() const; + [[nodiscard]] float rho_data_tover() const; + [[nodiscard]] float stalo_power_dgrad_limit() const; + [[nodiscard]] float stalo_power_maint_limit() const; + [[nodiscard]] float min_h_pwr_sense() const; + [[nodiscard]] float min_v_pwr_sense() const; + [[nodiscard]] float h_pwr_sense_offset() const; + [[nodiscard]] float v_pwr_sense_offset() const; + [[nodiscard]] float ps_gain_ref() const; + [[nodiscard]] float rf_pallet_broad_loss() const; + [[nodiscard]] float ame_ps_tolerance() const; + [[nodiscard]] float ame_max_temp() const; + [[nodiscard]] float ame_min_temp() const; + [[nodiscard]] float rcvr_mod_max_temp() const; + [[nodiscard]] float rcvr_mod_min_temp() const; + [[nodiscard]] float bite_mod_max_temp() const; + [[nodiscard]] float bite_mod_min_temp() const; + [[nodiscard]] std::uint32_t default_polarization() const; + [[nodiscard]] float tr_limit_dgrad_limit() const; + [[nodiscard]] float tr_limit_fail_limit() const; + [[nodiscard]] bool rfp_stepper_enabled() const; + [[nodiscard]] float ame_current_tolerance() const; + [[nodiscard]] std::uint32_t h_only_polarization() const; + [[nodiscard]] std::uint32_t v_only_polarization() const; + [[nodiscard]] float sun_bias() const; + [[nodiscard]] float a_min_shelter_temp_warn() const; + [[nodiscard]] float power_meter_zero() const; + [[nodiscard]] float txb_baseline() const; + [[nodiscard]] float txb_alarm_thresh() const; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static std::shared_ptr Create(Level2MessageHeader&& header, std::istream& is); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp index a4e7702a..fcd937ec 100644 --- a/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/rda_status_data.hpp @@ -2,14 +2,8 @@ #include -namespace scwx +namespace scwx::wsr88d::rda { -namespace wsr88d -{ -namespace rda -{ - -class RdaStatusDataImpl; class RdaStatusData : public Level2Message { @@ -23,46 +17,45 @@ public: RdaStatusData(RdaStatusData&&) noexcept; RdaStatusData& operator=(RdaStatusData&&) noexcept; - uint16_t rda_status() const; - uint16_t operability_status() const; - uint16_t control_status() const; - uint16_t auxiliary_power_generator_state() const; - uint16_t average_transmitter_power() const; - float horizontal_reflectivity_calibration_correction() const; - uint16_t data_transmission_enabled() const; - uint16_t volume_coverage_pattern_number() const; - uint16_t rda_control_authorization() const; - uint16_t rda_build_number() const; - uint16_t operational_mode() const; - uint16_t super_resolution_status() const; - uint16_t clutter_mitigation_decision_status() const; - uint16_t rda_scan_and_data_flags() const; - uint16_t rda_alarm_summary() const; - uint16_t command_acknowledgement() const; - uint16_t channel_control_status() const; - uint16_t spot_blanking_status() const; - uint16_t bypass_map_generation_date() const; - uint16_t bypass_map_generation_time() const; - uint16_t clutter_filter_map_generation_date() const; - uint16_t clutter_filter_map_generation_time() const; - float vertical_reflectivity_calibration_correction() const; - uint16_t transition_power_source_status() const; - uint16_t rms_control_status() const; - uint16_t performance_check_status() const; - uint16_t alarm_codes(unsigned i) const; - uint16_t signal_processing_options() const; - uint16_t downloaded_pattern_number() const; - uint16_t status_version() const; + [[nodiscard]] std::uint16_t rda_status() const; + [[nodiscard]] std::uint16_t operability_status() const; + [[nodiscard]] std::uint16_t control_status() const; + [[nodiscard]] std::uint16_t auxiliary_power_generator_state() const; + [[nodiscard]] std::uint16_t average_transmitter_power() const; + [[nodiscard]] float horizontal_reflectivity_calibration_correction() const; + [[nodiscard]] std::uint16_t data_transmission_enabled() const; + [[nodiscard]] std::uint16_t volume_coverage_pattern_number() const; + [[nodiscard]] std::uint16_t rda_control_authorization() const; + [[nodiscard]] std::uint16_t rda_build_number() const; + [[nodiscard]] std::uint16_t operational_mode() const; + [[nodiscard]] std::uint16_t super_resolution_status() const; + [[nodiscard]] std::uint16_t clutter_mitigation_decision_status() const; + [[nodiscard]] std::uint16_t rda_scan_and_data_flags() const; + [[nodiscard]] std::uint16_t rda_alarm_summary() const; + [[nodiscard]] std::uint16_t command_acknowledgement() const; + [[nodiscard]] std::uint16_t channel_control_status() const; + [[nodiscard]] std::uint16_t spot_blanking_status() const; + [[nodiscard]] std::uint16_t bypass_map_generation_date() const; + [[nodiscard]] std::uint16_t bypass_map_generation_time() const; + [[nodiscard]] std::uint16_t clutter_filter_map_generation_date() const; + [[nodiscard]] std::uint16_t clutter_filter_map_generation_time() const; + [[nodiscard]] float vertical_reflectivity_calibration_correction() const; + [[nodiscard]] std::uint16_t transition_power_source_status() const; + [[nodiscard]] std::uint16_t rms_control_status() const; + [[nodiscard]] std::uint16_t performance_check_status() const; + [[nodiscard]] std::uint16_t alarm_codes(unsigned i) const; + [[nodiscard]] std::uint16_t signal_processing_options() const; + [[nodiscard]] std::uint16_t downloaded_pattern_number() const; + [[nodiscard]] std::uint16_t status_version() const; - bool Parse(std::istream& is); + bool Parse(std::istream& is) override; static std::shared_ptr Create(Level2MessageHeader&& header, std::istream& is); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/awips/message.cpp b/wxdata/source/scwx/awips/message.cpp index 73ae88a2..f4dbe9b7 100644 --- a/wxdata/source/scwx/awips/message.cpp +++ b/wxdata/source/scwx/awips/message.cpp @@ -9,17 +9,22 @@ namespace awips static const std::string logPrefix_ = "scwx::awips::message"; static const auto logger_ = util::Logger::Create(logPrefix_); -class MessageImpl +class Message::Impl { public: - explicit MessageImpl() {}; - ~MessageImpl() = default; + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; }; -Message::Message() : p(std::make_unique()) {} +Message::Message() : p(std::make_unique()) {} Message::~Message() = default; -Message::Message(Message&&) noexcept = default; +Message::Message(Message&&) noexcept = default; Message& Message::operator=(Message&&) noexcept = default; bool Message::ValidateMessage(std::istream& is, size_t bytesRead) const diff --git a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp index 79705a70..4004fd07 100644 --- a/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp +++ b/wxdata/source/scwx/wsr88d/rda/digital_radar_data_generic.cpp @@ -1,11 +1,7 @@ #include #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { static const std::string logPrefix_ = @@ -27,9 +23,9 @@ static const std::unordered_map strToDataBlock_ { class DigitalRadarDataGeneric::DataBlock::Impl { public: - explicit Impl(const std::string& dataBlockType, - const std::string& dataName) : - dataBlockType_ {dataBlockType}, dataName_ {dataName} + explicit Impl(std::string dataBlockType, std::string dataName) : + dataBlockType_ {std::move(dataBlockType)}, + dataName_ {std::move(dataName)} { } @@ -51,7 +47,13 @@ DigitalRadarDataGeneric::DataBlock::operator=(DataBlock&&) noexcept = default; class DigitalRadarDataGeneric::MomentDataBlock::Impl { public: - explicit Impl() {} + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::uint16_t numberOfDataMomentGates_ {0}; std::int16_t dataMomentRange_ {0}; @@ -89,7 +91,9 @@ DigitalRadarDataGeneric::MomentDataBlock::number_of_data_moment_gates() const units::kilometers DigitalRadarDataGeneric::MomentDataBlock::data_moment_range() const { - return units::kilometers {p->dataMomentRange_ * 0.001f}; + static constexpr float kScale_ = 0.001f; + return units::kilometers {static_cast(p->dataMomentRange_) * + kScale_}; } std::int16_t @@ -102,7 +106,9 @@ units::kilometers DigitalRadarDataGeneric::MomentDataBlock::data_moment_range_sample_interval() const { - return units::kilometers {p->dataMomentRangeSampleInterval_ * 0.001f}; + static constexpr float kScale_ = 0.001f; + return units::kilometers { + static_cast(p->dataMomentRangeSampleInterval_) * kScale_}; } std::uint16_t DigitalRadarDataGeneric::MomentDataBlock:: @@ -113,7 +119,8 @@ std::uint16_t DigitalRadarDataGeneric::MomentDataBlock:: float DigitalRadarDataGeneric::MomentDataBlock::snr_threshold() const { - return p->snrThreshold_ * 0.1f; + static constexpr float kScale_ = 0.1f; + return static_cast(p->snrThreshold_) * kScale_; } std::int16_t DigitalRadarDataGeneric::MomentDataBlock::snr_threshold_raw() const @@ -138,14 +145,14 @@ float DigitalRadarDataGeneric::MomentDataBlock::offset() const const void* DigitalRadarDataGeneric::MomentDataBlock::data_moments() const { - const void* dataMoments; + const void* dataMoments = nullptr; switch (p->dataWordSize_) { - case 8: + case 8: // NOLINT(cppcoreguidelines-avoid-magic-numbers) dataMoments = p->momentGates8_.data(); break; - case 16: + case 16: // NOLINT(cppcoreguidelines-avoid-magic-numbers) dataMoments = p->momentGates16_.data(); break; default: @@ -189,13 +196,15 @@ bool DigitalRadarDataGeneric::MomentDataBlock::Parse(std::istream& is) is.read(reinterpret_cast(&p->scale_), 4); // 20-23 is.read(reinterpret_cast(&p->offset_), 4); // 24-27 - p->numberOfDataMomentGates_ = ntohs(p->numberOfDataMomentGates_); - p->dataMomentRange_ = ntohs(p->dataMomentRange_); + p->numberOfDataMomentGates_ = ntohs(p->numberOfDataMomentGates_); + p->dataMomentRange_ = static_cast(ntohs(p->dataMomentRange_)); p->dataMomentRangeSampleInterval_ = ntohs(p->dataMomentRangeSampleInterval_); p->tover_ = ntohs(p->tover_); - p->snrThreshold_ = ntohs(p->snrThreshold_); - p->scale_ = awips::Message::SwapFloat(p->scale_); - p->offset_ = awips::Message::SwapFloat(p->offset_); + p->snrThreshold_ = static_cast(ntohs(p->snrThreshold_)); + p->scale_ = awips::Message::SwapFloat(p->scale_); + p->offset_ = awips::Message::SwapFloat(p->offset_); + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) if (p->numberOfDataMomentGates_ <= 1840) { @@ -209,7 +218,7 @@ bool DigitalRadarDataGeneric::MomentDataBlock::Parse(std::istream& is) { p->momentGates16_.resize(p->numberOfDataMomentGates_); is.read(reinterpret_cast(p->momentGates16_.data()), - p->numberOfDataMomentGates_ * 2); + static_cast(p->numberOfDataMomentGates_) * 2); awips::Message::SwapVector(p->momentGates16_); } else @@ -225,13 +234,21 @@ bool DigitalRadarDataGeneric::MomentDataBlock::Parse(std::istream& is) dataBlockValid = false; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return dataBlockValid; } class DigitalRadarDataGeneric::VolumeDataBlock::Impl { public: - explicit Impl() {} + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::uint16_t lrtup_ {0}; std::uint8_t versionNumberMajor_ {0}; @@ -321,7 +338,7 @@ bool DigitalRadarDataGeneric::VolumeDataBlock::Parse(std::istream& is) p->lrtup_ = ntohs(p->lrtup_); p->latitude_ = awips::Message::SwapFloat(p->latitude_); p->longitude_ = awips::Message::SwapFloat(p->longitude_); - p->siteHeight_ = ntohs(p->siteHeight_); + p->siteHeight_ = static_cast(ntohs(p->siteHeight_)); p->feedhornHeight_ = ntohs(p->feedhornHeight_); p->calibrationConstant_ = awips::Message::SwapFloat(p->calibrationConstant_); p->horizontaShvTxPower_ = awips::Message::SwapFloat(p->horizontaShvTxPower_); @@ -333,6 +350,8 @@ bool DigitalRadarDataGeneric::VolumeDataBlock::Parse(std::istream& is) p->volumeCoveragePatternNumber_ = ntohs(p->volumeCoveragePatternNumber_); p->processingStatus_ = ntohs(p->processingStatus_); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (p->lrtup_ >= 46) { is.read(reinterpret_cast(&p->zdrBiasEstimateWeightedMean_), @@ -345,13 +364,21 @@ bool DigitalRadarDataGeneric::VolumeDataBlock::Parse(std::istream& is) is.seekg(6, std::ios_base::cur); // 46-51 } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return dataBlockValid; } class DigitalRadarDataGeneric::ElevationDataBlock::Impl { public: - explicit Impl() {} + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::uint16_t lrtup_ {0}; std::int16_t atmos_ {0}; @@ -397,7 +424,7 @@ bool DigitalRadarDataGeneric::ElevationDataBlock::Parse(std::istream& is) is.read(reinterpret_cast(&p->calibrationConstant_), 4); // 8-11 p->lrtup_ = ntohs(p->lrtup_); - p->atmos_ = ntohs(p->atmos_); + p->atmos_ = static_cast(ntohs(p->atmos_)); p->calibrationConstant_ = awips::Message::SwapFloat(p->calibrationConstant_); return dataBlockValid; @@ -406,7 +433,13 @@ bool DigitalRadarDataGeneric::ElevationDataBlock::Parse(std::istream& is) class DigitalRadarDataGeneric::RadialDataBlock::Impl { public: - explicit Impl() {} + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::uint16_t lrtup_ {0}; std::uint16_t unambigiousRange_ {0}; @@ -433,7 +466,8 @@ DigitalRadarDataGeneric::RadialDataBlock::operator=( float DigitalRadarDataGeneric::RadialDataBlock::unambiguous_range() const { - return p->unambigiousRange_ / 10.0f; + static constexpr float kScale_ = 0.1f; + return static_cast(p->unambigiousRange_) * kScale_; } std::shared_ptr @@ -486,24 +520,31 @@ bool DigitalRadarDataGeneric::RadialDataBlock::Parse(std::istream& is) class DigitalRadarDataGeneric::Impl { public: - explicit Impl() {}; - ~Impl() = default; + explicit Impl() = default; + ~Impl() = default; - std::string radarIdentifier_ {}; - std::uint32_t collectionTime_ {0}; - std::uint16_t modifiedJulianDate_ {0}; - std::uint16_t azimuthNumber_ {0}; - float azimuthAngle_ {0.0f}; - std::uint8_t compressionIndicator_ {0}; - std::uint16_t radialLength_ {0}; - std::uint8_t azimuthResolutionSpacing_ {0}; - std::uint8_t radialStatus_ {0}; - std::uint8_t elevationNumber_ {0}; - std::uint8_t cutSectorNumber_ {0}; - float elevationAngle_ {0.0f}; - std::uint8_t radialSpotBlankingStatus_ {0}; - std::uint8_t azimuthIndexingMode_ {0}; - std::uint16_t dataBlockCount_ {0}; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + std::string radarIdentifier_ {}; + std::uint32_t collectionTime_ {0}; + std::uint16_t modifiedJulianDate_ {0}; + std::uint16_t azimuthNumber_ {0}; + float azimuthAngle_ {0.0f}; + std::uint8_t compressionIndicator_ {0}; + std::uint16_t radialLength_ {0}; + std::uint8_t azimuthResolutionSpacing_ {0}; + std::uint8_t radialStatus_ {0}; + std::uint8_t elevationNumber_ {0}; + std::uint8_t cutSectorNumber_ {0}; + float elevationAngle_ {0.0f}; + std::uint8_t radialSpotBlankingStatus_ {0}; + std::uint8_t azimuthIndexingMode_ {0}; + std::uint16_t dataBlockCount_ {0}; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) std::array dataBlockPointer_ {0}; std::shared_ptr volumeDataBlock_ {nullptr}; @@ -679,6 +720,8 @@ bool DigitalRadarDataGeneric::Parse(std::istream& is) p->elevationAngle_ = SwapFloat(p->elevationAngle_); p->dataBlockCount_ = ntohs(p->dataBlockCount_); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (p->azimuthNumber_ < 1 || p->azimuthNumber_ > 720) { logger_->warn("Invalid azimuth number: {}", p->azimuthNumber_); @@ -700,18 +743,22 @@ bool DigitalRadarDataGeneric::Parse(std::istream& is) messageValid = false; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + if (!messageValid) { p->dataBlockCount_ = 0; } is.read(reinterpret_cast(&p->dataBlockPointer_), - p->dataBlockCount_ * 4); + static_cast(p->dataBlockCount_) * 4); SwapArray(p->dataBlockPointer_, p->dataBlockCount_); for (uint16_t b = 0; b < p->dataBlockCount_; ++b) { + // Index already has bounds check + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index) is.seekg(isBegin + std::streamoff(p->dataBlockPointer_[b]), std::ios_base::beg); @@ -784,6 +831,4 @@ DigitalRadarDataGeneric::Create(Level2MessageHeader&& header, std::istream& is) return message; } -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp b/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp index fcdd19c8..647221e6 100644 --- a/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/performance_maintenance_data.cpp @@ -3,295 +3,300 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { static const std::string logPrefix_ = "scwx::wsr88d::rda::performance_maintenance_data"; static const auto logger_ = util::Logger::Create(logPrefix_); -class PerformanceMaintenanceDataImpl +class PerformanceMaintenanceData::Impl { public: - explicit PerformanceMaintenanceDataImpl() = default; - ~PerformanceMaintenanceDataImpl() = default; + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; // Communications - uint16_t loopBackSetStatus_ {0}; - uint32_t t1OutputFrames_ {0}; - uint32_t t1InputFrames_ {0}; - uint32_t routerMemoryUsed_ {0}; - uint32_t routerMemoryFree_ {0}; - uint16_t routerMemoryUtilization_ {0}; - uint16_t routeToRpg_ {0}; - uint16_t t1PortStatus_ {0}; - uint16_t routerDedicatedEthernetPortStatus_ {0}; - uint16_t routerCommercialEthernetPortStatus_ {0}; - uint32_t csu24HrErroredSeconds_ {0}; - uint32_t csu24HrSeverelyErroredSeconds_ {0}; - uint32_t csu24HrSeverelyErroredFramingSeconds_ {0}; - uint32_t csu24HrUnavailableSeconds_ {0}; - uint32_t csu24HrControlledSlipSeconds_ {0}; - uint32_t csu24HrPathCodingViolations_ {0}; - uint32_t csu24HrLineErroredSeconds_ {0}; - uint32_t csu24HrBurstyErroredSeconds_ {0}; - uint32_t csu24HrDegradedMinutes_ {0}; - uint32_t lanSwitchCpuUtilization_ {0}; - uint16_t lanSwitchMemoryUtilization_ {0}; - uint16_t ifdrChasisTemperature_ {0}; - uint16_t ifdrFpgaTemperature_ {0}; - uint16_t ntpStatus_ {0}; - uint16_t ipcStatus_ {0}; - uint16_t commandedChannelControl_ {0}; + std::uint16_t loopBackSetStatus_ {0}; + std::uint32_t t1OutputFrames_ {0}; + std::uint32_t t1InputFrames_ {0}; + std::uint32_t routerMemoryUsed_ {0}; + std::uint32_t routerMemoryFree_ {0}; + std::uint16_t routerMemoryUtilization_ {0}; + std::uint16_t routeToRpg_ {0}; + std::uint16_t t1PortStatus_ {0}; + std::uint16_t routerDedicatedEthernetPortStatus_ {0}; + std::uint16_t routerCommercialEthernetPortStatus_ {0}; + std::uint32_t csu24HrErroredSeconds_ {0}; + std::uint32_t csu24HrSeverelyErroredSeconds_ {0}; + std::uint32_t csu24HrSeverelyErroredFramingSeconds_ {0}; + std::uint32_t csu24HrUnavailableSeconds_ {0}; + std::uint32_t csu24HrControlledSlipSeconds_ {0}; + std::uint32_t csu24HrPathCodingViolations_ {0}; + std::uint32_t csu24HrLineErroredSeconds_ {0}; + std::uint32_t csu24HrBurstyErroredSeconds_ {0}; + std::uint32_t csu24HrDegradedMinutes_ {0}; + std::uint32_t lanSwitchCpuUtilization_ {0}; + std::uint16_t lanSwitchMemoryUtilization_ {0}; + std::uint16_t ifdrChasisTemperature_ {0}; + std::uint16_t ifdrFpgaTemperature_ {0}; + std::uint16_t ntpStatus_ {0}; + std::uint16_t ipcStatus_ {0}; + std::uint16_t commandedChannelControl_ {0}; // AME - uint16_t polarization_ {0}; - float ameInternalTemperature_ {0.0f}; - float ameReceiverModuleTemperature_ {0.0f}; - float ameBiteCalModuleTemperature_ {0.0f}; - uint16_t amePeltierPulseWidthModulation_ {0}; - uint16_t amePeltierStatus_ {0}; - uint16_t ameADConverterStatus_ {0}; - uint16_t ameState_ {0}; - float ame3_3VPsVoltage_ {0.0f}; - float ame5VPsVoltage_ {0.0f}; - float ame6_5VPsVoltage_ {0.0f}; - float ame15VPsVoltage_ {0.0f}; - float ame48VPsVoltage_ {0.0f}; - float ameStaloPower_ {0.0f}; - float peltierCurrent_ {0.0f}; - float adcCalibrationReferenceVoltage_ {0.0f}; - uint16_t ameMode_ {0}; - uint16_t amePeltierMode_ {0}; - float amePeltierInsideFanCurrent_ {0.0f}; - float amePeltierOutsideFanCurrent_ {0.0f}; - float horizontalTrLimiterVoltage_ {0.0f}; - float verticalTrLimiterVoltage_ {0.0f}; - float adcCalibrationOffsetVoltage_ {0.0f}; - float adcCalibrationGainCorrection_ {0.0f}; + std::uint16_t polarization_ {0}; + float ameInternalTemperature_ {0.0f}; + float ameReceiverModuleTemperature_ {0.0f}; + float ameBiteCalModuleTemperature_ {0.0f}; + std::uint16_t amePeltierPulseWidthModulation_ {0}; + std::uint16_t amePeltierStatus_ {0}; + std::uint16_t ameADConverterStatus_ {0}; + std::uint16_t ameState_ {0}; + float ame3_3VPsVoltage_ {0.0f}; + float ame5VPsVoltage_ {0.0f}; + float ame6_5VPsVoltage_ {0.0f}; + float ame15VPsVoltage_ {0.0f}; + float ame48VPsVoltage_ {0.0f}; + float ameStaloPower_ {0.0f}; + float peltierCurrent_ {0.0f}; + float adcCalibrationReferenceVoltage_ {0.0f}; + std::uint16_t ameMode_ {0}; + std::uint16_t amePeltierMode_ {0}; + float amePeltierInsideFanCurrent_ {0.0f}; + float amePeltierOutsideFanCurrent_ {0.0f}; + float horizontalTrLimiterVoltage_ {0.0f}; + float verticalTrLimiterVoltage_ {0.0f}; + float adcCalibrationOffsetVoltage_ {0.0f}; + float adcCalibrationGainCorrection_ {0.0f}; // RCP/SPIP Power Button Status - uint16_t rcpStatus_ {0}; - std::string rcpString_ {}; - uint16_t spipPowerButtons_ {0}; + std::uint16_t rcpStatus_ {0}; + std::string rcpString_ {}; + std::uint16_t spipPowerButtons_ {0}; // Power float masterPowerAdministratorLoad_ {0.0f}; float expansionPowerAdministratorLoad_ {0.0f}; // Transmitter - uint16_t _5VdcPs_ {0}; - uint16_t _15VdcPs_ {0}; - uint16_t _28VdcPs_ {0}; - uint16_t neg15VdcPs_ {0}; - uint16_t _45VdcPs_ {0}; - uint16_t filamentPsVoltage_ {0}; - uint16_t vacuumPumpPsVoltage_ {0}; - uint16_t focusCoilPsVoltage_ {0}; - uint16_t filamentPs_ {0}; - uint16_t klystronWarmup_ {0}; - uint16_t transmitterAvailable_ {0}; - uint16_t wgSwitchPosition_ {0}; - uint16_t wgPfnTransferInterlock_ {0}; - uint16_t maintenanceMode_ {0}; - uint16_t maintenanceRequired_ {0}; - uint16_t pfnSwitchPosition_ {0}; - uint16_t modulatorOverload_ {0}; - uint16_t modulatorInvCurrent_ {0}; - uint16_t modulatorSwitchFail_ {0}; - uint16_t mainPowerVoltage_ {0}; - uint16_t chargingSystemFail_ {0}; - uint16_t inverseDiodeCurrent_ {0}; - uint16_t triggerAmplifier_ {0}; - uint16_t circulatorTemperature_ {0}; - uint16_t spectrumFilterPressure_ {0}; - uint16_t wgArcVswr_ {0}; - uint16_t cabinetInterlock_ {0}; - uint16_t cabinetAirTemperature_ {0}; - uint16_t cabinetAirflow_ {0}; - uint16_t klystronCurrent_ {0}; - uint16_t klystronFilamentCurrent_ {0}; - uint16_t klystronVacionCurrent_ {0}; - uint16_t klystronAirTemperature_ {0}; - uint16_t klystronAirflow_ {0}; - uint16_t modulatorSwitchMaintenance_ {0}; - uint16_t postChargeRegulatorMaintenance_ {0}; - uint16_t wgPressureHumidity_ {0}; - uint16_t transmitterOvervoltage_ {0}; - uint16_t transmitterOvercurrent_ {0}; - uint16_t focusCoilCurrent_ {0}; - uint16_t focusCoilAirflow_ {0}; - uint16_t oilTemperature_ {0}; - uint16_t prfLimit_ {0}; - uint16_t transmitterOilLevel_ {0}; - uint16_t transmitterBatteryCharging_ {0}; - uint16_t highVoltageStatus_ {0}; - uint16_t transmitterRecyclingSummary_ {0}; - uint16_t transmitterInoperable_ {0}; - uint16_t transmitterAirFilter_ {0}; - std::array zeroTestBit_ {0}; - std::array oneTestBit_ {0}; - uint16_t xmtrSpipInterface_ {0}; - uint16_t transmitterSummaryStatus_ {0}; - float transmitterRfPower_ {0.0f}; - float horizontalXmtrPeakPower_ {0.0f}; - float xmtrPeakPower_ {0.0f}; - float verticalXmtrPeakPower_ {0.0f}; - float xmtrRfAvgPower_ {0.0f}; - uint32_t xmtrRecycleCount_ {0}; - float receiverBias_ {0.0f}; - float transmitImbalance_ {0.0f}; - float xmtrPowerMeterZero_ {0.0f}; + std::uint16_t _5VdcPs_ {0}; + std::uint16_t _15VdcPs_ {0}; + std::uint16_t _28VdcPs_ {0}; + std::uint16_t neg15VdcPs_ {0}; + std::uint16_t _45VdcPs_ {0}; + std::uint16_t filamentPsVoltage_ {0}; + std::uint16_t vacuumPumpPsVoltage_ {0}; + std::uint16_t focusCoilPsVoltage_ {0}; + std::uint16_t filamentPs_ {0}; + std::uint16_t klystronWarmup_ {0}; + std::uint16_t transmitterAvailable_ {0}; + std::uint16_t wgSwitchPosition_ {0}; + std::uint16_t wgPfnTransferInterlock_ {0}; + std::uint16_t maintenanceMode_ {0}; + std::uint16_t maintenanceRequired_ {0}; + std::uint16_t pfnSwitchPosition_ {0}; + std::uint16_t modulatorOverload_ {0}; + std::uint16_t modulatorInvCurrent_ {0}; + std::uint16_t modulatorSwitchFail_ {0}; + std::uint16_t mainPowerVoltage_ {0}; + std::uint16_t chargingSystemFail_ {0}; + std::uint16_t inverseDiodeCurrent_ {0}; + std::uint16_t triggerAmplifier_ {0}; + std::uint16_t circulatorTemperature_ {0}; + std::uint16_t spectrumFilterPressure_ {0}; + std::uint16_t wgArcVswr_ {0}; + std::uint16_t cabinetInterlock_ {0}; + std::uint16_t cabinetAirTemperature_ {0}; + std::uint16_t cabinetAirflow_ {0}; + std::uint16_t klystronCurrent_ {0}; + std::uint16_t klystronFilamentCurrent_ {0}; + std::uint16_t klystronVacionCurrent_ {0}; + std::uint16_t klystronAirTemperature_ {0}; + std::uint16_t klystronAirflow_ {0}; + std::uint16_t modulatorSwitchMaintenance_ {0}; + std::uint16_t postChargeRegulatorMaintenance_ {0}; + std::uint16_t wgPressureHumidity_ {0}; + std::uint16_t transmitterOvervoltage_ {0}; + std::uint16_t transmitterOvercurrent_ {0}; + std::uint16_t focusCoilCurrent_ {0}; + std::uint16_t focusCoilAirflow_ {0}; + std::uint16_t oilTemperature_ {0}; + std::uint16_t prfLimit_ {0}; + std::uint16_t transmitterOilLevel_ {0}; + std::uint16_t transmitterBatteryCharging_ {0}; + std::uint16_t highVoltageStatus_ {0}; + std::uint16_t transmitterRecyclingSummary_ {0}; + std::uint16_t transmitterInoperable_ {0}; + std::uint16_t transmitterAirFilter_ {0}; + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + std::array zeroTestBit_ {0}; + std::array oneTestBit_ {0}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + std::uint16_t xmtrSpipInterface_ {0}; + std::uint16_t transmitterSummaryStatus_ {0}; + float transmitterRfPower_ {0.0f}; + float horizontalXmtrPeakPower_ {0.0f}; + float xmtrPeakPower_ {0.0f}; + float verticalXmtrPeakPower_ {0.0f}; + float xmtrRfAvgPower_ {0.0f}; + std::uint32_t xmtrRecycleCount_ {0}; + float receiverBias_ {0.0f}; + float transmitImbalance_ {0.0f}; + float xmtrPowerMeterZero_ {0.0f}; // Tower/Utilities - uint16_t acUnit1CompressorShutOff_ {0}; - uint16_t acUnit2CompressorShutOff_ {0}; - uint16_t generatorMaintenanceRequired_ {0}; - uint16_t generatorBatteryVoltage_ {0}; - uint16_t generatorEngine_ {0}; - uint16_t generatorVoltFrequency_ {0}; - uint16_t powerSource_ {0}; - uint16_t transitionalPowerSource_ {0}; - uint16_t generatorAutoRunOffSwitch_ {0}; - uint16_t aircraftHazardLighting_ {0}; + std::uint16_t acUnit1CompressorShutOff_ {0}; + std::uint16_t acUnit2CompressorShutOff_ {0}; + std::uint16_t generatorMaintenanceRequired_ {0}; + std::uint16_t generatorBatteryVoltage_ {0}; + std::uint16_t generatorEngine_ {0}; + std::uint16_t generatorVoltFrequency_ {0}; + std::uint16_t powerSource_ {0}; + std::uint16_t transitionalPowerSource_ {0}; + std::uint16_t generatorAutoRunOffSwitch_ {0}; + std::uint16_t aircraftHazardLighting_ {0}; // Equipment Shelter - uint16_t equipmentShelterFireDetectionSystem_ {0}; - uint16_t equipmentShelterFireSmoke_ {0}; - uint16_t generatorShelterFireSmoke_ {0}; - uint16_t utilityVoltageFrequency_ {0}; - uint16_t siteSecurityAlarm_ {0}; - uint16_t securityEquipment_ {0}; - uint16_t securitySystem_ {0}; - uint16_t receiverConnectedToAntenna_ {0}; - uint16_t radomeHatch_ {0}; - uint16_t acUnit1FilterDirty_ {0}; - uint16_t acUnit2FilterDirty_ {0}; - float equipmentShelterTemperature_ {0.0f}; - float outsideAmbientTemperature_ {0.0f}; - float transmitterLeavingAirTemp_ {0.0f}; - float acUnit1DischargeAirTemp_ {0.0f}; - float generatorShelterTemperature_ {0.0f}; - float radomeAirTemperature_ {0.0f}; - float acUnit2DischargeAirTemp_ {0.0f}; - float spip15VPs_ {0.0f}; - float spipNeg15VPs_ {0.0f}; - uint16_t spip28VPsStatus_ {0}; - float spip5VPs_ {0.0f}; - uint16_t convertedGeneratorFuelLevel_ {0}; + std::uint16_t equipmentShelterFireDetectionSystem_ {0}; + std::uint16_t equipmentShelterFireSmoke_ {0}; + std::uint16_t generatorShelterFireSmoke_ {0}; + std::uint16_t utilityVoltageFrequency_ {0}; + std::uint16_t siteSecurityAlarm_ {0}; + std::uint16_t securityEquipment_ {0}; + std::uint16_t securitySystem_ {0}; + std::uint16_t receiverConnectedToAntenna_ {0}; + std::uint16_t radomeHatch_ {0}; + std::uint16_t acUnit1FilterDirty_ {0}; + std::uint16_t acUnit2FilterDirty_ {0}; + float equipmentShelterTemperature_ {0.0f}; + float outsideAmbientTemperature_ {0.0f}; + float transmitterLeavingAirTemp_ {0.0f}; + float acUnit1DischargeAirTemp_ {0.0f}; + float generatorShelterTemperature_ {0.0f}; + float radomeAirTemperature_ {0.0f}; + float acUnit2DischargeAirTemp_ {0.0f}; + float spip15VPs_ {0.0f}; + float spipNeg15VPs_ {0.0f}; + std::uint16_t spip28VPsStatus_ {0}; + float spip5VPs_ {0.0f}; + std::uint16_t convertedGeneratorFuelLevel_ {0}; // Antenna/Pedestal - uint16_t elevationPosDeadLimit_ {0}; - uint16_t _150VOvervoltage_ {0}; - uint16_t _150VUndervoltage_ {0}; - uint16_t elevationServoAmpInhibit_ {0}; - uint16_t elevationServoAmpShortCircuit_ {0}; - uint16_t elevationServoAmpOvertemp_ {0}; - uint16_t elevationMotorOvertemp_ {0}; - uint16_t elevationStowPin_ {0}; - uint16_t elevationHousing5VPs_ {0}; - uint16_t elevationNegDeadLimit_ {0}; - uint16_t elevationPosNormalLimit_ {0}; - uint16_t elevationNegNormalLimit_ {0}; - uint16_t elevationEncoderLight_ {0}; - uint16_t elevationGearboxOil_ {0}; - uint16_t elevationHandwheel_ {0}; - uint16_t elevationAmpPs_ {0}; - uint16_t azimuthServoAmpInhibit_ {0}; - uint16_t azimuthServoAmpShortCircuit_ {0}; - uint16_t azimuthServoAmpOvertemp_ {0}; - uint16_t azimuthMotorOvertemp_ {0}; - uint16_t azimuthStowPin_ {0}; - uint16_t azimuthHousing5VPs_ {0}; - uint16_t azimuthEncoderLight_ {0}; - uint16_t azimuthGearboxOil_ {0}; - uint16_t azimuthBullGearOil_ {0}; - uint16_t azimuthHandwheel_ {0}; - uint16_t azimuthServoAmpPs_ {0}; - uint16_t servo_ {0}; - uint16_t pedestalInterlockSwitch_ {0}; + std::uint16_t elevationPosDeadLimit_ {0}; + std::uint16_t _150VOvervoltage_ {0}; + std::uint16_t _150VUndervoltage_ {0}; + std::uint16_t elevationServoAmpInhibit_ {0}; + std::uint16_t elevationServoAmpShortCircuit_ {0}; + std::uint16_t elevationServoAmpOvertemp_ {0}; + std::uint16_t elevationMotorOvertemp_ {0}; + std::uint16_t elevationStowPin_ {0}; + std::uint16_t elevationHousing5VPs_ {0}; + std::uint16_t elevationNegDeadLimit_ {0}; + std::uint16_t elevationPosNormalLimit_ {0}; + std::uint16_t elevationNegNormalLimit_ {0}; + std::uint16_t elevationEncoderLight_ {0}; + std::uint16_t elevationGearboxOil_ {0}; + std::uint16_t elevationHandwheel_ {0}; + std::uint16_t elevationAmpPs_ {0}; + std::uint16_t azimuthServoAmpInhibit_ {0}; + std::uint16_t azimuthServoAmpShortCircuit_ {0}; + std::uint16_t azimuthServoAmpOvertemp_ {0}; + std::uint16_t azimuthMotorOvertemp_ {0}; + std::uint16_t azimuthStowPin_ {0}; + std::uint16_t azimuthHousing5VPs_ {0}; + std::uint16_t azimuthEncoderLight_ {0}; + std::uint16_t azimuthGearboxOil_ {0}; + std::uint16_t azimuthBullGearOil_ {0}; + std::uint16_t azimuthHandwheel_ {0}; + std::uint16_t azimuthServoAmpPs_ {0}; + std::uint16_t servo_ {0}; + std::uint16_t pedestalInterlockSwitch_ {0}; // RF Generator/Receiver - uint16_t cohoClock_ {0}; - uint16_t rfGeneratorFrequencySelectOscillator_ {0}; - uint16_t rfGeneratorRfStalo_ {0}; - uint16_t rfGeneratorPhaseShiftedCoho_ {0}; - uint16_t _9VReceiverPs_ {0}; - uint16_t _5VReceiverPs_ {0}; - uint16_t _18VReceiverPs_ {0}; - uint16_t neg9VReceiverPs_ {0}; - uint16_t _5VSingleChannelRdaiuPs_ {0}; - float horizontalShortPulseNoise_ {0.0f}; - float horizontalLongPulseNoise_ {0.0f}; - float horizontalNoiseTemperature_ {0.0f}; - float verticalShortPulseNoise_ {0.0f}; - float verticalLongPulseNoise_ {0.0f}; - float verticalNoiseTemperature_ {0.0f}; + std::uint16_t cohoClock_ {0}; + std::uint16_t rfGeneratorFrequencySelectOscillator_ {0}; + std::uint16_t rfGeneratorRfStalo_ {0}; + std::uint16_t rfGeneratorPhaseShiftedCoho_ {0}; + std::uint16_t _9VReceiverPs_ {0}; + std::uint16_t _5VReceiverPs_ {0}; + std::uint16_t _18VReceiverPs_ {0}; + std::uint16_t neg9VReceiverPs_ {0}; + std::uint16_t _5VSingleChannelRdaiuPs_ {0}; + float horizontalShortPulseNoise_ {0.0f}; + float horizontalLongPulseNoise_ {0.0f}; + float horizontalNoiseTemperature_ {0.0f}; + float verticalShortPulseNoise_ {0.0f}; + float verticalLongPulseNoise_ {0.0f}; + float verticalNoiseTemperature_ {0.0f}; // Calibration - float horizontalLinearity_ {0.0f}; - float horizontalDynamicRange_ {0.0f}; - float horizontalDeltaDbz0_ {0.0f}; - float verticalDeltaDbz0_ {0.0f}; - float kdPeakMeasured_ {0.0f}; - float shortPulseHorizontalDbz0_ {0.0f}; - float longPulseHorizontalDbz0_ {0.0f}; - uint16_t velocityProcessed_ {0}; - uint16_t widthProcessed_ {0}; - uint16_t velocityRfGen_ {0}; - uint16_t widthRfGen_ {0}; - float horizontalI0_ {0.0f}; - float verticalI0_ {0.0f}; - float verticalDynamicRange_ {0.0f}; - float shortPulseVerticalDbz0_ {0.0f}; - float longPulseVerticalDbz0_ {0.0f}; - float horizontalPowerSense_ {0.0f}; - float verticalPowerSense_ {0.0f}; - float zdrOffset_ {0.0f}; - float clutterSuppressionDelta_ {0.0f}; - float clutterSuppressionUnfilteredPower_ {0.0f}; - float clutterSuppressionFilteredPower_ {0.0f}; - float verticalLinearity_ {0.0f}; + float horizontalLinearity_ {0.0f}; + float horizontalDynamicRange_ {0.0f}; + float horizontalDeltaDbz0_ {0.0f}; + float verticalDeltaDbz0_ {0.0f}; + float kdPeakMeasured_ {0.0f}; + float shortPulseHorizontalDbz0_ {0.0f}; + float longPulseHorizontalDbz0_ {0.0f}; + std::uint16_t velocityProcessed_ {0}; + std::uint16_t widthProcessed_ {0}; + std::uint16_t velocityRfGen_ {0}; + std::uint16_t widthRfGen_ {0}; + float horizontalI0_ {0.0f}; + float verticalI0_ {0.0f}; + float verticalDynamicRange_ {0.0f}; + float shortPulseVerticalDbz0_ {0.0f}; + float longPulseVerticalDbz0_ {0.0f}; + float horizontalPowerSense_ {0.0f}; + float verticalPowerSense_ {0.0f}; + float zdrOffset_ {0.0f}; + float clutterSuppressionDelta_ {0.0f}; + float clutterSuppressionUnfilteredPower_ {0.0f}; + float clutterSuppressionFilteredPower_ {0.0f}; + float verticalLinearity_ {0.0f}; // File Status - uint16_t stateFileReadStatus_ {0}; - uint16_t stateFileWriteStatus_ {0}; - uint16_t bypassMapFileReadStatus_ {0}; - uint16_t bypassMapFileWriteStatus_ {0}; - uint16_t currentAdaptationFileReadStatus_ {0}; - uint16_t currentAdaptationFileWriteStatus_ {0}; - uint16_t censorZoneFileReadStatus_ {0}; - uint16_t censorZoneFileWriteStatus_ {0}; - uint16_t remoteVcpFileReadStatus_ {0}; - uint16_t remoteVcpFileWriteStatus_ {0}; - uint16_t baselineAdaptationFileReadStatus_ {0}; - uint16_t readStatusOfPrfSets_ {0}; - uint16_t clutterFilterMapFileReadStatus_ {0}; - uint16_t clutterFilterMapFileWriteStatus_ {0}; - uint16_t generalDiskIoError_ {0}; - uint8_t rspStatus_ {0}; - uint8_t cpu1Temperature_ {0}; - uint8_t cpu2Temperature_ {0}; - uint16_t rspMotherboardPower_ {0}; + std::uint16_t stateFileReadStatus_ {0}; + std::uint16_t stateFileWriteStatus_ {0}; + std::uint16_t bypassMapFileReadStatus_ {0}; + std::uint16_t bypassMapFileWriteStatus_ {0}; + std::uint16_t currentAdaptationFileReadStatus_ {0}; + std::uint16_t currentAdaptationFileWriteStatus_ {0}; + std::uint16_t censorZoneFileReadStatus_ {0}; + std::uint16_t censorZoneFileWriteStatus_ {0}; + std::uint16_t remoteVcpFileReadStatus_ {0}; + std::uint16_t remoteVcpFileWriteStatus_ {0}; + std::uint16_t baselineAdaptationFileReadStatus_ {0}; + std::uint16_t readStatusOfPrfSets_ {0}; + std::uint16_t clutterFilterMapFileReadStatus_ {0}; + std::uint16_t clutterFilterMapFileWriteStatus_ {0}; + std::uint16_t generalDiskIoError_ {0}; + std::uint8_t rspStatus_ {0}; + std::uint8_t cpu1Temperature_ {0}; + std::uint8_t cpu2Temperature_ {0}; + std::uint16_t rspMotherboardPower_ {0}; // Device Status - uint16_t spipCommStatus_ {0}; - uint16_t hciCommStatus_ {0}; - uint16_t signalProcessorCommandStatus_ {0}; - uint16_t ameCommunicationStatus_ {0}; - uint16_t rmsLinkStatus_ {0}; - uint16_t rpgLinkStatus_ {0}; - uint16_t interpanelLinkStatus_ {0}; - uint32_t performanceCheckTime_ {0}; - uint16_t version_ {0}; + std::uint16_t spipCommStatus_ {0}; + std::uint16_t hciCommStatus_ {0}; + std::uint16_t signalProcessorCommandStatus_ {0}; + std::uint16_t ameCommunicationStatus_ {0}; + std::uint16_t rmsLinkStatus_ {0}; + std::uint16_t rpgLinkStatus_ {0}; + std::uint16_t interpanelLinkStatus_ {0}; + std::uint32_t performanceCheckTime_ {0}; + std::uint16_t version_ {0}; }; PerformanceMaintenanceData::PerformanceMaintenanceData() : - Level2Message(), p(std::make_unique()) + Level2Message(), p(std::make_unique()) { } PerformanceMaintenanceData::~PerformanceMaintenanceData() = default; @@ -301,140 +306,144 @@ PerformanceMaintenanceData::PerformanceMaintenanceData( PerformanceMaintenanceData& PerformanceMaintenanceData::operator=( PerformanceMaintenanceData&&) noexcept = default; -uint16_t PerformanceMaintenanceData::loop_back_set_status() const +std::uint16_t PerformanceMaintenanceData::loop_back_set_status() const { return p->loopBackSetStatus_; } -uint32_t PerformanceMaintenanceData::t1_output_frames() const +std::uint32_t PerformanceMaintenanceData::t1_output_frames() const { return p->t1OutputFrames_; } -uint32_t PerformanceMaintenanceData::t1_input_frames() const +std::uint32_t PerformanceMaintenanceData::t1_input_frames() const { return p->t1InputFrames_; } -uint32_t PerformanceMaintenanceData::router_memory_used() const +std::uint32_t PerformanceMaintenanceData::router_memory_used() const { return p->routerMemoryUsed_; } -uint32_t PerformanceMaintenanceData::router_memory_free() const +std::uint32_t PerformanceMaintenanceData::router_memory_free() const { return p->routerMemoryFree_; } -uint16_t PerformanceMaintenanceData::router_memory_utilization() const +std::uint16_t PerformanceMaintenanceData::router_memory_utilization() const { return p->routerMemoryUtilization_; } -uint16_t PerformanceMaintenanceData::route_to_rpg() const +std::uint16_t PerformanceMaintenanceData::route_to_rpg() const { return p->routeToRpg_; } -uint16_t PerformanceMaintenanceData::t1_port_status() const +std::uint16_t PerformanceMaintenanceData::t1_port_status() const { return p->t1PortStatus_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::router_dedicated_ethernet_port_status() const { return p->routerDedicatedEthernetPortStatus_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::router_commercial_ethernet_port_status() const { return p->routerCommercialEthernetPortStatus_; } -uint32_t PerformanceMaintenanceData::csu_24hr_errored_seconds() const +std::uint32_t PerformanceMaintenanceData::csu_24hr_errored_seconds() const { return p->csu24HrErroredSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_severely_errored_seconds() const +std::uint32_t +PerformanceMaintenanceData::csu_24hr_severely_errored_seconds() const { return p->csu24HrSeverelyErroredSeconds_; } -uint32_t +std::uint32_t PerformanceMaintenanceData::csu_24hr_severely_errored_framing_seconds() const { return p->csu24HrSeverelyErroredFramingSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_unavailable_seconds() const +std::uint32_t PerformanceMaintenanceData::csu_24hr_unavailable_seconds() const { return p->csu24HrUnavailableSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_controlled_slip_seconds() const +std::uint32_t +PerformanceMaintenanceData::csu_24hr_controlled_slip_seconds() const { return p->csu24HrControlledSlipSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_path_coding_violations() const +std::uint32_t +PerformanceMaintenanceData::csu_24hr_path_coding_violations() const { return p->csu24HrPathCodingViolations_; } -uint32_t PerformanceMaintenanceData::csu_24hr_line_errored_seconds() const +std::uint32_t PerformanceMaintenanceData::csu_24hr_line_errored_seconds() const { return p->csu24HrLineErroredSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_bursty_errored_seconds() const +std::uint32_t +PerformanceMaintenanceData::csu_24hr_bursty_errored_seconds() const { return p->csu24HrBurstyErroredSeconds_; } -uint32_t PerformanceMaintenanceData::csu_24hr_degraded_minutes() const +std::uint32_t PerformanceMaintenanceData::csu_24hr_degraded_minutes() const { return p->csu24HrDegradedMinutes_; } -uint32_t PerformanceMaintenanceData::lan_switch_cpu_utilization() const +std::uint32_t PerformanceMaintenanceData::lan_switch_cpu_utilization() const { return p->lanSwitchCpuUtilization_; } -uint16_t PerformanceMaintenanceData::lan_switch_memory_utilization() const +std::uint16_t PerformanceMaintenanceData::lan_switch_memory_utilization() const { return p->lanSwitchMemoryUtilization_; } -uint16_t PerformanceMaintenanceData::ifdr_chasis_temperature() const +std::uint16_t PerformanceMaintenanceData::ifdr_chasis_temperature() const { return p->ifdrChasisTemperature_; } -uint16_t PerformanceMaintenanceData::ifdr_fpga_temperature() const +std::uint16_t PerformanceMaintenanceData::ifdr_fpga_temperature() const { return p->ifdrFpgaTemperature_; } -uint16_t PerformanceMaintenanceData::ntp_status() const +std::uint16_t PerformanceMaintenanceData::ntp_status() const { return p->ntpStatus_; } -uint16_t PerformanceMaintenanceData::ipc_status() const +std::uint16_t PerformanceMaintenanceData::ipc_status() const { return p->ipcStatus_; } -uint16_t PerformanceMaintenanceData::commanded_channel_control() const +std::uint16_t PerformanceMaintenanceData::commanded_channel_control() const { return p->commandedChannelControl_; } -uint16_t PerformanceMaintenanceData::polarization() const +std::uint16_t PerformanceMaintenanceData::polarization() const { return p->polarization_; } @@ -454,22 +463,23 @@ float PerformanceMaintenanceData::ame_bite_cal_module_temperature() const return p->ameBiteCalModuleTemperature_; } -uint16_t PerformanceMaintenanceData::ame_peltier_pulse_width_modulation() const +std::uint16_t +PerformanceMaintenanceData::ame_peltier_pulse_width_modulation() const { return p->amePeltierPulseWidthModulation_; } -uint16_t PerformanceMaintenanceData::ame_peltier_status() const +std::uint16_t PerformanceMaintenanceData::ame_peltier_status() const { return p->amePeltierStatus_; } -uint16_t PerformanceMaintenanceData::ame_a_d_converter_status() const +std::uint16_t PerformanceMaintenanceData::ame_a_d_converter_status() const { return p->ameADConverterStatus_; } -uint16_t PerformanceMaintenanceData::ame_state() const +std::uint16_t PerformanceMaintenanceData::ame_state() const { return p->ameState_; } @@ -514,12 +524,12 @@ float PerformanceMaintenanceData::adc_calibration_reference_voltage() const return p->adcCalibrationReferenceVoltage_; } -uint16_t PerformanceMaintenanceData::ame_mode() const +std::uint16_t PerformanceMaintenanceData::ame_mode() const { return p->ameMode_; } -uint16_t PerformanceMaintenanceData::ame_peltier_mode() const +std::uint16_t PerformanceMaintenanceData::ame_peltier_mode() const { return p->amePeltierMode_; } @@ -554,7 +564,7 @@ float PerformanceMaintenanceData::adc_calibration_gain_correction() const return p->adcCalibrationGainCorrection_; } -uint16_t PerformanceMaintenanceData::rcp_status() const +std::uint16_t PerformanceMaintenanceData::rcp_status() const { return p->rcpStatus_; } @@ -564,7 +574,7 @@ std::string PerformanceMaintenanceData::rcp_string() const return p->rcpString_; } -uint16_t PerformanceMaintenanceData::spip_power_buttons() const +std::uint16_t PerformanceMaintenanceData::spip_power_buttons() const { return p->spipPowerButtons_; } @@ -579,267 +589,268 @@ float PerformanceMaintenanceData::expansion_power_administrator_load() const return p->expansionPowerAdministratorLoad_; } -uint16_t PerformanceMaintenanceData::_5vdc_ps() const +std::uint16_t PerformanceMaintenanceData::_5vdc_ps() const { return p->_5VdcPs_; } -uint16_t PerformanceMaintenanceData::_15vdc_ps() const +std::uint16_t PerformanceMaintenanceData::_15vdc_ps() const { return p->_15VdcPs_; } -uint16_t PerformanceMaintenanceData::_28vdc_ps() const +std::uint16_t PerformanceMaintenanceData::_28vdc_ps() const { return p->_28VdcPs_; } -uint16_t PerformanceMaintenanceData::neg_15vdc_ps() const +std::uint16_t PerformanceMaintenanceData::neg_15vdc_ps() const { return p->neg15VdcPs_; } -uint16_t PerformanceMaintenanceData::_45vdc_ps() const +std::uint16_t PerformanceMaintenanceData::_45vdc_ps() const { return p->_45VdcPs_; } -uint16_t PerformanceMaintenanceData::filament_ps_voltage() const +std::uint16_t PerformanceMaintenanceData::filament_ps_voltage() const { return p->filamentPsVoltage_; } -uint16_t PerformanceMaintenanceData::vacuum_pump_ps_voltage() const +std::uint16_t PerformanceMaintenanceData::vacuum_pump_ps_voltage() const { return p->vacuumPumpPsVoltage_; } -uint16_t PerformanceMaintenanceData::focus_coil_ps_voltage() const +std::uint16_t PerformanceMaintenanceData::focus_coil_ps_voltage() const { return p->focusCoilPsVoltage_; } -uint16_t PerformanceMaintenanceData::filament_ps() const +std::uint16_t PerformanceMaintenanceData::filament_ps() const { return p->filamentPs_; } -uint16_t PerformanceMaintenanceData::klystron_warmup() const +std::uint16_t PerformanceMaintenanceData::klystron_warmup() const { return p->klystronWarmup_; } -uint16_t PerformanceMaintenanceData::transmitter_available() const +std::uint16_t PerformanceMaintenanceData::transmitter_available() const { return p->transmitterAvailable_; } -uint16_t PerformanceMaintenanceData::wg_switch_position() const +std::uint16_t PerformanceMaintenanceData::wg_switch_position() const { return p->wgSwitchPosition_; } -uint16_t PerformanceMaintenanceData::wg_pfn_transfer_interlock() const +std::uint16_t PerformanceMaintenanceData::wg_pfn_transfer_interlock() const { return p->wgPfnTransferInterlock_; } -uint16_t PerformanceMaintenanceData::maintenance_mode() const +std::uint16_t PerformanceMaintenanceData::maintenance_mode() const { return p->maintenanceMode_; } -uint16_t PerformanceMaintenanceData::maintenance_required() const +std::uint16_t PerformanceMaintenanceData::maintenance_required() const { return p->maintenanceRequired_; } -uint16_t PerformanceMaintenanceData::pfn_switch_position() const +std::uint16_t PerformanceMaintenanceData::pfn_switch_position() const { return p->pfnSwitchPosition_; } -uint16_t PerformanceMaintenanceData::modulator_overload() const +std::uint16_t PerformanceMaintenanceData::modulator_overload() const { return p->modulatorOverload_; } -uint16_t PerformanceMaintenanceData::modulator_inv_current() const +std::uint16_t PerformanceMaintenanceData::modulator_inv_current() const { return p->modulatorInvCurrent_; } -uint16_t PerformanceMaintenanceData::modulator_switch_fail() const +std::uint16_t PerformanceMaintenanceData::modulator_switch_fail() const { return p->modulatorSwitchFail_; } -uint16_t PerformanceMaintenanceData::main_power_voltage() const +std::uint16_t PerformanceMaintenanceData::main_power_voltage() const { return p->mainPowerVoltage_; } -uint16_t PerformanceMaintenanceData::charging_system_fail() const +std::uint16_t PerformanceMaintenanceData::charging_system_fail() const { return p->chargingSystemFail_; } -uint16_t PerformanceMaintenanceData::inverse_diode_current() const +std::uint16_t PerformanceMaintenanceData::inverse_diode_current() const { return p->inverseDiodeCurrent_; } -uint16_t PerformanceMaintenanceData::trigger_amplifier() const +std::uint16_t PerformanceMaintenanceData::trigger_amplifier() const { return p->triggerAmplifier_; } -uint16_t PerformanceMaintenanceData::circulator_temperature() const +std::uint16_t PerformanceMaintenanceData::circulator_temperature() const { return p->circulatorTemperature_; } -uint16_t PerformanceMaintenanceData::spectrum_filter_pressure() const +std::uint16_t PerformanceMaintenanceData::spectrum_filter_pressure() const { return p->spectrumFilterPressure_; } -uint16_t PerformanceMaintenanceData::wg_arc_vswr() const +std::uint16_t PerformanceMaintenanceData::wg_arc_vswr() const { return p->wgArcVswr_; } -uint16_t PerformanceMaintenanceData::cabinet_interlock() const +std::uint16_t PerformanceMaintenanceData::cabinet_interlock() const { return p->cabinetInterlock_; } -uint16_t PerformanceMaintenanceData::cabinet_air_temperature() const +std::uint16_t PerformanceMaintenanceData::cabinet_air_temperature() const { return p->cabinetAirTemperature_; } -uint16_t PerformanceMaintenanceData::cabinet_airflow() const +std::uint16_t PerformanceMaintenanceData::cabinet_airflow() const { return p->cabinetAirflow_; } -uint16_t PerformanceMaintenanceData::klystron_current() const +std::uint16_t PerformanceMaintenanceData::klystron_current() const { return p->klystronCurrent_; } -uint16_t PerformanceMaintenanceData::klystron_filament_current() const +std::uint16_t PerformanceMaintenanceData::klystron_filament_current() const { return p->klystronFilamentCurrent_; } -uint16_t PerformanceMaintenanceData::klystron_vacion_current() const +std::uint16_t PerformanceMaintenanceData::klystron_vacion_current() const { return p->klystronVacionCurrent_; } -uint16_t PerformanceMaintenanceData::klystron_air_temperature() const +std::uint16_t PerformanceMaintenanceData::klystron_air_temperature() const { return p->klystronAirTemperature_; } -uint16_t PerformanceMaintenanceData::klystron_airflow() const +std::uint16_t PerformanceMaintenanceData::klystron_airflow() const { return p->klystronAirflow_; } -uint16_t PerformanceMaintenanceData::modulator_switch_maintenance() const +std::uint16_t PerformanceMaintenanceData::modulator_switch_maintenance() const { return p->modulatorSwitchMaintenance_; } -uint16_t PerformanceMaintenanceData::post_charge_regulator_maintenance() const +std::uint16_t +PerformanceMaintenanceData::post_charge_regulator_maintenance() const { return p->postChargeRegulatorMaintenance_; } -uint16_t PerformanceMaintenanceData::wg_pressure_humidity() const +std::uint16_t PerformanceMaintenanceData::wg_pressure_humidity() const { return p->wgPressureHumidity_; } -uint16_t PerformanceMaintenanceData::transmitter_overvoltage() const +std::uint16_t PerformanceMaintenanceData::transmitter_overvoltage() const { return p->transmitterOvervoltage_; } -uint16_t PerformanceMaintenanceData::transmitter_overcurrent() const +std::uint16_t PerformanceMaintenanceData::transmitter_overcurrent() const { return p->transmitterOvercurrent_; } -uint16_t PerformanceMaintenanceData::focus_coil_current() const +std::uint16_t PerformanceMaintenanceData::focus_coil_current() const { return p->focusCoilCurrent_; } -uint16_t PerformanceMaintenanceData::focus_coil_airflow() const +std::uint16_t PerformanceMaintenanceData::focus_coil_airflow() const { return p->focusCoilAirflow_; } -uint16_t PerformanceMaintenanceData::oil_temperature() const +std::uint16_t PerformanceMaintenanceData::oil_temperature() const { return p->oilTemperature_; } -uint16_t PerformanceMaintenanceData::prf_limit() const +std::uint16_t PerformanceMaintenanceData::prf_limit() const { return p->prfLimit_; } -uint16_t PerformanceMaintenanceData::transmitter_oil_level() const +std::uint16_t PerformanceMaintenanceData::transmitter_oil_level() const { return p->transmitterOilLevel_; } -uint16_t PerformanceMaintenanceData::transmitter_battery_charging() const +std::uint16_t PerformanceMaintenanceData::transmitter_battery_charging() const { return p->transmitterBatteryCharging_; } -uint16_t PerformanceMaintenanceData::high_voltage_status() const +std::uint16_t PerformanceMaintenanceData::high_voltage_status() const { return p->highVoltageStatus_; } -uint16_t PerformanceMaintenanceData::transmitter_recycling_summary() const +std::uint16_t PerformanceMaintenanceData::transmitter_recycling_summary() const { return p->transmitterRecyclingSummary_; } -uint16_t PerformanceMaintenanceData::transmitter_inoperable() const +std::uint16_t PerformanceMaintenanceData::transmitter_inoperable() const { return p->transmitterInoperable_; } -uint16_t PerformanceMaintenanceData::transmitter_air_filter() const +std::uint16_t PerformanceMaintenanceData::transmitter_air_filter() const { return p->transmitterAirFilter_; } -uint16_t PerformanceMaintenanceData::zero_test_bit(unsigned i) const +std::uint16_t PerformanceMaintenanceData::zero_test_bit(unsigned i) const { - return p->zeroTestBit_[i]; + return p->zeroTestBit_.at(i); } -uint16_t PerformanceMaintenanceData::one_test_bit(unsigned i) const +std::uint16_t PerformanceMaintenanceData::one_test_bit(unsigned i) const { - return p->oneTestBit_[i]; + return p->oneTestBit_.at(i); } -uint16_t PerformanceMaintenanceData::xmtr_spip_interface() const +std::uint16_t PerformanceMaintenanceData::xmtr_spip_interface() const { return p->xmtrSpipInterface_; } -uint16_t PerformanceMaintenanceData::transmitter_summary_status() const +std::uint16_t PerformanceMaintenanceData::transmitter_summary_status() const { return p->transmitterSummaryStatus_; } @@ -869,7 +880,7 @@ float PerformanceMaintenanceData::xmtr_rf_avg_power() const return p->xmtrRfAvgPower_; } -uint32_t PerformanceMaintenanceData::xmtr_recycle_count() const +std::uint32_t PerformanceMaintenanceData::xmtr_recycle_count() const { return p->xmtrRecycleCount_; } @@ -889,108 +900,108 @@ float PerformanceMaintenanceData::xmtr_power_meter_zero() const return p->xmtrPowerMeterZero_; } -uint16_t PerformanceMaintenanceData::ac_unit1_compressor_shut_off() const +std::uint16_t PerformanceMaintenanceData::ac_unit1_compressor_shut_off() const { return p->acUnit1CompressorShutOff_; } -uint16_t PerformanceMaintenanceData::ac_unit2_compressor_shut_off() const +std::uint16_t PerformanceMaintenanceData::ac_unit2_compressor_shut_off() const { return p->acUnit2CompressorShutOff_; } -uint16_t PerformanceMaintenanceData::generator_maintenance_required() const +std::uint16_t PerformanceMaintenanceData::generator_maintenance_required() const { return p->generatorMaintenanceRequired_; } -uint16_t PerformanceMaintenanceData::generator_battery_voltage() const +std::uint16_t PerformanceMaintenanceData::generator_battery_voltage() const { return p->generatorBatteryVoltage_; } -uint16_t PerformanceMaintenanceData::generator_engine() const +std::uint16_t PerformanceMaintenanceData::generator_engine() const { return p->generatorEngine_; } -uint16_t PerformanceMaintenanceData::generator_volt_frequency() const +std::uint16_t PerformanceMaintenanceData::generator_volt_frequency() const { return p->generatorVoltFrequency_; } -uint16_t PerformanceMaintenanceData::power_source() const +std::uint16_t PerformanceMaintenanceData::power_source() const { return p->powerSource_; } -uint16_t PerformanceMaintenanceData::transitional_power_source() const +std::uint16_t PerformanceMaintenanceData::transitional_power_source() const { return p->transitionalPowerSource_; } -uint16_t PerformanceMaintenanceData::generator_auto_run_off_switch() const +std::uint16_t PerformanceMaintenanceData::generator_auto_run_off_switch() const { return p->generatorAutoRunOffSwitch_; } -uint16_t PerformanceMaintenanceData::aircraft_hazard_lighting() const +std::uint16_t PerformanceMaintenanceData::aircraft_hazard_lighting() const { return p->aircraftHazardLighting_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::equipment_shelter_fire_detection_system() const { return p->equipmentShelterFireDetectionSystem_; } -uint16_t PerformanceMaintenanceData::equipment_shelter_fire_smoke() const +std::uint16_t PerformanceMaintenanceData::equipment_shelter_fire_smoke() const { return p->equipmentShelterFireSmoke_; } -uint16_t PerformanceMaintenanceData::generator_shelter_fire_smoke() const +std::uint16_t PerformanceMaintenanceData::generator_shelter_fire_smoke() const { return p->generatorShelterFireSmoke_; } -uint16_t PerformanceMaintenanceData::utility_voltage_frequency() const +std::uint16_t PerformanceMaintenanceData::utility_voltage_frequency() const { return p->utilityVoltageFrequency_; } -uint16_t PerformanceMaintenanceData::site_security_alarm() const +std::uint16_t PerformanceMaintenanceData::site_security_alarm() const { return p->siteSecurityAlarm_; } -uint16_t PerformanceMaintenanceData::security_equipment() const +std::uint16_t PerformanceMaintenanceData::security_equipment() const { return p->securityEquipment_; } -uint16_t PerformanceMaintenanceData::security_system() const +std::uint16_t PerformanceMaintenanceData::security_system() const { return p->securitySystem_; } -uint16_t PerformanceMaintenanceData::receiver_connected_to_antenna() const +std::uint16_t PerformanceMaintenanceData::receiver_connected_to_antenna() const { return p->receiverConnectedToAntenna_; } -uint16_t PerformanceMaintenanceData::radome_hatch() const +std::uint16_t PerformanceMaintenanceData::radome_hatch() const { return p->radomeHatch_; } -uint16_t PerformanceMaintenanceData::ac_unit1_filter_dirty() const +std::uint16_t PerformanceMaintenanceData::ac_unit1_filter_dirty() const { return p->acUnit1FilterDirty_; } -uint16_t PerformanceMaintenanceData::ac_unit2_filter_dirty() const +std::uint16_t PerformanceMaintenanceData::ac_unit2_filter_dirty() const { return p->acUnit2FilterDirty_; } @@ -1040,7 +1051,7 @@ float PerformanceMaintenanceData::spip_neg_15v_ps() const return p->spipNeg15VPs_; } -uint16_t PerformanceMaintenanceData::spip_28v_ps_status() const +std::uint16_t PerformanceMaintenanceData::spip_28v_ps_status() const { return p->spip28VPsStatus_; } @@ -1050,198 +1061,201 @@ float PerformanceMaintenanceData::spip_5v_ps() const return p->spip5VPs_; } -uint16_t PerformanceMaintenanceData::converted_generator_fuel_level() const +std::uint16_t PerformanceMaintenanceData::converted_generator_fuel_level() const { return p->convertedGeneratorFuelLevel_; } -uint16_t PerformanceMaintenanceData::elevation_pos_dead_limit() const +std::uint16_t PerformanceMaintenanceData::elevation_pos_dead_limit() const { return p->elevationPosDeadLimit_; } -uint16_t PerformanceMaintenanceData::_150v_overvoltage() const +std::uint16_t PerformanceMaintenanceData::_150v_overvoltage() const { return p->_150VOvervoltage_; } -uint16_t PerformanceMaintenanceData::_150v_undervoltage() const +std::uint16_t PerformanceMaintenanceData::_150v_undervoltage() const { return p->_150VUndervoltage_; } -uint16_t PerformanceMaintenanceData::elevation_servo_amp_inhibit() const +std::uint16_t PerformanceMaintenanceData::elevation_servo_amp_inhibit() const { return p->elevationServoAmpInhibit_; } -uint16_t PerformanceMaintenanceData::elevation_servo_amp_short_circuit() const +std::uint16_t +PerformanceMaintenanceData::elevation_servo_amp_short_circuit() const { return p->elevationServoAmpShortCircuit_; } -uint16_t PerformanceMaintenanceData::elevation_servo_amp_overtemp() const +std::uint16_t PerformanceMaintenanceData::elevation_servo_amp_overtemp() const { return p->elevationServoAmpOvertemp_; } -uint16_t PerformanceMaintenanceData::elevation_motor_overtemp() const +std::uint16_t PerformanceMaintenanceData::elevation_motor_overtemp() const { return p->elevationMotorOvertemp_; } -uint16_t PerformanceMaintenanceData::elevation_stow_pin() const +std::uint16_t PerformanceMaintenanceData::elevation_stow_pin() const { return p->elevationStowPin_; } -uint16_t PerformanceMaintenanceData::elevation_housing_5v_ps() const +std::uint16_t PerformanceMaintenanceData::elevation_housing_5v_ps() const { return p->elevationHousing5VPs_; } -uint16_t PerformanceMaintenanceData::elevation_neg_dead_limit() const +std::uint16_t PerformanceMaintenanceData::elevation_neg_dead_limit() const { return p->elevationNegDeadLimit_; } -uint16_t PerformanceMaintenanceData::elevation_pos_normal_limit() const +std::uint16_t PerformanceMaintenanceData::elevation_pos_normal_limit() const { return p->elevationPosNormalLimit_; } -uint16_t PerformanceMaintenanceData::elevation_neg_normal_limit() const +std::uint16_t PerformanceMaintenanceData::elevation_neg_normal_limit() const { return p->elevationNegNormalLimit_; } -uint16_t PerformanceMaintenanceData::elevation_encoder_light() const +std::uint16_t PerformanceMaintenanceData::elevation_encoder_light() const { return p->elevationEncoderLight_; } -uint16_t PerformanceMaintenanceData::elevation_gearbox_oil() const +std::uint16_t PerformanceMaintenanceData::elevation_gearbox_oil() const { return p->elevationGearboxOil_; } -uint16_t PerformanceMaintenanceData::elevation_handwheel() const +std::uint16_t PerformanceMaintenanceData::elevation_handwheel() const { return p->elevationHandwheel_; } -uint16_t PerformanceMaintenanceData::elevation_amp_ps() const +std::uint16_t PerformanceMaintenanceData::elevation_amp_ps() const { return p->elevationAmpPs_; } -uint16_t PerformanceMaintenanceData::azimuth_servo_amp_inhibit() const +std::uint16_t PerformanceMaintenanceData::azimuth_servo_amp_inhibit() const { return p->azimuthServoAmpInhibit_; } -uint16_t PerformanceMaintenanceData::azimuth_servo_amp_short_circuit() const +std::uint16_t +PerformanceMaintenanceData::azimuth_servo_amp_short_circuit() const { return p->azimuthServoAmpShortCircuit_; } -uint16_t PerformanceMaintenanceData::azimuth_servo_amp_overtemp() const +std::uint16_t PerformanceMaintenanceData::azimuth_servo_amp_overtemp() const { return p->azimuthServoAmpOvertemp_; } -uint16_t PerformanceMaintenanceData::azimuth_motor_overtemp() const +std::uint16_t PerformanceMaintenanceData::azimuth_motor_overtemp() const { return p->azimuthMotorOvertemp_; } -uint16_t PerformanceMaintenanceData::azimuth_stow_pin() const +std::uint16_t PerformanceMaintenanceData::azimuth_stow_pin() const { return p->azimuthStowPin_; } -uint16_t PerformanceMaintenanceData::azimuth_housing_5v_ps() const +std::uint16_t PerformanceMaintenanceData::azimuth_housing_5v_ps() const { return p->azimuthHousing5VPs_; } -uint16_t PerformanceMaintenanceData::azimuth_encoder_light() const +std::uint16_t PerformanceMaintenanceData::azimuth_encoder_light() const { return p->azimuthEncoderLight_; } -uint16_t PerformanceMaintenanceData::azimuth_gearbox_oil() const +std::uint16_t PerformanceMaintenanceData::azimuth_gearbox_oil() const { return p->azimuthGearboxOil_; } -uint16_t PerformanceMaintenanceData::azimuth_bull_gear_oil() const +std::uint16_t PerformanceMaintenanceData::azimuth_bull_gear_oil() const { return p->azimuthBullGearOil_; } -uint16_t PerformanceMaintenanceData::azimuth_handwheel() const +std::uint16_t PerformanceMaintenanceData::azimuth_handwheel() const { return p->azimuthHandwheel_; } -uint16_t PerformanceMaintenanceData::azimuth_servo_amp_ps() const +std::uint16_t PerformanceMaintenanceData::azimuth_servo_amp_ps() const { return p->azimuthServoAmpPs_; } -uint16_t PerformanceMaintenanceData::servo() const +std::uint16_t PerformanceMaintenanceData::servo() const { return p->servo_; } -uint16_t PerformanceMaintenanceData::pedestal_interlock_switch() const +std::uint16_t PerformanceMaintenanceData::pedestal_interlock_switch() const { return p->pedestalInterlockSwitch_; } -uint16_t PerformanceMaintenanceData::coho_clock() const +std::uint16_t PerformanceMaintenanceData::coho_clock() const { return p->cohoClock_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::rf_generator_frequency_select_oscillator() const { return p->rfGeneratorFrequencySelectOscillator_; } -uint16_t PerformanceMaintenanceData::rf_generator_rf_stalo() const +std::uint16_t PerformanceMaintenanceData::rf_generator_rf_stalo() const { return p->rfGeneratorRfStalo_; } -uint16_t PerformanceMaintenanceData::rf_generator_phase_shifted_coho() const +std::uint16_t +PerformanceMaintenanceData::rf_generator_phase_shifted_coho() const { return p->rfGeneratorPhaseShiftedCoho_; } -uint16_t PerformanceMaintenanceData::_9v_receiver_ps() const +std::uint16_t PerformanceMaintenanceData::_9v_receiver_ps() const { return p->_9VReceiverPs_; } -uint16_t PerformanceMaintenanceData::_5v_receiver_ps() const +std::uint16_t PerformanceMaintenanceData::_5v_receiver_ps() const { return p->_5VReceiverPs_; } -uint16_t PerformanceMaintenanceData::_18v_receiver_ps() const +std::uint16_t PerformanceMaintenanceData::_18v_receiver_ps() const { return p->_18VReceiverPs_; } -uint16_t PerformanceMaintenanceData::neg_9v_receiver_ps() const +std::uint16_t PerformanceMaintenanceData::neg_9v_receiver_ps() const { return p->neg9VReceiverPs_; } -uint16_t PerformanceMaintenanceData::_5v_single_channel_rdaiu_ps() const +std::uint16_t PerformanceMaintenanceData::_5v_single_channel_rdaiu_ps() const { return p->_5VSingleChannelRdaiuPs_; } @@ -1311,22 +1325,22 @@ float PerformanceMaintenanceData::long_pulse_horizontal_dbz0() const return p->longPulseHorizontalDbz0_; } -uint16_t PerformanceMaintenanceData::velocity_processed() const +std::uint16_t PerformanceMaintenanceData::velocity_processed() const { return p->velocityProcessed_; } -uint16_t PerformanceMaintenanceData::width_processed() const +std::uint16_t PerformanceMaintenanceData::width_processed() const { return p->widthProcessed_; } -uint16_t PerformanceMaintenanceData::velocity_rf_gen() const +std::uint16_t PerformanceMaintenanceData::velocity_rf_gen() const { return p->velocityRfGen_; } -uint16_t PerformanceMaintenanceData::width_rf_gen() const +std::uint16_t PerformanceMaintenanceData::width_rf_gen() const { return p->widthRfGen_; } @@ -1391,145 +1405,148 @@ float PerformanceMaintenanceData::vertical_linearity() const return p->verticalLinearity_; } -uint16_t PerformanceMaintenanceData::state_file_read_status() const +std::uint16_t PerformanceMaintenanceData::state_file_read_status() const { return p->stateFileReadStatus_; } -uint16_t PerformanceMaintenanceData::state_file_write_status() const +std::uint16_t PerformanceMaintenanceData::state_file_write_status() const { return p->stateFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::bypass_map_file_read_status() const +std::uint16_t PerformanceMaintenanceData::bypass_map_file_read_status() const { return p->bypassMapFileReadStatus_; } -uint16_t PerformanceMaintenanceData::bypass_map_file_write_status() const +std::uint16_t PerformanceMaintenanceData::bypass_map_file_write_status() const { return p->bypassMapFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::current_adaptation_file_read_status() const +std::uint16_t +PerformanceMaintenanceData::current_adaptation_file_read_status() const { return p->currentAdaptationFileReadStatus_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::current_adaptation_file_write_status() const { return p->currentAdaptationFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::censor_zone_file_read_status() const +std::uint16_t PerformanceMaintenanceData::censor_zone_file_read_status() const { return p->censorZoneFileReadStatus_; } -uint16_t PerformanceMaintenanceData::censor_zone_file_write_status() const +std::uint16_t PerformanceMaintenanceData::censor_zone_file_write_status() const { return p->censorZoneFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::remote_vcp_file_read_status() const +std::uint16_t PerformanceMaintenanceData::remote_vcp_file_read_status() const { return p->remoteVcpFileReadStatus_; } -uint16_t PerformanceMaintenanceData::remote_vcp_file_write_status() const +std::uint16_t PerformanceMaintenanceData::remote_vcp_file_write_status() const { return p->remoteVcpFileWriteStatus_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::baseline_adaptation_file_read_status() const { return p->baselineAdaptationFileReadStatus_; } -uint16_t PerformanceMaintenanceData::read_status_of_prf_sets() const +std::uint16_t PerformanceMaintenanceData::read_status_of_prf_sets() const { return p->readStatusOfPrfSets_; } -uint16_t PerformanceMaintenanceData::clutter_filter_map_file_read_status() const +std::uint16_t +PerformanceMaintenanceData::clutter_filter_map_file_read_status() const { return p->clutterFilterMapFileReadStatus_; } -uint16_t +std::uint16_t PerformanceMaintenanceData::clutter_filter_map_file_write_status() const { return p->clutterFilterMapFileWriteStatus_; } -uint16_t PerformanceMaintenanceData::general_disk_io_error() const +std::uint16_t PerformanceMaintenanceData::general_disk_io_error() const { return p->generalDiskIoError_; } -uint8_t PerformanceMaintenanceData::rsp_status() const +std::uint8_t PerformanceMaintenanceData::rsp_status() const { return p->rspStatus_; } -uint8_t PerformanceMaintenanceData::cpu1_temperature() const +std::uint8_t PerformanceMaintenanceData::cpu1_temperature() const { return p->cpu1Temperature_; } -uint8_t PerformanceMaintenanceData::cpu2_temperature() const +std::uint8_t PerformanceMaintenanceData::cpu2_temperature() const { return p->cpu2Temperature_; } -uint16_t PerformanceMaintenanceData::rsp_motherboard_power() const +std::uint16_t PerformanceMaintenanceData::rsp_motherboard_power() const { return p->rspMotherboardPower_; } -uint16_t PerformanceMaintenanceData::spip_comm_status() const +std::uint16_t PerformanceMaintenanceData::spip_comm_status() const { return p->spipCommStatus_; } -uint16_t PerformanceMaintenanceData::hci_comm_status() const +std::uint16_t PerformanceMaintenanceData::hci_comm_status() const { return p->hciCommStatus_; } -uint16_t PerformanceMaintenanceData::signal_processor_command_status() const +std::uint16_t +PerformanceMaintenanceData::signal_processor_command_status() const { return p->signalProcessorCommandStatus_; } -uint16_t PerformanceMaintenanceData::ame_communication_status() const +std::uint16_t PerformanceMaintenanceData::ame_communication_status() const { return p->ameCommunicationStatus_; } -uint16_t PerformanceMaintenanceData::rms_link_status() const +std::uint16_t PerformanceMaintenanceData::rms_link_status() const { return p->rmsLinkStatus_; } -uint16_t PerformanceMaintenanceData::rpg_link_status() const +std::uint16_t PerformanceMaintenanceData::rpg_link_status() const { return p->rpgLinkStatus_; } -uint16_t PerformanceMaintenanceData::interpanel_link_status() const +std::uint16_t PerformanceMaintenanceData::interpanel_link_status() const { return p->interpanelLinkStatus_; } -uint32_t PerformanceMaintenanceData::performance_check_time() const +std::uint32_t PerformanceMaintenanceData::performance_check_time() const { return p->performanceCheckTime_; } -uint16_t PerformanceMaintenanceData::version() const +std::uint16_t PerformanceMaintenanceData::version() const { return p->version_; } @@ -1538,9 +1555,10 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) { logger_->trace("Parsing Performance/Maintenance Data (Message Type 3)"); - bool messageValid = true; - size_t bytesRead = 0; + bool messageValid = true; + std::size_t bytesRead = 0; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Readability p->rcpString_.resize(16); // Communications @@ -1684,10 +1702,10 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) is.read(reinterpret_cast(&p->transmitterInoperable_), 2); // 184 is.read(reinterpret_cast(&p->transmitterAirFilter_), 2); // 185 is.read(reinterpret_cast(&p->zeroTestBit_[0]), - p->zeroTestBit_.size() * 2); // 186-193 + static_cast(p->zeroTestBit_.size() * 2)); // 186-193 is.read(reinterpret_cast(&p->oneTestBit_[0]), - p->oneTestBit_.size() * 2); // 194-201 - is.read(reinterpret_cast(&p->xmtrSpipInterface_), 2); // 202 + static_cast(p->oneTestBit_.size() * 2)); // 194-201 + is.read(reinterpret_cast(&p->xmtrSpipInterface_), 2); // 202 is.read(reinterpret_cast(&p->transmitterSummaryStatus_), 2); // 203 is.seekg(2, std::ios_base::cur); // 204 is.read(reinterpret_cast(&p->transmitterRfPower_), 4); // 205-206 @@ -1882,6 +1900,8 @@ bool PerformanceMaintenanceData::Parse(std::istream& is) bytesRead += 960; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // Communications p->loopBackSetStatus_ = ntohs(p->loopBackSetStatus_); p->t1OutputFrames_ = ntohl(p->t1OutputFrames_); @@ -2190,6 +2210,4 @@ PerformanceMaintenanceData::Create(Level2MessageHeader&& header, return message; } -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp index aff8664a..05887f2a 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp @@ -1,11 +1,7 @@ #include #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { static const std::string logPrefix_ = "scwx::wsr88d::rda::rda_adaptation_data"; @@ -13,30 +9,26 @@ static const auto logger_ = util::Logger::Create(logPrefix_); struct AntManualSetup { - int32_t ielmin_; - int32_t ielmax_; - uint32_t fazvelmax_; - uint32_t felvelmax_; - int32_t igndHgt_; - uint32_t iradHgt_; - - AntManualSetup() : - ielmin_ {0}, - ielmax_ {0}, - fazvelmax_ {0}, - felvelmax_ {0}, - igndHgt_ {0}, - iradHgt_ {0} - { - } + std::int32_t ielmin_ {0}; + std::int32_t ielmax_ {0}; + std::uint32_t fazvelmax_ {0}; + std::uint32_t felvelmax_ {0}; + std::int32_t igndHgt_ {0}; + std::uint32_t iradHgt_ {0}; }; -class RdaAdaptationDataImpl +class RdaAdaptationData::Impl { public: - explicit RdaAdaptationDataImpl() = default; - ~RdaAdaptationDataImpl() = default; + explicit Impl() = default; + ~Impl() = default; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) std::string adapFileName_ {}; std::string adapFormat_ {}; std::string adapRevision_ {}; @@ -65,27 +57,27 @@ public: bool specFilterInstalled_ {false}; bool tpsInstalled_ {false}; bool rmsInstalled_ {false}; - uint32_t aHvdlTstInt_ {0}; - uint32_t aRpgLtInt_ {0}; - uint32_t aMinStabUtilPwrTime_ {0}; - uint32_t aGenAutoExerInterval_ {0}; - uint32_t aUtilPwrSwReqInterval_ {0}; + std::uint32_t aHvdlTstInt_ {0}; + std::uint32_t aRpgLtInt_ {0}; + std::uint32_t aMinStabUtilPwrTime_ {0}; + std::uint32_t aGenAutoExerInterval_ {0}; + std::uint32_t aUtilPwrSwReqInterval_ {0}; float aLowFuelLevel_ {0.0f}; - uint32_t configChanNumber_ {0}; - uint32_t redundantChanConfig_ {0}; + std::uint32_t configChanNumber_ {0}; + std::uint32_t redundantChanConfig_ {0}; std::array attenTable_ {0.0f}; std::map pathLosses_ {}; float vTsCw_ {0.0f}; std::array hRnscale_ {0.0f}; std::array atmos_ {0.0f}; std::array elIndex_ {0.0f}; - uint32_t tfreqMhz_ {0}; + std::uint32_t tfreqMhz_ {0}; float baseDataTcn_ {0.0f}; float reflDataTover_ {0.0f}; float tarHDbz0Lp_ {0.0f}; float tarVDbz0Lp_ {0.0f}; - uint32_t initPhiDp_ {0}; - uint32_t normInitPhiDp_ {0}; + std::uint32_t initPhiDp_ {0}; + std::uint32_t normInitPhiDp_ {0}; float lxLp_ {0.0f}; float lxSp_ {0.0f}; float meteorParam_ {0.0f}; @@ -93,9 +85,9 @@ public: float velDegradLimit_ {0.0f}; float wthDegradLimit_ {0.0f}; float hNoisetempDgradLimit_ {0.0f}; - uint32_t hMinNoisetemp_ {0}; + std::uint32_t hMinNoisetemp_ {0}; float vNoisetempDgradLimit_ {0.0f}; - uint32_t vMinNoisetemp_ {0}; + std::uint32_t vMinNoisetemp_ {0}; float klyDegradeLimit_ {0.0f}; float tsCoho_ {0.0f}; float hTsCw_ {0.0f}; @@ -112,19 +104,19 @@ public: float vDbz0DeltaLimit_ {0.0f}; float tarHDbz0Sp_ {0.0f}; float tarVDbz0Sp_ {0.0f}; - uint32_t deltaprf_ {0}; - uint32_t tauSp_ {0}; - uint32_t tauLp_ {0}; - uint32_t ncDeadValue_ {0}; - uint32_t tauRfSp_ {0}; - uint32_t tauRfLp_ {0}; + std::uint32_t deltaprf_ {0}; + std::uint32_t tauSp_ {0}; + std::uint32_t tauLp_ {0}; + std::uint32_t ncDeadValue_ {0}; + std::uint32_t tauRfSp_ {0}; + std::uint32_t tauRfLp_ {0}; float seg1Lim_ {0.0f}; float slatsec_ {0.0f}; float slonsec_ {0.0f}; - uint32_t slatdeg_ {0}; - uint32_t slatmin_ {0}; - uint32_t slondeg_ {0}; - uint32_t slonmin_ {0}; + std::uint32_t slatdeg_ {0}; + std::uint32_t slatmin_ {0}; + std::uint32_t slondeg_ {0}; + std::uint32_t slonmin_ {0}; char slatdir_ {0}; char slondir_ {0}; double digRcvrClockFreq_ {0.0}; @@ -155,16 +147,16 @@ public: float azEncoderAlignment_ {0.0f}; float elEncoderAlignment_ {0.0f}; std::string refinedPark_ {}; - uint32_t rvp8nvIwaveguideLength_ {0}; + std::uint32_t rvp8nvIwaveguideLength_ {0}; std::array vRnscale_ {0.0f}; float velDataTover_ {0.0f}; float widthDataTover_ {0.0f}; float dopplerRangeStart_ {0.0f}; - uint32_t maxElIndex_ {0}; + std::uint32_t maxElIndex_ {0}; float seg2Lim_ {0.0f}; float seg3Lim_ {0.0f}; float seg4Lim_ {0.0f}; - uint32_t nbrElSegments_ {0}; + std::uint32_t nbrElSegments_ {0}; float hNoiseLong_ {0.0f}; float antNoiseTemp_ {0.0f}; float hNoiseShort_ {0.0f}; @@ -196,22 +188,23 @@ public: float rcvrModMinTemp_ {0.0f}; float biteModMaxTemp_ {0.0f}; float biteModMinTemp_ {0.0f}; - uint32_t defaultPolarization_ {0}; + std::uint32_t defaultPolarization_ {0}; float trLimitDgradLimit_ {0.0f}; float trLimitFailLimit_ {0.0f}; bool rfpStepperEnabled_ {false}; float ameCurrentTolerance_ {0.0f}; - uint32_t hOnlyPolarization_ {0}; - uint32_t vOnlyPolarization_ {0}; + std::uint32_t hOnlyPolarization_ {0}; + std::uint32_t vOnlyPolarization_ {0}; float sunBias_ {0.0f}; float aMinShelterTempWarn_ {0.0f}; float powerMeterZero_ {0.0f}; float txbBaseline_ {0.0f}; float txbAlarmThresh_ {0.0f}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) }; RdaAdaptationData::RdaAdaptationData() : - Level2Message(), p(std::make_unique()) + Level2Message(), p(std::make_unique()) { } RdaAdaptationData::~RdaAdaptationData() = default; @@ -277,7 +270,7 @@ float RdaAdaptationData::parkel() const float RdaAdaptationData::a_fuel_conv(unsigned i) const { - return p->aFuelConv_[i]; + return p->aFuelConv_.at(i); } float RdaAdaptationData::a_min_shelter_temp() const @@ -360,27 +353,27 @@ bool RdaAdaptationData::rms_installed() const return p->rmsInstalled_; } -uint32_t RdaAdaptationData::a_hvdl_tst_int() const +std::uint32_t RdaAdaptationData::a_hvdl_tst_int() const { return p->aHvdlTstInt_; } -uint32_t RdaAdaptationData::a_rpg_lt_int() const +std::uint32_t RdaAdaptationData::a_rpg_lt_int() const { return p->aRpgLtInt_; } -uint32_t RdaAdaptationData::a_min_stab_util_pwr_time() const +std::uint32_t RdaAdaptationData::a_min_stab_util_pwr_time() const { return p->aMinStabUtilPwrTime_; } -uint32_t RdaAdaptationData::a_gen_auto_exer_interval() const +std::uint32_t RdaAdaptationData::a_gen_auto_exer_interval() const { return p->aGenAutoExerInterval_; } -uint32_t RdaAdaptationData::a_util_pwr_sw_req_interval() const +std::uint32_t RdaAdaptationData::a_util_pwr_sw_req_interval() const { return p->aUtilPwrSwReqInterval_; } @@ -390,19 +383,19 @@ float RdaAdaptationData::a_low_fuel_level() const return p->aLowFuelLevel_; } -uint32_t RdaAdaptationData::config_chan_number() const +std::uint32_t RdaAdaptationData::config_chan_number() const { return p->configChanNumber_; } -uint32_t RdaAdaptationData::redundant_chan_config() const +std::uint32_t RdaAdaptationData::redundant_chan_config() const { return p->redundantChanConfig_; } float RdaAdaptationData::atten_table(unsigned i) const { - return p->attenTable_[i]; + return p->attenTable_.at(i); } float RdaAdaptationData::path_losses(unsigned i) const @@ -412,41 +405,49 @@ float RdaAdaptationData::path_losses(unsigned i) const float RdaAdaptationData::h_coupler_xmt_loss() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(29); } float RdaAdaptationData::h_coupler_cw_loss() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(48); } float RdaAdaptationData::v_coupler_xmt_loss() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(49); } float RdaAdaptationData::ame_ts_bias() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(51); } float RdaAdaptationData::v_coupler_cw_loss() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(53); } float RdaAdaptationData::pwr_sense_bias() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(56); } float RdaAdaptationData::ame_v_noise_enr() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(57); } float RdaAdaptationData::chan_cal_diff() const { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) return path_losses(70); } @@ -457,20 +458,20 @@ float RdaAdaptationData::v_ts_cw() const float RdaAdaptationData::h_rnscale(unsigned i) const { - return p->hRnscale_[i]; + return p->hRnscale_.at(i); } float RdaAdaptationData::atmos(unsigned i) const { - return p->atmos_[i]; + return p->atmos_.at(i); } float RdaAdaptationData::el_index(unsigned i) const { - return p->elIndex_[i]; + return p->elIndex_.at(i); } -uint32_t RdaAdaptationData::tfreq_mhz() const +std::uint32_t RdaAdaptationData::tfreq_mhz() const { return p->tfreqMhz_; } @@ -495,12 +496,12 @@ float RdaAdaptationData::tar_v_dbz0_lp() const return p->tarVDbz0Lp_; } -uint32_t RdaAdaptationData::init_phi_dp() const +std::uint32_t RdaAdaptationData::init_phi_dp() const { return p->initPhiDp_; } -uint32_t RdaAdaptationData::norm_init_phi_dp() const +std::uint32_t RdaAdaptationData::norm_init_phi_dp() const { return p->normInitPhiDp_; } @@ -540,7 +541,7 @@ float RdaAdaptationData::h_noisetemp_dgrad_limit() const return p->hNoisetempDgradLimit_; } -uint32_t RdaAdaptationData::h_min_noisetemp() const +std::uint32_t RdaAdaptationData::h_min_noisetemp() const { return p->hMinNoisetemp_; } @@ -550,7 +551,7 @@ float RdaAdaptationData::v_noisetemp_dgrad_limit() const return p->vNoisetempDgradLimit_; } -uint32_t RdaAdaptationData::v_min_noisetemp() const +std::uint32_t RdaAdaptationData::v_min_noisetemp() const { return p->vMinNoisetemp_; } @@ -635,32 +636,32 @@ float RdaAdaptationData::tar_v_dbz0_sp() const return p->tarVDbz0Sp_; } -uint32_t RdaAdaptationData::deltaprf() const +std::uint32_t RdaAdaptationData::deltaprf() const { return p->deltaprf_; } -uint32_t RdaAdaptationData::tau_sp() const +std::uint32_t RdaAdaptationData::tau_sp() const { return p->tauSp_; } -uint32_t RdaAdaptationData::tau_lp() const +std::uint32_t RdaAdaptationData::tau_lp() const { return p->tauLp_; } -uint32_t RdaAdaptationData::nc_dead_value() const +std::uint32_t RdaAdaptationData::nc_dead_value() const { return p->ncDeadValue_; } -uint32_t RdaAdaptationData::tau_rf_sp() const +std::uint32_t RdaAdaptationData::tau_rf_sp() const { return p->tauRfSp_; } -uint32_t RdaAdaptationData::tau_rf_lp() const +std::uint32_t RdaAdaptationData::tau_rf_lp() const { return p->tauRfLp_; } @@ -680,22 +681,22 @@ float RdaAdaptationData::slonsec() const return p->slonsec_; } -uint32_t RdaAdaptationData::slatdeg() const +std::uint32_t RdaAdaptationData::slatdeg() const { return p->slatdeg_; } -uint32_t RdaAdaptationData::slatmin() const +std::uint32_t RdaAdaptationData::slatmin() const { return p->slatmin_; } -uint32_t RdaAdaptationData::slondeg() const +std::uint32_t RdaAdaptationData::slondeg() const { return p->slondeg_; } -uint32_t RdaAdaptationData::slonmin() const +std::uint32_t RdaAdaptationData::slonmin() const { return p->slonmin_; } @@ -738,31 +739,31 @@ std::string RdaAdaptationData::site_name() const float RdaAdaptationData::ant_manual_setup_ielmin() const { constexpr float SCALE = 360.0f / 65536.0f; - return p->antManualSetup_.ielmin_ * SCALE; + return static_cast(p->antManualSetup_.ielmin_) * SCALE; } float RdaAdaptationData::ant_manual_setup_ielmax() const { constexpr float SCALE = 360.0f / 65536.0f; - return p->antManualSetup_.ielmax_ * SCALE; + return static_cast(p->antManualSetup_.ielmax_) * SCALE; } -uint32_t RdaAdaptationData::ant_manual_setup_fazvelmax() const +std::uint32_t RdaAdaptationData::ant_manual_setup_fazvelmax() const { return p->antManualSetup_.fazvelmax_; } -uint32_t RdaAdaptationData::ant_manual_setup_felvelmax() const +std::uint32_t RdaAdaptationData::ant_manual_setup_felvelmax() const { return p->antManualSetup_.felvelmax_; } -int32_t RdaAdaptationData::ant_manual_setup_ignd_hgt() const +std::int32_t RdaAdaptationData::ant_manual_setup_ignd_hgt() const { return p->antManualSetup_.igndHgt_; } -uint32_t RdaAdaptationData::ant_manual_setup_irad_hgt() const +std::uint32_t RdaAdaptationData::ant_manual_setup_irad_hgt() const { return p->antManualSetup_.iradHgt_; } @@ -877,14 +878,14 @@ std::string RdaAdaptationData::refined_park() const return p->refinedPark_; } -uint32_t RdaAdaptationData::rvp8nv_iwaveguide_length() const +std::uint32_t RdaAdaptationData::rvp8nv_iwaveguide_length() const { return p->rvp8nvIwaveguideLength_; } float RdaAdaptationData::v_rnscale(unsigned i) const { - return p->vRnscale_[i]; + return p->vRnscale_.at(i); } float RdaAdaptationData::vel_data_tover() const @@ -902,7 +903,7 @@ float RdaAdaptationData::doppler_range_start() const return p->dopplerRangeStart_; } -uint32_t RdaAdaptationData::max_el_index() const +std::uint32_t RdaAdaptationData::max_el_index() const { return p->maxElIndex_; } @@ -922,7 +923,7 @@ float RdaAdaptationData::seg4_lim() const return p->seg4Lim_; } -uint32_t RdaAdaptationData::nbr_el_segments() const +std::uint32_t RdaAdaptationData::nbr_el_segments() const { return p->nbrElSegments_; } @@ -1082,7 +1083,7 @@ float RdaAdaptationData::bite_mod_min_temp() const return p->biteModMinTemp_; } -uint32_t RdaAdaptationData::default_polarization() const +std::uint32_t RdaAdaptationData::default_polarization() const { return p->defaultPolarization_; } @@ -1107,12 +1108,12 @@ float RdaAdaptationData::ame_current_tolerance() const return p->ameCurrentTolerance_; } -uint32_t RdaAdaptationData::h_only_polarization() const +std::uint32_t RdaAdaptationData::h_only_polarization() const { return p->hOnlyPolarization_; } -uint32_t RdaAdaptationData::v_only_polarization() const +std::uint32_t RdaAdaptationData::v_only_polarization() const { return p->vOnlyPolarization_; } @@ -1146,9 +1147,10 @@ bool RdaAdaptationData::Parse(std::istream& is) { logger_->trace("Parsing RDA Adaptation Data (Message Type 18)"); - bool messageValid = true; - size_t bytesRead = 0; + bool messageValid = true; + std::size_t bytesRead = 0; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Readability p->adapFileName_.resize(12); p->adapFormat_.resize(4); p->adapRevision_.resize(4); @@ -1171,7 +1173,7 @@ bool RdaAdaptationData::Parse(std::istream& is) is.read(reinterpret_cast(&p->parkel_), 4); // 64-67 is.read(reinterpret_cast(&p->aFuelConv_[0]), - p->aFuelConv_.size() * 4); // 68-111 + static_cast(p->aFuelConv_.size() * 4)); // 68-111 is.read(reinterpret_cast(&p->aMinShelterTemp_), 4); // 112-115 is.read(reinterpret_cast(&p->aMaxShelterTemp_), 4); // 116-119 @@ -1209,7 +1211,7 @@ bool RdaAdaptationData::Parse(std::istream& is) is.read(reinterpret_cast(&p->redundantChanConfig_), 4); // 224-227 is.read(reinterpret_cast(&p->attenTable_[0]), - p->attenTable_.size() * 4); // 228-643 + static_cast(p->attenTable_.size() * 4)); // 228-643 is.seekg(24, std::ios_base::cur); // 644-667 is.read(reinterpret_cast(&p->pathLosses_[7]), 4); // 668-671 @@ -1262,13 +1264,13 @@ bool RdaAdaptationData::Parse(std::istream& is) is.read(reinterpret_cast(&p->vTsCw_), 4); // 936-939 is.read(reinterpret_cast(&p->hRnscale_[0]), - p->hRnscale_.size() * 4); // 940-991 + static_cast(p->hRnscale_.size() * 4)); // 940-991 is.read(reinterpret_cast(&p->atmos_[0]), - p->atmos_.size() * 4); // 992-1043 + static_cast(p->atmos_.size() * 4)); // 992-1043 is.read(reinterpret_cast(&p->elIndex_[0]), - p->elIndex_.size() * 4); // 1044-1091 + static_cast(p->elIndex_.size() * 4)); // 1044-1091 is.read(reinterpret_cast(&p->tfreqMhz_), 4); // 1092-1095 is.read(reinterpret_cast(&p->baseDataTcn_), 4); // 1096-1099 @@ -1394,11 +1396,12 @@ bool RdaAdaptationData::Parse(std::istream& is) 4); // 8696-8699 is.read(reinterpret_cast(&p->vRnscale_[0]), - 11 * 4); // 8700-8743 + static_cast(11 * 4)); // 8700-8743 - is.read(reinterpret_cast(&p->velDataTover_), 4); // 8744-8747 - is.read(reinterpret_cast(&p->widthDataTover_), 4); // 8748-8751 - is.read(reinterpret_cast(&p->vRnscale_[11]), 2 * 4); // 8752-8759 + is.read(reinterpret_cast(&p->velDataTover_), 4); // 8744-8747 + is.read(reinterpret_cast(&p->widthDataTover_), 4); // 8748-8751 + is.read(reinterpret_cast(&p->vRnscale_[11]), + static_cast(2 * 4)); // 8752-8759 is.seekg(4, std::ios_base::cur); // 8760-8763 @@ -1468,6 +1471,8 @@ bool RdaAdaptationData::Parse(std::istream& is) bytesRead += 9468; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + p->lowerPreLimit_ = SwapFloat(p->lowerPreLimit_); p->azLat_ = SwapFloat(p->azLat_); p->upperPreLimit_ = SwapFloat(p->upperPreLimit_); @@ -1507,84 +1512,87 @@ bool RdaAdaptationData::Parse(std::istream& is) SwapArray(p->atmos_); SwapArray(p->elIndex_); - p->tfreqMhz_ = ntohl(p->tfreqMhz_); - p->baseDataTcn_ = SwapFloat(p->baseDataTcn_); - p->reflDataTover_ = SwapFloat(p->reflDataTover_); - p->tarHDbz0Lp_ = SwapFloat(p->tarHDbz0Lp_); - p->tarVDbz0Lp_ = SwapFloat(p->tarVDbz0Lp_); - p->initPhiDp_ = ntohl(p->initPhiDp_); - p->normInitPhiDp_ = ntohl(p->normInitPhiDp_); - p->lxLp_ = SwapFloat(p->lxLp_); - p->lxSp_ = SwapFloat(p->lxSp_); - p->meteorParam_ = SwapFloat(p->meteorParam_); - p->antennaGain_ = SwapFloat(p->antennaGain_); - p->velDegradLimit_ = SwapFloat(p->velDegradLimit_); - p->wthDegradLimit_ = SwapFloat(p->wthDegradLimit_); - p->hNoisetempDgradLimit_ = SwapFloat(p->hNoisetempDgradLimit_); - p->hMinNoisetemp_ = ntohl(p->hMinNoisetemp_); - p->vNoisetempDgradLimit_ = SwapFloat(p->vNoisetempDgradLimit_); - p->vMinNoisetemp_ = ntohl(p->vMinNoisetemp_); - p->klyDegradeLimit_ = SwapFloat(p->klyDegradeLimit_); - p->tsCoho_ = SwapFloat(p->tsCoho_); - p->hTsCw_ = SwapFloat(p->hTsCw_); - p->tsStalo_ = SwapFloat(p->tsStalo_); - p->ameHNoiseEnr_ = SwapFloat(p->ameHNoiseEnr_); - p->xmtrPeakPwrHighLimit_ = SwapFloat(p->xmtrPeakPwrHighLimit_); - p->xmtrPeakPwrLowLimit_ = SwapFloat(p->xmtrPeakPwrLowLimit_); - p->hDbz0DeltaLimit_ = SwapFloat(p->hDbz0DeltaLimit_); - p->threshold1_ = SwapFloat(p->threshold1_); - p->threshold2_ = SwapFloat(p->threshold2_); - p->clutSuppDgradLim_ = SwapFloat(p->clutSuppDgradLim_); - p->range0Value_ = SwapFloat(p->range0Value_); - p->xmtrPwrMtrScale_ = SwapFloat(p->xmtrPwrMtrScale_); - p->vDbz0DeltaLimit_ = SwapFloat(p->vDbz0DeltaLimit_); - p->tarHDbz0Sp_ = SwapFloat(p->tarHDbz0Sp_); - p->tarVDbz0Sp_ = SwapFloat(p->tarVDbz0Sp_); - p->deltaprf_ = ntohl(p->deltaprf_); - p->tauSp_ = ntohl(p->tauSp_); - p->tauLp_ = ntohl(p->tauLp_); - p->ncDeadValue_ = ntohl(p->ncDeadValue_); - p->tauRfSp_ = ntohl(p->tauRfSp_); - p->tauRfLp_ = ntohl(p->tauRfLp_); - p->seg1Lim_ = SwapFloat(p->seg1Lim_); - p->slatsec_ = SwapFloat(p->slatsec_); - p->slonsec_ = SwapFloat(p->slonsec_); - p->slatdeg_ = ntohl(p->slatdeg_); - p->slatmin_ = ntohl(p->slatmin_); - p->slondeg_ = ntohl(p->slondeg_); - p->slonmin_ = ntohl(p->slonmin_); - p->digRcvrClockFreq_ = SwapDouble(p->digRcvrClockFreq_); - p->cohoFreq_ = SwapDouble(p->cohoFreq_); - p->azCorrectionFactor_ = SwapFloat(p->azCorrectionFactor_); - p->elCorrectionFactor_ = SwapFloat(p->elCorrectionFactor_); - p->antManualSetup_.ielmin_ = ntohl(p->antManualSetup_.ielmin_); - p->antManualSetup_.ielmax_ = ntohl(p->antManualSetup_.ielmax_); + p->tfreqMhz_ = ntohl(p->tfreqMhz_); + p->baseDataTcn_ = SwapFloat(p->baseDataTcn_); + p->reflDataTover_ = SwapFloat(p->reflDataTover_); + p->tarHDbz0Lp_ = SwapFloat(p->tarHDbz0Lp_); + p->tarVDbz0Lp_ = SwapFloat(p->tarVDbz0Lp_); + p->initPhiDp_ = ntohl(p->initPhiDp_); + p->normInitPhiDp_ = ntohl(p->normInitPhiDp_); + p->lxLp_ = SwapFloat(p->lxLp_); + p->lxSp_ = SwapFloat(p->lxSp_); + p->meteorParam_ = SwapFloat(p->meteorParam_); + p->antennaGain_ = SwapFloat(p->antennaGain_); + p->velDegradLimit_ = SwapFloat(p->velDegradLimit_); + p->wthDegradLimit_ = SwapFloat(p->wthDegradLimit_); + p->hNoisetempDgradLimit_ = SwapFloat(p->hNoisetempDgradLimit_); + p->hMinNoisetemp_ = ntohl(p->hMinNoisetemp_); + p->vNoisetempDgradLimit_ = SwapFloat(p->vNoisetempDgradLimit_); + p->vMinNoisetemp_ = ntohl(p->vMinNoisetemp_); + p->klyDegradeLimit_ = SwapFloat(p->klyDegradeLimit_); + p->tsCoho_ = SwapFloat(p->tsCoho_); + p->hTsCw_ = SwapFloat(p->hTsCw_); + p->tsStalo_ = SwapFloat(p->tsStalo_); + p->ameHNoiseEnr_ = SwapFloat(p->ameHNoiseEnr_); + p->xmtrPeakPwrHighLimit_ = SwapFloat(p->xmtrPeakPwrHighLimit_); + p->xmtrPeakPwrLowLimit_ = SwapFloat(p->xmtrPeakPwrLowLimit_); + p->hDbz0DeltaLimit_ = SwapFloat(p->hDbz0DeltaLimit_); + p->threshold1_ = SwapFloat(p->threshold1_); + p->threshold2_ = SwapFloat(p->threshold2_); + p->clutSuppDgradLim_ = SwapFloat(p->clutSuppDgradLim_); + p->range0Value_ = SwapFloat(p->range0Value_); + p->xmtrPwrMtrScale_ = SwapFloat(p->xmtrPwrMtrScale_); + p->vDbz0DeltaLimit_ = SwapFloat(p->vDbz0DeltaLimit_); + p->tarHDbz0Sp_ = SwapFloat(p->tarHDbz0Sp_); + p->tarVDbz0Sp_ = SwapFloat(p->tarVDbz0Sp_); + p->deltaprf_ = ntohl(p->deltaprf_); + p->tauSp_ = ntohl(p->tauSp_); + p->tauLp_ = ntohl(p->tauLp_); + p->ncDeadValue_ = ntohl(p->ncDeadValue_); + p->tauRfSp_ = ntohl(p->tauRfSp_); + p->tauRfLp_ = ntohl(p->tauRfLp_); + p->seg1Lim_ = SwapFloat(p->seg1Lim_); + p->slatsec_ = SwapFloat(p->slatsec_); + p->slonsec_ = SwapFloat(p->slonsec_); + p->slatdeg_ = ntohl(p->slatdeg_); + p->slatmin_ = ntohl(p->slatmin_); + p->slondeg_ = ntohl(p->slondeg_); + p->slonmin_ = ntohl(p->slonmin_); + p->digRcvrClockFreq_ = SwapDouble(p->digRcvrClockFreq_); + p->cohoFreq_ = SwapDouble(p->cohoFreq_); + p->azCorrectionFactor_ = SwapFloat(p->azCorrectionFactor_); + p->elCorrectionFactor_ = SwapFloat(p->elCorrectionFactor_); + p->antManualSetup_.ielmin_ = + static_cast(ntohl(p->antManualSetup_.ielmin_)); + p->antManualSetup_.ielmax_ = + static_cast(ntohl(p->antManualSetup_.ielmax_)); p->antManualSetup_.fazvelmax_ = ntohl(p->antManualSetup_.fazvelmax_); p->antManualSetup_.felvelmax_ = ntohl(p->antManualSetup_.felvelmax_); - p->antManualSetup_.igndHgt_ = ntohl(p->antManualSetup_.igndHgt_); - p->antManualSetup_.iradHgt_ = ntohl(p->antManualSetup_.iradHgt_); - p->azPosSustainDrive_ = SwapFloat(p->azPosSustainDrive_); - p->azNegSustainDrive_ = SwapFloat(p->azNegSustainDrive_); - p->azNomPosDriveSlope_ = SwapFloat(p->azNomPosDriveSlope_); - p->azNomNegDriveSlope_ = SwapFloat(p->azNomNegDriveSlope_); - p->azFeedbackSlope_ = SwapFloat(p->azFeedbackSlope_); - p->elPosSustainDrive_ = SwapFloat(p->elPosSustainDrive_); - p->elNegSustainDrive_ = SwapFloat(p->elNegSustainDrive_); - p->elNomPosDriveSlope_ = SwapFloat(p->elNomPosDriveSlope_); - p->elNomNegDriveSlope_ = SwapFloat(p->elNomNegDriveSlope_); - p->elFeedbackSlope_ = SwapFloat(p->elFeedbackSlope_); - p->elFirstSlope_ = SwapFloat(p->elFirstSlope_); - p->elSecondSlope_ = SwapFloat(p->elSecondSlope_); - p->elThirdSlope_ = SwapFloat(p->elThirdSlope_); - p->elDroopPos_ = SwapFloat(p->elDroopPos_); - p->elOffNeutralDrive_ = SwapFloat(p->elOffNeutralDrive_); - p->azIntertia_ = SwapFloat(p->azIntertia_); - p->elInertia_ = SwapFloat(p->elInertia_); - p->azStowAngle_ = SwapFloat(p->azStowAngle_); - p->elStowAngle_ = SwapFloat(p->elStowAngle_); - p->azEncoderAlignment_ = SwapFloat(p->azEncoderAlignment_); - p->elEncoderAlignment_ = SwapFloat(p->elEncoderAlignment_); - p->rvp8nvIwaveguideLength_ = ntohl(p->rvp8nvIwaveguideLength_); + p->antManualSetup_.igndHgt_ = + static_cast(ntohl(p->antManualSetup_.igndHgt_)); + p->antManualSetup_.iradHgt_ = ntohl(p->antManualSetup_.iradHgt_); + p->azPosSustainDrive_ = SwapFloat(p->azPosSustainDrive_); + p->azNegSustainDrive_ = SwapFloat(p->azNegSustainDrive_); + p->azNomPosDriveSlope_ = SwapFloat(p->azNomPosDriveSlope_); + p->azNomNegDriveSlope_ = SwapFloat(p->azNomNegDriveSlope_); + p->azFeedbackSlope_ = SwapFloat(p->azFeedbackSlope_); + p->elPosSustainDrive_ = SwapFloat(p->elPosSustainDrive_); + p->elNegSustainDrive_ = SwapFloat(p->elNegSustainDrive_); + p->elNomPosDriveSlope_ = SwapFloat(p->elNomPosDriveSlope_); + p->elNomNegDriveSlope_ = SwapFloat(p->elNomNegDriveSlope_); + p->elFeedbackSlope_ = SwapFloat(p->elFeedbackSlope_); + p->elFirstSlope_ = SwapFloat(p->elFirstSlope_); + p->elSecondSlope_ = SwapFloat(p->elSecondSlope_); + p->elThirdSlope_ = SwapFloat(p->elThirdSlope_); + p->elDroopPos_ = SwapFloat(p->elDroopPos_); + p->elOffNeutralDrive_ = SwapFloat(p->elOffNeutralDrive_); + p->azIntertia_ = SwapFloat(p->azIntertia_); + p->elInertia_ = SwapFloat(p->elInertia_); + p->azStowAngle_ = SwapFloat(p->azStowAngle_); + p->elStowAngle_ = SwapFloat(p->elStowAngle_); + p->azEncoderAlignment_ = SwapFloat(p->azEncoderAlignment_); + p->elEncoderAlignment_ = SwapFloat(p->elEncoderAlignment_); + p->rvp8nvIwaveguideLength_ = ntohl(p->rvp8nvIwaveguideLength_); SwapArray(p->vRnscale_); @@ -1660,6 +1668,4 @@ RdaAdaptationData::Create(Level2MessageHeader&& header, std::istream& is) return message; } -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp index 50ebd596..375d46b8 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp @@ -1,209 +1,214 @@ #include #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { static const std::string logPrefix_ = "scwx::wsr88d::rda::rda_status_data"; static const auto logger_ = util::Logger::Create(logPrefix_); -class RdaStatusDataImpl +class RdaStatusData::Impl { public: - explicit RdaStatusDataImpl() = default; - ~RdaStatusDataImpl() = default; + explicit Impl() = default; + ~Impl() = default; - uint16_t rdaStatus_ {0}; - uint16_t operabilityStatus_ {0}; - uint16_t controlStatus_ {0}; - uint16_t auxiliaryPowerGeneratorState_ {0}; - uint16_t averageTransmitterPower_ {0}; - int16_t horizontalReflectivityCalibrationCorrection_ {0}; - uint16_t dataTransmissionEnabled_ {0}; - uint16_t volumeCoveragePatternNumber_ {0}; - uint16_t rdaControlAuthorization_ {0}; - uint16_t rdaBuildNumber_ {0}; - uint16_t operationalMode_ {0}; - uint16_t superResolutionStatus_ {0}; - uint16_t clutterMitigationDecisionStatus_ {0}; - uint16_t rdaScanAndDataFlags_ {0}; - uint16_t rdaAlarmSummary_ {0}; - uint16_t commandAcknowledgement_ {0}; - uint16_t channelControlStatus_ {0}; - uint16_t spotBlankingStatus_ {0}; - uint16_t bypassMapGenerationDate_ {0}; - uint16_t bypassMapGenerationTime_ {0}; - uint16_t clutterFilterMapGenerationDate_ {0}; - uint16_t clutterFilterMapGenerationTime_ {0}; - int16_t verticalReflectivityCalibrationCorrection_ {0}; - uint16_t transitionPowerSourceStatus_ {0}; - uint16_t rmsControlStatus_ {0}; - uint16_t performanceCheckStatus_ {0}; - std::array alarmCodes_ {0}; - uint16_t signalProcessingOptions_ {0}; - uint16_t downloadedPatternNumber_ {0}; - uint16_t statusVersion_ {0}; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + std::uint16_t rdaStatus_ {0}; + std::uint16_t operabilityStatus_ {0}; + std::uint16_t controlStatus_ {0}; + std::uint16_t auxiliaryPowerGeneratorState_ {0}; + std::uint16_t averageTransmitterPower_ {0}; + std::int16_t horizontalReflectivityCalibrationCorrection_ {0}; + std::uint16_t dataTransmissionEnabled_ {0}; + std::uint16_t volumeCoveragePatternNumber_ {0}; + std::uint16_t rdaControlAuthorization_ {0}; + std::uint16_t rdaBuildNumber_ {0}; + std::uint16_t operationalMode_ {0}; + std::uint16_t superResolutionStatus_ {0}; + std::uint16_t clutterMitigationDecisionStatus_ {0}; + std::uint16_t rdaScanAndDataFlags_ {0}; + std::uint16_t rdaAlarmSummary_ {0}; + std::uint16_t commandAcknowledgement_ {0}; + std::uint16_t channelControlStatus_ {0}; + std::uint16_t spotBlankingStatus_ {0}; + std::uint16_t bypassMapGenerationDate_ {0}; + std::uint16_t bypassMapGenerationTime_ {0}; + std::uint16_t clutterFilterMapGenerationDate_ {0}; + std::uint16_t clutterFilterMapGenerationTime_ {0}; + std::int16_t verticalReflectivityCalibrationCorrection_ {0}; + std::uint16_t transitionPowerSourceStatus_ {0}; + std::uint16_t rmsControlStatus_ {0}; + std::uint16_t performanceCheckStatus_ {0}; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + std::array alarmCodes_ {0}; + + std::uint16_t signalProcessingOptions_ {0}; + std::uint16_t downloadedPatternNumber_ {0}; + std::uint16_t statusVersion_ {0}; }; -RdaStatusData::RdaStatusData() : - Level2Message(), p(std::make_unique()) -{ -} +RdaStatusData::RdaStatusData() : Level2Message(), p(std::make_unique()) {} RdaStatusData::~RdaStatusData() = default; RdaStatusData::RdaStatusData(RdaStatusData&&) noexcept = default; RdaStatusData& RdaStatusData::operator=(RdaStatusData&&) noexcept = default; -uint16_t RdaStatusData::rda_status() const +std::uint16_t RdaStatusData::rda_status() const { return p->rdaStatus_; } -uint16_t RdaStatusData::operability_status() const +std::uint16_t RdaStatusData::operability_status() const { return p->operabilityStatus_; } -uint16_t RdaStatusData::control_status() const +std::uint16_t RdaStatusData::control_status() const { return p->controlStatus_; } -uint16_t RdaStatusData::auxiliary_power_generator_state() const +std::uint16_t RdaStatusData::auxiliary_power_generator_state() const { return p->auxiliaryPowerGeneratorState_; } -uint16_t RdaStatusData::average_transmitter_power() const +std::uint16_t RdaStatusData::average_transmitter_power() const { return p->averageTransmitterPower_; } float RdaStatusData::horizontal_reflectivity_calibration_correction() const { - return p->horizontalReflectivityCalibrationCorrection_ * 0.01f; + constexpr float kScale_ = 0.01f; + return static_cast(p->horizontalReflectivityCalibrationCorrection_) * + kScale_; } -uint16_t RdaStatusData::data_transmission_enabled() const +std::uint16_t RdaStatusData::data_transmission_enabled() const { return p->dataTransmissionEnabled_; } -uint16_t RdaStatusData::volume_coverage_pattern_number() const +std::uint16_t RdaStatusData::volume_coverage_pattern_number() const { return p->volumeCoveragePatternNumber_; } -uint16_t RdaStatusData::rda_control_authorization() const +std::uint16_t RdaStatusData::rda_control_authorization() const { return p->rdaControlAuthorization_; } -uint16_t RdaStatusData::rda_build_number() const +std::uint16_t RdaStatusData::rda_build_number() const { return p->rdaBuildNumber_; } -uint16_t RdaStatusData::operational_mode() const +std::uint16_t RdaStatusData::operational_mode() const { return p->operationalMode_; } -uint16_t RdaStatusData::super_resolution_status() const +std::uint16_t RdaStatusData::super_resolution_status() const { return p->superResolutionStatus_; } -uint16_t RdaStatusData::clutter_mitigation_decision_status() const +std::uint16_t RdaStatusData::clutter_mitigation_decision_status() const { return p->clutterMitigationDecisionStatus_; } -uint16_t RdaStatusData::rda_scan_and_data_flags() const +std::uint16_t RdaStatusData::rda_scan_and_data_flags() const { return p->rdaScanAndDataFlags_; } -uint16_t RdaStatusData::rda_alarm_summary() const +std::uint16_t RdaStatusData::rda_alarm_summary() const { return p->rdaAlarmSummary_; } -uint16_t RdaStatusData::command_acknowledgement() const +std::uint16_t RdaStatusData::command_acknowledgement() const { return p->commandAcknowledgement_; } -uint16_t RdaStatusData::channel_control_status() const +std::uint16_t RdaStatusData::channel_control_status() const { return p->channelControlStatus_; } -uint16_t RdaStatusData::spot_blanking_status() const +std::uint16_t RdaStatusData::spot_blanking_status() const { return p->spotBlankingStatus_; } -uint16_t RdaStatusData::bypass_map_generation_date() const +std::uint16_t RdaStatusData::bypass_map_generation_date() const { return p->bypassMapGenerationDate_; } -uint16_t RdaStatusData::bypass_map_generation_time() const +std::uint16_t RdaStatusData::bypass_map_generation_time() const { return p->bypassMapGenerationTime_; } -uint16_t RdaStatusData::clutter_filter_map_generation_date() const +std::uint16_t RdaStatusData::clutter_filter_map_generation_date() const { return p->clutterFilterMapGenerationDate_; } -uint16_t RdaStatusData::clutter_filter_map_generation_time() const +std::uint16_t RdaStatusData::clutter_filter_map_generation_time() const { return p->clutterFilterMapGenerationTime_; } float RdaStatusData::vertical_reflectivity_calibration_correction() const { - return p->verticalReflectivityCalibrationCorrection_ * 0.01f; + constexpr float kScale_ = 0.01f; + return static_cast(p->verticalReflectivityCalibrationCorrection_) * + kScale_; } -uint16_t RdaStatusData::transition_power_source_status() const +std::uint16_t RdaStatusData::transition_power_source_status() const { return p->transitionPowerSourceStatus_; } -uint16_t RdaStatusData::rms_control_status() const +std::uint16_t RdaStatusData::rms_control_status() const { return p->rmsControlStatus_; } -uint16_t RdaStatusData::performance_check_status() const +std::uint16_t RdaStatusData::performance_check_status() const { return p->performanceCheckStatus_; } -uint16_t RdaStatusData::alarm_codes(unsigned i) const +std::uint16_t RdaStatusData::alarm_codes(unsigned i) const { - return p->alarmCodes_[i]; + return p->alarmCodes_.at(i); } -uint16_t RdaStatusData::signal_processing_options() const +std::uint16_t RdaStatusData::signal_processing_options() const { return p->signalProcessingOptions_; } -uint16_t RdaStatusData::downloaded_pattern_number() const +std::uint16_t RdaStatusData::downloaded_pattern_number() const { return p->downloadedPatternNumber_; } -uint16_t RdaStatusData::status_version() const +std::uint16_t RdaStatusData::status_version() const { return p->statusVersion_; } @@ -215,6 +220,7 @@ bool RdaStatusData::Parse(std::istream& is) bool messageValid = true; size_t bytesRead = 0; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Readability is.read(reinterpret_cast(&p->rdaStatus_), 2); // 1 is.read(reinterpret_cast(&p->operabilityStatus_), 2); // 2 is.read(reinterpret_cast(&p->controlStatus_), 2); // 3 @@ -249,7 +255,7 @@ bool RdaStatusData::Parse(std::istream& is) is.read(reinterpret_cast(&p->rmsControlStatus_), 2); // 25 is.read(reinterpret_cast(&p->performanceCheckStatus_), 2); // 26 is.read(reinterpret_cast(&p->alarmCodes_), - p->alarmCodes_.size() * 2); // 27-40 + static_cast(p->alarmCodes_.size() * 2)); // 27-40 bytesRead += 80; p->rdaStatus_ = ntohs(p->rdaStatus_); @@ -257,8 +263,8 @@ bool RdaStatusData::Parse(std::istream& is) p->controlStatus_ = ntohs(p->controlStatus_); p->auxiliaryPowerGeneratorState_ = ntohs(p->auxiliaryPowerGeneratorState_); p->averageTransmitterPower_ = ntohs(p->averageTransmitterPower_); - p->horizontalReflectivityCalibrationCorrection_ = - ntohs(p->horizontalReflectivityCalibrationCorrection_); + p->horizontalReflectivityCalibrationCorrection_ = static_cast( + ntohs(p->horizontalReflectivityCalibrationCorrection_)); p->dataTransmissionEnabled_ = ntohs(p->dataTransmissionEnabled_); p->volumeCoveragePatternNumber_ = ntohs(p->volumeCoveragePatternNumber_); p->rdaControlAuthorization_ = ntohs(p->rdaControlAuthorization_); @@ -278,15 +284,16 @@ bool RdaStatusData::Parse(std::istream& is) ntohs(p->clutterFilterMapGenerationDate_); p->clutterFilterMapGenerationTime_ = ntohs(p->clutterFilterMapGenerationTime_); - p->verticalReflectivityCalibrationCorrection_ = - ntohs(p->verticalReflectivityCalibrationCorrection_); + p->verticalReflectivityCalibrationCorrection_ = static_cast( + ntohs(p->verticalReflectivityCalibrationCorrection_)); p->transitionPowerSourceStatus_ = ntohs(p->transitionPowerSourceStatus_); p->rmsControlStatus_ = ntohs(p->rmsControlStatus_); p->performanceCheckStatus_ = ntohs(p->performanceCheckStatus_); SwapArray(p->alarmCodes_); // RDA Build 18.0 increased the size of the message from 80 to 120 bytes - if (header().message_size() * 2 > Level2MessageHeader::SIZE + 80) + if (static_cast(header().message_size()) * 2 > + Level2MessageHeader::SIZE + 80) { is.read(reinterpret_cast(&p->signalProcessingOptions_), 2); // 41 is.seekg(34, std::ios_base::cur); // 42-58 @@ -297,6 +304,8 @@ bool RdaStatusData::Parse(std::istream& is) p->signalProcessingOptions_ = ntohs(p->signalProcessingOptions_); p->statusVersion_ = ntohs(p->statusVersion_); } + + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) if (!ValidateMessage(is, bytesRead)) { @@ -320,6 +329,4 @@ RdaStatusData::Create(Level2MessageHeader&& header, std::istream& is) return message; } -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda From f709380a9714cb00b53a12330ec48a80dfeef621 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 13 May 2025 00:28:31 -0500 Subject: [PATCH 597/762] Add RdaPrfData (message type 32) --- wxdata/include/scwx/awips/message.hpp | 35 ++++- .../include/scwx/wsr88d/rda/rda_prf_data.hpp | 30 +++++ .../wsr88d/rda/level2_message_factory.cpp | 4 +- .../source/scwx/wsr88d/rda/rda_prf_data.cpp | 120 ++++++++++++++++++ wxdata/wxdata.cmake | 2 + 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp create mode 100644 wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp diff --git a/wxdata/include/scwx/awips/message.hpp b/wxdata/include/scwx/awips/message.hpp index f13a2a90..486e7f06 100644 --- a/wxdata/include/scwx/awips/message.hpp +++ b/wxdata/include/scwx/awips/message.hpp @@ -121,13 +121,44 @@ public: [](auto& p) { p.second = SwapFloat(p.second); }); } - static void SwapVector(std::vector& v) + template + static void SwapVector(std::vector& v) { std::transform(std::execution::par_unseq, v.begin(), v.end(), v.begin(), - [](std::uint16_t u) { return ntohs(u); }); + [](T u) + { + if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(ntohs(u)); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(ntohl(u)); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(ntohll(u)); + } + else if constexpr (std::is_same_v) + { + return SwapFloat(u); + } + else if constexpr (std::is_same_v) + { + return SwapDouble(u); + } + else + { + static_assert(std::is_same_v, + "Unsupported type for SwapVector"); + } + }); } private: diff --git a/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp new file mode 100644 index 00000000..1a04aacb --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace scwx::wsr88d::rda +{ + +class RdaPrfData : public Level2Message +{ +public: + explicit RdaPrfData(); + ~RdaPrfData(); + + RdaPrfData(const RdaPrfData&) = delete; + RdaPrfData& operator=(const RdaPrfData&) = delete; + + RdaPrfData(RdaPrfData&&) noexcept; + RdaPrfData& operator=(RdaPrfData&&) noexcept; + + bool Parse(std::istream& is) override; + + static std::shared_ptr Create(Level2MessageHeader&& header, + std::istream& is); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp index 2a478a2f..a659b734 100644 --- a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,8 @@ static const std::unordered_map {13, ClutterFilterBypassMap::Create}, {15, ClutterFilterMap::Create}, {18, RdaAdaptationData::Create}, - {31, DigitalRadarDataGeneric::Create}}; + {31, DigitalRadarDataGeneric::Create}, + {32, RdaPrfData::Create}}; struct Level2MessageFactory::Context { diff --git a/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp new file mode 100644 index 00000000..d516309b --- /dev/null +++ b/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp @@ -0,0 +1,120 @@ +#include +#include + +namespace scwx::wsr88d::rda +{ + +static const std::string logPrefix_ = "scwx::wsr88d::rda::rda_prf_data"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +struct RdaPrfWaveformData +{ + std::uint16_t waveformType_ {0}; + std::uint16_t prfCount_ {0}; + std::vector prfValues_ {}; +}; + +class RdaPrfData::Impl +{ +public: + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + std::uint16_t numberOfWaveforms_ {0}; + std::vector waveformData_ {}; +}; + +RdaPrfData::RdaPrfData() : p(std::make_unique()) {} +RdaPrfData::~RdaPrfData() = default; + +RdaPrfData::RdaPrfData(RdaPrfData&&) noexcept = default; +RdaPrfData& RdaPrfData::operator=(RdaPrfData&&) noexcept = default; + +bool RdaPrfData::Parse(std::istream& is) +{ + logger_->trace("Parsing RDA PRF Data (Message Type 32)"); + + bool messageValid = true; + std::size_t bytesRead = 0; + + std::streampos isBegin = is.tellg(); + + is.read(reinterpret_cast(&p->numberOfWaveforms_), 2); // 1 + is.seekg(2, std::ios_base::cur); // 2 + + bytesRead += 4; + + p->numberOfWaveforms_ = ntohs(p->numberOfWaveforms_); + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability + if (p->numberOfWaveforms_ < 1 || p->numberOfWaveforms_ > 5) + { + logger_->warn("Invalid number of waveforms: {}", p->numberOfWaveforms_); + p->numberOfWaveforms_ = 0; + messageValid = false; + } + + p->waveformData_.resize(p->numberOfWaveforms_); + + for (std::uint16_t i = 0; i < p->numberOfWaveforms_; ++i) + { + auto& w = p->waveformData_[i]; + + is.read(reinterpret_cast(&w.waveformType_), 2); // P1 + is.read(reinterpret_cast(&w.prfCount_), 2); // P2 + + w.waveformType_ = ntohs(w.waveformType_); + w.prfCount_ = ntohs(w.prfCount_); + + bytesRead += 4; + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability + if (w.prfCount_ > 255) + { + logger_->warn("Invalid PRF count: {} (waveform {})", w.prfCount_, i); + w.prfCount_ = 0; + messageValid = false; + break; + } + + w.prfValues_.resize(w.prfCount_); + + for (std::uint16_t j = 0; j < w.prfCount_; ++j) + { + is.read(reinterpret_cast(&w.prfValues_[j]), 4); + } + + bytesRead += static_cast(w.prfCount_) * 4; + + SwapVector(w.prfValues_); + } + + is.seekg(isBegin, std::ios_base::beg); + if (!ValidateMessage(is, bytesRead)) + { + messageValid = false; + } + + return messageValid; +} + +std::shared_ptr RdaPrfData::Create(Level2MessageHeader&& header, + std::istream& is) +{ + std::shared_ptr message = std::make_shared(); + message->set_header(std::move(header)); + + if (!message->Parse(is)) + { + message.reset(); + } + + return message; +} + +} // namespace scwx::wsr88d::rda diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 2c062f4b..6a02a265 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -125,6 +125,7 @@ set(HDR_WSR88D_RDA include/scwx/wsr88d/rda/clutter_filter_bypass_map.hpp include/scwx/wsr88d/rda/level2_message_header.hpp include/scwx/wsr88d/rda/performance_maintenance_data.hpp include/scwx/wsr88d/rda/rda_adaptation_data.hpp + include/scwx/wsr88d/rda/rda_prf_data.hpp include/scwx/wsr88d/rda/rda_status_data.hpp include/scwx/wsr88d/rda/rda_types.hpp include/scwx/wsr88d/rda/volume_coverage_pattern_data.hpp) @@ -138,6 +139,7 @@ set(SRC_WSR88D_RDA source/scwx/wsr88d/rda/clutter_filter_bypass_map.cpp source/scwx/wsr88d/rda/level2_message_header.cpp source/scwx/wsr88d/rda/performance_maintenance_data.cpp source/scwx/wsr88d/rda/rda_adaptation_data.cpp + source/scwx/wsr88d/rda/rda_prf_data.cpp source/scwx/wsr88d/rda/rda_status_data.cpp source/scwx/wsr88d/rda/volume_coverage_pattern_data.cpp) set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp From 926cce1eac5a33ecf0fb1d921bd36430d0745948 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 13 May 2025 21:41:53 -0500 Subject: [PATCH 598/762] Level2MessageFactory clang-tidy cleanup --- .../wsr88d/rda/level2_message_factory.hpp | 24 +++------- .../wsr88d/rda/level2_message_factory.cpp | 46 +++++++++---------- 2 files changed, 27 insertions(+), 43 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rda/level2_message_factory.hpp b/wxdata/include/scwx/wsr88d/rda/level2_message_factory.hpp index 7359e72b..ac2b8234 100644 --- a/wxdata/include/scwx/wsr88d/rda/level2_message_factory.hpp +++ b/wxdata/include/scwx/wsr88d/rda/level2_message_factory.hpp @@ -2,28 +2,19 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { struct Level2MessageInfo { - std::shared_ptr message; - bool headerValid; - bool messageValid; - - Level2MessageInfo() : - message(nullptr), headerValid(false), messageValid(false) - { - } + std::shared_ptr message {nullptr}; + bool headerValid {false}; + bool messageValid {false}; }; class Level2MessageFactory { -private: +public: explicit Level2MessageFactory() = delete; ~Level2MessageFactory() = delete; @@ -33,7 +24,6 @@ private: Level2MessageFactory(Level2MessageFactory&&) noexcept = delete; Level2MessageFactory& operator=(Level2MessageFactory&&) noexcept = delete; -public: struct Context; static std::shared_ptr CreateContext(); @@ -41,6 +31,4 @@ public: std::shared_ptr& ctx); }; -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda diff --git a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp index a659b734..b253cdf8 100644 --- a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp @@ -15,20 +15,16 @@ #include #include -namespace scwx -{ -namespace wsr88d -{ -namespace rda +namespace scwx::wsr88d::rda { static const std::string logPrefix_ = "scwx::wsr88d::rda::level2_message_factory"; static const auto logger_ = util::Logger::Create(logPrefix_); -typedef std::function(Level2MessageHeader&&, - std::istream&)> - CreateLevel2MessageFunction; +using CreateLevel2MessageFunction = + std::function(Level2MessageHeader&&, + std::istream&)>; static const std::unordered_map create_ {{1, DigitalRadarData::Create}, @@ -44,15 +40,12 @@ static const std::unordered_map struct Level2MessageFactory::Context { Context() : - messageData_ {}, - bufferedSize_ {}, - messageBuffer_ {messageData_}, - messageBufferStream_ {&messageBuffer_} + messageBuffer_ {messageData_}, messageBufferStream_ {&messageBuffer_} { } - std::vector messageData_; - size_t bufferedSize_; + std::vector messageData_ {}; + std::size_t bufferedSize_ {}; util::vectorbuf messageBuffer_; std::istream messageBufferStream_; bool bufferingData_ {false}; @@ -78,13 +71,16 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, if (info.headerValid) { - if (header.message_size() == 65535) + if (header.message_size() == std::numeric_limits::max()) { + // A message size of 65535 indicates a message with a single segment. + // The size is specified in the bytes normally reserved for the segment + // number and total number of segments. segment = 1; totalSegments = 1; dataSize = (static_cast(header.number_of_message_segments()) - << 16) + + << 16) + // NOLINT(cppcoreguidelines-avoid-magic-numbers) header.message_segment_number(); } else @@ -145,14 +141,16 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, logger_->debug("Bad size estimate, increasing size"); // Estimate remaining size - uint16_t remainingSegments = - std::max(totalSegments - segment + 1, 100u); - size_t remainingSize = remainingSegments * dataSize; + static const std::uint16_t kMinRemainingSegments_ = 100u; + std::uint16_t remainingSegments = std::max( + totalSegments - segment + 1, kMinRemainingSegments_); + std::size_t remainingSize = remainingSegments * dataSize; ctx->messageData_.resize(ctx->bufferedSize_ + remainingSize); } - is.read(ctx->messageData_.data() + ctx->bufferedSize_, dataSize); + is.read(&ctx->messageData_[ctx->bufferedSize_], + static_cast(dataSize)); ctx->bufferedSize_ += dataSize; if (is.eof()) @@ -166,7 +164,7 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, else if (segment == totalSegments) { ctx->messageBuffer_.update_read_pointers(ctx->bufferedSize_); - header.set_message_size(static_cast( + header.set_message_size(static_cast( ctx->bufferedSize_ / 2 + Level2MessageHeader::SIZE)); messageStream = &ctx->messageBufferStream_; @@ -188,7 +186,7 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, else if (info.headerValid) { // Seek to the end of the current message - is.seekg(dataSize, std::ios_base::cur); + is.seekg(static_cast(dataSize), std::ios_base::cur); } if (info.message == nullptr) @@ -199,6 +197,4 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, return info; } -} // namespace rda -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rda From ef7caf5519a8eadac197a0f65acafd222f8f5c2c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 13 May 2025 21:42:27 -0500 Subject: [PATCH 599/762] RadarProductManager logging level fix --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 81f0fc52..18af3b02 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1460,7 +1460,7 @@ RadarProductManagerImpl::StoreRadarProductRecord( if (storedRecord != nullptr) { - logger_->error( + logger_->debug( "Level 2 product previously loaded, loading from cache"); } } From e49adafda9d385b47514806edcbd4098e2c05638 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 16 May 2025 23:50:42 -0500 Subject: [PATCH 600/762] Update RPG to Build 23.0 --- .../rpg/digital_raster_data_array_packet.hpp | 48 ++++ .../rpg/digital_raster_data_array_packet.cpp | 220 ++++++++++++++++++ .../wsr88d/rpg/level3_message_factory.cpp | 5 + .../source/scwx/wsr88d/rpg/packet_factory.cpp | 2 + .../wsr88d/rpg/product_description_block.cpp | 46 ++-- wxdata/wxdata.cmake | 2 + 6 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp create mode 100644 wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp diff --git a/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp new file mode 100644 index 00000000..76b0f2c3 --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include +#include + +namespace scwx::wsr88d::rpg +{ + +class DigitalRasterDataArrayPacket : public Packet +{ +public: + explicit DigitalRasterDataArrayPacket(); + ~DigitalRasterDataArrayPacket(); + + DigitalRasterDataArrayPacket(const DigitalRasterDataArrayPacket&) = delete; + DigitalRasterDataArrayPacket& + operator=(const DigitalRasterDataArrayPacket&) = delete; + + DigitalRasterDataArrayPacket(DigitalRasterDataArrayPacket&&) noexcept; + DigitalRasterDataArrayPacket& + operator=(DigitalRasterDataArrayPacket&&) noexcept; + + [[nodiscard]] std::uint16_t packet_code() const override; + [[nodiscard]] std::uint16_t i_coordinate_start() const; + [[nodiscard]] std::uint16_t j_coordinate_start() const; + [[nodiscard]] std::uint16_t i_scale_factor() const; + [[nodiscard]] std::uint16_t j_scale_factor() const; + [[nodiscard]] std::uint16_t number_of_cells() const; + [[nodiscard]] std::uint16_t number_of_rows() const; + + [[nodiscard]] std::uint16_t number_of_bytes_in_row(std::uint16_t r) const; + [[nodiscard]] const std::vector& level(std::uint16_t r) const; + + [[nodiscard]] std::size_t data_size() const override; + + bool Parse(std::istream& is) override; + + static std::shared_ptr + Create(std::istream& is); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp new file mode 100644 index 00000000..ece33807 --- /dev/null +++ b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp @@ -0,0 +1,220 @@ +#include +#include + +#include +#include + +namespace scwx::wsr88d::rpg +{ + +static const std::string logPrefix_ = + "scwx::wsr88d::rpg::digital_raster_data_array_packet"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +class DigitalRasterDataArrayPacket::Impl +{ +public: + struct Row + { + std::uint16_t numberOfBytes_ {0}; + std::vector level_ {}; + }; + + explicit Impl() = default; + ~Impl() = default; + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + std::uint16_t packetCode_ {0}; + std::uint16_t iCoordinateStart_ {0}; + std::uint16_t jCoordinateStart_ {0}; + std::uint16_t iScaleFactor_ {0}; + std::uint16_t jScaleFactor_ {0}; + std::uint16_t numberOfCells_ {0}; + std::uint16_t numberOfRows_ {0}; + std::uint16_t numberOfBytesInRow_ {0}; + + // Repeat for each row + std::vector row_ {}; + + std::size_t dataSize_ {0}; +}; + +DigitalRasterDataArrayPacket::DigitalRasterDataArrayPacket() : + p(std::make_unique()) +{ +} +DigitalRasterDataArrayPacket::~DigitalRasterDataArrayPacket() = default; + +DigitalRasterDataArrayPacket::DigitalRasterDataArrayPacket( + DigitalRasterDataArrayPacket&&) noexcept = default; +DigitalRasterDataArrayPacket& DigitalRasterDataArrayPacket::operator=( + DigitalRasterDataArrayPacket&&) noexcept = default; + +std::uint16_t DigitalRasterDataArrayPacket::packet_code() const +{ + return p->packetCode_; +} + +std::uint16_t DigitalRasterDataArrayPacket::i_coordinate_start() const +{ + return p->iCoordinateStart_; +} + +std::uint16_t DigitalRasterDataArrayPacket::j_coordinate_start() const +{ + return p->jCoordinateStart_; +} + +std::uint16_t DigitalRasterDataArrayPacket::i_scale_factor() const +{ + return p->iScaleFactor_; +} + +std::uint16_t DigitalRasterDataArrayPacket::j_scale_factor() const +{ + return p->jScaleFactor_; +} + +std::uint16_t DigitalRasterDataArrayPacket::number_of_cells() const +{ + return p->numberOfCells_; +} + +std::uint16_t DigitalRasterDataArrayPacket::number_of_rows() const +{ + return p->numberOfRows_; +} + +std::uint16_t +DigitalRasterDataArrayPacket::number_of_bytes_in_row(std::uint16_t r) const +{ + return p->row_[r].numberOfBytes_; +} + +const std::vector& +DigitalRasterDataArrayPacket::level(std::uint16_t r) const +{ + return p->row_[r].level_; +} + +bool DigitalRasterDataArrayPacket::Parse(std::istream& is) +{ + bool blockValid = true; + std::size_t bytesRead = 0; + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + + is.read(reinterpret_cast(&p->packetCode_), 2); + is.read(reinterpret_cast(&p->iCoordinateStart_), 2); + is.read(reinterpret_cast(&p->jCoordinateStart_), 2); + is.read(reinterpret_cast(&p->iScaleFactor_), 2); + is.read(reinterpret_cast(&p->jScaleFactor_), 2); + is.read(reinterpret_cast(&p->numberOfCells_), 2); + is.read(reinterpret_cast(&p->numberOfRows_), 2); + bytesRead += 14; + + p->packetCode_ = ntohs(p->packetCode_); + p->iCoordinateStart_ = ntohs(p->iCoordinateStart_); + p->jCoordinateStart_ = ntohs(p->jCoordinateStart_); + p->iScaleFactor_ = ntohs(p->iScaleFactor_); + p->jScaleFactor_ = ntohs(p->jScaleFactor_); + p->numberOfCells_ = ntohs(p->numberOfCells_); + p->numberOfRows_ = ntohs(p->numberOfRows_); + + if (is.eof()) + { + logger_->debug("Reached end of file"); + blockValid = false; + } + else + { + if (p->packetCode_ != 33) + { + logger_->warn("Invalid packet code: {}", p->packetCode_); + blockValid = false; + } + if (p->numberOfCells_ < 1 || p->numberOfCells_ > 1840) + { + logger_->warn("Invalid number of cells: {}", p->numberOfCells_); + blockValid = false; + } + if (p->numberOfRows_ < 1 || p->numberOfRows_ > 464) + { + logger_->warn("Invalid number of rows: {}", p->numberOfRows_); + blockValid = false; + } + } + + if (blockValid) + { + p->row_.resize(p->numberOfRows_); + + for (std::uint16_t r = 0; r < p->numberOfRows_; r++) + { + auto& row = p->row_[r]; + + is.read(reinterpret_cast(&row.numberOfBytes_), 2); + bytesRead += 2; + + row.numberOfBytes_ = ntohs(row.numberOfBytes_); + + if (row.numberOfBytes_ < 1 || row.numberOfBytes_ > 1840) + { + logger_->warn( + "Invalid number of bytes: {} (Row {})", row.numberOfBytes_, r); + blockValid = false; + break; + } + else if (row.numberOfBytes_ < p->numberOfCells_) + { + logger_->warn("Number of bytes < number of cells: {} < {} (Row {})", + row.numberOfBytes_, + p->numberOfCells_, + r); + blockValid = false; + break; + } + + // Read raster bins + std::size_t dataSize = p->numberOfCells_; + row.level_.resize(dataSize); + is.read(reinterpret_cast(row.level_.data()), + static_cast(dataSize)); + + is.seekg(static_cast(row.numberOfBytes_ - dataSize), + std::ios_base::cur); + bytesRead += row.numberOfBytes_; + } + } + + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + p->dataSize_ = bytesRead; + + if (!ValidateMessage(is, bytesRead)) + { + blockValid = false; + } + + return blockValid; +} + +std::shared_ptr +DigitalRasterDataArrayPacket::Create(std::istream& is) +{ + std::shared_ptr packet = + std::make_shared(); + + if (!packet->Parse(is)) + { + packet.reset(); + } + + return packet; +} + +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp index 25675470..77af1672 100644 --- a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp @@ -119,9 +119,14 @@ static const std::unordered_map // {182, GraphicProductMessage::Create}, {184, GraphicProductMessage::Create}, {186, GraphicProductMessage::Create}, + {189, GraphicProductMessage::Create}, + {190, GraphicProductMessage::Create}, + {191, GraphicProductMessage::Create}, + {192, GraphicProductMessage::Create}, {193, GraphicProductMessage::Create}, {195, GraphicProductMessage::Create}, {196, GraphicProductMessage::Create}, + {197, GraphicProductMessage::Create}, {202, GraphicProductMessage::Create}}; std::shared_ptr Level3MessageFactory::Create(std::istream& is) diff --git a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp index e3cd034b..eb85ebb4 100644 --- a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,7 @@ static const std::unordered_map create_ { {26, PointGraphicSymbolPacket::Create}, {28, GenericDataPacket::Create}, {29, GenericDataPacket::Create}, + {33, DigitalRasterDataArrayPacket::Create}, {0x0802, SetColorLevelPacket::Create}, {0x0E03, LinkedContourVectorPacket::Create}, {0x3501, UnlinkedContourVectorPacket::Create}, diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 10fdbe72..d9417d27 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -21,28 +21,13 @@ static const std::string logPrefix_ = static const auto logger_ = util::Logger::Create(logPrefix_); static const std::set compressedProducts_ = { - 32, 94, 99, 134, 135, 138, 149, 152, 153, 154, 155, - 159, 161, 163, 165, 167, 168, 170, 172, 173, 174, 175, - 176, 177, 178, 179, 180, 182, 186, 193, 195, 202}; + 32, 94, 99, 113, 134, 135, 138, 149, 152, 153, 154, 155, 159, + 161, 163, 165, 167, 168, 170, 172, 173, 174, 175, 176, 177, 178, + 179, 180, 182, 186, 189, 190, 191, 192, 193, 195, 197, 202}; -static const std::set uncodedDataLevelProducts_ = {32, - 34, - 81, - 93, - 94, - 99, - 134, - 135, - 138, - 153, - 154, - 155, - 159, - 161, - 163, - 177, - 193, - 195}; +static const std::set uncodedDataLevelProducts_ = { + 32, 34, 81, 93, 94, 99, 134, 135, 138, 153, 154, 155, + 159, 161, 163, 177, 189, 190, 191, 192, 193, 195, 197}; static const std::unordered_map rangeMap_ { {19, 230}, {20, 460}, {27, 230}, {30, 230}, {31, 230}, {32, 230}, @@ -57,7 +42,8 @@ static const std::unordered_map rangeMap_ { {163, 300}, {165, 300}, {166, 230}, {167, 300}, {168, 300}, {169, 230}, {170, 230}, {171, 230}, {172, 230}, {173, 230}, {174, 230}, {175, 230}, {176, 230}, {177, 230}, {178, 300}, {179, 300}, {180, 89}, {181, 89}, - {182, 89}, {184, 89}, {186, 417}, {193, 460}, {195, 460}, {196, 50}}; + {182, 89}, {184, 89}, {186, 417}, {193, 460}, {195, 460}, {196, 50}, + {197, 230}}; static const std::unordered_map xResolutionMap_ { {19, 1000}, {20, 2000}, {27, 1000}, {30, 1000}, {31, 2000}, {32, 1000}, @@ -71,7 +57,7 @@ static const std::unordered_map xResolutionMap_ { {166, 250}, {167, 250}, {168, 250}, {169, 2000}, {170, 250}, {171, 2000}, {172, 250}, {173, 250}, {174, 250}, {175, 250}, {176, 250}, {177, 250}, {178, 1000}, {179, 1000}, {180, 150}, {181, 150}, {182, 150}, {184, 150}, - {186, 300}, {193, 250}, {195, 1000}}; + {186, 300}, {193, 250}, {195, 1000}, {197, 250}}; static const std::unordered_map yResolutionMap_ {{37, 1000}, {38, 4000}, @@ -86,7 +72,11 @@ static const std::unordered_map yResolutionMap_ {{37, 1000}, {90, 4000}, {97, 1000}, {98, 4000}, - {166, 250}}; + {166, 250}, + {189, 20}, + {190, 20}, + {191, 20}, + {192, 20}}; // GR uses different internal units than defined units in level 3 products static const std::unordered_map grScale_ { @@ -580,6 +570,10 @@ uint16_t ProductDescriptionBlock::number_of_levels() const break; case 134: + case 189: + case 190: + case 191: + case 192: numberOfLevels = 256; break; @@ -864,6 +858,10 @@ ProductDescriptionBlock::data_level_code(std::uint8_t level) const case 163: case 167: case 168: + case 189: + case 190: + case 191: + case 192: case 195: switch (level) { diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 6a02a265..a30398a0 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -147,6 +147,7 @@ set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp include/scwx/wsr88d/rpg/cell_trend_volume_scan_times.hpp include/scwx/wsr88d/rpg/digital_precipitation_data_array_packet.hpp include/scwx/wsr88d/rpg/digital_radial_data_array_packet.hpp + include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp include/scwx/wsr88d/rpg/general_status_message.hpp include/scwx/wsr88d/rpg/generic_data_packet.hpp include/scwx/wsr88d/rpg/generic_radial_data_packet.hpp @@ -188,6 +189,7 @@ set(SRC_WSR88D_RPG source/scwx/wsr88d/rpg/ccb_header.cpp source/scwx/wsr88d/rpg/cell_trend_volume_scan_times.cpp source/scwx/wsr88d/rpg/digital_precipitation_data_array_packet.cpp source/scwx/wsr88d/rpg/digital_radial_data_array_packet.cpp + source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp source/scwx/wsr88d/rpg/general_status_message.cpp source/scwx/wsr88d/rpg/generic_data_packet.cpp source/scwx/wsr88d/rpg/generic_radial_data_packet.cpp From 68a5baa5c4db933592a0f35c3bbdf3ed80c8ea53 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 00:18:07 -0500 Subject: [PATCH 601/762] RPG clang-tidy cleanup --- .../wsr88d/rpg/level3_message_factory.hpp | 17 +- .../scwx/wsr88d/rpg/packet_factory.hpp | 17 +- .../wsr88d/rpg/product_description_block.hpp | 69 +++-- .../wsr88d/rpg/level3_message_factory.cpp | 20 +- .../source/scwx/wsr88d/rpg/packet_factory.cpp | 16 +- .../wsr88d/rpg/product_description_block.cpp | 288 ++++++++++-------- 6 files changed, 223 insertions(+), 204 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/level3_message_factory.hpp b/wxdata/include/scwx/wsr88d/rpg/level3_message_factory.hpp index c2556434..2a5e42ca 100644 --- a/wxdata/include/scwx/wsr88d/rpg/level3_message_factory.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/level3_message_factory.hpp @@ -2,29 +2,22 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rpg +namespace scwx::wsr88d::rpg { class Level3MessageFactory { -private: +public: explicit Level3MessageFactory() = delete; ~Level3MessageFactory() = delete; - Level3MessageFactory(const Level3MessageFactory&) = delete; + Level3MessageFactory(const Level3MessageFactory&) = delete; Level3MessageFactory& operator=(const Level3MessageFactory&) = delete; - Level3MessageFactory(Level3MessageFactory&&) noexcept = delete; + Level3MessageFactory(Level3MessageFactory&&) noexcept = delete; Level3MessageFactory& operator=(Level3MessageFactory&&) noexcept = delete; -public: static std::shared_ptr Create(std::istream& is); }; -} // namespace rpg -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/include/scwx/wsr88d/rpg/packet_factory.hpp b/wxdata/include/scwx/wsr88d/rpg/packet_factory.hpp index 889b8c3d..11e794e8 100644 --- a/wxdata/include/scwx/wsr88d/rpg/packet_factory.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/packet_factory.hpp @@ -2,29 +2,22 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rpg +namespace scwx::wsr88d::rpg { class PacketFactory { -private: +public: explicit PacketFactory() = delete; ~PacketFactory() = delete; - PacketFactory(const PacketFactory&) = delete; + PacketFactory(const PacketFactory&) = delete; PacketFactory& operator=(const PacketFactory&) = delete; - PacketFactory(PacketFactory&&) noexcept = delete; + PacketFactory(PacketFactory&&) noexcept = delete; PacketFactory& operator=(PacketFactory&&) noexcept = delete; -public: static std::shared_ptr Create(std::istream& is); }; -} // namespace rpg -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp b/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp index 30bdfdf2..a306460e 100644 --- a/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/product_description_block.hpp @@ -12,8 +12,6 @@ namespace scwx::wsr88d::rpg { -class ProductDescriptionBlockImpl; - class ProductDescriptionBlock : public awips::Message { public: @@ -26,38 +24,38 @@ public: ProductDescriptionBlock(ProductDescriptionBlock&&) noexcept; ProductDescriptionBlock& operator=(ProductDescriptionBlock&&) noexcept; - [[nodiscard]] int16_t block_divider() const; - [[nodiscard]] float latitude_of_radar() const; - [[nodiscard]] float longitude_of_radar() const; - [[nodiscard]] int16_t height_of_radar() const; - [[nodiscard]] int16_t product_code() const; - [[nodiscard]] uint16_t operational_mode() const; - [[nodiscard]] uint16_t volume_coverage_pattern() const; - [[nodiscard]] int16_t sequence_number() const; - [[nodiscard]] uint16_t volume_scan_number() const; - [[nodiscard]] uint16_t volume_scan_date() const; - [[nodiscard]] uint32_t volume_scan_start_time() const; - [[nodiscard]] uint16_t generation_date_of_product() const; - [[nodiscard]] uint32_t generation_time_of_product() const; - [[nodiscard]] uint16_t elevation_number() const; - [[nodiscard]] uint16_t data_level_threshold(size_t i) const; - [[nodiscard]] uint8_t version() const; - [[nodiscard]] uint8_t spot_blank() const; - [[nodiscard]] uint32_t offset_to_symbology() const; - [[nodiscard]] uint32_t offset_to_graphic() const; - [[nodiscard]] uint32_t offset_to_tabular() const; + [[nodiscard]] std::int16_t block_divider() const; + [[nodiscard]] float latitude_of_radar() const; + [[nodiscard]] float longitude_of_radar() const; + [[nodiscard]] std::int16_t height_of_radar() const; + [[nodiscard]] std::int16_t product_code() const; + [[nodiscard]] std::uint16_t operational_mode() const; + [[nodiscard]] std::uint16_t volume_coverage_pattern() const; + [[nodiscard]] std::int16_t sequence_number() const; + [[nodiscard]] std::uint16_t volume_scan_number() const; + [[nodiscard]] std::uint16_t volume_scan_date() const; + [[nodiscard]] std::uint32_t volume_scan_start_time() const; + [[nodiscard]] std::uint16_t generation_date_of_product() const; + [[nodiscard]] std::uint32_t generation_time_of_product() const; + [[nodiscard]] std::uint16_t elevation_number() const; + [[nodiscard]] std::uint16_t data_level_threshold(size_t i) const; + [[nodiscard]] std::uint8_t version() const; + [[nodiscard]] std::uint8_t spot_blank() const; + [[nodiscard]] std::uint32_t offset_to_symbology() const; + [[nodiscard]] std::uint32_t offset_to_graphic() const; + [[nodiscard]] std::uint32_t offset_to_tabular() const; - [[nodiscard]] float range() const; - [[nodiscard]] uint16_t range_raw() const; - [[nodiscard]] float x_resolution() const; - [[nodiscard]] uint16_t x_resolution_raw() const; - [[nodiscard]] float y_resolution() const; - [[nodiscard]] uint16_t y_resolution_raw() const; + [[nodiscard]] float range() const; + [[nodiscard]] std::uint16_t range_raw() const; + [[nodiscard]] float x_resolution() const; + [[nodiscard]] std::uint16_t x_resolution_raw() const; + [[nodiscard]] float y_resolution() const; + [[nodiscard]] std::uint16_t y_resolution_raw() const; - [[nodiscard]] uint16_t threshold() const; - [[nodiscard]] float offset() const; - [[nodiscard]] float scale() const; - [[nodiscard]] uint16_t number_of_levels() const; + [[nodiscard]] std::uint16_t threshold() const; + [[nodiscard]] float offset() const; + [[nodiscard]] float scale() const; + [[nodiscard]] std::uint16_t number_of_levels() const; [[nodiscard]] std::optional data_level_code(std::uint8_t level) const; @@ -78,14 +76,15 @@ public: [[nodiscard]] bool IsCompressionEnabled() const; [[nodiscard]] bool IsDataLevelCoded() const; - [[nodiscard]] size_t data_size() const override; + [[nodiscard]] std::size_t data_size() const override; bool Parse(std::istream& is) override; - static constexpr size_t SIZE = 102u; + static constexpr std::size_t SIZE = 102u; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp index 77af1672..0d9d1725 100644 --- a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp @@ -9,22 +9,17 @@ #include #include -#include -namespace scwx -{ -namespace wsr88d -{ -namespace rpg +namespace scwx::wsr88d::rpg { static const std::string logPrefix_ = "scwx::wsr88d::rpg::level3_message_factory"; static const auto logger_ = util::Logger::Create(logPrefix_); -typedef std::function(Level3MessageHeader&&, - std::istream&)> - CreateLevel3MessageFunction; +using CreateLevel3MessageFunction = + std::function(Level3MessageHeader&&, + std::istream&)>; static const std::unordered_map // create_ {{2, GeneralStatusMessage::Create}, @@ -154,13 +149,12 @@ std::shared_ptr Level3MessageFactory::Create(std::istream& is) else if (headerValid) { // Seek to the end of the current message - is.seekg(header.length_of_message() - Level3MessageHeader::SIZE, + is.seekg(static_cast(header.length_of_message()) - + static_cast(Level3MessageHeader::SIZE), std::ios_base::cur); } return message; } -} // namespace rpg -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp index eb85ebb4..5458119c 100644 --- a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp @@ -28,18 +28,14 @@ #include -namespace scwx -{ -namespace wsr88d -{ -namespace rpg +namespace scwx::wsr88d::rpg { static const std::string logPrefix_ = "scwx::wsr88d::rpg::packet_factory"; static const auto logger_ = util::Logger::Create(logPrefix_); -typedef std::function(std::istream&)> - CreateMessageFunction; +using CreateMessageFunction = + std::function(std::istream&)>; static const std::unordered_map create_ { {1, TextAndSpecialSymbolPacket::Create}, @@ -83,7 +79,7 @@ std::shared_ptr PacketFactory::Create(std::istream& is) std::shared_ptr packet = nullptr; bool packetValid = true; - uint16_t packetCode; + std::uint16_t packetCode {0}; is.read(reinterpret_cast(&packetCode), 2); packetCode = ntohs(packetCode); @@ -110,6 +106,4 @@ std::shared_ptr PacketFactory::Create(std::istream& is) return packet; } -} // namespace rpg -} // namespace wsr88d -} // namespace scwx +} // namespace scwx::wsr88d::rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index d9417d27..85550966 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -95,73 +96,57 @@ static const std::unordered_map grScale_ { {174, ((units::inches {1} * 0.01f) / units::millimeters {1})}, {175, ((units::inches {1} * 0.01f) / units::millimeters {1})}}; -class ProductDescriptionBlockImpl +class ProductDescriptionBlock::Impl { public: - explicit ProductDescriptionBlockImpl() : - blockDivider_ {0}, - latitudeOfRadar_ {0}, - longitudeOfRadar_ {0}, - heightOfRadar_ {0}, - productCode_ {0}, - operationalMode_ {0}, - volumeCoveragePattern_ {0}, - sequenceNumber_ {0}, - volumeScanNumber_ {0}, - volumeScanDate_ {0}, - volumeScanStartTime_ {0}, - generationDateOfProduct_ {0}, - generationTimeOfProduct_ {0}, - elevationNumber_ {0}, - version_ {0}, - spotBlank_ {0}, - offsetToSymbology_ {0}, - offsetToGraphic_ {0}, - offsetToTabular_ {0}, - parameters_ {0}, - halfwords_ {0} - { - } - ~ProductDescriptionBlockImpl() = default; + explicit Impl() = default; + ~Impl() = default; - uint16_t halfword(size_t i); + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; - int16_t blockDivider_; - int32_t latitudeOfRadar_; - int32_t longitudeOfRadar_; - int16_t heightOfRadar_; - int16_t productCode_; - uint16_t operationalMode_; - uint16_t volumeCoveragePattern_; - int16_t sequenceNumber_; - uint16_t volumeScanNumber_; - uint16_t volumeScanDate_; - uint32_t volumeScanStartTime_; - uint16_t generationDateOfProduct_; - uint32_t generationTimeOfProduct_; + std::uint16_t halfword(std::size_t i); + + std::int16_t blockDivider_ {0}; + std::int32_t latitudeOfRadar_ {0}; + std::int32_t longitudeOfRadar_ {0}; + std::int16_t heightOfRadar_ {0}; + std::int16_t productCode_ {0}; + std::uint16_t operationalMode_ {0}; + std::uint16_t volumeCoveragePattern_ {0}; + std::int16_t sequenceNumber_ {0}; + std::uint16_t volumeScanNumber_ {0}; + std::uint16_t volumeScanDate_ {0}; + std::uint32_t volumeScanStartTime_ {0}; + std::uint16_t generationDateOfProduct_ {0}; + std::uint32_t generationTimeOfProduct_ {0}; // 27-28: Product dependent parameters 1 and 2 (Table V) - uint16_t elevationNumber_; + std::uint16_t elevationNumber_ {0}; // 30: Product dependent parameter 3 (Table V) // 31-46: Product dependent (Note 1) // 47-53: Product dependent parameters 4-10 (Table V, Note 3) - uint8_t version_; - uint8_t spotBlank_; - uint32_t offsetToSymbology_; - uint32_t offsetToGraphic_; - uint32_t offsetToTabular_; + std::uint8_t version_ {0}; + std::uint8_t spotBlank_ {0}; + std::uint32_t offsetToSymbology_ {0}; + std::uint32_t offsetToGraphic_ {0}; + std::uint32_t offsetToTabular_ {0}; - std::array parameters_; - std::array halfwords_; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + std::array parameters_ {0}; + std::array halfwords_ {0}; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) }; -uint16_t ProductDescriptionBlockImpl::halfword(size_t i) +std::uint16_t ProductDescriptionBlock::Impl::halfword(std::size_t i) { // Halfwords start at halfword 31 - return halfwords_[i - 31]; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return halfwords_.at(i - 31); } -ProductDescriptionBlock::ProductDescriptionBlock() : - p(std::make_unique()) +ProductDescriptionBlock::ProductDescriptionBlock() : p(std::make_unique()) { } ProductDescriptionBlock::~ProductDescriptionBlock() = default; @@ -171,102 +156,104 @@ ProductDescriptionBlock::ProductDescriptionBlock( ProductDescriptionBlock& ProductDescriptionBlock::operator=( ProductDescriptionBlock&&) noexcept = default; -int16_t ProductDescriptionBlock::block_divider() const +std::int16_t ProductDescriptionBlock::block_divider() const { return p->blockDivider_; } float ProductDescriptionBlock::latitude_of_radar() const { - return p->latitudeOfRadar_ * 0.001f; + static constexpr float kScale = 0.001f; + return static_cast(p->latitudeOfRadar_) * kScale; } float ProductDescriptionBlock::longitude_of_radar() const { - return p->longitudeOfRadar_ * 0.001f; + static constexpr float kScale = 0.001f; + return static_cast(p->longitudeOfRadar_) * kScale; } -int16_t ProductDescriptionBlock::height_of_radar() const +std::int16_t ProductDescriptionBlock::height_of_radar() const { return p->heightOfRadar_; } -int16_t ProductDescriptionBlock::product_code() const +std::int16_t ProductDescriptionBlock::product_code() const { return p->productCode_; } -uint16_t ProductDescriptionBlock::operational_mode() const +std::uint16_t ProductDescriptionBlock::operational_mode() const { return p->operationalMode_; } -uint16_t ProductDescriptionBlock::volume_coverage_pattern() const +std::uint16_t ProductDescriptionBlock::volume_coverage_pattern() const { return p->volumeCoveragePattern_; } -int16_t ProductDescriptionBlock::sequence_number() const +std::int16_t ProductDescriptionBlock::sequence_number() const { return p->sequenceNumber_; } -uint16_t ProductDescriptionBlock::volume_scan_number() const +std::uint16_t ProductDescriptionBlock::volume_scan_number() const { return p->volumeScanNumber_; } -uint16_t ProductDescriptionBlock::volume_scan_date() const +std::uint16_t ProductDescriptionBlock::volume_scan_date() const { return p->volumeScanDate_; } -uint32_t ProductDescriptionBlock::volume_scan_start_time() const +std::uint32_t ProductDescriptionBlock::volume_scan_start_time() const { return p->volumeScanStartTime_; } -uint16_t ProductDescriptionBlock::generation_date_of_product() const +std::uint16_t ProductDescriptionBlock::generation_date_of_product() const { return p->generationDateOfProduct_; } -uint32_t ProductDescriptionBlock::generation_time_of_product() const +std::uint32_t ProductDescriptionBlock::generation_time_of_product() const { return p->generationTimeOfProduct_; } -uint16_t ProductDescriptionBlock::elevation_number() const +std::uint16_t ProductDescriptionBlock::elevation_number() const { return p->elevationNumber_; } -uint16_t ProductDescriptionBlock::data_level_threshold(size_t i) const +std::uint16_t ProductDescriptionBlock::data_level_threshold(std::size_t i) const { - return p->halfwords_[i]; + return p->halfwords_.at(i); } -uint8_t ProductDescriptionBlock::version() const +std::uint8_t ProductDescriptionBlock::version() const { return p->version_; } -uint8_t ProductDescriptionBlock::spot_blank() const +std::uint8_t ProductDescriptionBlock::spot_blank() const { return p->spotBlank_; } -uint32_t ProductDescriptionBlock::offset_to_symbology() const +std::uint32_t ProductDescriptionBlock::offset_to_symbology() const { return p->offsetToSymbology_; } -uint32_t ProductDescriptionBlock::offset_to_graphic() const +std::uint32_t ProductDescriptionBlock::offset_to_graphic() const { return p->offsetToGraphic_; } -uint32_t ProductDescriptionBlock::offset_to_tabular() const +std::uint32_t ProductDescriptionBlock::offset_to_tabular() const { return p->offsetToTabular_; } @@ -276,14 +263,14 @@ float ProductDescriptionBlock::range() const return range_raw(); } -uint16_t ProductDescriptionBlock::range_raw() const +std::uint16_t ProductDescriptionBlock::range_raw() const { - uint16_t range = 0; + std::uint16_t range = 0; auto it = rangeMap_.find(p->productCode_); if (it != rangeMap_.cend()) { - range = static_cast(it->second); + range = static_cast(it->second); } return range; @@ -291,17 +278,18 @@ uint16_t ProductDescriptionBlock::range_raw() const float ProductDescriptionBlock::x_resolution() const { - return x_resolution_raw() * 0.001f; + static constexpr float kScale = 0.001f; + return static_cast(x_resolution_raw()) * kScale; } -uint16_t ProductDescriptionBlock::x_resolution_raw() const +std::uint16_t ProductDescriptionBlock::x_resolution_raw() const { - uint16_t xResolution = 0; + std::uint16_t xResolution = 0; auto it = xResolutionMap_.find(p->productCode_); if (it != xResolutionMap_.cend()) { - xResolution = static_cast(it->second); + xResolution = static_cast(it->second); } return xResolution; @@ -309,25 +297,28 @@ uint16_t ProductDescriptionBlock::x_resolution_raw() const float ProductDescriptionBlock::y_resolution() const { - return y_resolution_raw() * 0.001f; + static constexpr float kScale = 0.001f; + return static_cast(y_resolution_raw()) * kScale; } -uint16_t ProductDescriptionBlock::y_resolution_raw() const +std::uint16_t ProductDescriptionBlock::y_resolution_raw() const { - uint16_t yResolution = 0; + std::uint16_t yResolution = 0; auto it = yResolutionMap_.find(p->productCode_); if (it != yResolutionMap_.cend()) { - yResolution = static_cast(it->second); + yResolution = static_cast(it->second); } return yResolution; } -uint16_t ProductDescriptionBlock::threshold() const +std::uint16_t ProductDescriptionBlock::threshold() const { - uint16_t threshold = 1; + std::uint16_t threshold = 1; + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) switch (p->productCode_) { @@ -384,6 +375,8 @@ uint16_t ProductDescriptionBlock::threshold() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return threshold; } @@ -391,6 +384,8 @@ float ProductDescriptionBlock::offset() const { float offset = 0.0f; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 32: @@ -406,7 +401,8 @@ float ProductDescriptionBlock::offset() const case 186: case 193: case 195: - offset = static_cast(p->halfword(31)) * 0.1f; + offset = + static_cast(static_cast(p->halfword(31))) * 0.1f; break; case 134: @@ -414,7 +410,7 @@ float ProductDescriptionBlock::offset() const break; case 135: - offset = static_cast(p->halfword(33)); + offset = static_cast(p->halfword(33)); break; case 159: @@ -435,6 +431,8 @@ float ProductDescriptionBlock::offset() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return offset; } @@ -442,6 +440,8 @@ float ProductDescriptionBlock::scale() const { float scale = 1.0f; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 32: @@ -456,11 +456,11 @@ float ProductDescriptionBlock::scale() const case 186: case 193: case 195: - scale = p->halfword(32) * 0.1f; + scale = static_cast(p->halfword(32)) * 0.1f; break; case 81: - scale = p->halfword(32) * 0.001f; + scale = static_cast(p->halfword(32)) * 0.001f; break; case 134: @@ -472,7 +472,7 @@ float ProductDescriptionBlock::scale() const break; case 138: - scale = p->halfword(32) * 0.01f; + scale = static_cast(p->halfword(32)) * 0.01f; break; case 159: @@ -493,12 +493,16 @@ float ProductDescriptionBlock::scale() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return scale; } -uint16_t ProductDescriptionBlock::number_of_levels() const +std::uint16_t ProductDescriptionBlock::number_of_levels() const { - uint16_t numberOfLevels = 16u; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + + std::uint16_t numberOfLevels = 16u; switch (p->productCode_) { @@ -613,6 +617,8 @@ uint16_t ProductDescriptionBlock::number_of_levels() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return numberOfLevels; } @@ -620,6 +626,8 @@ std::uint16_t ProductDescriptionBlock::log_start() const { std::uint16_t logStart = std::numeric_limits::max(); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 134: @@ -630,6 +638,8 @@ std::uint16_t ProductDescriptionBlock::log_start() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return logStart; } @@ -637,6 +647,8 @@ float ProductDescriptionBlock::log_offset() const { float logOffset = 0.0f; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 134: @@ -647,6 +659,8 @@ float ProductDescriptionBlock::log_offset() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return logOffset; } @@ -654,6 +668,8 @@ float ProductDescriptionBlock::log_scale() const { float logScale = 1.0f; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 134: @@ -664,6 +680,8 @@ float ProductDescriptionBlock::log_scale() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return logScale; } @@ -682,6 +700,8 @@ float ProductDescriptionBlock::gr_scale() const std::uint8_t ProductDescriptionBlock::data_mask() const { + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + std::uint8_t dataMask = 0xff; switch (p->productCode_) @@ -694,6 +714,8 @@ std::uint8_t ProductDescriptionBlock::data_mask() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return dataMask; } @@ -701,6 +723,8 @@ std::uint8_t ProductDescriptionBlock::topped_mask() const { std::uint8_t toppedMask = 0x00; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 135: @@ -711,6 +735,8 @@ std::uint8_t ProductDescriptionBlock::topped_mask() const break; } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return toppedMask; } @@ -722,7 +748,7 @@ units::angle::degrees ProductDescriptionBlock::elevation() const { // Elevation is given in tenths of a degree // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) - elevation = static_cast(p->parameters_[2]) * 0.1; + elevation = static_cast(p->parameters_[2]) * 0.1; } return units::angle::degrees {elevation}; @@ -737,11 +763,15 @@ bool ProductDescriptionBlock::IsCompressionEnabled() const { bool isCompressed = false; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + if (compressedProducts_.contains(p->productCode_)) { isCompressed = (p->parameters_[7] == 1u); } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return isCompressed; } @@ -750,7 +780,7 @@ bool ProductDescriptionBlock::IsDataLevelCoded() const return !uncodedDataLevelProducts_.contains(p->productCode_); } -size_t ProductDescriptionBlock::data_size() const +std::size_t ProductDescriptionBlock::data_size() const { return SIZE; } @@ -761,6 +791,8 @@ bool ProductDescriptionBlock::Parse(std::istream& is) const std::streampos blockStart = is.tellg(); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + is.read(reinterpret_cast(&p->blockDivider_), 2); // 10 is.read(reinterpret_cast(&p->latitudeOfRadar_), 4); // 11-12 is.read(reinterpret_cast(&p->longitudeOfRadar_), 4); // 13-14 @@ -774,27 +806,31 @@ bool ProductDescriptionBlock::Parse(std::istream& is) is.read(reinterpret_cast(&p->volumeScanStartTime_), 4); // 22-23 is.read(reinterpret_cast(&p->generationDateOfProduct_), 2); // 24 is.read(reinterpret_cast(&p->generationTimeOfProduct_), 4); // 25-26 - is.read(reinterpret_cast(&p->parameters_[0]), 2 * 2); // 27-28 - is.read(reinterpret_cast(&p->elevationNumber_), 2); // 29 - is.read(reinterpret_cast(&p->parameters_[2]), 2); // 30 - is.read(reinterpret_cast(&p->halfwords_[0]), 16 * 2); // 31-46 - is.read(reinterpret_cast(&p->parameters_[3]), 7 * 2); // 47-53 - is.read(reinterpret_cast(&p->version_), 1); // 54 - is.read(reinterpret_cast(&p->spotBlank_), 1); // 54 - is.read(reinterpret_cast(&p->offsetToSymbology_), 4); // 55-56 - is.read(reinterpret_cast(&p->offsetToGraphic_), 4); // 57-58 - is.read(reinterpret_cast(&p->offsetToTabular_), 4); // 59-60 + is.read(reinterpret_cast(&p->parameters_[0]), + static_cast(2 * 2)); // 27-28 + is.read(reinterpret_cast(&p->elevationNumber_), 2); // 29 + is.read(reinterpret_cast(&p->parameters_[2]), 2); // 30 + is.read(reinterpret_cast(&p->halfwords_[0]), + static_cast(16 * 2)); // 31-46 + is.read(reinterpret_cast(&p->parameters_[3]), + static_cast(7 * 2)); // 47-53 + is.read(reinterpret_cast(&p->version_), 1); // 54 + is.read(reinterpret_cast(&p->spotBlank_), 1); // 54 + is.read(reinterpret_cast(&p->offsetToSymbology_), 4); // 55-56 + is.read(reinterpret_cast(&p->offsetToGraphic_), 4); // 57-58 + is.read(reinterpret_cast(&p->offsetToTabular_), 4); // 59-60 - p->blockDivider_ = ntohs(p->blockDivider_); - p->latitudeOfRadar_ = ntohl(p->latitudeOfRadar_); - p->longitudeOfRadar_ = ntohl(p->longitudeOfRadar_); - p->heightOfRadar_ = ntohs(p->heightOfRadar_); - p->productCode_ = ntohs(p->productCode_); - p->operationalMode_ = ntohs(p->operationalMode_); - p->volumeCoveragePattern_ = ntohs(p->volumeCoveragePattern_); - p->sequenceNumber_ = ntohs(p->sequenceNumber_); - p->volumeScanNumber_ = ntohs(p->volumeScanNumber_); - p->volumeScanDate_ = ntohs(p->volumeScanDate_); + p->blockDivider_ = static_cast(ntohs(p->blockDivider_)); + p->latitudeOfRadar_ = static_cast(ntohl(p->latitudeOfRadar_)); + p->longitudeOfRadar_ = + static_cast(ntohl(p->longitudeOfRadar_)); + p->heightOfRadar_ = static_cast(ntohs(p->heightOfRadar_)); + p->productCode_ = static_cast(ntohs(p->productCode_)); + p->operationalMode_ = ntohs(p->operationalMode_); + p->volumeCoveragePattern_ = ntohs(p->volumeCoveragePattern_); + p->sequenceNumber_ = static_cast(ntohs(p->sequenceNumber_)); + p->volumeScanNumber_ = ntohs(p->volumeScanNumber_); + p->volumeScanDate_ = ntohs(p->volumeScanDate_); p->volumeScanStartTime_ = ntohl(p->volumeScanStartTime_); p->generationDateOfProduct_ = ntohs(p->generationDateOfProduct_); p->generationTimeOfProduct_ = ntohl(p->generationTimeOfProduct_); @@ -827,6 +863,8 @@ bool ProductDescriptionBlock::Parse(std::istream& is) } } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + if (blockValid) { logger_->trace("Product code: {}", p->productCode_); @@ -844,6 +882,8 @@ bool ProductDescriptionBlock::Parse(std::istream& is) std::optional ProductDescriptionBlock::data_level_code(std::uint8_t level) const { + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + switch (p->productCode_) { case 32: @@ -1033,11 +1073,11 @@ ProductDescriptionBlock::data_level_code(std::uint8_t level) const if (number_of_levels() <= 16 && level < 16 && !uncodedDataLevelProducts_.contains(p->productCode_)) { - uint16_t th = data_level_threshold(level); + std::uint16_t th = data_level_threshold(level); if ((th & 0x8000u)) { // If bit 0 is one, then the LSB is coded - uint16_t lsb = th & 0x00ffu; + std::uint16_t lsb = th & 0x00ffu; switch (lsb) { @@ -1081,6 +1121,8 @@ ProductDescriptionBlock::data_level_code(std::uint8_t level) const } } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + return std::nullopt; } @@ -1099,6 +1141,8 @@ ProductDescriptionBlock::data_value(std::uint8_t level) const std::optional f = std::nullopt; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // Different products use different scale/offset formulas if (numberOfLevels > 16 || uncodedDataLevelProducts_.contains(p->productCode_)) @@ -1116,17 +1160,17 @@ ProductDescriptionBlock::data_value(std::uint8_t level) const case 174: case 175: case 176: - f = (level - dataOffset) / dataScale; + f = (static_cast(level) - dataOffset) / dataScale; break; case 134: if (level < log_start()) { - f = (level - dataOffset) / dataScale; + f = (static_cast(level) - dataOffset) / dataScale; } else { - f = expf((level - log_offset()) / log_scale()); + f = expf((static_cast(level) - log_offset()) / log_scale()); } break; @@ -1135,7 +1179,7 @@ ProductDescriptionBlock::data_value(std::uint8_t level) const [[fallthrough]]; default: - f = level * dataScale + dataOffset; + f = static_cast(level) * dataScale + dataOffset; break; } } @@ -1175,6 +1219,8 @@ ProductDescriptionBlock::data_value(std::uint8_t level) const } } + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // Scale for GR compatibility if (f.has_value()) { From c0280fcfab95e1ecd29c704e21a83267b37cf31d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 00:25:07 -0500 Subject: [PATCH 602/762] Add missing data_size() function to DigitalRasterDataArrayPacket --- .../scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp index ece33807..c03c9244 100644 --- a/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp @@ -101,6 +101,11 @@ DigitalRasterDataArrayPacket::level(std::uint16_t r) const return p->row_[r].level_; } +size_t DigitalRasterDataArrayPacket::data_size() const +{ + return p->dataSize_; +} + bool DigitalRasterDataArrayPacket::Parse(std::istream& is) { bool blockValid = true; From b0e7f24be26b9d19f1d89909de7ac07ea6b29cc0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 00:45:36 -0500 Subject: [PATCH 603/762] Fix RdaAdaptationData seek offset --- wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp index 05887f2a..7ccd5863 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_adaptation_data.cpp @@ -1339,7 +1339,7 @@ bool RdaAdaptationData::Parse(std::istream& is) ReadChar(is, p->slatdir_); // 1316-1319 ReadChar(is, p->slondir_); // 1320-1323 - is.seekg(3824, std::ios_base::cur); // 1324-2499 + is.seekg(1176, std::ios_base::cur); // 1324-2499 is.read(reinterpret_cast(&p->digRcvrClockFreq_), 8); // 2500-2507 is.read(reinterpret_cast(&p->cohoFreq_), 8); // 2508-2515 From 2a5068c4bb066331da043da9545b2b6eb2c538dd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 01:21:32 -0500 Subject: [PATCH 604/762] Fixing formatting --- .../wsr88d/rda/digital_radar_data_generic.hpp | 22 ++++++++++--------- .../scwx/wsr88d/rda/rda_status_data.cpp | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp b/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp index 07f3c111..0441f17a 100644 --- a/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp +++ b/wxdata/include/scwx/wsr88d/rda/digital_radar_data_generic.hpp @@ -118,17 +118,19 @@ public: MomentDataBlock(MomentDataBlock&&) noexcept; MomentDataBlock& operator=(MomentDataBlock&&) noexcept; - [[nodiscard]] std::uint16_t number_of_data_moment_gates() const override; + [[nodiscard]] std::uint16_t number_of_data_moment_gates() const override; [[nodiscard]] units::kilometers data_moment_range() const override; - [[nodiscard]] std::int16_t data_moment_range_raw() const override; - [[nodiscard]] units::kilometers data_moment_range_sample_interval() const override; - [[nodiscard]] std::uint16_t data_moment_range_sample_interval_raw() const override; - [[nodiscard]] float snr_threshold() const; - [[nodiscard]] std::int16_t snr_threshold_raw() const override; - [[nodiscard]] std::uint8_t data_word_size() const override; - [[nodiscard]] float scale() const override; - [[nodiscard]] float offset() const override; - [[nodiscard]] const void* data_moments() const override; + [[nodiscard]] std::int16_t data_moment_range_raw() const override; + [[nodiscard]] units::kilometers + data_moment_range_sample_interval() const override; + [[nodiscard]] std::uint16_t + data_moment_range_sample_interval_raw() const override; + [[nodiscard]] float snr_threshold() const; + [[nodiscard]] std::int16_t snr_threshold_raw() const override; + [[nodiscard]] std::uint8_t data_word_size() const override; + [[nodiscard]] float scale() const override; + [[nodiscard]] float offset() const override; + [[nodiscard]] const void* data_moments() const override; static std::shared_ptr Create(const std::string& dataBlockType, diff --git a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp index 375d46b8..81ff4463 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_status_data.cpp @@ -304,7 +304,7 @@ bool RdaStatusData::Parse(std::istream& is) p->signalProcessingOptions_ = ntohs(p->signalProcessingOptions_); p->statusVersion_ = ntohs(p->statusVersion_); } - + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) if (!ValidateMessage(is, bytesRead)) From ade40806b660cebdf9eae2bceeea4414528b31aa Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 15:38:26 -0500 Subject: [PATCH 605/762] AWIPS message byte swap cleanup --- wxdata/include/scwx/awips/message.hpp | 205 ++++++++++++++++---------- 1 file changed, 126 insertions(+), 79 deletions(-) diff --git a/wxdata/include/scwx/awips/message.hpp b/wxdata/include/scwx/awips/message.hpp index 486e7f06..12ec226c 100644 --- a/wxdata/include/scwx/awips/message.hpp +++ b/wxdata/include/scwx/awips/message.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -52,113 +53,159 @@ public: static float SwapFloat(float f) { - std::uint32_t temp; - std::memcpy(&temp, &f, sizeof(std::uint32_t)); - temp = ntohl(temp); - std::memcpy(&f, &temp, sizeof(float)); + if constexpr (std::endian::native == std::endian::little) + { + // Variable is initialized by memcpy + // NOLINTNEXTLINE(cppcoreguidelines-init-variables) + std::uint32_t temp; + std::memcpy(&temp, &f, sizeof(std::uint32_t)); + temp = ntohl(temp); + std::memcpy(&f, &temp, sizeof(float)); + } return f; } static double SwapDouble(double d) { - std::uint64_t temp; - std::memcpy(&temp, &d, sizeof(std::uint64_t)); - temp = ntohll(temp); - std::memcpy(&d, &temp, sizeof(float)); + if constexpr (std::endian::native == std::endian::little) + { + // Variable is initialized by memcpy + // NOLINTNEXTLINE(cppcoreguidelines-init-variables) + std::uint64_t temp; + std::memcpy(&temp, &d, sizeof(std::uint64_t)); + temp = Swap64(temp); + std::memcpy(&d, &temp, sizeof(float)); + } return d; } - template - static void SwapArray(std::array& arr, - std::size_t size = _Size) + static std::uint64_t Swap64(std::uint64_t value) { - std::transform(std::execution::par_unseq, - arr.begin(), - arr.begin() + size, - arr.begin(), - [](float f) { return SwapFloat(f); }); + if constexpr (std::endian::native == std::endian::little) + { + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + std::uint32_t high = ntohl(static_cast(value >> 32)); + std::uint32_t low = + ntohl(static_cast(value & 0xFFFFFFFFULL)); + return (static_cast(low) << 32) | high; + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + } + else + { + return value; + } } - template - static void SwapArray(std::array& arr, - std::size_t size = _Size) + template + static void SwapArray(std::array& arr, + std::size_t size = kSize) { - std::transform(std::execution::par_unseq, - arr.begin(), - arr.begin() + size, - arr.begin(), - [](std::int16_t u) { return ntohs(u); }); + if constexpr (std::endian::native == std::endian::little) + { + std::transform(std::execution::par_unseq, + arr.begin(), + arr.begin() + size, + arr.begin(), + [](float f) { return SwapFloat(f); }); + } } - template - static void SwapArray(std::array& arr, - std::size_t size = _Size) + template + static void SwapArray(std::array& arr, + std::size_t size = kSize) { - std::transform(std::execution::par_unseq, - arr.begin(), - arr.begin() + size, - arr.begin(), - [](std::uint16_t u) { return ntohs(u); }); + if constexpr (std::endian::native == std::endian::little) + { + std::transform(std::execution::par_unseq, + arr.begin(), + arr.begin() + size, + arr.begin(), + [](std::int16_t u) { return ntohs(u); }); + } } - template - static void SwapArray(std::array& arr, - std::size_t size = _Size) + template + static void SwapArray(std::array& arr, + std::size_t size = kSize) { - std::transform(std::execution::par_unseq, - arr.begin(), - arr.begin() + size, - arr.begin(), - [](std::uint32_t u) { return ntohl(u); }); + if constexpr (std::endian::native == std::endian::little) + { + std::transform(std::execution::par_unseq, + arr.begin(), + arr.begin() + size, + arr.begin(), + [](std::uint16_t u) { return ntohs(u); }); + } + } + + template + static void SwapArray(std::array& arr, + std::size_t size = kSize) + { + if constexpr (std::endian::native == std::endian::little) + { + std::transform(std::execution::par_unseq, + arr.begin(), + arr.begin() + size, + arr.begin(), + [](std::uint32_t u) { return ntohl(u); }); + } } template static void SwapMap(std::map& m) { - std::for_each(std::execution::par_unseq, - m.begin(), - m.end(), - [](auto& p) { p.second = SwapFloat(p.second); }); + if constexpr (std::endian::native == std::endian::little) + { + std::for_each(std::execution::par_unseq, + m.begin(), + m.end(), + [](auto& p) { p.second = SwapFloat(p.second); }); + } } template static void SwapVector(std::vector& v) { - std::transform(std::execution::par_unseq, - v.begin(), - v.end(), - v.begin(), - [](T u) - { - if constexpr (std::is_same_v || - std::is_same_v) - { - return static_cast(ntohs(u)); - } - else if constexpr (std::is_same_v || - std::is_same_v) - { - return static_cast(ntohl(u)); - } - else if constexpr (std::is_same_v || - std::is_same_v) - { - return static_cast(ntohll(u)); - } - else if constexpr (std::is_same_v) - { - return SwapFloat(u); - } - else if constexpr (std::is_same_v) - { - return SwapDouble(u); - } - else - { - static_assert(std::is_same_v, - "Unsupported type for SwapVector"); - } - }); + if constexpr (std::endian::native == std::endian::little) + { + std::transform( + std::execution::par_unseq, + v.begin(), + v.end(), + v.begin(), + [](T u) + { + if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(ntohs(u)); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(ntohl(u)); + } + else if constexpr (std::is_same_v || + std::is_same_v) + { + return static_cast(Swap64(u)); + } + else if constexpr (std::is_same_v) + { + return SwapFloat(u); + } + else if constexpr (std::is_same_v) + { + return SwapDouble(u); + } + else + { + static_assert(std::is_same_v, + "Unsupported type for SwapVector"); + } + }); + } } private: From 018052b78db1fdf2f3ed7af56fc6f944d5e14b4e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 15:44:41 -0500 Subject: [PATCH 606/762] Address RDA/RPG Build 23.0 clang-tidy comments --- wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp | 2 +- .../scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp | 2 +- wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp | 4 ++-- wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp | 2 +- .../scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp | 2 +- wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp b/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp index 1a04aacb..eb42268b 100644 --- a/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp +++ b/wxdata/include/scwx/wsr88d/rda/rda_prf_data.hpp @@ -9,7 +9,7 @@ class RdaPrfData : public Level2Message { public: explicit RdaPrfData(); - ~RdaPrfData(); + ~RdaPrfData() override; RdaPrfData(const RdaPrfData&) = delete; RdaPrfData& operator=(const RdaPrfData&) = delete; diff --git a/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp index 76b0f2c3..c0309568 100644 --- a/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/digital_raster_data_array_packet.hpp @@ -12,7 +12,7 @@ class DigitalRasterDataArrayPacket : public Packet { public: explicit DigitalRasterDataArrayPacket(); - ~DigitalRasterDataArrayPacket(); + ~DigitalRasterDataArrayPacket() override; DigitalRasterDataArrayPacket(const DigitalRasterDataArrayPacket&) = delete; DigitalRasterDataArrayPacket& diff --git a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp index b253cdf8..7fe5b77e 100644 --- a/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rda/level2_message_factory.cpp @@ -142,9 +142,9 @@ Level2MessageInfo Level2MessageFactory::Create(std::istream& is, // Estimate remaining size static const std::uint16_t kMinRemainingSegments_ = 100u; - std::uint16_t remainingSegments = std::max( + const std::uint16_t remainingSegments = std::max( totalSegments - segment + 1, kMinRemainingSegments_); - std::size_t remainingSize = remainingSegments * dataSize; + const std::size_t remainingSize = remainingSegments * dataSize; ctx->messageData_.resize(ctx->bufferedSize_ + remainingSize); } diff --git a/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp b/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp index d516309b..147714c7 100644 --- a/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp +++ b/wxdata/source/scwx/wsr88d/rda/rda_prf_data.cpp @@ -42,7 +42,7 @@ bool RdaPrfData::Parse(std::istream& is) bool messageValid = true; std::size_t bytesRead = 0; - std::streampos isBegin = is.tellg(); + const std::streampos isBegin = is.tellg(); is.read(reinterpret_cast(&p->numberOfWaveforms_), 2); // 1 is.seekg(2, std::ios_base::cur); // 2 diff --git a/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp index c03c9244..2e0ef662 100644 --- a/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/digital_raster_data_array_packet.cpp @@ -185,7 +185,7 @@ bool DigitalRasterDataArrayPacket::Parse(std::istream& is) } // Read raster bins - std::size_t dataSize = p->numberOfCells_; + const std::size_t dataSize = p->numberOfCells_; row.level_.resize(dataSize); is.read(reinterpret_cast(row.level_.data()), static_cast(dataSize)); diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 85550966..2d70ad7b 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -1073,11 +1073,11 @@ ProductDescriptionBlock::data_level_code(std::uint8_t level) const if (number_of_levels() <= 16 && level < 16 && !uncodedDataLevelProducts_.contains(p->productCode_)) { - std::uint16_t th = data_level_threshold(level); + const std::uint16_t th = data_level_threshold(level); if ((th & 0x8000u)) { // If bit 0 is one, then the LSB is coded - std::uint16_t lsb = th & 0x00ffu; + const std::uint16_t lsb = th & 0x00ffu; switch (lsb) { From f3debc08de1befa8be3f60f187925771c6f62438 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 17:12:23 -0500 Subject: [PATCH 607/762] clang-tidy updates for AWIPS message --- wxdata/include/scwx/awips/message.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wxdata/include/scwx/awips/message.hpp b/wxdata/include/scwx/awips/message.hpp index 12ec226c..5cd81000 100644 --- a/wxdata/include/scwx/awips/message.hpp +++ b/wxdata/include/scwx/awips/message.hpp @@ -84,8 +84,9 @@ public: if constexpr (std::endian::native == std::endian::little) { // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) - std::uint32_t high = ntohl(static_cast(value >> 32)); - std::uint32_t low = + const std::uint32_t high = + ntohl(static_cast(value >> 32)); + const std::uint32_t low = ntohl(static_cast(value & 0xFFFFFFFFULL)); return (static_cast(low) << 32) | high; // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) From 68fcecdc1578b772413ff31bc4da709bff08c7db Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 18:06:38 -0500 Subject: [PATCH 608/762] Make sure shared uniforms are updated correctly per-map pane - Broken by shaders being shared between map panes --- scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index c1fbd6c4..2ad6ae65 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -58,6 +58,9 @@ public: bool cfpEnabled_ {false}; + std::uint16_t rangeMin_ {0}; + float scale_ {1.0f}; + bool colorTableNeedsUpdate_ {false}; bool sweepNeedsUpdate_ {false}; }; @@ -298,6 +301,9 @@ void RadarProductLayer::Render( gl.glUniform1i(p->uCFPEnabledLocation_, p->cfpEnabled_ ? 1 : 0); + gl.glUniform1ui(p->uDataMomentOffsetLocation_, p->rangeMin_); + gl.glUniform1f(p->uDataMomentScaleLocation_, p->scale_); + gl.glActiveTexture(GL_TEXTURE0); gl.glBindTexture(GL_TEXTURE_1D, p->texture_); gl.glBindVertexArray(p->vao_); @@ -553,8 +559,8 @@ void RadarProductLayer::UpdateColorTable( colorTable.data()); gl.glGenerateMipmap(GL_TEXTURE_1D); - gl.glUniform1ui(p->uDataMomentOffsetLocation_, rangeMin); - gl.glUniform1f(p->uDataMomentScaleLocation_, scale); + p->rangeMin_ = rangeMin; + p->scale_ = scale; } } // namespace scwx::qt::map From 4ed1fda1aece4c5076337e769ec9a2fd1ffaaa91 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 19:08:07 -0500 Subject: [PATCH 609/762] Move media objects to dedicated thread to avoid main thread delays --- .../source/scwx/qt/manager/media_manager.cpp | 105 +++++++++++++----- .../source/scwx/qt/manager/media_manager.hpp | 23 ++-- 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp index 349e73b9..014b9a37 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -5,13 +5,10 @@ #include #include #include +#include #include -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { static const std::string logPrefix_ = "scwx::qt::manager::media_manager"; @@ -20,46 +17,80 @@ 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)} + explicit Impl() { - logger_->debug("Audio device: {}", - audioOutput_->device().description().toStdString()); + mediaParent_ = std::make_unique(); + mediaParent_->moveToThread(&thread_); - mediaPlayer_->setAudioOutput(audioOutput_); + thread_.start(); - ConnectSignals(); + QMetaObject::invokeMethod( + mediaParent_.get(), + [this]() + { + // QObjects are managed by the parent + // NOLINTBEGIN(cppcoreguidelines-owning-memory) + + logger_->debug("Creating QMediaDevices"); + mediaDevices_ = new QMediaDevices(mediaParent_.get()); + logger_->debug("Creating QMediaPlayer"); + mediaPlayer_ = new QMediaPlayer(mediaParent_.get()); + logger_->debug("Creating QAudioOutput"); + audioOutput_ = new QAudioOutput(mediaParent_.get()); + + // NOLINTEND(cppcoreguidelines-owning-memory) + + logger_->debug("Audio device: {}", + audioOutput_->device().description().toStdString()); + + mediaPlayer_->setAudioOutput(audioOutput_); + + ConnectSignals(); + }); } - ~Impl() {} + ~Impl() + { + // Delete the media parent + mediaParent_.reset(); + + thread_.quit(); + thread_.wait(); + } + + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void ConnectSignals(); - MediaManager* self_; + QThread thread_ {}; - QMediaDevices* mediaDevices_; - QMediaPlayer* mediaPlayer_; - QAudioOutput* audioOutput_; + std::unique_ptr mediaParent_ {nullptr}; + QMediaDevices* mediaDevices_ {nullptr}; + QMediaPlayer* mediaPlayer_ {nullptr}; + QAudioOutput* audioOutput_ {nullptr}; }; -MediaManager::MediaManager() : p(std::make_unique(this)) {} +MediaManager::MediaManager() : p(std::make_unique()) {} MediaManager::~MediaManager() = default; +MediaManager::MediaManager(MediaManager&&) noexcept = default; +MediaManager& MediaManager::operator=(MediaManager&&) noexcept = default; + void MediaManager::Impl::ConnectSignals() { QObject::connect( mediaDevices_, &QMediaDevices::audioOutputsChanged, - self_, + mediaParent_.get(), [this]() { audioOutput_->setDevice(QMediaDevices::defaultAudioOutput()); }); QObject::connect(audioOutput_, &QAudioOutput::deviceChanged, - self_, + mediaParent_.get(), [this]() { logger_->debug( @@ -69,7 +100,7 @@ void MediaManager::Impl::ConnectSignals() QObject::connect(mediaPlayer_, &QMediaPlayer::errorOccurred, - self_, + mediaParent_.get(), [](QMediaPlayer::Error error, const QString& errorString) { logger_->error("Error {}: {}", @@ -81,30 +112,46 @@ void MediaManager::Impl::ConnectSignals() void MediaManager::Play(types::AudioFile media) { const std::string path = types::GetMediaPath(media); + Play(path); } void MediaManager::Play(const std::string& mediaPath) { logger_->debug("Playing audio: {}", mediaPath); + if (p->mediaPlayer_ == nullptr) + { + logger_->warn("Media player is not yet initialized"); + return; + } + if (mediaPath.starts_with(':')) { - p->mediaPlayer_->setSource( + QMetaObject::invokeMethod( + p->mediaPlayer_, + &QMediaPlayer::setSource, QUrl(QString("qrc%1").arg(QString::fromStdString(mediaPath)))); } else { - p->mediaPlayer_->setSource( + QMetaObject::invokeMethod( + p->mediaPlayer_, + &QMediaPlayer::setSource, QUrl::fromLocalFile(QString::fromStdString(mediaPath))); } - p->mediaPlayer_->setPosition(0); - + QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::setPosition, 0); QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::play); } void MediaManager::Stop() { + if (p->mediaPlayer_ == nullptr) + { + logger_->warn("Media player is not yet initialized"); + return; + } + QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::stop); } @@ -126,6 +173,4 @@ std::shared_ptr MediaManager::Instance() return mediaManager; } -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.hpp b/scwx-qt/source/scwx/qt/manager/media_manager.hpp index f1d73656..c10dd041 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.hpp @@ -4,24 +4,21 @@ #include -#include - -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { -class MediaManager : public QObject +class MediaManager { - Q_OBJECT - Q_DISABLE_COPY_MOVE(MediaManager) - public: explicit MediaManager(); ~MediaManager(); + MediaManager(const MediaManager&) = delete; + MediaManager& operator=(const MediaManager&) = delete; + + MediaManager(MediaManager&&) noexcept; + MediaManager& operator=(MediaManager&&) noexcept; + void Play(types::AudioFile media); void Play(const std::string& mediaPath); void Stop(); @@ -33,6 +30,4 @@ private: std::unique_ptr p; }; -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager From 04e45978adaf0f583f3452bd96a1a93b53b0c61f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 May 2025 22:47:01 -0500 Subject: [PATCH 610/762] EET does not use default scale and offset --- wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 2d70ad7b..016128f3 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -1176,7 +1176,8 @@ ProductDescriptionBlock::data_value(std::uint8_t level) const case 135: level = level & data_mask(); - [[fallthrough]]; + f = static_cast(level) / dataScale - dataOffset; + break; default: f = static_cast(level) * dataScale + dataOffset; From 699f60d399d9c0af170d7d90bd3c6ab8056ff7f0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 May 2025 00:13:46 -0500 Subject: [PATCH 611/762] Add build number to log and about dialog --- scwx-qt/source/scwx/qt/main/main.cpp | 3 ++- scwx-qt/source/scwx/qt/main/versions.hpp.in | 11 +++-------- scwx-qt/source/scwx/qt/ui/about_dialog.cpp | 4 +++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 65d7e998..904ff3b3 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -55,8 +55,9 @@ int main(int argc, char* argv[]) auto& logManager = scwx::qt::manager::LogManager::Instance(); logManager.Initialize(); - logger_->info("Supercell Wx v{} ({})", + logger_->info("Supercell Wx v{}.{} ({})", scwx::qt::main::kVersionString_, + scwx::qt::main::kBuildNumber_, scwx::qt::main::kCommitString_); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); diff --git a/scwx-qt/source/scwx/qt/main/versions.hpp.in b/scwx-qt/source/scwx/qt/main/versions.hpp.in index 32a69c59..bdf9cd62 100644 --- a/scwx-qt/source/scwx/qt/main/versions.hpp.in +++ b/scwx-qt/source/scwx/qt/main/versions.hpp.in @@ -3,17 +3,12 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace main +namespace scwx::qt::main { +const std::uint32_t kBuildNumber_ {${build_number}u}; const std::string kCommitString_ {"${commit_string}"}; const std::uint16_t kCopyrightYear_ {${copyright_year}u}; const std::string kVersionString_ {"${version_string}"}; -} // namespace main -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::main diff --git a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp index 42ec4e32..bc24d056 100644 --- a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp @@ -45,7 +45,9 @@ AboutDialog::AboutDialog(QWidget* parent) : } ui->versionLabel->setText( - tr("Version %1").arg(QString::fromStdString(main::kVersionString_))); + tr("Version %1 (Build %2)") + .arg(QString::fromStdString(main::kVersionString_)) + .arg(main::kBuildNumber_)); ui->revisionLabel->setText( tr("Git Revision %2") .arg(repositoryUrl) From 9d6a0358d08171a9ec03baa907ad71fc288ab249 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 May 2025 00:25:41 -0500 Subject: [PATCH 612/762] Flush logs every 3 seconds or on logging info or higher --- wxdata/source/scwx/util/logger.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/wxdata/source/scwx/util/logger.cpp b/wxdata/source/scwx/util/logger.cpp index 8a97d80e..407ce354 100644 --- a/wxdata/source/scwx/util/logger.cpp +++ b/wxdata/source/scwx/util/logger.cpp @@ -20,6 +20,12 @@ static std::vector> extraSinks_ {}; void Initialize() { spdlog::set_pattern(logPattern_); + + // Periodically flush every 3 seconds + spdlog::flush_every(std::chrono::seconds(3)); + + // Flush whenever logging info or higher + spdlog::flush_on(spdlog::level::level_enum::info); } void AddFileSink(const std::string& baseFilename) From e78aca737736dc68368430741d71aaa3bb98d9f8 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 18 May 2025 15:45:50 -0400 Subject: [PATCH 613/762] Disable modifying settings using the scroll wheel unless they are focused --- scwx-qt/scwx-qt.cmake | 6 + .../ui/modified_widgets/focused_combo_box.hpp | 23 ++++ .../focused_double_spin_box.hpp | 23 ++++ .../ui/modified_widgets/focused_spin_box.hpp | 23 ++++ .../qt/ui/settings/unit_settings_widget.cpp | 42 +++--- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 127 +++++++++++++++--- 6 files changed, 205 insertions(+), 39 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 0e8b2ef1..cd9abded 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -342,6 +342,10 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/serial_port_dialog.ui source/scwx/qt/ui/update_dialog.ui source/scwx/qt/ui/wfo_dialog.ui) +set(HDR_UI_MODIFIED_WIDGETS + source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp + source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp + source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp) set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp source/scwx/qt/ui/settings/hotkey_settings_widget.hpp source/scwx/qt/ui/settings/settings_page_widget.hpp @@ -469,6 +473,7 @@ set(PROJECT_SOURCES ${HDR_MAIN} ${HDR_UI} ${SRC_UI} ${UI_UI} + ${HDR_UI_MODIFIED_WIDGETS} ${HDR_UI_SETTINGS} ${SRC_UI_SETTINGS} ${HDR_UI_SETUP} @@ -508,6 +513,7 @@ source_group("Header Files\\types" FILES ${HDR_TYPES}) source_group("Source Files\\types" FILES ${SRC_TYPES}) source_group("Header Files\\ui" FILES ${HDR_UI}) source_group("Source Files\\ui" FILES ${SRC_UI}) +source_group("Header Files\\ui\\modified_widgets" FILES ${HDR_UI_MODIFIED_WIDGETS}) source_group("Header Files\\ui\\settings" FILES ${HDR_UI_SETTINGS}) source_group("Source Files\\ui\\settings" FILES ${SRC_UI_SETTINGS}) source_group("Header Files\\ui\\setup" FILES ${HDR_UI_SETUP}) diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp new file mode 100644 index 00000000..5c7d8f91 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp @@ -0,0 +1,23 @@ +#include +#include + +class QFocusedComboBox : public QComboBox +{ + Q_OBJECT + +public: + using QComboBox::QComboBox; + +protected: + void wheelEvent(QWheelEvent* event) override + { + if (hasFocus()) + { + QComboBox::wheelEvent(event); + } + else + { + event->ignore(); + } + } +}; diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp new file mode 100644 index 00000000..b18b3e89 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp @@ -0,0 +1,23 @@ +#include +#include + +class QFocusedDoubleSpinBox : public QDoubleSpinBox +{ + Q_OBJECT + +public: + using QDoubleSpinBox::QDoubleSpinBox; + +protected: + void wheelEvent(QWheelEvent* event) override + { + if (hasFocus()) + { + QDoubleSpinBox::wheelEvent(event); + } + else + { + event->ignore(); + } + } +}; diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp new file mode 100644 index 00000000..b83036b7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp @@ -0,0 +1,23 @@ +#include +#include + +class QFocusedSpinBox : public QSpinBox +{ + Q_OBJECT + +public: + using QSpinBox::QSpinBox; + +protected: + void wheelEvent(QWheelEvent* event) override + { + if (hasFocus()) + { + QSpinBox::wheelEvent(event); + } + else + { + event->ignore(); + } + } +}; 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 aff11583..83918696 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 @@ -2,21 +2,17 @@ #include #include #include +#include #include #include #include #include -#include #include #include #include -namespace scwx -{ -namespace qt -{ -namespace ui +namespace scwx::qt::ui { static const std::string logPrefix_ = @@ -51,7 +47,7 @@ public: [&row, &self, this]( settings::SettingsInterface& settingsInterface, const std::string& labelName, - QComboBox* comboBox) + QFocusedComboBox* comboBox) { QLabel* label = new QLabel(QObject::tr(labelName.c_str()), self); QToolButton* resetButton = new QToolButton(self); @@ -72,9 +68,12 @@ public: ++row; }; - QComboBox* accumulationComboBox = new QComboBox(self); + // Qt manages the memory for these widgets + // NOLINTBEGIN(cppcoreguidelines-owning-memory) + auto* accumulationComboBox = new QFocusedComboBox(self); accumulationComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + accumulationComboBox->setFocusPolicy(Qt::StrongFocus); accumulationUnits_.SetSettingsVariable(unitSettings.accumulation_units()); SCWX_SETTINGS_COMBO_BOX(accumulationUnits_, accumulationComboBox, @@ -82,9 +81,10 @@ public: types::GetAccumulationUnitsName); AddRow(accumulationUnits_, "Accumulation", accumulationComboBox); - QComboBox* echoTopsComboBox = new QComboBox(self); + auto* echoTopsComboBox = new QFocusedComboBox(self); echoTopsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + echoTopsComboBox->setFocusPolicy(Qt::StrongFocus); echoTopsUnits_.SetSettingsVariable(unitSettings.echo_tops_units()); SCWX_SETTINGS_COMBO_BOX(echoTopsUnits_, echoTopsComboBox, @@ -92,9 +92,10 @@ public: types::GetEchoTopsUnitsName); AddRow(echoTopsUnits_, "Echo Tops", echoTopsComboBox); - QComboBox* speedComboBox = new QComboBox(self); + auto* speedComboBox = new QFocusedComboBox(self); speedComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + speedComboBox->setFocusPolicy(Qt::StrongFocus); speedUnits_.SetSettingsVariable(unitSettings.speed_units()); SCWX_SETTINGS_COMBO_BOX(speedUnits_, speedComboBox, @@ -102,9 +103,10 @@ public: types::GetSpeedUnitsName); AddRow(speedUnits_, "Speed", speedComboBox); - QComboBox* distanceComboBox = new QComboBox(self); + auto* distanceComboBox = new QFocusedComboBox(self); distanceComboBox->setSizePolicy(QSizePolicy::Expanding, - QSizePolicy::Preferred); + QSizePolicy::Preferred); + distanceComboBox->setFocusPolicy(Qt::StrongFocus); distanceUnits_.SetSettingsVariable(unitSettings.distance_units()); SCWX_SETTINGS_COMBO_BOX(distanceUnits_, distanceComboBox, @@ -112,9 +114,10 @@ public: types::GetDistanceUnitsName); AddRow(distanceUnits_, "Distance", distanceComboBox); - QComboBox* otherComboBox = new QComboBox(self); + auto* otherComboBox = new QFocusedComboBox(self); otherComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + otherComboBox->setFocusPolicy(Qt::StrongFocus); otherUnits_.SetSettingsVariable(unitSettings.other_units()); SCWX_SETTINGS_COMBO_BOX(otherUnits_, otherComboBox, @@ -122,12 +125,19 @@ public: types::GetOtherUnitsName); AddRow(otherUnits_, "Other", otherComboBox); - QSpacerItem* spacer = + auto* spacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding); gridLayout_->addItem(spacer, row, 0); + + // NOLINTEND(cppcoreguidelines-owning-memory) } ~Impl() = default; + Impl(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(const Impl&) = delete; + Impl& operator=(Impl&&) = delete; + QWidget* contents_; QLayout* layout_; QScrollArea* scrollArea_ {}; @@ -147,6 +157,4 @@ UnitSettingsWidget::UnitSettingsWidget(QWidget* parent) : UnitSettingsWidget::~UnitSettingsWidget() = default; -} // namespace ui -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 5b9b37fd..ca99c620 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,7 +135,7 @@ 0 - -412 + 0 511 873 @@ -160,13 +160,25 @@ 0 - + + + Qt::FocusPolicy::StrongFocus + + - + + + Qt::FocusPolicy::StrongFocus + + - + + + Qt::FocusPolicy::StrongFocus + + @@ -265,7 +277,11 @@ - + + + Qt::FocusPolicy::StrongFocus + + @@ -293,10 +309,18 @@ - + + + Qt::FocusPolicy::StrongFocus + + - + + + Qt::FocusPolicy::StrongFocus + + @@ -321,7 +345,10 @@ - + + + Qt::FocusPolicy::StrongFocus + 1 @@ -413,13 +440,21 @@ - + + + Qt::FocusPolicy::StrongFocus + + - + + + Qt::FocusPolicy::StrongFocus + + @@ -443,7 +478,11 @@ - + + + Qt::FocusPolicy::StrongFocus + + @@ -466,13 +505,16 @@ - + 0 0 + + Qt::FocusPolicy::StrongFocus + Set to 0 to disable @@ -623,7 +665,10 @@ - + + + Qt::FocusPolicy::StrongFocus + 1 @@ -765,8 +810,8 @@ 0 0 - 80 - 18 + 503 + 380 @@ -875,7 +920,10 @@ - + + + Qt::FocusPolicy::StrongFocus + 4 @@ -891,7 +939,10 @@ - + + + Qt::FocusPolicy::StrongFocus + Set to 0 to disable @@ -943,7 +994,10 @@ - + + + Qt::FocusPolicy::StrongFocus + 4 @@ -1040,16 +1094,23 @@ - + + + Qt::FocusPolicy::StrongFocus + + - + 0 0 + + Qt::FocusPolicy::StrongFocus + @@ -1360,7 +1421,10 @@ - + + + Qt::FocusPolicy::StrongFocus + 999 @@ -1385,7 +1449,11 @@ - + + + Qt::FocusPolicy::StrongFocus + + @@ -1446,6 +1514,21 @@ QLineEdit
scwx/qt/ui/api_key_edit_widget.hpp
+ + QFocusedDoubleSpinBox + QDoubleSpinBox +
scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp
+
+ + QFocusedSpinBox + QSpinBox +
scwx/qt/ui/modified_widgets/focused_spin_box.hpp
+
+ + QFocusedComboBox + QComboBox +
scwx/qt/ui/modified_widgets/focused_combo_box.hpp
+
From 8f84a42e26072a616c5e4688a50d408e674f6fcc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Fri, 23 May 2025 11:53:12 -0400 Subject: [PATCH 614/762] fix level 2 settings big buttons --- scwx-qt/source/scwx/qt/main/main_window.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index dec713ed..bab92b7e 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1549,8 +1549,9 @@ void MainWindowImpl::UpdateRadarProductSettings() { if (activeMap_->GetRadarProductGroup() == common::RadarProductGroup::Level2) { - level2SettingsWidget_->UpdateSettings(activeMap_); level2SettingsGroup_->setVisible(true); + // This should be done after setting visible for correct sizing + level2SettingsWidget_->UpdateSettings(activeMap_); } else { From c4af3724c137e8bcb57d99f67cacd3681139b260 Mon Sep 17 00:00:00 2001 From: aware70 <7832566+aware70@users.noreply.github.com> Date: Fri, 23 May 2025 14:53:05 -0500 Subject: [PATCH 615/762] Add support statement for NixOS 25.05+ to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 321c33f2..01f2ced6 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Supercell Wx supports the following 64-bit operating systems: - Fedora Linux 34+ - openSUSE Tumbleweed - Ubuntu 22.04+ + - NixOS 25.05+ - Most distributions supporting the GCC Standard C++ Library 11+ ## Linux Dependencies From 81b0402e8a867df37b5cd3acce075a61e74371fa Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 24 May 2025 17:52:45 -0400 Subject: [PATCH 616/762] Move modified_widgets to widgets --- scwx-qt/scwx-qt.cmake | 11 +++++------ .../scwx/qt/ui/settings/unit_settings_widget.cpp | 2 +- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 6 +++--- .../focused_combo_box.hpp | 0 .../focused_double_spin_box.hpp | 0 .../focused_spin_box.hpp | 0 6 files changed, 9 insertions(+), 10 deletions(-) rename scwx-qt/source/scwx/qt/ui/{modified_widgets => widgets}/focused_combo_box.hpp (100%) rename scwx-qt/source/scwx/qt/ui/{modified_widgets => widgets}/focused_double_spin_box.hpp (100%) rename scwx-qt/source/scwx/qt/ui/{modified_widgets => widgets}/focused_spin_box.hpp (100%) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index cd9abded..466eddae 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -342,10 +342,6 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/serial_port_dialog.ui source/scwx/qt/ui/update_dialog.ui source/scwx/qt/ui/wfo_dialog.ui) -set(HDR_UI_MODIFIED_WIDGETS - source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp - source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp - source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp) set(HDR_UI_SETTINGS source/scwx/qt/ui/settings/alert_palette_settings_widget.hpp source/scwx/qt/ui/settings/hotkey_settings_widget.hpp source/scwx/qt/ui/settings/settings_page_widget.hpp @@ -366,6 +362,9 @@ set(SRC_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.cpp source/scwx/qt/ui/setup/map_provider_page.cpp source/scwx/qt/ui/setup/setup_wizard.cpp source/scwx/qt/ui/setup/welcome_page.cpp) +set(HDR_UI_WIDGETS source/scwx/qt/ui/widgets/focused_combo_box.hpp + source/scwx/qt/ui/widgets/focused_double_spin_box.hpp + source/scwx/qt/ui/widgets/focused_spin_box.hpp) set(HDR_UTIL source/scwx/qt/util/color.hpp source/scwx/qt/util/file.hpp source/scwx/qt/util/geographic_lib.hpp @@ -473,11 +472,11 @@ set(PROJECT_SOURCES ${HDR_MAIN} ${HDR_UI} ${SRC_UI} ${UI_UI} - ${HDR_UI_MODIFIED_WIDGETS} ${HDR_UI_SETTINGS} ${SRC_UI_SETTINGS} ${HDR_UI_SETUP} ${SRC_UI_SETUP} + ${HDR_UI_WIDGETS} ${HDR_UTIL} ${SRC_UTIL} ${HDR_VIEW} @@ -513,11 +512,11 @@ source_group("Header Files\\types" FILES ${HDR_TYPES}) source_group("Source Files\\types" FILES ${SRC_TYPES}) source_group("Header Files\\ui" FILES ${HDR_UI}) source_group("Source Files\\ui" FILES ${SRC_UI}) -source_group("Header Files\\ui\\modified_widgets" FILES ${HDR_UI_MODIFIED_WIDGETS}) source_group("Header Files\\ui\\settings" FILES ${HDR_UI_SETTINGS}) source_group("Source Files\\ui\\settings" FILES ${SRC_UI_SETTINGS}) source_group("Header Files\\ui\\setup" FILES ${HDR_UI_SETUP}) source_group("Source Files\\ui\\setup" FILES ${SRC_UI_SETUP}) +source_group("Header Files\\ui\\widgets" FILES ${HDR_UI_WIDGETS}) source_group("UI Files\\ui" FILES ${UI_UI}) source_group("Header Files\\util" FILES ${HDR_UTIL}) source_group("Source Files\\util" FILES ${SRC_UTIL}) 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 83918696..ea678e65 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 @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index ca99c620..d67ef84c 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -1517,17 +1517,17 @@ QFocusedDoubleSpinBox QDoubleSpinBox -
scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp
+
scwx/qt/ui/widgets/focused_double_spin_box.hpp
QFocusedSpinBox QSpinBox -
scwx/qt/ui/modified_widgets/focused_spin_box.hpp
+
scwx/qt/ui/widgets/focused_spin_box.hpp
QFocusedComboBox QComboBox -
scwx/qt/ui/modified_widgets/focused_combo_box.hpp
+
scwx/qt/ui/widgets/focused_combo_box.hpp
diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/ui/modified_widgets/focused_combo_box.hpp rename to scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/ui/modified_widgets/focused_double_spin_box.hpp rename to scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp diff --git a/scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp similarity index 100% rename from scwx-qt/source/scwx/qt/ui/modified_widgets/focused_spin_box.hpp rename to scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp From 10021a8ba28999f7d02dfe279407634b179c35cc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 25 May 2025 18:14:33 -0400 Subject: [PATCH 617/762] Add praga once to new widgets hpp files --- scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp | 2 ++ scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp | 2 ++ scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp | 2 ++ 3 files changed, 6 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp index 5c7d8f91..619bcd89 100644 --- a/scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp +++ b/scwx-qt/source/scwx/qt/ui/widgets/focused_combo_box.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp index b18b3e89..f4fe4f9f 100644 --- a/scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp +++ b/scwx-qt/source/scwx/qt/ui/widgets/focused_double_spin_box.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp b/scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp index b83036b7..5d120386 100644 --- a/scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp +++ b/scwx-qt/source/scwx/qt/ui/widgets/focused_spin_box.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include From 6d23b094e6f629dade66d70b3515295536f89b4a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 30 May 2025 02:08:17 +0000 Subject: [PATCH 618/762] Manually specify abseil version --- conanfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conanfile.py b/conanfile.py index b6eb2af5..19ee2f76 100644 --- a/conanfile.py +++ b/conanfile.py @@ -37,6 +37,8 @@ class SupercellWxConan(ConanFile): self.options["libcurl"].ca_path = "none" def requirements(self): + self.requires("abseil/20250127.0", override=True) + if self.settings.os == "Linux": self.requires("onetbb/2022.0.0") From 0da5051408a4afbd76589fc508dc6ce255a6bfe7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 16:25:07 +0000 Subject: [PATCH 619/762] Update dependency boost to v1.88.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 19ee2f76..4307a91a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,7 +5,7 @@ import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") - requires = ("boost/1.87.0", + requires = ("boost/1.88.0", "cpr/1.11.2", "fontconfig/2.15.0", "freetype/2.13.2", From 571fdfa692975163f66e4b92aa1c7fd8b4da8f13 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 29 May 2025 00:32:46 +0000 Subject: [PATCH 620/762] Boost 1.88 requires type_traits include --- wxdata/include/scwx/awips/coded_location.hpp | 1 + wxdata/include/scwx/awips/coded_time_motion_location.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/wxdata/include/scwx/awips/coded_location.hpp b/wxdata/include/scwx/awips/coded_location.hpp index 0b147444..1f29faa7 100644 --- a/wxdata/include/scwx/awips/coded_location.hpp +++ b/wxdata/include/scwx/awips/coded_location.hpp @@ -12,6 +12,7 @@ # pragma clang diagnostic ignored "-Wunused-parameter" #endif +#include #include #if defined(__clang__) diff --git a/wxdata/include/scwx/awips/coded_time_motion_location.hpp b/wxdata/include/scwx/awips/coded_time_motion_location.hpp index 123ea6e3..1d950ee0 100644 --- a/wxdata/include/scwx/awips/coded_time_motion_location.hpp +++ b/wxdata/include/scwx/awips/coded_time_motion_location.hpp @@ -13,6 +13,7 @@ # pragma clang diagnostic ignored "-Wunused-parameter" #endif +#include #include #if defined(__clang__) From ea7be1fa3a52ee61e5bdc910dd60446e38529dff Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 00:59:01 -0500 Subject: [PATCH 621/762] Update license year to 2025 --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 8c9c5fbd..799086a0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021-2024 Dan Paulat +Copyright (c) 2021-2025 Dan Paulat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 9c486d5018558337cb4dc04708f1dd7706ba042c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 01:01:08 -0500 Subject: [PATCH 622/762] CMAKE_AUTORCC does not work with multi-config --- scwx-qt/scwx-qt.cmake | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 466eddae..aff16705 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -6,7 +6,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTORCC ON) +set(CMAKE_AUTORCC OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -483,11 +483,12 @@ set(PROJECT_SOURCES ${HDR_MAIN} ${SRC_VIEW} ${SHADER_FILES} ${JSON_FILES} - ${RESOURCE_FILES} ${TS_FILES} ${CMAKE_FILES}) set(EXECUTABLE_SOURCES ${SRC_EXE_MAIN}) +qt_add_resources(PROJECT_SOURCES ${RESOURCE_FILES}) + source_group("Header Files\\main" FILES ${HDR_MAIN}) source_group("Source Files\\main" FILES ${SRC_MAIN}) source_group("Header Files\\config" FILES ${HDR_CONFIG}) From 102650e936110929390914d2afd3407142920e9f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 13:45:00 -0500 Subject: [PATCH 623/762] Add new utility setup scripts --- requirements.txt | 3 +++ setup-debug.bat | 25 ------------------------- setup-debug.sh | 28 ---------------------------- setup-multi.bat | 28 ---------------------------- setup-release.bat | 25 ------------------------- setup-release.sh | 28 ---------------------------- tools/configure-environment.bat | 22 ++++++++++++++++++++++ tools/lib/common-paths.bat | 1 + tools/lib/common-paths.sh | 2 ++ tools/lib/run-cmake-configure.bat | 19 +++++++++++++++++++ tools/lib/run-cmake-configure.sh | 23 +++++++++++++++++++++++ tools/lib/setup-common.bat | 26 ++++++++++++++++++++++++++ tools/lib/setup-common.sh | 27 +++++++++++++++++++++++++++ tools/lib/setup-conan.bat | 15 +++++++++++++++ tools/lib/setup-conan.sh | 16 ++++++++++++++++ tools/setup-common.bat | 4 ---- tools/setup-common.sh | 5 ----- tools/setup-debug-msvc2022.bat | 16 ++++++++++++++++ tools/setup-debug-ninja.bat | 16 ++++++++++++++++ tools/setup-debug.sh | 12 ++++++++++++ tools/setup-multi-msvc2022.bat | 15 +++++++++++++++ tools/setup-multi-ninja.bat | 15 +++++++++++++++ tools/setup-multi.sh | 9 +++++++++ tools/setup-release-msvc2022.bat | 16 ++++++++++++++++ tools/setup-release-ninja.bat | 16 ++++++++++++++++ tools/setup-release.sh | 12 ++++++++++++ 26 files changed, 281 insertions(+), 143 deletions(-) create mode 100644 requirements.txt delete mode 100644 setup-debug.bat delete mode 100755 setup-debug.sh delete mode 100644 setup-multi.bat delete mode 100644 setup-release.bat delete mode 100755 setup-release.sh create mode 100644 tools/configure-environment.bat create mode 100644 tools/lib/common-paths.bat create mode 100644 tools/lib/common-paths.sh create mode 100644 tools/lib/run-cmake-configure.bat create mode 100644 tools/lib/run-cmake-configure.sh create mode 100644 tools/lib/setup-common.bat create mode 100644 tools/lib/setup-common.sh create mode 100644 tools/lib/setup-conan.bat create mode 100644 tools/lib/setup-conan.sh delete mode 100644 tools/setup-common.bat delete mode 100755 tools/setup-common.sh create mode 100644 tools/setup-debug-msvc2022.bat create mode 100644 tools/setup-debug-ninja.bat create mode 100644 tools/setup-debug.sh create mode 100644 tools/setup-multi-msvc2022.bat create mode 100644 tools/setup-multi-ninja.bat create mode 100644 tools/setup-multi.sh create mode 100644 tools/setup-release-msvc2022.bat create mode 100644 tools/setup-release-ninja.bat create mode 100644 tools/setup-release.sh diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..afdd2a37 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +conan +geopandas +GitPython diff --git a/setup-debug.bat b/setup-debug.bat deleted file mode 100644 index e8076f02..00000000 --- a/setup-debug.bat +++ /dev/null @@ -1,25 +0,0 @@ -call tools\setup-common.bat - -set build_dir=build-debug -set build_type=Debug -set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.3 -set qt_arch=msvc2022_64 - -conan config install tools/conan/profiles/%conan_profile% -tf profiles -conan install . ^ - --remote conancenter ^ - --build missing ^ - --profile:all %conan_profile% ^ - --settings:all build_type=%build_type% ^ - --output-folder %build_dir%/conan - -mkdir %build_dir% -cmake -B %build_dir% -S . ^ - -DCMAKE_BUILD_TYPE=%build_type% ^ - -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ - -DCONAN_HOST_PROFILE=%conan_profile% ^ - -DCONAN_BUILD_PROFILE=%conan_profile% -pause diff --git a/setup-debug.sh b/setup-debug.sh deleted file mode 100755 index 689ac617..00000000 --- a/setup-debug.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -./tools/setup-common.sh - -build_dir=${1:-build-debug} -build_type=Debug -conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.3 -qt_arch=gcc_64 -script_dir="$(dirname "$(readlink -f "$0")")" - -conan config install tools/conan/profiles/${conan_profile} -tf profiles -conan install . \ - --remote conancenter \ - --build missing \ - --profile:all ${conan_profile} \ - --settings:all build_type=${build_type} \ - --output-folder ${build_dir}/conan - -mkdir -p ${build_dir} -cmake -B ${build_dir} -S . \ - -DCMAKE_BUILD_TYPE=${build_type} \ - -DCMAKE_CONFIGURATION_TYPES=${build_type} \ - -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ - -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ - -DCONAN_HOST_PROFILE=${conan_profile} \ - -DCONAN_BUILD_PROFILE=${conan_profile} \ - -G Ninja diff --git a/setup-multi.bat b/setup-multi.bat deleted file mode 100644 index b7f0b8df..00000000 --- a/setup-multi.bat +++ /dev/null @@ -1,28 +0,0 @@ -call tools\setup-common.bat - -set build_dir=build -set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.3 -set qt_arch=msvc2022_64 - -conan config install tools/conan/profiles/%conan_profile% -tf profiles -conan install . ^ - --remote conancenter ^ - --build missing ^ - --profile:all %conan_profile% ^ - --settings:all build_type=Debug ^ - --output-folder %build_dir%/conan -conan install . ^ - --remote conancenter ^ - --build missing ^ - --profile:all %conan_profile% ^ - --settings:all build_type=Release ^ - --output-folder %build_dir%/conan - -mkdir %build_dir% -cmake -B %build_dir% -S . ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ - -DCONAN_HOST_PROFILE=%conan_profile% ^ - -DCONAN_BUILD_PROFILE=%conan_profile% -pause diff --git a/setup-release.bat b/setup-release.bat deleted file mode 100644 index e019e204..00000000 --- a/setup-release.bat +++ /dev/null @@ -1,25 +0,0 @@ -call tools\setup-common.bat - -set build_dir=build-release -set build_type=Release -set conan_profile=scwx-win64_msvc2022 -set qt_version=6.8.3 -set qt_arch=msvc2022_64 - -conan config install tools/conan/profiles/%conan_profile% -tf profiles -conan install . ^ - --remote conancenter ^ - --build missing ^ - --profile:all %conan_profile% ^ - --settings:all build_type=%build_type% ^ - --output-folder %build_dir%/conan - -mkdir %build_dir% -cmake -B %build_dir% -S . ^ - -DCMAKE_BUILD_TYPE=%build_type% ^ - -DCMAKE_CONFIGURATION_TYPES=%build_type% ^ - -DCMAKE_PREFIX_PATH=C:/Qt/%qt_version%/%qt_arch% ^ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=external/cmake-conan/conan_provider.cmake ^ - -DCONAN_HOST_PROFILE=%conan_profile% ^ - -DCONAN_BUILD_PROFILE=%conan_profile% -pause diff --git a/setup-release.sh b/setup-release.sh deleted file mode 100755 index 8d2b5fe6..00000000 --- a/setup-release.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -./tools/setup-common.sh - -build_dir=${1:-build-release} -build_type=Release -conan_profile=${2:-scwx-linux_gcc-11} -qt_version=6.8.3 -qt_arch=gcc_64 -script_dir="$(dirname "$(readlink -f "$0")")" - -conan config install tools/conan/profiles/${conan_profile} -tf profiles -conan install . \ - --remote conancenter \ - --build missing \ - --profile:all ${conan_profile} \ - --settings:all build_type=${build_type} \ - --output-folder ${build_dir}/conan - -mkdir -p ${build_dir} -cmake -B ${build_dir} -S . \ - -DCMAKE_BUILD_TYPE=${build_type} \ - -DCMAKE_CONFIGURATION_TYPES=${build_type} \ - -DCMAKE_INSTALL_PREFIX=${build_dir}/${build_type}/supercell-wx \ - -DCMAKE_PREFIX_PATH=/opt/Qt/${qt_version}/${qt_arch} \ - -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${script_dir}/external/cmake-conan/conan_provider.cmake \ - -DCONAN_HOST_PROFILE=${conan_profile} \ - -DCONAN_BUILD_PROFILE=${conan_profile} \ - -G Ninja diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat new file mode 100644 index 00000000..caecbe33 --- /dev/null +++ b/tools/configure-environment.bat @@ -0,0 +1,22 @@ +@setlocal enabledelayedexpansion + +@set script_dir=%~dp0 + +:: Install Python packages +@pip install --upgrade -r "%script_dir%\..\requirements.txt" + +:: Configure default Conan profile +@conan profile detect -e + +:: Conan profiles +@set profile_count=1 +@set /a last_profile=profile_count - 1 +@set conan_profile[0]=scwx-win64_msvc2022 + +:: Install Conan profiles +@for /L %%i in (0,1,!last_profile!) do @( + set "profile_name=!conan_profile[%%i]!" + conan config install "%script_dir%\conan\profiles\%profile_name%" -tf profiles +) + +@pause diff --git a/tools/lib/common-paths.bat b/tools/lib/common-paths.bat new file mode 100644 index 00000000..69f6c6fc --- /dev/null +++ b/tools/lib/common-paths.bat @@ -0,0 +1 @@ +@set qt_version=6.8.3 diff --git a/tools/lib/common-paths.sh b/tools/lib/common-paths.sh new file mode 100644 index 00000000..a1f48932 --- /dev/null +++ b/tools/lib/common-paths.sh @@ -0,0 +1,2 @@ +#!/bin/bash +export qt_version=6.8.3 diff --git a/tools/lib/run-cmake-configure.bat b/tools/lib/run-cmake-configure.bat new file mode 100644 index 00000000..6dfdd7ba --- /dev/null +++ b/tools/lib/run-cmake-configure.bat @@ -0,0 +1,19 @@ +@echo off +set script_dir=%~dp0 + +set cmake_args=-B "%build_dir%" -S "%script_dir%\..\.." ^ + -G "%generator%" ^ + -DCMAKE_PREFIX_PATH="%qt_base%/%qt_version%/%qt_arch%" ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="%script_dir%\..\..\external\cmake-conan\conan_provider.cmake" ^ + -DCONAN_HOST_PROFILE=%conan_profile% ^ + -DCONAN_BUILD_PROFILE=%conan_profile% + +if defined build_type ( + set cmake_args=%cmake_args% ^ + -DCMAKE_BUILD_TYPE=%build_type% ^ + -DCMAKE_CONFIGURATION_TYPES=%build_type% +) +@echo on + +mkdir "%build_dir%" +cmake %cmake_args% diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh new file mode 100644 index 00000000..24e31c1f --- /dev/null +++ b/tools/lib/run-cmake-configure.sh @@ -0,0 +1,23 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +cmake_args=( + -B "${build_dir}" + -S "${script_dir}/../.." + -G "${generator}" + -DCMAKE_PREFIX_PATH="${qt_base}/${qt_version}/${qt_arch}" + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${script_dir}/../../external/cmake-conan/conan_provider.cmake" + -DCONAN_HOST_PROFILE="${conan_profile}" + -DCONAN_BUILD_PROFILE="${conan_profile}" +) + +if [[ -n "${build_type}" ]]; then + cmake_args+=( + -DCMAKE_BUILD_TYPE="${build_type}" + -DCMAKE_CONFIGURATION_TYPES="${build_type}" + -DCMAKE_INSTALL_PREFIX="${build_dir}/${build_type}/supercell-wx" + ) +fi + +mkdir -p "${build_dir}" +cmake "${cmake_args[@]}" diff --git a/tools/lib/setup-common.bat b/tools/lib/setup-common.bat new file mode 100644 index 00000000..d04cf430 --- /dev/null +++ b/tools/lib/setup-common.bat @@ -0,0 +1,26 @@ +@set script_dir=%~dp0 + +:: Import common paths +@call lib\common-paths.bat + +:: Install Python packages +pip install --upgrade -r "%script_dir%\..\..\requirements.txt" + +@if defined build_type ( + :: Install Conan profile and packages + call lib\setup-conan.bat +) else ( + :: Install Conan profile and debug packages + set build_type=Debug + call lib\setup-conan.bat + + :: Install Conan profile and release packages + set build_type=Release + call lib\setup-conan.bat + + :: Unset build_type + set build_type= +) + +:: Run CMake Configure +@call lib\run-cmake-configure.bat diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh new file mode 100644 index 00000000..48c6b6c6 --- /dev/null +++ b/tools/lib/setup-common.sh @@ -0,0 +1,27 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +# Import common paths +source ./common-paths.sh + +# Install Python packages +pip install --upgrade --user ${script_dir}/../../requirements.txt + +if [[ -n "${build_type}" ]]; then + # Install Conan profile and packages + ./setup-conan.sh +else + # Install Conan profile and debug packages + export build_type=Debug + ./setup-conan.sh + + # Install Conan profile and release packages + export build_type=Release + ./setup-conan.sh + + # Unset build_type + unset build_type +fi + +# Run CMake Configure +./run-cmake-configure.sh diff --git a/tools/lib/setup-conan.bat b/tools/lib/setup-conan.bat new file mode 100644 index 00000000..e3f4f444 --- /dev/null +++ b/tools/lib/setup-conan.bat @@ -0,0 +1,15 @@ +@set script_dir=%~dp0 + +:: Configure default Conan profile +conan profile detect -e + +:: Install selected Conan profile +conan config install "%script_dir%\..\conan\profiles\%conan_profile%" -tf profiles + +:: Install Conan packages +conan install "%script_dir%\..\.." ^ + --remote conancenter ^ + --build missing ^ + --profile:all %conan_profile% ^ + --settings:all build_type=%build_type% ^ + --output-folder "%build_dir%\conan" diff --git a/tools/lib/setup-conan.sh b/tools/lib/setup-conan.sh new file mode 100644 index 00000000..2ac38ee7 --- /dev/null +++ b/tools/lib/setup-conan.sh @@ -0,0 +1,16 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +# Configure default Conan profile +conan profile detect -e + +# Install selected Conan profile +conan config install "${script_dir}/../conan/profiles/${conan_profile}" -tf profiles + +# Install Conan packages +conan install "${script_dir}/../.." \ + --remote conancenter \ + --build missing \ + --profile:all ${conan_profile} \ + --settings:all build_type=${build_type} \ + --output-folder "${build_dir}/conan" diff --git a/tools/setup-common.bat b/tools/setup-common.bat deleted file mode 100644 index bada34ed..00000000 --- a/tools/setup-common.bat +++ /dev/null @@ -1,4 +0,0 @@ -pip install --upgrade conan -pip install --upgrade geopandas -pip install --upgrade GitPython -conan profile detect -e diff --git a/tools/setup-common.sh b/tools/setup-common.sh deleted file mode 100755 index 2533d6ec..00000000 --- a/tools/setup-common.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -pip install --upgrade --user conan -pip install --upgrade --user geopandas -pip install --upgrade --user GitPython -conan profile detect -e diff --git a/tools/setup-debug-msvc2022.bat b/tools/setup-debug-msvc2022.bat new file mode 100644 index 00000000..4e775e3f --- /dev/null +++ b/tools/setup-debug-msvc2022.bat @@ -0,0 +1,16 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-debug-msvc2022 +@set build_type=Debug +@set conan_profile=scwx-win64_msvc2022 +@set generator=Visual Studio 17 2022 +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-debug-ninja.bat b/tools/setup-debug-ninja.bat new file mode 100644 index 00000000..fb360f25 --- /dev/null +++ b/tools/setup-debug-ninja.bat @@ -0,0 +1,16 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-debug-ninja +@set build_type=Debug +@set conan_profile=scwx-win64_msvc2022 +@set generator=Ninja +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-debug.sh b/tools/setup-debug.sh new file mode 100644 index 00000000..bcbec034 --- /dev/null +++ b/tools/setup-debug.sh @@ -0,0 +1,12 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +export build_dir=${1:-${script_dir}/../build-debug} +export build_type=Debug +export conan_profile=${2:-scwx-linux_gcc-11} +export generator=Ninja +export qt_base=/opt/Qt +export qt_arch=gcc_64 + +# Perform common setup +./lib/setup-common.sh diff --git a/tools/setup-multi-msvc2022.bat b/tools/setup-multi-msvc2022.bat new file mode 100644 index 00000000..07d6c567 --- /dev/null +++ b/tools/setup-multi-msvc2022.bat @@ -0,0 +1,15 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-msvc2022 +@set conan_profile=scwx-win64_msvc2022 +@set generator=Visual Studio 17 2022 +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-multi-ninja.bat b/tools/setup-multi-ninja.bat new file mode 100644 index 00000000..11327488 --- /dev/null +++ b/tools/setup-multi-ninja.bat @@ -0,0 +1,15 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-ninja +@set conan_profile=scwx-win64_msvc2022 +@set generator=Ninja +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-multi.sh b/tools/setup-multi.sh new file mode 100644 index 00000000..35661767 --- /dev/null +++ b/tools/setup-multi.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export build_dir=${1:-build-release} +export conan_profile=${2:-scwx-linux_gcc-11} +export generator=Ninja +export qt_base=/opt/Qt +export qt_arch=gcc_64 + +# Perform common setup +./lib/setup-common.sh diff --git a/tools/setup-release-msvc2022.bat b/tools/setup-release-msvc2022.bat new file mode 100644 index 00000000..d1f57aca --- /dev/null +++ b/tools/setup-release-msvc2022.bat @@ -0,0 +1,16 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-release-msvc2022 +@set build_type=Release +@set conan_profile=scwx-win64_msvc2022 +@set generator=Visual Studio 17 2022 +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-release-ninja.bat b/tools/setup-release-ninja.bat new file mode 100644 index 00000000..d452f408 --- /dev/null +++ b/tools/setup-release-ninja.bat @@ -0,0 +1,16 @@ +@set script_dir=%~dp0 + +@set build_dir=%script_dir%\..\build-release-ninja +@set build_type=Release +@set conan_profile=scwx-win64_msvc2022 +@set generator=Ninja +@set qt_base=C:/Qt +@set qt_arch=msvc2022_64 + +:: Assign user-specified build directory +@if not "%~1"=="" set build_dir=%~1 + +:: Perform common setup +@call lib\setup-common.bat + +@pause diff --git a/tools/setup-release.sh b/tools/setup-release.sh new file mode 100644 index 00000000..3889983b --- /dev/null +++ b/tools/setup-release.sh @@ -0,0 +1,12 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +export build_dir=${1:-${script_dir}/../build-release} +export build_type=Release +export conan_profile=${2:-scwx-linux_gcc-11} +export generator=Ninja +export qt_base=/opt/Qt +export qt_arch=gcc_64 + +# Perform common setup +./lib/setup-common.sh From 6667f46c536d5b6bde397aec942120937af2344e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 14:56:26 -0500 Subject: [PATCH 624/762] Setup scripts should be runnable from any directory --- tools/lib/setup-common.bat | 10 +++++----- tools/setup-debug-msvc2022.bat | 2 +- tools/setup-debug-ninja.bat | 2 +- tools/setup-multi-msvc2022.bat | 2 +- tools/setup-multi-ninja.bat | 2 +- tools/setup-release-msvc2022.bat | 2 +- tools/setup-release-ninja.bat | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tools/lib/setup-common.bat b/tools/lib/setup-common.bat index d04cf430..9ad81104 100644 --- a/tools/lib/setup-common.bat +++ b/tools/lib/setup-common.bat @@ -1,26 +1,26 @@ @set script_dir=%~dp0 :: Import common paths -@call lib\common-paths.bat +@call %script_dir%\common-paths.bat :: Install Python packages pip install --upgrade -r "%script_dir%\..\..\requirements.txt" @if defined build_type ( :: Install Conan profile and packages - call lib\setup-conan.bat + call %script_dir%\setup-conan.bat ) else ( :: Install Conan profile and debug packages set build_type=Debug - call lib\setup-conan.bat + call %script_dir%\setup-conan.bat :: Install Conan profile and release packages set build_type=Release - call lib\setup-conan.bat + call %script_dir%\setup-conan.bat :: Unset build_type set build_type= ) :: Run CMake Configure -@call lib\run-cmake-configure.bat +@call %script_dir%\run-cmake-configure.bat diff --git a/tools/setup-debug-msvc2022.bat b/tools/setup-debug-msvc2022.bat index 4e775e3f..1aba72b1 100644 --- a/tools/setup-debug-msvc2022.bat +++ b/tools/setup-debug-msvc2022.bat @@ -11,6 +11,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause diff --git a/tools/setup-debug-ninja.bat b/tools/setup-debug-ninja.bat index fb360f25..4e488bd2 100644 --- a/tools/setup-debug-ninja.bat +++ b/tools/setup-debug-ninja.bat @@ -11,6 +11,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause diff --git a/tools/setup-multi-msvc2022.bat b/tools/setup-multi-msvc2022.bat index 07d6c567..ea54091f 100644 --- a/tools/setup-multi-msvc2022.bat +++ b/tools/setup-multi-msvc2022.bat @@ -10,6 +10,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause diff --git a/tools/setup-multi-ninja.bat b/tools/setup-multi-ninja.bat index 11327488..e7a424ca 100644 --- a/tools/setup-multi-ninja.bat +++ b/tools/setup-multi-ninja.bat @@ -10,6 +10,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause diff --git a/tools/setup-release-msvc2022.bat b/tools/setup-release-msvc2022.bat index d1f57aca..3a988f56 100644 --- a/tools/setup-release-msvc2022.bat +++ b/tools/setup-release-msvc2022.bat @@ -11,6 +11,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause diff --git a/tools/setup-release-ninja.bat b/tools/setup-release-ninja.bat index d452f408..667721d5 100644 --- a/tools/setup-release-ninja.bat +++ b/tools/setup-release-ninja.bat @@ -11,6 +11,6 @@ @if not "%~1"=="" set build_dir=%~1 :: Perform common setup -@call lib\setup-common.bat +@call %script_dir%\lib\setup-common.bat @pause From 3de270c2a18be69fdb44c9ecd9e8c4969ef19177 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 15:19:46 -0500 Subject: [PATCH 625/762] Remove Qt6::WidgetsPrivate from qt6ct-widgets --- external/qt6ct.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/qt6ct.cmake b/external/qt6ct.cmake index 665b5368..cdae0b25 100644 --- a/external/qt6ct.cmake +++ b/external/qt6ct.cmake @@ -46,7 +46,7 @@ target_compile_definitions(qt6ct-common PRIVATE QT6CT_LIBRARY) add_library(qt6ct-widgets STATIC ${qt6ct-widgets-source}) set_target_properties(qt6ct-widgets PROPERTIES VERSION ${QT6CT_VERSION}) -target_link_libraries(qt6ct-widgets PRIVATE Qt6::Widgets Qt6::WidgetsPrivate qt6ct-common) +target_link_libraries(qt6ct-widgets PRIVATE Qt6::Widgets qt6ct-common) target_compile_definitions(qt6ct-widgets PRIVATE QT6CT_LIBRARY) if (MSVC) From 6fca7234041b580f5e5b0d65a55ee47294b1e028 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 May 2025 19:08:40 -0500 Subject: [PATCH 626/762] Setup script cleanup for Linux --- tools/configure-environment.sh | 39 +++++++++++++++++++++++++++++++ tools/lib/common-paths.sh | 0 tools/lib/run-cmake-configure.bat | 15 +++++++----- tools/lib/run-cmake-configure.sh | 6 +++++ tools/lib/setup-common.sh | 22 ++++++++++++----- tools/lib/setup-conan.sh | 0 tools/setup-debug.sh | 4 ++-- tools/setup-multi-ninja.bat | 2 +- tools/setup-multi.sh | 8 ++++--- tools/setup-release.sh | 4 ++-- 10 files changed, 80 insertions(+), 20 deletions(-) create mode 100755 tools/configure-environment.sh mode change 100644 => 100755 tools/lib/common-paths.sh mode change 100644 => 100755 tools/lib/run-cmake-configure.sh mode change 100644 => 100755 tools/lib/setup-common.sh mode change 100644 => 100755 tools/lib/setup-conan.sh mode change 100644 => 100755 tools/setup-debug.sh mode change 100644 => 100755 tools/setup-multi.sh mode change 100644 => 100755 tools/setup-release.sh diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh new file mode 100755 index 00000000..86d8caaf --- /dev/null +++ b/tools/configure-environment.sh @@ -0,0 +1,39 @@ +#!/bin/bash +script_dir="$(dirname "$(readlink -f "$0")")" + +IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') + +if [ "${IN_VENV}" = "True" ]; then + # In a virtual environment, don't use --user + PIP_FLAGS="--upgrade" +else + # Not in a virtual environment, use --user + PIP_FLAGS="--upgrade --user" +fi + +# Install Python packages +pip install ${PIP_FLAGS} -r "${script_dir}/../requirements.txt" + +# Configure default Conan profile +conan profile detect -e + +# Conan profiles +conan_profiles=( + "scwx-linux_clang-17" + "scwx-linux_clang-17_armv8" + "scwx-linux_clang-18" + "scwx-linux_clang-18_armv8" + "scwx-linux_gcc-11" + "scwx-linux_gcc-11_armv8" + "scwx-linux_gcc-12" + "scwx-linux_gcc-12_armv8" + "scwx-linux_gcc-13" + "scwx-linux_gcc-13_armv8" + "scwx-linux_gcc-14" + "scwx-linux_gcc-14_armv8" + ) + +# Install Conan profiles +for profile_name in "${conan_profiles[@]}"; do + conan config install "${script_dir}/conan/profiles/${profile_name}" -tf profiles +done diff --git a/tools/lib/common-paths.sh b/tools/lib/common-paths.sh old mode 100644 new mode 100755 diff --git a/tools/lib/run-cmake-configure.bat b/tools/lib/run-cmake-configure.bat index 6dfdd7ba..4c25589c 100644 --- a/tools/lib/run-cmake-configure.bat +++ b/tools/lib/run-cmake-configure.bat @@ -1,19 +1,22 @@ -@echo off -set script_dir=%~dp0 +@set script_dir=%~dp0 -set cmake_args=-B "%build_dir%" -S "%script_dir%\..\.." ^ +@set cmake_args=-B "%build_dir%" -S "%script_dir%\..\.." ^ -G "%generator%" ^ -DCMAKE_PREFIX_PATH="%qt_base%/%qt_version%/%qt_arch%" ^ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="%script_dir%\..\..\external\cmake-conan\conan_provider.cmake" ^ -DCONAN_HOST_PROFILE=%conan_profile% ^ -DCONAN_BUILD_PROFILE=%conan_profile% -if defined build_type ( +@if defined build_type ( set cmake_args=%cmake_args% ^ -DCMAKE_BUILD_TYPE=%build_type% ^ -DCMAKE_CONFIGURATION_TYPES=%build_type% +) else ( + :: CMAKE_BUILD_TYPE isn't used to build, but is required by the Conan CMakeDeps generator + set cmake_args=%cmake_args% ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_CONFIGURATION_TYPES=Debug;Release ) -@echo on -mkdir "%build_dir%" +@mkdir "%build_dir%" cmake %cmake_args% diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh old mode 100644 new mode 100755 index 24e31c1f..a0961385 --- a/tools/lib/run-cmake-configure.sh +++ b/tools/lib/run-cmake-configure.sh @@ -17,6 +17,12 @@ if [[ -n "${build_type}" ]]; then -DCMAKE_CONFIGURATION_TYPES="${build_type}" -DCMAKE_INSTALL_PREFIX="${build_dir}/${build_type}/supercell-wx" ) +else + # CMAKE_BUILD_TYPE isn't used to build, but is required by the Conan CMakeDeps generator + cmake_args+=( + -DCMAKE_BUILD_TYPE="Release" + -DCMAKE_CONFIGURATION_TYPES="Debug;Release" + ) fi mkdir -p "${build_dir}" diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh old mode 100644 new mode 100755 index 48c6b6c6..3eacfa08 --- a/tools/lib/setup-common.sh +++ b/tools/lib/setup-common.sh @@ -2,26 +2,36 @@ script_dir="$(dirname "$(readlink -f "$0")")" # Import common paths -source ./common-paths.sh +source ${script_dir}/common-paths.sh + +IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') + +if [ "${IN_VENV}" = "True" ]; then + # In a virtual environment, don't use --user + PIP_FLAGS="--upgrade" +else + # Not in a virtual environment, use --user + PIP_FLAGS="--upgrade --user" +fi # Install Python packages -pip install --upgrade --user ${script_dir}/../../requirements.txt +pip install ${PIP_FLAGS} -r ${script_dir}/../../requirements.txt if [[ -n "${build_type}" ]]; then # Install Conan profile and packages - ./setup-conan.sh + ${script_dir}/setup-conan.sh else # Install Conan profile and debug packages export build_type=Debug - ./setup-conan.sh + ${script_dir}/setup-conan.sh # Install Conan profile and release packages export build_type=Release - ./setup-conan.sh + ${script_dir}/setup-conan.sh # Unset build_type unset build_type fi # Run CMake Configure -./run-cmake-configure.sh +${script_dir}/run-cmake-configure.sh diff --git a/tools/lib/setup-conan.sh b/tools/lib/setup-conan.sh old mode 100644 new mode 100755 diff --git a/tools/setup-debug.sh b/tools/setup-debug.sh old mode 100644 new mode 100755 index bcbec034..ae0f5d07 --- a/tools/setup-debug.sh +++ b/tools/setup-debug.sh @@ -1,7 +1,7 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir=${1:-${script_dir}/../build-debug} +export build_dir="${1:-${script_dir}/../build-debug}" export build_type=Debug export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja @@ -9,4 +9,4 @@ export qt_base=/opt/Qt export qt_arch=gcc_64 # Perform common setup -./lib/setup-common.sh +${script_dir}/lib/setup-common.sh diff --git a/tools/setup-multi-ninja.bat b/tools/setup-multi-ninja.bat index e7a424ca..9e2565e6 100644 --- a/tools/setup-multi-ninja.bat +++ b/tools/setup-multi-ninja.bat @@ -2,7 +2,7 @@ @set build_dir=%script_dir%\..\build-ninja @set conan_profile=scwx-win64_msvc2022 -@set generator=Ninja +@set generator=Ninja Multi-Config @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-multi.sh b/tools/setup-multi.sh old mode 100644 new mode 100755 index 35661767..6a9bc46f --- a/tools/setup-multi.sh +++ b/tools/setup-multi.sh @@ -1,9 +1,11 @@ #!/bin/bash -export build_dir=${1:-build-release} +script_dir="$(dirname "$(readlink -f "$0")")" + +export build_dir="${1:-${script_dir}/../build}" export conan_profile=${2:-scwx-linux_gcc-11} -export generator=Ninja +export generator="Ninja Multi-Config" export qt_base=/opt/Qt export qt_arch=gcc_64 # Perform common setup -./lib/setup-common.sh +${script_dir}/lib/setup-common.sh diff --git a/tools/setup-release.sh b/tools/setup-release.sh old mode 100644 new mode 100755 index 3889983b..989e387d --- a/tools/setup-release.sh +++ b/tools/setup-release.sh @@ -1,7 +1,7 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir=${1:-${script_dir}/../build-release} +export build_dir="${1:-${script_dir}/../build-release}" export build_type=Release export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja @@ -9,4 +9,4 @@ export qt_base=/opt/Qt export qt_arch=gcc_64 # Perform common setup -./lib/setup-common.sh +${script_dir}/lib/setup-common.sh From 5e10cdd6b43841b9ba174dc167e141e0df9fc5e1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 26 May 2025 09:41:04 -0500 Subject: [PATCH 627/762] Rename Windows Conan profile --- .github/workflows/ci.yml | 4 ++-- .../{scwx-win64_msvc2022 => scwx-windows_msvc2022_x64} | 0 tools/configure-environment.bat | 2 +- tools/setup-debug-msvc2022.bat | 2 +- tools/setup-debug-ninja.bat | 2 +- tools/setup-multi-msvc2022.bat | 2 +- tools/setup-multi-ninja.bat | 2 +- tools/setup-release-msvc2022.bat | 2 +- tools/setup-release-ninja.bat | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) rename tools/conan/profiles/{scwx-win64_msvc2022 => scwx-windows_msvc2022_x64} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad80a89f..f8bd4ca2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: include: - - name: win64_msvc2022 + - name: windows_msvc2022_x64 os: windows-2022 build_type: Release env_cc: '' @@ -34,7 +34,7 @@ jobs: qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' conan_package_manager: '' - conan_profile: scwx-win64_msvc2022 + conan_profile: scwx-windows_msvc2022_x64 appimage_arch: '' artifact_suffix: windows-x64 - name: linux_gcc_x64 diff --git a/tools/conan/profiles/scwx-win64_msvc2022 b/tools/conan/profiles/scwx-windows_msvc2022_x64 similarity index 100% rename from tools/conan/profiles/scwx-win64_msvc2022 rename to tools/conan/profiles/scwx-windows_msvc2022_x64 diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat index caecbe33..043c84f7 100644 --- a/tools/configure-environment.bat +++ b/tools/configure-environment.bat @@ -11,7 +11,7 @@ :: Conan profiles @set profile_count=1 @set /a last_profile=profile_count - 1 -@set conan_profile[0]=scwx-win64_msvc2022 +@set conan_profile[0]=scwx-windows_msvc2022_x64 :: Install Conan profiles @for /L %%i in (0,1,!last_profile!) do @( diff --git a/tools/setup-debug-msvc2022.bat b/tools/setup-debug-msvc2022.bat index 1aba72b1..8b85f18f 100644 --- a/tools/setup-debug-msvc2022.bat +++ b/tools/setup-debug-msvc2022.bat @@ -2,7 +2,7 @@ @set build_dir=%script_dir%\..\build-debug-msvc2022 @set build_type=Debug -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-debug-ninja.bat b/tools/setup-debug-ninja.bat index 4e488bd2..c7fcfa3c 100644 --- a/tools/setup-debug-ninja.bat +++ b/tools/setup-debug-ninja.bat @@ -2,7 +2,7 @@ @set build_dir=%script_dir%\..\build-debug-ninja @set build_type=Debug -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Ninja @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-multi-msvc2022.bat b/tools/setup-multi-msvc2022.bat index ea54091f..dac9c528 100644 --- a/tools/setup-multi-msvc2022.bat +++ b/tools/setup-multi-msvc2022.bat @@ -1,7 +1,7 @@ @set script_dir=%~dp0 @set build_dir=%script_dir%\..\build-msvc2022 -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-multi-ninja.bat b/tools/setup-multi-ninja.bat index 9e2565e6..fe0f8008 100644 --- a/tools/setup-multi-ninja.bat +++ b/tools/setup-multi-ninja.bat @@ -1,7 +1,7 @@ @set script_dir=%~dp0 @set build_dir=%script_dir%\..\build-ninja -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Ninja Multi-Config @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-release-msvc2022.bat b/tools/setup-release-msvc2022.bat index 3a988f56..5e3621ca 100644 --- a/tools/setup-release-msvc2022.bat +++ b/tools/setup-release-msvc2022.bat @@ -2,7 +2,7 @@ @set build_dir=%script_dir%\..\build-release-msvc2022 @set build_type=Release -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 diff --git a/tools/setup-release-ninja.bat b/tools/setup-release-ninja.bat index 667721d5..2160562c 100644 --- a/tools/setup-release-ninja.bat +++ b/tools/setup-release-ninja.bat @@ -2,7 +2,7 @@ @set build_dir=%script_dir%\..\build-release-ninja @set build_type=Release -@set conan_profile=scwx-win64_msvc2022 +@set conan_profile=scwx-windows_msvc2022_x64 @set generator=Ninja @set qt_base=C:/Qt @set qt_arch=msvc2022_64 From 4ad2983c24b79de972808141dba6d8b0cb821610 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 26 May 2025 22:14:36 -0500 Subject: [PATCH 628/762] Add CMakePresets --- .gitignore | 4 + CMakePresets.json | 205 ++++++++++++++++++++++++++++++++ tools/configure-environment.bat | 2 +- 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 CMakePresets.json diff --git a/.gitignore b/.gitignore index 7002668a..cbe7e26d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ install_manifest.txt compile_commands.json CTestTestfile.cmake _deps + +# Editor directories +.idea/ +.vs/ diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..ff3d87ec --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,205 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake" + } + }, + { + "name": "windows-base", + "inherits": "base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "enableClangTidyCodeAnalysis": true, + "hostOS": [ + "Windows" + ] + } + } + }, + { + "name": "windows-x64-base", + "inherits": "windows-base", + "hidden": true + }, + { + "name": "linux-base", + "inherits": "base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "windows-msvc2022-x64-base", + "inherits": "windows-x64-base", + "hidden": true, + "cacheVariables": { + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + } + }, + { + "name": "linux-gcc-base", + "inherits": "linux-base", + "hidden": true, + "cacheVariables": { + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11" + }, + "environment": { + "CC": "gcc-11", + "CXX": "g++-11" + } + }, + { + "name": "windows-msvc2022-x64-debug", + "inherits": "windows-msvc2022-x64-base", + "displayName": "Windows MSVC 2022 x64 Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-msvc2022-x64-release", + "inherits": "windows-msvc2022-x64-base", + "displayName": "Windows MSVC 2022 x64 Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-gcc-debug", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx" + } + }, + { + "name": "linux-gcc-release", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx" + } + }, + { + "name": "ci-linux-gcc14", + "inherits": "linux-gcc-base", + "displayName": "CI Linux GCC 14", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-14", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-14" + }, + "environment": { + "CC": "gcc-14", + "CXX": "g++-14" + } + }, + { + "name": "ci-linux-clang17", + "inherits": "linux-gcc-base", + "displayName": "CI Linux Clang 17", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_clang-17", + "CONAN_BUILD_PROFILE": "scwx-linux_clang-17" + }, + "environment": { + "CC": "clang-17", + "CXX": "clang++-17" + } + }, + { + "name": "ci-linux-gcc-arm64", + "inherits": "linux-gcc-base", + "displayName": "CI Linux GCC ARM64", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8", + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64" + }, + "environment": { + "CC": "gcc-11", + "CXX": "g++-11" + } + } + ], + "buildPresets": [ + { + "name": "windows-msvc2022-x64-debug", + "configurePreset": "windows-msvc2022-x64-debug", + "displayName": "Windows MSVC 2022 x64 Debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc2022-x64-release", + "configurePreset": "windows-msvc2022-x64-release", + "displayName": "Windows MSVC 2022 x64 Release", + "configuration": "Release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "displayName": "Linux GCC Debug", + "configuration": "Debug" + }, + { + "name": "linux-gcc-release", + "configurePreset": "linux-gcc-release", + "displayName": "Linux GCC Release", + "configuration": "Release" + } + ], + "testPresets": [ + { + "name": "windows-msvc2022-x64-debug", + "configurePreset": "windows-msvc2022-x64-debug", + "displayName": "Windows MSVC 2022 x64 Debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc2022-x64-release", + "configurePreset": "windows-msvc2022-x64-release", + "displayName": "Windows MSVC 2022 x64 Release", + "configuration": "Release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "displayName": "Linux GCC Debug", + "configuration": "Debug" + }, + { + "name": "linux-gcc-release", + "configurePreset": "linux-gcc-release", + "displayName": "Linux GCC Release", + "configuration": "Release" + } + ] +} \ No newline at end of file diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat index 043c84f7..0a8e5cee 100644 --- a/tools/configure-environment.bat +++ b/tools/configure-environment.bat @@ -16,7 +16,7 @@ :: Install Conan profiles @for /L %%i in (0,1,!last_profile!) do @( set "profile_name=!conan_profile[%%i]!" - conan config install "%script_dir%\conan\profiles\%profile_name%" -tf profiles + conan config install "%script_dir%\conan\profiles\!profile_name!" -tf profiles ) @pause From ac475999f5c036674c8298fe5195fa8de6d79e96 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 19:53:14 -0500 Subject: [PATCH 629/762] CMakePresets tweaks for Windows --- CMakePresets.json | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index ff3d87ec..c4885652 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -19,6 +19,7 @@ "name": "windows-base", "inherits": "base", "hidden": true, + "generator": "Visual Studio 17 2022", "condition": { "type": "equals", "lhs": "${hostSystemName}", @@ -26,7 +27,6 @@ }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { - "enableClangTidyCodeAnalysis": true, "hostOS": [ "Windows" ] @@ -58,6 +58,17 @@ "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" } }, + { + "name": "windows-msvc2022-x64-ninja-base", + "inherits": "windows-msvc2022-x64-base", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + } + }, { "name": "linux-gcc-base", "inherits": "linux-base", @@ -76,6 +87,10 @@ "name": "windows-msvc2022-x64-debug", "inherits": "windows-msvc2022-x64-base", "displayName": "Windows MSVC 2022 x64 Debug", + "architecture": { + "value": "x64", + "strategy": "external" + }, "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } @@ -84,6 +99,26 @@ "name": "windows-msvc2022-x64-release", "inherits": "windows-msvc2022-x64-base", "displayName": "Windows MSVC 2022 x64 Release", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-msvc2022-x64-ninja-debug", + "inherits": "windows-msvc2022-x64-ninja-base", + "displayName": "Windows MSVC 2022 x64 Ninja Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-msvc2022-x64-ninja-release", + "inherits": "windows-msvc2022-x64-ninja-base", + "displayName": "Windows MSVC 2022 x64 Ninja Release", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } From 0e90f8a7a11f14ffac494493e62f228f6b1e504c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 19:59:14 -0500 Subject: [PATCH 630/762] Activate Python virtual environment on Windows during configuration --- .gitignore | 3 +++ tools/configure-environment.bat | 14 +++++++++++++- tools/lib/run-cmake-configure.bat | 3 ++- tools/lib/setup-common.bat | 11 +++++++++++ tools/setup-debug-msvc2022.bat | 12 +++++++++++- tools/setup-debug-ninja.bat | 12 +++++++++++- tools/setup-multi-msvc2022.bat | 12 +++++++++++- tools/setup-multi-ninja.bat | 12 +++++++++++- tools/setup-release-msvc2022.bat | 12 +++++++++++- tools/setup-release-ninja.bat | 12 +++++++++++- 10 files changed, 95 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index cbe7e26d..60da0eda 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ _deps # Editor directories .idea/ .vs/ + +# Python Virtual Environment +.venv/ diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat index 0a8e5cee..e60336bb 100644 --- a/tools/configure-environment.bat +++ b/tools/configure-environment.bat @@ -1,9 +1,18 @@ @setlocal enabledelayedexpansion @set script_dir=%~dp0 +@set venv_path=%script_dir%\..\.venv + +:: Assign user-specified Python Virtual Environment +@if not "%~1"=="" set venv_path=%~f1 + +:: Activate Python Virtual Environment +python -m venv %venv_path% +@call %venv_path%\Scripts\activate.bat :: Install Python packages -@pip install --upgrade -r "%script_dir%\..\requirements.txt" +python -m pip install --upgrade pip +pip install --upgrade -r "%script_dir%\..\requirements.txt" :: Configure default Conan profile @conan profile detect -e @@ -19,4 +28,7 @@ conan config install "%script_dir%\conan\profiles\!profile_name!" -tf profiles ) +:: Deactivate Python Virtual Environment +@call %venv_path%\Scripts\deactivate.bat + @pause diff --git a/tools/lib/run-cmake-configure.bat b/tools/lib/run-cmake-configure.bat index 4c25589c..5c1f0b00 100644 --- a/tools/lib/run-cmake-configure.bat +++ b/tools/lib/run-cmake-configure.bat @@ -5,7 +5,8 @@ -DCMAKE_PREFIX_PATH="%qt_base%/%qt_version%/%qt_arch%" ^ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="%script_dir%\..\..\external\cmake-conan\conan_provider.cmake" ^ -DCONAN_HOST_PROFILE=%conan_profile% ^ - -DCONAN_BUILD_PROFILE=%conan_profile% + -DCONAN_BUILD_PROFILE=%conan_profile% ^ + -DSCWX_VENV_PATH=%venv_path% @if defined build_type ( set cmake_args=%cmake_args% ^ diff --git a/tools/lib/setup-common.bat b/tools/lib/setup-common.bat index 9ad81104..42b3dc16 100644 --- a/tools/lib/setup-common.bat +++ b/tools/lib/setup-common.bat @@ -3,7 +3,15 @@ :: Import common paths @call %script_dir%\common-paths.bat +:: Activate Python Virtual Environment +@if defined venv_path ( + echo Activating Python Virtual Environment: %venv_path% + python -m venv %venv_path% + call %venv_path%\Scripts\activate.bat +) + :: Install Python packages +python -m pip install --upgrade pip pip install --upgrade -r "%script_dir%\..\..\requirements.txt" @if defined build_type ( @@ -24,3 +32,6 @@ pip install --upgrade -r "%script_dir%\..\..\requirements.txt" :: Run CMake Configure @call %script_dir%\run-cmake-configure.bat + +:: Deactivate Python Virtual Environment +@call %venv_path%\Scripts\deactivate.bat diff --git a/tools/setup-debug-msvc2022.bat b/tools/setup-debug-msvc2022.bat index 8b85f18f..9ef029a3 100644 --- a/tools/setup-debug-msvc2022.bat +++ b/tools/setup-debug-msvc2022.bat @@ -6,9 +6,19 @@ @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat diff --git a/tools/setup-debug-ninja.bat b/tools/setup-debug-ninja.bat index c7fcfa3c..182c7956 100644 --- a/tools/setup-debug-ninja.bat +++ b/tools/setup-debug-ninja.bat @@ -6,9 +6,19 @@ @set generator=Ninja @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat diff --git a/tools/setup-multi-msvc2022.bat b/tools/setup-multi-msvc2022.bat index dac9c528..87e19fd3 100644 --- a/tools/setup-multi-msvc2022.bat +++ b/tools/setup-multi-msvc2022.bat @@ -5,9 +5,19 @@ @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat diff --git a/tools/setup-multi-ninja.bat b/tools/setup-multi-ninja.bat index fe0f8008..8962fc2b 100644 --- a/tools/setup-multi-ninja.bat +++ b/tools/setup-multi-ninja.bat @@ -5,9 +5,19 @@ @set generator=Ninja Multi-Config @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat diff --git a/tools/setup-release-msvc2022.bat b/tools/setup-release-msvc2022.bat index 5e3621ca..a4f805cc 100644 --- a/tools/setup-release-msvc2022.bat +++ b/tools/setup-release-msvc2022.bat @@ -6,9 +6,19 @@ @set generator=Visual Studio 17 2022 @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat diff --git a/tools/setup-release-ninja.bat b/tools/setup-release-ninja.bat index 2160562c..96ba0e55 100644 --- a/tools/setup-release-ninja.bat +++ b/tools/setup-release-ninja.bat @@ -6,9 +6,19 @@ @set generator=Ninja @set qt_base=C:/Qt @set qt_arch=msvc2022_64 +@set venv_path=%script_dir%\..\.venv :: Assign user-specified build directory -@if not "%~1"=="" set build_dir=%~1 +@if not "%~1"=="" set build_dir=%~f1 + +:: Assign user-specified Python Virtual Environment +@if not "%~2"=="" ( + if /i "%~2"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f2 + ) +) :: Perform common setup @call %script_dir%\lib\setup-common.bat From ffbe3aedad48cd9ce53c994da1602a5c558e9752 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 19:59:33 -0500 Subject: [PATCH 631/762] setup-multi.sh is not supported on Linux --- tools/setup-multi.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/setup-multi.sh b/tools/setup-multi.sh index 6a9bc46f..3b075bea 100755 --- a/tools/setup-multi.sh +++ b/tools/setup-multi.sh @@ -7,5 +7,9 @@ export generator="Ninja Multi-Config" export qt_base=/opt/Qt export qt_arch=gcc_64 +# FIXME: aws-sdk-cpp fails to configure using Ninja Multi-Config +echo "Ninja Multi-Config is not supported in Linux" +read -p "Press Enter to continue..." + # Perform common setup -${script_dir}/lib/setup-common.sh +# ${script_dir}/lib/setup-common.sh From ea2c2e8f589e5547a7df33e00b2f63b7df4ab5e8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 22:21:48 -0500 Subject: [PATCH 632/762] Add Python setup to CMakeLists.txt --- CMakeLists.txt | 7 ++++-- CMakePresets.json | 3 ++- tools/lib/run-cmake-configure.bat | 2 +- tools/scwx_config.cmake | 36 +++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bda4b47c..7a2da515 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.24) set(PROJECT_NAME supercell-wx) + +include(tools/scwx_config.cmake) + +scwx_python_setup() + project(${PROJECT_NAME} VERSION 0.4.9 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." @@ -11,8 +16,6 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_POLICY_DEFAULT_CMP0079 NEW) set(CMAKE_POLICY_DEFAULT_CMP0148 OLD) # aws-sdk-cpp uses FindPythonInterp -include(tools/scwx_config.cmake) - scwx_output_dirs_setup() enable_testing() diff --git a/CMakePresets.json b/CMakePresets.json index c4885652..440d1138 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -12,7 +12,8 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "cacheVariables": { - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake" + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake", + "SCWX_VIRTUAL_ENV": "${sourceDir}/.venv" } }, { diff --git a/tools/lib/run-cmake-configure.bat b/tools/lib/run-cmake-configure.bat index 5c1f0b00..785d5d8d 100644 --- a/tools/lib/run-cmake-configure.bat +++ b/tools/lib/run-cmake-configure.bat @@ -6,7 +6,7 @@ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="%script_dir%\..\..\external\cmake-conan\conan_provider.cmake" ^ -DCONAN_HOST_PROFILE=%conan_profile% ^ -DCONAN_BUILD_PROFILE=%conan_profile% ^ - -DSCWX_VENV_PATH=%venv_path% + -DSCWX_VIRTUAL_ENV=%venv_path% @if defined build_type ( set cmake_args=%cmake_args% ^ diff --git a/tools/scwx_config.cmake b/tools/scwx_config.cmake index 0919b22e..8373418b 100644 --- a/tools/scwx_config.cmake +++ b/tools/scwx_config.cmake @@ -17,3 +17,39 @@ macro(scwx_output_dirs_setup) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL ${CMAKE_CURRENT_BINARY_DIR}/MinSizeRel/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_CURRENT_BINARY_DIR}/Debug/lib) endmacro() + +macro(scwx_python_setup) + set(SCWX_VIRTUAL_ENV "" CACHE STRING "Python Virtual Environment") + + # Use a Python Virtual Environment + if (SCWX_VIRTUAL_ENV) + set(ENV{VIRTUAL_ENV} "${SCWX_VIRTUAL_ENV}") + + if (WIN32) + set(Python3_EXECUTABLE "$ENV{VIRTUAL_ENV}/Scripts/python.exe") + else() + set(Python3_EXECUTABLE "$ENV{VIRTUAL_ENV}/bin/python") + endif() + + message(STATUS "Using virtual environment: $ENV{VIRTUAL_ENV}") + else() + message(STATUS "Python virtual environment undefined") + endif() + + # Find Python + find_package(Python3 REQUIRED COMPONENTS Interpreter) + + # Verify we're using the right Python + message(STATUS "Python executable: ${Python3_EXECUTABLE}") + message(STATUS "Python version: ${Python3_VERSION}") + + # Only if we are in an application defined virtual environment + if (SCWX_VIRTUAL_ENV) + # Setup pip + set(PIP_ARGS install --upgrade -r "${CMAKE_SOURCE_DIR}/requirements.txt") + + # Install requirements + execute_process(COMMAND ${Python3_EXECUTABLE} -m pip ${PIP_ARGS} + RESULT_VARIABLE PIP_RESULT) + endif() +endmacro() From 9ce411d85ba8e9296c255cdbca46e7b2babff9f3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 23:15:17 -0500 Subject: [PATCH 633/762] Linux Python virtual environment updates --- tools/configure-environment.sh | 16 ++++++++++++++++ tools/lib/run-cmake-configure.sh | 1 + tools/lib/setup-common.sh | 13 +++++++++++++ tools/setup-debug.sh | 5 ++++- tools/setup-multi.sh | 5 ++++- tools/setup-release.sh | 5 ++++- 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh index 86d8caaf..0328db30 100755 --- a/tools/configure-environment.sh +++ b/tools/configure-environment.sh @@ -1,6 +1,16 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" +# Assign user-specified Python Virtual Environment +[ "${1:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${1:-${script_dir}/../.venv}")" + +# Activate Python Virtual Environment +if [ -n "${venv_path:-}" ]; then + python -m venv "${venv_path}" + source "${venv_path}/bin/activate" +fi + +# Detect if a Python Virtual Environment was specified above, or elsewhere IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') if [ "${IN_VENV}" = "True" ]; then @@ -12,6 +22,7 @@ else fi # Install Python packages +python -m pip install ${PIP_FLAGS} --upgrade pip pip install ${PIP_FLAGS} -r "${script_dir}/../requirements.txt" # Configure default Conan profile @@ -37,3 +48,8 @@ conan_profiles=( for profile_name in "${conan_profiles[@]}"; do conan config install "${script_dir}/conan/profiles/${profile_name}" -tf profiles done + +# Deactivate Python Virtual Environment +if [ -n "${venv_path:-}" ]; then + deactivate +fi diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh index a0961385..461d54d8 100755 --- a/tools/lib/run-cmake-configure.sh +++ b/tools/lib/run-cmake-configure.sh @@ -9,6 +9,7 @@ cmake_args=( -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${script_dir}/../../external/cmake-conan/conan_provider.cmake" -DCONAN_HOST_PROFILE="${conan_profile}" -DCONAN_BUILD_PROFILE="${conan_profile}" + -DSCWX_VIRTUAL_ENV="${venv_path}" ) if [[ -n "${build_type}" ]]; then diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh index 3eacfa08..a19d2fab 100755 --- a/tools/lib/setup-common.sh +++ b/tools/lib/setup-common.sh @@ -4,6 +4,13 @@ script_dir="$(dirname "$(readlink -f "$0")")" # Import common paths source ${script_dir}/common-paths.sh +# Activate Python Virtual Environment +if [ -n "${venv_path:-}" ]; then + python -m venv "${venv_path}" + source "${venv_path}/bin/activate" +fi + +# Detect if a Python Virtual Environment was specified above, or elsewhere IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') if [ "${IN_VENV}" = "True" ]; then @@ -15,6 +22,7 @@ else fi # Install Python packages +python -m pip install ${PIP_FLAGS} pip pip install ${PIP_FLAGS} -r ${script_dir}/../../requirements.txt if [[ -n "${build_type}" ]]; then @@ -35,3 +43,8 @@ fi # Run CMake Configure ${script_dir}/run-cmake-configure.sh + +# Deactivate Python Virtual Environment +if [ -n "${venv_path:-}" ]; then + deactivate +fi diff --git a/tools/setup-debug.sh b/tools/setup-debug.sh index ae0f5d07..6c17caba 100755 --- a/tools/setup-debug.sh +++ b/tools/setup-debug.sh @@ -1,12 +1,15 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir="${1:-${script_dir}/../build-debug}" +export build_dir="$(readlink -f "${1:-${script_dir}/../build-debug}")" export build_type=Debug export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja export qt_base=/opt/Qt export qt_arch=gcc_64 +# Assign user-specified Python Virtual Environment +[ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" + # Perform common setup ${script_dir}/lib/setup-common.sh diff --git a/tools/setup-multi.sh b/tools/setup-multi.sh index 3b075bea..f18bd5ac 100755 --- a/tools/setup-multi.sh +++ b/tools/setup-multi.sh @@ -1,12 +1,15 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir="${1:-${script_dir}/../build}" +export build_dir="$(readlink -f "${1:-${script_dir}/../build-debug}")" export conan_profile=${2:-scwx-linux_gcc-11} export generator="Ninja Multi-Config" export qt_base=/opt/Qt export qt_arch=gcc_64 +# Assign user-specified Python Virtual Environment +[ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" + # FIXME: aws-sdk-cpp fails to configure using Ninja Multi-Config echo "Ninja Multi-Config is not supported in Linux" read -p "Press Enter to continue..." diff --git a/tools/setup-release.sh b/tools/setup-release.sh index 989e387d..c55e75c7 100755 --- a/tools/setup-release.sh +++ b/tools/setup-release.sh @@ -1,12 +1,15 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir="${1:-${script_dir}/../build-release}" +export build_dir="$(readlink -f "${1:-${script_dir}/../build-debug}")" export build_type=Release export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja export qt_base=/opt/Qt export qt_arch=gcc_64 +# Assign user-specified Python Virtual Environment +[ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" + # Perform common setup ${script_dir}/lib/setup-common.sh From 7748d27b95cdf267782fb46b68ec709d5eaccb13 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 23:15:44 -0500 Subject: [PATCH 634/762] Add address sanitizer configurations to CMakePresets --- CMakePresets.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CMakePresets.json b/CMakePresets.json index 440d1138..14595a91 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -142,6 +142,32 @@ "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx" } }, + { + "name": "linux-gcc-debug-asan", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Debug Address Sanitizer", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx", + "SCWX_ADDRESS_SANITIZER": { + "type": "BOOL", + "value": "ON" + } + } + }, + { + "name": "linux-gcc-release-asan", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Release Address Sanitizer", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx", + "SCWX_ADDRESS_SANITIZER": { + "type": "BOOL", + "value": "ON" + } + } + }, { "name": "ci-linux-gcc14", "inherits": "linux-gcc-base", From 3718fc8872bb32ce06d04ad695d9478a7ba0de21 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 23:20:19 -0500 Subject: [PATCH 635/762] Add option to configure_environment.bat to use global Python environment if desired --- tools/configure-environment.bat | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat index e60336bb..d386ecc0 100644 --- a/tools/configure-environment.bat +++ b/tools/configure-environment.bat @@ -4,11 +4,20 @@ @set venv_path=%script_dir%\..\.venv :: Assign user-specified Python Virtual Environment -@if not "%~1"=="" set venv_path=%~f1 +@if not "%~1"=="" ( + if /i "%~1"=="none" ( + set venv_path= + ) else ( + set venv_path=%~f1 + ) +) :: Activate Python Virtual Environment -python -m venv %venv_path% -@call %venv_path%\Scripts\activate.bat +@if defined venv_path ( + echo Activating Python Virtual Environment: %venv_path% + python -m venv %venv_path% + call %venv_path%\Scripts\activate.bat +) :: Install Python packages python -m pip install --upgrade pip @@ -29,6 +38,8 @@ pip install --upgrade -r "%script_dir%\..\requirements.txt" ) :: Deactivate Python Virtual Environment -@call %venv_path%\Scripts\deactivate.bat +@if defined venv_path ( + call %venv_path%\Scripts\deactivate.bat +) @pause From f3620d99a20f4890ee22cfd2a629c438a119dffb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 28 May 2025 23:56:46 -0500 Subject: [PATCH 636/762] Minor fixes --- CMakePresets.json | 2 +- tools/lib/setup-common.bat | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 14595a91..7d3ddccc 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -264,4 +264,4 @@ "configuration": "Release" } ] -} \ No newline at end of file +} diff --git a/tools/lib/setup-common.bat b/tools/lib/setup-common.bat index 42b3dc16..9ba510c2 100644 --- a/tools/lib/setup-common.bat +++ b/tools/lib/setup-common.bat @@ -34,4 +34,6 @@ pip install --upgrade -r "%script_dir%\..\..\requirements.txt" @call %script_dir%\run-cmake-configure.bat :: Deactivate Python Virtual Environment -@call %venv_path%\Scripts\deactivate.bat +@if defined venv_path ( + call %venv_path%\Scripts\deactivate.bat +) From d12fe2b7a5d413fa76b11a71854bf68e10bba9f0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 30 May 2025 00:20:45 -0500 Subject: [PATCH 637/762] CMakePresets formatting --- CMakePresets.json | 522 +++++++++++++++++++++++----------------------- 1 file changed, 261 insertions(+), 261 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 7d3ddccc..575be130 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,267 +1,267 @@ { - "version": 5, - "cmakeMinimumRequired": { - "major": 3, - "minor": 24, - "patch": 0 + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake", + "SCWX_VIRTUAL_ENV": "${sourceDir}/.venv" + } }, - "configurePresets": [ - { - "name": "base", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/${presetName}", - "cacheVariables": { - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "${sourceDir}/external/cmake-conan/conan_provider.cmake", - "SCWX_VIRTUAL_ENV": "${sourceDir}/.venv" - } - }, - { - "name": "windows-base", - "inherits": "base", - "hidden": true, - "generator": "Visual Studio 17 2022", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "vendor": { - "microsoft.com/VisualStudioSettings/CMake/1.0": { - "hostOS": [ - "Windows" - ] - } - } - }, - { - "name": "windows-x64-base", - "inherits": "windows-base", - "hidden": true - }, - { - "name": "linux-base", - "inherits": "base", - "hidden": true, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - } - }, - { - "name": "windows-msvc2022-x64-base", - "inherits": "windows-x64-base", - "hidden": true, - "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", - "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", - "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" - } - }, - { - "name": "windows-msvc2022-x64-ninja-base", - "inherits": "windows-msvc2022-x64-base", - "hidden": true, - "generator": "Ninja", - "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", - "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", - "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" - } - }, - { - "name": "linux-gcc-base", - "inherits": "linux-base", - "hidden": true, - "cacheVariables": { - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64", - "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", - "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11" - }, - "environment": { - "CC": "gcc-11", - "CXX": "g++-11" - } - }, - { - "name": "windows-msvc2022-x64-debug", - "inherits": "windows-msvc2022-x64-base", - "displayName": "Windows MSVC 2022 x64 Debug", - "architecture": { - "value": "x64", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "windows-msvc2022-x64-release", - "inherits": "windows-msvc2022-x64-base", - "displayName": "Windows MSVC 2022 x64 Release", - "architecture": { - "value": "x64", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "windows-msvc2022-x64-ninja-debug", - "inherits": "windows-msvc2022-x64-ninja-base", - "displayName": "Windows MSVC 2022 x64 Ninja Debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "windows-msvc2022-x64-ninja-release", - "inherits": "windows-msvc2022-x64-ninja-base", - "displayName": "Windows MSVC 2022 x64 Ninja Release", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "linux-gcc-debug", - "inherits": "linux-gcc-base", - "displayName": "Linux GCC Debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx" - } - }, - { - "name": "linux-gcc-release", - "inherits": "linux-gcc-base", - "displayName": "Linux GCC Release", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx" - } - }, - { - "name": "linux-gcc-debug-asan", - "inherits": "linux-gcc-base", - "displayName": "Linux GCC Debug Address Sanitizer", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx", - "SCWX_ADDRESS_SANITIZER": { - "type": "BOOL", - "value": "ON" - } - } - }, - { - "name": "linux-gcc-release-asan", - "inherits": "linux-gcc-base", - "displayName": "Linux GCC Release Address Sanitizer", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx", - "SCWX_ADDRESS_SANITIZER": { - "type": "BOOL", - "value": "ON" - } - } - }, - { - "name": "ci-linux-gcc14", - "inherits": "linux-gcc-base", - "displayName": "CI Linux GCC 14", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CONAN_HOST_PROFILE": "scwx-linux_gcc-14", - "CONAN_BUILD_PROFILE": "scwx-linux_gcc-14" - }, - "environment": { - "CC": "gcc-14", - "CXX": "g++-14" - } - }, - { - "name": "ci-linux-clang17", - "inherits": "linux-gcc-base", - "displayName": "CI Linux Clang 17", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CONAN_HOST_PROFILE": "scwx-linux_clang-17", - "CONAN_BUILD_PROFILE": "scwx-linux_clang-17" - }, - "environment": { - "CC": "clang-17", - "CXX": "clang++-17" - } - }, - { - "name": "ci-linux-gcc-arm64", - "inherits": "linux-gcc-base", - "displayName": "CI Linux GCC ARM64", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8", - "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8", - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64" - }, - "environment": { - "CC": "gcc-11", - "CXX": "g++-11" - } + { + "name": "windows-base", + "inherits": "base", + "hidden": true, + "generator": "Visual Studio 17 2022", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "vendor": { + "microsoft.com/VisualStudioSettings/CMake/1.0": { + "hostOS": [ + "Windows" + ] } - ], - "buildPresets": [ - { - "name": "windows-msvc2022-x64-debug", - "configurePreset": "windows-msvc2022-x64-debug", - "displayName": "Windows MSVC 2022 x64 Debug", - "configuration": "Debug" - }, - { - "name": "windows-msvc2022-x64-release", - "configurePreset": "windows-msvc2022-x64-release", - "displayName": "Windows MSVC 2022 x64 Release", - "configuration": "Release" - }, - { - "name": "linux-gcc-debug", - "configurePreset": "linux-gcc-debug", - "displayName": "Linux GCC Debug", - "configuration": "Debug" - }, - { - "name": "linux-gcc-release", - "configurePreset": "linux-gcc-release", - "displayName": "Linux GCC Release", - "configuration": "Release" + } + }, + { + "name": "windows-x64-base", + "inherits": "windows-base", + "hidden": true + }, + { + "name": "linux-base", + "inherits": "base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "windows-msvc2022-x64-base", + "inherits": "windows-x64-base", + "hidden": true, + "cacheVariables": { + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + } + }, + { + "name": "windows-msvc2022-x64-ninja-base", + "inherits": "windows-msvc2022-x64-base", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + } + }, + { + "name": "linux-gcc-base", + "inherits": "linux-base", + "hidden": true, + "cacheVariables": { + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11" + }, + "environment": { + "CC": "gcc-11", + "CXX": "g++-11" + } + }, + { + "name": "windows-msvc2022-x64-debug", + "inherits": "windows-msvc2022-x64-base", + "displayName": "Windows MSVC 2022 x64 Debug", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-msvc2022-x64-release", + "inherits": "windows-msvc2022-x64-base", + "displayName": "Windows MSVC 2022 x64 Release", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-msvc2022-x64-ninja-debug", + "inherits": "windows-msvc2022-x64-ninja-base", + "displayName": "Windows MSVC 2022 x64 Ninja Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-msvc2022-x64-ninja-release", + "inherits": "windows-msvc2022-x64-ninja-base", + "displayName": "Windows MSVC 2022 x64 Ninja Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-gcc-debug", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx" + } + }, + { + "name": "linux-gcc-release", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx" + } + }, + { + "name": "linux-gcc-debug-asan", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Debug Address Sanitizer", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx", + "SCWX_ADDRESS_SANITIZER": { + "type": "BOOL", + "value": "ON" } - ], - "testPresets": [ - { - "name": "windows-msvc2022-x64-debug", - "configurePreset": "windows-msvc2022-x64-debug", - "displayName": "Windows MSVC 2022 x64 Debug", - "configuration": "Debug" - }, - { - "name": "windows-msvc2022-x64-release", - "configurePreset": "windows-msvc2022-x64-release", - "displayName": "Windows MSVC 2022 x64 Release", - "configuration": "Release" - }, - { - "name": "linux-gcc-debug", - "configurePreset": "linux-gcc-debug", - "displayName": "Linux GCC Debug", - "configuration": "Debug" - }, - { - "name": "linux-gcc-release", - "configurePreset": "linux-gcc-release", - "displayName": "Linux GCC Release", - "configuration": "Release" + } + }, + { + "name": "linux-gcc-release-asan", + "inherits": "linux-gcc-base", + "displayName": "Linux GCC Release Address Sanitizer", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx", + "SCWX_ADDRESS_SANITIZER": { + "type": "BOOL", + "value": "ON" } - ] + } + }, + { + "name": "ci-linux-gcc14", + "inherits": "linux-gcc-base", + "displayName": "CI Linux GCC 14", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-14", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-14" + }, + "environment": { + "CC": "gcc-14", + "CXX": "g++-14" + } + }, + { + "name": "ci-linux-clang17", + "inherits": "linux-gcc-base", + "displayName": "CI Linux Clang 17", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_clang-17", + "CONAN_BUILD_PROFILE": "scwx-linux_clang-17" + }, + "environment": { + "CC": "clang-17", + "CXX": "clang++-17" + } + }, + { + "name": "ci-linux-gcc-arm64", + "inherits": "linux-gcc-base", + "displayName": "CI Linux GCC ARM64", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8", + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64" + }, + "environment": { + "CC": "gcc-11", + "CXX": "g++-11" + } + } + ], + "buildPresets": [ + { + "name": "windows-msvc2022-x64-debug", + "configurePreset": "windows-msvc2022-x64-debug", + "displayName": "Windows MSVC 2022 x64 Debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc2022-x64-release", + "configurePreset": "windows-msvc2022-x64-release", + "displayName": "Windows MSVC 2022 x64 Release", + "configuration": "Release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "displayName": "Linux GCC Debug", + "configuration": "Debug" + }, + { + "name": "linux-gcc-release", + "configurePreset": "linux-gcc-release", + "displayName": "Linux GCC Release", + "configuration": "Release" + } + ], + "testPresets": [ + { + "name": "windows-msvc2022-x64-debug", + "configurePreset": "windows-msvc2022-x64-debug", + "displayName": "Windows MSVC 2022 x64 Debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc2022-x64-release", + "configurePreset": "windows-msvc2022-x64-release", + "displayName": "Windows MSVC 2022 x64 Release", + "configuration": "Release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "displayName": "Linux GCC Debug", + "configuration": "Debug" + }, + { + "name": "linux-gcc-release", + "configurePreset": "linux-gcc-release", + "displayName": "Linux GCC Release", + "configuration": "Release" + } + ] } From ad65bcf42445f95f83c33c03ffa1fa4910ed2fbd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 30 May 2025 07:44:51 -0500 Subject: [PATCH 638/762] abseil override is no longer needed, fixed in conan-center-index https://github.com/conan-io/conan-center-index/issues/27564 --- conanfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 4307a91a..d521bfb0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -37,8 +37,6 @@ class SupercellWxConan(ConanFile): self.options["libcurl"].ca_path = "none" def requirements(self): - self.requires("abseil/20250127.0", override=True) - if self.settings.os == "Linux": self.requires("onetbb/2022.0.0") From 83c5d15cdf808118c77fa2b6b2a9bfc77f353b37 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 31 May 2025 09:09:19 -0400 Subject: [PATCH 639/762] Working flatpak build --- .github/workflows/ci.yml | 38 ++++++++++++++++++- scwx-qt/res/linux/net.supercellwx.app.desktop | 7 ++++ tools/net.supercellwx.app.yml | 37 ++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/res/linux/net.supercellwx.app.desktop create mode 100644 tools/net.supercellwx.app.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad80a89f..1d9cbd1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,8 @@ jobs: wayland-protocols \ libwayland-dev \ libwayland-egl-backend-dev \ + flatpak \ + flatpak-builder \ ${{ matrix.compiler_packages }} - name: Setup Python Environment @@ -261,7 +263,8 @@ jobs: - name: Build AppImage (Linux) if: ${{ startsWith(matrix.os, 'ubuntu') }} env: - APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx/ + INSTALL_DIR: ${{ github.workspace }}/supercell-wx/ + APPIMAGE_DIR: ${{ github.workspace }}/supercell-wx-appimage/ LDAI_UPDATE_INFORMATION: gh-releases-zsync|dpaulat|supercell-wx|latest|*${{ matrix.appimage_arch }}.AppImage.zsync LDAI_OUTPUT: supercell-wx-${{ env.SCWX_VERSION }}-${{ matrix.appimage_arch }}.AppImage LINUXDEPLOY_OUTPUT_APP_NAME: supercell-wx @@ -272,6 +275,7 @@ jobs: chmod +x linuxdeploy-${{ matrix.appimage_arch }}.AppImage cp "${{ github.workspace }}/source/scwx-qt/res/icons/scwx-256.png" supercell-wx.png cp "${{ github.workspace }}/source/scwx-qt/res/linux/supercell-wx.desktop" . + cp -r "${{ env.INSTALL_DIR }}" "${{ env.APPIMAGE_DIR }}" pushd "${{ env.APPIMAGE_DIR }}" mkdir -p usr/ mv bin/ usr/ @@ -289,6 +293,38 @@ jobs: name: supercell-wx-appimage-${{ matrix.artifact_suffix }} path: ${{ github.workspace }}/*-${{ matrix.appimage_arch }}.AppImage* + - name: Build FlatPak (Linux) + if: ${{ startsWith(matrix.os, 'ubuntu') }} + env: + INSTALL_DIR: ${{ github.workspace }}/supercell-wx/ + FLATPAK_DIR: ${{ github.workspace }}/supercell-wx-flatpak/ + shell: bash + run: | + cp -r ${{ env.INSTALL_DIR }} ${{ env.FLATPAK_DIR }} + # Copy krb5 libraries to flatpak + cp /usr/lib/*/libkrb5.so* \ + /usr/lib/*/libkrb5support.so* \ + /usr/lib/*/libgssapi_krb5.so* \ + /usr/lib/*/libk5crypto.so* \ + /usr/lib/*/libkeyutils.so* \ + ${{ env.FLATPAK_DIR }}/lib + + flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak-builder --force-clean \ + --user \ + --install-deps-from=flathub \ + --repo=flatpak-repo \ + --install flatpak-build \ + ${{ github.workspace }}/source/tools/net.supercellwx.app.yml + flatpak build-bundle flatpak-repo supercell-wx.flatpak net.supercellwx.app + + - name: Upload FlatPak (Linux) + if: ${{ startsWith(matrix.os, 'ubuntu') }} + uses: actions/upload-artifact@v4 + with: + name: supercell-wx-flatpak-${{ matrix.artifact_suffix }} + path: ${{ github.workspace }}/supercell-wx.flatpak + - name: Test Supercell Wx working-directory: ${{ github.workspace }}/build env: diff --git a/scwx-qt/res/linux/net.supercellwx.app.desktop b/scwx-qt/res/linux/net.supercellwx.app.desktop new file mode 100644 index 00000000..8e671522 --- /dev/null +++ b/scwx-qt/res/linux/net.supercellwx.app.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Supercell Wx +Comment=Weather Radar and Data Viewer +Exec=supercell-wx +Icon=net.supercellwx.app.png +Categories=Network;Science; diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml new file mode 100644 index 00000000..9c4c3f72 --- /dev/null +++ b/tools/net.supercellwx.app.yml @@ -0,0 +1,37 @@ +id: net.supercellwx.app +version: '0.4.9' +runtime: "org.freedesktop.Platform" +runtime-version: "23.08" +sdk: "org.freedesktop.Sdk" +command: supercell-wx +modules: + - name: supercell-wx + buildsystem: simple + build-commands: + - install -Dm644 net.supercellwx.app.desktop /app/share/applications/${FLATPAK_ID}.desktop + - install -Dm644 scwx-256.png /app/share/icons/hicolor/256x256/apps/net.supercellwx.app.png + - install -Dm644 scwx-64.png /app/share/icons/hicolor/64x64/apps/net.supercellwx.app.png + - rm net.supercellwx.app.desktop scwx-256.png scwx-64.png + - cp -r * /app/ + sources: + - type: dir + path: ../../supercell-wx-flatpak + - type: file + path: ../scwx-qt/res/linux/net.supercellwx.app.desktop + - type: file + path: ../scwx-qt/res/icons/scwx-256.png + - type: file + path: ../scwx-qt/res/icons/scwx-64.png + +finish-args: + # X11 + XShm access + - --share=ipc + - --socket=fallback-x11 + # Wayland access + - --socket=wayland + # GPU acceleration if needed + - --device=dri + # Needs to talk to the network: + - --share=network + # Needs to save files locally + - --filesystem=xdg-documents From ed1bb7422d21469e059d2ff30bb2fff511de3770 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:14:13 +0000 Subject: [PATCH 640/762] Update dependency openssl to v3.5.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 4307a91a..bcf6b365 100644 --- a/conanfile.py +++ b/conanfile.py @@ -17,7 +17,7 @@ class SupercellWxConan(ConanFile): "libcurl/8.12.1", "libpng/1.6.48", "libxml2/2.13.8", - "openssl/3.4.1", + "openssl/3.5.0", "range-v3/0.12.0", "re2/20240702", "spdlog/1.15.1", From c36144bab34398e0a318cfd9c23587d45695a095 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 21:57:16 -0500 Subject: [PATCH 641/762] Linux setup scripts should use quotes around absolute paths to support spaces in pathname --- tools/lib/setup-common.sh | 12 ++++++------ tools/setup-debug.sh | 2 +- tools/setup-multi.sh | 2 +- tools/setup-release.sh | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh index a19d2fab..76c5230e 100755 --- a/tools/lib/setup-common.sh +++ b/tools/lib/setup-common.sh @@ -2,7 +2,7 @@ script_dir="$(dirname "$(readlink -f "$0")")" # Import common paths -source ${script_dir}/common-paths.sh +source "${script_dir}/common-paths.sh" # Activate Python Virtual Environment if [ -n "${venv_path:-}" ]; then @@ -23,26 +23,26 @@ fi # Install Python packages python -m pip install ${PIP_FLAGS} pip -pip install ${PIP_FLAGS} -r ${script_dir}/../../requirements.txt +pip install ${PIP_FLAGS} -r "${script_dir}/../../requirements.txt" if [[ -n "${build_type}" ]]; then # Install Conan profile and packages - ${script_dir}/setup-conan.sh + "${script_dir}/setup-conan.sh" else # Install Conan profile and debug packages export build_type=Debug - ${script_dir}/setup-conan.sh + "${script_dir}/setup-conan.sh" # Install Conan profile and release packages export build_type=Release - ${script_dir}/setup-conan.sh + "${script_dir}/setup-conan.sh" # Unset build_type unset build_type fi # Run CMake Configure -${script_dir}/run-cmake-configure.sh +"${script_dir}/run-cmake-configure.sh" # Deactivate Python Virtual Environment if [ -n "${venv_path:-}" ]; then diff --git a/tools/setup-debug.sh b/tools/setup-debug.sh index 6c17caba..ebfba62c 100755 --- a/tools/setup-debug.sh +++ b/tools/setup-debug.sh @@ -12,4 +12,4 @@ export qt_arch=gcc_64 [ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" # Perform common setup -${script_dir}/lib/setup-common.sh +"${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-multi.sh b/tools/setup-multi.sh index f18bd5ac..05a6cad5 100755 --- a/tools/setup-multi.sh +++ b/tools/setup-multi.sh @@ -15,4 +15,4 @@ echo "Ninja Multi-Config is not supported in Linux" read -p "Press Enter to continue..." # Perform common setup -# ${script_dir}/lib/setup-common.sh +# "${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-release.sh b/tools/setup-release.sh index c55e75c7..04dbc8e6 100755 --- a/tools/setup-release.sh +++ b/tools/setup-release.sh @@ -12,4 +12,4 @@ export qt_arch=gcc_64 [ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" # Perform common setup -${script_dir}/lib/setup-common.sh +"${script_dir}/lib/setup-common.sh" From c9d1e4cd082d6c0c7c061e6d274d9538b7e1235a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 22:11:08 -0500 Subject: [PATCH 642/762] Add user-setup script overrides for Linux --- .gitignore | 3 +++ tools/lib/setup-common.sh | 5 +++++ tools/lib/user-setup.example.sh | 10 ++++++++++ 3 files changed, 18 insertions(+) create mode 100644 tools/lib/user-setup.example.sh diff --git a/.gitignore b/.gitignore index 60da0eda..cb46cec7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ _deps # Python Virtual Environment .venv/ + +# Specific excludes for Supercell Wx +tools/lib/user-setup.sh diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh index 76c5230e..e28978e9 100755 --- a/tools/lib/setup-common.sh +++ b/tools/lib/setup-common.sh @@ -4,6 +4,11 @@ script_dir="$(dirname "$(readlink -f "$0")")" # Import common paths source "${script_dir}/common-paths.sh" +# Load custom build settings +if [ -f "${script_dir}/user-setup.sh" ]; then + source "${script_dir}/user-setup.sh" +fi + # Activate Python Virtual Environment if [ -n "${venv_path:-}" ]; then python -m venv "${venv_path}" diff --git a/tools/lib/user-setup.example.sh b/tools/lib/user-setup.example.sh new file mode 100644 index 00000000..5ea76d24 --- /dev/null +++ b/tools/lib/user-setup.example.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Example user setup script. Copy as user-setup.sh and modify as required. + +# gcc-13 is not the default gcc version +export CC=/usr/bin/gcc-13 +export CXX=/usr/bin/c++-13 + +# Override conan profile to be gcc-13 +export conan_profile=scwx-linux_gcc-13 From 0dcf8eababb710a3438afd4d066a86b0fc204fb3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 22:11:39 -0500 Subject: [PATCH 643/762] setup-release.sh should create a build-release directory, not a build-debug directory --- tools/setup-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/setup-release.sh b/tools/setup-release.sh index 04dbc8e6..c89fdfe5 100755 --- a/tools/setup-release.sh +++ b/tools/setup-release.sh @@ -1,7 +1,7 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir="$(readlink -f "${1:-${script_dir}/../build-debug}")" +export build_dir="$(readlink -f "${1:-${script_dir}/../build-release}")" export build_type=Release export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja From 9ec1a624037baad58bb707020a3150f8b8f29b62 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 22:12:36 -0500 Subject: [PATCH 644/762] Ensure CMake compile commands file gets generated --- tools/lib/run-cmake-configure.bat | 3 ++- tools/lib/run-cmake-configure.sh | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/lib/run-cmake-configure.bat b/tools/lib/run-cmake-configure.bat index 785d5d8d..424bcc34 100644 --- a/tools/lib/run-cmake-configure.bat +++ b/tools/lib/run-cmake-configure.bat @@ -6,7 +6,8 @@ -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="%script_dir%\..\..\external\cmake-conan\conan_provider.cmake" ^ -DCONAN_HOST_PROFILE=%conan_profile% ^ -DCONAN_BUILD_PROFILE=%conan_profile% ^ - -DSCWX_VIRTUAL_ENV=%venv_path% + -DSCWX_VIRTUAL_ENV=%venv_path% ^ + -DCMAKE_EXPORT_COMPILE_COMMANDS=on @if defined build_type ( set cmake_args=%cmake_args% ^ diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh index 461d54d8..ffe9e7f8 100755 --- a/tools/lib/run-cmake-configure.sh +++ b/tools/lib/run-cmake-configure.sh @@ -10,6 +10,7 @@ cmake_args=( -DCONAN_HOST_PROFILE="${conan_profile}" -DCONAN_BUILD_PROFILE="${conan_profile}" -DSCWX_VIRTUAL_ENV="${venv_path}" + -DCMAKE_EXPORT_COMPILE_COMMANDS=on ) if [[ -n "${build_type}" ]]; then From 0b3f2c49dbec8745fd939a34689dcc55fcd37fb2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 22:15:14 -0500 Subject: [PATCH 645/762] Add user-setup.sh to configure-environment.sh --- tools/configure-environment.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh index 0328db30..0da64217 100755 --- a/tools/configure-environment.sh +++ b/tools/configure-environment.sh @@ -4,6 +4,11 @@ script_dir="$(dirname "$(readlink -f "$0")")" # Assign user-specified Python Virtual Environment [ "${1:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${1:-${script_dir}/../.venv}")" +# Load custom build settings +if [ -f "${script_dir}/lib/user-setup.sh" ]; then + source "${script_dir}/lib/user-setup.sh" +fi + # Activate Python Virtual Environment if [ -n "${venv_path:-}" ]; then python -m venv "${venv_path}" From e76260dffa516c6173d829e3e54210e4a44e6f62 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 6 Jun 2025 22:37:59 -0500 Subject: [PATCH 646/762] Add setup script argument to toggle address sanitizer --- tools/lib/run-cmake-configure.sh | 13 ++++++++++++- tools/setup-debug.sh | 1 + tools/setup-release.sh | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh index ffe9e7f8..5e1a3bbf 100755 --- a/tools/lib/run-cmake-configure.sh +++ b/tools/lib/run-cmake-configure.sh @@ -10,7 +10,7 @@ cmake_args=( -DCONAN_HOST_PROFILE="${conan_profile}" -DCONAN_BUILD_PROFILE="${conan_profile}" -DSCWX_VIRTUAL_ENV="${venv_path}" - -DCMAKE_EXPORT_COMPILE_COMMANDS=on + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ) if [[ -n "${build_type}" ]]; then @@ -27,5 +27,16 @@ else ) fi +# Toggle address sanitizer based on argument +if [ "${address_sanitizer}" != "disabled" ]; then + cmake_args+=( + -DSCWX_ADDRESS_SANITIZER=ON + ) +else + cmake_args+=( + -DSCWX_ADDRESS_SANITIZER=OFF + ) +fi + mkdir -p "${build_dir}" cmake "${cmake_args[@]}" diff --git a/tools/setup-debug.sh b/tools/setup-debug.sh index ebfba62c..9b896d40 100755 --- a/tools/setup-debug.sh +++ b/tools/setup-debug.sh @@ -7,6 +7,7 @@ export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja export qt_base=/opt/Qt export qt_arch=gcc_64 +export address_sanitizer=${4:-disabled} # Assign user-specified Python Virtual Environment [ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" diff --git a/tools/setup-release.sh b/tools/setup-release.sh index c89fdfe5..e7e10db0 100755 --- a/tools/setup-release.sh +++ b/tools/setup-release.sh @@ -7,6 +7,7 @@ export conan_profile=${2:-scwx-linux_gcc-11} export generator=Ninja export qt_base=/opt/Qt export qt_arch=gcc_64 +export address_sanitizer=${4:-disabled} # Assign user-specified Python Virtual Environment [ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" From 34ec35caf0e7d8b984312d3956db4390243a095b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 31 May 2025 00:24:17 -0500 Subject: [PATCH 647/762] Update color table immediately on palette change --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 100 ++++++++++++---------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 78362a28..e219bfb6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -135,6 +135,9 @@ public: ~MapWidgetImpl() { + // Disconnect signals + colorPaletteConnection_.disconnect(); + DeinitializeCustomStyles(); // Set ImGui Context @@ -182,6 +185,7 @@ public: std::optional type); void SetRadarSite(const std::string& radarSite, bool checkProductAvailability = false); + void UpdateColorTable(const std::string& colorPalette); void UpdateLoadedStyle(); bool UpdateStoredMapParameters(); void CheckLevel3Availability(); @@ -214,6 +218,8 @@ public: boost::uuids::uuid customStyleUrlChangedCallbackId_ {}; boost::uuids::uuid customStyleDrawBelowChangedCallbackId_ {}; + boost::signals2::scoped_connection colorPaletteConnection_ {}; + ImGuiContext* imGuiContext_; std::string imGuiContextName_; bool imGuiRendererInitialized_; @@ -901,6 +907,13 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, (group == common::RadarProductGroup::Level2) ? common::GetLevel2Palette(common::GetLevel2Product(productName)) : common::GetLevel3Palette(productCode); + + auto& paletteSetting = + settings::PaletteSettings::Instance().palette(palette); + + p->colorPaletteConnection_ = paletteSetting.changed_signal().connect( + [this, palette]() { p->UpdateColorTable(palette); }); + p->InitializeNewRadarProductView(palette); } else if (update) @@ -1953,49 +1966,19 @@ void MapWidgetImpl::RadarProductManagerDisconnect() void MapWidgetImpl::InitializeNewRadarProductView( const std::string& colorPalette) { - boost::asio::post( - threadPool_, - [colorPalette, this]() - { - try - { - auto radarProductView = context_->radar_product_view(); - - auto& paletteSetting = - settings::PaletteSettings::Instance().palette(colorPalette); - - std::string colorTableFile = paletteSetting.GetValue(); - if (colorTableFile.empty()) - { - colorTableFile = paletteSetting.GetDefault(); - } - - std::unique_ptr colorTableStream = - util::OpenFile(colorTableFile); - if (colorTableStream->fail()) - { - logger_->warn("Could not open color table {}", colorTableFile); - colorTableStream = util::OpenFile(paletteSetting.GetDefault()); - } - - std::shared_ptr colorTable = - common::ColorTable::Load(*colorTableStream); - if (!colorTable->IsValid()) - { - logger_->warn("Could not load color table {}", colorTableFile); - colorTableStream = util::OpenFile(paletteSetting.GetDefault()); - colorTable = common::ColorTable::Load(*colorTableStream); - } - - radarProductView->LoadColorTable(colorTable); - - radarProductView->Initialize(); - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } - }); + boost::asio::post(threadPool_, + [colorPalette, this]() + { + try + { + UpdateColorTable(colorPalette); + context_->radar_product_view()->Initialize(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); if (map_ != nullptr) { @@ -2116,6 +2099,37 @@ void MapWidgetImpl::Update() } } +void MapWidgetImpl::UpdateColorTable(const std::string& colorPalette) +{ + auto& paletteSetting = + settings::PaletteSettings::Instance().palette(colorPalette); + + std::string colorTableFile = paletteSetting.GetValue(); + if (colorTableFile.empty()) + { + colorTableFile = paletteSetting.GetDefault(); + } + + std::unique_ptr colorTableStream = + util::OpenFile(colorTableFile); + if (colorTableStream->fail()) + { + logger_->warn("Could not open color table {}", colorTableFile); + colorTableStream = util::OpenFile(paletteSetting.GetDefault()); + } + + std::shared_ptr colorTable = + common::ColorTable::Load(*colorTableStream); + if (!colorTable->IsValid()) + { + logger_->warn("Could not load color table {}", colorTableFile); + colorTableStream = util::OpenFile(paletteSetting.GetDefault()); + colorTable = common::ColorTable::Load(*colorTableStream); + } + + context_->radar_product_view()->LoadColorTable(colorTable); +} + bool MapWidgetImpl::UpdateStoredMapParameters() { bool changed = false; From 0ff0f6ea17b43e788ce61ec820861b8c946a2347 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 7 Jun 2025 12:36:26 -0400 Subject: [PATCH 648/762] Add serial port permissions to flatpak --- tools/net.supercellwx.app.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml index 9c4c3f72..a31fa255 100644 --- a/tools/net.supercellwx.app.yml +++ b/tools/net.supercellwx.app.yml @@ -31,6 +31,8 @@ finish-args: - --socket=wayland # GPU acceleration if needed - --device=dri + # Needs access to serial port devices + - --device=serial # Needs to talk to the network: - --share=network # Needs to save files locally From 28ca8df5df4ad9e6f5e0ce5108458afa2bbc24c7 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 7 Jun 2025 12:48:33 -0400 Subject: [PATCH 649/762] use a random pid instead of 2 to avoid overlap in flatpaks --- scwx-qt/source/scwx/qt/manager/log_manager.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/log_manager.cpp b/scwx-qt/source/scwx/qt/manager/log_manager.cpp index 7ab18e56..34facaa4 100644 --- a/scwx-qt/source/scwx/qt/manager/log_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/log_manager.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include #include #include #include @@ -57,6 +59,14 @@ void LogManager::InitializeLogFile() QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) .toStdString(); p->pid_ = boost::this_process::get_id(); + if (p->pid_ == 2) + { + // The pid == 2 means that this is likely a flatpak. We assign a random + // number in this case to avoid overlap, scince it is always 2 in a + // flatpak + std::srand(std::time({})); + p->pid_ = std::rand(); + } p->logFile_ = fmt::format("{}/supercell-wx.{}.log", p->logPath_, p->pid_); // Create log directory if it doesn't exist From 52675721b4541751cf58aca443461cfe69986f52 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 7 Jun 2025 13:19:43 -0400 Subject: [PATCH 650/762] Flatpak does not have a specific one for serial, just for all --- tools/net.supercellwx.app.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml index a31fa255..ced79027 100644 --- a/tools/net.supercellwx.app.yml +++ b/tools/net.supercellwx.app.yml @@ -30,9 +30,7 @@ finish-args: # Wayland access - --socket=wayland # GPU acceleration if needed - - --device=dri - # Needs access to serial port devices - - --device=serial + - --device=all # Needs to talk to the network: - --share=network # Needs to save files locally From 36b2e77ecfef33a2a4f5d5f8fe9f3da4a287a031 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 09:46:17 -0400 Subject: [PATCH 651/762] Add hotkeys for switching between products in a category --- .clang-tidy | 1 + .../scwx/qt/settings/hotkey_settings.cpp | 6 ++ scwx-qt/source/scwx/qt/types/hotkey_types.cpp | 4 ++ scwx-qt/source/scwx/qt/types/hotkey_types.hpp | 2 + .../scwx/qt/ui/level3_products_widget.cpp | 68 ++++++++++++++++++- test/data | 2 +- 6 files changed, 80 insertions(+), 3 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 645c9c05..26230c78 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -8,6 +8,7 @@ Checks: - 'performance-*' - '-bugprone-easily-swappable-parameters' - '-cppcoreguidelines-pro-type-reinterpret-cast' + - '-cppcoreguidelines-avoid-do-while' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-misc-use-anonymous-namespace' diff --git a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp index ba5b4e3e..39f21379 100644 --- a/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/hotkey_settings.cpp @@ -25,6 +25,12 @@ static const std::unordered_map kDefaultHotkeys_ { {types::Hotkey::MapRotateCounterclockwise, QKeySequence {Qt::Key::Key_Q}}, {types::Hotkey::MapZoomIn, QKeySequence {Qt::Key::Key_Equal}}, {types::Hotkey::MapZoomOut, QKeySequence {Qt::Key::Key_Minus}}, + {types::Hotkey::ProductCategoryNext, + QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier, + Qt::Key::Key_BracketRight}}}, + {types::Hotkey::ProductCategoryPrevious, + QKeySequence {QKeyCombination {Qt::KeyboardModifier::ControlModifier, + Qt::Key::Key_BracketLeft}}}, {types::Hotkey::ProductTiltDecrease, QKeySequence {Qt::Key::Key_BracketLeft}}, {types::Hotkey::ProductTiltIncrease, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp index 72c77f63..7a121079 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.cpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.cpp @@ -25,6 +25,8 @@ static const std::unordered_map hotkeyShortName_ { {Hotkey::MapRotateCounterclockwise, "map_rotate_counterclockwise"}, {Hotkey::MapZoomIn, "map_zoom_in"}, {Hotkey::MapZoomOut, "map_zoom_out"}, + {Hotkey::ProductCategoryNext, "product_category_next"}, + {Hotkey::ProductCategoryPrevious, "product_category_last"}, {Hotkey::ProductTiltDecrease, "product_tilt_decrease"}, {Hotkey::ProductTiltIncrease, "product_tilt_increase"}, {Hotkey::SelectLevel2Ref, "select_l2_ref"}, @@ -65,6 +67,8 @@ static const std::unordered_map hotkeyLongName_ { {Hotkey::MapRotateCounterclockwise, "Map Rotate Counterclockwise"}, {Hotkey::MapZoomIn, "Map Zoom In"}, {Hotkey::MapZoomOut, "Map Zoom Out"}, + {Hotkey::ProductCategoryNext, "Next Product in Category"}, + {Hotkey::ProductCategoryPrevious, "Previous Product in Category"}, {Hotkey::ProductTiltDecrease, "Product Tilt Decrease"}, {Hotkey::ProductTiltIncrease, "Product Tilt Increase"}, {Hotkey::SelectLevel2Ref, "Select L2 REF"}, diff --git a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp index 2107a009..6e770b67 100644 --- a/scwx-qt/source/scwx/qt/types/hotkey_types.hpp +++ b/scwx-qt/source/scwx/qt/types/hotkey_types.hpp @@ -25,6 +25,8 @@ enum class Hotkey MapRotateCounterclockwise, MapZoomIn, MapZoomOut, + ProductCategoryPrevious, + ProductCategoryNext, ProductTiltDecrease, ProductTiltIncrease, SelectLevel2Ref, diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index e6de32b2..5c993b66 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -216,7 +216,9 @@ void Level3ProductsWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, if (productCategoryIt == kHotkeyProductCategoryMap_.cend() && hotkey != types::Hotkey::ProductTiltDecrease && - hotkey != types::Hotkey::ProductTiltIncrease) + hotkey != types::Hotkey::ProductTiltIncrease && + hotkey != types::Hotkey::ProductCategoryNext && + hotkey != types::Hotkey::ProductCategoryPrevious) { // Not handling this hotkey return; @@ -251,7 +253,69 @@ void Level3ProductsWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, return; } - std::shared_lock lock {awipsProductMutex_}; + if (hotkey == types::Hotkey::ProductCategoryNext || + hotkey == types::Hotkey::ProductCategoryPrevious) + { + const std::shared_lock lock1 {categoryMapMutex_}; + const std::shared_lock lock2 {awipsProductMutex_}; + + const common::Level3ProductCategory category = + common::GetLevel3CategoryByProduct(product); + auto productsIt = categoryMap_.find(category); + if (productsIt == categoryMap_.cend()) + { + logger_->error("Could not find the current category in category map"); + return; + } + auto availableProducts = productsIt->second; + const auto& products = common::GetLevel3ProductsByCategory(category); + + auto productIt = std::find(products.begin(), products.end(), product); + if (productIt == products.end()) + { + logger_->error("Could not find product in category"); + return; + } + + if (hotkey == types::Hotkey::ProductCategoryNext) + { + do + { + productIt = std::next(productIt); + if (productIt == products.cend()) + { + logger_->info("Cannot go past the last product"); + return; + } + } while (!availableProducts.contains(*productIt)); + } + else + { + do + { + if (productIt == products.begin()) + { + logger_->info("Cannot go past the first product"); + return; + } + productIt = std::prev(productIt); + } while (!availableProducts.contains(*productIt)); + } + + auto productTiltsIt = productTiltMap_.find(*productIt); + if (productTiltsIt == productTiltMap_.cend()) + { + logger_->error("Could not find product tilt map: {}", + common::GetLevel3ProductDescription(product)); + return; + } + + // Select the new tilt + productTiltsIt->second.at(0)->trigger(); + return; + } + + const std::shared_lock lock {awipsProductMutex_}; // Find the current product tilt auto productTiltsIt = productTiltMap_.find(product); diff --git a/test/data b/test/data index 6115c159..c68bee74 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 6115c15987fd75dd019db995e6bdc07a05b83dcc +Subproject commit c68bee74549963e9a02e0fa998efad0f10f8256b From 26e24da4b55f1cf2f797f0be9e13247595d619f6 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 15:40:44 -0400 Subject: [PATCH 652/762] Make animation dock follow default timezones --- scwx-qt/source/scwx/qt/main/main_window.cpp | 15 ++ scwx-qt/source/scwx/qt/map/map_widget.cpp | 5 + scwx-qt/source/scwx/qt/map/map_widget.hpp | 7 +- .../scwx/qt/ui/animation_dock_widget.cpp | 164 ++++++++++++++---- .../scwx/qt/ui/animation_dock_widget.hpp | 2 + .../scwx/qt/ui/animation_dock_widget.ui | 28 +-- scwx-qt/source/scwx/qt/util/time.cpp | 12 ++ scwx-qt/source/scwx/qt/util/time.hpp | 19 ++ 8 files changed, 205 insertions(+), 47 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index bab92b7e..4ab5d56b 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -141,6 +141,7 @@ public: ~MainWindowImpl() { homeRadarConnection_.disconnect(); + defaultTimeZoneConnection_.disconnect(); auto& generalSettings = settings::GeneralSettings::Instance(); @@ -245,6 +246,7 @@ public: bool layerActionsInitialized_ {false}; boost::signals2::scoped_connection homeRadarConnection_ {}; + boost::signals2::scoped_connection defaultTimeZoneConnection_ {}; std::vector maps_; @@ -377,6 +379,7 @@ MainWindow::MainWindow(QWidget* parent) : p->animationDockWidget_ = new ui::AnimationDockWidget(this); p->timelineGroup_->GetContentsLayout()->addWidget(p->animationDockWidget_); ui->radarToolboxScrollAreaContents->layout()->addWidget(p->timelineGroup_); + p->animationDockWidget_->UpdateTimeZone(p->activeMap_->GetDefaultTimeZone()); // Reset toolbox spacer at the bottom ui->radarToolboxScrollAreaContents->layout()->removeItem( @@ -982,6 +985,16 @@ void MainWindowImpl::ConnectMapSignals() void MainWindowImpl::ConnectAnimationSignals() { + defaultTimeZoneConnection_ = settings::GeneralSettings::Instance() + .default_time_zone() + .changed_signal() + .connect( + [this]() + { + animationDockWidget_->UpdateTimeZone( + activeMap_->GetDefaultTimeZone()); + }); + connect(animationDockWidget_, &ui::AnimationDockWidget::DateTimeChanged, timelineManager_.get(), @@ -1602,6 +1615,8 @@ void MainWindowImpl::UpdateRadarSite() alertManager_->SetRadarSite(radarSite); placefileManager_->SetRadarSite(radarSite); + + animationDockWidget_->UpdateTimeZone(activeMap_->GetDefaultTimeZone()); } void MainWindowImpl::UpdateVcp() diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index e219bfb6..763be1b6 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -821,6 +821,11 @@ void MapWidget::SetSmoothingEnabled(bool smoothingEnabled) } } +const scwx::util::time_zone* MapWidget::GetDefaultTimeZone() const +{ + return p->radarProductManager_->default_time_zone(); +} + void MapWidget::SelectElevation(float elevation) { auto radarProductView = p->context_->radar_product_view(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 348e9113..f5f68b22 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -45,9 +45,10 @@ public: void DumpLayerList() const; [[nodiscard]] common::Level3ProductCategoryMap - GetAvailableLevel3Categories(); - [[nodiscard]] std::optional GetElevation() const; - [[nodiscard]] std::vector GetElevationCuts() const; + GetAvailableLevel3Categories(); + [[nodiscard]] const scwx::util::time_zone* GetDefaultTimeZone() const; + [[nodiscard]] std::optional GetElevation() const; + [[nodiscard]] std::vector GetElevationCuts() const; [[nodiscard]] std::optional GetIncomingLevel2Elevation() const; [[nodiscard]] std::vector GetLevel3Products(); [[nodiscard]] std::string GetMapStyle() const; diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index e8519c47..1685e099 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -18,6 +18,14 @@ namespace ui static const std::string logPrefix_ = "scwx::qt::ui::animation_dock_widget"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); +#if (__cpp_lib_chrono >= 201907L) + using local_days = std::chrono::local_days; + using zoned_time_ = std::chrono::zoned_time; +#else + using local_days = date::local_days; + using zoned_time_ = date::zoned_time; +#endif + class AnimationDockWidgetImpl { public: @@ -47,8 +55,14 @@ public: types::MapTime viewType_ {types::MapTime::Live}; bool isLive_ {true}; - std::chrono::sys_days selectedDate_ {}; - std::chrono::seconds selectedTime_ {}; + local_days selectedDate_ {}; + std::chrono::seconds selectedTime_ {}; + + const scwx::util::time_zone* timeZone_ {nullptr}; + + void UpdateTimeZoneLabel(const zoned_time_ zonedTime); + std::chrono::system_clock::time_point GetTimePoint(); + void SetTimePoint(std::chrono::system_clock::time_point time); void ConnectSignals(); void UpdateAutoUpdateLabel(); @@ -61,21 +75,19 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : { ui->setupUi(this); - // Set current date/time - QDateTime currentDateTime = QDateTime::currentDateTimeUtc(); - QDate currentDate = currentDateTime.date(); - QTime currentTime = currentDateTime.time(); - currentTime = currentTime.addSecs(-currentTime.second() + 59); - - ui->dateEdit->setDate(currentDate); - ui->timeEdit->setTime(currentTime); - ui->dateEdit->setMaximumDate(currentDateTime.date()); - p->selectedDate_ = util::SysDays(currentDate); - p->selectedTime_ = - std::chrono::seconds(currentTime.msecsSinceStartOfDay() / 1000); +#if (__cpp_lib_chrono >= 201907L) + p->timeZone_ = std::chrono::get_tzdb().locate_zone("UTC"); +#else + p->timeZone_ = date::get_tzdb().locate_zone("UTC"); +#endif + const std::chrono::sys_seconds currentTimePoint = + std::chrono::floor( + std::chrono::system_clock::now()); + p->SetTimePoint(currentTimePoint); // Update maximum date on a timer - QTimer* maxDateTimer = new QTimer(this); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) Qt Owns this memory + auto* maxDateTimer = new QTimer(this); connect(maxDateTimer, &QTimer::timeout, this, @@ -108,6 +120,76 @@ AnimationDockWidget::~AnimationDockWidget() delete ui; } +void AnimationDockWidgetImpl::UpdateTimeZoneLabel(const zoned_time_ zonedTime) +{ +#if (__cpp_lib_chrono >= 201907L) + namespace df = std; + static constexpr std::string_view kFormatStringTimezone = "{:%Z}"; +#else + namespace df = date; + static constexpr std::string kFormatStringTimezone = "%Z"; +#endif + const std::string timeZoneStr = df::format(kFormatStringTimezone, zonedTime); + self_->ui->timeZoneLabel->setText(timeZoneStr.c_str()); +} + +std::chrono::system_clock::time_point AnimationDockWidgetImpl::GetTimePoint() +{ +#if (__cpp_lib_chrono >= 201907L) + using namespace std::chrono; +#else + using namespace date; +#endif + + // Convert the local time, to a zoned time, to a system time + const local_time localTime = + selectedDate_ + selectedTime_; + const auto zonedTime = + zoned_time(timeZone_, localTime); + const std::chrono::sys_time systemTime = zonedTime.get_sys_time(); + + // This is done to update it when the date changes + UpdateTimeZoneLabel(zonedTime); + + return systemTime; +} + +void AnimationDockWidgetImpl::SetTimePoint( + std::chrono::system_clock::time_point systemTime) +{ +#if (__cpp_lib_chrono >= 201907L) + using namespace std::chrono; +#else + using namespace date; +#endif + // Convert the time to a local time + auto systemTimeSeconds = time_point_cast(systemTime); + auto zonedTime = + zoned_time(timeZone_, systemTimeSeconds); + const local_seconds localTime = zonedTime.get_local_time(); + + // Get the date and time as seperate fields + selectedDate_ = floor(localTime); + selectedTime_ = localTime - selectedDate_; + + // Pull out the local date and time as qt times (with c++20 this could be + // simplified) + auto time = QTime::fromMSecsSinceStartOfDay(static_cast( + duration_cast(selectedTime_).count())); + auto yearMonthDay = year_month_day(selectedDate_); + auto date = QDate(int(yearMonthDay.year()), + // These are always in a small range, so cast is safe + static_cast(unsigned(yearMonthDay.month())), + static_cast(unsigned(yearMonthDay.day()))); + + // Update labels + self_->ui->timeEdit->setTime(time); + self_->ui->dateEdit->setDate(date); + + // Time zone almost certainly just changed, so update it + UpdateTimeZoneLabel(zonedTime); +} + void AnimationDockWidgetImpl::ConnectSignals() { // View type @@ -142,23 +224,24 @@ void AnimationDockWidgetImpl::ConnectSignals() { if (date.isValid()) { - selectedDate_ = util::SysDays(date); - Q_EMIT self_->DateTimeChanged(selectedDate_ + selectedTime_); - } - }); - QObject::connect( - self_->ui->timeEdit, - &QDateTimeEdit::timeChanged, - self_, - [this](QTime time) - { - if (time.isValid()) - { - selectedTime_ = - std::chrono::seconds(time.msecsSinceStartOfDay() / 1000); - Q_EMIT self_->DateTimeChanged(selectedDate_ + selectedTime_); + selectedDate_ = util::LocalDays(date); + Q_EMIT self_->DateTimeChanged(GetTimePoint()); } }); + QObject::connect(self_->ui->timeEdit, + &QDateTimeEdit::timeChanged, + self_, + [this](QTime time) + { + if (time.isValid()) + { + selectedTime_ = + std::chrono::duration_cast( + std::chrono::milliseconds( + time.msecsSinceStartOfDay())); + Q_EMIT self_->DateTimeChanged(GetTimePoint()); + } + }); // Loop controls QObject::connect( @@ -302,6 +385,27 @@ void AnimationDockWidgetImpl::UpdateAutoUpdateLabel() } } +void AnimationDockWidget::UpdateTimeZone(const scwx::util::time_zone* timeZone) +{ + // null timezone is really UTC. This simplifies other code. + if (timeZone == nullptr) + { +#if (__cpp_lib_chrono >= 201907L) + timeZone = std::chrono::get_tzdb().locate_zone("UTC"); +#else + timeZone = date::get_tzdb().locate_zone("UTC"); +#endif + } + + // Get the (UTC relative) time that is selected. We want to preserve this + // across timezone changes. + auto currentTime = p->GetTimePoint(); + p->timeZone_ = timeZone; + // Set the (UTC relative) time that was already selected. This ensures that + // the actual time does not change, only the time zone. + p->SetTimePoint(currentTime); +} + } // namespace ui } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp index abc79c88..c22b0849 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -32,6 +33,7 @@ public slots: void UpdateAnimationState(types::AnimationState state); void UpdateLiveState(bool isLive); void UpdateViewType(types::MapTime viewType); + void UpdateTimeZone(const scwx::util::time_zone* timeZone); signals: void ViewTypeChanged(types::MapTime viewType); diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui index d5cfe3aa..67203ffc 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui @@ -7,14 +7,14 @@ 0 0 189 - 264 + 276 - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised @@ -56,7 +56,7 @@ - QAbstractSpinBox::CorrectToNearestValue + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue @@ -79,10 +79,10 @@ - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised @@ -100,7 +100,7 @@ - QAbstractSpinBox::CorrectToNearestValue + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue HH:mm @@ -108,7 +108,7 @@ - + UTC @@ -120,10 +120,10 @@ - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised @@ -141,7 +141,7 @@ - QAbstractSpinBox::CorrectToNearestValue + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue min @@ -193,7 +193,7 @@ - QAbstractSpinBox::CorrectToNearestValue + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue x @@ -219,10 +219,10 @@ - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised diff --git a/scwx-qt/source/scwx/qt/util/time.cpp b/scwx-qt/source/scwx/qt/util/time.cpp index ea3afa1d..73d7820b 100644 --- a/scwx-qt/source/scwx/qt/util/time.cpp +++ b/scwx-qt/source/scwx/qt/util/time.cpp @@ -17,6 +17,18 @@ std::chrono::sys_days SysDays(const QDate& date) julianEpoch); } +local_days LocalDays(const QDate& date) +{ +#if (__cpp_lib_chrono >= 201907L) + using namespace std::chrono; +#else + using namespace date; +#endif + auto yearMonthDay = + year_month_day(year(date.year()), month(date.month()), day(date.day())); + return local_days(yearMonthDay); +} + } // namespace util } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/util/time.hpp b/scwx-qt/source/scwx/qt/util/time.hpp index 8f3e7a53..34af655e 100644 --- a/scwx-qt/source/scwx/qt/util/time.hpp +++ b/scwx-qt/source/scwx/qt/util/time.hpp @@ -2,6 +2,10 @@ #include +#if (__cpp_lib_chrono < 201907L) +# include +#endif + #include namespace scwx @@ -11,6 +15,12 @@ namespace qt namespace util { +#if (__cpp_lib_chrono >= 201907L) +using local_days = std::chrono::local_days; +#else +using local_days = date::local_days; +#endif + /** * @brief Convert QDate to std::chrono::sys_days. * @@ -20,6 +30,15 @@ namespace util */ std::chrono::sys_days SysDays(const QDate& date); +/** + * @brief Convert QDate to std::chrono::local_days. + * + * @param [in] date Date to convert + * + * @return Days + */ +local_days LocalDays(const QDate& date); + } // namespace util } // namespace qt } // namespace scwx From e3cf37f9ed6a770235270d6ec4754dd6884f10dc Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 15:47:04 -0400 Subject: [PATCH 653/762] Fix formatting issues for timezones-for-timeline --- .../scwx/qt/ui/animation_dock_widget.cpp | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 1685e099..8e376ddb 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -19,11 +19,11 @@ static const std::string logPrefix_ = "scwx::qt::ui::animation_dock_widget"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); #if (__cpp_lib_chrono >= 201907L) - using local_days = std::chrono::local_days; - using zoned_time_ = std::chrono::zoned_time; +using local_days = std::chrono::local_days; +using zoned_time_ = std::chrono::zoned_time; #else - using local_days = date::local_days; - using zoned_time_ = date::zoned_time; +using local_days = date::local_days; +using zoned_time_ = date::zoned_time; #endif class AnimationDockWidgetImpl @@ -228,20 +228,19 @@ void AnimationDockWidgetImpl::ConnectSignals() Q_EMIT self_->DateTimeChanged(GetTimePoint()); } }); - QObject::connect(self_->ui->timeEdit, - &QDateTimeEdit::timeChanged, - self_, - [this](QTime time) - { - if (time.isValid()) - { - selectedTime_ = - std::chrono::duration_cast( - std::chrono::milliseconds( - time.msecsSinceStartOfDay())); - Q_EMIT self_->DateTimeChanged(GetTimePoint()); - } - }); + QObject::connect( + self_->ui->timeEdit, + &QDateTimeEdit::timeChanged, + self_, + [this](QTime time) + { + if (time.isValid()) + { + selectedTime_ = std::chrono::duration_cast( + std::chrono::milliseconds(time.msecsSinceStartOfDay())); + Q_EMIT self_->DateTimeChanged(GetTimePoint()); + } + }); // Loop controls QObject::connect( From 8d7d29bf5e080948e874c2dcf6c972bdf9e4a66b Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 16:12:53 -0400 Subject: [PATCH 654/762] use const instead of constexpr because gcc-11 did not like it --- scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 8e376ddb..6714d925 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -123,11 +123,11 @@ AnimationDockWidget::~AnimationDockWidget() void AnimationDockWidgetImpl::UpdateTimeZoneLabel(const zoned_time_ zonedTime) { #if (__cpp_lib_chrono >= 201907L) - namespace df = std; - static constexpr std::string_view kFormatStringTimezone = "{:%Z}"; + namespace df = std; + static const std::string_view kFormatStringTimezone = "{:%Z}"; #else - namespace df = date; - static constexpr std::string kFormatStringTimezone = "%Z"; + namespace df = date; + static const std::string kFormatStringTimezone = "%Z"; #endif const std::string timeZoneStr = df::format(kFormatStringTimezone, zonedTime); self_->ui->timeZoneLabel->setText(timeZoneStr.c_str()); From a87cb01935f6ffd7dfbb1e3f35af0c53afa83e5e Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 16:14:30 -0400 Subject: [PATCH 655/762] use sys_seconds instead of sys_tim --- scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 6714d925..06ba6864 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -146,7 +146,7 @@ std::chrono::system_clock::time_point AnimationDockWidgetImpl::GetTimePoint() selectedDate_ + selectedTime_; const auto zonedTime = zoned_time(timeZone_, localTime); - const std::chrono::sys_time systemTime = zonedTime.get_sys_time(); + const std::chrono::sys_seconds systemTime = zonedTime.get_sys_time(); // This is done to update it when the date changes UpdateTimeZoneLabel(zonedTime); From cb5f5379cbb50d20ea9fd10435becc575095dbbf Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sun, 8 Jun 2025 16:59:43 -0400 Subject: [PATCH 656/762] use constexpr for things that need them --- scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 06ba6864..85fdb74f 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -123,8 +123,8 @@ AnimationDockWidget::~AnimationDockWidget() void AnimationDockWidgetImpl::UpdateTimeZoneLabel(const zoned_time_ zonedTime) { #if (__cpp_lib_chrono >= 201907L) - namespace df = std; - static const std::string_view kFormatStringTimezone = "{:%Z}"; + namespace df = std; + static constexpr std::string_view kFormatStringTimezone = "{:%Z}"; #else namespace df = date; static const std::string kFormatStringTimezone = "%Z"; From 012d70b3a212aeb69657d6e1e23def6482703613 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 7 Jun 2025 09:39:28 -0500 Subject: [PATCH 657/762] Initial project file updates for macos --- conanfile.py | 6 ++++++ scwx-qt/scwx-qt.cmake | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/conanfile.py b/conanfile.py index 13a978ca..ea880677 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,10 +35,16 @@ class SupercellWxConan(ConanFile): self.options["openssl"].shared = True self.options["libcurl"].ca_bundle = "none" self.options["libcurl"].ca_path = "none" + elif self.settings.os == "Macos": + self.options["openssl"].shared = True + self.options["libcurl"].ca_bundle = "none" + self.options["libcurl"].ca_path = "none" def requirements(self): if self.settings.os == "Linux": self.requires("onetbb/2022.0.0") + elif self.settings.os == "Macos": + self.requires("onetbb/2022.0.0") def generate(self): build_folder = os.path.join(self.build_folder, diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index aff16705..6606c2ad 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -694,7 +694,9 @@ if (MSVC) else() target_compile_options(scwx-qt PRIVATE "$<$:-g>") target_compile_options(supercell-wx PRIVATE "$<$:-g>") +endif() +if (LINUX) # Add wayland client packages find_package(QT NAMES Qt6 COMPONENTS WaylandClient @@ -746,6 +748,10 @@ install(TARGETS supercell-wx RUNTIME COMPONENT supercell-wx LIBRARY + COMPONENT supercell-wx + OPTIONAL + FRAMEWORK + DESTINATION Frameworks COMPONENT supercell-wx OPTIONAL) From e86fec8d9960e9a90533fbd524ba60f6844514e0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:16:32 -0500 Subject: [PATCH 658/762] Some parallel loops cannot be vectorized --- .../source/scwx/qt/manager/radar_product_manager.cpp | 10 +++++----- scwx-qt/source/scwx/qt/manager/resource_manager.cpp | 2 +- scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 18af3b02..d2bc15d0 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -175,7 +175,7 @@ public: level2ChunksProviderManager_->Disable(); std::shared_lock lock(level3ProviderManagerMutex_); - std::for_each(std::execution::par_unseq, + std::for_each(std::execution::par, level3ProviderManagerMap_.begin(), level3ProviderManagerMap_.end(), [](auto& p) @@ -693,7 +693,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, auto availableProducts = providerManager->provider_->GetAvailableProducts(); - if (std::find(std::execution::par_unseq, + if (std::find(std::execution::par, availableProducts.cbegin(), availableProducts.cend(), product) != availableProducts.cend()) @@ -920,13 +920,13 @@ RadarProductManager::GetActiveVolumeTimes( // For each provider (in parallel) std::for_each( - std::execution::par_unseq, + std::execution::par, providers.begin(), providers.end(), [&](const std::shared_ptr& provider) { // For yesterday, today and tomorrow (in parallel) - std::for_each(std::execution::par_unseq, + std::for_each(std::execution::par, dates.begin(), dates.end(), [&](const auto& date) @@ -1246,7 +1246,7 @@ void RadarProductManagerImpl::PopulateProductTimes( std::mutex volumeTimesMutex {}; // For yesterday, today and tomorrow (in parallel) - std::for_each(std::execution::par_unseq, + std::for_each(std::execution::par, dates.begin(), dates.end(), [&](const auto& date) diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index 41558378..0c8cecef 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -62,7 +62,7 @@ LoadImageResources(const std::vector& urlStrings) std::mutex m {}; std::vector> images {}; - std::for_each(std::execution::par_unseq, + std::for_each(std::execution::par, urlStrings.begin(), urlStrings.end(), [&](auto& urlString) diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index c069bfb3..3b6451c1 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -449,7 +449,7 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale) if (numChannels == 3) { std::for_each( - std::execution::par_unseq, + std::execution::par, view.begin(), view.end(), [](boost::gil::rgba8_pixel_t& pixel) From b6aa85a916e0fb6f6159b295598d894c90a8b5ee Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:17:36 -0500 Subject: [PATCH 659/762] Remove unused variables --- scwx-qt/source/scwx/qt/util/color.cpp | 2 -- scwx-qt/source/scwx/qt/util/file.cpp | 2 -- wxdata/source/scwx/network/cpr.cpp | 2 -- 3 files changed, 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/util/color.cpp b/scwx-qt/source/scwx/qt/util/color.cpp index 16060bb9..bdd3f6c2 100644 --- a/scwx-qt/source/scwx/qt/util/color.cpp +++ b/scwx-qt/source/scwx/qt/util/color.cpp @@ -13,8 +13,6 @@ namespace util namespace color { -static const std::string logPrefix_ = "scwx::qt::util::color"; - std::string ToArgbString(const boost::gil::rgba8_pixel_t& color) { return fmt::format( diff --git a/scwx-qt/source/scwx/qt/util/file.cpp b/scwx-qt/source/scwx/qt/util/file.cpp index b129e6ce..7e126345 100644 --- a/scwx-qt/source/scwx/qt/util/file.cpp +++ b/scwx-qt/source/scwx/qt/util/file.cpp @@ -12,8 +12,6 @@ namespace qt namespace util { -static const std::string logPrefix_ = "scwx::qt::util::file"; - std::unique_ptr OpenFile(const std::string& filename, std::ios_base::openmode mode) { diff --git a/wxdata/source/scwx/network/cpr.cpp b/wxdata/source/scwx/network/cpr.cpp index 81dea5ad..0f8fb956 100644 --- a/wxdata/source/scwx/network/cpr.cpp +++ b/wxdata/source/scwx/network/cpr.cpp @@ -7,8 +7,6 @@ namespace network namespace cpr { -static const std::string logPrefix_ = "scwx::network::cpr"; - static ::cpr::Header header_ {}; ::cpr::Header GetHeader() From 696c277f94726d2b1a715c2fe5d650884653db82 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:18:04 -0500 Subject: [PATCH 660/762] macOS has unique location for glu.h --- scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index 4f7f2c81..e5c8addd 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -4,10 +4,15 @@ #include -#include #include -#if defined(_WIN32) +#if !defined(__APPLE__) +# include +#else +# include +#endif + +#if defined(_WIN32) || defined(__APPLE__) typedef void (*_GLUfuncptr)(void); #endif From d4b3c1869bc36e973c89669ff88cf0b21090d944 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:18:27 -0500 Subject: [PATCH 661/762] Add missing include --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 93a6d2d1..156b1a6d 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include From 9f7f1bf860b99cb8cbefa87043c99383e273dcec Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:21:03 -0500 Subject: [PATCH 662/762] Make setup/configure scripts common between Linux/macOS --- tools/configure-environment.sh | 23 +++++++++++++++++------ tools/lib/run-cmake-configure.sh | 3 ++- tools/lib/setup-common.sh | 19 ++++++++++--------- tools/lib/setup-conan.sh | 3 ++- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh index 0da64217..bc844493 100755 --- a/tools/configure-environment.sh +++ b/tools/configure-environment.sh @@ -1,8 +1,19 @@ #!/bin/bash -script_dir="$(dirname "$(readlink -f "$0")")" +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" # Assign user-specified Python Virtual Environment -[ "${1:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${1:-${script_dir}/../.venv}")" +if [ "${1:-}" = "none" ]; then + unset venv_path +else + venv_arg="${1:-${script_dir}/../.venv}" + # Portable way to get absolute path without requiring the directory to exist + case "${venv_arg}" in + /*) venv_path="${venv_arg}" ;; + *) venv_path="$(cd "$(dirname "${venv_arg}")" && pwd)/$(basename "${venv_arg}")" ;; + esac + export venv_path +fi # Load custom build settings if [ -f "${script_dir}/lib/user-setup.sh" ]; then @@ -11,12 +22,12 @@ fi # Activate Python Virtual Environment if [ -n "${venv_path:-}" ]; then - python -m venv "${venv_path}" + python3 -m venv "${venv_path}" source "${venv_path}/bin/activate" fi # Detect if a Python Virtual Environment was specified above, or elsewhere -IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') +IN_VENV=$(python3 -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') if [ "${IN_VENV}" = "True" ]; then # In a virtual environment, don't use --user @@ -27,8 +38,8 @@ else fi # Install Python packages -python -m pip install ${PIP_FLAGS} --upgrade pip -pip install ${PIP_FLAGS} -r "${script_dir}/../requirements.txt" +python3 -m pip install ${PIP_FLAGS} --upgrade pip +python3 -m pip install ${PIP_FLAGS} -r "${script_dir}/../requirements.txt" # Configure default Conan profile conan profile detect -e diff --git a/tools/lib/run-cmake-configure.sh b/tools/lib/run-cmake-configure.sh index 5e1a3bbf..03bbcdb1 100755 --- a/tools/lib/run-cmake-configure.sh +++ b/tools/lib/run-cmake-configure.sh @@ -1,5 +1,6 @@ #!/bin/bash -script_dir="$(dirname "$(readlink -f "$0")")" +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" cmake_args=( -B "${build_dir}" diff --git a/tools/lib/setup-common.sh b/tools/lib/setup-common.sh index e28978e9..b11270e8 100755 --- a/tools/lib/setup-common.sh +++ b/tools/lib/setup-common.sh @@ -1,5 +1,6 @@ #!/bin/bash -script_dir="$(dirname "$(readlink -f "$0")")" +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" # Import common paths source "${script_dir}/common-paths.sh" @@ -9,14 +10,14 @@ if [ -f "${script_dir}/user-setup.sh" ]; then source "${script_dir}/user-setup.sh" fi -# Activate Python Virtual Environment +# Activate python3 Virtual Environment if [ -n "${venv_path:-}" ]; then - python -m venv "${venv_path}" + python3 -m venv "${venv_path}" source "${venv_path}/bin/activate" fi -# Detect if a Python Virtual Environment was specified above, or elsewhere -IN_VENV=$(python -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') +# Detect if a python3 Virtual Environment was specified above, or elsewhere +IN_VENV=$(python3 -c 'import sys; print(sys.prefix != getattr(sys, "base_prefix", sys.prefix))') if [ "${IN_VENV}" = "True" ]; then # In a virtual environment, don't use --user @@ -26,9 +27,9 @@ else PIP_FLAGS="--upgrade --user" fi -# Install Python packages -python -m pip install ${PIP_FLAGS} pip -pip install ${PIP_FLAGS} -r "${script_dir}/../../requirements.txt" +# Install python3 packages +python3 -m pip install ${PIP_FLAGS} pip +python3 -m pip install ${PIP_FLAGS} -r "${script_dir}/../../requirements.txt" if [[ -n "${build_type}" ]]; then # Install Conan profile and packages @@ -49,7 +50,7 @@ fi # Run CMake Configure "${script_dir}/run-cmake-configure.sh" -# Deactivate Python Virtual Environment +# Deactivate python3 Virtual Environment if [ -n "${venv_path:-}" ]; then deactivate fi diff --git a/tools/lib/setup-conan.sh b/tools/lib/setup-conan.sh index 2ac38ee7..0b6c5004 100755 --- a/tools/lib/setup-conan.sh +++ b/tools/lib/setup-conan.sh @@ -1,5 +1,6 @@ #!/bin/bash -script_dir="$(dirname "$(readlink -f "$0")")" +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" # Configure default Conan profile conan profile detect -e From 9f641dcdc512983a93ed2bda2752ad6ed961164a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 00:23:11 -0500 Subject: [PATCH 663/762] When using libc++, -fexperimental-library should be used --- wxdata/wxdata.cmake | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index a30398a0..e56c30ea 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.24) project(scwx-data) +include(CheckCXXSymbolExists) + find_package(Boost) find_package(cpr) find_package(LibXml2) @@ -10,6 +12,8 @@ find_package(range-v3) find_package(re2) find_package(spdlog) +check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP) + if (NOT MSVC) find_package(TBB) endif() @@ -327,10 +331,15 @@ if (NOT CHRONO_HAS_TIMEZONES_AND_CALENDERS) target_link_libraries(wxdata PUBLIC date::date-tz) endif() -if (NOT MSVC) +if (LINUX) target_link_libraries(wxdata PUBLIC TBB::tbb) endif() +if (LIBCPP) + # Enable support for parallel algorithms + target_compile_options(wxdata PUBLIC -fexperimental-library) +endif() + set_target_properties(wxdata PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) From ac4521483dd7a717d2c5ad372357f06d6bb736ea Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 13 Jun 2025 22:53:55 -0500 Subject: [PATCH 664/762] std::accumulate is defined in --- wxdata/source/scwx/wsr88d/rpg/raster_data_packet.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wxdata/source/scwx/wsr88d/rpg/raster_data_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/raster_data_packet.cpp index c0bf5557..03b00220 100644 --- a/wxdata/source/scwx/wsr88d/rpg/raster_data_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/raster_data_packet.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace scwx From 337406971a586aef2a30606aee31d7df0822b637 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 01:07:03 -0500 Subject: [PATCH 665/762] Additional macOS configuration with llvm@18 --- CMakePresets.json | 99 +++++++++++++++++++ tools/conan/profiles/scwx-macos_clang-18 | 8 ++ .../conan/profiles/scwx-macos_clang-18_armv8 | 8 ++ tools/configure-environment.sh | 35 ++++--- tools/setup-macos-debug.sh | 30 ++++++ tools/setup-macos-release.sh | 30 ++++++ 6 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 tools/conan/profiles/scwx-macos_clang-18 create mode 100644 tools/conan/profiles/scwx-macos_clang-18_armv8 create mode 100755 tools/setup-macos-debug.sh create mode 100755 tools/setup-macos-release.sh diff --git a/CMakePresets.json b/CMakePresets.json index 575be130..2e1fe5f6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -210,6 +210,81 @@ "CC": "gcc-11", "CXX": "g++-11" } + }, + { + "name": "macos-base", + "inherits": "base", + "hidden": true, + "cacheVariables": { + "CMAKE_PREFIX_PATH": "$env{HOME}/Qt/6.8.3/macos" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-clang18-base", + "inherits": "macos-base", + "hidden": true, + "environment": { + "CC": "/opt/homebrew/opt/llvm@18/bin/clang", + "CXX": "/opt/homebrew/opt/llvm@18/bin/clang++", + "PATH": "/opt/homebrew/opt/llvm@18/bin:$penv{PATH}", + "CPPFLAGS": "-I/opt/homebrew/opt/llvm@18/include", + "LDFLAGS": "-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" + } + }, + { + "name": "macos-clang18-x64-base", + "inherits": "macos-clang18-base", + "hidden": true, + "cacheVariables": { + "CONAN_HOST_PROFILE": "scwx-macos_clang-18", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18" + } + }, + { + "name": "macos-clang18-arm64-base", + "inherits": "macos-clang18-base", + "hidden": true, + "cacheVariables": { + "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8" + } + }, + { + "name": "macos-clang18-x64-debug", + "inherits": "macos-clang18-x64-base", + "displayName": "macOS Clang 18 x64 Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "macos-clang18-x64-release", + "inherits": "macos-clang18-x64-base", + "displayName": "macOS Clang 18 x64 Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "macos-clang18-arm64-debug", + "inherits": "macos-clang18-arm64-base", + "displayName": "macOS Clang 18 Arm64 Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "macos-clang18-arm64-release", + "inherits": "macos-clang18-arm64-base", + "displayName": "macOS Clang 18 Arm64 Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } } ], "buildPresets": [ @@ -236,6 +311,30 @@ "configurePreset": "linux-gcc-release", "displayName": "Linux GCC Release", "configuration": "Release" + }, + { + "name": "macos-clang18-x64-debug", + "configurePreset": "macos-clang18-x64-debug", + "displayName": "macOS Clang 18 x64 Debug", + "configuration": "Debug" + }, + { + "name": "macos-clang18-x64-release", + "configurePreset": "macos-clang18-x64-release", + "displayName": "macOS Clang 18 x64 Release", + "configuration": "Release" + }, + { + "name": "macos-clang18-arm64-debug", + "configurePreset": "macos-clang18-arm64-debug", + "displayName": "macOS Clang 18 Arm64 Debug", + "configuration": "Debug" + }, + { + "name": "macos-clang18-arm64-release", + "configurePreset": "macos-clang18-arm64-release", + "displayName": "macOS Clang 18 Arm64 Release", + "configuration": "Release" } ], "testPresets": [ diff --git a/tools/conan/profiles/scwx-macos_clang-18 b/tools/conan/profiles/scwx-macos_clang-18 new file mode 100644 index 00000000..70889284 --- /dev/null +++ b/tools/conan/profiles/scwx-macos_clang-18 @@ -0,0 +1,8 @@ +[settings] +arch=x86_64 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libc++ +compiler.version=18 +os=Macos diff --git a/tools/conan/profiles/scwx-macos_clang-18_armv8 b/tools/conan/profiles/scwx-macos_clang-18_armv8 new file mode 100644 index 00000000..63c6d597 --- /dev/null +++ b/tools/conan/profiles/scwx-macos_clang-18_armv8 @@ -0,0 +1,8 @@ +[settings] +arch=armv8 +build_type=Release +compiler=clang +compiler.cppstd=20 +compiler.libcxx=libc++ +compiler.version=18 +os=Macos diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh index bc844493..1dfc714c 100755 --- a/tools/configure-environment.sh +++ b/tools/configure-environment.sh @@ -45,20 +45,29 @@ python3 -m pip install ${PIP_FLAGS} -r "${script_dir}/../requirements.txt" conan profile detect -e # Conan profiles -conan_profiles=( - "scwx-linux_clang-17" - "scwx-linux_clang-17_armv8" - "scwx-linux_clang-18" - "scwx-linux_clang-18_armv8" - "scwx-linux_gcc-11" - "scwx-linux_gcc-11_armv8" - "scwx-linux_gcc-12" - "scwx-linux_gcc-12_armv8" - "scwx-linux_gcc-13" - "scwx-linux_gcc-13_armv8" - "scwx-linux_gcc-14" - "scwx-linux_gcc-14_armv8" +if [[ "$(uname)" == "Darwin" ]]; then + # macOS profiles + conan_profiles=( + "scwx-macos_clang-18" + "scwx-macos_clang-18_armv8" ) +else + # Linux profiles + conan_profiles=( + "scwx-linux_clang-17" + "scwx-linux_clang-17_armv8" + "scwx-linux_clang-18" + "scwx-linux_clang-18_armv8" + "scwx-linux_gcc-11" + "scwx-linux_gcc-11_armv8" + "scwx-linux_gcc-12" + "scwx-linux_gcc-12_armv8" + "scwx-linux_gcc-13" + "scwx-linux_gcc-13_armv8" + "scwx-linux_gcc-14" + "scwx-linux_gcc-14_armv8" + ) +fi # Install Conan profiles for profile_name in "${conan_profiles[@]}"; do diff --git a/tools/setup-macos-debug.sh b/tools/setup-macos-debug.sh new file mode 100755 index 00000000..d9c31c7e --- /dev/null +++ b/tools/setup-macos-debug.sh @@ -0,0 +1,30 @@ +#!/bin/bash +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" + +export build_dir="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${1:-${script_dir}/../build-debug}")" +export build_type=Debug +export conan_profile=${2:-scwx-macos_clang-18_armv8} +export generator=Ninja +export qt_base="/Users/${USER}/Qt" +export qt_arch=macos +export address_sanitizer=${4:-disabled} + +# Set explicit compiler paths +export CC=/opt/homebrew/opt/llvm@18/bin/clang +export CXX=/opt/homebrew/opt/llvm@18/bin/clang++ +export PATH="/opt/homebrew/opt/llvm@18/bin:$PATH" + +export LDFLAGS="-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" +export CPPFLAGS="-I/opt/homebrew/opt/llvm@18/include" + +# Assign user-specified Python Virtual Environment +if [ "${3:-}" = "none" ]; then + unset venv_path +else + # macOS does not have 'readlink -f', use python for realpath + export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" +fi + +# Perform common setup +"${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-macos-release.sh b/tools/setup-macos-release.sh new file mode 100755 index 00000000..39fd40e9 --- /dev/null +++ b/tools/setup-macos-release.sh @@ -0,0 +1,30 @@ +#!/bin/bash +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" + +export build_dir="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${1:-${script_dir}/../build-release}")" +export build_type=Release +export conan_profile=${2:-scwx-macos_clang-18_armv8} +export generator=Ninja +export qt_base="/Users/${USER}/Qt" +export qt_arch=macos +export address_sanitizer=${4:-disabled} + +# Set explicit compiler paths +export CC=/opt/homebrew/opt/llvm@18/bin/clang +export CXX=/opt/homebrew/opt/llvm@18/bin/clang++ +export PATH="/opt/homebrew/opt/llvm@18/bin:$PATH" + +export LDFLAGS="-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" +export CPPFLAGS="-I/opt/homebrew/opt/llvm@18/include" + +# Assign user-specified Python Virtual Environment +if [ "${3:-}" = "none" ]; then + unset venv_path +else + # macOS does not have 'readlink -f', use python for realpath + export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" +fi + +# Perform common setup +"${script_dir}/lib/setup-common.sh" From cb81287c088cde0b9f7636b8085aaa19a8d71cfe Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 01:10:35 -0500 Subject: [PATCH 666/762] libc++experimental needs linked with -fexperimental-library --- wxdata/wxdata.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index e56c30ea..288da639 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -338,6 +338,7 @@ endif() if (LIBCPP) # Enable support for parallel algorithms target_compile_options(wxdata PUBLIC -fexperimental-library) + target_link_libraries(wxdata INTERFACE c++experimental) endif() set_target_properties(wxdata PROPERTIES CXX_STANDARD 20 From 8a56d3deedb07a9256ddb6e6f8d2d410e447a23c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 01:11:21 -0500 Subject: [PATCH 667/762] Ensure conan can be found in the virtual environment --- tools/scwx_config.cmake | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/scwx_config.cmake b/tools/scwx_config.cmake index 8373418b..93205f90 100644 --- a/tools/scwx_config.cmake +++ b/tools/scwx_config.cmake @@ -31,6 +31,9 @@ macro(scwx_python_setup) set(Python3_EXECUTABLE "$ENV{VIRTUAL_ENV}/bin/python") endif() + # Add virtual environment to program search paths + set(CMAKE_PROGRAM_PATH "$ENV{VIRTUAL_ENV}/bin" ${CMAKE_PROGRAM_PATH}) + message(STATUS "Using virtual environment: $ENV{VIRTUAL_ENV}") else() message(STATUS "Python virtual environment undefined") From 3000e23cb129a501ebec528accb721983ee09287 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 09:20:55 -0500 Subject: [PATCH 668/762] TBB is used for Linux only --- scwx-qt/scwx-qt.cmake | 2 +- wxdata/wxdata.cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 6606c2ad..b218e6d1 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -635,7 +635,7 @@ if (WIN32) target_compile_definitions(supercell-wx PUBLIC WIN32_LEAN_AND_MEAN) endif() -if (NOT MSVC) +if (LINUX) # Qt emit keyword is incompatible with TBB target_compile_definitions(scwx-qt PRIVATE QT_NO_EMIT) target_compile_definitions(supercell-wx PRIVATE QT_NO_EMIT) diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 288da639..ab23a4e7 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -14,7 +14,7 @@ find_package(spdlog) check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP) -if (NOT MSVC) +if (LINUX) find_package(TBB) endif() From 19e8e51505c09ca265852fd0572ec21bf851b94d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 09:35:57 -0500 Subject: [PATCH 669/762] boost::irange with sys_days is not portable --- scwx-qt/source/scwx/qt/manager/text_event_manager.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index 8aa4c611..d7759275 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -488,7 +487,7 @@ void TextEventManager::Impl::LoadArchives( std::vector loadListEntries {}; - for (auto date : boost::irange(startDate, endDate)) + for (auto date = startDate; date < endDate; date += std::chrono::days {1}) { auto mapIt = unloadedProductMap_.find(date); if (mapIt == unloadedProductMap_.cend()) From a5a96eb7c42ec3bb5a6aff865b9a91a51e7f9d94 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 22:22:31 -0500 Subject: [PATCH 670/762] maplibre-native updates for OpenGL 4.1 core on macOS --- external/maplibre-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/maplibre-native b/external/maplibre-native index 554e6e9a..5dc28064 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit 554e6e9ac46b6eaf5970a219c88e3df11f1cee30 +Subproject commit 5dc2806426ea6483b6b399f5e1bc9062edf19471 From ccd27980a23fe88458926516ca590e5bce7e44f6 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 14 Jun 2025 22:35:34 -0500 Subject: [PATCH 671/762] Updates for OpenGL 4.1 Core on macOS --- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 8 ++++++++ scwx-qt/source/scwx/qt/main/main.cpp | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 4cc42879..9fd6bd85 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -12,6 +12,7 @@ namespace gl { static const std::string logPrefix_ = "scwx::qt::gl::gl_context"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class GlContext::Impl { @@ -84,6 +85,13 @@ void GlContext::Impl::InitializeGL() gl_->initializeOpenGLFunctions(); gl30_->initializeOpenGLFunctions(); + logger_->info("OpenGL Version: {}", + reinterpret_cast(gl_->glGetString(GL_VERSION))); + logger_->info("OpenGL Vendor: {}", + reinterpret_cast(gl_->glGetString(GL_VENDOR))); + logger_->info("OpenGL Renderer: {}", + reinterpret_cast(gl_->glGetString(GL_RENDERER))); + gl_->glGenTextures(1, &textureAtlas_); glInitialized_ = true; diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 904ff3b3..7a1993f7 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,16 @@ int main(int argc, char* argv[]) QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); +#if defined(__APPLE__) + // For macOS, we must choose between OpenGL 4.1 Core and OpenGL 2.1 + // Compatibility. OpenGL 2.1 does not meet requirements for shaders used by + // Supercell Wx. + QSurfaceFormat surfaceFormat = QSurfaceFormat::defaultFormat(); + surfaceFormat.setVersion(4, 1); + surfaceFormat.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + QSurfaceFormat::setDefaultFormat(surfaceFormat); +#endif + QApplication a(argc, argv); QCoreApplication::setApplicationName("Supercell Wx"); From 16a962dbec489b273526dc1311b1fa9b1c0074be Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Jun 2025 22:36:31 -0500 Subject: [PATCH 672/762] Create a macOS .app bundle --- scwx-qt/res/icons/scwx.icns | Bin 0 -> 1194146 bytes scwx-qt/res/scwx-qt.plist.in | 42 ++++++++++++++++++++++ scwx-qt/scwx-qt.cmake | 68 +++++++++++++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 scwx-qt/res/icons/scwx.icns create mode 100644 scwx-qt/res/scwx-qt.plist.in diff --git a/scwx-qt/res/icons/scwx.icns b/scwx-qt/res/icons/scwx.icns new file mode 100644 index 0000000000000000000000000000000000000000..bda4ea325322f4c2b77112b0ab6efe520f7abf71 GIT binary patch literal 1194146 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%0D^oq`?pIW6cx?)M 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%0DvigO7*S)AXQ5k+)jHRSRoN&rcs$>?gSx5|d%E2G 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
5s04S6P&e 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%0DvigO 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 z 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

fkw~i5r-y6#n~%-?a_1I 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%0DNzb{;A+)wXqEl;vg?}szi-(Qq&mmM&|tVxxc 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 + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + + CFBundleDevelopmentRegion + en + CFBundleAllowMixedLocalizations + + + NSPrincipalClass + NSApplication + + NSSupportsAutomaticGraphicsSwitching + + + diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b218e6d1..7e2f26a1 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -626,6 +626,26 @@ if (WIN32) if (SCWX_DISABLE_CONSOLE) set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $,TRUE,FALSE>) endif() +elseif (APPLE) + set(SCWX_ICON "${scwx-qt_SOURCE_DIR}/res/icons/scwx.icns") + + set_source_files_properties(${SCWX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + + qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${SCWX_ICON}) + + string(TIMESTAMP CURRENT_YEAR "%Y") + + set_target_properties(supercell-wx PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_LIST "${scwx-qt_SOURCE_DIR}/res/scwx-qt.plist.in" + MACOSX_BUNDLE_GUI_IDENTIFIER "net.supercellwx.app" + MACOSX_BUNDLE_BUNDLE_NAME "Supercell Wx" + MACOSX_BUNDLE_BUNDLE_VERSION "${SCWX_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${SCWX_VERSION}" + MACOSX_BUNDLE_COPYRIGHT "Copyright ${CURRENT_YEAR} Dan Paulat" + MACOSX_BUNDLE_ICON_FILE "scwx.icns" + MACOSX_BUNDLE_INFO_STRING "Free and open source advanced weather radar" + RESOURCE ${SCWX_ICON}) else() qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES}) endif() @@ -735,9 +755,11 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets target_link_libraries(supercell-wx PRIVATE scwx-qt wxdata) -# Set DT_RUNPATH for Linux targets -set_target_properties(MLNQtCore PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") # QMapLibre::Core -set_target_properties(supercell-wx PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") +if (LINUX) + # Set DT_RUNPATH for Linux targets + set_target_properties(MLNQtCore PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") # QMapLibre::Core + set_target_properties(supercell-wx PROPERTIES INSTALL_RPATH "\$ORIGIN/../lib") +endif() install(TARGETS supercell-wx MLNQtCore # QMapLibre::Core @@ -747,6 +769,10 @@ install(TARGETS supercell-wx "^(/usr)?/lib/.*\\.so(\\..*)?" RUNTIME COMPONENT supercell-wx + BUNDLE + DESTINATION . + COMPONENT supercell-wx + OPTIONAL LIBRARY COMPONENT supercell-wx OPTIONAL @@ -773,6 +799,42 @@ install(SCRIPT ${deploy_script_qmaplibre_core} install(SCRIPT ${deploy_script_scwx} COMPONENT supercell-wx) +if (APPLE) + # Install additional script to fix up the bundle + install(CODE [[ + include (BundleUtilities) + + # Define the bundle path + set(BUNDLE_PATH "${CMAKE_INSTALL_PREFIX}/supercell-wx.app") + + file(GLOB_RECURSE PLUGIN_DYLIBS "${BUNDLE_PATH}/Contents/PlugIns/**/*.dylib") + + # Add the correct rpath for plugins to find bundled frameworks + foreach(PLUGIN_DYLIB ${PLUGIN_DYLIBS}) + execute_process( + COMMAND install_name_tool -add_rpath "@loader_path/../../Frameworks" + ${PLUGIN_DYLIB} + ) + endforeach() + + # Fix up the bundle with all dependencies + fixup_bundle( + "${BUNDLE_PATH}" + "" + "${CMAKE_INSTALL_PREFIX}/lib;${CMAKE_INSTALL_PREFIX}/Frameworks" + ) + + # Re-sign the bundle + execute_process( + COMMAND codesign --force --deep --sign - "${BUNDLE_PATH}" + ) + + # Verify the bundle + verify_app("${BUNDLE_PATH}") + ]] + COMPONENT supercell-wx) +endif() + if (MSVC) set(CPACK_PACKAGE_NAME "Supercell Wx") set(CPACK_PACKAGE_VENDOR "Dan Paulat") From cfa7c774ac3ee392d7dad80b81a9fb2fea54bc90 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Jun 2025 23:13:02 -0500 Subject: [PATCH 673/762] Create Apple Disk Image --- scwx-qt/scwx-qt.cmake | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 7e2f26a1..613f8ae8 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -831,18 +831,32 @@ if (APPLE) # Verify the bundle verify_app("${BUNDLE_PATH}") + + # Rename to "Supercell Wx.app" + file(REMOVE_RECURSE + "${CMAKE_INSTALL_PREFIX}/Supercell Wx.app") + file(RENAME + "${BUNDLE_PATH}" + "${CMAKE_INSTALL_PREFIX}/Supercell Wx.app") + + # Remove extra directories + file(REMOVE_RECURSE + "${CMAKE_INSTALL_PREFIX}/Frameworks") + file(REMOVE_RECURSE + "${CMAKE_INSTALL_PREFIX}/lib") ]] COMPONENT supercell-wx) endif() +set(CPACK_PACKAGE_NAME "Supercell Wx") +set(CPACK_PACKAGE_VENDOR "Dan Paulat") +set(CPACK_PACKAGE_CHECKSUM SHA256) +set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt") + if (MSVC) - set(CPACK_PACKAGE_NAME "Supercell Wx") - set(CPACK_PACKAGE_VENDOR "Dan Paulat") set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-windows-x64") set(CPACK_PACKAGE_INSTALL_DIRECTORY "Supercell Wx") set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/icons/scwx-256.ico") - set(CPACK_PACKAGE_CHECKSUM SHA256) - set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt") set(CPACK_GENERATOR WIX) set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx") set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C) @@ -854,5 +868,15 @@ if (MSVC) set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/") + include(CPack) +elseif(APPLE) + set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-macos") + set(CPACK_PACKAGE_ICON "${SCWX_ICON}") + set(CPACK_PACKAGE_VERSION "${SCWX_VERSION}") + + set(CPACK_GENERATOR DragNDrop) + + set(CPACK_COMPONENTS_ALL supercell-wx) + include(CPack) endif() From 48c5dd4fc4e8b629505fa6595e34a42c5d0383ba Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 16 Jun 2025 23:16:07 -0500 Subject: [PATCH 674/762] Add macOS to CI --- .github/workflows/ci.yml | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60b6a15b..6d515c48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: env_cc: '' env_cxx: '' compiler: msvc + cppflags: '' + ldflags: '' msvc_arch: x64 msvc_version: 2022 qt_version: 6.8.3 @@ -43,6 +45,8 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc + cppflags: '' + ldflags: '' qt_version: 6.8.3 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 @@ -59,6 +63,8 @@ jobs: env_cc: clang-17 env_cxx: clang++-17 compiler: clang + cppflags: '' + ldflags: '' qt_version: 6.8.3 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 @@ -75,6 +81,8 @@ jobs: env_cc: gcc-11 env_cxx: g++-11 compiler: gcc + cppflags: '' + ldflags: '' qt_version: 6.8.3 qt_arch_aqt: linux_gcc_arm64 qt_arch_dir: gcc_arm64 @@ -85,10 +93,46 @@ jobs: appimage_arch: aarch64 artifact_suffix: linux-arm64 compiler_packages: g++-11 + - name: macos_clang18_x64 + os: macos-13 + build_type: Release + env_cc: '$(brew --prefix)/opt/llvm@18/bin/clang' + env_cxx: '$(brew --prefix)/opt/llvm@18/bin/clang++' + compiler: clang + cppflags: '-I$(brew --prefix)/opt/llvm@18/include' + ldflags: '-L$(brew --prefix)/opt/llvm@18/lib' + qt_version: 6.8.3 + qt_arch_aqt: clang_64 + qt_arch_dir: macos + qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport + qt_tools: '' + conan_package_manager: '' + conan_profile: scwx-macos_clang-18 + appimage_arch: '' + artifact_suffix: macos-x64 + - name: macos_clang18_arm64 + os: macos-15 + build_type: Release + env_cc: '$(brew --prefix)/opt/llvm@18/bin/clang' + env_cxx: '$(brew --prefix)/opt/llvm@18/bin/clang++' + compiler: clang + cppflags: '-I$(brew --prefix)/opt/llvm@18/include' + ldflags: '-L$(brew --prefix)/opt/llvm@18/lib' + qt_version: 6.8.3 + qt_arch_aqt: clang_64 + qt_arch_dir: macos + qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport + qt_tools: '' + conan_package_manager: '' + conan_profile: scwx-macos_clang-18_armv8 + appimage_arch: '' + artifact_suffix: macos-arm64 name: ${{ matrix.name }} env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} + CPPFLAGS: ${{ matrix.cppflags }} + LDFLAGS: ${{ matrix.ldflags }} SCWX_VERSION: v0.4.9 runs-on: ${{ matrix.os }} @@ -133,6 +177,12 @@ jobs: flatpak-builder \ ${{ matrix.compiler_packages }} + - name: Setup macOS Environment + if: ${{ startsWith(matrix.os, 'macos') }} + shell: bash + run: | + brew install llvm@18 + - name: Setup Python Environment shell: pwsh run: | @@ -325,6 +375,20 @@ jobs: name: supercell-wx-flatpak-${{ matrix.artifact_suffix }} path: ${{ github.workspace }}/supercell-wx.flatpak + - name: Build Disk Image (macOS) + if: ${{ startsWith(matrix.os, 'macos') }} + shell: pwsh + run: | + cd build + cpack + + - name: Upload Disk Image (macOs) + if: ${{ startsWith(matrix.os, 'macos') }} + uses: actions/upload-artifact@v4 + with: + name: supercell-wx-${{ matrix.artifact_suffix }} + path: ${{ github.workspace }}/build/supercell-wx-*.dmg* + - name: Test Supercell Wx working-directory: ${{ github.workspace }}/build env: From 69d9137faf2e93c2b42628e44415ac4e0a147ed9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 19:30:47 -0500 Subject: [PATCH 675/762] brew --prefix llvm@18 has different paths for x64 and arm64 --- CMakePresets.json | 23 +++++++++++++++-------- tools/setup-macos-debug.sh | 10 +++++----- tools/setup-macos-release.sh | 10 +++++----- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 2e1fe5f6..056a32b9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -227,14 +227,7 @@ { "name": "macos-clang18-base", "inherits": "macos-base", - "hidden": true, - "environment": { - "CC": "/opt/homebrew/opt/llvm@18/bin/clang", - "CXX": "/opt/homebrew/opt/llvm@18/bin/clang++", - "PATH": "/opt/homebrew/opt/llvm@18/bin:$penv{PATH}", - "CPPFLAGS": "-I/opt/homebrew/opt/llvm@18/include", - "LDFLAGS": "-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" - } + "hidden": true }, { "name": "macos-clang18-x64-base", @@ -243,6 +236,13 @@ "cacheVariables": { "CONAN_HOST_PROFILE": "scwx-macos_clang-18", "CONAN_BUILD_PROFILE": "scwx-macos_clang-18" + }, + "environment": { + "CC": "/usr/local/opt/llvm@18/bin/clang", + "CXX": "/usr/local/opt/llvm@18/bin/clang++", + "PATH": "/usr/local/opt/llvm@18/bin:$penv{PATH}", + "CPPFLAGS": "-I/usr/local/opt/llvm@18/include", + "LDFLAGS": "-L/usr/local/opt/llvm@18/lib -L/usr/local/opt/llvm@18/lib/c++" } }, { @@ -252,6 +252,13 @@ "cacheVariables": { "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8", "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8" + }, + "environment": { + "CC": "/opt/homebrew/opt/llvm@18/bin/clang", + "CXX": "/opt/homebrew/opt/llvm@18/bin/clang++", + "PATH": "/opt/homebrew/opt/llvm@18/bin:$penv{PATH}", + "CPPFLAGS": "-I/opt/homebrew/opt/llvm@18/include", + "LDFLAGS": "-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" } }, { diff --git a/tools/setup-macos-debug.sh b/tools/setup-macos-debug.sh index d9c31c7e..ec24dfe2 100755 --- a/tools/setup-macos-debug.sh +++ b/tools/setup-macos-debug.sh @@ -11,12 +11,12 @@ export qt_arch=macos export address_sanitizer=${4:-disabled} # Set explicit compiler paths -export CC=/opt/homebrew/opt/llvm@18/bin/clang -export CXX=/opt/homebrew/opt/llvm@18/bin/clang++ -export PATH="/opt/homebrew/opt/llvm@18/bin:$PATH" +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +export PATH="$(brew --prefix llvm@18)/bin:$PATH" -export LDFLAGS="-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" -export CPPFLAGS="-I/opt/homebrew/opt/llvm@18/include" +export LDFLAGS="-L$(brew --prefix llvm@18)/lib -L$(brew --prefix llvm@18)/lib/c++" +export CPPFLAGS="-I$(brew --prefix llvm@18)/include" # Assign user-specified Python Virtual Environment if [ "${3:-}" = "none" ]; then diff --git a/tools/setup-macos-release.sh b/tools/setup-macos-release.sh index 39fd40e9..870dd841 100755 --- a/tools/setup-macos-release.sh +++ b/tools/setup-macos-release.sh @@ -11,12 +11,12 @@ export qt_arch=macos export address_sanitizer=${4:-disabled} # Set explicit compiler paths -export CC=/opt/homebrew/opt/llvm@18/bin/clang -export CXX=/opt/homebrew/opt/llvm@18/bin/clang++ -export PATH="/opt/homebrew/opt/llvm@18/bin:$PATH" +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +export PATH="$(brew --prefix llvm@18)/bin:$PATH" -export LDFLAGS="-L/opt/homebrew/opt/llvm@18/lib -L/opt/homebrew/opt/llvm@18/lib/c++" -export CPPFLAGS="-I/opt/homebrew/opt/llvm@18/include" +export LDFLAGS="-L$(brew --prefix llvm@18)/lib -L$(brew --prefix llvm@18)/lib/c++" +export CPPFLAGS="-I$(brew --prefix llvm@18)/include" # Assign user-specified Python Virtual Environment if [ "${3:-}" = "none" ]; then From dbde1adaa305852e400a598bed992296b0ebfa52 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 19:31:43 -0500 Subject: [PATCH 676/762] Additional maplibre-native updates --- external/maplibre-native | 2 +- external/maplibre-native-qt.cmake | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/external/maplibre-native b/external/maplibre-native index 5dc28064..3654f5fa 160000 --- a/external/maplibre-native +++ b/external/maplibre-native @@ -1 +1 @@ -Subproject commit 5dc2806426ea6483b6b399f5e1bc9062edf19471 +Subproject commit 3654f5fa9f06534d7fd2d95b810049a82e5953ef diff --git a/external/maplibre-native-qt.cmake b/external/maplibre-native-qt.cmake index 736c6049..7db42c10 100644 --- a/external/maplibre-native-qt.cmake +++ b/external/maplibre-native-qt.cmake @@ -35,6 +35,12 @@ else() target_compile_options(MLNQtCore PRIVATE "$<$:-g>") endif() +if (APPLE) + # Enable GL check error debug + target_compile_definitions(mbgl-core PRIVATE MLN_GL_CHECK_ERRORS=1) + target_compile_definitions(MLNQtCore PRIVATE MLN_GL_CHECK_ERRORS=1) +endif() + set(MLN_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/maplibre-native/include ${CMAKE_CURRENT_SOURCE_DIR}/maplibre-native-qt/src/core/include ${CMAKE_CURRENT_BINARY_DIR}/maplibre-native-qt/src/core/include PARENT_SCOPE) From 5d39d4206152c5782c00cefcd1686b6d89c4175f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 19:52:25 -0500 Subject: [PATCH 677/762] brew --prefix is not evaluated in matrix variables --- .github/workflows/ci.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d515c48..8840a524 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,11 +96,9 @@ jobs: - name: macos_clang18_x64 os: macos-13 build_type: Release - env_cc: '$(brew --prefix)/opt/llvm@18/bin/clang' - env_cxx: '$(brew --prefix)/opt/llvm@18/bin/clang++' + env_cc: clang + env_cxx: clang++ compiler: clang - cppflags: '-I$(brew --prefix)/opt/llvm@18/include' - ldflags: '-L$(brew --prefix)/opt/llvm@18/lib' qt_version: 6.8.3 qt_arch_aqt: clang_64 qt_arch_dir: macos @@ -113,11 +111,9 @@ jobs: - name: macos_clang18_arm64 os: macos-15 build_type: Release - env_cc: '$(brew --prefix)/opt/llvm@18/bin/clang' - env_cxx: '$(brew --prefix)/opt/llvm@18/bin/clang++' + env_cc: clang + env_cxx: clang++ compiler: clang - cppflags: '-I$(brew --prefix)/opt/llvm@18/include' - ldflags: '-L$(brew --prefix)/opt/llvm@18/lib' qt_version: 6.8.3 qt_arch_aqt: clang_64 qt_arch_dir: macos @@ -131,8 +127,6 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - CPPFLAGS: ${{ matrix.cppflags }} - LDFLAGS: ${{ matrix.ldflags }} SCWX_VERSION: v0.4.9 runs-on: ${{ matrix.os }} @@ -182,6 +176,11 @@ jobs: shell: bash run: | brew install llvm@18 + LLVM_PATH=$(brew --prefix llvm@18) + echo "CC=${LLVM_PATH}/clang" >> $GITHUB_ENV + echo "CXX=${LLVM_PATH}/clang++" >> $GITHUB_ENV + echo "CPPFLAGS=-I${LLVM_PATH}/include" >> $GITHUB_ENV + echo "LDFLAGS=-L${LLVM_PATH}/lib" >> $GITHUB_ENV - name: Setup Python Environment shell: pwsh From b626293f54c849ce790784f7503eaa1183750846 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 19:52:42 -0500 Subject: [PATCH 678/762] Add CMakePresets.json to .clang-format-ignore --- .clang-format-ignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .clang-format-ignore diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..5e71f5bf --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1 @@ +CMakePresets.json From fdcc5f01c9489046b34146fd8402fc9f575134e5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 20:16:36 -0500 Subject: [PATCH 679/762] Fix macOS clang/clang++ path --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8840a524..d6131179 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,8 +177,8 @@ jobs: run: | brew install llvm@18 LLVM_PATH=$(brew --prefix llvm@18) - echo "CC=${LLVM_PATH}/clang" >> $GITHUB_ENV - echo "CXX=${LLVM_PATH}/clang++" >> $GITHUB_ENV + echo "CC=${LLVM_PATH}/bin/clang" >> $GITHUB_ENV + echo "CXX=${LLVM_PATH}/bin/clang++" >> $GITHUB_ENV echo "CPPFLAGS=-I${LLVM_PATH}/include" >> $GITHUB_ENV echo "LDFLAGS=-L${LLVM_PATH}/lib" >> $GITHUB_ENV From 9d13023a51af84ba196a15218a41ef720ff5bd13 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 20:17:58 -0500 Subject: [PATCH 680/762] Format texture_atlas.cpp --- scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index 3b6451c1..f477be9b 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -390,10 +390,9 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale) QUrl url = QUrl::fromUserInput(qImagePath); - if (url.isLocalFile()) { - QString suffix = QFileInfo(qImagePath).suffix().toLower(); + QString suffix = QFileInfo(qImagePath).suffix().toLower(); QString qLocalImagePath = url.toString(QUrl::PreferLocalFile); if (suffix == "svg") @@ -448,18 +447,18 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale) // If no alpha channel, replace black with transparent if (numChannels == 3) { - std::for_each( - std::execution::par, - view.begin(), - view.end(), - [](boost::gil::rgba8_pixel_t& pixel) - { - static const boost::gil::rgba8_pixel_t kBlack {0, 0, 0, 255}; - if (pixel == kBlack) - { - pixel[3] = 0; - } - }); + std::for_each(std::execution::par, + view.begin(), + view.end(), + [](boost::gil::rgba8_pixel_t& pixel) + { + static const boost::gil::rgba8_pixel_t kBlack { + 0, 0, 0, 255}; + if (pixel == kBlack) + { + pixel[3] = 0; + } + }); } stbi_image_free(pixelData); From 99219f1c44e5d54ae380309a28e4d9ceff357870 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 21:05:18 -0500 Subject: [PATCH 681/762] ${LLVM_PATH}/lib/c++ should also be part of LDFLAGS --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6131179..a3d56548 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,7 @@ jobs: echo "CC=${LLVM_PATH}/bin/clang" >> $GITHUB_ENV echo "CXX=${LLVM_PATH}/bin/clang++" >> $GITHUB_ENV echo "CPPFLAGS=-I${LLVM_PATH}/include" >> $GITHUB_ENV - echo "LDFLAGS=-L${LLVM_PATH}/lib" >> $GITHUB_ENV + echo "LDFLAGS=-L${LLVM_PATH}/lib -L${LLVM_PATH}/lib/c++" >> $GITHUB_ENV - name: Setup Python Environment shell: pwsh From 41a2b989ae7300e02c646c2affb90e5350660dda Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 22:08:25 -0500 Subject: [PATCH 682/762] Add const to address clang-tidy findings --- scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index f477be9b..b8b5bd67 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -392,8 +392,8 @@ TextureAtlas::Impl::LoadImage(const std::string& imagePath, double scale) if (url.isLocalFile()) { - QString suffix = QFileInfo(qImagePath).suffix().toLower(); - QString qLocalImagePath = url.toString(QUrl::PreferLocalFile); + const QString suffix = QFileInfo(qImagePath).suffix().toLower(); + const QString qLocalImagePath = url.toString(QUrl::PreferLocalFile); if (suffix == "svg") { From a656a723db0f23801b53fc7be883b05164ff1de4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 23:22:32 -0500 Subject: [PATCH 683/762] Icons std::find_if cannot be vectorized on Intel macOS 13 --- scwx-qt/source/scwx/qt/gl/draw/icons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp index d7f6b64c..50e30a82 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp @@ -740,7 +740,7 @@ bool Icons::RunMousePicking( // For each pickable icon auto it = std::find_if( // - std::execution::par_unseq, + std::execution::par, p->currentHoverIcons_.crbegin(), p->currentHoverIcons_.crend(), [&mouseLocalCoords](const auto& icon) From 5262551e1d692cad5796c05503bc50aec4307bb2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 17 Jun 2025 23:39:36 -0500 Subject: [PATCH 684/762] Set a macOS deployment target of 12.0 --- CMakeLists.txt | 2 ++ tools/conan/profiles/scwx-macos_clang-18 | 1 + tools/conan/profiles/scwx-macos_clang-18_armv8 | 1 + 3 files changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a2da515..d04b97fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ set(PROJECT_NAME supercell-wx) include(tools/scwx_config.cmake) +set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0) + scwx_python_setup() project(${PROJECT_NAME} diff --git a/tools/conan/profiles/scwx-macos_clang-18 b/tools/conan/profiles/scwx-macos_clang-18 index 70889284..f7a5b0ce 100644 --- a/tools/conan/profiles/scwx-macos_clang-18 +++ b/tools/conan/profiles/scwx-macos_clang-18 @@ -6,3 +6,4 @@ compiler.cppstd=20 compiler.libcxx=libc++ compiler.version=18 os=Macos +os.version=12.0 diff --git a/tools/conan/profiles/scwx-macos_clang-18_armv8 b/tools/conan/profiles/scwx-macos_clang-18_armv8 index 63c6d597..65d9afd4 100644 --- a/tools/conan/profiles/scwx-macos_clang-18_armv8 +++ b/tools/conan/profiles/scwx-macos_clang-18_armv8 @@ -6,3 +6,4 @@ compiler.cppstd=20 compiler.libcxx=libc++ compiler.version=18 os=Macos +os.version=12.0 From 2a24716d7790595d1387b38cd0ab0236d4cee759 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 18 Jun 2025 21:46:17 -0500 Subject: [PATCH 685/762] UpdateManager tests are unreliable on CI due to rate limiting --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3d56548..6e63e8d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -381,7 +381,7 @@ jobs: cd build cpack - - name: Upload Disk Image (macOs) + - name: Upload Disk Image (macOS) if: ${{ startsWith(matrix.os, 'macos') }} uses: actions/upload-artifact@v4 with: @@ -393,7 +393,7 @@ jobs: env: MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }} MAPTILER_API_KEY: ${{ secrets.MAPTILER_API_KEY }} - run: ctest -C ${{ matrix.build_type }} --exclude-regex test_mln.* + run: ctest -C ${{ matrix.build_type }} --exclude-regex "test_mln.*|UpdateManager.*" - name: Upload Test Logs if: ${{ !cancelled() }} From 55e797907089c8a34ae9b6e923f0522b541efd44 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 18 Jun 2025 21:54:23 -0500 Subject: [PATCH 686/762] Add macOS to README.md --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 01f2ced6..d5995d2f 100644 --- a/README.md +++ b/README.md @@ -28,31 +28,32 @@ Supercell Wx supports the following 64-bit operating systems: - Ubuntu 22.04+ - NixOS 25.05+ - Most distributions supporting the GCC Standard C++ Library 11+ - +- macOS 12 (Monterey) or later + ## Linux Dependencies Supercell Wx requires the following Linux dependencies: -- Linux/X11 (Wayland works too) with support for GCC 11 and OpenGL 3.3 +- Linux/X11 (Wayland works too) with support for GCC 11, OpenGL 3.3 and OpenGL ES 3.0 - X11/XCB libraries including xcb-cursor - + ## FAQ Frequently asked questions: - Q: Why is the map black when loading for the first time? - + - A. You must obtain a free API key from either (or both) [MapTiler](https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/) which currently does not require a credit/debit card, or [Mapbox](https://account.mapbox.com/) which ***does*** require a credit/debit card, but as of writing, you will receive 200K free requests per month, which should be sufficient for an individual user. - Q: Why is it that when I change my color table, API key, grid width/height settings, nothing happens after hitting apply? - A. As of right now, you must restart Supercell Wx in order to apply these changes. In future iterations, this will no longer be an issue. - + - Q. Is it possible to get dark mode? - + - A. In Windows, make sure to set the flag `-style fusion` at the end of the target path of the .exe - Example: `C:\Users\Administrator\Desktop\Supercell-Wx\bin\supercell-wx.exe -style fusion` - A. In Linux, if you're using KDE, Supercell Wx should automatically follow your theme settings. - + - Q: How can I contribute? - A. Head to [Developer Setup](https://supercell-wx.readthedocs.io/en/stable/development/developer-setup.html) and [Contributing](CONTRIBUTING.md) to configure the Supercell Wx development environment for your IDE. Currently Visual Studio and Visual Studio Code are recommended, with other IDEs remaining untested at this time. From ceb58c1a4ce9c99843e3633b1b4e099d47f7542c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 18 Jun 2025 21:54:47 -0500 Subject: [PATCH 687/762] Update audio codec setup page for Mac --- scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp index dd86e1f6..364c9959 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp @@ -124,6 +124,9 @@ void AudioCodecPage::Impl::SetInstructionsLabelText() self_, [](const QString& link) { QDesktopServices::openUrl(QUrl {link}); }); +#elif defined(__APPLE__) + instructionsLabel_->setText(tr( + "Please see the instructions for your Mac for installing media codecs.")); #else instructionsLabel_->setText( tr("Please see the instructions for your Linux distribution for " From 559f57759706d915dcd4e04e2fe6a1e4a110eac3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 18 Jun 2025 23:56:46 -0500 Subject: [PATCH 688/762] Correct bundle info plist variable name --- scwx-qt/scwx-qt.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 613f8ae8..4bffdc2e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -637,7 +637,7 @@ elseif (APPLE) set_target_properties(supercell-wx PROPERTIES MACOSX_BUNDLE TRUE - MACOSX_BUNDLE_INFO_LIST "${scwx-qt_SOURCE_DIR}/res/scwx-qt.plist.in" + MACOSX_BUNDLE_INFO_PLIST "${scwx-qt_SOURCE_DIR}/res/scwx-qt.plist.in" MACOSX_BUNDLE_GUI_IDENTIFIER "net.supercellwx.app" MACOSX_BUNDLE_BUNDLE_NAME "Supercell Wx" MACOSX_BUNDLE_BUNDLE_VERSION "${SCWX_VERSION}" From ec06cc62e116c00646184d58fe7c526c088a18cc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 22 Jun 2025 17:50:36 -0500 Subject: [PATCH 689/762] Don't query for available products when disabling product refresh --- .../scwx/qt/manager/radar_product_manager.cpp | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index d2bc15d0..b12df441 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -683,29 +683,36 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, p->GetLevel3ProviderManager(product); // Only enable refresh on available products - boost::asio::post( - p->threadPool_, - [=, this]() - { - try + if (enabled) + { + boost::asio::post( + p->threadPool_, + [=, this]() { - providerManager->provider_->RequestAvailableProducts(); - auto availableProducts = - providerManager->provider_->GetAvailableProducts(); - - if (std::find(std::execution::par, - availableProducts.cbegin(), - availableProducts.cend(), - product) != availableProducts.cend()) + try { - p->EnableRefresh(uuid, {providerManager}, enabled); + providerManager->provider_->RequestAvailableProducts(); + auto availableProducts = + providerManager->provider_->GetAvailableProducts(); + + if (std::find(std::execution::par, + availableProducts.cbegin(), + availableProducts.cend(), + product) != availableProducts.cend()) + { + p->EnableRefresh(uuid, {providerManager}, enabled); + } } - } - catch (const std::exception& ex) - { - logger_->error(ex.what()); - } - }); + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); + } + else + { + p->EnableRefresh(uuid, {providerManager}, enabled); + } } } From e51fd8b77b9450e6ff99475a852420a61f970b69 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 22 Jun 2025 22:31:14 -0500 Subject: [PATCH 690/762] macOS does not populate the the OpenGL 3.0 compatibility functions --- scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp | 7 +++++++ scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp | 7 +++++++ scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp | 7 +++++++ scwx-qt/source/scwx/qt/gl/gl_context.cpp | 2 ++ scwx-qt/source/scwx/qt/gl/gl_context.hpp | 3 +++ 5 files changed, 26 insertions(+) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp index b321e149..c86007f9 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -162,7 +162,10 @@ void PlacefileIcons::set_thresholded(bool thresholded) void PlacefileIcons::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); + +#if !defined(__APPLE__) auto& gl30 = p->context_->gl30(); +#endif p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, @@ -253,7 +256,11 @@ void PlacefileIcons::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed +#if !defined(__APPLE__) gl30.glVertexAttribI1i(7, 1); +#else + glVertexAttribI1i(7, 1); +#endif p->dirty_ = true; } diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp index aafaef8d..d7dddf68 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp @@ -140,7 +140,10 @@ void PlacefileImages::set_thresholded(bool thresholded) void PlacefileImages::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); + +#if !defined(__APPLE__) auto& gl30 = p->context_->gl30(); +#endif p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, @@ -222,7 +225,11 @@ void PlacefileImages::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed +#if !defined(__APPLE__) gl30.glVertexAttribI1i(7, 1); +#else + glVertexAttribI1i(7, 1); +#endif p->dirty_ = true; } diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index 6ec2750a..d9c49085 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -128,7 +128,10 @@ void PlacefileLines::set_thresholded(bool thresholded) void PlacefileLines::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); + +#if !defined(__APPLE__) auto& gl30 = p->context_->gl30(); +#endif p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, @@ -207,7 +210,11 @@ void PlacefileLines::Initialize() gl.glEnableVertexAttribArray(6); // aDisplayed +#if !defined(__APPLE__) gl30.glVertexAttribI1i(7, 1); +#else + glVertexAttribI1i(7, 1); +#endif p->dirty_ = true; } diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 9fd6bd85..b2cbbde3 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -56,10 +56,12 @@ gl::OpenGLFunctions& GlContext::gl() return *p->gl_; } +#if !defined(__APPLE__) QOpenGLFunctions_3_0& GlContext::gl30() { return *p->gl30_; } +#endif std::uint64_t GlContext::texture_buffer_count() const { diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.hpp b/scwx-qt/source/scwx/qt/gl/gl_context.hpp index b4a6a866..b506fca1 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.hpp @@ -25,7 +25,10 @@ public: GlContext& operator=(GlContext&&) noexcept; gl::OpenGLFunctions& gl(); + +#if !defined(__APPLE__) QOpenGLFunctions_3_0& gl30(); +#endif std::uint64_t texture_buffer_count() const; From b89938b48a53f2aac17c6f42bf45867e386523df Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Jun 2025 13:14:27 +0000 Subject: [PATCH 691/762] Build using macos 14 instead of 15 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e63e8d7..0f6c17dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,7 +109,7 @@ jobs: appimage_arch: '' artifact_suffix: macos-x64 - name: macos_clang18_arm64 - os: macos-15 + os: macos-14 build_type: Release env_cc: clang env_cxx: clang++ From 76c6ac2ccd4eedef7be9624ebee2d1acd41da22a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Jun 2025 23:09:37 -0500 Subject: [PATCH 692/762] Update setup script naming convention --- ...up-debug.sh => setup-linux-ninja-debug.sh} | 0 ...up-multi.sh => setup-linux-ninja-multi.sh} | 3 +- ...elease.sh => setup-linux-ninja-release.sh} | 0 ...os-debug.sh => setup-macos-ninja-debug.sh} | 0 ...elease.sh => setup-macos-ninja-release.sh} | 0 tools/setup-macos-xcode-debug.sh | 30 +++++++++++++++++++ tools/setup-macos-xcode-multi.sh | 29 ++++++++++++++++++ tools/setup-macos-xcode-release.sh | 30 +++++++++++++++++++ ...2.bat => setup-windows-msvc2022-debug.bat} | 0 ...2.bat => setup-windows-msvc2022-multi.bat} | 0 ...bat => setup-windows-msvc2022-release.bat} | 0 ...inja.bat => setup-windows-ninja-debug.bat} | 0 ...inja.bat => setup-windows-ninja-multi.bat} | 0 ...ja.bat => setup-windows-ninja-release.bat} | 0 14 files changed, 91 insertions(+), 1 deletion(-) rename tools/{setup-debug.sh => setup-linux-ninja-debug.sh} (100%) rename tools/{setup-multi.sh => setup-linux-ninja-multi.sh} (83%) rename tools/{setup-release.sh => setup-linux-ninja-release.sh} (100%) rename tools/{setup-macos-debug.sh => setup-macos-ninja-debug.sh} (100%) rename tools/{setup-macos-release.sh => setup-macos-ninja-release.sh} (100%) create mode 100755 tools/setup-macos-xcode-debug.sh create mode 100755 tools/setup-macos-xcode-multi.sh create mode 100755 tools/setup-macos-xcode-release.sh rename tools/{setup-debug-msvc2022.bat => setup-windows-msvc2022-debug.bat} (100%) rename tools/{setup-multi-msvc2022.bat => setup-windows-msvc2022-multi.bat} (100%) rename tools/{setup-release-msvc2022.bat => setup-windows-msvc2022-release.bat} (100%) rename tools/{setup-debug-ninja.bat => setup-windows-ninja-debug.bat} (100%) rename tools/{setup-multi-ninja.bat => setup-windows-ninja-multi.bat} (100%) rename tools/{setup-release-ninja.bat => setup-windows-ninja-release.bat} (100%) diff --git a/tools/setup-debug.sh b/tools/setup-linux-ninja-debug.sh similarity index 100% rename from tools/setup-debug.sh rename to tools/setup-linux-ninja-debug.sh diff --git a/tools/setup-multi.sh b/tools/setup-linux-ninja-multi.sh similarity index 83% rename from tools/setup-multi.sh rename to tools/setup-linux-ninja-multi.sh index 05a6cad5..85bb9a97 100755 --- a/tools/setup-multi.sh +++ b/tools/setup-linux-ninja-multi.sh @@ -1,11 +1,12 @@ #!/bin/bash script_dir="$(dirname "$(readlink -f "$0")")" -export build_dir="$(readlink -f "${1:-${script_dir}/../build-debug}")" +export build_dir="$(readlink -f "${1:-${script_dir}/../build-multi}")" export conan_profile=${2:-scwx-linux_gcc-11} export generator="Ninja Multi-Config" export qt_base=/opt/Qt export qt_arch=gcc_64 +export address_sanitizer=${4:-disabled} # Assign user-specified Python Virtual Environment [ "${3:-}" = "none" ] && unset venv_path || export venv_path="$(readlink -f "${3:-${script_dir}/../.venv}")" diff --git a/tools/setup-release.sh b/tools/setup-linux-ninja-release.sh similarity index 100% rename from tools/setup-release.sh rename to tools/setup-linux-ninja-release.sh diff --git a/tools/setup-macos-debug.sh b/tools/setup-macos-ninja-debug.sh similarity index 100% rename from tools/setup-macos-debug.sh rename to tools/setup-macos-ninja-debug.sh diff --git a/tools/setup-macos-release.sh b/tools/setup-macos-ninja-release.sh similarity index 100% rename from tools/setup-macos-release.sh rename to tools/setup-macos-ninja-release.sh diff --git a/tools/setup-macos-xcode-debug.sh b/tools/setup-macos-xcode-debug.sh new file mode 100755 index 00000000..267a4463 --- /dev/null +++ b/tools/setup-macos-xcode-debug.sh @@ -0,0 +1,30 @@ +#!/bin/bash +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" + +export build_dir="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${1:-${script_dir}/../build-xcode-debug}")" +export build_type=Debug +export conan_profile=${2:-scwx-macos_clang-18_armv8} +export generator=Xcode +export qt_base="/Users/${USER}/Qt" +export qt_arch=macos +export address_sanitizer=${4:-disabled} + +# Set explicit compiler paths +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +export PATH="$(brew --prefix llvm@18)/bin:$PATH" + +export LDFLAGS="-L$(brew --prefix llvm@18)/lib -L$(brew --prefix llvm@18)/lib/c++" +export CPPFLAGS="-I$(brew --prefix llvm@18)/include" + +# Assign user-specified Python Virtual Environment +if [ "${3:-}" = "none" ]; then + unset venv_path +else + # macOS does not have 'readlink -f', use python for realpath + export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" +fi + +# Perform common setup +"${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-macos-xcode-multi.sh b/tools/setup-macos-xcode-multi.sh new file mode 100755 index 00000000..a5de4d35 --- /dev/null +++ b/tools/setup-macos-xcode-multi.sh @@ -0,0 +1,29 @@ +#!/bin/bash +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" + +export build_dir="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${1:-${script_dir}/../build-xcode}")" +export conan_profile=${2:-scwx-macos_clang-18_armv8} +export generator=Xcode +export qt_base=/opt/Qt +export qt_arch=gcc_64 +export address_sanitizer=${4:-disabled} + +# Set explicit compiler paths +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +export PATH="$(brew --prefix llvm@18)/bin:$PATH" + +export LDFLAGS="-L$(brew --prefix llvm@18)/lib -L$(brew --prefix llvm@18)/lib/c++" +export CPPFLAGS="-I$(brew --prefix llvm@18)/include" + +# Assign user-specified Python Virtual Environment +if [ "${3:-}" = "none" ]; then + unset venv_path +else + # macOS does not have 'readlink -f', use python for realpath + export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" +fi + +# Perform common setup +"${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-macos-xcode-release.sh b/tools/setup-macos-xcode-release.sh new file mode 100755 index 00000000..f9ba914e --- /dev/null +++ b/tools/setup-macos-xcode-release.sh @@ -0,0 +1,30 @@ +#!/bin/bash +script_source="${BASH_SOURCE[0]:-$0}" +script_dir="$(cd "$(dirname "${script_source}")" && pwd)" + +export build_dir="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${1:-${script_dir}/../build-xcode-release}")" +export build_type=Release +export conan_profile=${2:-scwx-macos_clang-18_armv8} +export generator=Xcode +export qt_base="/Users/${USER}/Qt" +export qt_arch=macos +export address_sanitizer=${4:-disabled} + +# Set explicit compiler paths +export CC=$(brew --prefix llvm@18)/bin/clang +export CXX=$(brew --prefix llvm@18)/bin/clang++ +export PATH="$(brew --prefix llvm@18)/bin:$PATH" + +export LDFLAGS="-L$(brew --prefix llvm@18)/lib -L$(brew --prefix llvm@18)/lib/c++" +export CPPFLAGS="-I$(brew --prefix llvm@18)/include" + +# Assign user-specified Python Virtual Environment +if [ "${3:-}" = "none" ]; then + unset venv_path +else + # macOS does not have 'readlink -f', use python for realpath + export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" +fi + +# Perform common setup +"${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-debug-msvc2022.bat b/tools/setup-windows-msvc2022-debug.bat similarity index 100% rename from tools/setup-debug-msvc2022.bat rename to tools/setup-windows-msvc2022-debug.bat diff --git a/tools/setup-multi-msvc2022.bat b/tools/setup-windows-msvc2022-multi.bat similarity index 100% rename from tools/setup-multi-msvc2022.bat rename to tools/setup-windows-msvc2022-multi.bat diff --git a/tools/setup-release-msvc2022.bat b/tools/setup-windows-msvc2022-release.bat similarity index 100% rename from tools/setup-release-msvc2022.bat rename to tools/setup-windows-msvc2022-release.bat diff --git a/tools/setup-debug-ninja.bat b/tools/setup-windows-ninja-debug.bat similarity index 100% rename from tools/setup-debug-ninja.bat rename to tools/setup-windows-ninja-debug.bat diff --git a/tools/setup-multi-ninja.bat b/tools/setup-windows-ninja-multi.bat similarity index 100% rename from tools/setup-multi-ninja.bat rename to tools/setup-windows-ninja-multi.bat diff --git a/tools/setup-release-ninja.bat b/tools/setup-windows-ninja-release.bat similarity index 100% rename from tools/setup-release-ninja.bat rename to tools/setup-windows-ninja-release.bat From f684d62cb1ed0237164f703e1c0d7b486be4a2cd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 29 Jun 2025 23:12:06 -0500 Subject: [PATCH 693/762] Use debug conan profiles for debug configurations --- CMakePresets.json | 68 +++++++++++++++++++-------------- tools/configure-environment.bat | 18 ++++++++- tools/configure-environment.sh | 18 +++++++++ 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 056a32b9..9fec9614 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -54,9 +54,7 @@ "inherits": "windows-x64-base", "hidden": true, "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", - "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", - "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64" } }, { @@ -65,9 +63,7 @@ "hidden": true, "generator": "Ninja", "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64", - "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", - "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" + "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64" } }, { @@ -75,9 +71,7 @@ "inherits": "linux-base", "hidden": true, "cacheVariables": { - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64", - "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", - "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11" + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64" }, "environment": { "CC": "gcc-11", @@ -93,7 +87,9 @@ "strategy": "external" }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64-debug", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64-debug" } }, { @@ -105,7 +101,9 @@ "strategy": "external" }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" } }, { @@ -113,7 +111,9 @@ "inherits": "windows-msvc2022-x64-ninja-base", "displayName": "Windows MSVC 2022 x64 Ninja Debug", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64-debug", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64-debug" } }, { @@ -121,7 +121,9 @@ "inherits": "windows-msvc2022-x64-ninja-base", "displayName": "Windows MSVC 2022 x64 Ninja Release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-windows_msvc2022_x64", + "CONAN_BUILD_PROFILE": "scwx-windows_msvc2022_x64" } }, { @@ -130,7 +132,9 @@ "displayName": "Linux GCC Debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx" + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11-debug", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11-debug" } }, { @@ -139,7 +143,9 @@ "displayName": "Linux GCC Release", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx" + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11" } }, { @@ -149,6 +155,8 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Debug/supercell-wx", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11-debug", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11-debug", "SCWX_ADDRESS_SANITIZER": { "type": "BOOL", "value": "ON" @@ -162,6 +170,8 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/${presetName}/Release/supercell-wx", + "CONAN_HOST_PROFILE": "scwx-linux_gcc-11", + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11", "SCWX_ADDRESS_SANITIZER": { "type": "BOOL", "value": "ON" @@ -202,9 +212,9 @@ "displayName": "CI Linux GCC ARM64", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", + "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64", "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8", - "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8", - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64" + "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8" }, "environment": { "CC": "gcc-11", @@ -233,10 +243,6 @@ "name": "macos-clang18-x64-base", "inherits": "macos-clang18-base", "hidden": true, - "cacheVariables": { - "CONAN_HOST_PROFILE": "scwx-macos_clang-18", - "CONAN_BUILD_PROFILE": "scwx-macos_clang-18" - }, "environment": { "CC": "/usr/local/opt/llvm@18/bin/clang", "CXX": "/usr/local/opt/llvm@18/bin/clang++", @@ -249,10 +255,6 @@ "name": "macos-clang18-arm64-base", "inherits": "macos-clang18-base", "hidden": true, - "cacheVariables": { - "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8", - "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8" - }, "environment": { "CC": "/opt/homebrew/opt/llvm@18/bin/clang", "CXX": "/opt/homebrew/opt/llvm@18/bin/clang++", @@ -266,7 +268,9 @@ "inherits": "macos-clang18-x64-base", "displayName": "macOS Clang 18 x64 Debug", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "CONAN_HOST_PROFILE": "scwx-macos_clang-18-debug", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18-debug" } }, { @@ -274,7 +278,9 @@ "inherits": "macos-clang18-x64-base", "displayName": "macOS Clang 18 x64 Release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-macos_clang-18", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18" } }, { @@ -282,7 +288,9 @@ "inherits": "macos-clang18-arm64-base", "displayName": "macOS Clang 18 Arm64 Debug", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" + "CMAKE_BUILD_TYPE": "Debug", + "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8-debug", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8-debug" } }, { @@ -290,7 +298,9 @@ "inherits": "macos-clang18-arm64-base", "displayName": "macOS Clang 18 Arm64 Release", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "Release", + "CONAN_HOST_PROFILE": "scwx-macos_clang-18_armv8", + "CONAN_BUILD_PROFILE": "scwx-macos_clang-18_armv8" } } ], diff --git a/tools/configure-environment.bat b/tools/configure-environment.bat index d386ecc0..bf9118a4 100644 --- a/tools/configure-environment.bat +++ b/tools/configure-environment.bat @@ -33,8 +33,24 @@ pip install --upgrade -r "%script_dir%\..\requirements.txt" :: Install Conan profiles @for /L %%i in (0,1,!last_profile!) do @( + :: Install the base profile set "profile_name=!conan_profile[%%i]!" - conan config install "%script_dir%\conan\profiles\!profile_name!" -tf profiles + set "profile_path=%script_dir%\conan\profiles\!profile_name!" + conan config install "!profile_path!" -tf profiles + + :: Create debug profile in temp directory + set "debug_profile_name=!profile_name!-debug" + set "debug_profile_path=%TEMP%\!debug_profile_name!" + copy "!profile_path!" "!debug_profile_path!" >nul + + :: Replace build_type=Release with build_type=Debug + powershell -Command "(Get-Content '!debug_profile_path!') -replace 'build_type=Release', 'build_type=Debug' | Set-Content '!debug_profile_path!'" + + :: Install the debug profile + conan config install "!debug_profile_path!" -tf profiles + + :: Remove temporary debug profile + del "!debug_profile_path!" ) :: Deactivate Python Virtual Environment diff --git a/tools/configure-environment.sh b/tools/configure-environment.sh index 1dfc714c..f88ef504 100755 --- a/tools/configure-environment.sh +++ b/tools/configure-environment.sh @@ -71,7 +71,25 @@ fi # Install Conan profiles for profile_name in "${conan_profiles[@]}"; do + # Install original profile conan config install "${script_dir}/conan/profiles/${profile_name}" -tf profiles + + # Create debug profile in temp directory + debug_profile="/tmp/${profile_name}-debug" + cp "${script_dir}/conan/profiles/${profile_name}" "${debug_profile}" + + # Replace build_type=Release with build_type=Debug + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' 's/build_type=Release/build_type=Debug/g' "${debug_profile}" + else + sed -i 's/build_type=Release/build_type=Debug/g' "${debug_profile}" + fi + + # Install the debug profile + conan config install "${debug_profile}" -tf profiles + + # Remove temporary debug profile + rm "${debug_profile}" done # Deactivate Python Virtual Environment From 9eea0a4dc827d62b8b39c77f176ae74e0112f06a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 20:19:30 -0500 Subject: [PATCH 694/762] Updating minimum requirements for macOS --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d5995d2f..81b0e698 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ Supercell Wx supports the following 64-bit operating systems: - Ubuntu 22.04+ - NixOS 25.05+ - Most distributions supporting the GCC Standard C++ Library 11+ -- macOS 12 (Monterey) or later +- macOS + - 13.6+ for Intel-based Macs + - 14.0+ for Apple silicon-based Macs ## Linux Dependencies From 5716d89d52d430e2a97f0d3f876ba38d68a23941 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 21:42:50 -0500 Subject: [PATCH 695/762] On macOS, set font pixel size instead of point size to prevent tiny display --- scwx-qt/source/scwx/qt/manager/font_manager.cpp | 6 ++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index d92d3bd7..c1553418 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -191,7 +191,13 @@ void FontManager::Impl::UpdateQFont(types::FontCategory fontCategory) QFont font = QFontDatabase::font(QString::fromStdString(family), QString::fromStdString(styles), static_cast(size.value())); + +#if !defined(__APPLE__) font.setPointSizeF(size.value()); +#else + const units::font_size::pixels pixelSize {size}; + font.setPixelSize(static_cast(pixelSize.value())); +#endif fontCategoryQFontMap_.insert_or_assign(fontCategory, font); } diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 4f63743f..81e3a96d 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -1496,6 +1496,12 @@ void SettingsDialogImpl::UpdateFontDisplayData() self_->ui->fontStyleLabel->setText(font.styleName()); self_->ui->fontSizeLabel->setText(QString::number(font.pointSizeF())); +#if defined(__APPLE__) + const units::font_size::points fontSize {font.pointSizeF()}; + const units::font_size::pixels fontPixels {fontSize}; + font.setPixelSize(static_cast(fontPixels.value())); +#endif + self_->ui->fontPreviewLabel->setFont(font); if (selectedFontCategory_ != types::FontCategory::Unknown) From 5f661df9fddfbff8792d719d23190f5e4cc4b8c9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 21:43:14 -0500 Subject: [PATCH 696/762] Alert dialog should display a fixed pitch font --- scwx-qt/source/scwx/qt/ui/alert_dialog.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/alert_dialog.cpp b/scwx-qt/source/scwx/qt/ui/alert_dialog.cpp index 76aa284a..3d9d1af5 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/alert_dialog.cpp @@ -54,7 +54,13 @@ AlertDialog::AlertDialog(QWidget* parent) : // Set monospace font for alert view QFont monospaceFont("?"); - monospaceFont.setStyleHint(QFont::TypeWriter); + monospaceFont.setStyleHint(QFont::StyleHint::TypeWriter); + + if (!monospaceFont.fixedPitch()) + { + monospaceFont.setStyleHint(QFont::StyleHint::Monospace); + } + ui->alertText->setFont(monospaceFont); // Add Go button to button box From 847c5d951e3fadc3f150d3de28e95ff0cc47d6e7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 22:04:55 -0500 Subject: [PATCH 697/762] Don't report shader warnings as errors --- scwx-qt/source/scwx/qt/gl/shader_program.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp index 4da07a32..b4855ca9 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp @@ -138,7 +138,7 @@ bool ShaderProgram::Load( } else if (logLength > 0) { - logger_->error("Shader compiled with warnings: {}", infoLog); + logger_->warn("Shader compiled with warnings: {}", infoLog); } } @@ -160,7 +160,7 @@ bool ShaderProgram::Load( } else if (logLength > 0) { - logger_->error("Shader program linked with warnings: {}", infoLog); + logger_->warn("Shader program linked with warnings: {}", infoLog); } } From 27501682876d91839e62f0d70fc8fc19c3843574 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 22:25:42 -0500 Subject: [PATCH 698/762] Fix display of placefile polygons and triangles on macOS --- scwx-qt/gl/map_color.vert | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/gl/map_color.vert b/scwx-qt/gl/map_color.vert index 6ae98e92..609d1c34 100644 --- a/scwx-qt/gl/map_color.vert +++ b/scwx-qt/gl/map_color.vert @@ -26,6 +26,9 @@ void main() // Always set displayed to true vsOut.displayed = 1; + // Initialize texCoord to default value + vsOut.texCoord = vec3(0.0f, 0.0f, 0.0f); + // Pass the threshold and time range to the geometry shader vsOut.threshold = aThreshold; vsOut.timeRange = aTimeRange; From 3a8cdb8ea1c26691424b4b4fb8d3e21a51b63357 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 22:48:13 -0500 Subject: [PATCH 699/762] Add build numbers to non-Windows builds --- scwx-qt/scwx-qt.cmake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 4bffdc2e..c1dba7f2 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -577,7 +577,8 @@ else() -v ${SCWX_VERSION} -c ${VERSIONS_CACHE} -i ${VERSIONS_INPUT} - -o ${VERSIONS_HEADER}) + -o ${VERSIONS_HEADER} + -b ${SCWX_BUILD_NUM}) endif() add_custom_target(scwx-qt_generate_versions ALL From afcca785eb9fe20c172d613e1080f7058979f4ff Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 22:55:32 -0500 Subject: [PATCH 700/762] Formatting font_manager.cpp --- .../source/scwx/qt/manager/font_manager.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index c1553418..bfcc1242 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -139,22 +139,22 @@ void FontManager::Impl::ConnectSignals() }); } - QObject::connect( - &SettingsManager::Instance(), - &SettingsManager::SettingsSaved, - self_, - [this]() - { - std::scoped_lock lock {dirtyFontsMutex_, fontCategoryMutex_}; + QObject::connect(&SettingsManager::Instance(), + &SettingsManager::SettingsSaved, + self_, + [this]() + { + std::scoped_lock lock {dirtyFontsMutex_, + fontCategoryMutex_}; - for (auto fontCategory : dirtyFonts_) - { - UpdateImGuiFont(fontCategory); - UpdateQFont(fontCategory); - } + for (auto fontCategory : dirtyFonts_) + { + UpdateImGuiFont(fontCategory); + UpdateQFont(fontCategory); + } - dirtyFonts_.clear(); - }); + dirtyFonts_.clear(); + }); } void FontManager::InitializeFonts() From b8a0dae042797283ac937bbd8a4413c7105e77bc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 30 Jun 2025 23:00:58 -0500 Subject: [PATCH 701/762] Radar product manager clang-tidy cleanup --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index b12df441..13787496 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -679,7 +679,7 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, } else { - std::shared_ptr providerManager = + const std::shared_ptr providerManager = p->GetLevel3ProviderManager(product); // Only enable refresh on available products @@ -687,12 +687,12 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, { boost::asio::post( p->threadPool_, - [=, this]() + [providerManager, product, uuid, enabled, this]() { try { providerManager->provider_->RequestAvailableProducts(); - auto availableProducts = + const auto availableProducts = providerManager->provider_->GetAvailableProducts(); if (std::find(std::execution::par, From 788bd114cc3451ee97c2089d5034db3a6f4e3131 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Jul 2025 00:15:54 -0500 Subject: [PATCH 702/762] Update title font sizes for macOS --- scwx-qt/source/scwx/qt/ui/about_dialog.cpp | 12 +++++++++--- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp index bc24d056..9ae6ce18 100644 --- a/scwx-qt/source/scwx/qt/ui/about_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/about_dialog.cpp @@ -24,13 +24,19 @@ AboutDialog::AboutDialog(QWidget* parent) : p {std::make_unique()}, ui(new Ui::AboutDialog) { +#if !defined(__APPLE__) + static constexpr int titleFontSize = 14; +#else + static constexpr int titleFontSize = 18; +#endif + ui->setupUi(this); - int titleFontId = + const int titleFontId = manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); - QString titleFontFamily = + const QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); - QFont titleFont(titleFontFamily, 14); + const QFont titleFont(titleFontFamily, titleFontSize); ui->titleLabel->setFont(titleFont); QString repositoryUrl = diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 0ca61a18..6473a30b 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -42,13 +42,19 @@ public: UpdateDialog::UpdateDialog(QWidget* parent) : QDialog(parent), p {std::make_unique(this)}, ui(new Ui::UpdateDialog) { +#if !defined(__APPLE__) + static constexpr int titleFontSize = 12; +#else + static constexpr int titleFontSize = 16; +#endif + ui->setupUi(this); - int titleFontId = + const int titleFontId = manager::FontManager::Instance().GetFontId(types::Font::din1451alt_g); - QString titleFontFamily = + const QString titleFontFamily = QFontDatabase::applicationFontFamilies(titleFontId).at(0); - QFont titleFont(titleFontFamily, 12); + const QFont titleFont(titleFontFamily, titleFontSize); ui->bannerLabel->setFont(titleFont); ui->releaseNotesText->setOpenExternalLinks(true); From b2653fd585233257d4a26a340956694db1ebbb8a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Jul 2025 00:17:24 -0500 Subject: [PATCH 703/762] Remove raised styling from most QFrames This was never intended to be part of the UI, and did not show on Windows. It shows on Fusion, and is especially noticable on macOS. --- scwx-qt/source/scwx/qt/ui/about_dialog.ui | 20 ++++------- scwx-qt/source/scwx/qt/ui/alert_dialog.ui | 14 +++----- .../source/scwx/qt/ui/alert_dock_widget.ui | 10 ++---- .../scwx/qt/ui/animation_dock_widget.ui | 24 ------------- scwx-qt/source/scwx/qt/ui/county_dialog.ui | 10 ++---- .../source/scwx/qt/ui/imgui_debug_dialog.ui | 10 ++---- scwx-qt/source/scwx/qt/ui/layer_dialog.ui | 34 +++++-------------- scwx-qt/source/scwx/qt/ui/marker_dialog.ui | 10 ++---- .../scwx/qt/ui/marker_settings_widget.ui | 6 ---- scwx-qt/source/scwx/qt/ui/open_url_dialog.ui | 12 ++----- scwx-qt/source/scwx/qt/ui/placefile_dialog.ui | 10 ++---- .../scwx/qt/ui/placefile_settings_widget.ui | 8 +---- .../source/scwx/qt/ui/radar_site_dialog.ui | 12 ++----- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 32 +++-------------- scwx-qt/source/scwx/qt/ui/update_dialog.ui | 24 ++----------- scwx-qt/source/scwx/qt/ui/wfo_dialog.ui | 6 ---- 16 files changed, 43 insertions(+), 199 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/about_dialog.ui b/scwx-qt/source/scwx/qt/ui/about_dialog.ui index 64e8ed46..f8ef5270 100644 --- a/scwx-qt/source/scwx/qt/ui/about_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/about_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -44,7 +38,7 @@ :/res/icons/scwx-256.png - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -54,7 +48,7 @@ Supercell Wx - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -64,7 +58,7 @@ Version X.Y.Z - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -74,7 +68,7 @@ Git Revision 0000000000 - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -84,7 +78,7 @@ Copyright © 2021-YYYY Dan Paulat - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -94,10 +88,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/alert_dialog.ui b/scwx-qt/source/scwx/qt/ui/alert_dialog.ui index 55686925..f180dc71 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/alert_dialog.ui @@ -17,18 +17,12 @@ - QTextEdit::NoWrap + QTextEdit::LineWrapMode::NoWrap - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -108,7 +102,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -121,10 +115,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close + QDialogButtonBox::StandardButton::Close diff --git a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.ui index 96328278..317fc566 100644 --- a/scwx-qt/source/scwx/qt/ui/alert_dock_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/alert_dock_widget.ui @@ -30,12 +30,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -62,7 +56,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -85,7 +79,7 @@ :/res/icons/font-awesome-6/sliders-solid.svg:/res/icons/font-awesome-6/sliders-solid.svg - QToolButton::InstantPopup + QToolButton::ToolButtonPopupMode::InstantPopup diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui index 67203ffc..dbbed2a7 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.ui @@ -10,12 +10,6 @@ 276 - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -78,12 +72,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -119,12 +107,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -218,12 +200,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 1 diff --git a/scwx-qt/source/scwx/qt/ui/county_dialog.ui b/scwx-qt/source/scwx/qt/ui/county_dialog.ui index 71741c86..6a9a0257 100644 --- a/scwx-qt/source/scwx/qt/ui/county_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/county_dialog.ui @@ -29,12 +29,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -54,10 +48,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_dialog.ui b/scwx-qt/source/scwx/qt/ui/imgui_debug_dialog.ui index 5752a7c1..6b371769 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -54,10 +48,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close + QDialogButtonBox::StandardButton::Close diff --git a/scwx-qt/source/scwx/qt/ui/layer_dialog.ui b/scwx-qt/source/scwx/qt/ui/layer_dialog.ui index f9b2a076..45f8413e 100644 --- a/scwx-qt/source/scwx/qt/ui/layer_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/layer_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -41,16 +35,16 @@ true - QAbstractItemView::InternalMove + QAbstractItemView::DragDropMode::InternalMove - Qt::MoveAction + Qt::DropAction::MoveAction true - QAbstractItemView::ExtendedSelection + QAbstractItemView::SelectionMode::ExtendedSelection 0 @@ -59,12 +53,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -81,7 +69,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -138,7 +126,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -156,12 +144,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -188,7 +170,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -207,10 +189,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close|QDialogButtonBox::Reset + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Reset diff --git a/scwx-qt/source/scwx/qt/ui/marker_dialog.ui b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui index 6256b756..86e8fad3 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/marker_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -41,10 +35,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close + QDialogButtonBox::StandardButton::Close diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui index 12315d24..3804f318 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui @@ -29,12 +29,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 diff --git a/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui b/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui index a796d20d..117d62c1 100644 --- a/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/open_url_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -58,7 +52,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -71,10 +65,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui b/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui index 8ff045a6..aa3659bf 100644 --- a/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/placefile_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -41,10 +35,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close + QDialogButtonBox::StandardButton::Close diff --git a/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui index b95ab784..6355fdc0 100644 --- a/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/placefile_settings_widget.ui @@ -29,12 +29,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -61,7 +55,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal 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 74eae33b..bfaf7a5c 100644 --- a/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui @@ -17,7 +17,7 @@ - QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + QAbstractItemView::EditTrigger::CurrentChanged|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed|QAbstractItemView::EditTrigger::SelectedClicked true @@ -32,12 +32,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -64,10 +58,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index d67ef84c..aa12d3b8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -136,8 +130,8 @@ 0 0 - 511 - 873 + 513 + 845 @@ -810,8 +804,8 @@ 0 0 - 503 - 380 + 505 + 384 @@ -1197,12 +1191,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -1231,12 +1219,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 @@ -1376,12 +1358,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.ui b/scwx-qt/source/scwx/qt/ui/update_dialog.ui index 5aa8e054..84540fe9 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.ui @@ -16,12 +16,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -56,12 +50,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -113,12 +101,6 @@ - - QFrame::StyledPanel - - - QFrame::Raised - 0 @@ -149,7 +131,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -162,10 +144,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Ok diff --git a/scwx-qt/source/scwx/qt/ui/wfo_dialog.ui b/scwx-qt/source/scwx/qt/ui/wfo_dialog.ui index 26623489..cfb59664 100644 --- a/scwx-qt/source/scwx/qt/ui/wfo_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/wfo_dialog.ui @@ -29,12 +29,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - 0 From 7722eeb539ed52ee1799d2b3700a6b7e66fc6e33 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Jul 2025 00:22:02 -0500 Subject: [PATCH 704/762] Remove styling from radar info frame --- scwx-qt/source/scwx/qt/main/main_window.ui | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 94346c68..e760bab2 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 @@ -155,8 +155,8 @@ 0 0 - 205 - 701 + 191 + 703 @@ -174,12 +174,6 @@ - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - From bbb09f64f0f3c70c792712b3d422ec029c4622fc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Jul 2025 20:53:38 -0500 Subject: [PATCH 705/762] std::scoped_lock should be const in font_manager.cpp --- scwx-qt/source/scwx/qt/manager/font_manager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index bfcc1242..28c7b515 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -144,8 +144,8 @@ void FontManager::Impl::ConnectSignals() self_, [this]() { - std::scoped_lock lock {dirtyFontsMutex_, - fontCategoryMutex_}; + const std::scoped_lock lock {dirtyFontsMutex_, + fontCategoryMutex_}; for (auto fontCategory : dirtyFonts_) { From 1e821fdf6cdf111d38adcf1ca2b5418343b57827 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 1 Jul 2025 20:57:02 -0500 Subject: [PATCH 706/762] Xcode is not supported yet --- tools/setup-macos-xcode-debug.sh | 6 +++++- tools/setup-macos-xcode-multi.sh | 6 +++++- tools/setup-macos-xcode-release.sh | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tools/setup-macos-xcode-debug.sh b/tools/setup-macos-xcode-debug.sh index 267a4463..ced08867 100755 --- a/tools/setup-macos-xcode-debug.sh +++ b/tools/setup-macos-xcode-debug.sh @@ -26,5 +26,9 @@ else export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" fi +# FIXME: aws-sdk-cpp fails to configure using Xcode +echo "Xcode is not supported" +read -p "Press Enter to continue..." + # Perform common setup -"${script_dir}/lib/setup-common.sh" +# "${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-macos-xcode-multi.sh b/tools/setup-macos-xcode-multi.sh index a5de4d35..f706b072 100755 --- a/tools/setup-macos-xcode-multi.sh +++ b/tools/setup-macos-xcode-multi.sh @@ -25,5 +25,9 @@ else export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" fi +# FIXME: aws-sdk-cpp fails to configure using Xcode +echo "Xcode is not supported" +read -p "Press Enter to continue..." + # Perform common setup -"${script_dir}/lib/setup-common.sh" +# "${script_dir}/lib/setup-common.sh" diff --git a/tools/setup-macos-xcode-release.sh b/tools/setup-macos-xcode-release.sh index f9ba914e..ab788c7e 100755 --- a/tools/setup-macos-xcode-release.sh +++ b/tools/setup-macos-xcode-release.sh @@ -26,5 +26,9 @@ else export venv_path="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${3:-${script_dir}/../.venv}")" fi +# FIXME: aws-sdk-cpp fails to configure using Xcode +echo "Xcode is not supported" +read -p "Press Enter to continue..." + # Perform common setup -"${script_dir}/lib/setup-common.sh" +# "${script_dir}/lib/setup-common.sh" From 0744740f9a636f731c2640cd816155bd969c55d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:36:25 +0000 Subject: [PATCH 707/762] Update dependency libpng to v1.6.50 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index ea880677..908aab88 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "glm/1.0.1", "gtest/1.16.0", "libcurl/8.12.1", - "libpng/1.6.48", + "libpng/1.6.50", "libxml2/2.13.8", "openssl/3.5.0", "range-v3/0.12.0", From fa50defe6222588ca6dafac04a6500e6c8b06918 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 5 Jul 2025 14:49:24 -0500 Subject: [PATCH 708/762] Bump version to v0.5.0 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- tools/net.supercellwx.app.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f6c17dc..48b242b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.9 + SCWX_VERSION: v0.5.0 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index d04b97fe..5f48bb51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0) scwx_python_setup() project(${PROJECT_NAME} - VERSION 0.4.9 + VERSION 0.5.0 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -32,7 +32,7 @@ set_property(DIRECTORY set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.9") +set(SCWX_VERSION "0.5.0") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml index ced79027..bd40b1c4 100644 --- a/tools/net.supercellwx.app.yml +++ b/tools/net.supercellwx.app.yml @@ -1,5 +1,5 @@ id: net.supercellwx.app -version: '0.4.9' +version: '0.5.0' runtime: "org.freedesktop.Platform" runtime-version: "23.08" sdk: "org.freedesktop.Sdk" From 7723aee1d8c22e65c6c671aadb856818d8ef4ac1 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Tue, 8 Jul 2025 21:44:55 -0400 Subject: [PATCH 709/762] Initial attempt to disable Wayland platform on Nvidia --- scwx-qt/source/scwx/qt/main/main.cpp | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 7a1993f7..2aa3df31 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include @@ -42,6 +44,7 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static void ConfigureTheme(const std::vector& args); static void OverrideDefaultStyle(const std::vector& args); +static void OverridePlatform(); int main(int argc, char* argv[]) { @@ -52,6 +55,8 @@ int main(int argc, char* argv[]) args.push_back(argv[i]); } + OverridePlatform(); + // Initialize logger auto& logManager = scwx::qt::manager::LogManager::Instance(); logManager.Initialize(); @@ -252,3 +257,32 @@ OverrideDefaultStyle([[maybe_unused]] const std::vector& args) } #endif } + +constexpr std::string NVIDIA_ID = "0x10de"; +static void OverridePlatform() +{ +#if defined(__linux__) + namespace fs = std::filesystem; + for (const auto& entry : fs::directory_iterator("/sys/class/drm")) + { + if (!entry.is_directory() || + !entry.path().filename().string().starts_with("card")) + { + continue; + } + + auto vendorPath = entry.path() / "device" / "vendor"; + std::ifstream vendorFile(vendorPath); + std::string vendor; + if (vendorFile && std::getline(vendorFile, vendor)) + { + if (vendor == NVIDIA_ID) + { + // Force xcb on NVIDIA + setenv("QT_QPA_PLATFORM", "xcb", 1); + return; + } + } + } +#endif +} From d160ec2e28c7a372d00f708a4d375ccd3d640615 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Wed, 9 Jul 2025 07:42:00 -0400 Subject: [PATCH 710/762] Remove constexpr std::string in linux Nvidia code --- scwx-qt/source/scwx/qt/main/main.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 2aa3df31..25f4ee96 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -258,11 +258,11 @@ OverrideDefaultStyle([[maybe_unused]] const std::vector& args) #endif } -constexpr std::string NVIDIA_ID = "0x10de"; -static void OverridePlatform() +static void OverridePlatform() { #if defined(__linux__) - namespace fs = std::filesystem; + static const std::string NVIDIA_ID = "0x10de"; + namespace fs = std::filesystem; for (const auto& entry : fs::directory_iterator("/sys/class/drm")) { if (!entry.is_directory() || From 331b2d855f0ef6bb7b9f0a59ccb88d963860dd31 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 7 Jul 2025 22:44:01 -0500 Subject: [PATCH 711/762] Use GLEW instead of QOpenGLFunctions --- scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp | 22 +-- scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp | 2 +- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 182 +++++++++--------- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 162 ++++++++-------- scwx-qt/source/scwx/qt/gl/draw/icons.cpp | 150 +++++++-------- .../source/scwx/qt/gl/draw/linked_vectors.cpp | 2 +- .../scwx/qt/gl/draw/placefile_icons.cpp | 178 ++++++++--------- .../scwx/qt/gl/draw/placefile_images.cpp | 164 +++++++--------- .../scwx/qt/gl/draw/placefile_lines.cpp | 146 +++++++------- .../scwx/qt/gl/draw/placefile_polygons.cpp | 124 ++++++------ .../source/scwx/qt/gl/draw/placefile_text.cpp | 2 +- .../scwx/qt/gl/draw/placefile_triangles.cpp | 124 ++++++------ scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp | 70 +++---- scwx-qt/source/scwx/qt/gl/gl.hpp | 17 +- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 50 ++--- scwx-qt/source/scwx/qt/gl/gl_context.hpp | 8 - scwx-qt/source/scwx/qt/gl/shader_program.cpp | 40 ++-- scwx-qt/source/scwx/qt/gl/shader_program.hpp | 2 +- scwx-qt/source/scwx/qt/main/main.cpp | 30 +-- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 2 - .../source/scwx/qt/map/color_table_layer.cpp | 77 ++++---- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 9 +- scwx-qt/source/scwx/qt/map/map_widget.hpp | 1 + scwx-qt/source/scwx/qt/map/marker_layer.cpp | 2 - scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 7 +- .../scwx/qt/map/overlay_product_layer.cpp | 2 - .../source/scwx/qt/map/placefile_layer.cpp | 2 - .../scwx/qt/map/radar_product_layer.cpp | 120 ++++++------ .../source/scwx/qt/map/radar_site_layer.cpp | 2 - .../source/scwx/qt/ui/imgui_debug_widget.cpp | 10 +- .../source/scwx/qt/ui/imgui_debug_widget.hpp | 2 + scwx-qt/source/scwx/qt/util/texture_atlas.cpp | 34 ++-- scwx-qt/source/scwx/qt/util/texture_atlas.hpp | 2 +- 33 files changed, 788 insertions(+), 959 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp index c6737a10..6002d2ce 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp @@ -30,13 +30,11 @@ static const std::string logPrefix_ = "scwx::qt::gl::draw::draw_item"; class DrawItem::Impl { public: - explicit Impl(OpenGLFunctions& gl) : gl_ {gl} {} + explicit Impl() {} ~Impl() {} - - OpenGLFunctions& gl_; }; -DrawItem::DrawItem(OpenGLFunctions& gl) : p(std::make_unique(gl)) {} +DrawItem::DrawItem() : p(std::make_unique()) {} DrawItem::~DrawItem() = default; DrawItem::DrawItem(DrawItem&&) noexcept = default; @@ -74,7 +72,7 @@ void DrawItem::UseDefaultProjection( 0.0f, static_cast(params.height)); - p->gl_.glUniformMatrix4fv( + glUniformMatrix4fv( uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection)); } @@ -91,7 +89,7 @@ void DrawItem::UseRotationProjection( glm::radians(params.bearing), glm::vec3(0.0f, 0.0f, 1.0f)); - p->gl_.glUniformMatrix4fv( + glUniformMatrix4fv( uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(projection)); } @@ -100,16 +98,14 @@ void DrawItem::UseMapProjection( GLint uMVPMatrixLocation, GLint uMapScreenCoordLocation) { - OpenGLFunctions& gl = p->gl_; - const glm::mat4 uMVPMatrix = util::maplibre::GetMapMatrix(params); - gl.glUniform2fv(uMapScreenCoordLocation, - 1, - glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( - {params.latitude, params.longitude}))); + glUniform2fv(uMapScreenCoordLocation, + 1, + glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( + {params.latitude, params.longitude}))); - gl.glUniformMatrix4fv( + glUniformMatrix4fv( uMVPMatrixLocation, 1, GL_FALSE, glm::value_ptr(uMVPMatrix)); } diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp index 281b189a..28350190 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.hpp @@ -21,7 +21,7 @@ namespace draw class DrawItem { public: - explicit DrawItem(OpenGLFunctions& gl); + explicit DrawItem(); virtual ~DrawItem(); DrawItem(const DrawItem&) = delete; diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 743b0df9..5c1555ac 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -144,7 +144,7 @@ public: }; GeoIcons::GeoIcons(const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } GeoIcons::~GeoIcons() = default; @@ -165,8 +165,6 @@ void GeoIcons::set_thresholded(bool thresholded) void GeoIcons::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -181,87 +179,87 @@ void GeoIcons::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aLatLong - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); // aAngle - gl.glVertexAttribPointer(4, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(8 * sizeof(float))); - gl.glEnableVertexAttribArray(4); + glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + glEnableVertexAttribArray(4); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aTexCoord - gl.glVertexAttribPointer(2, - 3, - GL_FLOAT, - GL_FALSE, - kPointsPerTexCoord * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(2); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(5, // - 1, - GL_INT, - 0, - static_cast(0)); - gl.glEnableVertexAttribArray(5); + glVertexAttribIPointer(5, // + 1, + GL_INT, + 0, + static_cast(0)); + glEnableVertexAttribArray(5); // aTimeRange - gl.glVertexAttribIPointer(6, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(6); + glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(6); // aDisplayed - gl.glVertexAttribIPointer(7, - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(3 * sizeof(GLint))); - gl.glEnableVertexAttribArray(7); + glVertexAttribIPointer(7, + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(3 * sizeof(GLint))); + glEnableVertexAttribArray(7); p->dirty_ = true; } @@ -283,9 +281,7 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params, if (!p->currentIconList_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(textureAtlasChanged); p->shaderProgram_->Use(); @@ -298,12 +294,12 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params, // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -311,27 +307,25 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params, (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Interpolate texture coordinates - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void GeoIcons::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->iconMutex_}; @@ -847,8 +841,6 @@ void GeoIcons::Impl::UpdateModifiedIconBuffers() void GeoIcons::Impl::Update(bool textureAtlasChanged) { - gl::OpenGLFunctions& gl = context_->gl(); - UpdateModifiedIconBuffers(); // If the texture atlas has changed @@ -864,11 +856,11 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged) UpdateTextureBuffer(); // Buffer texture data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); lastTextureAtlasChanged_ = false; } @@ -877,18 +869,18 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged) if (dirty_) { // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentIconBuffer_.size(), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index aa4d2ccb..80bd6b2e 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -130,7 +130,7 @@ public: }; GeoLines::GeoLines(std::shared_ptr context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } GeoLines::~GeoLines() = default; @@ -151,8 +151,6 @@ void GeoLines::set_thresholded(bool thresholded) void GeoLines::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -167,78 +165,78 @@ void GeoLines::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * kLineBufferLength_, - nullptr, - GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * kLineBufferLength_, + nullptr, + GL_DYNAMIC_DRAW); // aLatLong - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); // aAngle - gl.glVertexAttribPointer(4, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(8 * sizeof(float))); - gl.glEnableVertexAttribArray(4); + glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + glEnableVertexAttribArray(4); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(5, // - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - static_cast(0)); - gl.glEnableVertexAttribArray(5); + glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + glEnableVertexAttribArray(5); // aTimeRange - gl.glVertexAttribIPointer(6, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(6); + glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(6); // aDisplayed - gl.glVertexAttribIPointer(7, - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(3 * sizeof(GLint))); - gl.glEnableVertexAttribArray(7); + glVertexAttribIPointer(7, + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(3 * sizeof(GLint))); + glEnableVertexAttribArray(7); p->dirty_ = true; } @@ -254,9 +252,7 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params) if (p->newLineList_.size() > 0) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(); p->shaderProgram_->Use(); @@ -269,12 +265,12 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params) // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -282,26 +278,24 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params) (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, - 0, - static_cast(p->currentLineList_.size() * - kVerticesPerRectangle)); + glDrawArrays(GL_TRIANGLES, + 0, + static_cast(p->currentLineList_.size() * + kVerticesPerRectangle)); } } void GeoLines::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->lineMutex_}; @@ -671,21 +665,19 @@ void GeoLines::Impl::Update() // If the lines have been updated if (dirty_) { - gl::OpenGLFunctions& gl = context_->gl(); - // Buffer lines data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentLinesBuffer_.size(), - currentLinesBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentLinesBuffer_.size(), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); } dirty_ = false; diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp index 50e30a82..ffbb7e2e 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp @@ -21,11 +21,11 @@ namespace draw static const std::string logPrefix_ = "scwx::qt::gl::draw::icons"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static constexpr std::size_t kNumRectangles = 1; -static constexpr std::size_t kNumTriangles = kNumRectangles * 2; -static constexpr std::size_t kVerticesPerTriangle = 3; -static constexpr std::size_t kPointsPerVertex = 10; -static constexpr std::size_t kPointsPerTexCoord = 3; +static constexpr std::size_t kNumRectangles = 1; +static constexpr std::size_t kNumTriangles = kNumRectangles * 2; +static constexpr std::size_t kVerticesPerTriangle = 3; +static constexpr std::size_t kPointsPerVertex = 10; +static constexpr std::size_t kPointsPerTexCoord = 3; static constexpr std::size_t kIconBufferLength = kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; static constexpr std::size_t kTextureBufferLength = @@ -116,7 +116,7 @@ public: }; Icons::Icons(const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } Icons::~Icons() = default; @@ -126,8 +126,6 @@ Icons& Icons::operator=(Icons&&) noexcept = default; void Icons::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/texture2d_array.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -135,69 +133,69 @@ void Icons::Initialize() p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aVertex - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); // aAngle - gl.glVertexAttribPointer(4, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(8 * sizeof(float))); - gl.glEnableVertexAttribArray(4); + glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + glEnableVertexAttribArray(4); // aDisplayed - gl.glVertexAttribPointer(5, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(9 * sizeof(float))); - gl.glEnableVertexAttribArray(5); + glVertexAttribPointer(5, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(9 * sizeof(float))); + glEnableVertexAttribArray(5); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aTexCoord - gl.glVertexAttribPointer(2, - 3, - GL_FLOAT, - GL_FALSE, - kPointsPerTexCoord * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(2); p->dirty_ = true; } @@ -219,29 +217,25 @@ void Icons::Render(const QMapLibre::CustomLayerRenderParameters& params, if (!p->currentIconList_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(textureAtlasChanged); p->shaderProgram_->Use(); UseDefaultProjection(params, p->uMVPMatrixLocation_); // Interpolate texture coordinates - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void Icons::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->iconMutex_}; @@ -679,8 +673,6 @@ void Icons::Impl::UpdateModifiedIconBuffers() void Icons::Impl::Update(bool textureAtlasChanged) { - gl::OpenGLFunctions& gl = context_->gl(); - UpdateModifiedIconBuffers(); // If the texture atlas has changed @@ -696,11 +688,11 @@ void Icons::Impl::Update(bool textureAtlasChanged) UpdateTextureBuffer(); // Buffer texture data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); lastTextureAtlasChanged_ = false; } @@ -709,11 +701,11 @@ void Icons::Impl::Update(bool textureAtlasChanged) if (dirty_) { // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentIconBuffer_.size(), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index 3dbab117..bc83ffe1 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -79,7 +79,7 @@ public: }; LinkedVectors::LinkedVectors(std::shared_ptr context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } LinkedVectors::~LinkedVectors() = default; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp index c86007f9..6de20db0 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -140,7 +140,7 @@ public: }; PlacefileIcons::PlacefileIcons(const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } PlacefileIcons::~PlacefileIcons() = default; @@ -161,12 +161,6 @@ void PlacefileIcons::set_thresholded(bool thresholded) void PlacefileIcons::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - -#if !defined(__APPLE__) - auto& gl30 = p->context_->gl30(); -#endif - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -181,86 +175,82 @@ void PlacefileIcons::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aLatLong - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); // aAngle - gl.glVertexAttribPointer(4, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(8 * sizeof(float))); - gl.glEnableVertexAttribArray(4); + glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + glEnableVertexAttribArray(4); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aTexCoord - gl.glVertexAttribPointer(2, - 3, - GL_FLOAT, - GL_FALSE, - kPointsPerTexCoord * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(2); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(5, // - 1, - GL_INT, - 0, - static_cast(0)); - gl.glEnableVertexAttribArray(5); + glVertexAttribIPointer(5, // + 1, + GL_INT, + 0, + static_cast(0)); + glEnableVertexAttribArray(5); // aTimeRange - gl.glVertexAttribIPointer(6, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(6); + glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(6); // aDisplayed -#if !defined(__APPLE__) - gl30.glVertexAttribI1i(7, 1); -#else glVertexAttribI1i(7, 1); -#endif p->dirty_ = true; } @@ -273,9 +263,7 @@ void PlacefileIcons::Render( if (!p->currentIconList_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(textureAtlasChanged); p->shaderProgram_->Use(); @@ -288,12 +276,12 @@ void PlacefileIcons::Render( // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -301,27 +289,25 @@ void PlacefileIcons::Render( (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Interpolate texture coordinates - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void PlacefileIcons::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->iconMutex_}; @@ -649,8 +635,6 @@ void PlacefileIcons::Impl::UpdateTextureBuffer() void PlacefileIcons::Impl::Update(bool textureAtlasChanged) { - gl::OpenGLFunctions& gl = context_->gl(); - // If the texture atlas has changed if (dirty_ || textureAtlasChanged) { @@ -664,29 +648,29 @@ void PlacefileIcons::Impl::Update(bool textureAtlasChanged) UpdateTextureBuffer(); // Buffer texture data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); } // If buffers need updating if (dirty_) { // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentIconBuffer_.size(), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp index d7dddf68..c8dbc194 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp @@ -117,7 +117,7 @@ public: }; PlacefileImages::PlacefileImages(const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } PlacefileImages::~PlacefileImages() = default; @@ -139,12 +139,6 @@ void PlacefileImages::set_thresholded(bool thresholded) void PlacefileImages::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - -#if !defined(__APPLE__) - auto& gl30 = p->context_->gl30(); -#endif - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -159,77 +153,73 @@ void PlacefileImages::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aLatLong - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aTexCoord - gl.glVertexAttribPointer(2, - 3, - GL_FLOAT, - GL_FALSE, - kPointsPerTexCoord * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 3, + GL_FLOAT, + GL_FALSE, + kPointsPerTexCoord * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(2); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(5, // - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - static_cast(0)); - gl.glEnableVertexAttribArray(5); + glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + glEnableVertexAttribArray(5); // aTimeRange - gl.glVertexAttribIPointer(6, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(6); + glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(6); // aDisplayed -#if !defined(__APPLE__) - gl30.glVertexAttribI1i(7, 1); -#else glVertexAttribI1i(7, 1); -#endif p->dirty_ = true; } @@ -242,9 +232,7 @@ void PlacefileImages::Render( if (!p->currentImageList_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(textureAtlasChanged); p->shaderProgram_->Use(); @@ -257,12 +245,12 @@ void PlacefileImages::Render( // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -270,27 +258,25 @@ void PlacefileImages::Render( (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Interpolate texture coordinates - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Draw images - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void PlacefileImages::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); std::unique_lock lock {p->imageMutex_}; @@ -446,8 +432,6 @@ void PlacefileImages::Impl::UpdateTextureBuffer() void PlacefileImages::Impl::Update(bool textureAtlasChanged) { - gl::OpenGLFunctions& gl = context_->gl(); - // If the texture atlas has changed if (dirty_ || textureAtlasChanged) { @@ -461,29 +445,29 @@ void PlacefileImages::Impl::Update(bool textureAtlasChanged) UpdateTextureBuffer(); // Buffer texture data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * textureBuffer_.size(), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); } // If buffers need updating if (dirty_) { // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentImageBuffer_.size(), - currentImageBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentImageBuffer_.size(), + currentImageBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentImageBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index d9c49085..44b81407 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -106,7 +106,7 @@ public: }; PlacefileLines::PlacefileLines(const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } PlacefileLines::~PlacefileLines() = default; @@ -127,12 +127,6 @@ void PlacefileLines::set_thresholded(bool thresholded) void PlacefileLines::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - -#if !defined(__APPLE__) - auto& gl30 = p->context_->gl30(); -#endif - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -147,74 +141,70 @@ void PlacefileLines::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(2, p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(2, p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aLatLong - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aModulate - gl.glVertexAttribPointer(3, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + glVertexAttribPointer(3, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(3); // aAngle - gl.glVertexAttribPointer(4, - 1, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(8 * sizeof(float))); - gl.glEnableVertexAttribArray(4); + glVertexAttribPointer(4, + 1, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(8 * sizeof(float))); + glEnableVertexAttribArray(4); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(5, // - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - static_cast(0)); - gl.glEnableVertexAttribArray(5); + glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + glEnableVertexAttribArray(5); // aTimeRange - gl.glVertexAttribIPointer(6, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(6); + glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(6); // aDisplayed -#if !defined(__APPLE__) - gl30.glVertexAttribI1i(7, 1); -#else glVertexAttribI1i(7, 1); -#endif p->dirty_ = true; } @@ -226,9 +216,7 @@ void PlacefileLines::Render( if (p->currentNumLines_ > 0) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(); p->shaderProgram_->Use(); @@ -241,12 +229,12 @@ void PlacefileLines::Render( // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -254,23 +242,21 @@ void PlacefileLines::Render( (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void PlacefileLines::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(2, p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(2, p->vbo_.data()); std::unique_lock lock {p->lineMutex_}; @@ -482,21 +468,19 @@ void PlacefileLines::Impl::Update() // If the placefile has been updated if (dirty_) { - gl::OpenGLFunctions& gl = context_->gl(); - // Buffer lines data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentLinesBuffer_.size(), - currentLinesBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentLinesBuffer_.size(), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); } dirty_ = false; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index e5c8addd..e0f36c0b 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -130,7 +130,7 @@ public: PlacefilePolygons::PlacefilePolygons( const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } PlacefilePolygons::~PlacefilePolygons() = default; @@ -152,8 +152,6 @@ void PlacefilePolygons::set_thresholded(bool thresholded) void PlacefilePolygons::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/map_color.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -168,58 +166,58 @@ void PlacefilePolygons::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(2, p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(2, p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aScreenCoord - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aColor - gl.glVertexAttribPointer(2, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(2); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(3, // - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - static_cast(0)); - gl.glEnableVertexAttribArray(3); + glVertexAttribIPointer(3, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + glEnableVertexAttribArray(3); // aTimeRange - gl.glVertexAttribIPointer(4, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(4); + glVertexAttribIPointer(4, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(4); p->dirty_ = true; } @@ -229,9 +227,7 @@ void PlacefilePolygons::Render( { if (!p->currentBuffer_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(); p->shaderProgram_->Use(); @@ -244,12 +240,12 @@ void PlacefilePolygons::Render( // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -257,23 +253,21 @@ void PlacefilePolygons::Render( (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void PlacefilePolygons::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(2, p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(2, p->vbo_.data()); std::unique_lock lock {p->bufferMutex_}; @@ -318,23 +312,21 @@ void PlacefilePolygons::Impl::Update() { if (dirty_) { - gl::OpenGLFunctions& gl = context_->gl(); - std::unique_lock lock {bufferMutex_}; // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLfloat) * currentBuffer_.size(), - currentBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLfloat) * currentBuffer_.size(), + currentBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 640d1c52..337716d0 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -72,7 +72,7 @@ public: PlacefileText::PlacefileText(const std::shared_ptr& context, const std::string& placefileName) : - DrawItem(context->gl()), p(std::make_unique(context, placefileName)) + DrawItem(), p(std::make_unique(context, placefileName)) { } PlacefileText::~PlacefileText() = default; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp index 0b5f9c30..9d57150a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp @@ -74,7 +74,7 @@ public: PlacefileTriangles::PlacefileTriangles( const std::shared_ptr& context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } PlacefileTriangles::~PlacefileTriangles() = default; @@ -96,8 +96,6 @@ void PlacefileTriangles::set_thresholded(bool thresholded) void PlacefileTriangles::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram( {{GL_VERTEX_SHADER, ":/gl/map_color.vert"}, {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, @@ -112,58 +110,58 @@ void PlacefileTriangles::Initialize() p->uSelectedTimeLocation_ = p->shaderProgram_->GetUniformLocation("uSelectedTime"); - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(2, p->vbo_.data()); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(2, p->vbo_.data()); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aScreenCoord - gl.glVertexAttribPointer(0, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); // aXYOffset - gl.glVertexAttribPointer(1, - 2, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(2 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 2, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); // aColor - gl.glVertexAttribPointer(2, - 4, - GL_FLOAT, - GL_FALSE, - kPointsPerVertex * sizeof(float), - reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(2); + glVertexAttribPointer(2, + 4, + GL_FLOAT, + GL_FALSE, + kPointsPerVertex * sizeof(float), + reinterpret_cast(4 * sizeof(float))); + glEnableVertexAttribArray(2); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); // aThreshold - gl.glVertexAttribIPointer(3, // - 1, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - static_cast(0)); - gl.glEnableVertexAttribArray(3); + glVertexAttribIPointer(3, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + glEnableVertexAttribArray(3); // aTimeRange - gl.glVertexAttribIPointer(4, // - 2, - GL_INT, - kIntegersPerVertex_ * sizeof(GLint), - reinterpret_cast(1 * sizeof(GLint))); - gl.glEnableVertexAttribArray(4); + glVertexAttribIPointer(4, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + glEnableVertexAttribArray(4); p->dirty_ = true; } @@ -173,9 +171,7 @@ void PlacefileTriangles::Render( { if (!p->currentBuffer_.empty()) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); p->Update(); p->shaderProgram_->Use(); @@ -188,12 +184,12 @@ void PlacefileTriangles::Render( // If thresholding is enabled, set the map distance units::length::nautical_miles mapDistance = util::maplibre::GetMapDistance(params); - gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); } else { // If thresholding is disabled, set the map distance to 0 - gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + glUniform1f(p->uMapDistanceLocation_, 0.0f); } // Selected time @@ -201,23 +197,21 @@ void PlacefileTriangles::Render( (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? std::chrono::system_clock::now() : p->selectedTime_; - gl.glUniform1i( + glUniform1i( p->uSelectedTimeLocation_, static_cast(std::chrono::duration_cast( selectedTime.time_since_epoch()) .count())); // Draw icons - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); } } void PlacefileTriangles::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(2, p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(2, p->vbo_.data()); std::unique_lock lock {p->bufferMutex_}; @@ -320,23 +314,21 @@ void PlacefileTriangles::Impl::Update() { if (dirty_) { - gl::OpenGLFunctions& gl = context_->gl(); - std::unique_lock lock {bufferMutex_}; // Buffer vertex data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLfloat) * currentBuffer_.size(), - currentBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLfloat) * currentBuffer_.size(), + currentBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data - gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); + glBufferData(GL_ARRAY_BUFFER, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp index 800e199f..d6ec850a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp @@ -73,7 +73,7 @@ public: }; Rectangle::Rectangle(std::shared_ptr context) : - DrawItem(context->gl()), p(std::make_unique(context)) + DrawItem(), p(std::make_unique(context)) { } Rectangle::~Rectangle() = default; @@ -83,41 +83,39 @@ Rectangle& Rectangle::operator=(Rectangle&&) noexcept = default; void Rectangle::Initialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - p->shaderProgram_ = p->context_->GetShaderProgram(":/gl/color.vert", ":/gl/color.frag"); p->uMVPMatrixLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); + glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); if (p->uMVPMatrixLocation_ == -1) { logger_->warn("Could not find uMVPMatrix"); } - gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(1, &p->vbo_); + glGenVertexArrays(1, &p->vao_); + glGenBuffers(1, &p->vbo_); - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); - gl.glBufferData( + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); + glBufferData( GL_ARRAY_BUFFER, sizeof(float) * BUFFER_LENGTH, nullptr, GL_DYNAMIC_DRAW); - gl.glVertexAttribPointer(0, - 3, - GL_FLOAT, - GL_FALSE, - POINTS_PER_VERTEX * sizeof(float), - static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, + 3, + GL_FLOAT, + GL_FALSE, + POINTS_PER_VERTEX * sizeof(float), + static_cast(0)); + glEnableVertexAttribArray(0); - gl.glVertexAttribPointer(1, - 4, - GL_FLOAT, - GL_FALSE, - POINTS_PER_VERTEX * sizeof(float), - reinterpret_cast(3 * sizeof(float))); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, + 4, + GL_FLOAT, + GL_FALSE, + POINTS_PER_VERTEX * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + glEnableVertexAttribArray(1); p->dirty_ = true; } @@ -126,10 +124,8 @@ void Rectangle::Render(const QMapLibre::CustomLayerRenderParameters& params) { if (p->visible_) { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); p->Update(); p->shaderProgram_->Use(); @@ -138,23 +134,21 @@ void Rectangle::Render(const QMapLibre::CustomLayerRenderParameters& params) if (p->fillColor_.has_value()) { // Draw fill - gl.glDrawArrays(GL_TRIANGLES, 24, 6); + glDrawArrays(GL_TRIANGLES, 24, 6); } if (p->borderWidth_ > 0.0f) { // Draw border - gl.glDrawArrays(GL_TRIANGLES, 0, 24); + glDrawArrays(GL_TRIANGLES, 0, 24); } } } void Rectangle::Deinitialize() { - gl::OpenGLFunctions& gl = p->context_->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(1, &p->vbo_); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(1, &p->vbo_); } void Rectangle::SetBorder(float width, boost::gil::rgba8_pixel_t color) @@ -206,8 +200,6 @@ void Rectangle::Impl::Update() { if (dirty_) { - gl::OpenGLFunctions& gl = context_->gl(); - const float lox = x_; const float rox = x_ + width_; const float boy = y_; @@ -289,10 +281,10 @@ void Rectangle::Impl::Update() {lox, toy, z_, fc0, fc1, fc2, fc3} // TL }}; - gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * BUFFER_LENGTH, - buffer, - GL_DYNAMIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * BUFFER_LENGTH, + buffer, + GL_DYNAMIC_DRAW); dirty_ = false; } diff --git a/scwx-qt/source/scwx/qt/gl/gl.hpp b/scwx-qt/source/scwx/qt/gl/gl.hpp index e87454c8..3361094b 100644 --- a/scwx-qt/source/scwx/qt/gl/gl.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl.hpp @@ -1,25 +1,12 @@ #pragma once -#include +#include #define SCWX_GL_CHECK_ERROR() \ { \ GLenum err; \ - while ((err = gl.glGetError()) != GL_NO_ERROR) \ + while ((err = glGetError()) != GL_NO_ERROR) \ { \ logger_->error("GL Error: {}, {}: {}", err, __FILE__, __LINE__); \ } \ } - -namespace scwx -{ -namespace qt -{ -namespace gl -{ - -using OpenGLFunctions = QOpenGLFunctions_3_3_Core; - -} -} // namespace qt -} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index b2cbbde3..995a149a 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -30,9 +30,6 @@ public: static std::size_t GetShaderKey(std::initializer_list> shaders); - gl::OpenGLFunctions* gl_ {nullptr}; - QOpenGLFunctions_3_0* gl30_ {nullptr}; - bool glInitialized_ {false}; std::unordered_map> @@ -51,18 +48,6 @@ GlContext::~GlContext() = default; GlContext::GlContext(GlContext&&) noexcept = default; GlContext& GlContext::operator=(GlContext&&) noexcept = default; -gl::OpenGLFunctions& GlContext::gl() -{ - return *p->gl_; -} - -#if !defined(__APPLE__) -QOpenGLFunctions_3_0& GlContext::gl30() -{ - return *p->gl30_; -} -#endif - std::uint64_t GlContext::texture_buffer_count() const { return p->textureBufferCount_; @@ -75,26 +60,21 @@ void GlContext::Impl::InitializeGL() return; } - // QOpenGLFunctions objects will not be freed. Since "destruction" takes - // place at the end of program execution, it is OK to intentionally leak - // these. - - // NOLINTBEGIN(cppcoreguidelines-owning-memory) - gl_ = new gl::OpenGLFunctions(); - gl30_ = new QOpenGLFunctions_3_0(); - // NOLINTEND(cppcoreguidelines-owning-memory) - - gl_->initializeOpenGLFunctions(); - gl30_->initializeOpenGLFunctions(); + GLenum error = glewInit(); + if (error != GLEW_OK) + { + logger_->error("glewInit failed: {}", + reinterpret_cast(glewGetErrorString(error))); + } logger_->info("OpenGL Version: {}", - reinterpret_cast(gl_->glGetString(GL_VERSION))); + reinterpret_cast(glGetString(GL_VERSION))); logger_->info("OpenGL Vendor: {}", - reinterpret_cast(gl_->glGetString(GL_VENDOR))); + reinterpret_cast(glGetString(GL_VENDOR))); logger_->info("OpenGL Renderer: {}", - reinterpret_cast(gl_->glGetString(GL_RENDERER))); + reinterpret_cast(glGetString(GL_RENDERER))); - gl_->glGenTextures(1, &textureAtlas_); + glGenTextures(1, &textureAtlas_); glInitialized_ = true; } @@ -119,7 +99,7 @@ std::shared_ptr GlContext::GetShaderProgram( if (it == p->shaderProgramMap_.end()) { - shaderProgram = std::make_shared(*p->gl_); + shaderProgram = std::make_shared(); shaderProgram->Load(shaders); p->shaderProgramMap_[key] = shaderProgram; } @@ -142,7 +122,7 @@ GLuint GlContext::GetTextureAtlas() if (p->textureBufferCount_ != textureAtlas.BuildCount()) { p->textureBufferCount_ = textureAtlas.BuildCount(); - textureAtlas.BufferAtlas(*p->gl_, p->textureAtlas_); + textureAtlas.BufferAtlas(p->textureAtlas_); } return p->textureAtlas_; @@ -155,10 +135,8 @@ void GlContext::Initialize() void GlContext::StartFrame() { - auto& gl = p->gl_; - - gl->glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - gl->glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); } std::size_t GlContext::Impl::GetShaderKey( diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.hpp b/scwx-qt/source/scwx/qt/gl/gl_context.hpp index b506fca1..1af68b1f 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.hpp @@ -3,8 +3,6 @@ #include #include -#include - namespace scwx { namespace qt @@ -24,12 +22,6 @@ public: GlContext(GlContext&&) noexcept; GlContext& operator=(GlContext&&) noexcept; - gl::OpenGLFunctions& gl(); - -#if !defined(__APPLE__) - QOpenGLFunctions_3_0& gl30(); -#endif - std::uint64_t texture_buffer_count() const; std::shared_ptr diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp index b4855ca9..ec96c23e 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace scwx { @@ -23,29 +24,24 @@ static const std::unordered_map kShaderNames_ { class ShaderProgram::Impl { public: - explicit Impl(OpenGLFunctions& gl) : gl_(gl), id_ {GL_INVALID_INDEX} + explicit Impl() : id_ {GL_INVALID_INDEX} { // Create shader program - id_ = gl_.glCreateProgram(); + id_ = glCreateProgram(); } ~Impl() { // Delete shader program - gl_.glDeleteProgram(id_); + glDeleteProgram(id_); } static std::string ShaderName(GLenum type); - OpenGLFunctions& gl_; - GLuint id_; }; -ShaderProgram::ShaderProgram(OpenGLFunctions& gl) : - p(std::make_unique(gl)) -{ -} +ShaderProgram::ShaderProgram() : p(std::make_unique()) {} ShaderProgram::~ShaderProgram() = default; ShaderProgram::ShaderProgram(ShaderProgram&&) noexcept = default; @@ -58,7 +54,7 @@ GLuint ShaderProgram::id() const GLint ShaderProgram::GetUniformLocation(const std::string& name) { - GLint location = p->gl_.glGetUniformLocation(p->id_, name.c_str()); + GLint location = glGetUniformLocation(p->id_, name.c_str()); if (location == -1) { logger_->warn("Could not find {}", name); @@ -88,8 +84,6 @@ bool ShaderProgram::Load( { logger_->debug("Load()"); - OpenGLFunctions& gl = p->gl_; - GLint glSuccess; bool success = true; char infoLog[kInfoLogBufSize]; @@ -120,16 +114,16 @@ bool ShaderProgram::Load( const char* shaderSourceC = shaderSource.c_str(); // Create a shader - GLuint shaderId = gl.glCreateShader(shader.first); + GLuint shaderId = glCreateShader(shader.first); shaderIds.push_back(shaderId); // Attach the shader source code and compile the shader - gl.glShaderSource(shaderId, 1, &shaderSourceC, NULL); - gl.glCompileShader(shaderId); + glShaderSource(shaderId, 1, &shaderSourceC, NULL); + glCompileShader(shaderId); // Check for errors - gl.glGetShaderiv(shaderId, GL_COMPILE_STATUS, &glSuccess); - gl.glGetShaderInfoLog(shaderId, kInfoLogBufSize, &logLength, infoLog); + glGetShaderiv(shaderId, GL_COMPILE_STATUS, &glSuccess); + glGetShaderInfoLog(shaderId, kInfoLogBufSize, &logLength, infoLog); if (!glSuccess) { logger_->error("Shader compilation failed: {}", infoLog); @@ -146,13 +140,13 @@ bool ShaderProgram::Load( { for (auto& shaderId : shaderIds) { - gl.glAttachShader(p->id_, shaderId); + glAttachShader(p->id_, shaderId); } - gl.glLinkProgram(p->id_); + glLinkProgram(p->id_); // Check for errors - gl.glGetProgramiv(p->id_, GL_LINK_STATUS, &glSuccess); - gl.glGetProgramInfoLog(p->id_, kInfoLogBufSize, &logLength, infoLog); + glGetProgramiv(p->id_, GL_LINK_STATUS, &glSuccess); + glGetProgramInfoLog(p->id_, kInfoLogBufSize, &logLength, infoLog); if (!glSuccess) { logger_->error("Shader program link failed: {}", infoLog); @@ -167,7 +161,7 @@ bool ShaderProgram::Load( // Delete shaders for (auto& shaderId : shaderIds) { - gl.glDeleteShader(shaderId); + glDeleteShader(shaderId); } return success; @@ -175,7 +169,7 @@ bool ShaderProgram::Load( void ShaderProgram::Use() const { - p->gl_.glUseProgram(p->id_); + glUseProgram(p->id_); } } // namespace gl diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.hpp b/scwx-qt/source/scwx/qt/gl/shader_program.hpp index a2b887d8..13fe043b 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.hpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.hpp @@ -19,7 +19,7 @@ namespace gl class ShaderProgram { public: - explicit ShaderProgram(OpenGLFunctions& gl); + explicit ShaderProgram(); virtual ~ShaderProgram(); ShaderProgram(const ShaderProgram&) = delete; diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 25f4ee96..321f1b15 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -43,6 +43,7 @@ static const std::string logPrefix_ = "scwx::main"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static void ConfigureTheme(const std::vector& args); +static void InitializeOpenGL(); static void OverrideDefaultStyle(const std::vector& args); static void OverridePlatform(); @@ -66,17 +67,7 @@ int main(int argc, char* argv[]) scwx::qt::main::kBuildNumber_, scwx::qt::main::kCommitString_); - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); - -#if defined(__APPLE__) - // For macOS, we must choose between OpenGL 4.1 Core and OpenGL 2.1 - // Compatibility. OpenGL 2.1 does not meet requirements for shaders used by - // Supercell Wx. - QSurfaceFormat surfaceFormat = QSurfaceFormat::defaultFormat(); - surfaceFormat.setVersion(4, 1); - surfaceFormat.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - QSurfaceFormat::setDefaultFormat(surfaceFormat); -#endif + InitializeOpenGL(); QApplication a(argc, argv); @@ -234,6 +225,23 @@ static void ConfigureTheme(const std::vector& args) } } +static void InitializeOpenGL() +{ + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); + + QSurfaceFormat surfaceFormat = QSurfaceFormat::defaultFormat(); + surfaceFormat.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + +#if defined(__APPLE__) + // For macOS, we must choose between OpenGL 4.1 Core and OpenGL 2.1 + // Compatibility. OpenGL 2.1 does not meet requirements for shaders used by + // Supercell Wx. + surfaceFormat.setVersion(4, 1); +#endif + + QSurfaceFormat::setDefaultFormat(surfaceFormat); +} + static void OverrideDefaultStyle([[maybe_unused]] const std::vector& args) { diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 156b1a6d..6599e0e5 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -300,8 +300,6 @@ void AlertLayer::Initialize(const std::shared_ptr& mapContext) void AlertLayer::Render(const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - for (auto alertActive : {false, true}) { p->geoLines_.at(alertActive)->set_selected_time(p->selectedTime_); diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index 30b8a11e..7585334c 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -55,38 +55,36 @@ void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) auto glContext = gl_context(); - gl::OpenGLFunctions& gl = glContext->gl(); - // Load and configure overlay shader p->shaderProgram_ = glContext->GetShaderProgram(":/gl/texture1d.vert", ":/gl/texture1d.frag"); p->uMVPMatrixLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); + glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); if (p->uMVPMatrixLocation_ == -1) { logger_->warn("Could not find uMVPMatrix"); } - gl.glGenTextures(1, &p->texture_); + glGenTextures(1, &p->texture_); p->shaderProgram_->Use(); // Generate a vertex array object - gl.glGenVertexArrays(1, &p->vao_); + glGenVertexArrays(1, &p->vao_); // Generate vertex buffer objects - gl.glGenBuffers(2, p->vbo_.data()); + glGenBuffers(2, p->vbo_.data()); - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); // Bottom panel - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferData( + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferData( GL_ARRAY_BUFFER, sizeof(float) * 6 * 2, nullptr, GL_DYNAMIC_DRAW); - gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, static_cast(0)); + glEnableVertexAttribArray(0); // Color table panel texture coordinates const float textureCoords[6][1] = {{0.0f}, // TL @@ -96,12 +94,12 @@ void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) {0.0f}, // BL {1.0f}, // TR {1.0f}}; // BR - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - gl.glBufferData( + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBufferData( GL_ARRAY_BUFFER, sizeof(textureCoords), textureCoords, GL_STATIC_DRAW); - gl.glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, static_cast(0)); - gl.glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, static_cast(0)); + glEnableVertexAttribArray(1); connect(mapContext->radar_product_view().get(), &view::RadarProductView::ColorTableLutUpdated, @@ -113,8 +111,7 @@ void ColorTableLayer::Render( const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - auto radarProductView = mapContext->radar_product_view(); + auto radarProductView = mapContext->radar_product_view(); if (radarProductView == nullptr || !radarProductView->IsInitialized()) { @@ -130,28 +127,28 @@ void ColorTableLayer::Render( p->shaderProgram_->Use(); // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - gl.glUniformMatrix4fv( + glUniformMatrix4fv( p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(projection)); if (p->colorTableNeedsUpdate_) { p->colorTable_ = radarProductView->color_table_lut(); - gl.glActiveTexture(GL_TEXTURE0); - gl.glBindTexture(GL_TEXTURE_1D, p->texture_); - gl.glTexImage1D(GL_TEXTURE_1D, - 0, - GL_RGBA, - (GLsizei) p->colorTable_.size(), - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - p->colorTable_.data()); - gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.glGenerateMipmap(GL_TEXTURE_1D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, p->texture_); + glTexImage1D(GL_TEXTURE_1D, + 0, + GL_RGBA, + (GLsizei) p->colorTable_.size(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + p->colorTable_.data()); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenerateMipmap(GL_TEXTURE_1D); } if (p->colorTable_.size() > 0 && radarProductView->sweep_time() != @@ -171,10 +168,10 @@ void ColorTableLayer::Render( {vertexRX, vertexBY}}; // BR // Draw vertices - gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - gl.glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); - gl.glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(p->vao_); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glDrawArrays(GL_TRIANGLES, 0, 6); static constexpr int kLeftMargin_ = 0; static constexpr int kTopMargin_ = 0; @@ -196,11 +193,9 @@ void ColorTableLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = gl_context()->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(2, p->vbo_.data()); - gl.glDeleteTextures(1, &p->texture_); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(2, p->vbo_.data()); + glDeleteTextures(1, &p->texture_); p->uMVPMatrixLocation_ = GL_INVALID_INDEX; p->vao_ = GL_INVALID_INDEX; diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 4128f893..755b9925 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -127,8 +127,7 @@ void DrawLayer::RenderWithoutImGui( { auto& glContext = p->glContext_; - gl::OpenGLFunctions& gl = glContext->gl(); - p->textureAtlas_ = glContext->GetTextureAtlas(); + p->textureAtlas_ = glContext->GetTextureAtlas(); // Determine if the texture atlas changed since last render const std::uint64_t newTextureAtlasBuildCount = @@ -137,10 +136,10 @@ void DrawLayer::RenderWithoutImGui( newTextureAtlasBuildCount != p->textureAtlasBuildCount_; // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - gl.glActiveTexture(GL_TEXTURE0); - gl.glBindTexture(GL_TEXTURE_2D_ARRAY, p->textureAtlas_); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, p->textureAtlas_); for (auto& item : p->drawList_) { diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index f5f68b22..f721044e 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp index 69a22e91..3e49c1d1 100644 --- a/scwx-qt/source/scwx/qt/map/marker_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -165,8 +165,6 @@ void MarkerLayer::Impl::set_icon_sheets() void MarkerLayer::Render(const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - DrawLayer::Render(mapContext, params); SCWX_GL_CHECK_ERROR(); diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 2af24d43..50311b83 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -338,10 +338,9 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, { const std::unique_lock lock {p->renderMutex_}; - gl::OpenGLFunctions& gl = gl_context()->gl(); - auto radarProductView = mapContext->radar_product_view(); - auto& settings = mapContext->settings(); - const float pixelRatio = mapContext->pixel_ratio(); + auto radarProductView = mapContext->radar_product_view(); + auto& settings = mapContext->settings(); + const float pixelRatio = mapContext->pixel_ratio(); ImGuiFrameStart(mapContext); diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 4684d227..457ebccd 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -142,8 +142,6 @@ void OverlayProductLayer::Render( const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - if (p->stiNeedsUpdate_) { p->UpdateStormTrackingInformation(mapContext); diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index d725850e..abafad8e 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -132,8 +132,6 @@ void PlacefileLayer::Render( const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - std::shared_ptr placefileManager = manager::PlacefileManager::Instance(); diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 2ad6ae65..9d759c8e 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -78,42 +78,40 @@ void RadarProductLayer::Initialize( auto glContext = gl_context(); - gl::OpenGLFunctions& gl = glContext->gl(); - // Load and configure radar shader p->shaderProgram_ = glContext->GetShaderProgram(":/gl/radar.vert", ":/gl/radar.frag"); p->uMVPMatrixLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); + glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); if (p->uMVPMatrixLocation_ == -1) { logger_->warn("Could not find uMVPMatrix"); } p->uMapScreenCoordLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord"); + glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord"); if (p->uMapScreenCoordLocation_ == -1) { logger_->warn("Could not find uMapScreenCoord"); } p->uDataMomentOffsetLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentOffset"); + glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentOffset"); if (p->uDataMomentOffsetLocation_ == -1) { logger_->warn("Could not find uDataMomentOffset"); } p->uDataMomentScaleLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentScale"); + glGetUniformLocation(p->shaderProgram_->id(), "uDataMomentScale"); if (p->uDataMomentScaleLocation_ == -1) { logger_->warn("Could not find uDataMomentScale"); } p->uCFPEnabledLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uCFPEnabled"); + glGetUniformLocation(p->shaderProgram_->id(), "uCFPEnabled"); if (p->uCFPEnabledLocation_ == -1) { logger_->warn("Could not find uCFPEnabled"); @@ -122,22 +120,22 @@ void RadarProductLayer::Initialize( p->shaderProgram_->Use(); // Generate a vertex array object - gl.glGenVertexArrays(1, &p->vao_); + glGenVertexArrays(1, &p->vao_); // Generate vertex buffer objects - gl.glGenBuffers(3, p->vbo_.data()); + glGenBuffers(3, p->vbo_.data()); // Update radar sweep p->sweepNeedsUpdate_ = true; UpdateSweep(mapContext); // Create color table - gl.glGenTextures(1, &p->texture_); + glGenTextures(1, &p->texture_); p->colorTableNeedsUpdate_ = true; UpdateColorTable(mapContext); - gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); auto radarProductView = mapContext->radar_product_view(); connect(radarProductView.get(), @@ -153,8 +151,6 @@ void RadarProductLayer::Initialize( void RadarProductLayer::UpdateSweep( const std::shared_ptr& mapContext) { - gl::OpenGLFunctions& gl = gl_context()->gl(); - boost::timer::cpu_timer timer; std::shared_ptr radarProductView = @@ -174,20 +170,20 @@ void RadarProductLayer::UpdateSweep( const std::vector& vertices = radarProductView->vertices(); // Bind a vertex array object - gl.glBindVertexArray(p->vao_); + glBindVertexArray(p->vao_); // Buffer vertices - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); timer.start(); - gl.glBufferData(GL_ARRAY_BUFFER, - vertices.size() * sizeof(GLfloat), - vertices.data(), - GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, + vertices.size() * sizeof(GLfloat), + vertices.data(), + GL_STATIC_DRAW); timer.stop(); logger_->debug("Vertices buffered in {}", timer.format(6, "%ws")); - gl.glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, static_cast(0)); - gl.glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, static_cast(0)); + glEnableVertexAttribArray(0); // Buffer data moments const GLvoid* data; @@ -206,14 +202,14 @@ void RadarProductLayer::UpdateSweep( type = GL_UNSIGNED_SHORT; } - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); timer.start(); - gl.glBufferData(GL_ARRAY_BUFFER, dataSize, data, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, dataSize, data, GL_STATIC_DRAW); timer.stop(); logger_->debug("Data moments buffered in {}", timer.format(6, "%ws")); - gl.glVertexAttribIPointer(1, 1, type, 0, static_cast(0)); - gl.glEnableVertexAttribArray(1); + glVertexAttribIPointer(1, 1, type, 0, static_cast(0)); + glEnableVertexAttribArray(1); // Buffer CFP data const GLvoid* cfpData; @@ -235,18 +231,18 @@ void RadarProductLayer::UpdateSweep( cfpType = GL_UNSIGNED_SHORT; } - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); + glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[2]); timer.start(); - gl.glBufferData(GL_ARRAY_BUFFER, cfpDataSize, cfpData, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, cfpDataSize, cfpData, GL_STATIC_DRAW); timer.stop(); logger_->debug("CFP moments buffered in {}", timer.format(6, "%ws")); - gl.glVertexAttribIPointer(2, 1, cfpType, 0, static_cast(0)); - gl.glEnableVertexAttribArray(2); + glVertexAttribIPointer(2, 1, cfpType, 0, static_cast(0)); + glEnableVertexAttribArray(2); } else { - gl.glDisableVertexAttribArray(2); + glDisableVertexAttribArray(2); } p->numVertices_ = vertices.size() / 2; @@ -256,18 +252,17 @@ void RadarProductLayer::Render( const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - gl::OpenGLFunctions& gl = gl_context()->gl(); p->shaderProgram_->Use(); // Set OpenGL blend mode for transparency - gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const bool wireframeEnabled = mapContext->settings().radarWireframeEnabled_; if (wireframeEnabled) { // Set polygon mode to draw wireframe - gl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); } if (p->colorTableNeedsUpdate_) @@ -291,28 +286,28 @@ void RadarProductLayer::Render( glm::radians(params.bearing), glm::vec3(0.0f, 0.0f, 1.0f)); - gl.glUniform2fv(p->uMapScreenCoordLocation_, - 1, - glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( - {params.latitude, params.longitude}))); + glUniform2fv(p->uMapScreenCoordLocation_, + 1, + glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( + {params.latitude, params.longitude}))); - gl.glUniformMatrix4fv( + glUniformMatrix4fv( p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(uMVPMatrix)); - gl.glUniform1i(p->uCFPEnabledLocation_, p->cfpEnabled_ ? 1 : 0); + glUniform1i(p->uCFPEnabledLocation_, p->cfpEnabled_ ? 1 : 0); - gl.glUniform1ui(p->uDataMomentOffsetLocation_, p->rangeMin_); - gl.glUniform1f(p->uDataMomentScaleLocation_, p->scale_); + glUniform1ui(p->uDataMomentOffsetLocation_, p->rangeMin_); + glUniform1f(p->uDataMomentScaleLocation_, p->scale_); - gl.glActiveTexture(GL_TEXTURE0); - gl.glBindTexture(GL_TEXTURE_1D, p->texture_); - gl.glBindVertexArray(p->vao_); - gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, p->texture_); + glBindVertexArray(p->vao_); + glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); if (wireframeEnabled) { // Restore polygon mode to default - gl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } SCWX_GL_CHECK_ERROR(); @@ -322,10 +317,8 @@ void RadarProductLayer::Deinitialize() { logger_->debug("Deinitialize()"); - gl::OpenGLFunctions& gl = gl_context()->gl(); - - gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(3, p->vbo_.data()); + glDeleteVertexArrays(1, &p->vao_); + glDeleteBuffers(3, p->vbo_.data()); p->uMVPMatrixLocation_ = GL_INVALID_INDEX; p->uMapScreenCoordLocation_ = GL_INVALID_INDEX; @@ -536,7 +529,6 @@ void RadarProductLayer::UpdateColorTable( p->colorTableNeedsUpdate_ = false; - gl::OpenGLFunctions& gl = gl_context()->gl(); std::shared_ptr radarProductView = mapContext->radar_product_view(); @@ -547,17 +539,17 @@ void RadarProductLayer::UpdateColorTable( const float scale = rangeMax - rangeMin; - gl.glActiveTexture(GL_TEXTURE0); - gl.glBindTexture(GL_TEXTURE_1D, p->texture_); - gl.glTexImage1D(GL_TEXTURE_1D, - 0, - GL_RGBA, - (GLsizei) colorTable.size(), - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - colorTable.data()); - gl.glGenerateMipmap(GL_TEXTURE_1D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, p->texture_); + glTexImage1D(GL_TEXTURE_1D, + 0, + GL_RGBA, + (GLsizei) colorTable.size(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + colorTable.data()); + glGenerateMipmap(GL_TEXTURE_1D); p->rangeMin_ = rangeMin; p->scale_ = scale; diff --git a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp index 418fa2ff..9f6866b0 100644 --- a/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_site_layer.cpp @@ -107,8 +107,6 @@ void RadarSiteLayer::Render( return; } - gl::OpenGLFunctions& gl = gl_context()->gl(); - // Update map screen coordinate and scale information p->mapScreenCoordLocation_ = util::maplibre::LatLongToScreenCoordinate( {params.latitude, params.longitude}); diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp index acb8eda3..860f5612 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp @@ -1,5 +1,4 @@ #include -#include #include #include @@ -60,8 +59,6 @@ public: ImGuiContext* currentContext_; - gl::OpenGLFunctions gl_; - std::set renderedSet_ {}; bool imGuiRendererInitialized_ {false}; std::uint64_t imGuiFontsBuildCount_ {}; @@ -106,9 +103,6 @@ void ImGuiDebugWidget::initializeGL() { makeCurrent(); - // Initialize OpenGL Functions - p->gl_.initializeOpenGLFunctions(); - // Initialize ImGui OpenGL3 backend ImGui::SetCurrentContext(p->context_); ImGui_ImplOpenGL3_Init(); @@ -119,8 +113,8 @@ void ImGuiDebugWidget::initializeGL() void ImGuiDebugWidget::paintGL() { - p->gl_.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - p->gl_.glClear(GL_COLOR_BUFFER_BIT); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); ImGui::SetCurrentContext(p->currentContext_); diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.hpp b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.hpp index 695a6be9..585d2432 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.hpp +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include struct ImGuiContext; diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp index b8b5bd67..8ed20533 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.cpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.cpp @@ -314,7 +314,7 @@ void TextureAtlas::BuildAtlas(std::size_t width, std::size_t height) logger_->debug("Texture atlas built in {}", timer.format(6, "%ws")); } -void TextureAtlas::BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture) +void TextureAtlas::BufferAtlas(GLuint texture) { std::shared_lock lock(p->atlasMutex_); @@ -343,25 +343,23 @@ void TextureAtlas::BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture) lock.unlock(); - gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture); + glBindTexture(GL_TEXTURE_2D_ARRAY, texture); - gl.glTexParameteri( - GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - gl.glTexParameteri( - GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, - 0, - GL_RGBA, - static_cast(width), - static_cast(height), - static_cast(numLayers), - 0, - GL_RGBA, - GL_UNSIGNED_BYTE, - pixelData.data()); + glTexImage3D(GL_TEXTURE_2D_ARRAY, + 0, + GL_RGBA, + static_cast(width), + static_cast(height), + static_cast(numLayers), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + pixelData.data()); } } diff --git a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp index 64e23b43..b0214a66 100644 --- a/scwx-qt/source/scwx/qt/util/texture_atlas.hpp +++ b/scwx-qt/source/scwx/qt/util/texture_atlas.hpp @@ -77,7 +77,7 @@ public: std::shared_ptr CacheTexture( const std::string& name, const std::string& path, double scale = 1); void BuildAtlas(std::size_t width, std::size_t height); - void BufferAtlas(gl::OpenGLFunctions& gl, GLuint texture); + void BufferAtlas(GLuint texture); TextureAttributes GetTextureAttributes(const std::string& name); From a6f854745583fbb9a0ed7e296ffaeaaec8681fd7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 10 Jul 2025 23:27:24 -0500 Subject: [PATCH 712/762] Fix GLEW clang-tidy issues --- scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp | 4 +-- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 35 ++++++++++++------- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 26 +++++++++----- scwx-qt/source/scwx/qt/gl/draw/icons.cpp | 26 +++++++++----- .../source/scwx/qt/gl/draw/linked_vectors.cpp | 4 +-- .../scwx/qt/gl/draw/placefile_icons.cpp | 35 ++++++++++++------- .../scwx/qt/gl/draw/placefile_images.cpp | 35 ++++++++++++------- .../scwx/qt/gl/draw/placefile_lines.cpp | 26 +++++++++----- .../scwx/qt/gl/draw/placefile_polygons.cpp | 26 +++++++++----- .../source/scwx/qt/gl/draw/placefile_text.cpp | 12 +++---- .../source/scwx/qt/gl/draw/placefile_text.hpp | 3 +- .../scwx/qt/gl/draw/placefile_triangles.cpp | 26 +++++++++----- scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp | 8 ++++- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 2 +- .../source/scwx/qt/map/placefile_layer.cpp | 3 +- 15 files changed, 176 insertions(+), 95 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp index 6002d2ce..0f18e40f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/draw_item.cpp @@ -30,8 +30,8 @@ static const std::string logPrefix_ = "scwx::qt::gl::draw::draw_item"; class DrawItem::Impl { public: - explicit Impl() {} - ~Impl() {} + explicit Impl() = default; + ~Impl() = default; }; DrawItem::DrawItem() : p(std::make_unique()) {} diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index 5c1555ac..ba3162e9 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -186,6 +186,10 @@ void GeoIcons::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aLatLong glVertexAttribPointer(0, 2, @@ -261,6 +265,10 @@ void GeoIcons::Initialize() reinterpret_cast(3 * sizeof(GLint))); glEnableVertexAttribArray(7); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -857,10 +865,11 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged) // Buffer texture data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * textureBuffer_.size()), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); lastTextureAtlasChanged_ = false; } @@ -870,17 +879,19 @@ void GeoIcons::Impl::Update(bool textureAtlasChanged) { // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentIconBuffer_.size()), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index 80bd6b2e..aa376b00 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -175,6 +175,10 @@ void GeoLines::Initialize() nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aLatLong glVertexAttribPointer(0, 2, @@ -238,6 +242,10 @@ void GeoLines::Initialize() reinterpret_cast(3 * sizeof(GLint))); glEnableVertexAttribArray(7); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -667,17 +675,19 @@ void GeoLines::Impl::Update() { // Buffer lines data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentLinesBuffer_.size(), - currentLinesBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentLinesBuffer_.size()), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); } dirty_ = false; diff --git a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp index ffbb7e2e..b72f4443 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/icons.cpp @@ -140,6 +140,10 @@ void Icons::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aVertex glVertexAttribPointer(0, 2, @@ -197,6 +201,10 @@ void Icons::Initialize() static_cast(0)); glEnableVertexAttribArray(2); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -689,10 +697,11 @@ void Icons::Impl::Update(bool textureAtlasChanged) // Buffer texture data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * textureBuffer_.size()), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); lastTextureAtlasChanged_ = false; } @@ -702,10 +711,11 @@ void Icons::Impl::Update(bool textureAtlasChanged) { // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentIconBuffer_.size()), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index bc83ffe1..dfc3dd07 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -63,14 +63,12 @@ class LinkedVectors::Impl { public: explicit Impl(std::shared_ptr context) : - context_ {context}, geoLines_ {std::make_shared(context)} + geoLines_ {std::make_shared(context)} { } ~Impl() {} - std::shared_ptr context_; - bool borderEnabled_ {true}; bool visible_ {true}; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp index 6de20db0..9ce7dc8f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -182,6 +182,10 @@ void PlacefileIcons::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aLatLong glVertexAttribPointer(0, 2, @@ -252,6 +256,10 @@ void PlacefileIcons::Initialize() // aDisplayed glVertexAttribI1i(7, 1); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -649,10 +657,11 @@ void PlacefileIcons::Impl::Update(bool textureAtlasChanged) // Buffer texture data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * textureBuffer_.size()), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); } // If buffers need updating @@ -660,17 +669,19 @@ void PlacefileIcons::Impl::Update(bool textureAtlasChanged) { // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentIconBuffer_.size(), - currentIconBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentIconBuffer_.size()), + currentIconBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentIconBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp index c8dbc194..16d4d19b 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp @@ -160,6 +160,10 @@ void PlacefileImages::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aLatLong glVertexAttribPointer(0, 2, @@ -221,6 +225,10 @@ void PlacefileImages::Initialize() // aDisplayed glVertexAttribI1i(7, 1); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -446,10 +454,11 @@ void PlacefileImages::Impl::Update(bool textureAtlasChanged) // Buffer texture data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * textureBuffer_.size(), - textureBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * textureBuffer_.size()), + textureBuffer_.data(), + GL_DYNAMIC_DRAW); } // If buffers need updating @@ -457,17 +466,19 @@ void PlacefileImages::Impl::Update(bool textureAtlasChanged) { // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentImageBuffer_.size(), - currentImageBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentImageBuffer_.size()), + currentImageBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[2]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentImageBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index 44b81407..134d9c6b 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -148,6 +148,10 @@ void PlacefileLines::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aLatLong glVertexAttribPointer(0, 2, @@ -206,6 +210,10 @@ void PlacefileLines::Initialize() // aDisplayed glVertexAttribI1i(7, 1); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -470,17 +478,19 @@ void PlacefileLines::Impl::Update() { // Buffer lines data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * currentLinesBuffer_.size(), - currentLinesBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(float) * currentLinesBuffer_.size()), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); } dirty_ = false; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index e0f36c0b..646739ef 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -173,6 +173,10 @@ void PlacefilePolygons::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aScreenCoord glVertexAttribPointer(0, 2, @@ -219,6 +223,10 @@ void PlacefilePolygons::Initialize() reinterpret_cast(1 * sizeof(GLint))); glEnableVertexAttribArray(4); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -316,17 +324,19 @@ void PlacefilePolygons::Impl::Update() // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLfloat) * currentBuffer_.size(), - currentBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLfloat) * currentBuffer_.size()), + currentBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 337716d0..e133f98a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -25,9 +25,8 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class PlacefileText::Impl { public: - explicit Impl(const std::shared_ptr& context, - const std::string& placefileName) : - context_ {context}, placefileName_ {placefileName} + explicit Impl(const std::string& placefileName) : + placefileName_ {placefileName} { } @@ -43,8 +42,6 @@ public: float x, float y); - std::shared_ptr context_; - std::string placefileName_; bool thresholded_ {false}; @@ -70,9 +67,8 @@ public: std::vector> newFonts_ {}; }; -PlacefileText::PlacefileText(const std::shared_ptr& context, - const std::string& placefileName) : - DrawItem(), p(std::make_unique(context, placefileName)) +PlacefileText::PlacefileText(const std::string& placefileName) : + DrawItem(), p(std::make_unique(placefileName)) { } PlacefileText::~PlacefileText() = default; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp index 4cbaf0af..e36be5a6 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp @@ -19,8 +19,7 @@ namespace draw class PlacefileText : public DrawItem { public: - explicit PlacefileText(const std::shared_ptr& context, - const std::string& placefileName); + explicit PlacefileText(const std::string& placefileName); ~PlacefileText(); PlacefileText(const PlacefileText&) = delete; diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp index 9d57150a..5ad54bc7 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp @@ -117,6 +117,10 @@ void PlacefileTriangles::Initialize() glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) + // NOLINTBEGIN(performance-no-int-to-ptr) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // aScreenCoord glVertexAttribPointer(0, 2, @@ -163,6 +167,10 @@ void PlacefileTriangles::Initialize() reinterpret_cast(1 * sizeof(GLint))); glEnableVertexAttribArray(4); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) + p->dirty_ = true; } @@ -318,17 +326,19 @@ void PlacefileTriangles::Impl::Update() // Buffer vertex data glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLfloat) * currentBuffer_.size(), - currentBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLfloat) * currentBuffer_.size()), + currentBuffer_.data(), + GL_DYNAMIC_DRAW); // Buffer threshold data glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); - glBufferData(GL_ARRAY_BUFFER, - sizeof(GLint) * currentIntegerBuffer_.size(), - currentIntegerBuffer_.data(), - GL_DYNAMIC_DRAW); + glBufferData( + GL_ARRAY_BUFFER, + static_cast(sizeof(GLint) * currentIntegerBuffer_.size()), + currentIntegerBuffer_.data(), + GL_DYNAMIC_DRAW); numVertices_ = static_cast(currentBuffer_.size() / kPointsPerVertex); diff --git a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp index d6ec850a..616917a4 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp @@ -101,6 +101,8 @@ void Rectangle::Initialize() glBufferData( GL_ARRAY_BUFFER, sizeof(float) * BUFFER_LENGTH, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(performance-no-int-to-ptr) + glVertexAttribPointer(0, 3, GL_FLOAT, @@ -117,6 +119,8 @@ void Rectangle::Initialize() reinterpret_cast(3 * sizeof(float))); glEnableVertexAttribArray(1); + // NOLINTEND(performance-no-int-to-ptr) + p->dirty_ = true; } @@ -134,12 +138,14 @@ void Rectangle::Render(const QMapLibre::CustomLayerRenderParameters& params) if (p->fillColor_.has_value()) { // Draw fill + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) glDrawArrays(GL_TRIANGLES, 24, 6); } if (p->borderWidth_ > 0.0f) { // Draw border + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) glDrawArrays(GL_TRIANGLES, 0, 24); } } @@ -283,7 +289,7 @@ void Rectangle::Impl::Update() glBufferData(GL_ARRAY_BUFFER, sizeof(float) * BUFFER_LENGTH, - buffer, + static_cast(buffer), GL_DYNAMIC_DRAW); dirty_ = false; diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 995a149a..11cfb4d9 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -60,7 +60,7 @@ void GlContext::Impl::InitializeGL() return; } - GLenum error = glewInit(); + const GLenum error = glewInit(); if (error != GLEW_OK) { logger_->error("glewInit failed: {}", diff --git a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp index abafad8e..a5bcdc58 100644 --- a/scwx-qt/source/scwx/qt/map/placefile_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/placefile_layer.cpp @@ -34,8 +34,7 @@ public: std::make_shared(glContext)}, placefileTriangles_ { std::make_shared(glContext)}, - placefileText_ { - std::make_shared(glContext, placefileName)} + placefileText_ {std::make_shared(placefileName)} { ConnectSignals(); } From 8e9db6a2fe663621105b0a19d5a4e7c1f73845e7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 11 Jul 2025 00:26:52 -0500 Subject: [PATCH 713/762] Display an informational dialog when OpenGL cannot properly be initialized --- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 60 +++++++++++++++++------ scwx-qt/source/scwx/qt/main/main.cpp | 19 ++++++- scwx-qt/source/scwx/qt/map/map_widget.cpp | 12 +++-- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 11cfb4d9..4729eab5 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -3,12 +3,9 @@ #include #include +#include -namespace scwx -{ -namespace qt -{ -namespace gl +namespace scwx::qt::gl { static const std::string logPrefix_ = "scwx::qt::gl::gl_context"; @@ -63,16 +60,49 @@ void GlContext::Impl::InitializeGL() const GLenum error = glewInit(); if (error != GLEW_OK) { - logger_->error("glewInit failed: {}", - reinterpret_cast(glewGetErrorString(error))); + auto glewErrorString = + reinterpret_cast(glewGetErrorString(error)); + logger_->error("glewInit failed: {}", glewErrorString); + + QMessageBox::critical( + nullptr, + "Supercell Wx", + QString("Unable to initialize OpenGL: %1").arg(glewErrorString)); + + throw std::runtime_error("Unable to initialize OpenGL"); } - logger_->info("OpenGL Version: {}", - reinterpret_cast(glGetString(GL_VERSION))); - logger_->info("OpenGL Vendor: {}", - reinterpret_cast(glGetString(GL_VENDOR))); - logger_->info("OpenGL Renderer: {}", - reinterpret_cast(glGetString(GL_RENDERER))); + auto glVersion = reinterpret_cast(glGetString(GL_VERSION)); + auto glVendor = reinterpret_cast(glGetString(GL_VENDOR)); + auto glRenderer = reinterpret_cast(glGetString(GL_RENDERER)); + + logger_->info("OpenGL Version: {}", glVersion); + logger_->info("OpenGL Vendor: {}", glVendor); + logger_->info("OpenGL Renderer: {}", glRenderer); + + // Get OpenGL version + GLint major = 0; + GLint minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + if (major < 3 || (major == 3 && minor < 3)) + { + logger_->error( + "OpenGL 3.3 or greater is required, found {}.{}", major, minor); + + QMessageBox::critical( + nullptr, + "Supercell Wx", + QString("OpenGL 3.3 or greater is required, found %1.%2\n\n%3\n%4\n%5") + .arg(major) + .arg(minor) + .arg(glVersion) + .arg(glVendor) + .arg(glRenderer)); + + throw std::runtime_error("OpenGL version too low"); + } glGenTextures(1, &textureAtlas_); @@ -151,6 +181,4 @@ std::size_t GlContext::Impl::GetShaderKey( return seed; } -} // namespace gl -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::gl diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 321f1b15..fbb716c9 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -151,8 +151,23 @@ int main(int argc, char* argv[]) // Run Qt main loop { scwx::qt::main::MainWindow w; - w.show(); - result = a.exec(); + + bool initialized = false; + + try + { + w.show(); + initialized = true; + } + catch (const std::exception& ex) + { + logger_->critical(ex.what()); + } + + if (initialized) + { + result = a.exec(); + } } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 763be1b6..d619de79 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -2011,10 +2011,14 @@ void MapWidgetImpl::RadarProductViewConnect() std::shared_ptr radarSite = radarProductManager_->radar_site(); - RadarRangeLayer::Update( - map_, - radarProductView->range(), - {radarSite->latitude(), radarSite->longitude()}); + if (map_ != nullptr) + { + RadarRangeLayer::Update( + map_, + radarProductView->range(), + {radarSite->latitude(), radarSite->longitude()}); + } + widget_->update(); Q_EMIT widget_->RadarSweepUpdated(); }, From 17af5e27acc0f9d1dec9d85f27dd6799741d512c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 11 Jul 2025 23:04:31 -0500 Subject: [PATCH 714/762] Another round of clang-tidy fixes --- .../source/scwx/qt/gl/draw/placefile_text.cpp | 23 ++++++-------- scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp | 23 +++++++------- scwx-qt/source/scwx/qt/gl/shader_program.cpp | 30 ++++++++----------- .../source/scwx/qt/map/color_table_layer.cpp | 24 +++++++++++++-- .../scwx/qt/map/radar_product_layer.cpp | 12 ++++++-- 5 files changed, 62 insertions(+), 50 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index e133f98a..832a1292 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -10,13 +10,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace gl -{ -namespace draw +namespace scwx::qt::gl::draw { static const std::string logPrefix_ = "scwx::qt::gl::draw::placefile_text"; @@ -25,12 +19,16 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class PlacefileText::Impl { public: - explicit Impl(const std::string& placefileName) : - placefileName_ {placefileName} + explicit Impl(std::string placefileName) : + placefileName_ {std::move(placefileName)} { } + ~Impl() = default; - ~Impl() {} + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; void RenderTextDrawItem( const QMapLibre::CustomLayerRenderParameters& params, @@ -306,7 +304,4 @@ void PlacefileText::FinishText() p->newFonts_.clear(); } -} // namespace draw -} // namespace gl -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::gl::draw diff --git a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp index 616917a4..d86ba163 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/rectangle.cpp @@ -3,13 +3,7 @@ #include -namespace scwx -{ -namespace qt -{ -namespace gl -{ -namespace draw +namespace scwx::qt::gl::draw { static const std::string logPrefix_ = "scwx::qt::gl::draw::rectangle"; @@ -27,7 +21,7 @@ class Rectangle::Impl { public: explicit Impl(std::shared_ptr context) : - context_ {context}, + context_ {std::move(context)}, dirty_ {false}, visible_ {true}, x_ {0.0f}, @@ -44,8 +38,12 @@ public: vbo_ {GL_INVALID_INDEX} { } + ~Impl() = default; - ~Impl() {} + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; std::shared_ptr context_; @@ -101,6 +99,7 @@ void Rectangle::Initialize() glBufferData( GL_ARRAY_BUFFER, sizeof(float) * BUFFER_LENGTH, nullptr, GL_DYNAMIC_DRAW); + // NOLINTBEGIN(modernize-use-nullptr) // NOLINTBEGIN(performance-no-int-to-ptr) glVertexAttribPointer(0, @@ -120,6 +119,7 @@ void Rectangle::Initialize() glEnableVertexAttribArray(1); // NOLINTEND(performance-no-int-to-ptr) + // NOLINTEND(modernize-use-nullptr) p->dirty_ = true; } @@ -296,7 +296,4 @@ void Rectangle::Impl::Update() } } -} // namespace draw -} // namespace gl -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::gl::draw diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp index ec96c23e..61808ad0 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp @@ -4,11 +4,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace gl +namespace scwx::qt::gl { static const std::string logPrefix_ = "scwx::qt::gl::shader_program"; @@ -24,11 +20,7 @@ static const std::unordered_map kShaderNames_ { class ShaderProgram::Impl { public: - explicit Impl() : id_ {GL_INVALID_INDEX} - { - // Create shader program - id_ = glCreateProgram(); - } + explicit Impl() : id_ {glCreateProgram()} {} ~Impl() { @@ -36,6 +28,11 @@ public: glDeleteProgram(id_); } + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + static std::string ShaderName(GLenum type); GLuint id_; @@ -54,7 +51,7 @@ GLuint ShaderProgram::id() const GLint ShaderProgram::GetUniformLocation(const std::string& name) { - GLint location = glGetUniformLocation(p->id_, name.c_str()); + const GLint location = glGetUniformLocation(p->id_, name.c_str()); if (location == -1) { logger_->warn("Could not find {}", name); @@ -114,16 +111,17 @@ bool ShaderProgram::Load( const char* shaderSourceC = shaderSource.c_str(); // Create a shader - GLuint shaderId = glCreateShader(shader.first); + const GLuint shaderId = glCreateShader(shader.first); shaderIds.push_back(shaderId); // Attach the shader source code and compile the shader - glShaderSource(shaderId, 1, &shaderSourceC, NULL); + glShaderSource(shaderId, 1, &shaderSourceC, nullptr); glCompileShader(shaderId); // Check for errors glGetShaderiv(shaderId, GL_COMPILE_STATUS, &glSuccess); - glGetShaderInfoLog(shaderId, kInfoLogBufSize, &logLength, infoLog); + glGetShaderInfoLog( + shaderId, kInfoLogBufSize, &logLength, static_cast(infoLog)); if (!glSuccess) { logger_->error("Shader compilation failed: {}", infoLog); @@ -172,6 +170,4 @@ void ShaderProgram::Use() const glUseProgram(p->id_); } -} // namespace gl -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::gl diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index 7585334c..f55bd94a 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -78,6 +78,9 @@ void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) glBindVertexArray(p->vao_); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // NOLINTBEGIN(modernize-use-nullptr) + // Bottom panel glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); glBufferData( @@ -87,6 +90,7 @@ void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) glEnableVertexAttribArray(0); // Color table panel texture coordinates + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) const float textureCoords[6][1] = {{0.0f}, // TL {0.0f}, // BL {1.0f}, // TR @@ -95,12 +99,17 @@ void ColorTableLayer::Initialize(const std::shared_ptr& mapContext) {1.0f}, // TR {1.0f}}; // BR glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); - glBufferData( - GL_ARRAY_BUFFER, sizeof(textureCoords), textureCoords, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, + sizeof(textureCoords), + static_cast(textureCoords), + GL_STATIC_DRAW); glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, static_cast(0)); glEnableVertexAttribArray(1); + // NOLINTEND(modernize-use-nullptr) + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + connect(mapContext->radar_product_view().get(), &view::RadarProductView::ColorTableLutUpdated, this, @@ -154,6 +163,9 @@ void ColorTableLayer::Render( if (p->colorTable_.size() > 0 && radarProductView->sweep_time() != std::chrono::system_clock::time_point()) { + // NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays) + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // Color table panel vertices const float vertexLX = 0.0f; const float vertexRX = static_cast(params.width); @@ -170,7 +182,10 @@ void ColorTableLayer::Render( // Draw vertices glBindVertexArray(p->vao_); glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); - glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glBufferSubData(GL_ARRAY_BUFFER, + 0, + sizeof(vertices), + static_cast(vertices)); glDrawArrays(GL_TRIANGLES, 0, 6); static constexpr int kLeftMargin_ = 0; @@ -180,6 +195,9 @@ void ColorTableLayer::Render( mapContext->set_color_table_margins( QMargins {kLeftMargin_, kTopMargin_, kRightMargin_, kBottomMargin_}); + + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + // NOLINTEND(cppcoreguidelines-avoid-c-arrays) } else { diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 9d759c8e..22b43a87 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -151,6 +151,9 @@ void RadarProductLayer::Initialize( void RadarProductLayer::UpdateSweep( const std::shared_ptr& mapContext) { + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers) + // NOLINTBEGIN(modernize-use-nullptr) + boost::timer::cpu_timer timer; std::shared_ptr radarProductView = @@ -176,7 +179,7 @@ void RadarProductLayer::UpdateSweep( glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); timer.start(); glBufferData(GL_ARRAY_BUFFER, - vertices.size() * sizeof(GLfloat), + static_cast(vertices.size() * sizeof(GLfloat)), vertices.data(), GL_STATIC_DRAW); timer.stop(); @@ -245,7 +248,10 @@ void RadarProductLayer::UpdateSweep( glDisableVertexAttribArray(2); } - p->numVertices_ = vertices.size() / 2; + p->numVertices_ = static_cast(vertices.size() / 2); + + // NOLINTEND(modernize-use-nullptr) + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) } void RadarProductLayer::Render( @@ -302,7 +308,7 @@ void RadarProductLayer::Render( glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_1D, p->texture_); glBindVertexArray(p->vao_); - glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + glDrawArrays(GL_TRIANGLES, 0, static_cast(p->numVertices_)); if (wireframeEnabled) { From cf2a495e267177c6476163d4b98ae59c99382417 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 00:05:46 -0500 Subject: [PATCH 715/762] Explicit cast when getting OpenGL program info log --- scwx-qt/source/scwx/qt/gl/shader_program.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/shader_program.cpp b/scwx-qt/source/scwx/qt/gl/shader_program.cpp index 61808ad0..e1b791c0 100644 --- a/scwx-qt/source/scwx/qt/gl/shader_program.cpp +++ b/scwx-qt/source/scwx/qt/gl/shader_program.cpp @@ -144,7 +144,8 @@ bool ShaderProgram::Load( // Check for errors glGetProgramiv(p->id_, GL_LINK_STATUS, &glSuccess); - glGetProgramInfoLog(p->id_, kInfoLogBufSize, &logLength, infoLog); + glGetProgramInfoLog( + p->id_, kInfoLogBufSize, &logLength, static_cast(infoLog)); if (!glSuccess) { logger_->error("Shader program link failed: {}", infoLog); From c3d39e25715d83a1d99a9cd9880249a7ca49ad9c Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 12 Jul 2025 08:48:07 -0400 Subject: [PATCH 716/762] Add a label to the location marker settings widget to indicate the existance of hotkeys for adding/editing location markers --- .../scwx/qt/ui/marker_settings_widget.cpp | 32 +++++++++++++------ .../scwx/qt/ui/marker_settings_widget.ui | 29 ++++++++++++++++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp index 51d9e40a..da1d3809 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp @@ -3,17 +3,15 @@ #include #include +#include +#include #include #include #include #include -namespace scwx -{ -namespace qt -{ -namespace ui +namespace scwx::qt::ui { static const std::string logPrefix_ = "scwx::qt::ui::marker_settings_widget"; @@ -34,6 +32,7 @@ public: } void ConnectSignals(); + void UpdateHotkeyLabel(); MarkerSettingsWidget* self_; model::MarkerModel* markerModel_; @@ -41,9 +40,9 @@ public: std::shared_ptr markerManager_ { manager::MarkerManager::Instance()}; std::shared_ptr editMarkerDialog_ {nullptr}; + boost::signals2::scoped_connection hotkeyConnection_; }; - MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : QFrame(parent), p {std::make_unique(this)}, @@ -53,6 +52,7 @@ MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : ui->removeButton->setEnabled(false); ui->markerView->setModel(p->proxyModel_); + p->UpdateHotkeyLabel(); p->editMarkerDialog_ = std::make_shared(this); @@ -128,8 +128,22 @@ void MarkerSettingsWidgetImpl::ConnectSignals() editMarkerDialog_->setup(id.toULongLong()); editMarkerDialog_->show(); }); + hotkeyConnection_ = settings::HotkeySettings::Instance() + .hotkey(types::Hotkey::AddLocationMarker) + .changed_signal() + .connect([this]() { UpdateHotkeyLabel(); }); } -} // namespace ui -} // namespace qt -} // namespace scwx +void MarkerSettingsWidgetImpl::UpdateHotkeyLabel() +{ + self_->ui->hotkeyLabel->setText( + fmt::format( + "A Location Marker can be placed at the location under the cursor by " + "pressing \"{}\" and edited by right clicking it on the map.", + settings::HotkeySettings::Instance() + .hotkey(types::Hotkey::AddLocationMarker) + .GetValue()) + .c_str()); +} + +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui index 3804f318..49b41c8b 100644 --- a/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui +++ b/scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui @@ -14,6 +14,33 @@ Frame + + 0 + + + + + + 0 + 0 + + + + + 9 + + + + QFrame::Shape::NoFrame + + + + + + Qt::TextFormat::PlainText + + + @@ -34,7 +61,7 @@ 0 - 0 + 6 0 From 6cb668f546413b655daea9e601373448f2d66628 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 14:26:37 -0500 Subject: [PATCH 717/762] Add submodule for glad --- .gitmodules | 3 +++ ACKNOWLEDGEMENTS.md | 1 + external/glad | 1 + 3 files changed, 5 insertions(+) create mode 160000 external/glad diff --git a/.gitmodules b/.gitmodules index dc9749dc..a6f47235 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "external/qt6ct"] path = external/qt6ct url = https://github.com/AdenKoperczak/qt6ct.git +[submodule "external/glad"] + path = external/glad + url = https://github.com/Dav1dde/glad.git diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 6f7cd4ef..b0225bbe 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -23,6 +23,7 @@ Supercell Wx uses code from the following dependencies: | [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) | +| [GLAD](https://github.com/Dav1dde/glad) | [MIT License](https://spdx.org/licenses/MIT.html) | | [GLEW](https://www.opengl.org/sdk/libs/GLEW/) | [MIT License](https://spdx.org/licenses/MIT.html) | | [GLM](https://github.com/g-truc/glm) | [MIT License](https://spdx.org/licenses/MIT.html) | | [GoogleTest](https://google.github.io/googletest/) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | diff --git a/external/glad b/external/glad new file mode 160000 index 00000000..73db193f --- /dev/null +++ b/external/glad @@ -0,0 +1 @@ +Subproject commit 73db193f853e2ee079bf3ca8a64aa2eaf6459043 From d5cda9b353f58787c3004edb22db08366ba522ec Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 16:23:02 -0500 Subject: [PATCH 718/762] Use glad instead of GLEW to load OpenGL --- external/CMakeLists.txt | 2 ++ external/glad.cmake | 11 +++++++++++ scwx-qt/scwx-qt.cmake | 5 +++-- scwx-qt/source/scwx/qt/gl/gl.hpp | 2 +- scwx-qt/source/scwx/qt/gl/gl_context.cpp | 18 +++++++++--------- 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 external/glad.cmake diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 2137ae62..1039e96e 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -6,6 +6,7 @@ set_property(DIRECTORY PROPERTY CMAKE_CONFIGURE_DEPENDS aws-sdk-cpp.cmake date.cmake + glad.cmake hsluv-c.cmake imgui.cmake maplibre-native-qt.cmake @@ -16,6 +17,7 @@ set_property(DIRECTORY include(aws-sdk-cpp.cmake) include(date.cmake) +include(glad.cmake) include(hsluv-c.cmake) include(imgui.cmake) include(maplibre-native-qt.cmake) diff --git a/external/glad.cmake b/external/glad.cmake new file mode 100644 index 00000000..59ceb179 --- /dev/null +++ b/external/glad.cmake @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.24) +set(PROJECT_NAME scwx-glad) + +# Path to glad directory +set(GLAD_SOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/glad/") + +# Path to glad CMake files +add_subdirectory("${GLAD_SOURCES_DIR}/cmake" glad_cmake) + +# Specify glad settings +glad_add_library(glad_gl_core_33 LOADER REPRODUCIBLE API gl:core=3.3) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index c1dba7f2..7bd2d089 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -745,7 +745,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets GeographicLib::GeographicLib GEOS::geos GEOS::geos_cxx_flags - GLEW::GLEW + glad_gl_core_33 glm::glm imgui qt6ct-common @@ -753,7 +753,8 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets SQLite::SQLite3 wxdata) -target_link_libraries(supercell-wx PRIVATE scwx-qt +target_link_libraries(supercell-wx PRIVATE GLEW::GLEW + scwx-qt wxdata) if (LINUX) diff --git a/scwx-qt/source/scwx/qt/gl/gl.hpp b/scwx-qt/source/scwx/qt/gl/gl.hpp index 3361094b..ef5d0053 100644 --- a/scwx-qt/source/scwx/qt/gl/gl.hpp +++ b/scwx-qt/source/scwx/qt/gl/gl.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #define SCWX_GL_CHECK_ERROR() \ { \ diff --git a/scwx-qt/source/scwx/qt/gl/gl_context.cpp b/scwx-qt/source/scwx/qt/gl/gl_context.cpp index 4729eab5..8cdd08b4 100644 --- a/scwx-qt/source/scwx/qt/gl/gl_context.cpp +++ b/scwx-qt/source/scwx/qt/gl/gl_context.cpp @@ -57,21 +57,21 @@ void GlContext::Impl::InitializeGL() return; } - const GLenum error = glewInit(); - if (error != GLEW_OK) + const int gladVersion = gladLoaderLoadGL(); + if (!gladVersion) { - auto glewErrorString = - reinterpret_cast(glewGetErrorString(error)); - logger_->error("glewInit failed: {}", glewErrorString); + logger_->error("gladLoaderLoadGL failed"); QMessageBox::critical( - nullptr, - "Supercell Wx", - QString("Unable to initialize OpenGL: %1").arg(glewErrorString)); + nullptr, "Supercell Wx", "Unable to initialize OpenGL"); throw std::runtime_error("Unable to initialize OpenGL"); } + logger_->info("GLAD initialization complete: OpenGL {}.{}", + GLAD_VERSION_MAJOR(gladVersion), + GLAD_VERSION_MINOR(gladVersion)); + auto glVersion = reinterpret_cast(glGetString(GL_VERSION)); auto glVendor = reinterpret_cast(glGetString(GL_VENDOR)); auto glRenderer = reinterpret_cast(glGetString(GL_RENDERER)); @@ -86,7 +86,7 @@ void GlContext::Impl::InitializeGL() glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); - if (major < 3 || (major == 3 && minor < 3)) + if (major < 3 || (major == 3 && minor < 3) || !GLAD_GL_VERSION_3_3) { logger_->error( "OpenGL 3.3 or greater is required, found {}.{}", major, minor); From 40fc41b7245d1d09ee58c12faa1614eda31bf64e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 16:45:43 -0500 Subject: [PATCH 719/762] Move GLEW dependency back to scwx-qt (provides GLU) --- scwx-qt/scwx-qt.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 7bd2d089..e96e828a 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -746,6 +746,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets GEOS::geos GEOS::geos_cxx_flags glad_gl_core_33 + GLEW::GLEW glm::glm imgui qt6ct-common @@ -753,8 +754,7 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets SQLite::SQLite3 wxdata) -target_link_libraries(supercell-wx PRIVATE GLEW::GLEW - scwx-qt +target_link_libraries(supercell-wx PRIVATE scwx-qt wxdata) if (LINUX) From 4e74a8d38e717bb7ea3ab860dac43b345bf0af92 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 23:09:23 -0500 Subject: [PATCH 720/762] Build glad_gl_core_33 as part of autogenerate step --- .github/workflows/clang-tidy-review.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 2137b451..4feb238e 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -113,7 +113,8 @@ jobs: -DCONAN_HOST_PROFILE="${{ matrix.conan_profile }}" ` -DCONAN_BUILD_PROFILE="${{ matrix.conan_profile }}" ` -DCMAKE_EXPORT_COMPILE_COMMANDS=on - ninja scwx-qt_generate_counties_db ` + ninja glad_gl_core_33 ` + scwx-qt_generate_counties_db ` scwx-qt_generate_versions ` scwx-qt_autogen From 6b09b7cb9b852087a61ca012663f76da522758a5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 12 Jul 2025 23:22:08 -0500 Subject: [PATCH 721/762] Specify OpenGL as the renderable type --- scwx-qt/source/scwx/qt/main/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index fbb716c9..3df56a0d 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -246,6 +246,7 @@ static void InitializeOpenGL() QSurfaceFormat surfaceFormat = QSurfaceFormat::defaultFormat(); surfaceFormat.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); + surfaceFormat.setRenderableType(QSurfaceFormat::RenderableType::OpenGL); #if defined(__APPLE__) // For macOS, we must choose between OpenGL 4.1 Core and OpenGL 2.1 From 5c9e90d8050a516a3997bff6749e7978dcada2d9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 09:31:09 -0500 Subject: [PATCH 722/762] Revert "Disable Wayland platform plugin on Nvidia" --- scwx-qt/source/scwx/qt/main/main.cpp | 34 ---------------------------- 1 file changed, 34 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 3df56a0d..13fb3a05 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -19,8 +19,6 @@ #include #include -#include -#include #include #include @@ -45,7 +43,6 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static void ConfigureTheme(const std::vector& args); static void InitializeOpenGL(); static void OverrideDefaultStyle(const std::vector& args); -static void OverridePlatform(); int main(int argc, char* argv[]) { @@ -56,8 +53,6 @@ int main(int argc, char* argv[]) args.push_back(argv[i]); } - OverridePlatform(); - // Initialize logger auto& logManager = scwx::qt::manager::LogManager::Instance(); logManager.Initialize(); @@ -281,32 +276,3 @@ OverrideDefaultStyle([[maybe_unused]] const std::vector& args) } #endif } - -static void OverridePlatform() -{ -#if defined(__linux__) - static const std::string NVIDIA_ID = "0x10de"; - namespace fs = std::filesystem; - for (const auto& entry : fs::directory_iterator("/sys/class/drm")) - { - if (!entry.is_directory() || - !entry.path().filename().string().starts_with("card")) - { - continue; - } - - auto vendorPath = entry.path() / "device" / "vendor"; - std::ifstream vendorFile(vendorPath); - std::string vendor; - if (vendorFile && std::getline(vendorFile, vendor)) - { - if (vendor == NVIDIA_ID) - { - // Force xcb on NVIDIA - setenv("QT_QPA_PLATFORM", "xcb", 1); - return; - } - } - } -#endif -} From 7527e845a961b02e6d30adf1d620e9434b4cd0f0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 12:09:30 -0500 Subject: [PATCH 723/762] Bump version to v0.5.1 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- tools/net.supercellwx.app.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48b242b8..19e80256 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.5.0 + SCWX_VERSION: v0.5.1 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f48bb51..e76cb8d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0) scwx_python_setup() project(${PROJECT_NAME} - VERSION 0.5.0 + VERSION 0.5.1 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -32,7 +32,7 @@ set_property(DIRECTORY set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.5.0") +set(SCWX_VERSION "0.5.1") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml index bd40b1c4..762edf28 100644 --- a/tools/net.supercellwx.app.yml +++ b/tools/net.supercellwx.app.yml @@ -1,5 +1,5 @@ id: net.supercellwx.app -version: '0.5.0' +version: '0.5.1' runtime: "org.freedesktop.Platform" runtime-version: "23.08" sdk: "org.freedesktop.Sdk" From 791bf165863669dc1f795203269b2dbcbfa30786 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 14 Jul 2025 18:16:50 -0500 Subject: [PATCH 724/762] Look for the appropriate .msi asset suffix depending on platform --- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index 6473a30b..edb0396e 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -91,7 +91,14 @@ void UpdateDialog::UpdateReleaseInfo(const std::string& latestVersion, void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset) { #if defined(_WIN32) - if (asset.name_.ends_with(".msi")) + +# if defined(_M_AMD64) + static constexpr std::string assetSuffix = "-x64.msi"; +# else + static constexpr std::string assetSuffix = "-arm64.msi"; +# endif + + if (asset.name_.ends_with(assetSuffix)) { self_->ui->installUpdateButton->setVisible(true); installUrl_ = asset.browserDownloadUrl_; From 9076772cf600def6f735e4e2d2d3dc6928e5d153 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 14:31:25 -0500 Subject: [PATCH 725/762] Remove GLEW as a dependency (cherry picked from commit 1b404d8b9ff95baaa0d7ee11089e79d6d5e65ff9) --- conanfile.py | 1 - scwx-qt/scwx-qt.cmake | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/conanfile.py b/conanfile.py index 908aab88..8ed594d3 100644 --- a/conanfile.py +++ b/conanfile.py @@ -11,7 +11,6 @@ class SupercellWxConan(ConanFile): "freetype/2.13.2", "geographiclib/2.4", "geos/3.13.0", - "glew/2.2.0", "glm/1.0.1", "gtest/1.16.0", "libcurl/8.12.1", diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index e96e828a..e4a392b3 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -17,8 +17,8 @@ find_package(Boost) find_package(Fontconfig) find_package(geographiclib) find_package(geos) -find_package(GLEW) find_package(glm) +find_package(OpenGL) find_package(Python COMPONENTS Interpreter) find_package(SQLite3) @@ -746,9 +746,9 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets GEOS::geos GEOS::geos_cxx_flags glad_gl_core_33 - GLEW::GLEW glm::glm imgui + OpenGL::GLU qt6ct-common qt6ct-widgets SQLite::SQLite3 From 9ac941686b75821ac3e43cb0e4a85b1fa1d4fb30 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 17:17:09 -0500 Subject: [PATCH 726/762] GL_SILENCE_DEPRECATION is required for macOS (cherry picked from commit e0d1379423e688c27931e28aef17f4b066e8ac68) --- scwx-qt/scwx-qt.cmake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index e4a392b3..d013bfb8 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -662,6 +662,11 @@ if (LINUX) target_compile_definitions(supercell-wx PRIVATE QT_NO_EMIT) endif() +if (APPLE) + target_compile_definitions(scwx-qt PRIVATE GL_SILENCE_DEPRECATION) + target_compile_definitions(supercell-wx PRIVATE GL_SILENCE_DEPRECATION) +endif() + target_include_directories(scwx-qt PUBLIC ${scwx-qt_SOURCE_DIR}/source ${FTGL_INCLUDE_DIR} ${IMGUI_INCLUDE_DIRS} From f387e1e5258fbcb255e469b203c82cbda3124b89 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 17:31:25 -0500 Subject: [PATCH 727/762] Add mesa-glu requirement for Linux - Remove onetbb for macOS (cherry picked from commit 7a4ce740cd6a6f46bf1df212cd32617c3f55d487) --- conanfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 8ed594d3..ca2dfbd2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -41,8 +41,7 @@ class SupercellWxConan(ConanFile): def requirements(self): if self.settings.os == "Linux": - self.requires("onetbb/2022.0.0") - elif self.settings.os == "Macos": + self.requires("mesa-glu/9.0.3") self.requires("onetbb/2022.0.0") def generate(self): From ad8569532a2341fec513e76c3aacbfd46c5d8f9f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 13 Jul 2025 23:12:47 -0500 Subject: [PATCH 728/762] Ensure the mesa-glu::mesa-glu target is used on Linux instead of OpenGL::GLU (cherry picked from commit b0a8c6576dc29f46f3cea26fbd06966b713fb4e7) --- scwx-qt/scwx-qt.cmake | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index d013bfb8..194601e9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -734,6 +734,13 @@ if (LINUX) target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::WaylandClient) endif() +if (LINUX) + find_package(mesa-glu REQUIRED) + target_link_libraries(scwx-qt PUBLIC mesa-glu::mesa-glu) +else() + target_link_libraries(scwx-qt PUBLIC OpenGL::GLU) +endif() + target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::OpenGLWidgets Qt${QT_VERSION_MAJOR}::Multimedia @@ -753,7 +760,6 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets glad_gl_core_33 glm::glm imgui - OpenGL::GLU qt6ct-common qt6ct-widgets SQLite::SQLite3 From 2ffc1ca9b034c37074496d3957869417b6834e80 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 15 Jul 2025 23:35:44 -0500 Subject: [PATCH 729/762] Update acknowledgements for Mesa instead of GLEW --- ACKNOWLEDGEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index b0225bbe..1608b2ae 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -24,7 +24,6 @@ Supercell Wx uses code from the following dependencies: | [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) | | [GLAD](https://github.com/Dav1dde/glad) | [MIT License](https://spdx.org/licenses/MIT.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) | | [HSLuv](https://www.hsluv.org/) | [MIT License](https://spdx.org/licenses/MIT.html) | @@ -33,6 +32,7 @@ Supercell Wx uses code from the following dependencies: | [libpng](http://libpng.org/pub/png/libpng.html) | [PNG Reference Library version 2](https://spdx.org/licenses/libpng-2.0.html) | | [libxml2](http://xmlsoft.org/) | [MIT License](https://spdx.org/licenses/MIT.html) | | [MapLibre Native](https://maplibre.org/projects/maplibre-native/) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | +| [Mesa 3D](https://mesa3d.org/) | [MIT License](https://spdx.org/licenses/MIT.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 Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | From 76038686d5daec86fd9452681ad2530dd07c0142 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:00:37 +0000 Subject: [PATCH 730/762] Update dependency onetbb to v2022.2.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index ca2dfbd2..5c9dbb75 100644 --- a/conanfile.py +++ b/conanfile.py @@ -42,7 +42,7 @@ class SupercellWxConan(ConanFile): def requirements(self): if self.settings.os == "Linux": self.requires("mesa-glu/9.0.3") - self.requires("onetbb/2022.0.0") + self.requires("onetbb/2022.2.0") def generate(self): build_folder = os.path.join(self.build_folder, From 0f3a5fe010b488741a8727c895b5873981957562 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:28:14 +0000 Subject: [PATCH 731/762] Update dependency cpr to v1.12.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 5c9dbb75..2b396bee 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,7 +6,7 @@ import os class SupercellWxConan(ConanFile): settings = ("os", "compiler", "build_type", "arch") requires = ("boost/1.88.0", - "cpr/1.11.2", + "cpr/1.12.0", "fontconfig/2.15.0", "freetype/2.13.2", "geographiclib/2.4", From 0f3c1af3e1dfd3dd8cd35bf1b8f2b3ec2c14e553 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:28:18 +0000 Subject: [PATCH 732/762] Update dependency re2 to v20250722 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 5c9dbb75..25309236 100644 --- a/conanfile.py +++ b/conanfile.py @@ -18,7 +18,7 @@ class SupercellWxConan(ConanFile): "libxml2/2.13.8", "openssl/3.5.0", "range-v3/0.12.0", - "re2/20240702", + "re2/20250722", "spdlog/1.15.1", "sqlite3/3.49.1", "vulkan-loader/1.3.290.0", From 94d81c0c6bef5cb0cd7252c4897d7a8f73de2d65 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 9 Aug 2025 01:19:51 -0500 Subject: [PATCH 733/762] Initial NTP protocol functionality --- test/source/scwx/network/ntp_client.test.cpp | 21 +++ test/test.cmake | 3 +- wxdata/include/scwx/network/ntp_client.hpp | 32 +++++ wxdata/include/scwx/types/ntp_types.hpp | 61 ++++++++ wxdata/source/scwx/network/ntp_client.cpp | 143 +++++++++++++++++++ wxdata/source/scwx/types/ntp_types.cpp | 51 +++++++ wxdata/wxdata.cmake | 12 +- 7 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 test/source/scwx/network/ntp_client.test.cpp create mode 100644 wxdata/include/scwx/network/ntp_client.hpp create mode 100644 wxdata/include/scwx/types/ntp_types.hpp create mode 100644 wxdata/source/scwx/network/ntp_client.cpp create mode 100644 wxdata/source/scwx/types/ntp_types.cpp diff --git a/test/source/scwx/network/ntp_client.test.cpp b/test/source/scwx/network/ntp_client.test.cpp new file mode 100644 index 00000000..cebd8cc2 --- /dev/null +++ b/test/source/scwx/network/ntp_client.test.cpp @@ -0,0 +1,21 @@ +#include + +#include + +namespace scwx +{ +namespace network +{ + +TEST(NtpClient, Poll) +{ + NtpClient client {}; + + client.Open("time.nist.gov", "123"); + //client.Open("pool.ntp.org", "123"); + //client.Open("time.windows.com", "123"); + client.Poll(); +} + +} // namespace network +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index 0ae26b53..57fbc37e 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -17,7 +17,8 @@ set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp source/scwx/common/products.test.cpp) set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) -set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) +set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp + source/scwx/network/ntp_client.test.cpp) set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp source/scwx/provider/aws_level3_data_provider.test.cpp source/scwx/provider/iem_api_provider.test.cpp diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp new file mode 100644 index 00000000..55ef4204 --- /dev/null +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace scwx::network +{ + +/** + * @brief NTP Client + */ +class NtpClient +{ +public: + explicit NtpClient(); + ~NtpClient(); + + NtpClient(const NtpClient&) = delete; + NtpClient& operator=(const NtpClient&) = delete; + + NtpClient(NtpClient&&) noexcept; + NtpClient& operator=(NtpClient&&) noexcept; + + void Open(std::string_view host, std::string_view service); + void Poll(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace scwx::network diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp new file mode 100644 index 00000000..39c1f12f --- /dev/null +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +namespace scwx::types::ntp +{ + +/* Adapted from: + * https://github.com/lettier/ntpclient/blob/master/source/c/main.c + * + * Copyright (c) 2014 David Lettier + * Copyright (c) 2020 Krystian Stasiowski + * Distributed under the BSD 3-Clause License (See + * https://github.com/lettier/ntpclient/blob/master/LICENSE) + */ + +#pragma pack(push, 1) + +struct NtpPacket +{ + union + { + std::uint8_t li_vn_mode; + struct + { + std::uint8_t mode : 3; // Client will pick mode 3 for client. + std::uint8_t vn : 3; // Version number of the protocol. + std::uint8_t li : 2; // Leap indicator. + } fields; + }; + + std::uint8_t stratum; // Stratum level of the local clock. + std::uint8_t poll; // Maximum interval between successive messages. + std::uint8_t precision; // Precision of the local clock. + + std::uint32_t rootDelay; // Total round trip delay time. + std::uint32_t rootDispersion; // Max error aloud from primary clock source. + std::uint32_t refId; // Reference clock identifier. + + std::uint32_t refTm_s; // Reference time-stamp seconds. + std::uint32_t refTm_f; // Reference time-stamp fraction of a second. + + std::uint32_t origTm_s; // Originate time-stamp seconds. + std::uint32_t origTm_f; // Originate time-stamp fraction of a second. + + std::uint32_t rxTm_s; // Received time-stamp seconds. + std::uint32_t rxTm_f; // Received time-stamp fraction of a second. + + std::uint32_t txTm_s; // The most important field the client cares about. + // Transmit time-stamp seconds. + std::uint32_t txTm_f; // Transmit time-stamp fraction of a second. + + static NtpPacket Parse(const std::span data); +}; +// Total: 48 bytes. + +#pragma pack(pop) + +} // namespace scwx::types::ntp diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp new file mode 100644 index 00000000..7315eeec --- /dev/null +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include + +#include +#include +#include + +namespace scwx::network +{ + +static const std::string logPrefix_ = "scwx::network::ntp_client"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static constexpr std::size_t kReceiveBufferSize_ {48u}; + +class NtpClient::Impl +{ +public: + explicit Impl(); + ~Impl(); + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(const Impl&&) = delete; + Impl& operator=(const Impl&&) = delete; + + void Open(std::string_view host, std::string_view service); + void Poll(); + void ReceivePacket(std::size_t length); + + boost::asio::thread_pool threadPool_ {2u}; + + types::ntp::NtpPacket transmitPacket_ {}; + + boost::asio::ip::udp::socket socket_; + std::optional serverEndpoint_ {}; + std::array receiveBuffer_ {}; + + std::vector serverList_ { + "time.nist.gov", "ntp.pool.org", "time.windows.com"}; +}; + +NtpClient::NtpClient() : p(std::make_unique()) {} +NtpClient::~NtpClient() = default; + +NtpClient::NtpClient(NtpClient&&) noexcept = default; +NtpClient& NtpClient::operator=(NtpClient&&) noexcept = default; + +void NtpClient::Open(std::string_view host, std::string_view service) +{ + p->Open(host, service); +} + +void NtpClient::Poll() +{ + p->Poll(); +} + +NtpClient::Impl::Impl() : socket_ {threadPool_} +{ + transmitPacket_.fields.vn = 3; // Version + transmitPacket_.fields.mode = 3; // Client (3) +} + +NtpClient::Impl::~Impl() +{ + threadPool_.join(); +} + +void NtpClient::Impl::Open(std::string_view host, std::string_view service) +{ + boost::asio::ip::udp::resolver resolver(threadPool_); + boost::system::error_code ec; + + auto results = resolver.resolve(host, service, ec); + if (ec.value() == boost::system::errc::success && !results.empty()) + { + logger_->info("Using NTP server: {}", host); + serverEndpoint_ = *results.begin(); + socket_.open(serverEndpoint_->protocol()); + } + else + { + serverEndpoint_ = std::nullopt; + logger_->warn("Could not resolve host {}: {}", host, ec.message()); + } +} + +void NtpClient::Impl::Poll() +{ + using namespace std::chrono_literals; + + static constexpr auto kTimeout_ = 15s; + + try + { + std::size_t transmitPacketSize = sizeof(transmitPacket_); + // Send NTP request + socket_.send_to(boost::asio::buffer(&transmitPacket_, transmitPacketSize), + *serverEndpoint_); + + // Receive NTP response + auto future = + socket_.async_receive_from(boost::asio::buffer(receiveBuffer_), + *serverEndpoint_, + boost::asio::use_future); + std::size_t bytesReceived = 0; + + switch (future.wait_for(kTimeout_)) + { + case std::future_status::ready: + bytesReceived = future.get(); + ReceivePacket(bytesReceived); + break; + + case std::future_status::timeout: + case std::future_status::deferred: + logger_->warn("Timeout waiting for NTP response"); + socket_.cancel(); + break; + } + } + catch (const std::exception& ex) + { + logger_->error("Error polling: {}", ex.what()); + } +} + +void NtpClient::Impl::ReceivePacket(std::size_t length) +{ + if (length >= sizeof(types::ntp::NtpPacket)) + { + auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); + (void) packet; + } + else + { + logger_->warn("Received too few bytes: {}", length); + } +} + +} // namespace scwx::network diff --git a/wxdata/source/scwx/types/ntp_types.cpp b/wxdata/source/scwx/types/ntp_types.cpp new file mode 100644 index 00000000..cca0c422 --- /dev/null +++ b/wxdata/source/scwx/types/ntp_types.cpp @@ -0,0 +1,51 @@ +#include + +#include +#include + +#ifdef _WIN32 +# include +#else +# include +#endif + +namespace scwx::types::ntp +{ + +NtpPacket NtpPacket::Parse(const std::span data) +{ + NtpPacket packet; + + assert(data.size() >= sizeof(NtpPacket)); + + packet = *reinterpret_cast(data.data()); + + // Detect Kiss-o'-Death (KoD) packet + if (packet.stratum == 0) + { + // TODO + std::string kissCode = + std::string(reinterpret_cast(&packet.refId), 4); + (void) kissCode; + } + + packet.rootDelay = ntohl(packet.rootDelay); + packet.rootDispersion = ntohl(packet.rootDispersion); + packet.refId = ntohl(packet.refId); + + packet.refTm_s = ntohl(packet.refTm_s); + packet.refTm_f = ntohl(packet.refTm_f); + + packet.origTm_s = ntohl(packet.origTm_s); + packet.origTm_f = ntohl(packet.origTm_f); + + packet.rxTm_s = ntohl(packet.rxTm_s); + packet.rxTm_f = ntohl(packet.rxTm_f); + + packet.txTm_s = ntohl(packet.txTm_s); + packet.txTm_f = ntohl(packet.txTm_f); + + return packet; +} + +} // namespace scwx::types::ntp diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index ab23a4e7..bc489cb3 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -60,9 +60,11 @@ set(HDR_GR include/scwx/gr/color.hpp set(SRC_GR source/scwx/gr/color.cpp source/scwx/gr/placefile.cpp) set(HDR_NETWORK include/scwx/network/cpr.hpp - include/scwx/network/dir_list.hpp) + include/scwx/network/dir_list.hpp + include/scwx/network/ntp_client.hpp) set(SRC_NETWORK source/scwx/network/cpr.cpp - source/scwx/network/dir_list.cpp) + source/scwx/network/dir_list.cpp + source/scwx/network/ntp_client.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp include/scwx/provider/aws_level2_chunks_data_provider.hpp include/scwx/provider/aws_level3_data_provider.hpp @@ -80,8 +82,10 @@ 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_TYPES include/scwx/types/iem_types.hpp) -set(SRC_TYPES source/scwx/types/iem_types.cpp) +set(HDR_TYPES include/scwx/types/iem_types.hpp + include/scwx/types/ntp_types.hpp) +set(SRC_TYPES source/scwx/types/iem_types.cpp + source/scwx/types/ntp_types.cpp) set(HDR_UTIL include/scwx/util/digest.hpp include/scwx/util/enum.hpp include/scwx/util/environment.hpp From f85bf9283ae52d2a1206a6fdfa83d886719d1164 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 9 Aug 2025 01:20:30 -0500 Subject: [PATCH 734/762] Union access should not be flagged by clang-tidy --- .clang-tidy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.clang-tidy b/.clang-tidy index 26230c78..c93d61fe 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -7,8 +7,9 @@ Checks: - 'modernize-*' - 'performance-*' - '-bugprone-easily-swappable-parameters' - - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-cppcoreguidelines-avoid-do-while' + - '-cppcoreguidelines-pro-type-reinterpret-cast' + - '-cppcoreguidelines-pro-type-union-access' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-misc-use-anonymous-namespace' From 258466e02c3cc94ecc4755b3116bd51aae6823e5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 10 Aug 2025 00:24:48 -0500 Subject: [PATCH 735/762] NTP time offset calculation --- wxdata/include/scwx/types/ntp_types.hpp | 3 +- wxdata/source/scwx/network/ntp_client.cpp | 162 ++++++++++++++++++++-- wxdata/source/scwx/types/ntp_types.cpp | 9 -- 3 files changed, 154 insertions(+), 20 deletions(-) diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp index 39c1f12f..cfb8f764 100644 --- a/wxdata/include/scwx/types/ntp_types.hpp +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -48,8 +48,7 @@ struct NtpPacket std::uint32_t rxTm_s; // Received time-stamp seconds. std::uint32_t rxTm_f; // Received time-stamp fraction of a second. - std::uint32_t txTm_s; // The most important field the client cares about. - // Transmit time-stamp seconds. + std::uint32_t txTm_s; // Transmit time-stamp seconds. std::uint32_t txTm_f; // Transmit time-stamp fraction of a second. static NtpPacket Parse(const std::span data); diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index 7315eeec..a20540c4 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace scwx::network { @@ -15,15 +16,86 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr std::size_t kReceiveBufferSize_ {48u}; +class NtpTimestamp +{ +public: + // NTP epoch: January 1, 1900 + // Unix epoch: January 1, 1970 + // Difference = 70 years = 2,208,988,800 seconds + static constexpr std::uint32_t kNtpToUnixOffset_ = 2208988800UL; + + // NTP fractional part represents 1/2^32 of a second + static constexpr std::uint64_t kFractionalMultiplier_ = 0x100000000ULL; + + static constexpr std::uint64_t _1e9 = 1000000000ULL; + + std::uint32_t seconds_ {0}; + std::uint32_t fraction_ {0}; + + explicit NtpTimestamp() = default; + explicit NtpTimestamp(std::uint32_t seconds, std::uint32_t fraction) : + seconds_ {seconds}, fraction_ {fraction} + { + } + ~NtpTimestamp() = default; + + NtpTimestamp(const NtpTimestamp&) = default; + NtpTimestamp& operator=(const NtpTimestamp&) = default; + NtpTimestamp(NtpTimestamp&&) = default; + NtpTimestamp& operator=(NtpTimestamp&&) = default; + + template + std::chrono::time_point ToTimePoint() const + { + // Convert NTP seconds to Unix seconds + // Don't cast to a larger type to account for rollover, and this should + // work until 2106 + const std::uint32_t unixSeconds = seconds_ - kNtpToUnixOffset_; + + // Convert NTP fraction to nanoseconds + const auto nanoseconds = + static_cast(fraction_) * _1e9 / kFractionalMultiplier_; + + return std::chrono::time_point( + std::chrono::duration_cast( + std::chrono::seconds {unixSeconds} + + std::chrono::nanoseconds {nanoseconds})); + } + + template + static NtpTimestamp FromTimePoint(std::chrono::time_point timePoint) + { + // Convert to duration since Unix epoch + const auto unixDuration = timePoint.time_since_epoch(); + + // Extract seconds and nanoseconds + const auto unixSeconds = + std::chrono::duration_cast(unixDuration); + const auto nanoseconds = + std::chrono::duration_cast(unixDuration - + unixSeconds); + + // Convert Unix seconds to NTP seconds + const auto ntpSeconds = + static_cast(unixSeconds.count() + kNtpToUnixOffset_); + + // Convert nanoseconds to NTP fractional seconds + const auto ntpFraction = static_cast( + nanoseconds.count() * kFractionalMultiplier_ / _1e9); + + return NtpTimestamp(ntpSeconds, ntpFraction); + } +}; + class NtpClient::Impl { public: explicit Impl(); ~Impl(); - Impl(const Impl&) = delete; - Impl& operator=(const Impl&) = delete; - Impl(const Impl&&) = delete; - Impl& operator=(const Impl&&) = delete; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(Impl&&) = delete; void Open(std::string_view host, std::string_view service); void Poll(); @@ -31,14 +103,22 @@ public: boost::asio::thread_pool threadPool_ {2u}; + bool enabled_; + types::ntp::NtpPacket transmitPacket_ {}; boost::asio::ip::udp::socket socket_; std::optional serverEndpoint_ {}; std::array receiveBuffer_ {}; - std::vector serverList_ { - "time.nist.gov", "ntp.pool.org", "time.windows.com"}; + std::chrono::system_clock::duration timeOffset_ {}; + + std::vector serverList_ {"time.nist.gov", + "time.cloudflare.com", + "ntp.pool.org", + "time.aws.com", + "time.windows.com", + "time.apple.com"}; }; NtpClient::NtpClient() : p(std::make_unique()) {} @@ -59,6 +139,18 @@ void NtpClient::Poll() NtpClient::Impl::Impl() : socket_ {threadPool_} { + using namespace std::chrono_literals; + + const auto now = + std::chrono::floor(std::chrono::system_clock::now()); + + // The NTP timestamp will overflow in 2036. Overflow is handled in such a way + // that should work until 2106. Additional handling for subsequent eras is + // required. + static constexpr auto kMaxYear_ = 2106y; + + enabled_ = now < kMaxYear_ / 1 / 1; + transmitPacket_.fields.vn = 3; // Version transmitPacket_.fields.mode = 3; // Client (3) } @@ -95,10 +187,15 @@ void NtpClient::Impl::Poll() try { + const auto originTimestamp = + NtpTimestamp::FromTimePoint(std::chrono::system_clock::now()); + transmitPacket_.txTm_s = ntohl(originTimestamp.seconds_); + transmitPacket_.txTm_f = ntohl(originTimestamp.fraction_); + std::size_t transmitPacketSize = sizeof(transmitPacket_); // Send NTP request socket_.send_to(boost::asio::buffer(&transmitPacket_, transmitPacketSize), - *serverEndpoint_); + *serverEndpoint_); // Receive NTP response auto future = @@ -131,8 +228,55 @@ void NtpClient::Impl::ReceivePacket(std::size_t length) { if (length >= sizeof(types::ntp::NtpPacket)) { - auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); - (void) packet; + const auto destinationTime = std::chrono::system_clock::now(); + + const auto packet = types::ntp::NtpPacket::Parse(receiveBuffer_); + + if (packet.stratum == 0) + { + const std::uint32_t refId = ntohl(packet.refId); + const std::string kod = + std::string(reinterpret_cast(&refId), 4); + + logger_->warn("KoD packet received: {}", kod); + + if (kod == "DENY" || kod == "RSTR") + { + // TODO + // The client MUST demobilize any associations to that server and + // stop sending packets to that server + } + else if (kod == "RATE") + { + // TODO + // The client MUST immediately reduce its polling interval to that + // server and continue to reduce it each time it receives a RATE + // kiss code + } + } + else + { + const auto originTimestamp = + NtpTimestamp(packet.origTm_s, packet.origTm_f); + const auto receiveTimestamp = + NtpTimestamp(packet.rxTm_s, packet.rxTm_f); + const auto transmitTimestamp = + NtpTimestamp(packet.txTm_s, packet.txTm_f); + + const auto originTime = originTimestamp.ToTimePoint(); + const auto receiveTime = receiveTimestamp.ToTimePoint(); + const auto transmitTime = transmitTimestamp.ToTimePoint(); + + const auto& t0 = originTime; + const auto& t1 = receiveTime; + const auto& t2 = transmitTime; + const auto& t3 = destinationTime; + + // Update time offset + timeOffset_ = ((t1 - t0) + (t2 - t3)) / 2; + + logger_->debug("Time offset updated: {:%jd %T}", timeOffset_); + } } else { diff --git a/wxdata/source/scwx/types/ntp_types.cpp b/wxdata/source/scwx/types/ntp_types.cpp index cca0c422..9ff39095 100644 --- a/wxdata/source/scwx/types/ntp_types.cpp +++ b/wxdata/source/scwx/types/ntp_types.cpp @@ -20,15 +20,6 @@ NtpPacket NtpPacket::Parse(const std::span data) packet = *reinterpret_cast(data.data()); - // Detect Kiss-o'-Death (KoD) packet - if (packet.stratum == 0) - { - // TODO - std::string kissCode = - std::string(reinterpret_cast(&packet.refId), 4); - (void) kissCode; - } - packet.rootDelay = ntohl(packet.rootDelay); packet.rootDispersion = ntohl(packet.rootDispersion); packet.refId = ntohl(packet.refId); From dfb00b96df4a9f1113ff744ab3d14c42147d777d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 10 Aug 2025 18:01:45 -0500 Subject: [PATCH 736/762] NTP polling --- test/source/scwx/network/ntp_client.test.cpp | 22 +- wxdata/include/scwx/network/ntp_client.hpp | 15 +- wxdata/source/scwx/network/ntp_client.cpp | 276 +++++++++++++++++-- 3 files changed, 281 insertions(+), 32 deletions(-) diff --git a/test/source/scwx/network/ntp_client.test.cpp b/test/source/scwx/network/ntp_client.test.cpp index cebd8cc2..bdfcb4ae 100644 --- a/test/source/scwx/network/ntp_client.test.cpp +++ b/test/source/scwx/network/ntp_client.test.cpp @@ -11,10 +11,24 @@ TEST(NtpClient, Poll) { NtpClient client {}; - client.Open("time.nist.gov", "123"); - //client.Open("pool.ntp.org", "123"); - //client.Open("time.windows.com", "123"); - client.Poll(); + const std::string firstServer = client.RotateServer(); + std::string currentServer = firstServer; + std::string lastServer = firstServer; + bool error = false; + + do + { + client.RunOnce(); + error = client.error(); + + EXPECT_EQ(error, false); + + // Loop until the current server repeats the first server, or fails to + // rotate + lastServer = currentServer; + currentServer = client.RotateServer(); + } while (currentServer != firstServer && currentServer != lastServer && + !error); } } // namespace network diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp index 55ef4204..6a0a78b7 100644 --- a/wxdata/include/scwx/network/ntp_client.hpp +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include namespace scwx::network @@ -21,8 +23,17 @@ public: NtpClient(NtpClient&&) noexcept; NtpClient& operator=(NtpClient&&) noexcept; - void Open(std::string_view host, std::string_view service); - void Poll(); + bool error(); + std::chrono::system_clock::duration time_offset() const; + + void Start(); + void Open(std::string_view host, std::string_view service); + void OpenCurrentServer(); + void Poll(); + std::string RotateServer(); + void RunOnce(); + + static std::shared_ptr Instance(); private: class Impl; diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index a20540c4..89aba5d0 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,12 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr std::size_t kReceiveBufferSize_ {48u}; +// Reasonable min/max values for polling intervals. We don't want to poll too +// quickly and upset the server, but we don't want to poll too slowly in the +// event of a time jump. +static constexpr std::uint32_t kMinPollInterval_ = 6u; // 2^6 = 64 seconds +static constexpr std::uint32_t kMaxPollInterval_ = 9u; // 2^9 = 512 seconds + class NtpTimestamp { public: @@ -97,28 +104,42 @@ public: Impl(Impl&&) = delete; Impl& operator=(Impl&&) = delete; - void Open(std::string_view host, std::string_view service); - void Poll(); - void ReceivePacket(std::size_t length); + void Open(std::string_view host, std::string_view service); + void OpenCurrentServer(); + void Poll(); + void ReceivePacket(std::size_t length); + std::string RotateServer(); + void Run(); + void RunOnce(); boost::asio::thread_pool threadPool_ {2u}; - bool enabled_; + boost::asio::steady_timer pollTimer_ {threadPool_}; + std::uint32_t pollInterval_ {kMinPollInterval_}; + + bool enabled_ {true}; + bool error_ {false}; + bool disableServer_ {false}; + bool rotateServer_ {false}; types::ntp::NtpPacket transmitPacket_ {}; - boost::asio::ip::udp::socket socket_; + boost::asio::ip::udp::socket socket_ {threadPool_}; std::optional serverEndpoint_ {}; std::array receiveBuffer_ {}; std::chrono::system_clock::duration timeOffset_ {}; - std::vector serverList_ {"time.nist.gov", - "time.cloudflare.com", - "ntp.pool.org", - "time.aws.com", - "time.windows.com", - "time.apple.com"}; + const std::vector serverList_ {"time.nist.gov", + "time.cloudflare.com", + "pool.ntp.org", + "time.aws.com", + "time.windows.com", + "time.apple.com"}; + std::vector disabledServers_ {}; + + std::vector::const_iterator currentServer_ = + serverList_.begin(); }; NtpClient::NtpClient() : p(std::make_unique()) {} @@ -127,17 +148,7 @@ NtpClient::~NtpClient() = default; NtpClient::NtpClient(NtpClient&&) noexcept = default; NtpClient& NtpClient::operator=(NtpClient&&) noexcept = default; -void NtpClient::Open(std::string_view host, std::string_view service) -{ - p->Open(host, service); -} - -void NtpClient::Poll() -{ - p->Poll(); -} - -NtpClient::Impl::Impl() : socket_ {threadPool_} +NtpClient::Impl::Impl() { using namespace std::chrono_literals; @@ -145,8 +156,8 @@ NtpClient::Impl::Impl() : socket_ {threadPool_} std::chrono::floor(std::chrono::system_clock::now()); // The NTP timestamp will overflow in 2036. Overflow is handled in such a way - // that should work until 2106. Additional handling for subsequent eras is - // required. + // that dates prior to 1970 result in a Unix timestamp after 2036. Additional + // handling for the year 2106 and subsequent eras is required. static constexpr auto kMaxYear_ = 2106y; enabled_ = now < kMaxYear_ / 1 / 1; @@ -160,6 +171,51 @@ NtpClient::Impl::~Impl() threadPool_.join(); } +bool NtpClient::error() +{ + bool returnValue = p->error_; + p->error_ = false; + return returnValue; +} + +std::chrono::system_clock::duration NtpClient::time_offset() const +{ + return p->timeOffset_; +} + +void NtpClient::Start() +{ + if (p->enabled_) + { + boost::asio::post(p->threadPool_, [this]() { p->Run(); }); + } +} + +void NtpClient::Open(std::string_view host, std::string_view service) +{ + p->Open(host, service); +} + +void NtpClient::OpenCurrentServer() +{ + p->OpenCurrentServer(); +} + +void NtpClient::Poll() +{ + p->Poll(); +} + +std::string NtpClient::RotateServer() +{ + return p->RotateServer(); +} + +void NtpClient::RunOnce() +{ + p->RunOnce(); +} + void NtpClient::Impl::Open(std::string_view host, std::string_view service) { boost::asio::ip::udp::resolver resolver(threadPool_); @@ -176,9 +232,15 @@ void NtpClient::Impl::Open(std::string_view host, std::string_view service) { serverEndpoint_ = std::nullopt; logger_->warn("Could not resolve host {}: {}", host, ec.message()); + rotateServer_ = true; } } +void NtpClient::Impl::OpenCurrentServer() +{ + Open(*currentServer_, "123"); +} + void NtpClient::Impl::Poll() { using namespace std::chrono_literals; @@ -215,6 +277,7 @@ void NtpClient::Impl::Poll() case std::future_status::deferred: logger_->warn("Timeout waiting for NTP response"); socket_.cancel(); + error_ = true; break; } } @@ -242,17 +305,29 @@ void NtpClient::Impl::ReceivePacket(std::size_t length) if (kod == "DENY" || kod == "RSTR") { - // TODO // The client MUST demobilize any associations to that server and // stop sending packets to that server + disableServer_ = true; } else if (kod == "RATE") { - // TODO // The client MUST immediately reduce its polling interval to that // server and continue to reduce it each time it receives a RATE // kiss code + if (pollInterval_ < kMaxPollInterval_) + { + ++pollInterval_; + } + else + { + // The server wants us to reduce the polling interval lower than + // what we deem useful. Move to the next server. + rotateServer_ = true; + } } + + // Consider a KoD packet an error + error_ = true; } else { @@ -276,12 +351,161 @@ void NtpClient::Impl::ReceivePacket(std::size_t length) timeOffset_ = ((t1 - t0) + (t2 - t3)) / 2; logger_->debug("Time offset updated: {:%jd %T}", timeOffset_); + + // TODO: Signal } } else { logger_->warn("Received too few bytes: {}", length); + error_ = true; } } +std::string NtpClient::Impl::RotateServer() +{ + socket_.close(); + + bool newServerFound = false; + + // Save the current server + const auto oldServer = currentServer_; + + while (!newServerFound) + { + // Increment the current server + ++currentServer_; + + // If we are at the end of the list, start over at the beginning + if (currentServer_ == serverList_.end()) + { + currentServer_ = serverList_.begin(); + } + + // If we have reached the end of the list, give up + if (currentServer_ == oldServer) + { + enabled_ = false; + break; + } + + // If the current server is disabled, continue searching + while (std::find(disabledServers_.cbegin(), + disabledServers_.cend(), + *currentServer_) != disabledServers_.cend()) + { + continue; + } + + // A new server has been found + newServerFound = true; + } + + pollInterval_ = kMinPollInterval_; + rotateServer_ = false; + + return *currentServer_; +} + +void NtpClient::Impl::Run() +{ + RunOnce(); + + if (enabled_) + { + std::chrono::seconds pollIntervalSeconds {1u << pollInterval_}; + pollTimer_.expires_after(pollIntervalSeconds); + pollTimer_.async_wait( + [this](const boost::system::error_code& e) + { + if (e == boost::asio::error::operation_aborted) + { + logger_->debug("Poll timer cancelled"); + } + else if (e != boost::system::errc::success) + { + logger_->warn("Poll timer error: {}", e.message()); + } + else + { + try + { + Run(); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + } + }); + } +} + +void NtpClient::Impl::RunOnce() +{ + if (disableServer_) + { + // Disable the current server + disabledServers_.push_back(*currentServer_); + + // Disable the NTP client if all servers are disabled + enabled_ = disabledServers_.size() == serverList_.size(); + + if (!enabled_) + { + error_ = true; + } + + disableServer_ = false; + rotateServer_ = enabled_; + } + + if (!enabled_ && socket_.is_open()) + { + // Sockets should be closed if the client is disabled + socket_.close(); + } + + if (rotateServer_) + { + // Rotate the server if requested + RotateServer(); + } + + if (enabled_ && !socket_.is_open()) + { + // Open the current server if it is not open + OpenCurrentServer(); + } + + if (socket_.is_open()) + { + // Send an NTP message to determine the current time offset + Poll(); + } + else if (enabled_) + { + // Did not poll this frame + error_ = true; + } +} + +std::shared_ptr NtpClient::Instance() +{ + static std::weak_ptr ntpClientReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr ntpClient = ntpClientReference_.lock(); + + if (ntpClient == nullptr) + { + ntpClient = std::make_shared(); + ntpClientReference_ = ntpClient; + } + + return ntpClient; +} + } // namespace scwx::network From 6b26d4e7b2bca5897ea46922652820a0d7b49b9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:29:46 +0000 Subject: [PATCH 737/762] Update actions/checkout action to v5 --- .github/workflows/ci.yml | 2 +- .github/workflows/clang-format-check.yml | 2 +- .github/workflows/clang-tidy-review.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19e80256..525cb049 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: run: git config --global core.longpaths true - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: source submodules: recursive diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index 02c5cdd3..7eaa1e95 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 submodules: false diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 4feb238e..10059650 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -36,13 +36,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: source submodules: recursive - name: Checkout clang-tidy-review Repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: ZedThree/clang-tidy-review ref: v0.20.1 From f497f1ef84fbd3434479a914fdad320f1e64f74e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:47:21 +0000 Subject: [PATCH 738/762] Update dependency gtest to v1.17.0 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 65702ed7..c6569a16 100644 --- a/conanfile.py +++ b/conanfile.py @@ -12,7 +12,7 @@ class SupercellWxConan(ConanFile): "geographiclib/2.4", "geos/3.13.0", "glm/1.0.1", - "gtest/1.16.0", + "gtest/1.17.0", "libcurl/8.12.1", "libpng/1.6.50", "libxml2/2.13.8", From 86838739f020aa64f298c25a4110dcc089fed2da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:02:23 +0000 Subject: [PATCH 739/762] Update dependency libxml2 to v2.14.5 --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 65702ed7..55d7d5f2 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,7 +15,7 @@ class SupercellWxConan(ConanFile): "gtest/1.16.0", "libcurl/8.12.1", "libpng/1.6.50", - "libxml2/2.13.8", + "libxml2/2.14.5", "openssl/3.5.0", "range-v3/0.12.0", "re2/20250722", From 63e6ba770939953ececed6ecbb4ad9e4bfbdd91a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 19:25:01 -0500 Subject: [PATCH 740/762] Update initialization order to ensure initial log entries make it to log file --- scwx-qt/source/scwx/qt/main/main.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 13fb3a05..b255c7b6 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -53,10 +53,19 @@ int main(int argc, char* argv[]) args.push_back(argv[i]); } + if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) + { + QStandardPaths::setTestModeEnabled(true); + } + // Initialize logger auto& logManager = scwx::qt::manager::LogManager::Instance(); logManager.Initialize(); + QCoreApplication::setApplicationName("Supercell Wx"); + + logManager.InitializeLogFile(); + logger_->info("Supercell Wx v{}.{} ({})", scwx::qt::main::kVersionString_, scwx::qt::main::kBuildNumber_, @@ -66,7 +75,6 @@ int main(int argc, char* argv[]) QApplication a(argc, argv); - QCoreApplication::setApplicationName("Supercell Wx"); scwx::network::cpr::SetUserAgent( fmt::format("SupercellWx/{}", scwx::qt::main::kVersionString_)); @@ -77,11 +85,6 @@ int main(int argc, char* argv[]) QCoreApplication::installTranslator(&translator); } - if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) - { - QStandardPaths::setTestModeEnabled(true); - } - // Test to see if scwx was run with high privilege scwx::qt::main::PrivilegeChecker privilegeChecker; if (privilegeChecker.pre_settings_check()) @@ -116,7 +119,6 @@ int main(int argc, char* argv[]) Aws::InitAPI(awsSdkOptions); // Initialize application - logManager.InitializeLogFile(); scwx::qt::config::RadarSite::Initialize(); scwx::qt::config::CountyDatabase::Initialize(); scwx::qt::manager::SettingsManager::Instance().Initialize(); From 88d968a533ab0731f292d823c22cc58ec39a9409 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 20:04:21 -0500 Subject: [PATCH 741/762] Run NTP client in the background --- scwx-qt/scwx-qt.cmake | 2 ++ scwx-qt/source/scwx/qt/main/main.cpp | 3 ++ .../source/scwx/qt/manager/task_manager.cpp | 29 +++++++++++++++++++ .../source/scwx/qt/manager/task_manager.hpp | 9 ++++++ wxdata/include/scwx/network/ntp_client.hpp | 4 ++- wxdata/source/scwx/network/ntp_client.cpp | 8 +++++ 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/source/scwx/qt/manager/task_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/task_manager.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 194601e9..ecd26ef9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -109,6 +109,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/radar_product_manager_notifier.hpp source/scwx/qt/manager/resource_manager.hpp source/scwx/qt/manager/settings_manager.hpp + source/scwx/qt/manager/task_manager.hpp source/scwx/qt/manager/text_event_manager.hpp source/scwx/qt/manager/thread_manager.hpp source/scwx/qt/manager/timeline_manager.hpp @@ -126,6 +127,7 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp source/scwx/qt/manager/radar_product_manager_notifier.cpp source/scwx/qt/manager/resource_manager.cpp source/scwx/qt/manager/settings_manager.cpp + source/scwx/qt/manager/task_manager.cpp source/scwx/qt/manager/text_event_manager.cpp source/scwx/qt/manager/thread_manager.cpp source/scwx/qt/manager/timeline_manager.cpp diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index b255c7b6..a2416cec 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -121,6 +122,7 @@ int main(int argc, char* argv[]) // Initialize application scwx::qt::config::RadarSite::Initialize(); scwx::qt::config::CountyDatabase::Initialize(); + scwx::qt::manager::TaskManager::Initialize(); scwx::qt::manager::SettingsManager::Instance().Initialize(); scwx::qt::manager::ResourceManager::Initialize(); @@ -181,6 +183,7 @@ int main(int argc, char* argv[]) // Shutdown application scwx::qt::manager::ResourceManager::Shutdown(); scwx::qt::manager::SettingsManager::Instance().Shutdown(); + scwx::qt::manager::TaskManager::Shutdown(); // Shutdown AWS SDK Aws::ShutdownAPI(awsSdkOptions); diff --git a/scwx-qt/source/scwx/qt/manager/task_manager.cpp b/scwx-qt/source/scwx/qt/manager/task_manager.cpp new file mode 100644 index 00000000..130318f5 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/task_manager.cpp @@ -0,0 +1,29 @@ +#include +#include +#include + +namespace scwx::qt::manager::TaskManager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::task_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static std::shared_ptr ntpClient_ {}; + +void Initialize() +{ + logger_->debug("Initialize"); + + ntpClient_ = network::NtpClient::Instance(); + + ntpClient_->Start(); +} + +void Shutdown() +{ + logger_->debug("Shutdown"); + + ntpClient_->Stop(); +} + +} // namespace scwx::qt::manager::TaskManager diff --git a/scwx-qt/source/scwx/qt/manager/task_manager.hpp b/scwx-qt/source/scwx/qt/manager/task_manager.hpp new file mode 100644 index 00000000..bb50fb3b --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/task_manager.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace scwx::qt::manager::TaskManager +{ + +void Initialize(); +void Shutdown(); + +} // namespace scwx::qt::manager::TaskManager diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp index 6a0a78b7..6462a6e9 100644 --- a/wxdata/include/scwx/network/ntp_client.hpp +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -26,7 +26,9 @@ public: bool error(); std::chrono::system_clock::duration time_offset() const; - void Start(); + void Start(); + void Stop(); + void Open(std::string_view host, std::string_view service); void OpenCurrentServer(); void Poll(); diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index 89aba5d0..45c0ad74 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -191,6 +191,14 @@ void NtpClient::Start() } } +void NtpClient::Stop() +{ + p->enabled_ = false; + p->socket_.cancel(); + p->pollTimer_.cancel(); + p->threadPool_.join(); +} + void NtpClient::Open(std::string_view host, std::string_view service) { p->Open(host, service); From c76c9b57ed93f841daa437a04c0c8aeaab000216 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 22:28:55 -0500 Subject: [PATCH 742/762] Wait for an initial offset prior to proceeding with initialization --- .../source/scwx/qt/manager/task_manager.cpp | 1 + wxdata/include/scwx/network/ntp_client.hpp | 2 + wxdata/source/scwx/network/ntp_client.cpp | 49 +++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/task_manager.cpp b/scwx-qt/source/scwx/qt/manager/task_manager.cpp index 130318f5..66b7285a 100644 --- a/scwx-qt/source/scwx/qt/manager/task_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/task_manager.cpp @@ -17,6 +17,7 @@ void Initialize() ntpClient_ = network::NtpClient::Instance(); ntpClient_->Start(); + ntpClient_->WaitForInitialOffset(); } void Shutdown() diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp index 6462a6e9..beb4be21 100644 --- a/wxdata/include/scwx/network/ntp_client.hpp +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -35,6 +35,8 @@ public: std::string RotateServer(); void RunOnce(); + void WaitForInitialOffset(); + static std::shared_ptr Instance(); private: diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index 45c0ad74..0a340f87 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include + #include #include #include @@ -112,6 +115,8 @@ public: void Run(); void RunOnce(); + void FinishInitialization(); + boost::asio::thread_pool threadPool_ {2u}; boost::asio::steady_timer pollTimer_ {threadPool_}; @@ -122,6 +127,10 @@ public: bool disableServer_ {false}; bool rotateServer_ {false}; + std::mutex initializationMutex_ {}; + std::condition_variable initializationCondition_ {}; + std::atomic initialized_ {false}; + types::ntp::NtpPacket transmitPacket_ {}; boost::asio::ip::udp::socket socket_ {threadPool_}; @@ -164,6 +173,14 @@ NtpClient::Impl::Impl() transmitPacket_.fields.vn = 3; // Version transmitPacket_.fields.mode = 3; // Client (3) + + // If the NTP client is enabled, wait until the first refresh to consider + // "initialized". Otherwise, mark as initialized immediately to prevent a + // deadlock. + if (!enabled_) + { + initialized_ = true; + } } NtpClient::Impl::~Impl() @@ -253,7 +270,7 @@ void NtpClient::Impl::Poll() { using namespace std::chrono_literals; - static constexpr auto kTimeout_ = 15s; + static constexpr auto kTimeout_ = 5s; try { @@ -359,8 +376,6 @@ void NtpClient::Impl::ReceivePacket(std::size_t length) timeOffset_ = ((t1 - t0) + (t2 - t3)) / 2; logger_->debug("Time offset updated: {:%jd %T}", timeOffset_); - - // TODO: Signal } } else @@ -496,6 +511,34 @@ void NtpClient::Impl::RunOnce() // Did not poll this frame error_ = true; } + + FinishInitialization(); +} + +void NtpClient::Impl::FinishInitialization() +{ + if (!initialized_) + { + // Set initialized to true + std::unique_lock lock(initializationMutex_); + initialized_ = true; + lock.unlock(); + + // Notify any threads waiting for initialization + initializationCondition_.notify_all(); + } +} + +void NtpClient::WaitForInitialOffset() +{ + std::unique_lock lock(p->initializationMutex_); + + // While not yet initialized + while (!p->initialized_) + { + // Wait for initialization + p->initializationCondition_.wait(lock); + } } std::shared_ptr NtpClient::Instance() From 719142ca12c25febead8edc5a23f5aa5fd3cfe0f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 22:29:25 -0500 Subject: [PATCH 743/762] UTC in the main window should use the NTP time offset --- scwx-qt/source/scwx/qt/main/main_window.cpp | 4 +-- wxdata/include/scwx/util/time.hpp | 23 ++++++++++++++---- wxdata/source/scwx/util/time.cpp | 27 +++++++++++++++++++-- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 4ab5d56b..4fa2f28a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1300,8 +1300,8 @@ void MainWindowImpl::ConnectOtherSignals() this, [this]() { - timeLabel_->setText(QString::fromStdString( - util::TimeString(std::chrono::system_clock::now()))); + timeLabel_->setText( + QString::fromStdString(util::TimeString(util::time::now()))); timeLabel_->setVisible(true); }); clockTimer_.start(1000); diff --git a/wxdata/include/scwx/util/time.hpp b/wxdata/include/scwx/util/time.hpp index e31e8ca9..fe052756 100644 --- a/wxdata/include/scwx/util/time.hpp +++ b/wxdata/include/scwx/util/time.hpp @@ -10,9 +10,7 @@ # include #endif -namespace scwx -{ -namespace util +namespace scwx::util::time { #if (__cpp_lib_chrono >= 201907L) @@ -34,6 +32,9 @@ typedef scwx::util:: ClockFormat GetClockFormat(const std::string& name); const std::string& GetClockFormatName(ClockFormat clockFormat); +template +std::chrono::time_point now(); + std::chrono::system_clock::time_point TimePoint(uint32_t modifiedJulianDate, uint32_t milliseconds); @@ -46,5 +47,17 @@ template std::optional> TryParseDateTime(const std::string& dateTimeFormat, const std::string& str); -} // namespace util -} // namespace scwx +} // namespace scwx::util::time + +namespace scwx::util +{ +// Add types and functions to scwx::util for compatibility +using time::ClockFormat; +using time::ClockFormatIterator; +using time::GetClockFormat; +using time::GetClockFormatName; +using time::time_zone; +using time::TimePoint; +using time::TimeString; +using time::TryParseDateTime; +} // namespace scwx::util diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 563aea1b..1a687d32 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -8,6 +8,7 @@ # define __cpp_lib_format 202110L #endif +#include #include #include #include @@ -21,7 +22,7 @@ # include #endif -namespace scwx::util +namespace scwx::util::time { static const std::string logPrefix_ = "scwx::util::time"; @@ -32,6 +33,8 @@ static const std::unordered_map clockFormatName_ { {ClockFormat::_24Hour, "24-hour"}, {ClockFormat::Unknown, "?"}}; +static std::shared_ptr ntpClient_ {nullptr}; + SCWX_GET_ENUM(ClockFormat, GetClockFormat, clockFormatName_) const std::string& GetClockFormatName(ClockFormat clockFormat) @@ -39,6 +42,26 @@ const std::string& GetClockFormatName(ClockFormat clockFormat) return clockFormatName_.at(clockFormat); } +template +std::chrono::time_point now() +{ + if (ntpClient_ == nullptr) + { + ntpClient_ = network::NtpClient::Instance(); + } + + if (ntpClient_ != nullptr) + { + return Clock::now() + ntpClient_->time_offset(); + } + else + { + return Clock::now(); + } +} + +template std::chrono::time_point now(); + std::chrono::system_clock::time_point TimePoint(uint32_t modifiedJulianDate, uint32_t milliseconds) { @@ -153,4 +176,4 @@ template std::optional> TryParseDateTime(const std::string& dateTimeFormat, const std::string& str); -} // namespace scwx::util +} // namespace scwx::util::time From e0a4dee72bde110ac3860a57a8db11985263d575 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 22:57:03 -0500 Subject: [PATCH 744/762] Don't flag non-const global variables --- .clang-tidy | 1 + 1 file changed, 1 insertion(+) diff --git a/.clang-tidy b/.clang-tidy index c93d61fe..0ca21696 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -8,6 +8,7 @@ Checks: - 'performance-*' - '-bugprone-easily-swappable-parameters' - '-cppcoreguidelines-avoid-do-while' + - '-cppcoreguidelines-avoid-non-const-global-variables' - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-cppcoreguidelines-pro-type-union-access' - '-misc-include-cleaner' From bd27d0e56246dcfb203bd7be36d4a7f784beb6a1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 22 Aug 2025 23:42:08 -0500 Subject: [PATCH 745/762] Update most instances of current time to use a time offset --- scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp | 5 +++-- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 5 +++-- scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp | 5 +++-- scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp | 3 ++- scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp | 5 +++-- .../source/scwx/qt/gl/draw/placefile_polygons.cpp | 3 ++- scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp | 3 ++- .../source/scwx/qt/gl/draw/placefile_triangles.cpp | 3 ++- scwx-qt/source/scwx/qt/manager/alert_manager.cpp | 7 ++++--- .../source/scwx/qt/manager/radar_product_manager.cpp | 9 +++++---- .../source/scwx/qt/manager/text_event_manager.cpp | 2 +- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 12 ++++++------ scwx-qt/source/scwx/qt/map/alert_layer.cpp | 4 ++-- scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp | 3 ++- scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp | 4 ++-- scwx-qt/source/scwx/qt/view/overlay_product_view.cpp | 2 +- .../provider/aws_level2_chunks_data_provider.cpp | 5 ++--- .../scwx/provider/aws_nexrad_data_provider.cpp | 4 ++-- wxdata/source/scwx/provider/warnings_provider.cpp | 2 +- 19 files changed, 48 insertions(+), 38 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp index ba3162e9..05cc26a5 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_icons.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -313,7 +314,7 @@ void GeoIcons::Render(const QMapLibre::CustomLayerRenderParameters& params, // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, @@ -930,7 +931,7 @@ bool GeoIcons::RunMousePicking( // If no time has been selected, use the current time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; // For each pickable icon diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index aa376b00..e35cca4f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -284,7 +285,7 @@ void GeoLines::Render(const QMapLibre::CustomLayerRenderParameters& params) // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, @@ -723,7 +724,7 @@ bool GeoLines::RunMousePicking( // If no time has been selected, use the current time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; // For each pickable line diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp index 9ce7dc8f..21dd25c2 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_icons.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -295,7 +296,7 @@ void PlacefileIcons::Render( // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, @@ -720,7 +721,7 @@ bool PlacefileIcons::RunMousePicking( // If no time has been selected, use the current time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; // For each pickable icon diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp index 16d4d19b..b14a2723 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_images.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -264,7 +265,7 @@ void PlacefileImages::Render( // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp index 134d9c6b..b908389c 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_lines.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -248,7 +249,7 @@ void PlacefileLines::Render( // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, @@ -526,7 +527,7 @@ bool PlacefileLines::RunMousePicking( // If no time has been selected, use the current time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; // For each pickable line diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp index 646739ef..a30991f9 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_polygons.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -259,7 +260,7 @@ void PlacefilePolygons::Render( // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 832a1292..93aa4461 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -127,7 +128,7 @@ void PlacefileText::Impl::RenderTextDrawItem( // If no time has been selected, use the current time std::chrono::system_clock::time_point selectedTime = (selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : selectedTime_; const bool thresholdMet = diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp index 5ad54bc7..222713ae 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_triangles.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -203,7 +204,7 @@ void PlacefileTriangles::Render( // Selected time std::chrono::system_clock::time_point selectedTime = (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? - std::chrono::system_clock::now() : + scwx::util::time::now() : p->selectedTime_; glUniform1i( p->uSelectedTimeLocation_, diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index 748e0943..41e74d7a 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -2,12 +2,13 @@ #include #include #include +#include #include +#include #include #include #include -#include -#include +#include #include #include @@ -172,7 +173,7 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, // 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 || + if (eventEnd < scwx::util::time::now() || !alertActive || !audioSettings.alert_enabled(phenomenon).GetValue()) { continue; diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 13787496..bda6e232 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -821,7 +822,7 @@ void RadarProductManagerImpl::RefreshDataSync( auto latestTime = providerManager->provider_->FindLatestTime(); auto updatePeriod = providerManager->provider_->update_period(); auto lastModified = providerManager->provider_->last_modified(); - auto sinceLastModified = std::chrono::system_clock::now() - lastModified; + auto sinceLastModified = scwx::util::time::now() - lastModified; // For the default interval, assume products are updated at a // constant rate. Expect the next product at a time based on the @@ -939,7 +940,7 @@ RadarProductManager::GetActiveVolumeTimes( [&](const auto& date) { // Don't query for a time point in the future - if (date > std::chrono::system_clock::now()) + if (date > scwx::util::time::now()) { return; } @@ -1259,7 +1260,7 @@ void RadarProductManagerImpl::PopulateProductTimes( [&](const auto& date) { // Don't query for a time point in the future - if (date > std::chrono::system_clock::now()) + if (date > scwx::util::time::now()) { return; } @@ -1556,7 +1557,7 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, bool needArchive = true; static const auto maxChunkDelay = std::chrono::minutes(10); const std::chrono::system_clock::time_point firstValidChunkTime = - (isEpox ? std::chrono::system_clock::now() : time) - maxChunkDelay; + (isEpox ? scwx::util::time::now() : time) - maxChunkDelay; // See if we have this one in the chunk provider. auto chunkFile = std::dynamic_pointer_cast( diff --git a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp index d7759275..009f2441 100644 --- a/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/text_event_manager.cpp @@ -678,7 +678,7 @@ void TextEventManager::Impl::Refresh() // If the time jumps, we should attempt to load from no later than the // previous load time auto loadTime = - std::chrono::floor(std::chrono::system_clock::now()); + std::chrono::floor(scwx::util::time::now()); auto startTime = loadTime - loadHistoryDuration_; if (prevLoadTime_ != std::chrono::sys_time {}) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 0bcf4f68..70c4b2ed 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -218,7 +218,7 @@ void TimelineManager::AnimationStepBegin() p->pinnedTime_ == std::chrono::system_clock::time_point {}) { // If the selected view type is live, select the current products - p->SelectTimeAsync(std::chrono::system_clock::now() - p->loopTime_); + p->SelectTimeAsync(scwx::util::time::now() - p->loopTime_); } else { @@ -385,8 +385,8 @@ TimelineManager::Impl::GetLoopStartAndEndTimes() if (viewType_ == types::MapTime::Live || pinnedTime_ == std::chrono::system_clock::time_point {}) { - endTime = std::chrono::floor( - std::chrono::system_clock::now()); + endTime = + std::chrono::floor(scwx::util::time::now()); } else { @@ -656,8 +656,8 @@ void TimelineManager::Impl::Step(Direction direction) { if (direction == Direction::Back) { - newTime = std::chrono::floor( - std::chrono::system_clock::now()); + newTime = + std::chrono::floor(scwx::util::time::now()); } else { @@ -688,7 +688,7 @@ void TimelineManager::Impl::Step(Direction direction) newTime += 1min; // If the new time is more than 2 minutes in the future, stop stepping - if (newTime > std::chrono::system_clock::now() + 2min) + if (newTime > scwx::util::time::now() + 2min) { break; } diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 6599e0e5..7bea5938 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -581,8 +582,7 @@ void AlertLayer::Impl::ScheduleRefresh() // Expires at the top of the next minute std::chrono::system_clock::time_point now = - std::chrono::floor( - std::chrono::system_clock::now()); + std::chrono::floor(scwx::util::time::now()); refreshTimer_.expires_at(now + 1min); refreshTimer_.async_wait( diff --git a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp index 4dfeca41..0fa55d96 100644 --- a/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_proxy_model.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -67,7 +68,7 @@ bool AlertProxyModel::filterAcceptsRow(int sourceRow, .value(); // Compare end time to current - if (endTime < std::chrono::system_clock::now()) + if (endTime < scwx::util::time::now()) { acceptAlertActiveFilter = false; } diff --git a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp index 85fdb74f..eac16c44 100644 --- a/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/animation_dock_widget.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -81,8 +82,7 @@ AnimationDockWidget::AnimationDockWidget(QWidget* parent) : p->timeZone_ = date::get_tzdb().locate_zone("UTC"); #endif const std::chrono::sys_seconds currentTimePoint = - std::chrono::floor( - std::chrono::system_clock::now()); + std::chrono::floor(scwx::util::time::now()); p->SetTimePoint(currentTimePoint); // Update maximum date on a timer diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index 3200dcca..ccc41b9d 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -187,7 +187,7 @@ void OverlayProductView::Impl::LoadProduct( header.date_of_message(), header.time_of_message() * 1000); // If the record is from the last 30 minutes - if (productTime + 30min >= std::chrono::system_clock::now() || + if (productTime + 30min >= scwx::util::time::now() || (selectedTime_ != std::chrono::system_clock::time_point {} && productTime + 30min >= selectedTime_)) { diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index 74a9afd7..f7de36ca 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -302,7 +302,7 @@ AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) if (scanTimeIt != scanTimes_.cend()) { // If the time is greater than 2 hours ago, it may be a new scan - auto replaceBy = system_clock::now() - hours {2}; + auto replaceBy = util::time::now() - hours {2}; if (scanTimeIt->second > replaceBy) { return scanTimeIt->second; @@ -333,8 +333,7 @@ AwsLevel2ChunksDataProvider::Impl::ListObjects() size_t newObjects = 0; const size_t totalObjects = 0; - const std::chrono::system_clock::time_point now = - std::chrono::system_clock::now(); + const std::chrono::system_clock::time_point now = util::time::now(); if (currentScan_.valid_ && !currentScan_.hasAllFiles_ && lastTimeListed_ + std::chrono::minutes(2) > now) diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index dceb45d8..97528a9e 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -352,7 +352,7 @@ std::pair AwsNexradDataProvider::Refresh() logger_->debug("Refresh()"); - auto today = floor(system_clock::now()); + auto today = floor(util::time::now()); auto yesterday = today - days {1}; std::unique_lock lock(p->refreshMutex_); @@ -388,7 +388,7 @@ void AwsNexradDataProvider::Impl::PruneObjects() { using namespace std::chrono; - auto today = floor(system_clock::now()); + auto today = floor(util::time::now()); auto yesterday = today - days {1}; std::unique_lock lock(objectsMutex_); diff --git a/wxdata/source/scwx/provider/warnings_provider.cpp b/wxdata/source/scwx/provider/warnings_provider.cpp index 78eb687c..a762cd99 100644 --- a/wxdata/source/scwx/provider/warnings_provider.cpp +++ b/wxdata/source/scwx/provider/warnings_provider.cpp @@ -108,7 +108,7 @@ WarningsProvider::LoadUpdatedFiles( std::vector> updatedFiles; const std::chrono::sys_time now = - std::chrono::floor(std::chrono::system_clock::now()); + std::chrono::floor(util::time::now()); std::chrono::sys_time currentHour = (startTime != std::chrono::sys_time {}) ? startTime : From c5b858021f0bc2192699fedb81cd977a9c387fed Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Aug 2025 22:14:30 -0500 Subject: [PATCH 746/762] #include is required --- wxdata/source/scwx/network/ntp_client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index 0a340f87..e95599dd 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include From 193f42318ca34ffa1e3e9f8dba4d8641069ab6a0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Aug 2025 22:16:30 -0500 Subject: [PATCH 747/762] li/vn/mode struct should not be anonymous --- wxdata/include/scwx/types/ntp_types.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp index cfb8f764..92c18fa7 100644 --- a/wxdata/include/scwx/types/ntp_types.hpp +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -23,7 +23,7 @@ struct NtpPacket union { std::uint8_t li_vn_mode; - struct + struct LiVnMode { std::uint8_t mode : 3; // Client will pick mode 3 for client. std::uint8_t vn : 3; // Version number of the protocol. From 072865d2e5277048abac77d1c4da3091950760cc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Aug 2025 22:28:38 -0500 Subject: [PATCH 748/762] Addressing clang-tidy findings --- test/.clang-tidy | 4 ++++ test/source/scwx/network/ntp_client.test.cpp | 7 ++----- wxdata/include/scwx/network/ntp_client.hpp | 5 +++-- wxdata/source/scwx/network/ntp_client.cpp | 20 ++++++++++++++------ wxdata/source/scwx/types/ntp_types.cpp | 2 +- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/test/.clang-tidy b/test/.clang-tidy index d5079a03..254b44ad 100644 --- a/test/.clang-tidy +++ b/test/.clang-tidy @@ -8,9 +8,13 @@ Checks: - 'performance-*' - '-bugprone-easily-swappable-parameters' - '-cppcoreguidelines-avoid-magic-numbers' + - '-cppcoreguidelines-avoid-do-while' + - '-cppcoreguidelines-avoid-non-const-global-variables' - '-cppcoreguidelines-pro-type-reinterpret-cast' + - '-cppcoreguidelines-pro-type-union-access' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' + - '-misc-use-anonymous-namespace' - '-modernize-return-braced-init-list' - '-modernize-use-trailing-return-type' FormatStyle: 'file' diff --git a/test/source/scwx/network/ntp_client.test.cpp b/test/source/scwx/network/ntp_client.test.cpp index bdfcb4ae..1450b324 100644 --- a/test/source/scwx/network/ntp_client.test.cpp +++ b/test/source/scwx/network/ntp_client.test.cpp @@ -2,9 +2,7 @@ #include -namespace scwx -{ -namespace network +namespace scwx::network { TEST(NtpClient, Poll) @@ -31,5 +29,4 @@ TEST(NtpClient, Poll) !error); } -} // namespace network -} // namespace scwx +} // namespace scwx::network diff --git a/wxdata/include/scwx/network/ntp_client.hpp b/wxdata/include/scwx/network/ntp_client.hpp index beb4be21..8be912b3 100644 --- a/wxdata/include/scwx/network/ntp_client.hpp +++ b/wxdata/include/scwx/network/ntp_client.hpp @@ -23,8 +23,9 @@ public: NtpClient(NtpClient&&) noexcept; NtpClient& operator=(NtpClient&&) noexcept; - bool error(); - std::chrono::system_clock::duration time_offset() const; + bool error(); + + [[nodiscard]] std::chrono::system_clock::duration time_offset() const; void Start(); void Stop(); diff --git a/wxdata/source/scwx/network/ntp_client.cpp b/wxdata/source/scwx/network/ntp_client.cpp index e95599dd..494aadde 100644 --- a/wxdata/source/scwx/network/ntp_client.cpp +++ b/wxdata/source/scwx/network/ntp_client.cpp @@ -56,7 +56,7 @@ public: NtpTimestamp& operator=(NtpTimestamp&&) = default; template - std::chrono::time_point ToTimePoint() const + [[nodiscard]] std::chrono::time_point ToTimePoint() const { // Convert NTP seconds to Unix seconds // Don't cast to a larger type to account for rollover, and this should @@ -191,8 +191,8 @@ NtpClient::Impl::~Impl() bool NtpClient::error() { - bool returnValue = p->error_; - p->error_ = false; + const bool returnValue = p->error_; + p->error_ = false; return returnValue; } @@ -273,6 +273,13 @@ void NtpClient::Impl::Poll() static constexpr auto kTimeout_ = 5s; + if (!serverEndpoint_.has_value()) + { + logger_->error("Server endpoint not set"); + error_ = true; + return; + } + try { const auto originTimestamp = @@ -280,7 +287,7 @@ void NtpClient::Impl::Poll() transmitPacket_.txTm_s = ntohl(originTimestamp.seconds_); transmitPacket_.txTm_f = ntohl(originTimestamp.fraction_); - std::size_t transmitPacketSize = sizeof(transmitPacket_); + const std::size_t transmitPacketSize = sizeof(transmitPacket_); // Send NTP request socket_.send_to(boost::asio::buffer(&transmitPacket_, transmitPacketSize), *serverEndpoint_); @@ -310,6 +317,7 @@ void NtpClient::Impl::Poll() catch (const std::exception& ex) { logger_->error("Error polling: {}", ex.what()); + error_ = true; } } @@ -437,7 +445,7 @@ void NtpClient::Impl::Run() if (enabled_) { - std::chrono::seconds pollIntervalSeconds {1u << pollInterval_}; + const std::chrono::seconds pollIntervalSeconds {1u << pollInterval_}; pollTimer_.expires_after(pollIntervalSeconds); pollTimer_.async_wait( [this](const boost::system::error_code& e) @@ -547,7 +555,7 @@ std::shared_ptr NtpClient::Instance() static std::weak_ptr ntpClientReference_ {}; static std::mutex instanceMutex_ {}; - std::unique_lock lock(instanceMutex_); + const std::unique_lock lock(instanceMutex_); std::shared_ptr ntpClient = ntpClientReference_.lock(); diff --git a/wxdata/source/scwx/types/ntp_types.cpp b/wxdata/source/scwx/types/ntp_types.cpp index 9ff39095..daf5d46b 100644 --- a/wxdata/source/scwx/types/ntp_types.cpp +++ b/wxdata/source/scwx/types/ntp_types.cpp @@ -14,7 +14,7 @@ namespace scwx::types::ntp NtpPacket NtpPacket::Parse(const std::span data) { - NtpPacket packet; + NtpPacket packet {}; assert(data.size() >= sizeof(NtpPacket)); From d9a7858c2da84f884aceb419424775c6257c8c6d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 24 Aug 2025 23:13:42 -0500 Subject: [PATCH 749/762] Fix nested structure compliance with C++ standard --- wxdata/include/scwx/types/ntp_types.hpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/wxdata/include/scwx/types/ntp_types.hpp b/wxdata/include/scwx/types/ntp_types.hpp index 92c18fa7..db1aa61f 100644 --- a/wxdata/include/scwx/types/ntp_types.hpp +++ b/wxdata/include/scwx/types/ntp_types.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -20,15 +19,17 @@ namespace scwx::types::ntp struct NtpPacket { + struct LiVnMode + { + std::uint8_t mode : 3; // Client will pick mode 3 for client. + std::uint8_t vn : 3; // Version number of the protocol. + std::uint8_t li : 2; // Leap indicator. + }; + union { std::uint8_t li_vn_mode; - struct LiVnMode - { - std::uint8_t mode : 3; // Client will pick mode 3 for client. - std::uint8_t vn : 3; // Version number of the protocol. - std::uint8_t li : 2; // Leap indicator. - } fields; + LiVnMode fields; }; std::uint8_t stratum; // Stratum level of the local clock. From 89de7718e6bb6e7a28707596e012128e915d8881 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 25 Aug 2025 23:27:11 -0500 Subject: [PATCH 750/762] Don't run NTP tests on CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19e80256..093cfcbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -393,7 +393,7 @@ jobs: env: MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }} MAPTILER_API_KEY: ${{ secrets.MAPTILER_API_KEY }} - run: ctest -C ${{ matrix.build_type }} --exclude-regex "test_mln.*|UpdateManager.*" + run: ctest -C ${{ matrix.build_type }} --exclude-regex "test_mln.*|NtpClient.*|UpdateManager.*" - name: Upload Test Logs if: ${{ !cancelled() }} From fa71e1fc85b81a5a418fce553efbdd972f0462eb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 26 Aug 2025 23:26:28 -0500 Subject: [PATCH 751/762] Update Qt to 6.9.2 --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/clang-tidy-review.yml | 2 +- CMakePresets.json | 10 +++++----- tools/lib/common-paths.bat | 2 +- tools/lib/common-paths.sh | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5049fbd..e3033807 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: ldflags: '' msvc_arch: x64 msvc_version: 2022 - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: win64_msvc2022_64 qt_arch_dir: msvc2022_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -47,7 +47,7 @@ jobs: compiler: gcc cppflags: '' ldflags: '' - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -65,7 +65,7 @@ jobs: compiler: clang cppflags: '' ldflags: '' - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: linux_gcc_64 qt_arch_dir: gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -83,7 +83,7 @@ jobs: compiler: gcc cppflags: '' ldflags: '' - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: linux_gcc_arm64 qt_arch_dir: gcc_arm64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -99,7 +99,7 @@ jobs: env_cc: clang env_cxx: clang++ compiler: clang - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: clang_64 qt_arch_dir: macos qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport @@ -114,7 +114,7 @@ jobs: env_cc: clang env_cxx: clang++ compiler: clang - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: clang_64 qt_arch_dir: macos qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 10059650..b3de8edf 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -20,7 +20,7 @@ jobs: build_type: Release env_cc: clang-18 env_cxx: clang++-18 - qt_version: 6.8.3 + qt_version: 6.9.2 qt_arch_aqt: linux_gcc_64 qt_modules: qtimageformats qtmultimedia qtpositioning qtserialport qt_tools: '' diff --git a/CMakePresets.json b/CMakePresets.json index 9fec9614..ec997016 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -54,7 +54,7 @@ "inherits": "windows-x64-base", "hidden": true, "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64" + "CMAKE_PREFIX_PATH": "C:/Qt/6.9.2/msvc2022_64" } }, { @@ -63,7 +63,7 @@ "hidden": true, "generator": "Ninja", "cacheVariables": { - "CMAKE_PREFIX_PATH": "C:/Qt/6.8.3/msvc2022_64" + "CMAKE_PREFIX_PATH": "C:/Qt/6.9.2/msvc2022_64" } }, { @@ -71,7 +71,7 @@ "inherits": "linux-base", "hidden": true, "cacheVariables": { - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_64" + "CMAKE_PREFIX_PATH": "/opt/Qt/6.9.2/gcc_64" }, "environment": { "CC": "gcc-11", @@ -212,7 +212,7 @@ "displayName": "CI Linux GCC ARM64", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", - "CMAKE_PREFIX_PATH": "/opt/Qt/6.8.3/gcc_arm64", + "CMAKE_PREFIX_PATH": "/opt/Qt/6.9.2/gcc_arm64", "CONAN_HOST_PROFILE": "scwx-linux_gcc-11_armv8", "CONAN_BUILD_PROFILE": "scwx-linux_gcc-11_armv8" }, @@ -226,7 +226,7 @@ "inherits": "base", "hidden": true, "cacheVariables": { - "CMAKE_PREFIX_PATH": "$env{HOME}/Qt/6.8.3/macos" + "CMAKE_PREFIX_PATH": "$env{HOME}/Qt/6.9.2/macos" }, "condition": { "type": "equals", diff --git a/tools/lib/common-paths.bat b/tools/lib/common-paths.bat index 69f6c6fc..8cd0cb60 100644 --- a/tools/lib/common-paths.bat +++ b/tools/lib/common-paths.bat @@ -1 +1 @@ -@set qt_version=6.8.3 +@set qt_version=6.9.2 diff --git a/tools/lib/common-paths.sh b/tools/lib/common-paths.sh index a1f48932..aadbe417 100755 --- a/tools/lib/common-paths.sh +++ b/tools/lib/common-paths.sh @@ -1,2 +1,2 @@ #!/bin/bash -export qt_version=6.8.3 +export qt_version=6.9.2 From dc96e3e3ad1d5f3b2d7bc616a6a0023e5edfd59e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 27 Aug 2025 22:48:49 -0500 Subject: [PATCH 752/762] Update ImGui to v1.92.2b --- external/imgui | 2 +- .../source/scwx/qt/gl/draw/placefile_text.cpp | 2 +- .../source/scwx/qt/manager/font_manager.cpp | 20 +--------- .../source/scwx/qt/manager/font_manager.hpp | 1 - scwx-qt/source/scwx/qt/map/draw_layer.cpp | 3 +- scwx-qt/source/scwx/qt/map/map_widget.cpp | 37 ++----------------- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 +- .../scwx/qt/model/imgui_context_model.cpp | 25 +++++++++---- .../scwx/qt/model/imgui_context_model.hpp | 12 ++---- .../source/scwx/qt/ui/imgui_debug_widget.cpp | 37 ++----------------- scwx-qt/source/scwx/qt/util/imgui.cpp | 2 +- 11 files changed, 35 insertions(+), 108 deletions(-) diff --git a/external/imgui b/external/imgui index 993fa347..45acd5e0 160000 --- a/external/imgui +++ b/external/imgui @@ -1 +1 @@ -Subproject commit 993fa347495860ed44b83574254ef2a317d0c14f +Subproject commit 45acd5e0e82f4c954432533ae9985ff0e1aad6d5 diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index 93aa4461..aee2d884 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -159,7 +159,7 @@ void PlacefileText::Impl::RenderTextDrawItem( std::size_t fontNumber = std::clamp(di->fontNumber_, 0, 8); // Set the font for the drop shadow and text - ImGui::PushFont(fonts_[fontNumber]->font()); + ImGui::PushFont(fonts_[fontNumber]->font(), 0.0f); if (settings::TextSettings::Instance() .placefile_text_drop_shadow_enabled() diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 28c7b515..d492a5be 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -18,11 +18,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { static const std::string logPrefix_ = "scwx::qt::manager::font_manager"; @@ -81,8 +77,6 @@ public: std::shared_mutex imguiFontAtlasMutex_ {}; - std::uint64_t imguiFontsBuildCount_ {}; - boost::unordered_flat_map, FontRecordHash> @@ -207,11 +201,6 @@ std::shared_mutex& FontManager::imgui_font_atlas_mutex() return p->imguiFontAtlasMutex_; } -std::uint64_t FontManager::imgui_fonts_build_count() const -{ - return p->imguiFontsBuildCount_; -} - int FontManager::GetFontId(types::Font font) const { auto it = p->fontIds_.find(font); @@ -327,9 +316,6 @@ FontManager::LoadImGuiFont(const std::string& family, // Store the ImGui font p->imguiFonts_.insert_or_assign(imguiFontKey, imguiFont); - // Increment ImGui font build count - ++p->imguiFontsBuildCount_; - // Return the ImGui font return imguiFont; } @@ -585,6 +571,4 @@ bool operator==(const FontRecord& lhs, const FontRecord& rhs) lhs.filename_ == rhs.filename_; } -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.hpp b/scwx-qt/source/scwx/qt/manager/font_manager.hpp index e52d0d16..26d5484b 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.hpp @@ -26,7 +26,6 @@ public: ~FontManager(); std::shared_mutex& imgui_font_atlas_mutex(); - std::uint64_t imgui_fonts_build_count() const; int GetFontId(types::Font font) const; std::shared_ptr diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index 755b9925..c59ed79f 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -98,10 +98,11 @@ void DrawLayer::ImGuiFrameStart(const std::shared_ptr& mapContext) ImGui::SetCurrentContext(p->imGuiContext_); // Start ImGui Frame + model::ImGuiContextModel::Instance().NewFrame(); ImGui_ImplQt_NewFrame(mapContext->widget()); ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); - ImGui::PushFont(defaultFont->font()); + ImGui::PushFont(defaultFont->font(), 0.0f); } void DrawLayer::ImGuiFrameEnd() diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index d619de79..5426734c 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -172,7 +172,6 @@ public: void HandleHotkeyReleased(types::Hotkey hotkey); void HandleHotkeyUpdates(); void HandlePinchGesture(QPinchGesture* gesture); - void ImGuiCheckFonts(); void InitializeCustomStyles(); void InitializeNewRadarProductView(const std::string& colorPalette); void RadarProductManagerConnect(); @@ -222,8 +221,7 @@ public: ImGuiContext* imGuiContext_; std::string imGuiContextName_; - bool imGuiRendererInitialized_; - std::uint64_t imGuiFontsBuildCount_ {}; + bool imGuiRendererInitialized_ {false}; std::shared_ptr layerModel_ { model::LayerModel::Instance()}; @@ -1575,8 +1573,6 @@ void MapWidget::initializeGL() ImGui::SetCurrentContext(p->imGuiContext_); ImGui_ImplQt_RegisterWidget(this); ImGui_ImplOpenGL3_Init(); - p->imGuiFontsBuildCount_ = - manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; p->map_.reset( @@ -1628,10 +1624,6 @@ void MapWidget::paintGL() std::shared_lock imguiFontAtlasLock { manager::FontManager::Instance().imgui_font_atlas_mutex()}; - // Check ImGui fonts - ImGui::SetCurrentContext(p->imGuiContext_); - p->ImGuiCheckFonts(); - // Update pixel ratio p->context_->set_pixel_ratio(pixelRatio()); @@ -1646,12 +1638,13 @@ void MapWidget::paintGL() ImGui::SetCurrentContext(p->imGuiContext_); // Start ImGui Frame + model::ImGuiContextModel::Instance().NewFrame(); ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); // Set default font - ImGui::PushFont(defaultFont->font()); + ImGui::PushFont(defaultFont->font(), 0.0f); // Perform mouse picking if (p->hasMouse_) @@ -1682,30 +1675,6 @@ void MapWidget::paintGL() p->isPainting_ = false; } -void MapWidgetImpl::ImGuiCheckFonts() -{ - // Update ImGui Fonts if required - std::uint64_t currentImGuiFontsBuildCount = - manager::FontManager::Instance().imgui_fonts_build_count(); - - if (imGuiFontsBuildCount_ != currentImGuiFontsBuildCount || - !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) - { - ImGui_ImplOpenGL3_DestroyFontsTexture(); - ImGui_ImplOpenGL3_CreateFontsTexture(); - } - - static bool haveLogged = false; - if (!model::ImGuiContextModel::Instance().font_atlas()->IsBuilt() && - !haveLogged) - { - logger_->error("ImGui font atlas could not be built."); - haveLogged = true; - } - - imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; -} - void MapWidgetImpl::RunMousePicking() { const QMapLibre::CustomLayerRenderParameters params = { diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 50311b83..8c60bc33 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -539,7 +539,7 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImVec2 {1.0f, 1.0f}); ImGui::SetNextWindowBgAlpha(0.5f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {3.0f, 2.0f}); - ImGui::PushFont(attributionFont->font()); + ImGui::PushFont(attributionFont->font(), 0.0f); ImGui::Begin("Attribution", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | diff --git a/scwx-qt/source/scwx/qt/model/imgui_context_model.cpp b/scwx-qt/source/scwx/qt/model/imgui_context_model.cpp index 37ca523f..33cbfe79 100644 --- a/scwx-qt/source/scwx/qt/model/imgui_context_model.cpp +++ b/scwx-qt/source/scwx/qt/model/imgui_context_model.cpp @@ -4,11 +4,12 @@ #include -namespace scwx -{ -namespace qt -{ -namespace model +// Expose required functions from internal API +void ImFontAtlasUpdateNewFrame(ImFontAtlas* atlas, + int frame_count, + bool renderer_has_textures); + +namespace scwx::qt::model { static const std::string logPrefix_ = "scwx::qt::model::imgui_context_model"; @@ -23,6 +24,8 @@ public: std::vector contexts_ {}; ImFontAtlas fontAtlas_ {}; + + int frameCount_ {0}; }; ImGuiContextModel::ImGuiContextModel() : @@ -135,6 +138,14 @@ void ImGuiContextModel::DestroyContext(const std::string& name) } } +void ImGuiContextModel::NewFrame() +{ + static constexpr bool kRendererHasTextures_ = true; + + ImFontAtlasUpdateNewFrame( + &p->fontAtlas_, ++p->frameCount_, kRendererHasTextures_); +} + std::vector ImGuiContextModel::contexts() const { return p->contexts_; @@ -153,6 +164,4 @@ ImGuiContextModel& ImGuiContextModel::Instance() bool ImGuiContextInfo::operator==(const ImGuiContextInfo& o) const = default; -} // namespace model -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::model diff --git a/scwx-qt/source/scwx/qt/model/imgui_context_model.hpp b/scwx-qt/source/scwx/qt/model/imgui_context_model.hpp index 894931c5..926501fc 100644 --- a/scwx-qt/source/scwx/qt/model/imgui_context_model.hpp +++ b/scwx-qt/source/scwx/qt/model/imgui_context_model.hpp @@ -8,11 +8,7 @@ struct ImFontAtlas; struct ImGuiContext; -namespace scwx -{ -namespace qt -{ -namespace model +namespace scwx::qt::model { class ImGuiContextModelImpl; @@ -46,6 +42,8 @@ public: ImGuiContext* CreateContext(const std::string& name); void DestroyContext(const std::string& name); + void NewFrame(); + std::vector contexts() const; ImFontAtlas* font_atlas(); @@ -59,6 +57,4 @@ private: std::unique_ptr p; }; -} // namespace model -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::model diff --git a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp index 860f5612..541c8e3e 100644 --- a/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/imgui_debug_widget.cpp @@ -9,11 +9,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace ui +namespace scwx::qt::ui { static const std::string logPrefix_ = "scwx::qt::ui::imgui_debug_widget"; @@ -51,8 +47,6 @@ public: model::ImGuiContextModel::Instance().DestroyContext(contextName_); } - void ImGuiCheckFonts(); - ImGuiDebugWidget* self_; ImGuiContext* context_; std::string contextName_; @@ -61,7 +55,6 @@ public: std::set renderedSet_ {}; bool imGuiRendererInitialized_ {false}; - std::uint64_t imGuiFontsBuildCount_ {}; }; ImGuiDebugWidget::ImGuiDebugWidget(QWidget* parent) : @@ -106,8 +99,6 @@ void ImGuiDebugWidget::initializeGL() // Initialize ImGui OpenGL3 backend ImGui::SetCurrentContext(p->context_); ImGui_ImplOpenGL3_Init(); - p->imGuiFontsBuildCount_ = - manager::FontManager::Instance().imgui_fonts_build_count(); p->imGuiRendererInitialized_ = true; } @@ -122,9 +113,9 @@ void ImGuiDebugWidget::paintGL() std::shared_lock imguiFontAtlasLock { manager::FontManager::Instance().imgui_font_atlas_mutex()}; + model::ImGuiContextModel::Instance().NewFrame(); ImGui_ImplQt_NewFrame(this); ImGui_ImplOpenGL3_NewFrame(); - p->ImGuiCheckFonts(); ImGui::NewFrame(); if (!p->renderedSet_.contains(p->currentContext_)) @@ -149,26 +140,4 @@ void ImGuiDebugWidget::paintGL() imguiFontAtlasLock.unlock(); } -void ImGuiDebugWidgetImpl::ImGuiCheckFonts() -{ - // Update ImGui Fonts if required - std::uint64_t currentImGuiFontsBuildCount = - manager::FontManager::Instance().imgui_fonts_build_count(); - - if ((context_ == currentContext_ && - imGuiFontsBuildCount_ != currentImGuiFontsBuildCount) || - !model::ImGuiContextModel::Instance().font_atlas()->IsBuilt()) - { - ImGui_ImplOpenGL3_DestroyFontsTexture(); - ImGui_ImplOpenGL3_CreateFontsTexture(); - } - - if (context_ == currentContext_) - { - imGuiFontsBuildCount_ = currentImGuiFontsBuildCount; - } -} - -} // namespace ui -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::ui diff --git a/scwx-qt/source/scwx/qt/util/imgui.cpp b/scwx-qt/source/scwx/qt/util/imgui.cpp index 64076dab..1fa857c8 100644 --- a/scwx-qt/source/scwx/qt/util/imgui.cpp +++ b/scwx-qt/source/scwx/qt/util/imgui.cpp @@ -34,7 +34,7 @@ void ImGui::DrawTooltip(const std::string& hoverText) if (::ImGui::BeginTooltip()) { - ::ImGui::PushFont(tooltipFont->font()); + ::ImGui::PushFont(tooltipFont->font(), 0.0f); ::ImGui::TextUnformatted(hoverText.c_str()); ::ImGui::PopFont(); ::ImGui::EndTooltip(); From c82b9753c672dbf55d1288701ef90ecfbaf48522 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 27 Aug 2025 23:43:11 -0500 Subject: [PATCH 753/762] Use dynamically sized ImGui fonts --- .../source/scwx/qt/gl/draw/placefile_text.cpp | 16 +++-- .../source/scwx/qt/gl/draw/placefile_text.hpp | 6 +- .../source/scwx/qt/manager/font_manager.cpp | 71 ++++++++++--------- .../source/scwx/qt/manager/font_manager.hpp | 12 ++-- .../scwx/qt/manager/placefile_manager.cpp | 24 +++---- .../scwx/qt/manager/placefile_manager.hpp | 9 ++- scwx-qt/source/scwx/qt/map/draw_layer.cpp | 2 +- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 +- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 3 +- scwx-qt/source/scwx/qt/types/imgui_font.cpp | 23 +++--- scwx-qt/source/scwx/qt/types/imgui_font.hpp | 7 +- scwx-qt/source/scwx/qt/util/imgui.cpp | 2 +- 12 files changed, 89 insertions(+), 88 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index aee2d884..cd1ca34e 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -62,8 +61,12 @@ public: std::vector> textList_ {}; std::vector> newList_ {}; - std::vector> fonts_ {}; - std::vector> newFonts_ {}; + std::vector, + units::font_size::pixels>> + fonts_ {}; + std::vector, + units::font_size::pixels>> + newFonts_ {}; }; PlacefileText::PlacefileText(const std::string& placefileName) : @@ -159,7 +162,8 @@ void PlacefileText::Impl::RenderTextDrawItem( std::size_t fontNumber = std::clamp(di->fontNumber_, 0, 8); // Set the font for the drop shadow and text - ImGui::PushFont(fonts_[fontNumber]->font(), 0.0f); + ImGui::PushFont(fonts_[fontNumber].first->font(), + fonts_[fontNumber].second.value()); if (settings::TextSettings::Instance() .placefile_text_drop_shadow_enabled() @@ -261,9 +265,7 @@ void PlacefileText::StartText() p->newList_.clear(); } -void PlacefileText::SetFonts( - const boost::unordered_flat_map>& fonts) +void PlacefileText::SetFonts(const manager::PlacefileManager::FontMap& fonts) { auto defaultFont = manager::FontManager::Instance().GetImGuiFont( types::FontCategory::Default); diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp index e36be5a6..e4e46755 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -54,10 +55,7 @@ public: * * @param [in] fonts A map of ImGui fonts */ - void - SetFonts(const boost::unordered_flat_map>& - fonts); + void SetFonts(const manager::PlacefileManager::FontMap& fonts); /** * Adds placefile text to the internal draw list. diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index d492a5be..587c4e60 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -34,15 +34,13 @@ struct FontRecord std::string filename_ {}; }; -typedef std::pair> FontRecordPair; - template struct FontRecordHash; template<> -struct FontRecordHash +struct FontRecordHash { - size_t operator()(const FontRecordPair& x) const; + size_t operator()(const FontRecord& x) const; }; class FontManager::Impl @@ -77,18 +75,20 @@ public: std::shared_mutex imguiFontAtlasMutex_ {}; - boost::unordered_flat_map, - FontRecordHash> + FontRecordHash> imguiFonts_ {}; std::shared_mutex imguiFontsMutex_ {}; boost::unordered_flat_map> rawFontData_ {}; std::mutex rawFontDataMutex_ {}; - std::shared_ptr defaultFont_ {}; + std::pair, units::font_size::pixels> + defaultFont_ {}; boost::unordered_flat_map> + std::pair, + units::font_size::pixels>> fontCategoryImguiFontMap_ {}; boost::unordered_flat_map fontCategoryQFontMap_ {}; @@ -160,6 +160,17 @@ void FontManager::InitializeFonts() } } +units::font_size::pixels +FontManager::ImFontSize(units::font_size::pixels size) +{ + // Only allow whole pixels, and clamp to 6-72 pt + units::font_size::pixels pixels {size}; + units::font_size::pixels imFontSize { + std::clamp(static_cast(pixels.value()), 8, 96)}; + + return imFontSize; +} + void FontManager::Impl::UpdateImGuiFont(types::FontCategory fontCategory) { auto& textSettings = settings::TextSettings::Instance(); @@ -170,7 +181,8 @@ void FontManager::Impl::UpdateImGuiFont(types::FontCategory fontCategory) textSettings.font_point_size(fontCategory).GetValue()}; fontCategoryImguiFontMap_.insert_or_assign( - fontCategory, self_->LoadImGuiFont(family, {styles}, size)); + fontCategory, + std::make_pair(self_->LoadImGuiFont(family, {styles}), ImFontSize(size))); } void FontManager::Impl::UpdateQFont(types::FontCategory fontCategory) @@ -211,7 +223,7 @@ int FontManager::GetFontId(types::Font font) const return -1; } -std::shared_ptr +std::pair, units::font_size::pixels> FontManager::GetImGuiFont(types::FontCategory fontCategory) { std::unique_lock lock {p->fontCategoryMutex_}; @@ -239,31 +251,23 @@ QFont FontManager::GetQFont(types::FontCategory fontCategory) } std::shared_ptr -FontManager::LoadImGuiFont(const std::string& family, - const std::vector& styles, - units::font_size::points size, - bool loadIfNotFound) +FontManager::LoadImGuiFont(const std::string& family, + const std::vector& styles, + bool loadIfNotFound) { const std::string styleString = fmt::format("{}", fmt::join(styles, " ")); - const std::string fontString = - fmt::format("{}-{}:{}", family, size.value(), styleString); + const std::string fontString = fmt::format("{}:{}", family, styleString); logger_->debug("LoadFontResource: {}", fontString); FontRecord fontRecord = Impl::MatchFontFile(family, styles); - // Only allow whole pixels, and clamp to 6-72 pt - units::font_size::pixels pixels {size}; - units::font_size::pixels imFontSize { - std::clamp(static_cast(pixels.value()), 8, 96)}; - auto imguiFontKey = std::make_pair(fontRecord, imFontSize); - // Search for a loaded ImGui font { std::shared_lock imguiFontLock {p->imguiFontsMutex_}; // Search for the associated ImGui font - auto it = p->imguiFonts_.find(imguiFontKey); + auto it = p->imguiFonts_.find(fontRecord); if (it != p->imguiFonts_.end()) { return it->second; @@ -288,7 +292,7 @@ FontManager::LoadImGuiFont(const std::string& family, // Search for the associated ImGui font again, to prevent loading the same // font twice - auto it = p->imguiFonts_.find(imguiFontKey); + auto it = p->imguiFonts_.find(fontRecord); if (it != p->imguiFonts_.end()) { return it->second; @@ -299,22 +303,20 @@ FontManager::LoadImGuiFont(const std::string& family, try { fontName = fmt::format( - "{}:{}", - std::filesystem::path(fontRecord.filename_).filename().string(), - imFontSize.value()); + "{}", std::filesystem::path(fontRecord.filename_).filename().string()); } catch (const std::exception& ex) { logger_->warn(ex.what()); - fontName = fmt::format("{}:{}", fontRecord.filename_, imFontSize.value()); + fontName = fmt::format("{}", fontRecord.filename_); } // Create an ImGui font std::shared_ptr imguiFont = - std::make_shared(fontName, rawFontData, imFontSize); + std::make_shared(fontName, rawFontData); // Store the ImGui font - p->imguiFonts_.insert_or_assign(imguiFontKey, imguiFont); + p->imguiFonts_.insert_or_assign(fontRecord, imguiFont); // Return the ImGui font return imguiFont; @@ -554,13 +556,12 @@ FontManager& FontManager::Instance() return instance_; } -size_t FontRecordHash::operator()(const FontRecordPair& x) const +size_t FontRecordHash::operator()(const FontRecord& x) const { size_t seed = 0; - boost::hash_combine(seed, x.first.family_); - boost::hash_combine(seed, x.first.style_); - boost::hash_combine(seed, x.first.filename_); - boost::hash_combine(seed, x.second.value()); + boost::hash_combine(seed, x.family_); + boost::hash_combine(seed, x.style_); + boost::hash_combine(seed, x.filename_); return seed; } diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.hpp b/scwx-qt/source/scwx/qt/manager/font_manager.hpp index 26d5484b..04c254ea 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.hpp @@ -28,18 +28,20 @@ public: std::shared_mutex& imgui_font_atlas_mutex(); int GetFontId(types::Font font) const; - std::shared_ptr + std::pair, units::font_size::pixels> GetImGuiFont(types::FontCategory fontCategory); QFont GetQFont(types::FontCategory fontCategory); std::shared_ptr - LoadImGuiFont(const std::string& family, - const std::vector& styles, - units::font_size::points size, - bool loadIfNotFound = true); + LoadImGuiFont(const std::string& family, + const std::vector& styles, + bool loadIfNotFound = true); void LoadApplicationFont(types::Font font, const std::string& filename); void InitializeFonts(); + static units::font_size::pixels + ImFontSize(units::font_size::pixels size); + static FontManager& Instance(); private: diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp index d85f9e40..9685f33f 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.cpp @@ -52,8 +52,7 @@ public: void ReadPlacefileSettings(); void WritePlacefileSettings(); - static boost::unordered_flat_map> + static FontMap LoadFontResources(const std::shared_ptr& placefile); static std::vector> LoadImageResources(const std::shared_ptr& placefile); @@ -147,8 +146,7 @@ public: std::mutex refreshMutex_ {}; std::mutex timerMutex_ {}; - boost::unordered_flat_map> - fonts_ {}; + FontMap fonts_ {}; std::mutex fontsMutex_ {}; std::vector> images_ {}; @@ -235,7 +233,7 @@ PlacefileManager::placefile(const std::string& name) return nullptr; } -boost::unordered_flat_map> +PlacefileManager::FontMap PlacefileManager::placefile_fonts(const std::string& name) { std::shared_lock lock(p->placefileRecordLock_); @@ -775,13 +773,11 @@ std::shared_ptr PlacefileManager::Instance() return placefileManager; } -boost::unordered_flat_map> -PlacefileManager::Impl::LoadFontResources( +PlacefileManager::FontMap PlacefileManager::Impl::LoadFontResources( const std::shared_ptr& placefile) { - boost::unordered_flat_map> - imGuiFonts {}; - auto fonts = placefile->fonts(); + FontMap imGuiFonts {}; + auto fonts = placefile->fonts(); for (auto& font : fonts) { @@ -797,9 +793,11 @@ PlacefileManager::Impl::LoadFontResources( styles.push_back("italic"); } - auto imGuiFont = FontManager::Instance().LoadImGuiFont( - font.second->face_, styles, size); - imGuiFonts.emplace(font.first, std::move(imGuiFont)); + auto imGuiFont = + FontManager::Instance().LoadImGuiFont(font.second->face_, styles); + imGuiFonts.emplace( + font.first, + std::make_pair(std::move(imGuiFont), FontManager::ImFontSize(size))); } return imGuiFonts; diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp index 17b39b2f..0a667c16 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -22,12 +23,16 @@ public: explicit PlacefileManager(); ~PlacefileManager(); + using FontMap = + boost::unordered_flat_map, + units::font_size::pixels>>; + bool placefile_enabled(const std::string& name); bool placefile_thresholded(const std::string& name); std::string placefile_title(const std::string& name); std::shared_ptr placefile(const std::string& name); - boost::unordered_flat_map> - placefile_fonts(const std::string& name); + FontMap placefile_fonts(const std::string& name); void set_placefile_enabled(const std::string& name, bool enabled); void set_placefile_thresholded(const std::string& name, bool thresholded); diff --git a/scwx-qt/source/scwx/qt/map/draw_layer.cpp b/scwx-qt/source/scwx/qt/map/draw_layer.cpp index c59ed79f..f07cb2ac 100644 --- a/scwx-qt/source/scwx/qt/map/draw_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/draw_layer.cpp @@ -102,7 +102,7 @@ void DrawLayer::ImGuiFrameStart(const std::shared_ptr& mapContext) ImGui_ImplQt_NewFrame(mapContext->widget()); ImGui_ImplOpenGL3_NewFrame(); ImGui::NewFrame(); - ImGui::PushFont(defaultFont->font(), 0.0f); + ImGui::PushFont(defaultFont.first->font(), defaultFont.second.value()); } void DrawLayer::ImGuiFrameEnd() diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 5426734c..537318f0 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -1644,7 +1644,7 @@ void MapWidget::paintGL() ImGui::NewFrame(); // Set default font - ImGui::PushFont(defaultFont->font(), 0.0f); + ImGui::PushFont(defaultFont.first->font(), defaultFont.second.value()); // Perform mouse picking if (p->hasMouse_) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 8c60bc33..04b0890e 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -539,7 +539,8 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImVec2 {1.0f, 1.0f}); ImGui::SetNextWindowBgAlpha(0.5f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {3.0f, 2.0f}); - ImGui::PushFont(attributionFont->font(), 0.0f); + ImGui::PushFont(attributionFont.first->font(), + attributionFont.second.value()); ImGui::Begin("Attribution", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.cpp b/scwx-qt/source/scwx/qt/types/imgui_font.cpp index e6f22ad1..647c584b 100644 --- a/scwx-qt/source/scwx/qt/types/imgui_font.cpp +++ b/scwx-qt/source/scwx/qt/types/imgui_font.cpp @@ -23,10 +23,9 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class ImGuiFont::Impl { public: - explicit Impl(const std::string& fontName, - const std::vector& fontData, - units::font_size::pixels size) : - fontName_ {fontName}, size_ {size} + explicit Impl(const std::string& fontName, + const std::vector& fontData) : + fontName_ {fontName} { CreateImGuiFont(fontData); } @@ -35,16 +34,14 @@ public: void CreateImGuiFont(const std::vector& fontData); - const std::string fontName_; - const units::font_size::pixels size_; + const std::string fontName_; ImFont* imFont_ {nullptr}; }; -ImGuiFont::ImGuiFont(const std::string& fontName, - const std::vector& fontData, - units::font_size::pixels size) : - p(std::make_unique(fontName, fontData, size)) +ImGuiFont::ImGuiFont(const std::string& fontName, + const std::vector& fontData) : + p(std::make_unique(fontName, fontData)) { } ImGuiFont::~ImGuiFont() = default; @@ -53,11 +50,11 @@ void ImGuiFont::Impl::CreateImGuiFont(const std::vector& fontData) { logger_->debug("Creating Font: {}", fontName_); + static constexpr float kSizePixels_ = 0.0f; + ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); ImFontConfig fontConfig {}; - const float sizePixels = static_cast(size_.value()); - // Do not transfer ownership of font data to ImGui, makes const_cast safe fontConfig.FontDataOwnedByAtlas = false; @@ -69,7 +66,7 @@ void ImGuiFont::Impl::CreateImGuiFont(const std::vector& fontData) const_cast(static_cast(fontData.data())), static_cast(std::clamp( fontData.size(), 0, std::numeric_limits::max())), - sizePixels, + kSizePixels_, &fontConfig); } diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.hpp b/scwx-qt/source/scwx/qt/types/imgui_font.hpp index ace8ba09..d1c28a0c 100644 --- a/scwx-qt/source/scwx/qt/types/imgui_font.hpp +++ b/scwx-qt/source/scwx/qt/types/imgui_font.hpp @@ -4,8 +4,6 @@ #include #include -#include - struct ImFont; namespace scwx @@ -18,9 +16,8 @@ namespace types class ImGuiFont { public: - explicit ImGuiFont(const std::string& fontName, - const std::vector& fontData, - units::font_size::pixels size); + explicit ImGuiFont(const std::string& fontName, + const std::vector& fontData); ~ImGuiFont(); ImGuiFont(const ImGuiFont&) = delete; diff --git a/scwx-qt/source/scwx/qt/util/imgui.cpp b/scwx-qt/source/scwx/qt/util/imgui.cpp index 1fa857c8..1bb8af3c 100644 --- a/scwx-qt/source/scwx/qt/util/imgui.cpp +++ b/scwx-qt/source/scwx/qt/util/imgui.cpp @@ -34,7 +34,7 @@ void ImGui::DrawTooltip(const std::string& hoverText) if (::ImGui::BeginTooltip()) { - ::ImGui::PushFont(tooltipFont->font(), 0.0f); + ::ImGui::PushFont(tooltipFont.first->font(), tooltipFont.second.value()); ::ImGui::TextUnformatted(hoverText.c_str()); ::ImGui::PopFont(); ::ImGui::EndTooltip(); From ffe1e7efafc5c38d79bd5db7328d1a37551703de Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:16:23 -0500 Subject: [PATCH 754/762] Address ImGui clang-tidy comments, store font size as float --- .../source/scwx/qt/gl/draw/placefile_text.cpp | 4 ++-- .../source/scwx/qt/manager/font_manager.cpp | 19 ++++++++++++------- .../source/scwx/qt/manager/font_manager.hpp | 4 ++-- .../scwx/qt/manager/placefile_manager.hpp | 2 +- scwx-qt/source/scwx/qt/types/imgui_font.cpp | 7 +++---- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp index cd1ca34e..0ce6ab47 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/placefile_text.cpp @@ -62,10 +62,10 @@ public: std::vector> newList_ {}; std::vector, - units::font_size::pixels>> + units::font_size::pixels>> fonts_ {}; std::vector, - units::font_size::pixels>> + units::font_size::pixels>> newFonts_ {}; }; diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.cpp b/scwx-qt/source/scwx/qt/manager/font_manager.cpp index 587c4e60..fc645388 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.cpp @@ -84,11 +84,11 @@ public: boost::unordered_flat_map> rawFontData_ {}; std::mutex rawFontDataMutex_ {}; - std::pair, units::font_size::pixels> + std::pair, units::font_size::pixels> defaultFont_ {}; boost::unordered_flat_map, - units::font_size::pixels>> + units::font_size::pixels>> fontCategoryImguiFontMap_ {}; boost::unordered_flat_map fontCategoryQFontMap_ {}; @@ -160,13 +160,18 @@ void FontManager::InitializeFonts() } } -units::font_size::pixels +units::font_size::pixels FontManager::ImFontSize(units::font_size::pixels size) { + static constexpr units::font_size::pixels kMinFontSize_ {8}; + static constexpr units::font_size::pixels kMaxFontSize_ {96}; + // Only allow whole pixels, and clamp to 6-72 pt - units::font_size::pixels pixels {size}; - units::font_size::pixels imFontSize { - std::clamp(static_cast(pixels.value()), 8, 96)}; + const units::font_size::pixels pixels {size}; + const units::font_size::pixels imFontSize { + std::clamp(static_cast(pixels.value()), + kMinFontSize_.value(), + kMaxFontSize_.value())}; return imFontSize; } @@ -223,7 +228,7 @@ int FontManager::GetFontId(types::Font font) const return -1; } -std::pair, units::font_size::pixels> +std::pair, units::font_size::pixels> FontManager::GetImGuiFont(types::FontCategory fontCategory) { std::unique_lock lock {p->fontCategoryMutex_}; diff --git a/scwx-qt/source/scwx/qt/manager/font_manager.hpp b/scwx-qt/source/scwx/qt/manager/font_manager.hpp index 04c254ea..50e05d9d 100644 --- a/scwx-qt/source/scwx/qt/manager/font_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/font_manager.hpp @@ -28,7 +28,7 @@ public: std::shared_mutex& imgui_font_atlas_mutex(); int GetFontId(types::Font font) const; - std::pair, units::font_size::pixels> + std::pair, units::font_size::pixels> GetImGuiFont(types::FontCategory fontCategory); QFont GetQFont(types::FontCategory fontCategory); std::shared_ptr @@ -39,7 +39,7 @@ public: void LoadApplicationFont(types::Font font, const std::string& filename); void InitializeFonts(); - static units::font_size::pixels + static units::font_size::pixels ImFontSize(units::font_size::pixels size); static FontManager& Instance(); diff --git a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp index 0a667c16..4408fb2f 100644 --- a/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/placefile_manager.hpp @@ -26,7 +26,7 @@ public: using FontMap = boost::unordered_flat_map, - units::font_size::pixels>>; + units::font_size::pixels>>; bool placefile_enabled(const std::string& name); bool placefile_thresholded(const std::string& name); diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.cpp b/scwx-qt/source/scwx/qt/types/imgui_font.cpp index 647c584b..82dab97e 100644 --- a/scwx-qt/source/scwx/qt/types/imgui_font.cpp +++ b/scwx-qt/source/scwx/qt/types/imgui_font.cpp @@ -23,9 +23,8 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class ImGuiFont::Impl { public: - explicit Impl(const std::string& fontName, - const std::vector& fontData) : - fontName_ {fontName} + explicit Impl(std::string fontName, const std::vector& fontData) : + fontName_ {std::move(fontName)} { CreateImGuiFont(fontData); } @@ -34,7 +33,7 @@ public: void CreateImGuiFont(const std::vector& fontData); - const std::string fontName_; + std::string fontName_; ImFont* imFont_ {nullptr}; }; From 69b39bd884e6fdb92bb1c241d7233505dc30b6d2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:29:39 -0500 Subject: [PATCH 755/762] Change default ImGui font size to 16px --- scwx-qt/source/scwx/qt/types/imgui_font.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/types/imgui_font.cpp b/scwx-qt/source/scwx/qt/types/imgui_font.cpp index 82dab97e..f2cab082 100644 --- a/scwx-qt/source/scwx/qt/types/imgui_font.cpp +++ b/scwx-qt/source/scwx/qt/types/imgui_font.cpp @@ -49,7 +49,8 @@ void ImGuiFont::Impl::CreateImGuiFont(const std::vector& fontData) { logger_->debug("Creating Font: {}", fontName_); - static constexpr float kSizePixels_ = 0.0f; + // Default render size, used in debug widget + static constexpr float kSizePixels_ = 16.0f; ImFontAtlas* fontAtlas = model::ImGuiContextModel::Instance().font_atlas(); ImFontConfig fontConfig {}; From fa7904a46908407223ac65f1a233b4af57171503 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:37:06 -0500 Subject: [PATCH 756/762] Update aws-sdk-cpp to 1.11.636 --- external/aws-sdk-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/aws-sdk-cpp b/external/aws-sdk-cpp index 9c95a05a..8d31e042 160000 --- a/external/aws-sdk-cpp +++ b/external/aws-sdk-cpp @@ -1 +1 @@ -Subproject commit 9c95a05a5a99718965c033c6e1fc3f8bb34a1b83 +Subproject commit 8d31e042f950fe70924391a205cceaf342ecec00 From 335cb5d396751b1b5797b96741dee6c0ab26b273 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:40:27 -0500 Subject: [PATCH 757/762] Update cmake-conan to latest develop2 (2025-06-03) --- external/cmake-conan | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/cmake-conan b/external/cmake-conan index c22bbf0a..b0e4d1ec 160000 --- a/external/cmake-conan +++ b/external/cmake-conan @@ -1 +1 @@ -Subproject commit c22bbf0af0b73d5f0def24a9cdf4ce503ae79e5d +Subproject commit b0e4d1ec08edb35ef31033938567d621f6643c17 From 72ddf606f3a7a430d6abe8f9e7ab2f7ee7f0cb82 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:41:08 -0500 Subject: [PATCH 758/762] Update date to latest (2025-08-13) --- external/date | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/date b/external/date index ca572785..a5db3aec 160000 --- a/external/date +++ b/external/date @@ -1 +1 @@ -Subproject commit ca5727855bd1bae12b2c6ca36cd88649d43ec862 +Subproject commit a5db3aecec580bc78b6c01c118f2628676769b69 From 33419cfee34014af3559e774a155f46cfeb6f0db Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 11:44:46 -0500 Subject: [PATCH 759/762] Update stb to latest (2025-05-26) --- external/stb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/stb b/external/stb index 5c205738..f58f558c 160000 --- a/external/stb +++ b/external/stb @@ -1 +1 @@ -Subproject commit 5c205738c191bcb0abc65c4febfa9bd25ff35234 +Subproject commit f58f558c120e9b32c217290b80bad1a0729fbb2c From f59bb2be3e1112552cec13aa51b7af9de74aa483 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 12:04:49 -0500 Subject: [PATCH 760/762] Update maplibre-native-qt to latest (2025-08-18) --- external/maplibre-native-qt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/maplibre-native-qt b/external/maplibre-native-qt index 527734fd..8b406978 160000 --- a/external/maplibre-native-qt +++ b/external/maplibre-native-qt @@ -1 +1 @@ -Subproject commit 527734fdcacf8d85185776f4b020b94a8c937cdd +Subproject commit 8b40697895c19da4479cd037a76608f4c36935e8 From e94e6fabd2130e9b7235de58ddf0a7a4c2029aeb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 12:05:36 -0500 Subject: [PATCH 761/762] Sorting .gitmodules --- .gitmodules | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.gitmodules b/.gitmodules index a6f47235..5bf3b307 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,45 +1,45 @@ -[submodule "external/cmake-conan"] - path = external/cmake-conan - url = https://github.com/conan-io/cmake-conan.git -[submodule "test/data"] - path = test/data - url = https://github.com/dpaulat/supercell-wx-test-data -[submodule "external/hsluv-c"] - path = external/hsluv-c - url = https://github.com/hsluv/hsluv-c.git -[submodule "external/stb"] - path = external/stb - url = https://github.com/nothings/stb.git [submodule "data"] path = data url = https://github.com/dpaulat/supercell-wx-data +[submodule "external/aws-sdk-cpp"] + path = external/aws-sdk-cpp + url = https://github.com/aws/aws-sdk-cpp.git +[submodule "external/cmake-conan"] + path = external/cmake-conan + url = https://github.com/conan-io/cmake-conan.git +[submodule "external/date"] + path = external/date + url = https://github.com/HowardHinnant/date.git +[submodule "external/glad"] + path = external/glad + url = https://github.com/Dav1dde/glad.git +[submodule "external/hsluv-c"] + path = external/hsluv-c + url = https://github.com/hsluv/hsluv-c.git [submodule "external/imgui"] path = external/imgui url = https://github.com/ocornut/imgui.git [submodule "external/imgui-backend-qt"] path = external/imgui-backend-qt url = https://github.com/dpaulat/imgui-backend-qt -[submodule "external/aws-sdk-cpp"] - path = external/aws-sdk-cpp - url = https://github.com/aws/aws-sdk-cpp.git -[submodule "external/date"] - path = external/date - url = https://github.com/HowardHinnant/date.git -[submodule "external/units"] - path = external/units - url = https://github.com/nholthaus/units.git -[submodule "external/textflowcpp"] - path = external/textflowcpp - url = https://github.com/catchorg/textflowcpp.git -[submodule "external/maplibre-native-qt"] - path = external/maplibre-native-qt - url = https://github.com/dpaulat/maplibre-native-qt.git [submodule "external/maplibre-native"] path = external/maplibre-native url = https://github.com/dpaulat/maplibre-gl-native.git +[submodule "external/maplibre-native-qt"] + path = external/maplibre-native-qt + url = https://github.com/dpaulat/maplibre-native-qt.git [submodule "external/qt6ct"] path = external/qt6ct url = https://github.com/AdenKoperczak/qt6ct.git -[submodule "external/glad"] - path = external/glad - url = https://github.com/Dav1dde/glad.git +[submodule "external/stb"] + path = external/stb + url = https://github.com/nothings/stb.git +[submodule "external/textflowcpp"] + path = external/textflowcpp + url = https://github.com/catchorg/textflowcpp.git +[submodule "external/units"] + path = external/units + url = https://github.com/nholthaus/units.git +[submodule "test/data"] + path = test/data + url = https://github.com/dpaulat/supercell-wx-test-data From d0f4815ef9bd46f939271bdba944179bca65c899 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 12:09:32 -0500 Subject: [PATCH 762/762] Synchronizing imconfig.h to v1.92.2b --- external/include/scwx/external/imgui/imconfig.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/external/include/scwx/external/imgui/imconfig.h b/external/include/scwx/external/imgui/imconfig.h index 6064c40d..ac954a77 100644 --- a/external/include/scwx/external/imgui/imconfig.h +++ b/external/include/scwx/external/imgui/imconfig.h @@ -89,8 +89,7 @@ //---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT) // Only works in combination with IMGUI_ENABLE_FREETYPE. -// - lunasvg is currently easier to acquire/install, as e.g. it is part of vcpkg. -// - plutosvg will support more fonts and may load them faster. It currently requires to be built manually but it is fairly easy. See misc/freetype/README for instructions. +// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions. // - Both require headers to be available in the include path + program to be linked with the library code (not provided). // - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) //#define IMGUI_ENABLE_FREETYPE_PLUTOSVG @@ -131,6 +130,10 @@ //#define IM_DEBUG_BREAK IM_ASSERT(0) //#define IM_DEBUG_BREAK __debugbreak() +//---- Debug Tools: Enable highlight ID conflicts _before_ hovering items. When io.ConfigDebugHighlightIdConflicts is set. +// (THIS WILL SLOW DOWN DEAR IMGUI. Only use occasionally and disable after use) +//#define IMGUI_DEBUG_HIGHLIGHT_ALL_ID_CONFLICTS + //---- Debug Tools: Enable slower asserts //#define IMGUI_DEBUG_PARANOID