mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 07:20:04 +00:00 
			
		
		
		
	Merge pull request #281 from AdenKoperczak/location_markers
Add Location Markers (Part 1)
This commit is contained in:
		
						commit
						1843f387ff
					
				
					 25 changed files with 1343 additions and 3 deletions
				
			
		
							
								
								
									
										11
									
								
								scwx-qt/res/textures/images/location-marker.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								scwx-qt/res/textures/images/location-marker.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| <svg version="1.1" | ||||
|      viewBox="0 0 170 150" | ||||
|      width="17" height="15" | ||||
|      xmlns="http://www.w3.org/2000/svg"> | ||||
| 
 | ||||
|     <path d="M 40,118 L 85,40 L 130,118 L 40,118 L 85,40 Z" | ||||
|         stroke="black" stroke-width="40" fill="none"/> | ||||
|     <path d="M 40,118 L 85,40 L 130,118 L 40,118 L 85,40 Z" | ||||
|         stroke="red"   stroke-width="20" fill="none"/> | ||||
| 
 | ||||
|  </svg> | ||||
| After Width: | Height: | Size: 355 B | 
|  | @ -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 | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ | |||
|         <file>res/textures/images/cursor-17.png</file> | ||||
|         <file>res/textures/images/crosshairs-24.png</file> | ||||
|         <file>res/textures/images/dot-3.png</file> | ||||
|         <file>res/textures/images/location-marker.svg</file> | ||||
|         <file>res/textures/images/mapbox-logo.svg</file> | ||||
|         <file>res/textures/images/maptiler-logo.svg</file> | ||||
|     </qresource> | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| #include <scwx/qt/manager/alert_manager.hpp> | ||||
| #include <scwx/qt/manager/hotkey_manager.hpp> | ||||
| #include <scwx/qt/manager/placefile_manager.hpp> | ||||
| #include <scwx/qt/manager/marker_manager.hpp> | ||||
| #include <scwx/qt/manager/position_manager.hpp> | ||||
| #include <scwx/qt/manager/radar_product_manager.hpp> | ||||
| #include <scwx/qt/manager/text_event_manager.hpp> | ||||
|  | @ -30,6 +31,7 @@ | |||
| #include <scwx/qt/ui/level2_settings_widget.hpp> | ||||
| #include <scwx/qt/ui/level3_products_widget.hpp> | ||||
| #include <scwx/qt/ui/placefile_dialog.hpp> | ||||
| #include <scwx/qt/ui/marker_dialog.hpp> | ||||
| #include <scwx/qt/ui/radar_site_dialog.hpp> | ||||
| #include <scwx/qt/ui/settings_dialog.hpp> | ||||
| #include <scwx/qt/ui/update_dialog.hpp> | ||||
|  | @ -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<manager::HotkeyManager> hotkeyManager_ { | ||||
|       manager::HotkeyManager::Instance()}; | ||||
|    std::shared_ptr<manager::PlacefileManager> placefileManager_; | ||||
|    std::shared_ptr<manager::MarkerManager>    markerManager_; | ||||
|    std::shared_ptr<manager::PositionManager>  positionManager_; | ||||
|    std::shared_ptr<manager::TextEventManager> textEventManager_; | ||||
|    std::shared_ptr<manager::TimelineManager>  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(); | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ | |||
|      <x>0</x> | ||||
|      <y>0</y> | ||||
|      <width>1024</width> | ||||
|      <height>33</height> | ||||
|      <height>22</height> | ||||
|     </rect> | ||||
|    </property> | ||||
|    <widget class="QMenu" name="menuFile"> | ||||
|  | @ -104,6 +104,7 @@ | |||
|     </property> | ||||
|     <addaction name="actionPlacefileManager"/> | ||||
|     <addaction name="actionLayerManager"/> | ||||
|     <addaction name="actionMarkerManager"/> | ||||
|    </widget> | ||||
|    <addaction name="menuFile"/> | ||||
|    <addaction name="menuView"/> | ||||
|  | @ -152,8 +153,8 @@ | |||
|          <rect> | ||||
|           <x>0</x> | ||||
|           <y>0</y> | ||||
|           <width>190</width> | ||||
|           <height>686</height> | ||||
|           <width>205</width> | ||||
|           <height>701</height> | ||||
|          </rect> | ||||
|         </property> | ||||
|         <layout class="QVBoxLayout" name="verticalLayout_6"> | ||||
|  | @ -487,6 +488,15 @@ | |||
|     <string>&GPS Info</string> | ||||
|    </property> | ||||
|   </action> | ||||
|   <action name="actionMarkerManager"> | ||||
|    <property name="icon"> | ||||
|     <iconset resource="../../../../scwx-qt.qrc"> | ||||
|      <normaloff>:/res/icons/font-awesome-6/house-solid.svg</normaloff>:/res/icons/font-awesome-6/house-solid.svg</iconset> | ||||
|    </property> | ||||
|    <property name="text"> | ||||
|     <string>Location &Marker Manager</string> | ||||
|    </property> | ||||
|   </action> | ||||
|  </widget> | ||||
|  <resources> | ||||
|   <include location="../../../../scwx-qt.qrc"/> | ||||
|  |  | |||
							
								
								
									
										317
									
								
								scwx-qt/source/scwx/qt/manager/marker_manager.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								scwx-qt/source/scwx/qt/manager/marker_manager.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,317 @@ | |||
