Hi. I am new to using Visual Studio 2022 for ArcGIS Pro SDK for .net and would like to create an add-in for labeling lines at a start and end point as well as points, all based on a particular attribute. This was an add-in that was used in ArcMap, and I'm not sure this is possible to create in Pro. I'm wondering if there are any existing add-in templates for this. I didn't see one in the community samples (Esri/arcgis-pro-sdk-community-samples: ArcGIS Pro SDK for Microsoft .NET Framework Community Samples), but I could be using the wrong key words. This tool basically creates label boxes with leader lines at each end point of a project (based on start and end point attributes), or on a point for a point layer, and is easily moveable. They had the option to choose classes from a drop down that had pre-determined expressions for varying attributes in the callout box. Any guidance on where to find this would be appreciated.
There are a lot of samples and snippits and it's a challenge tying it all together since no single sample usually accomplishes what you want. I'm by no means a seasoned veteran at this but here's the code I developed to label features in Pro the same way I was accustomed to in Arcmap. I disagree with much of Pro's forced workflow and labels in feature classes is one of them. It's overkill for adding 30 or 50 labels for a map but I digress..
This tool attempts to replicate the Arcmap label tool- label symbology is defined in each layer and the tool will obtain that based on what features were clicked on. I also tried to implement layer hierarchy so that the feature that gets labelled is the one the drew on top of the other features (point on a line, line on a polygon, etc).
Anyways, this won't do what you want right out of the box but hopefully this ties together some of the tasks you need to pull off your labelling and you can modify where you need to do things differently. The code is C# and should work using the v3.3 API.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using ArcGIS.Core.Arcade;
using ArcGIS.Core.CIM;
using ArcGIS.Core.Data;
using ArcGIS.Core.Data.DDL;
using ArcGIS.Core.Data.UtilityNetwork.Trace;
using ArcGIS.Core.Geometry;
using ArcGIS.Core.Internal.CIM;
using ArcGIS.Core.Internal.Geometry;
using ArcGIS.Desktop.Catalog;
using ArcGIS.Desktop.Core;
using ArcGIS.Desktop.Editing;
using ArcGIS.Desktop.Extensions;
using ArcGIS.Desktop.Framework;
using ArcGIS.Desktop.Framework.Contracts;
using ArcGIS.Desktop.Framework.Dialogs;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Internal.Mapping.CommonControls;
using ArcGIS.Desktop.Layouts;
using ArcGIS.Desktop.Mapping;
using Microsoft.VisualBasic;
namespace pwTools
{
internal class arcmapLabeller : MapTool
{
public arcmapLabeller()
{
IsSketchTool = true;
SketchType = SketchGeometryType.Point;
SketchOutputMode = SketchOutputMode.Map;
UseSnapping = true;
}
protected override Task OnToolActivateAsync(bool active)
{
QueuedTask.Run(() =>
{
var graphicsLayer = MapView.Active.Map.TargetGraphicsLayer;
if (graphicsLayer == null)
{
var graphicsLayerCreationParams = new GraphicsLayerCreationParams { Name = "Map Labels Layer", MapMemberPosition = MapMemberPosition.AutoArrange };
var theOutGraphicsLayer = LayerFactory.Instance.CreateLayer<ArcGIS.Desktop.Mapping.GraphicsLayer>(graphicsLayerCreationParams, MapView.Active.Map);
}
});
return base.OnToolActivateAsync(active);
}
protected override Task<bool> OnSketchCompleteAsync(ArcGIS.Core.Geometry.Geometry geometry)
{
var popupContent = QueuedTask.Run(async () =>
{
var popupContents = new List<PopupContent>();
var mapView = MapView.Active;
Dictionary<MapMember, List<long>> features = new Dictionary<MapMember, List<long>>();
await QueuedTask.Run(() =>
{
features = mapView.GetFeatures(geometry).ToDictionary();
});
var featCount = features.Count;
//The click event returns a list of features encountered but that list has an unexpected hierachy so the first value is not
//necessarily the "top" layer. The list of layers must be sorted in order to evaluate which layer is the top layer
Dictionary<int, string> theHitList = new Dictionary<int, string>();
await QueuedTask.Run(() =>
{
var counter = 0;
foreach (var feature in features)
{
theHitList.Add(counter, feature.Key.Name);
counter++;
}
});
//Based on the click hit, get a reference to the layer name, Map, and Object ID
var theLayerName = "";
await QueuedTask.Run(() =>
{
theLayerName = getTopLayer(theHitList, features.First().Key.Map);
});
var theMap = features.First().Key.Map;
//Loop through the layer list to find theLayerName and get it's ObjectID
var theOID = features.First().Value[0];
foreach (var item in features)
{
if (item.Key.ToString() == theLayerName)
{
theOID = item.Value[0];
}
}
//Now get references to the layer and its geometry type
var theLayer = theMap.FindLayers(theLayerName).First();
var theFLayer = theMap.FindLayers(theLayerName).First() as FeatureLayer;
var theFeatureGeometry = "";
var fieldDescriptions = theFLayer.GetFieldDescriptions();
//Determine the geometry type of the layer whose feature was clicked
ShapeDescription alternativeShapeDescription = new ShapeDescription(theFLayer.GetFeatureClass().GetDefinition());
if (alternativeShapeDescription.GeometryType == GeometryType.Polyline)
{
theFeatureGeometry = "Polyline";
} else if (alternativeShapeDescription.GeometryType == GeometryType.Polygon)
{
theFeatureGeometry = "Point";
} else
{
theFeatureGeometry = "Polygon";
}
//Now get symbol information that's been defined for the layer
var lyrDefn = theFLayer.GetDefinition() as CIMFeatureLayer;
var listLabelClasses = lyrDefn.LabelClasses.ToList();
var theLabelClass = listLabelClasses.FirstOrDefault();
var theTextSymbol = theLabelClass.TextSymbol;
ArcGIS.Core.Geometry.Geometry theFeatGeometry = null;
var theExpression = theLabelClass.Expression;
var theLabelField = theExpression.Split(".").ElementAt(1);
var theLabelText = "";
//Get the attribute value for the field that provides the label
theLabelText = await QueuedTask.Run(() =>
{
ArcGIS.Core.Data.QueryFilter queryFilter = new ArcGIS.Core.Data.QueryFilter { WhereClause = "OBJECTID = " + theOID };
Feature curFeature = null;
var curLabel = "";
using (RowCursor rowCursor = theFLayer.Search(queryFilter))
{
if (rowCursor != null)
{
while (rowCursor.MoveNext())
{
using (Row row = rowCursor.Current)
{
curFeature = row as Feature;
//curLabel = curFeature.GetOriginalValue(curFeature.FindField(theLabelField)).ToString();
//--------------------------------------------
// BEGIN 3.2 CODE BLOCK
// Source: https://community.esri.com/t5/arcgis-pro-sdk-questions/evaluate-python-label-expression/m-p/1399197
//--------------------------------------------
var arcade_expr = new CIMExpressionInfo()
{
Expression = theExpression,
ReturnType = ExpressionReturnType.Default
};
var variables = new List<KeyValuePair<string, object>>() { new KeyValuePair<string, object>("$feature", curFeature) };
var arcade = ArcadeScriptEngine.Instance.CreateEvaluator(arcade_expr, ArcadeProfile.Popups);
var result = arcade.Evaluate(variables).GetResult();
curLabel = result.ToString();
//-------------------------------------------
// END 3.2 CODE BLOCK
//-------------------------------------------
if (theFeatureGeometry == "Polyline")
{
theFeatGeometry = curFeature.GetShape(); //Needed if the feature clicked was a line. Otherwise the point geometry where the user clicked is used
//Using a line as the geometry for graphic text is problematic as lines with a general bearing of 180-359 degrees tend to produce a graphic text
//that appears upside down when labelled using this add-in tool. Because of this, we must examine the line's bearing and, if needed, flip the
//line's geometry so that the text that gets produced will appear properly...
var length2D = GeometryEngine.Instance.Length(theFeatGeometry);
var qStartPoint = GeometryEngine.Instance.QueryPoint(theFeatGeometry as Multipart, SegmentExtensionType.NoExtension, 0, AsRatioOrLength.AsLength);
var qEndPoint = GeometryEngine.Instance.QueryPoint(theFeatGeometry as Multipart, SegmentExtensionType.NoExtension, length2D, AsRatioOrLength.AsLength);
double Rad2Degrees = 180 / Math.PI;
var lineBuilder = new LineBuilder(qStartPoint, qEndPoint);
var lineSeg = lineBuilder.ToSegment();
var degrees = lineSeg.Angle * Rad2Degrees;
degrees = 90 - degrees;
if (degrees < 0)
degrees = degrees + 360;
if (180 < degrees && degrees < 360)
{
var theReverseGeometry = GeometryEngine.Instance.ReverseOrientation(theFeatGeometry as Multipart);
theFeatGeometry = theReverseGeometry;
}
else
{
theFeatGeometry = curFeature.GetShape();
}
}
else
{
theFeatGeometry = geometry;
}
}
}
}
}
return curLabel;
});
await QueuedTask.Run(() =>
{
var graphicsLayer = MapView.Active?.Map.TargetGraphicsLayer;
if (theLabelText.Length > 1)
{
var graphic = new CIMTextGraphic()
{
Symbol = theTextSymbol
};
graphic.Shape = theFeatGeometry;
graphic.Text = theLabelText;
graphicsLayer.AddElement(graphic);
}
else
{
MessageBox.Show("No label text exists for the feature clicked", "Arcmap Labeller");
}
});
//MessageBox.Show("Label Parameters:\nLayer Name: " + theLayerName + "\nGeometry Type: " + theFeatureGeometry + "\nLabelling Field Specified: " + theLabelField + "\nLabel Placed would be: " + theLabelText, "Arcmap Labeller Result");
});
var clickPoint = MouseCursorPosition.GetMouseCursorPosition();
return base.OnSketchCompleteAsync(geometry);
}
private static string getTopLayer(Dictionary<int,string> clickList, Map theMap)
{
//This function processes the supplied dictionary list of layers and sorts that list according to each layer's
//position in the Table of Contents. The first value in the list will be the top layer in the TOC
var layers = MapView.Active.Map.GetLayersAsFlattenedList(); // MapView.Active.Map.Layers.Where(layer => layer is FeatureLayer);
SortedDictionary<int, string> sortedLayerList = new SortedDictionary<int, string>();
foreach (var layer in clickList)
{
var curLayerName = layer.Value;
for (int i = 0; i < layers.Count; i++)
{
if (layers[i].Name == curLayerName)
{
sortedLayerList.Add(i, layers[i].Name);
break;
}
}
}
return sortedLayerList.First().Value;
}
}
}
Thank you for this reply! I am finally able to spend some time on this again and just tried the suggested code. I am getting an error when I pasted it into the module:
The error CS0103 says "The name MouseCursorPosition does not exist in the current context." It offers some suggested edits, but when I try to do a screen capture they disappear. Any idea what might be happening?
i was able to edit the code by replacing the above with:
var screenPoint = System.Windows.Input.Mouse.GetPosition(System.Windows.Application.Current.MainWindow);
var clickPoint = MapView.Active?.ClientToMap(screenPoint);
There are no more errors or warnings and it opens arcpro. I now just have to figure out how to try it out.
First, add the tool to the ribbon. I hate the context switching UI of Pro so I tried to make a ribbon group where I placed all my common tools in it and that's where I put my add-in tool.
Using my tool is pretty straightforward. First. set up the labelling properties for all the layers you want to manually label in your map & then point and click like you would have in Arcmap. It attempts to use hierarchy logic in labelling so labelling a point feature that's on top of a line & polygon would take precedence. Basically, highest point in the TOC > highest line in the TOC > highest polygon in the TOC.
The labels are placed into a graphic layer. If one is not found, it creates one. It's not perfect but it's close enough for how I work.
I did get this to work! Now I have to get it so that it will label the whole feature class as well as so the labels have callouts. A bit more customizing but this is a great start! Thank you again so much for your time.
Here is the code that is working:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ArcGIS.Core.Arcade;
using ArcGIS.Core.CIM;
using ArcGIS.Core.Data;
using ArcGIS.Core.Data.DDL;
using ArcGIS.Core.Geometry;
using ArcGIS.Core.Internal.Geometry;
using ArcGIS.Desktop.Framework.Dialogs;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Internal.Editing;
using ArcGIS.Desktop.Mapping;
namespace Labeler_from_ESRI_Community
{
internal class Labeler_Module : MapTool
{
public Labeler_Module()
{
IsSketchTool = true;
SketchType = SketchGeometryType.Point;
SketchOutputMode = SketchOutputMode.Map;
UseSnapping = true;
}
protected override Task OnToolActivateAsync(bool active)
{
QueuedTask.Run(() =>
{
var graphicsLayer = MapView.Active.Map.TargetGraphicsLayer;
if (graphicsLayer == null)
{
var graphicsLayerCreationParams = new GraphicsLayerCreationParams { Name = "Map Labels Layer", MapMemberPosition = MapMemberPosition.AutoArrange };
var theOutGraphicsLayer = LayerFactory.Instance.CreateLayer<ArcGIS.Desktop.Mapping.GraphicsLayer>(graphicsLayerCreationParams, MapView.Active.Map);
}
});
return base.OnToolActivateAsync(active);
}
protected override Task<bool> OnSketchCompleteAsync(ArcGIS.Core.Geometry.Geometry geometry)
{
var popupContent = QueuedTask.Run(async () =>
{
var popupContents = new List<PopupContent>();
var mapView = MapView.Active;
Dictionary<MapMember, List<long>> features = new Dictionary<MapMember, List<long>>();
await QueuedTask.Run(() =>
{
features = mapView.GetFeatures(geometry).ToDictionary();
});
var featCount = features.Count;
//The click event returns a list of features encountered but that list has an unexpected hierachy so the first value is not
//necessarily the "top" layer. The list of layers must be sorted in order to evaluate which layer is the top layer
Dictionary<int, string> theHitList = new Dictionary<int, string>();
await QueuedTask.Run(() =>
{
var counter = 0;
foreach (var feature in features)
{
theHitList.Add(counter, feature.Key.Name);
counter++;
}
});
//Based on the click hit, get a reference to the layer name, Map, and Object ID
var theLayerName = "";
await QueuedTask.Run(() =>
{
theLayerName = getTopLayer(theHitList, features.First().Key.Map);
});
var theMap = features.First().Key.Map;
//Loop through the layer list to find theLayerName and get it's ObjectID
var theOID = features.First().Value[0];
foreach (var item in features)
{
if (item.Key.ToString() == theLayerName)
{
theOID = item.Value[0];
}
}
//Now get references to the layer and its geometry type
var theLayer = theMap.FindLayers(theLayerName).First();
var theFLayer = theMap.FindLayers(theLayerName).First() as FeatureLayer;
var theFeatureGeometry = "";
var fieldDescriptions = theFLayer.GetFieldDescriptions();
//Determine the geometry type of the layer whose feature was clicked
ShapeDescription alternativeShapeDescription = new ShapeDescription(theFLayer.GetFeatureClass().GetDefinition());
if (alternativeShapeDescription.GeometryType == GeometryType.Polyline)
{
theFeatureGeometry = "Polyline";
}
else if (alternativeShapeDescription.GeometryType == GeometryType.Polygon)
{
theFeatureGeometry = "Point";
}
else
{
theFeatureGeometry = "Polygon";
}
//Now get symbol information that's been defined for the layer
var lyrDefn = theFLayer.GetDefinition() as CIMFeatureLayer;
var listLabelClasses = lyrDefn.LabelClasses.ToList();
var theLabelClass = listLabelClasses.FirstOrDefault();
var theTextSymbol = theLabelClass.TextSymbol;
ArcGIS.Core.Geometry.Geometry theFeatGeometry = null;
var theExpression = theLabelClass.Expression;
var theLabelField = theExpression.Split(".").ElementAt(1);
var theLabelText = "";
//Get the attribute value for the field that provides the label
theLabelText = await QueuedTask.Run(() =>
{
ArcGIS.Core.Data.QueryFilter queryFilter = new ArcGIS.Core.Data.QueryFilter { WhereClause = "OBJECTID = " + theOID };
Feature curFeature = null;
var curLabel = "";
using (RowCursor rowCursor = theFLayer.Search(queryFilter))
{
if (rowCursor != null)
{
while (rowCursor.MoveNext())
{
using (Row row = rowCursor.Current)
{
curFeature = row as Feature;
//curLabel = curFeature.GetOriginalValue(curFeature.FindField(theLabelField)).ToString();
//--------------------------------------------
// BEGIN 3.2 CODE BLOCK
// Source: https://community.esri.com/t5/arcgis-pro-sdk-questions/evaluate-python-label-expression/m-p/1399197
//--------------------------------------------
var arcade_expr = new CIMExpressionInfo()
{
Expression = theExpression,
ReturnType = ExpressionReturnType.Default
};
var variables = new List<KeyValuePair<string, object>>() { new KeyValuePair<string, object>("$feature", curFeature) };
var arcade = ArcadeScriptEngine.Instance.CreateEvaluator(arcade_expr, ArcadeProfile.Popups);
var result = arcade.Evaluate(variables).GetResult();
curLabel = result.ToString();
//-------------------------------------------
// END 3.2 CODE BLOCK
//-------------------------------------------
if (theFeatureGeometry == "Polyline")
{
theFeatGeometry = curFeature.GetShape(); //Needed if the feature clicked was a line. Otherwise the point geometry where the user clicked is used
//Using a line as the geometry for graphic text is problematic as lines with a general bearing of 180-359 degrees tend to produce a graphic text
//that appears upside down when labelled using this add-in tool. Because of this, we must examine the line's bearing and, if needed, flip the
//line's geometry so that the text that gets produced will appear properly...
var length2D = GeometryEngine.Instance.Length(theFeatGeometry);
var qStartPoint = GeometryEngine.Instance.QueryPoint(theFeatGeometry as Multipart, SegmentExtensionType.NoExtension, 0, AsRatioOrLength.AsLength);
var qEndPoint = GeometryEngine.Instance.QueryPoint(theFeatGeometry as Multipart, SegmentExtensionType.NoExtension, length2D, AsRatioOrLength.AsLength);
double Rad2Degrees = 180 / Math.PI;
var lineBuilder = new LineBuilder(qStartPoint, qEndPoint);
var lineSeg = lineBuilder.ToSegment();
var degrees = lineSeg.Angle * Rad2Degrees;
degrees = 90 - degrees;
if (degrees < 0)
degrees = degrees + 360;
if (180 < degrees && degrees < 360)
{
var theReverseGeometry = GeometryEngine.Instance.ReverseOrientation(theFeatGeometry as Multipart);
theFeatGeometry = theReverseGeometry;
}
else
{
theFeatGeometry = curFeature.GetShape();
}
}
else
{
theFeatGeometry = geometry;
}
}
}
}
}
return curLabel;
});
await QueuedTask.Run(() =>
{
var graphicsLayer = MapView.Active?.Map.TargetGraphicsLayer;
if (theLabelText.Length > 1)
{
var graphic = new CIMTextGraphic()
{
Symbol = theTextSymbol
};
graphic.Shape = theFeatGeometry;
graphic.Text = theLabelText;
graphicsLayer.AddElement(graphic);
}
else
{
MessageBox.Show("No label text exists for the feature clicked", "Arcmap Labeller");
}
});
//MessageBox.Show("Label Parameters:\nLayer Name: " + theLayerName + "\nGeometry Type: " + theFeatureGeometry + "\nLabelling Field Specified: " + theLabelField + "\nLabel Placed would be: " + theLabelText, "Arcmap Labeller Result");
});
var screenPoint = System.Windows.Input.Mouse.GetPosition(System.Windows.Application.Current.MainWindow);
var clickPoint = MapView.Active?.ClientToMap(screenPoint);
return base.OnSketchCompleteAsync(geometry);
}
private static string getTopLayer(Dictionary<int, string> clickList, Map theMap)
{
//This function processes the supplied dictionary list of layers and sorts that list according to each layer's
//position in the Table of Contents. The first value in the list will be the top layer in the TOC
var layers = MapView.Active.Map.GetLayersAsFlattenedList(); // MapView.Active.Map.Layers.Where(layer => layer is FeatureLayer);
SortedDictionary<int, string> sortedLayerList = new SortedDictionary<int, string>();
foreach (var layer in clickList)
{
var curLayerName = layer.Value;
for (int i = 0; i < layers.Count; i++)
{
if (layers[i].Name == curLayerName)
{
sortedLayerList.Add(i, layers[i].Name);
break;
}
}
}
return sortedLayerList.First().Value;
}
}
}