Use the Geometry Editor to edit geometries using utility network connectivity rules.

Use case
A field worker can create new features in a utility network by editing and snapping the vertices of a geometry to existing features on a map. In a gas utility network, gas pipeline features can be represented with the polyline geometry type. Utility networks use geometric coincident-based connectivity to provide pathways for resources. Rule-based snapping uses utility network connectivity rules when editing features based on their asset type and asset group to help maintain network connectivity.
How to use the sample
To edit a geometry, tap a point geometry to be edited in the map to select it. Then edit the geometry by clicking the button to start the geometry editor.
Snap sources can be enabled and disabled. Snapping will not occur when SnapRuleBehavior.RulesPreventSnapping even when the source is enabled.
To interactively snap a vertex to a feature or graphic, ensure that snapping is enabled for the relevant snap source, then move the mouse pointer or drag a vertex close to an existing feature or graphic. If the existing feature or graphic has valid utility network connectivity rules for the asset type that is being created or edited, the edit position will be adjusted to coincide with (or snap to) edges and vertices of its geometry. Click or release the touch pointer to place the vertex at the snapped location.
To discard changes and stop the geometry editor, press the discard button.
To save your edits, press the save button.
How it works
-
Create a map with
LoadSettings.FeatureTilingModeset toEnabledWithFullResolutionWhenSupported. -
Create a
Geodatabaseusing the mobile geodatabase file location. -
Display
Geodatabase.FeatureTableson the map using subtype feature layers. -
Create a
GeometryEditorand connect it to the map view. -
When editing a feature:
a. Call
SnapRules.CreateAsync(UtilityNetwork, UtilityAssetType)to get the snap rules associated with a givenUtilityAssetType.b. Use
SyncSourceSettings(SnapRules, SnapSourceEnablingBehavior.SetFromRules)to populate theSnapSettings.SourceSettingswithSnapSourceSettingsenabling the sources with rules. -
Start the geometry editor with an existing geometry or
GeometryType.Point.
Relevant API
- FeatureLayer
- Geometry
- GeometryEditor
- GeometryEditorStyle
- GraphicsOverlay
- MapView
- SnapRuleBehavior
- SnapRules
- SnapSettings
- SnapSource
- SnapSourceEnablingBehavior
- SnapSourceSettings
- UtilityNetwork
Offline data
Read more about how to set up the sample’s offline data here.
| Link | Local Location |
|---|---|
| Naperville gas network | <userhome>/ArcGIS/Runtime/Data/raster/NapervilleGasUtilities.geodatabase |
About the data
The Naperville gas network mobile geodatabase contains a utility network with a set of connectivity rules that can be used to perform geometry edits with rules based snapping.
Tags
edit, feature, geometry editor, graphics, layers, map, snapping, utility network
Sample Code
// [WriteFile Name=SnapGeometryEditsWithRules, Category=EditData]// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
import QtQuickimport QtQuick.Controlsimport QtQuick.Layouts
// This component defines each of the buttons in the Geometry Editor control UI
RoundButton { id: geometryEditorButton
property string buttonName: "" property string iconPath: ""
Layout.fillWidth: true
// Set the focus policy so that the buttons do not take focus from the MapView focusPolicy: Qt.NoFocus
radius: 5
Rectangle { anchors.fill: parent radius: parent.radius opacity: parent.enabled || parent.checked ? 1 : 0.3 color: geometryEditorButton.down ? "#d0d0d0" : "#e0e0e0" }
Image { id: imgComponent anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter verticalCenterOffset: -textComponent.height/2 } source: iconPath width: 20 fillMode: Image.PreserveAspectFit }
Text { id: textComponent anchors { top: imgComponent.bottom horizontalCenter: parent.horizontalCenter } text: buttonName font.pixelSize: 8 }}// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
// sample headers#include "SnapGeometryEditsWithRules.h"#include "SnapSourcesListModel.h"
// ArcGIS Maps SDK headers#include "ArcGISFeature.h"#include "ArcGISFeatureLayerInfo.h"#include "DrawingInfo.h"#include "Error.h"#include "ErrorException.h"#include "FeatureLayer.h"#include "Geodatabase.h"#include "GeodatabaseFeatureTable.h"#include "GeometryEditor.h"#include "GeometryEditorStyle.h"#include "GeometryEditorTool.h"#include "GeometryEditorTypes.h"#include "Graphic.h"#include "GraphicListModel.h"#include "GraphicsOverlay.h"#include "GraphicsOverlayListModel.h"#include "IdentifyLayerResult.h"#include "LayerListModel.h"#include "LoadSettings.h"#include "Map.h"#include "MapQuickView.h"#include "MapTypes.h"#include "Point.h"#include "ReticleVertexTool.h"#include "SimpleLineSymbol.h"#include "SimpleRenderer.h"#include "SnapRules.h"#include "SnapSettings.h"#include "SnapSourceSettings.h"#include "SpatialReference.h"#include "SubtypeFeatureLayer.h"#include "SubtypeSublayer.h"#include "SubtypeSublayerListModel.h"#include "SymbolTypes.h"#include "UtilityAssetGroup.h"#include "UtilityAssetType.h"#include "UtilityElement.h"#include "UtilityNetwork.h"#include "UtilityNetworkListModel.h"#include "Viewpoint.h"
// Qt headers#include <QFile>#include <QFuture>#include <QStandardPaths>#include <QTimer>#include <QtGlobal>
using namespace Esri::ArcGISRuntime;
namespace{ QString defaultDataPath() { QString dataPath;
#ifdef Q_OS_IOS dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);#else dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);#endif
return dataPath; }
// Helper load function that converts a Loadable::doneLoading signal to a QFuture representation. template <typename T> QFuture<void> load(T* loadable) { static_assert(std::is_base_of<Loadable, T>::value, "T must derive from Loadable");
QPromise<void> promise; QFuture<void> future = promise.future();
QObject::connect(loadable, &T::doneLoading, [promise = std::move(promise)](const Error& error) mutable { if (!error.isEmpty()) { promise.setException(ErrorException{error}); } else { promise.finish(); } });
loadable->load(); return future; }
// Helper function to create a QFuture with an error. template <typename T> QFuture<T> makeExceptionalFutureHelper(const QString& error, const QString& additionalInfo = "") { // Create an error object with the provided error message and additional info. Error e{error, additionalInfo};
// Return a future that is already completed with the error. return QtFuture::makeExceptionalFuture<T>(ErrorException{e}); }}
SnapGeometryEditsWithRules::SnapGeometryEditsWithRules(QObject* parent) : QObject(parent), m_map(new Map(BasemapStyle::ArcGISStreetsNight, this)), m_snapSourcesListModel(new SnapSourcesListModel(this)), m_defaultGraphicsOverlayRenderer(new SimpleRenderer(new SimpleLineSymbol(SimpleLineSymbolStyle::Dash, QColor(Qt::gray), 3, this), this)), m_rulesLimitSymbol(new SimpleLineSymbol(SimpleLineSymbolStyle::Solid, QColor("orange"), 3, this)), m_rulesPreventSymbol(new SimpleLineSymbol(SimpleLineSymbolStyle::Solid, QColor("red"), 3, this)), m_noneSymbol(new SimpleLineSymbol(SimpleLineSymbolStyle::Dash, QColor("green"), 3, this)){ initializeMap();}
SnapGeometryEditsWithRules::~SnapGeometryEditsWithRules() = default;
void SnapGeometryEditsWithRules::init(){ // Register the map view for QML qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView"); qmlRegisterType<SnapGeometryEditsWithRules>("Esri.Samples", 1, 0, "SnapGeometryEditsWithRulesSample");}
void SnapGeometryEditsWithRules::initializeMap(){ m_map->loadSettings()->setFeatureTilingMode(FeatureTilingMode::EnabledWithFullResolutionWhenSupported); m_map->setInitialViewpoint(Viewpoint(Point(-9811055.156028448, 5131792.19502501, SpatialReference::webMercator()), 10000));
// Load the geodatabase loadGeodatabase().then(this, [this]() { loadOperationalLayers().then(this, [this](const LoadOperationalLayersReturnStruct& layers) { // Pass the returned pipeLayer and deviceLayer from the future to modify their visibility modifyOperationalLayersVisibility(layers.m_pipeSubtypeLayer, layers.m_deviceSubtypeLayer); });
loadUtilityNetwork(); });}
QFuture<void> SnapGeometryEditsWithRules::loadGeodatabase(){ // Get the path to the local geodatabase for this platform (temp directory, for example). const QString geodatabasePath = defaultDataPath() + "/ArcGIS/Runtime/Data/geodatabase/NapervilleGasUtilities.geodatabase";
if (!QFile::exists(geodatabasePath)) { return makeExceptionalFutureHelper<void>("Geodatabase file does not exist"); }
m_geodatabase = new Geodatabase(geodatabasePath, this); return load(m_geodatabase);}
QFuture<LoadOperationalLayersReturnStruct> SnapGeometryEditsWithRules::loadOperationalLayers(){ // Get and load feature tables from the geodatabase. // Create subtype feature layers from the feature tables and add them to the map. QList<QFuture<void>> layerLoadingFutures;
auto* pipeTable = m_geodatabase->geodatabaseFeatureTable("PipelineLine"); if (!pipeTable) { return makeExceptionalFutureHelper<LoadOperationalLayersReturnStruct>("Unable to create PipelineLine feature table"); } auto pipeLayer = new SubtypeFeatureLayer(pipeTable, this); m_map->operationalLayers()->append(pipeLayer); layerLoadingFutures.emplace_back(load(pipeLayer));
auto* deviceTable = m_geodatabase->geodatabaseFeatureTable("PipelineDevice"); if (!deviceTable) { return makeExceptionalFutureHelper<LoadOperationalLayersReturnStruct>("Unable to create PipelineDevice feature table"); } auto deviceLayer = new SubtypeFeatureLayer(deviceTable, this); m_map->operationalLayers()->append(deviceLayer); layerLoadingFutures.emplace_back(load(deviceLayer));
auto* junctionTable = m_geodatabase->geodatabaseFeatureTable("PipelineJunction"); if (!junctionTable) { return makeExceptionalFutureHelper<LoadOperationalLayersReturnStruct>("Unable to create PipelineJunction feature table"); } auto junctionLayer = new SubtypeFeatureLayer(junctionTable, this); m_map->operationalLayers()->append(junctionLayer); layerLoadingFutures.emplace_back(load(junctionLayer));
return QtFuture::whenAll(layerLoadingFutures.begin(), layerLoadingFutures.end()).then(this, [pipeLayer, deviceLayer](const QList<QFuture<void>>&) { return LoadOperationalLayersReturnStruct{pipeLayer, deviceLayer}; });}
QFuture<void> SnapGeometryEditsWithRules::loadUtilityNetwork(){ // Add the utility network to the map and load it. auto* utilityNetwork = m_geodatabase->utilityNetworks().constFirst(); if (!utilityNetwork) { return makeExceptionalFutureHelper<void>("Unable to create UtilityNetwork"); }
m_map->utilityNetworks()->append(utilityNetwork);
return load(utilityNetwork);}
ArcGISFeature* SnapGeometryEditsWithRules::getFeatureFromResult(const QList<IdentifyLayerResult*>& identifyResult){ if (identifyResult.isEmpty()) { return nullptr; }
QList<IdentifyLayerResult*> sublayerResults = identifyResult.first()->sublayerResults(); if (sublayerResults.isEmpty()) { return nullptr; }
QList<GeoElement*> geoElements = sublayerResults.first()->geoElements(); if (geoElements.isEmpty()) { return nullptr; }
auto* feature = dynamic_cast<ArcGISFeature*>(geoElements.first()); if (!feature) { return nullptr; }
return feature;}
void SnapGeometryEditsWithRules::onMapViewClicked(const QMouseEvent& mouseEvent){ if (m_mapView->geometryEditor()->isStarted()) { return; }
// Identify the feature at the tapped location. m_mapView->identifyLayersAsync(mouseEvent.position(), 5, false).then(this, [this](const QList<IdentifyLayerResult*>& identifyResult) { ArcGISFeature* selectedFeature = getFeatureFromResult(identifyResult);
// In this sample we only allow selection of point features. // If the identified feature is null or the feature is not a point feature then reset the selection and return. if (!selectedFeature || !selectedFeature->featureTable() || selectedFeature->featureTable()->geometryType() != GeometryType::Point) { resetSelections(); return; }
if (m_selectedFeature != nullptr && selectedFeature != m_selectedFeature) { // If a feature is already selected and the tapped feature is not the selected feature then clear the previous selection. static_cast<FeatureLayer*>(m_selectedFeature->featureTable()->layer())->clearSelection(); }
// Update the selected feature. m_selectedFeature = selectedFeature;
// Select the feature on the layer. if (auto* layer = static_cast<FeatureLayer*>(m_selectedFeature->featureTable()->layer()); layer) { layer->selectFeature(m_selectedFeature); }
// Create a utility element for the selected feature using the utility network. std::unique_ptr<UtilityElement> utilityEle = (m_mapView->map()->utilityNetworks() && m_mapView->map()->utilityNetworks()->first()) ? std::unique_ptr<UtilityElement>(m_mapView->map()->utilityNetworks()->first()->createElementWithArcGISFeature(m_selectedFeature)) : nullptr;
// Update the UI visbility with the selected feature information. emit assetTypeChanged(utilityEle ? utilityEle->assetType()->name() : QString()); emit assetGroupChanged(utilityEle ? utilityEle->assetGroup()->name() : QString()); emit isElementSelectedChanged();
setSnapSettings(utilityEle ? utilityEle->assetType() : nullptr); });}
void SnapGeometryEditsWithRules::setMapView(MapQuickView* mapView){ if (m_mapView == mapView) { return; }
m_mapView = mapView; m_mapView->setMap(m_map); m_mapView->setGeometryEditor(new GeometryEditor(this));#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID) m_mapView->geometryEditor()->setTool(new ReticleVertexTool(this));#endif
auto* snapSettings = new SnapSettings(this); snapSettings->setEnabled(true); snapSettings->setFeatureSnappingEnabled(true); m_mapView->geometryEditor()->setSnapSettings(snapSettings);
auto* graphicsOverlay = new GraphicsOverlay(this); graphicsOverlay->setRenderer(m_defaultGraphicsOverlayRenderer);
const QString graphicJson = R"({"paths":[[[-9811826.6810284462,5132074.7700250093],[-9811786.4643617794,5132440.9533583419],[-9811384.2976951133,5132354.1700250087],[-9810372.5310284477,5132360.5200250093],[-9810353.4810284469,5132066.3033583425]]],"spatialReference":{"wkid":102100,"latestWkid":3857}})"; graphicsOverlay->graphics()->append(new Graphic(Geometry::fromJson(graphicJson), this));
m_mapView->graphicsOverlays()->append(graphicsOverlay);
connect(m_mapView, &MapQuickView::mouseClicked, this, &SnapGeometryEditsWithRules::onMapViewClicked);
emit mapViewChanged();}
MapQuickView* SnapGeometryEditsWithRules::mapView() const{ return m_mapView;}
bool SnapGeometryEditsWithRules::isElementSelected() const{ return (m_selectedFeature != nullptr);}
SnapSourcesListModel* SnapGeometryEditsWithRules::snapSourcesListModel() const{ return m_snapSourcesListModel;}
bool SnapGeometryEditsWithRules::geometryEditorStarted() const{ return (m_mapView && m_mapView->geometryEditor() && m_mapView->geometryEditor()->isStarted());}
void SnapGeometryEditsWithRules::startEditor(){ if (!m_selectedFeature || !m_selectedFeature->featureTable() || !m_selectedFeature->featureTable()->layer()) { return; }
// Get the symbol for the selected feature. Symbol* selectedFeatureSymbol = static_cast<GeodatabaseFeatureTable*>(m_selectedFeature->featureTable())->layerInfo().drawingInfo().renderer()->symbol(m_selectedFeature);
// Set the vertex symbol for the geometry editor tool. auto* geometryEditorStyle = m_mapView->geometryEditor()->tool()->style(); geometryEditorStyle->setVertexSymbol(selectedFeatureSymbol); geometryEditorStyle->setFeedbackVertexSymbol(selectedFeatureSymbol); geometryEditorStyle->setSelectedVertexSymbol(selectedFeatureSymbol);
// Hide the selected feature. static_cast<FeatureLayer*>(m_selectedFeature->featureTable()->layer())->setFeatureVisible(m_selectedFeature, false);
// Start the geometry editor. m_mapView->geometryEditor()->start(m_selectedFeature->geometry());
emit geometryEditorStartedChanged();}
void SnapGeometryEditsWithRules::stopEditing(){ // Stop the geometry editor and get the updated geometry. auto geometry = m_mapView->geometryEditor()->stop(); emit geometryEditorStartedChanged();
// Update the feature with the new geometry. if (m_selectedFeature) { m_selectedFeature->setGeometry(geometry); m_selectedFeature->featureTable()->updateFeatureAsync(m_selectedFeature).then(this, [this]() { // Reset the selection. resetSelections(); }); }}
void SnapGeometryEditsWithRules::discardEdits(){ // Discard the current edit. m_mapView->geometryEditor()->stop();
emit geometryEditorStartedChanged();
// Reset the selection. resetSelections();}
void SnapGeometryEditsWithRules::modifyOperationalLayersVisibility(SubtypeFeatureLayer* pipeLayer, SubtypeFeatureLayer* deviceLayer){ // Set the visibility of the sublayers and store the default renderer for the distribution and service pipe layers. // In this sample we will only set a small subset of sublayers to be visible. auto pipelineSublayers = pipeLayer->subtypeSublayers(); std::for_each(std::begin(*pipelineSublayers), std::end(*pipelineSublayers), [this](SubtypeSublayer* sublayer) { if (!sublayer) { return; } if (sublayer->name() == "Distribution Pipe") { // Store the default renderer for the distribution pipe layer. m_distributionPipeLayer = sublayer; m_defaultDistributionRenderer = sublayer->renderer(); } else if (sublayer->name() == "Service Pipe") { // Store the default renderer for the service pipe layer. m_servicePipeLayer = sublayer; m_defaultServiceRenderer = sublayer->renderer(); } else { sublayer->setVisible(false); } });
auto deviceSublayers = deviceLayer->subtypeSublayers(); std::for_each(std::begin(*deviceSublayers), std::end(*deviceSublayers), [](SubtypeSublayer* sublayer) { if (!sublayer) { return; } if (sublayer->name() == "Excess Flow Valve" || sublayer->name() == "Controllable Tee") { sublayer->setVisible(true); } else { sublayer->setVisible(false); } });}
void SnapGeometryEditsWithRules::setSnapSettings(UtilityAssetType* assetType){ if (!assetType) { return; }
// Get the snap rules associated with the asset type. SnapRules::createAsync(m_mapView->map()->utilityNetworks()->first(), assetType).then(this, [this](SnapRules* createdRules) { // Synchronize the snap source collection with the map's operational layers using the snap rules. // Setting SnapSourceEnablingBehavior.SetFromRules will enable snapping for the layers and sublayers specified in the snap rules. m_mapView->geometryEditor()->snapSettings()->syncSourceSettings(createdRules, SnapSourceEnablingBehavior::SetFromRules);
QList<SnapSourceSettings*> sourceSettingsForListModel; const auto currentSourceSettings = m_mapView->geometryEditor()->snapSettings()->sourceSettings(); std::for_each(std::begin(currentSourceSettings), std::end(currentSourceSettings), [&sourceSettingsForListModel](SnapSourceSettings* sourceSetting) { // Enable snapping for the graphics overlay. if (auto graphicsOverlay = dynamic_cast<GraphicsOverlay*>(sourceSetting->source()); graphicsOverlay) { sourceSetting->setEnabled(true); sourceSettingsForListModel.append(sourceSetting); } else if (auto subtypeFeatureLayer = dynamic_cast<SubtypeFeatureLayer*>(sourceSetting->source()); subtypeFeatureLayer && subtypeFeatureLayer->name() == "PipelineLine") { QList<SnapSourceSettings*> childSourceSettings = sourceSetting->childSourceSettings(); std::for_each(std::begin(childSourceSettings), std::end(childSourceSettings), [&sourceSettingsForListModel](SnapSourceSettings* sourceSetting) { if (auto* subtypeSublayer = dynamic_cast<SubtypeSublayer*>(sourceSetting->source()); subtypeSublayer && (subtypeSublayer->name() == "Service Pipe" || subtypeSublayer->name() == "Distribution Pipe")) { sourceSettingsForListModel.append(sourceSetting); } }); } });
m_snapSourcesListModel->setSnapSourceSettings(sourceSettingsForListModel); updateSnapSourceRenderers(sourceSettingsForListModel);
emit snapSourceModelChanged(); });}
void SnapGeometryEditsWithRules::updateSnapSourceRenderers(const QList<SnapSourceSettings*>& snapSourceSettings){ // Update the renderer for each snap source based on their snap rule behavior. std::for_each(std::begin(snapSourceSettings), std::end(snapSourceSettings), [this](const SnapSourceSettings* settings) { Symbol* symbol = nullptr; switch (settings->ruleBehavior()) { case SnapRuleBehavior::None: symbol = m_noneSymbol; break; case SnapRuleBehavior::RulesLimitSnapping: symbol = m_rulesLimitSymbol; break; case SnapRuleBehavior::RulesPreventSnapping: symbol = m_rulesPreventSymbol; break; }
if (auto graphicsOverlay = dynamic_cast<GraphicsOverlay*>(settings->source()); graphicsOverlay) { graphicsOverlay->setRenderer(std::make_unique<SimpleRenderer>(symbol).get()); } else if (auto subtypeSublayer = dynamic_cast<SubtypeSublayer*>(settings->source()); subtypeSublayer) { subtypeSublayer->setRenderer(std::make_unique<SimpleRenderer>(symbol).get()); } });}
void SnapGeometryEditsWithRules::resetSelections(){ if (m_selectedFeature && m_selectedFeature->featureTable() && m_selectedFeature->featureTable()->layer()) { // Clear the existing selection and show the selected feature; auto* featureLayer = static_cast<FeatureLayer*>(m_selectedFeature->featureTable()->layer()); featureLayer->clearSelection(); featureLayer->setFeatureVisible(m_selectedFeature, true); }
// Reset the selected feature. m_selectedFeature = nullptr;
// Revert back to the default renderer for the distribution and service pipe layers and graphics overlay. if (m_distributionPipeLayer) { m_distributionPipeLayer->setRenderer(m_defaultDistributionRenderer); } if (m_servicePipeLayer) { m_servicePipeLayer->setRenderer(m_defaultServiceRenderer); } if (m_mapView && m_mapView->graphicsOverlays() && m_mapView->graphicsOverlays()->at(0)) { m_mapView->graphicsOverlays()->at(0)->setRenderer(m_defaultGraphicsOverlayRenderer); }
// Clear the snap sources list. m_snapSourcesListModel->clear();
// Update the UI visibility. emit isElementSelectedChanged(); emit snapSourceModelChanged();}// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
#ifndef SNAPGEOMETRYEDITSWITHRULES_H#define SNAPGEOMETRYEDITSWITHRULES_H
// sample headers#include "SnapSourcesListModel.h"
// Qt headers#include <QImage>#include <QObject>
template <typename T> class QFuture;class QMouseEvent;
namespace Esri::ArcGISRuntime { class ArcGISFeature; class Geodatabase; class GeometryEditor; class IdentifyLayerResult; class Map; class MapQuickView; class Renderer; class SimpleLineSymbol; class SnapSourceSettings; class SubtypeFeatureLayer; class SubtypeSublayer; class UtilityAssetType;}
Q_MOC_INCLUDE("MapQuickView.h");
struct LoadOperationalLayersReturnStruct{ Esri::ArcGISRuntime::SubtypeFeatureLayer* m_pipeSubtypeLayer = nullptr; Esri::ArcGISRuntime::SubtypeFeatureLayer* m_deviceSubtypeLayer = nullptr;};
class SnapGeometryEditsWithRules : public QObject{ Q_OBJECT Q_PROPERTY(Esri::ArcGISRuntime::MapQuickView* mapView READ mapView WRITE setMapView NOTIFY mapViewChanged) Q_PROPERTY(bool geometryEditorStarted READ geometryEditorStarted NOTIFY geometryEditorStartedChanged) Q_PROPERTY(bool isElementSelected READ isElementSelected NOTIFY isElementSelectedChanged) Q_PROPERTY(SnapSourcesListModel* snapSourcesListModel READ snapSourcesListModel NOTIFY snapSourceModelChanged)
public: explicit SnapGeometryEditsWithRules(QObject* parent = nullptr); ~SnapGeometryEditsWithRules() override;
static void init(); Q_INVOKABLE void startEditor(); Q_INVOKABLE void stopEditing(); Q_INVOKABLE void discardEdits();
Esri::ArcGISRuntime::MapQuickView* mapView() const; void setMapView(Esri::ArcGISRuntime::MapQuickView* mapView);
bool isElementSelected() const; bool geometryEditorStarted() const; SnapSourcesListModel* snapSourcesListModel() const;
signals: void mapViewChanged(); void isElementSelectedChanged(); void geometryEditorStartedChanged(); void snapSourceModelChanged(); void assetGroupChanged(const QString& assetGroup); void assetTypeChanged(const QString& assetType);
private slots: void onMapViewClicked(const QMouseEvent& mouseEvent);
private: void initializeMap();
QFuture<void> loadGeodatabase(); QFuture<LoadOperationalLayersReturnStruct> loadOperationalLayers(); QFuture<void> loadUtilityNetwork();
void modifyOperationalLayersVisibility(Esri::ArcGISRuntime::SubtypeFeatureLayer* pipeLayer, Esri::ArcGISRuntime::SubtypeFeatureLayer* deviceLayer); void updateSnapSourceRenderers(const QList<Esri::ArcGISRuntime::SnapSourceSettings*>& snapSourceSettings); Esri::ArcGISRuntime::ArcGISFeature* getFeatureFromResult(const QList<Esri::ArcGISRuntime::IdentifyLayerResult*>& identifyResult); void setSnapSettings(Esri::ArcGISRuntime::UtilityAssetType* assetType); void resetSelections();
Esri::ArcGISRuntime::Map* m_map = nullptr; Esri::ArcGISRuntime::MapQuickView* m_mapView = nullptr;
Esri::ArcGISRuntime::Geodatabase* m_geodatabase = nullptr;
SnapSourcesListModel* m_snapSourcesListModel = nullptr;
Esri::ArcGISRuntime::Renderer* m_defaultDistributionRenderer = nullptr; Esri::ArcGISRuntime::Renderer* m_defaultServiceRenderer = nullptr; Esri::ArcGISRuntime::Renderer* m_defaultGraphicsOverlayRenderer = nullptr;
Esri::ArcGISRuntime::SubtypeSublayer* m_distributionPipeLayer = nullptr; Esri::ArcGISRuntime::SubtypeSublayer* m_servicePipeLayer = nullptr;
Esri::ArcGISRuntime::SimpleLineSymbol* m_rulesLimitSymbol = nullptr; Esri::ArcGISRuntime::SimpleLineSymbol* m_rulesPreventSymbol = nullptr; Esri::ArcGISRuntime::SimpleLineSymbol* m_noneSymbol = nullptr;
Esri::ArcGISRuntime::ArcGISFeature* m_selectedFeature = nullptr;
};
#endif // SNAPGEOMETRYEDITSWITHRULES_Himport QtQuickimport QtQuick.Controlsimport QtQuick.Layoutsimport Esri.Samples
Item { // MapView component MapView { id: mapView anchors.fill: parent
Component.onCompleted: { forceActiveFocus(); // Enable keyboard navigation } }
// Declare the C++ instance and bind the mapView SnapGeometryEditsWithRulesSample { id: snapGeometryEditsWithRulesModel mapView: mapView }
Rectangle { id: instructionsPanel visible: snapGeometryEditsWithRulesModel.isElementSelected ? false : true anchors { right: parent.right margins: 10 } width: textItem.width + 20 height: textItem.height + 10 color: "black" opacity: .5
Text { id: textItem anchors.centerIn: parent text: "Tap a point feature to edit." color: "white" font.pixelSize: 16 font.bold: true } }
Control { id: control visible: snapGeometryEditsWithRulesModel.isElementSelected ? true : false anchors.right: parent.right padding: 5
background: Rectangle { color: "black" opacity: .5 } contentItem: ColumnLayout { id: columns anchors { verticalCenter: parent.verticalCenter horizontalCenter: parent.horizontalCenter }
GridLayout { id: geometryColumn Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter columns: 2
Text { id: geometryHeader Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.columnSpan: 2 text: "Feature selected" color: "white" font.pixelSize: 16 font.bold: true }
Text { id: assetGroupText Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.columnSpan: 2 text: "AssetGroup: " color: "white" font.pixelSize: 12 }
Text { id: assetTypeText Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.columnSpan: 2 text: "AssetType: " color: "white" font.pixelSize: 12 }
Connections { target: snapGeometryEditsWithRulesModel function onAssetGroupChanged(assetGroup) { assetGroupText.text = "AssetGroup: " + assetGroup; } function onAssetTypeChanged(assetType) { assetTypeText.text = "AssetType: " + assetType; } } }
GridLayout { id: editingColumn Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter columns: 2
GeometryEditorButton { id: startButton buttonName: qsTr("Start editor") iconPath: "" Layout.columnSpan: 2 enabled: !snapGeometryEditsWithRulesModel.geometryEditorStarted onClicked: snapGeometryEditsWithRulesModel.startEditor(); } GeometryEditorButton { id: snapSettingsButton buttonName: qsTr("Snap Settings") iconPath: "qrc:/Samples/EditData/SnapGeometryEditsWithRules/iconAssets/settings.png" Layout.columnSpan: 2 enabled: snapGeometryEditsWithRulesModel.isElementSelected onClicked: { optionPanel.visible = !optionPanel.visible } } GeometryEditorButton { id: saveButton buttonName: qsTr("Save") iconPath: "qrc:/Samples/EditData/SnapGeometryEditsWithRules/iconAssets/save-32.png" enabled: snapGeometryEditsWithRulesModel.geometryEditorStarted onClicked: snapGeometryEditsWithRulesModel.stopEditing(); }
GeometryEditorButton { id: discardButton buttonName: qsTr("Discard") iconPath: "qrc:/Samples/EditData/SnapGeometryEditsWithRules/iconAssets/trash-32.png" enabled: snapGeometryEditsWithRulesModel.geometryEditorStarted onClicked: snapGeometryEditsWithRulesModel.discardEdits(); } }
GridLayout { id: snappingLegend Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter columns: 2
Rectangle { width: 10 height: 10 color: "green" radius: 2 } Text { text: "None" color: "white" font.pixelSize: 12 }
Rectangle { width: 10 height: 10 color: "orange" radius: 2 } Text { text: "RulesLimitSnapping" color: "white" font.pixelSize: 12 }
Rectangle { width: 10 height: 10 color: "red" radius: 2 } Text { text: "RulesPreventSnapping" color: "white" font.pixelSize: 12 } } } }
Rectangle { id: optionPanel anchors { left: parent.left top: parent.top bottom: parent.bottom } width: 220 visible: false color: "black" opacity: .5
Connections { target: snapGeometryEditsWithRulesModel function onIsElementSelectedChanged() { if (!snapGeometryEditsWithRulesModel.isElementSelected) { optionPanel.visible = false; } } }
ListView { id: snapSourceView anchors { fill: parent margins: 10 }
header: ColumnLayout { id: snappingColumn Layout.minimumWidth: optionPanel.width spacing: 0 RowLayout { Layout.minimumWidth: optionPanel.width Layout.minimumHeight: 35
Text { Layout.alignment: Qt.AlignLeft Layout.fillWidth: true Layout.minimumWidth: optionPanel.width * 0.75 text: "Snapping" font.pixelSize: 12 color: "white" font.bold: true }
Text { Layout.alignment: Qt.AlignRight Layout.fillWidth: true Layout.minimumWidth: optionPanel.width * 0.25 text: "Done" font.pixelSize: 12 color: "white" font.bold: true MouseArea { anchors.fill: parent onClicked: optionPanel.visible = false; } } } }
model: snapGeometryEditsWithRulesModel.snapSourcesListModel
delegate: Item { height: 35 width: optionPanel.width id:delegate Rectangle { id: wrapper color: "#E9DFEA" width: snapSourceView.width - (snapSourceView.anchors.margins / 2) height: delegate.height anchors { margins: 15 }
RowLayout { id : row Layout.fillWidth: true Layout.minimumWidth: optionPanel.width width: wrapper.width
Text { Layout.alignment: Qt.AlignLeft Layout.fillWidth: true Layout.minimumWidth: optionPanel.width / 2 text: name font.pixelSize: 12 Layout.leftMargin: 10 }
Switch { Layout.alignment: Qt.AlignRight Layout.fillWidth: true checked: isEnabled onCheckedChanged: { if (isEnabled !== checked) { isEnabled = checked; // Update only if there's a difference } } } } } } } }}// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
// sample headers#include "SnapSourcesListModel.h"
// ArcGIS Maps SDK headers#include "FeatureLayer.h"#include "GraphicsOverlay.h"#include "SnapSourceSettings.h"#include "SubtypeSublayer.h"
// Qt headers#include <QAbstractListModel>#include <QByteArray>#include <QDir>#include <QFileInfo>#include <QHash>#include <QList>#include <QModelIndex>#include <QObject>#include <QVariant>
// STL headers#include <utility>
using namespace Esri::ArcGISRuntime;
SnapSourcesListModel::SnapSourcesListModel(QObject* parent) : QAbstractListModel(parent){ setupRoles();}
void SnapSourcesListModel::setupRoles(){ m_roles[NameRole] = "name"; m_roles[IsEnabledRole] = "isEnabled";}
int SnapSourcesListModel::size() const{ return m_snapSourceSettings.size();}
void SnapSourcesListModel::clear(){ beginResetModel(); m_snapSourceSettings.clear(); endResetModel();}
int SnapSourcesListModel::rowCount(const QModelIndex& parent) const{ Q_UNUSED(parent); return m_snapSourceSettings.count();}
QVariant SnapSourcesListModel::data(const QModelIndex& index, int role) const{ if (!index.isValid() || index.row() < 0 || index.row() >= m_snapSourceSettings.count()) { return {}; }
auto* snapSourceSetting = m_snapSourceSettings.at(index.row()); if (role == NameRole) { return determineName(snapSourceSetting); } if (role == IsEnabledRole) { return snapSourceSetting->isEnabled(); } return {};}
bool SnapSourcesListModel::setData(const QModelIndex& index, const QVariant& value, int role){ if (!index.isValid() || index.row() < 0 || index.row() >= m_snapSourceSettings.count()) { return false; }
if (role == IsEnabledRole) { auto* snapSourceSettingsWrapper = m_snapSourceSettings.at(index.row()); if (snapSourceSettingsWrapper) { snapSourceSettingsWrapper->setEnabled(value.toBool()); emit dataChanged(index, index, { role }); return true; } } return false;}
QHash<int, QByteArray> SnapSourcesListModel::roleNames() const{ return m_roles;}
void SnapSourcesListModel::setSnapSourceSettings(QList<SnapSourceSettings*> snapSourceSettings){ beginResetModel(); m_snapSourceSettings.clear(); m_snapSourceSettings = std::move(snapSourceSettings); endResetModel();}
QList<SnapSourceSettings*> SnapSourcesListModel::snapSourceSettings() const{ return m_snapSourceSettings;}
QString SnapSourcesListModel::determineName(Esri::ArcGISRuntime::SnapSourceSettings* snapSourceSettings) const{ if (auto graphicsOverlay = dynamic_cast<Esri::ArcGISRuntime::GraphicsOverlay*>(snapSourceSettings->source()); graphicsOverlay) { QString overlayId = graphicsOverlay->overlayId(); return overlayId.isEmpty() ? "Default Graphics Overlay" : overlayId; } if (auto subtypeSublayer = dynamic_cast<Esri::ArcGISRuntime::SubtypeSublayer*>(snapSourceSettings->source()); subtypeSublayer) { return subtypeSublayer->name(); } return {};}// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.
#ifndef SNAPSOURCESLISTMODEL_H#define SNAPSOURCESLISTMODEL_H
// Qt headers#include <QAbstractListModel>#include <QByteArray>#include <QHash>#include <QList>
namespace Esri::ArcGISRuntime { class SnapSourceSettings;}
class SnapSourcesListModel : public QAbstractListModel{ Q_OBJECT
public: enum SnapSourceRoles { NameRole = Qt::UserRole + 1, IsEnabledRole };
explicit SnapSourcesListModel(QObject* parent = nullptr); ~SnapSourcesListModel() override = default;
public: void setSnapSourceSettings(QList<Esri::ArcGISRuntime::SnapSourceSettings*> snapSourceSettings); QList<Esri::ArcGISRuntime::SnapSourceSettings*> snapSourceSettings() const; void setupRoles(); int size() const; void clear();
// QAbstractItemModel interface int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
protected: QHash<int, QByteArray> roleNames() const override;
private: QHash<int, QByteArray> m_roles; QList<Esri::ArcGISRuntime::SnapSourceSettings*> m_snapSourceSettings; QString determineName(Esri::ArcGISRuntime::SnapSourceSettings* snapSourceSettings) const;};
#endif // SNAPSOURCESLISTMODEL_H// [Legal]// Copyright 2025 Esri.//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.// [Legal]
// sample headers#include "SnapGeometryEditsWithRules.h"
// ArcGIS Maps SDK headers#include "ArcGISRuntimeEnvironment.h"
// Qt headers#include <QCommandLineParser>#include <QDir>#include <QGuiApplication>#include <QQmlApplicationEngine>
// Platform specific headers#ifdef Q_OS_WIN#include <Windows.h>#endif
int main(int argc, char *argv[]){ Esri::ArcGISRuntime::ArcGISRuntimeEnvironment::setUseLegacyAuthentication(false); QGuiApplication app(argc, argv); app.setApplicationName(QString("SnapGeometryEditsWithRules"));
// Use of ArcGIS location services, such as basemap styles, geocoding, and routing services, // requires an access token. For more information see // https://links.esri.com/arcgis-runtime-security-auth.
// The following methods grant an access token:
// 1. User authentication: Grants a temporary access token associated with a user's ArcGIS account. // To generate a token, a user logs in to the app with an ArcGIS account that is part of an // organization in ArcGIS Online or ArcGIS Enterprise.
// 2. API key authentication: Get a long-lived access token that gives your application access to // ArcGIS location services. Go to the tutorial at https://links.esri.com/create-an-api-key. // Copy the API Key access token.
const QString accessToken = QString("");
if (accessToken.isEmpty()) { qWarning() << "Use of ArcGIS location services, such as the basemap styles service, requires" << "you to authenticate with an ArcGIS account or set the API Key property."; } else { Esri::ArcGISRuntime::ArcGISRuntimeEnvironment::setApiKey(accessToken); }
// Initialize the sample SnapGeometryEditsWithRules::init();
// Initialize application view QQmlApplicationEngine engine; // Add the import Path engine.addImportPath(QDir(QCoreApplication::applicationDirPath()).filePath("qml"));
#ifdef ARCGIS_RUNTIME_IMPORT_PATH_2 engine.addImportPath(ARCGIS_RUNTIME_IMPORT_PATH_2);#endif
// Set the source engine.load(QUrl("qrc:/Samples/EditData/SnapGeometryEditsWithRules/main.qml"));
return app.exec();}// Copyright 2025 Esri.
// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.
import QtQuick.Controlsimport Esri.Samples
ApplicationWindow { visible: true width: 800 height: 600
SnapGeometryEditsWithRules { anchors.fill: parent }}