| #include <scwx/qt/manager/marker_manager.hpp> | ||||
| #include <scwx/qt/types/marker_types.hpp> | ||||
| #include <scwx/qt/util/json.hpp> | ||||
| #include <scwx/qt/main/application.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <filesystem> | ||||
| #include <shared_mutex> | ||||
| #include <vector> | ||||
| #include <string> | ||||
| 
 | ||||
| #include <QStandardPaths> | ||||
| #include <boost/json.hpp> | ||||
| #include <boost/asio/post.hpp> | ||||
| #include <boost/asio/thread_pool.hpp> | ||||
| 
 | ||||
| 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<std::shared_ptr<MarkerRecord>> markerRecords_ {}; | ||||
| 
 | ||||
|    MarkerManager* self_; | ||||
| 
 | ||||
|    boost::asio::thread_pool threadPool_ {1u}; | ||||
|    std::shared_mutex        markerRecordLock_ {}; | ||||
| 
 | ||||
|    void                          InitializeMarkerSettings(); | ||||
|    void                          ReadMarkerSettings(); | ||||
|    void                          WriteMarkerSettings(); | ||||
|    std::shared_ptr<MarkerRecord> 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<MarkerRecord>& record) | ||||
|    { | ||||
|       jv = {{kNameName_, record->markerInfo_.name}, | ||||
|             {kLatitudeName_, record->markerInfo_.latitude}, | ||||
|             {kLongitudeName_, record->markerInfo_.longitude}}; | ||||
|    } | ||||
| 
 | ||||
