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..8ebb064f --- /dev/null +++ b/scwx-qt/res/textures/images/location-marker.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 06646bbe..71700cec 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/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 @@ -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/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 @@ -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/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) @@ -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/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) @@ -154,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) @@ -162,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) @@ -215,6 +221,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/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 @@ -260,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 @@ -288,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 @@ -307,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/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/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 861bc69a..2c744ff7 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 @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -86,11 +88,13 @@ public: imGuiDebugDialog_ {nullptr}, layerDialog_ {nullptr}, placefileDialog_ {nullptr}, + markerDialog_ {nullptr}, radarSiteDialog_ {nullptr}, settingsDialog_ {nullptr}, updateDialog_ {nullptr}, alertManager_ {manager::AlertManager::Instance()}, placefileManager_ {manager::PlacefileManager::Instance()}, + markerManager_ {manager::MarkerManager::Instance()}, positionManager_ {manager::PositionManager::Instance()}, textEventManager_ {manager::TextEventManager::Instance()}, timelineManager_ {manager::TimelineManager::Instance()}, @@ -203,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_; @@ -217,6 +222,7 @@ public: std::shared_ptr hotkeyManager_ { manager::HotkeyManager::Instance()}; std::shared_ptr placefileManager_; + std::shared_ptr markerManager_; std::shared_ptr positionManager_; std::shared_ptr textEventManager_; std::shared_ptr timelineManager_; @@ -303,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); @@ -610,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 new file mode 100644 index 00000000..382aafd7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.cpp @@ -0,0 +1,317 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::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 MarkerManager::Impl +{ +public: + class MarkerRecord; + + explicit Impl(MarkerManager* self) : self_ {self} {} + ~Impl() { threadPool_.join(); } + + std::string markerSettingsPath_ {}; + std::vector> markerRecords_ {}; + + MarkerManager* self_; + + boost::asio::thread_pool threadPool_ {1u}; + std::shared_mutex markerRecordLock_ {}; + + void InitializeMarkerSettings(); + void ReadMarkerSettings(); + void WriteMarkerSettings(); + std::shared_ptr GetMarkerByName(const std::string& name); +}; + +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} + { + } + + const 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->markerInfo_.name}, + {kLatitudeName_, record->markerInfo_.latitude}, + {kLongitudeName_, record->markerInfo_.longitude}}; + } + + friend MarkerRecord tag_invoke(boost::json::value_to_tag, + const boost::json::value& jv) + { + return MarkerRecord( + boost::json::value_to(jv.at(kNameName_)), + boost::json::value_to(jv.at(kLatitudeName_)), + boost::json::value_to(jv.at(kLongitudeName_))); + } +}; + +void MarkerManager::Impl::InitializeMarkerSettings() +{ + 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); + } + } + + markerSettingsPath_ = appDataPath + "/location-markers.json"; +} + +void MarkerManager::Impl::ReadMarkerSettings() +{ + logger_->info("Reading location marker settings"); + + boost::json::value markerJson = nullptr; + { + std::unique_lock lock(markerRecordLock_); + + // Determine if marker settings exists + if (std::filesystem::exists(markerSettingsPath_)) + { + markerJson = util::json::ReadJsonFile(markerSettingsPath_); + } + + 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(); +} + +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); +} + +std::shared_ptr +MarkerManager::Impl::GetMarkerByName(const std::string& name) +{ + for (auto& markerRecord : markerRecords_) + { + if (markerRecord->markerInfo_.name == name) + { + return markerRecord; + } + } + + return nullptr; +} + +MarkerManager::MarkerManager() : p(std::make_unique(this)) +{ + + boost::asio::post(p->threadPool_, + [this]() + { + try + { + p->InitializeMarkerSettings(); + + // Read Marker settings on startup + main::Application::WaitForInitialization(); + p->ReadMarkerSettings(); + + Q_EMIT MarkersInitialized(p->markerRecords_.size()); + } + catch (const std::exception& ex) + { + logger_->error(ex.what()); + } + }); +} + +MarkerManager::~MarkerManager() +{ + p->WriteMarkerSettings(); +} + +size_t MarkerManager::marker_count() +{ + return p->markerRecords_.size(); +} + +std::optional MarkerManager::get_marker(size_t index) +{ + std::shared_lock lock(p->markerRecordLock_); + 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::unique_lock lock(p->markerRecordLock_); + if (index >= p->markerRecords_.size()) + { + return; + } + 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) +{ + { + 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) +{ + { + std::unique_lock lock(p->markerRecordLock_); + if (index >= p->markerRecords_.size()) + { + return; + } + + p->markerRecords_.erase(std::next(p->markerRecords_.begin(), index)); + } + + Q_EMIT MarkerRemoved(index); + Q_EMIT MarkersUpdated(); +} + +void MarkerManager::move_marker(size_t from, size_t to) +{ + { + 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++) + { + p->markerRecords_[i] = p->markerRecords_[i + 1]; + } + p->markerRecords_[to] = markerRecord; + } + else + { + for (size_t i = from; i > to; i--) + { + p->markerRecords_[i] = p->markerRecords_[i - 1]; + } + p->markerRecords_[to] = markerRecord; + } + } + Q_EMIT MarkersUpdated(); +} + +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(); + + if (markerManager == nullptr) + { + markerManager = std::make_shared(); + markerManagerReference_ = markerManager; + } + + return markerManager; +} + +} // 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 new file mode 100644 index 00000000..2f073ab7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/marker_manager.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class MarkerManager : public QObject +{ + Q_OBJECT + +public: + explicit MarkerManager(); + ~MarkerManager(); + + 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); + + 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); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index e718bc0e..da5bef38 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}, + markerLayer_ {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 markerLayer_; 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 location marker layer + case types::InformationLayer::Markers: + markerLayer_ = std::make_shared(context_); + AddLayer(layerName, markerLayer_, before); + break; + default: break; } diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.cpp b/scwx-qt/source/scwx/qt/map/marker_layer.cpp new file mode 100644 index 00000000..ab97322f --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/marker_layer.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +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: + 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::LocationMarker)}; + + 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++) + { + 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_->FinishIcons(); + Q_EMIT self_->NeedsRendering(); +} + +MarkerLayer::MarkerLayer(const std::shared_ptr& context) : + DrawLayer(context), p(std::make_unique(this, context)) +{ + AddDrawItem(p->geoIcons_); +} + +MarkerLayer::~MarkerLayer() = default; + +void MarkerLayer::Initialize() +{ + logger_->debug("Initialize()"); + DrawLayer::Initialize(); + + p->geoIcons_->StartIconSheets(); + p->geoIcons_->AddIconSheet(p->markerIconName_); + p->geoIcons_->FinishIconSheets(); + + p->ReloadMarkers(); +} + +void MarkerLayer::Render(const QMapLibre::CustomLayerRenderParameters& params) +{ + // auto markerManager = manager::MarkerManager::Instance(); + gl::OpenGLFunctions& gl = context()->gl(); + + DrawLayer::Render(params); + + SCWX_GL_CHECK_ERROR(); +} + +void MarkerLayer::Deinitialize() +{ + logger_->debug("Deinitialize()"); + + DrawLayer::Deinitialize(); +} + +} // namespace map +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/marker_layer.hpp b/scwx-qt/source/scwx/qt/map/marker_layer.hpp new file mode 100644 index 00000000..9cd0674c --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/marker_layer.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +class MarkerLayer : public DrawLayer +{ + Q_OBJECT + +public: + explicit MarkerLayer(const std::shared_ptr& context); + ~MarkerLayer(); + + 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/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 999a2de9..23d05cd6 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::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/model/marker_model.cpp b/scwx-qt/source/scwx/qt/model/marker_model.cpp new file mode 100644 index 00000000..eb3e8bee --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/marker_model.cpp @@ -0,0 +1,290 @@ +#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::Latitude); +static constexpr int kLastColumn = + static_cast(MarkerModel::Column::Name); +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::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, + &MarkerModel::HandleMarkerRemoved); +} + +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 = 10; + + if (!index.isValid() || index.row() < 0) + { + return QVariant(); + } + + std::optional markerInfo = + p->markerManager_->get_marker(index.row()); + if (!markerInfo) + { + return QVariant(); + } + + 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) + { + 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): + if (role == Qt::ItemDataRole::DisplayRole || + 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; + } + + 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) + { + return false; + } + std::optional markerInfo = + p->markerManager_->get_marker(index.row()); + 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(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() && ok && -90 <= latitude && latitude <= 90) + { + 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() && ok && -180 <= longitude && longitude <= 180) + { + 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::HandleMarkersInitialized(size_t count) +{ + const int index = static_cast(count - 1); + + beginInsertRows(QModelIndex(), 0, index); + endInsertRows(); +} + +void MarkerModel::HandleMarkerAdded() +{ + const int newIndex = static_cast(p->markerManager_->marker_count() - 1); + + beginInsertRows(QModelIndex(), newIndex, newIndex); + endInsertRows(); +} + +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); + + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + endRemoveRows(); +} + +} // 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..85112fa1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/model/marker_model.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace model +{ + +class MarkerModel : public QAbstractTableModel +{ +public: + enum class Column : int + { + Latitude = 0, + Longitude = 1, + Name = 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 HandleMarkersInitialized(size_t count); + void HandleMarkerAdded(); + void HandleMarkerChanged(size_t index); + void HandleMarkerRemoved(size_t index); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace model +} // 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..bd607cc7 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::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 f0561a6e..bfc10839 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, + Markers, Unknown }; diff --git a/scwx-qt/source/scwx/qt/types/marker_types.hpp b/scwx-qt/source/scwx/qt/types/marker_types.hpp new file mode 100644 index 00000000..0d9c575b --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/marker_types.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +struct MarkerInfo +{ + MarkerInfo(const 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 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 }; 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 + + + + + + + + + + +