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