|    friend MarkerRecord tag_invoke(boost::json::value_to_tag<MarkerRecord>, | ||||
|                                   const boost::json::value& jv) | ||||
|    { | ||||
|       return MarkerRecord( | ||||
|          boost::json::value_to<std::string>(jv.at(kNameName_)), | ||||
|          boost::json::value_to<double>(jv.at(kLatitudeName_)), | ||||
|          boost::json::value_to<double>(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<MarkerRecord>(markerEntry); | ||||
| 
 | ||||
|                if (!record.markerInfo_.name.empty()) | ||||
|                { | ||||
|                   markerRecords_.emplace_back( | ||||
|                      std::make_shared<MarkerRecord>(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::MarkerRecord> | ||||
| 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<Impl>(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<types::MarkerInfo> MarkerManager::get_marker(size_t index) | ||||
| { | ||||
|    std::shared_lock lock(p->markerRecordLock_); | ||||
|    if (index >= p->markerRecords_.size()) | ||||
|    { | ||||
|       return {}; | ||||
|    } | ||||
|    std::shared_ptr<MarkerManager::Impl::MarkerRecord>& 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<MarkerManager::Impl::MarkerRecord>& 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<Impl::MarkerRecord>(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<MarkerManager::Impl::MarkerRecord>& 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> MarkerManager::Instance() | ||||
| { | ||||
|    static std::weak_ptr<MarkerManager> markerManagerReference_ {}; | ||||
|    static std::mutex                   instanceMutex_ {}; | ||||
| 
 | ||||
|    std::unique_lock lock(instanceMutex_); | ||||
| 
 | ||||
|    std::shared_ptr<MarkerManager> markerManager = | ||||
|       markerManagerReference_.lock(); | ||||
| 
 | ||||
|    if (markerManager == nullptr) | ||||
|    { | ||||
|       markerManager           = std::make_shared<MarkerManager>(); | ||||
|       markerManagerReference_ = markerManager; | ||||
|    } | ||||
| 
 | ||||
|    return markerManager; | ||||
| } | ||||
| 
 | ||||
| } // namespace manager
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										46
									
								
								scwx-qt/source/scwx/qt/manager/marker_manager.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								scwx-qt/source/scwx/qt/manager/marker_manager.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/types/marker_types.hpp> | ||||
| 
 | ||||
| #include <QObject> | ||||
| #include <optional> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace manager | ||||
| { | ||||
| 
 | ||||
| class MarkerManager : public QObject | ||||
| { | ||||
|    Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|    explicit MarkerManager(); | ||||
|    ~MarkerManager(); | ||||
| 
 | ||||
|    size_t                   marker_count(); | ||||
|    std::optional<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<MarkerManager> 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<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace manager
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -12,6 +12,7 @@ | |||
| #include <scwx/qt/map/overlay_layer.hpp> | ||||
| #include <scwx/qt/map/overlay_product_layer.hpp> | ||||
| #include <scwx/qt/map/placefile_layer.hpp> | ||||
| #include <scwx/qt/map/marker_layer.hpp> | ||||
| #include <scwx/qt/map/radar_product_layer.hpp> | ||||
| #include <scwx/qt/map/radar_range_layer.hpp> | ||||
| #include <scwx/qt/map/radar_site_layer.hpp> | ||||
|  | @ -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>        overlayLayer_; | ||||
|    std::shared_ptr<OverlayProductLayer> overlayProductLayer_ {nullptr}; | ||||
|    std::shared_ptr<PlacefileLayer>      placefileLayer_; | ||||
|    std::shared_ptr<MarkerLayer>            markerLayer_; | ||||
|    std::shared_ptr<ColorTableLayer>     colorTableLayer_; | ||||
|    std::shared_ptr<RadarSiteLayer>      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<MarkerLayer>(context_); | ||||
|          AddLayer(layerName, markerLayer_, before); | ||||
|          break; | ||||
| 
 | ||||
|       default: | ||||
|          break; | ||||
|       } | ||||
|  |  | |||
							
								
								
									
										113
									
								
								scwx-qt/source/scwx/qt/map/marker_layer.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								scwx-qt/source/scwx/qt/map/marker_layer.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,113 @@ | |||
| #include <scwx/qt/map/marker_layer.hpp> | ||||
| #include <scwx/qt/manager/marker_manager.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| #include <scwx/qt/types/marker_types.hpp> | ||||
| #include <scwx/qt/types/texture_types.hpp> | ||||
| #include <scwx/qt/gl/draw/geo_icons.hpp> | ||||
| 
 | ||||
| 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<MapContext> context) : | ||||
|        self_ {self}, geoIcons_ {std::make_shared<gl::draw::GeoIcons>(context)} | ||||
|    { | ||||
|       ConnectSignals(); | ||||
|    } | ||||
|    ~Impl() {} | ||||
| 
 | ||||
|    void ReloadMarkers(); | ||||
|    void ConnectSignals(); | ||||
| 
 | ||||
|    MarkerLayer* self_; | ||||
|    const std::string& markerIconName_ { | ||||
|       types::GetTextureName(types::ImageTexture::LocationMarker)}; | ||||
| 
 | ||||
|    std::shared_ptr<gl::draw::GeoIcons> 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<types::MarkerInfo> marker = markerManager->get_marker(i); | ||||
|       if (!marker) | ||||
|       { | ||||
|          break; | ||||
|       } | ||||
|       std::shared_ptr<gl::draw::GeoIconDrawItem> 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<MapContext>& context) : | ||||
|     DrawLayer(context), p(std::make_unique<MarkerLayer::Impl>(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
 | ||||
							
								
								
									
										33
									
								
								scwx-qt/source/scwx/qt/map/marker_layer.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								scwx-qt/source/scwx/qt/map/marker_layer.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <scwx/qt/map/draw_layer.hpp> | ||||
| 
 | ||||
| #include <string> | ||||
| 
 | ||||
| namespace scwx | ||||
| { | ||||
| namespace qt | ||||
| { | ||||
| namespace map | ||||
| { | ||||
| 
 | ||||
| class MarkerLayer : public DrawLayer | ||||
| { | ||||
|    Q_OBJECT | ||||
| 
 | ||||
| public: | ||||
|    explicit MarkerLayer(const std::shared_ptr<MapContext>& context); | ||||
|    ~MarkerLayer(); | ||||
| 
 | ||||
|    void Initialize() override final; | ||||
|    void Render(const QMapLibre::CustomLayerRenderParameters&) override final; | ||||
|    void Deinitialize() override final; | ||||
| 
 | ||||
| private: | ||||
|    class Impl; | ||||
|    std::unique_ptr<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace map
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -43,6 +43,7 @@ static const std::vector<types::LayerInfo> 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}, | ||||
|  |  | |||
							
								
								
									
										290
									
								
								scwx-qt/source/scwx/qt/model/marker_model.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								scwx-qt/source/scwx/qt/model/marker_model.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,290 @@ | |||
| #include <scwx/common/geographic.hpp> | ||||
| #include <scwx/qt/model/marker_model.hpp> | ||||
| #include <scwx/qt/manager/marker_manager.hpp> | ||||
| #include <scwx/qt/types/marker_types.hpp> | ||||
| #include <scwx/qt/types/qt_types.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <QApplication> | ||||
| 
 | ||||
| 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<int>(MarkerModel::Column::Latitude); | ||||
| static constexpr int kLastColumn = | ||||
|    static_cast<int>(MarkerModel::Column::Name); | ||||
| static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; | ||||
| 
 | ||||
| class MarkerModel::Impl | ||||
| { | ||||
| public: | ||||
|    explicit Impl() {} | ||||
|    ~Impl() = default; | ||||
|    std::shared_ptr<manager::MarkerManager> markerManager_ { | ||||
|       manager::MarkerManager::Instance()}; | ||||
| }; | ||||
| 
 | ||||
| MarkerModel::MarkerModel(QObject* parent) : | ||||
|    QAbstractTableModel(parent), p(std::make_unique<Impl>()) | ||||
| { | ||||
| 
 | ||||
|    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<int>(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<int>(Column::Name): | ||||
|    case static_cast<int>(Column::Latitude): | ||||
|    case static_cast<int>(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<types::MarkerInfo> markerInfo = | ||||
|       p->markerManager_->get_marker(index.row()); | ||||
|    if (!markerInfo) | ||||
|    { | ||||
|       return QVariant(); | ||||
|    } | ||||
| 
 | ||||
|    switch(index.column()) | ||||
|    { | ||||
|    case static_cast<int>(Column::Name): | ||||
|       if (role == Qt::ItemDataRole::DisplayRole || | ||||
|           role == Qt::ItemDataRole::ToolTipRole || | ||||
|           role == Qt::ItemDataRole::EditRole) | ||||
|       { | ||||
|          return QString::fromStdString(markerInfo->name); | ||||
|       } | ||||
|       break; | ||||
| 
 | ||||
|    case static_cast<int>(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<int>(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<int>(Column::Name): | ||||
|                return tr("Name"); | ||||
| 
 | ||||
|             case static_cast<int>(Column::Latitude): | ||||
|                return tr("Latitude"); | ||||
| 
 | ||||
|             case static_cast<int>(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<types::MarkerInfo> markerInfo = | ||||
|       p->markerManager_->get_marker(index.row()); | ||||
|    if (!markerInfo) | ||||
|    { | ||||
|       return false; | ||||
|    } | ||||
|    bool result = false; | ||||
| 
 | ||||
|    switch(index.column()) | ||||
|    { | ||||
|    case static_cast<int>(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<int>(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<int>(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<int>(count - 1); | ||||
| 
 | ||||
|    beginInsertRows(QModelIndex(), 0, index); | ||||
|    endInsertRows(); | ||||
| } | ||||
| 
 | ||||
| void MarkerModel::HandleMarkerAdded() | ||||
| { | ||||
|    const int newIndex = static_cast<int>(p->markerManager_->marker_count() - 1); | ||||
| 
 | ||||
|    beginInsertRows(QModelIndex(), newIndex, newIndex); | ||||
|    endInsertRows(); | ||||
| } | ||||
| 
 | ||||
| void MarkerModel::HandleMarkerChanged(size_t index) | ||||
| { | ||||
|    const int changedIndex = static_cast<int>(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<int>(index); | ||||
| 
 | ||||
|    beginRemoveRows(QModelIndex(), removedIndex, removedIndex); | ||||
|    endRemoveRows(); | ||||
| } | ||||
| 
 | ||||
| } // namespace model
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										54
									
								
								scwx-qt/source/scwx/qt/model/marker_model.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								scwx-qt/source/scwx/qt/model/marker_model.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <QAbstractTableModel> | ||||
| 
 | ||||
| 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<Impl> p; | ||||
| }; | ||||
| 
 | ||||
| } // namespace model
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
|  | @ -31,6 +31,7 @@ static const std::unordered_map<InformationLayer, std::string> | |||
|    informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, | ||||
|                           {InformationLayer::RadarSite, "Radar Sites"}, | ||||
|                           {InformationLayer::ColorTable, "Color Table"}, | ||||
|                           {InformationLayer::Markers, "Location Markers"}, | ||||
|                           {InformationLayer::Unknown, "?"}}; | ||||
| 
 | ||||
| static const std::unordered_map<MapLayer, std::string> mapLayerName_ { | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ enum class InformationLayer | |||
|    MapOverlay, | ||||
|    RadarSite, | ||||
|    ColorTable, | ||||
|    Markers, | ||||
|    Unknown | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										26
									
								
								scwx-qt/source/scwx/qt/types/marker_types.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								scwx-qt/source/scwx/qt/types/marker_types.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <string> | ||||
| 
 | ||||
| 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
 | ||||
|  | @ -25,6 +25,8 @@ static const std::unordered_map<ImageTexture, TextureInfo> 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, | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ enum class ImageTexture | |||
|    Crosshairs24, | ||||
|    Cursor17, | ||||
|    Dot3, | ||||
|    LocationMarker, | ||||
|    MapboxLogo, | ||||
|    MapTilerLogo | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										45
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| #include "marker_dialog.hpp" | ||||
| #include "ui_marker_dialog.h" | ||||
| 
 | ||||
| #include <scwx/qt/ui/marker_settings_widget.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| 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<MarkerDialogImpl>()}, | ||||
|     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
 | ||||
							
								
								
									
										35
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <QDialog> | ||||
| 
 | ||||
| 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<MarkerDialogImpl> p; | ||||
|    Ui::MarkerDialog*                 ui; | ||||
| }; | ||||
| 
 | ||||
| } // namespace ui
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										88
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								scwx-qt/source/scwx/qt/ui/marker_dialog.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>MarkerDialog</class> | ||||
|  <widget class="QDialog" name="MarkerDialog"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>700</width> | ||||
|     <height>600</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Marker Manager</string> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|    <item> | ||||
|     <widget class="QFrame" name="contentsFrame"> | ||||
|      <property name="frameShape"> | ||||
|       <enum>QFrame::StyledPanel</enum> | ||||
|      </property> | ||||
|      <property name="frameShadow"> | ||||
|       <enum>QFrame::Raised</enum> | ||||
|      </property> | ||||
|      <layout class="QVBoxLayout" name="verticalLayout_2"> | ||||
|       <property name="leftMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="topMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="rightMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="bottomMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <widget class="QDialogButtonBox" name="buttonBox"> | ||||
|      <property name="orientation"> | ||||
|       <enum>Qt::Horizontal</enum> | ||||
|      </property> | ||||
|      <property name="standardButtons"> | ||||
|       <set>QDialogButtonBox::Close</set> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|  <connections> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>accepted()</signal> | ||||
|    <receiver>MarkerDialog</receiver> | ||||
|    <slot>accept()</slot> | ||||
|    <hints> | ||||
|     <hint type="sourcelabel"> | ||||
|      <x>248</x> | ||||
|      <y>254</y> | ||||
|     </hint> | ||||
|     <hint type="destinationlabel"> | ||||
|      <x>157</x> | ||||
|      <y>274</y> | ||||
|     </hint> | ||||
|    </hints> | ||||
|   </connection> | ||||
|   <connection> | ||||
|    <sender>buttonBox</sender> | ||||
|    <signal>rejected()</signal> | ||||
|    <receiver>MarkerDialog</receiver> | ||||
|    <slot>reject()</slot> | ||||
|    <hints> | ||||
|     <hint type="sourcelabel"> | ||||
|      <x>316</x> | ||||
|      <y>260</y> | ||||
|     </hint> | ||||
|     <hint type="destinationlabel"> | ||||
|      <x>286</x> | ||||
|      <y>274</y> | ||||
|     </hint> | ||||
|    </hints> | ||||
|   </connection> | ||||
|  </connections> | ||||
| </ui> | ||||
							
								
								
									
										105
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | |||
| #include "marker_settings_widget.hpp" | ||||
| #include "ui_marker_settings_widget.h" | ||||
| 
 | ||||
| #include <scwx/qt/manager/marker_manager.hpp> | ||||
| #include <scwx/qt/model/marker_model.hpp> | ||||
| #include <scwx/qt/types/qt_types.hpp> | ||||
| #include <scwx/qt/ui/open_url_dialog.hpp> | ||||
| #include <scwx/util/logger.hpp> | ||||
| 
 | ||||
| #include <QSortFilterProxyModel> | ||||
| 
 | ||||
| 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<manager::MarkerManager> markerManager_ { | ||||
|       manager::MarkerManager::Instance()}; | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| MarkerSettingsWidget::MarkerSettingsWidget(QWidget* parent) : | ||||
|     QFrame(parent), | ||||
|     p {std::make_unique<MarkerSettingsWidgetImpl>(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<int>( | ||||
|                                 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
 | ||||
							
								
								
									
										35
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <QFrame> | ||||
| 
 | ||||
| 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<MarkerSettingsWidgetImpl> p; | ||||
|    Ui::MarkerSettingsWidget*                 ui; | ||||
| }; | ||||
| 
 | ||||
| } // namespace ui
 | ||||
| } // namespace qt
 | ||||
| } // namespace scwx
 | ||||
							
								
								
									
										88
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								scwx-qt/source/scwx/qt/ui/marker_settings_widget.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>MarkerSettingsWidget</class> | ||||
|  <widget class="QFrame" name="MarkerSettingsWidget"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>400</width> | ||||
|     <height>300</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Frame</string> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|    <item> | ||||
|     <widget class="QTreeView" name="markerView"> | ||||
|      <property name="alternatingRowColors"> | ||||
|       <bool>true</bool> | ||||
|      </property> | ||||
|      <property name="indentation"> | ||||
|       <number>0</number> | ||||
|      </property> | ||||
|      <property name="sortingEnabled"> | ||||
|       <bool>true</bool> | ||||
|      </property> | ||||
|     </widget> | ||||
|    </item> | ||||
|    <item> | ||||
|     <widget class="QFrame" name="buttonFrame"> | ||||
|      <property name="frameShape"> | ||||
|       <enum>QFrame::Shape::StyledPanel</enum> | ||||
|      </property> | ||||
|      <property name="frameShadow"> | ||||
|       <enum>QFrame::Shadow::Raised</enum> | ||||
|      </property> | ||||
|      <layout class="QHBoxLayout" name="horizontalLayout_2"> | ||||
|       <property name="leftMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="topMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="rightMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <property name="bottomMargin"> | ||||
|        <number>0</number> | ||||
|       </property> | ||||
|       <item> | ||||
|        <spacer name="horizontalSpacer"> | ||||
|         <property name="orientation"> | ||||
|          <enum>Qt::Orientation::Horizontal</enum> | ||||
|         </property> | ||||
|         <property name="sizeHint" stdset="0"> | ||||
|          <size> | ||||
|           <width>40</width> | ||||
|           <height>20</height> | ||||
|          </size> | ||||
|         </property> | ||||
|        </spacer> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QPushButton" name="addButton"> | ||||
|         <property name="text"> | ||||
|          <string>&Add</string> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|       <item> | ||||
|        <widget class="QPushButton" name="removeButton"> | ||||
|         <property name="enabled"> | ||||
|          <bool>false</bool> | ||||
|         </property> | ||||
|         <property name="text"> | ||||
|          <string>R&emove</string> | ||||
|         </property> | ||||
|        </widget> | ||||
|       </item> | ||||
|      </layout> | ||||
|     </widget> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat