ProGuide Editing tool - Esri/arcgis-pro-sdk GitHub Wiki
Language: C#
Subject: Editing
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 10/06/2024
ArcGIS Pro: 3.4
Visual Studio: 2022
This ProGuide shows how to build an editing tool to interact with and modify existing features. The code used to illustrate this add-in can be found at Sketch Tool Demo Sample.
Prerequisite
Create a new add-in, and add a map tool to the add-in called CutTool.cs
.
Step 1
In the constructor, change the SketchType
of the map tool to a line geometry type. This is the geometry of the map tool feedback used to symbolize the user interaction. For this example, you're using a single solid line that adds a linear segment on each mouse click.
// select the type of tool feedback you wish to implement.
SketchType = SketchGeometryType.Line;
// a sketch feedback is needed
IsSketchTool = true;
// the geometry is needed in map coordinates
SketchOutputMode = ArcGIS.Desktop.Mapping.SketchOutputMode.Map;
Step 2
Replace the code in the OnSketchCompleteAsync
method. Assign a method (ExecuteCut) to modify features in the editor, and, since the API calls need to happen on the CIM thread, use a lambda function to handle the operation using the sketch geometry. For now, simply declare the method. The code will be specified in step 4.
protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
return QueuedTask.Run(() => ExecuteCut(geometry));
}
protected Task<bool> ExecuteCut(Geometry geometry)
{
return Task.FromResult(true);
}
Step 3
Open the Config.daml
file and go to the entry for the tool. Add a categoryRefID
attribute setting it to categoryRefID="esri_editing_CommandList"
. This specifies the tool is to appear in the Modify Features pane. Also add a content
xml node to the tool specification. The attribute L_group
specifies the group that the tool will live under in the Modify Features pane. Use your own group name to create a new group entry in the pane. The tool daml should be as below
<tool id="ProAppModule1_CutTool" categoryRefID="esri_editing_CommandList"
caption="CutTool" className="CutTool" loadOnClick="true"
smallImage="Images\GenericButtonRed16.png"
largeImage="Images\GenericButtonRed32.png">
<tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
<content L_group="Pro SDK Samples" />
</tool>
The attributes gallery2d
and gallery3d
also live within the content tag and specify whether the tool is shown in the favorites group in the gallery for 2D or 3D editing tools on the Edit ribbon. In this guide, you'll add the tool to the favorites group in the 2D gallery so set the gallery2d
to true and the gallery3d
to false.
The modified xml configuration for the editing tool should look similar to the following definition:
<tool id="ProAppModule1_CutTool" categoryRefID="esri_editing_CommandList"
caption="CutTool" className="CutTool" loadOnClick="true"
smallImage="Images\GenericButtonRed16.png"
largeImage="Images\GenericButtonRed32.png">
<tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
<content L_group="Pro SDK Samples" gallery2d="true" gallery3d="false" />
</tool>
Step 4
Go back to the CutTool.cs
file and the ExecuteCut
method. We'll be using the sketch geometry to perform a cut against all editable polygon features in the active map. Add the following code to the method to first obtain the editable polygon layers.
if (geometry == null)
return Task.FromResult(false);
// create a collection of feature layers that can be edited
var editableLayers = ActiveMapView.Map.GetLayersAsFlattenedList()
.OfType<FeatureLayer>()
.Where(lyr => lyr.CanEditData() == true).Where(lyr =>
lyr.ShapeType == esriGeometryType.esriGeometryPolygon);
// ensure that there are target layers
if (editableLayers.Count() == 0)
return Task.FromResult(false);
Since an edit is going to be performed, create a new instance of an EditOperation
to scope the modifies. See EditOperation in the ProConcept documentation to learn about its functionality.
// create an edit operation
EditOperation cutOperation = new EditOperation()
{
Name = "Cut Elements",
ProgressMessage = "Working...",
CancelMessage = "Operation canceled.",
ErrorMessage = "Error cutting polygons",
SelectModifiedFeatures = false,
SelectNewFeatures = false
};
Step 5
For each identified editable polygon layer, do the following:
- Get the underlying feature class and determine the field index for the "Description" attribute field.
- Search for the features that are crossing the sketch geometry. Use a SpatialQueryFilter.
// initialize a list of ObjectIDs that need to be cut
var cutOIDs = new List<long>();
// for each of the layers
foreach (FeatureLayer editableFeatureLayer in editableLayers)
{
// get the feature class associated with the layer
var fc = editableFeatureLayer.GetTable();
// find the field index for the 'Description' attribute
int descriptionIndex = -1;
descriptionIndex = fc.GetDefinition().FindField("Description");
// find the features crossed by the sketch geometry
// use the featureClass to search. We need to be able to search with a recycling cursor
// seeing we want to Modify the row results
// define a spatial query filter
var spatialQueryFilter = new SpatialQueryFilter
{
// passing the search geometry to the spatial filter
FilterGeometry = geometry,
// define the spatial relationship between search geometry and feature class
SpatialRelationship = SpatialRelationship.Crosses
};
using (var rowCursor = fc.Search(spatialQueryFilter, false))
{
}
}
For each returned feature, test whether the feature is completely intersected by the sketch geometry by using the GeometryEngine.Instance.Relate
method. The string argument is detailed in the Dimensionally Extended Nine-Intersection Model. If the feature's geometry is completely intersected, store its ObjectID
in a list. Here's the code for the search results:
using (var feature = rowCursor.Current as Feature)
{
var geomTest = feature.GetShape();
if (geomTest != null)
{
// make sure we have the same projection for geomProjected and geomTest
var geomProjected = GeometryEngine.Instance.Project(geometry, geomTest.SpatialReference);
// we are looking for polygons are completely intersected by the cut line
if (GeometryEngine.Instance.Relate(geomProjected, geomTest, "TT*F*****"))
{
var oid = feature.GetObjectID();
// add the current feature to the overall list of features to cut
cutOIDs.Add(oid);
}
}
}
Step 6
Once we have the list of objectIDs for each polygon layer, modify the Description field for each record using the EditOperation.Modify
routine. And perform the Cut operation using the EditOperation.Split
method.
// adjust the attribute before the cut
if (descriptionIndex != -1)
{
var atts = new Dictionary<string, object>();
atts.Add("Description", "Pro Sample");
foreach (var oid in cutOIDs)
cutOperation.Modify(editableFeatureLayer, oid, atts);
}
// add the elements to cut into the edit operation
cutOperation.Split(editableFeatureLayer, cutOIDs, geometry);
Once the code enumerates all the layers, execute the edit operation. Regardless of the number of modified features across the polygon layers, there will be only one edit operation listed in the Undo/Redo stack for the active view.
//execute the operation
var operationResult = cutOperation.Execute();
return Task.FromResult(operationResult);
The complete ExecuteCut function looks like this.
protected Task<bool> ExecuteCut(Geometry geometry)
{
if (geometry == null)
return Task.FromResult(false);
// create a collection of feature layers that can be edited
var editableLayers = ActiveMapView.Map.GetLayersAsFlattenedList()
.OfType<FeatureLayer>()
.Where(lyr => lyr.CanEditData() == true).Where(lyr =>
lyr.ShapeType == esriGeometryType.esriGeometryPolygon);
// ensure that there are target layers
if (editableLayers.Count() == 0)
return Task.FromResult(false);
// create an edit operation
EditOperation cutOperation = new EditOperation()
{
Name = "Cut Elements",
ProgressMessage = "Working...",
CancelMessage = "Operation canceled.",
ErrorMessage = "Error cutting polygons",
SelectModifiedFeatures = false,
SelectNewFeatures = false
};
// initialize a list of ObjectIDs that need to be cut
var cutOIDs = new List<long>();
// for each of the layers
foreach (FeatureLayer editableFeatureLayer in editableLayers)
{
// get the feature class associated with the layer
var fc = editableFeatureLayer.GetTable();
// find the field index for the 'Description' attribute
int descriptionIndex = -1;
descriptionIndex = fc.GetDefinition().FindField("Description");
// find the features crossed by the sketch geometry
// use the featureClass to search. We need to be able to search with a recycling cursor
// seeing we want to Modify the row results
// define a spatial query filter
var spatialQueryFilter = new SpatialQueryFilter
{
// passing the search geometry to the spatial filter
FilterGeometry = geometry,
// define the spatial relationship between search geometry and feature class
SpatialRelationship = SpatialRelationship.Crosses
};
using (var rowCursor = fc.Search(spatialQueryFilter, false))
{
// add the feature IDs into our prepared list
while (rowCursor.MoveNext())
{
using (var feature = rowCursor.Current as Feature)
{
var geomTest = feature.GetShape();
if (geomTest != null)
{
// make sure we have the same projection for geomProjected and geomTest
var geomProjected = GeometryEngine.Instance.Project(geometry, geomTest.SpatialReference);
// we are looking for polygons are completely intersected by the cut line
if (GeometryEngine.Instance.Relate(geomProjected, geomTest, "TT*F*****"))
{
var oid = feature.GetObjectID();
// add the current feature to the overall list of features to cut
cutOIDs.Add(oid);
}
}
}
}
// adjust the attribute before the cut
if (descriptionIndex != -1)
{
var atts = new Dictionary<string, object>();
atts.Add("Description", "Pro Sample");
foreach (var oid in cutOIDs)
cutOperation.Modify(editableFeatureLayer, oid, atts);
}
// add the elements to cut into the edit operation
cutOperation.Split(editableFeatureLayer, cutOIDs, geometry);
}
}
// execute the operation
var operationResult = cutOperation.Execute();
return Task.FromResult(operationResult);
}
Step 7
To emphasize the interactive operation, you may want to change the sketch symbol. The appearance of the sketch can be changed by overriding the OnSketchModifiedAsync
method. In this example, you'll change the style, width, and color of the line sketch after the second vertex.
protected override async Task<bool> OnSketchModifiedAsync()
{
// retrieve the current sketch geometry
Polyline cutGeometry = await base.GetCurrentSketchAsync() as Polyline;
await QueuedTask.Run(() =>
{
// if there are more than 2 vertices in the geometry
if (cutGeometry.PointCount > 2)
{
// adjust the sketch symbol
var symbolReference = base.SketchSymbol;
if (symbolReference == null)
{
var cimLineSymbol = SymbolFactory.Instance.ConstructLineSymbol(ColorFactory.Instance.RedRGB,
3, SimpleLineStyle.DashDotDot);
base.SketchSymbol = cimLineSymbol.MakeSymbolReference();
}
else
{
symbolReference.Symbol.SetColor(ColorFactory.Instance.RedRGB);
base.SketchSymbol = symbolReference;
}
}
});
return true;
}
Step 8
For the last step, set up a custom action after the edit operation has successfully completed to show how many features have been modified. Switch to the Module1.cs
file and override the Initialize
method for the add-in. Subscribe to the EditCompletedEvent
and define a callback method called ReportNumberOfRowsChanged
.
protected override bool Initialize()
{
// subscribe to the completed edit operation event
EditCompletedEvent.Subscribe(ReportNumberOfRowsChanged);
return true;
}
The callback method finds the modifies that happened during the edit operation and reports the number of modifies back to the user.
/// <summary>
/// Method containing actions as the result of the EditCompleted (the operation) event.
/// This method reports the total number of changed rows/features.
/// </summary>
/// <param name="editArgs">Argument containing the layers where edits occurred and what
/// types of changes.</param>
/// <returns></returns>
private Task<bool> ReportNumberOfRowsChanged(EditCompletedEventArgs editArgs)
{
if (editArgs.CompletedType != EditCompletedType.Operation)
return Task.FromResult(true);
// get the dictionary containing the modifies on the current feature
// operation
var editChanges = editArgs.Modifies;
// use this variable to store the total number of modifies
int countOfModifies = editChanges.Count;
if (countOfModifies > 0)
MessageBox.Show($"{countOfModifies.ToString()} features changed");
else
MessageBox.Show("The current edit operation did not contain any row/feature
modification.");
return Task.FromResult(true);
}
Step 9
Compile the solution and fix any compile errors. Run ArcGIS Pro, open a project containing some polygon data. Open the Modify Features pane and verify that your tool exists in the named group. Verify your tool can also be found on the Edit Tools gallery. Activate the CutTool and sketch a line across one or more polygons. The features will be cut (split) and a message box should appear indicating the number of features modified.
For additional information on edit sketch tools consult ProConcepts Editing, sketch tools.