ProGuide Annotation Construction Tools - 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 demonstrates how to build tools for creating annotation features. The code used to illustrate this guide can be found at Anno Tools Sample.

The accompanying ProGuide Annotation Editing Tools demonstrates how to build tools for modifying annotation features.

Important please consult ProConcepts Editing Annotation which covers special considerations for authors of editing tools for annotation and ProConcepts Annotation for general reference information on Annotation.

Prerequisites

Step 1

Add a new ArcGIS Pro Add-ins | ArcGIS Pro Construction Tool item to the add-in project, and name the item AnnoSimpleConstructionTool. Modify the Config.daml file tool item as follows:

  • Change the caption to "Simple Anno Tool (Template)"
  • Change the tool heading to "Simple Anno Tool (Template)" and the ToolTip text to "Simple Anno Construction Tool."

Step 2

By default, construction tools created with the Visual Studio template are assigned to the esri_editing_construction_point category meaning point features will be created. You can see this in the Config.daml file by looking at the categoryRefID attribute.

  <tool id="AnnoTools_AnnoSimpleConstructionTool" caption="Simple Anno Tool (Template)"
        categoryRefID="esri_editing_construction_point" 
        className="AnnoSimpleConstructionTool" 
        loadOnClick="true" 
        smallImage="Images\GenericButtonRed16.png" 
        largeImage="Images\GenericButtonRed32.png">
     <tooltip heading="Simple Anno Tool (Template)">Simple Anno Construction Tool
        <disabledText /></tooltip>
  </tool>

Modify this attribute to esri_editing_construction_annotation to signify that this tool will create annotation features.

Step 3

Open the AnnoSimpleConstructionTool.cs file and review the code.

public AnnoSimpleConstructionTool()
{
  IsSketchTool = true;
  UseSnapping = true;
  // set the sketch type to point
  SketchType = SketchGeometryType.Point;
  //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
  UsesCurrentTemplate = true;
  //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
  //Default value is false.
  FireSketchEvents = true;
}

/// <summary>
/// Called when the sketch finishes. This is where we will create the edit  operation and 
/// then execute it.</summary>
/// <param name="geometry">The geometry created by the sketch.</param>
/// <returns>A Task returning a Boolean indicating if the sketch complete event was 
/// successfully handled.</returns>
protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  if (CurrentTemplate == null || geometry == null)
    return Task.FromResult(false); 

  // Create an edit operation
  var createOperation = new EditOperation();
  createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
  createOperation.SelectNewFeatures = true;

  // pass the point geometry to the Create method
  createOperation.Create(CurrentTemplate, geometry);

  // Execute the operation
  return createOperation.ExecuteAsync();
}

This is the standard code generated by the Visual Studio template for any construction tool regardless of feature class type. The constructor sets up the construction tool to have a SketchType of Point. The OnSketchCompleteAsync method creates a feature with the attribute values defined by the CurrentTemplate and the geometry returned from the sketch, wrapped in an EditOperation. Annotation construction tools differ from construction tools for other feature class types in only one way; the geometry passed to the EditOperation.Create method is the geometry of the CIMTextGraphic, rather than the geometry of the feature.

In this construction tool, we will continue to use a SketchType of Point and pass a point geometry to the Create method, thus our annotation features will have a CIMTextGraphic shape of type point. (Remember that the annotation feature itself has a shape of type polygon.)

Step 4

Build the sample. Debug the add-in and start ArcGIS Pro. Open the SampleAnno.aprx project. Validate the UI by activating an annotation template.

simpleAnnoTool

Select the new construction tool and create some annotation features by digitizing points. Verify that the annotation text is orientated in a horizontal manner. Also note that the feature is created with the default text string of 'Text' and other attributes of the template including SymbolID.

Consider opening the template properties (right click on the template in the Create Features dock pane and choose Properties) and modifying some of the text attributes under the Annotation tab such as horizontal alignment, font size, font color or text string. Modifying individual text attributes allows you to customize the template symbol independently of modifying the symbol at the feature class definition level. There is only 1 symbol defined for the annotation feature class in this project so do not change the SymbolID of the template. Create some more features and verify that the symbol of your new annotation features match the text attributes of the template.

When finished, close ArcGIS Pro and return to your Visual Studio solution.

Step 5

In Visual Studio, add another ArcGIS Pro Add-ins | ArcGIS Pro Construction Tool item to the add-in project and call it AnnoAdvancedConstructionTool. We will modify the default implementation of this tool to create annotation features using a two point line and custom annotation text attributes.

Update the Config.daml file so that it looks like below (changing the categoryRefID and tooltip attributes).

   <tool id="AnnoTools_AnnoAdvancedConstructionTool" caption="Advanced Anno Tool" 
         categoryRefID="esri_editing_construction_annotation" 
         className="AnnoAdvancedConstructionTool" 
         loadOnClick="true" 
         smallImage="Images\GenericButtonRed16.png" 
         largeImage="Images\GenericButtonRed32.png">
     <tooltip heading="Advanced Anno Tool">Advanced Anno Construction Tool
       <disabledText /></tooltip>
   </tool>

Step 6

Open the AnnoAdvancedConstructionTool.cs file. In the constructor, change the SketchType of the construction tool to be of type Line.

    public AnnoAdvancedConstructionTool()
    {
      IsSketchTool = true;
      UseSnapping = true;
      // set the sketch type to line
      SketchType = SketchGeometryType.Line;
      UsesCurrentTemplate = true;
      FireSketchEvents = true;
    }

Next, override the OnSketchModifiedAsync method of the tool to force the sketch to finish when two points have been digitized. This is achieved by retrieving the current sketch geometry using the MapTool GetCurrentSketchAsync method, casting the geometry to a Polyline and interrogating the number of points. If there are 2 points digitized, call the MapTools FinishSketchAsync method.

Add the following code to the AnnoAdvancedConstructionTool class.

protected override async Task<bool> OnSketchModifiedAsync()
{
   // restrict the sketch to a 2 point line
   bool finish = await QueuedTask.Run(async () =>
   {
     // get the current sketch
     var geometry = await base.GetCurrentSketchAsync();
     // cast to a polyline
     var geom = geometry as ArcGIS.Core.Geometry.Polyline;
     // check the point count
     return geom?.PointCount >= 2;
   });

   // call FinishSketchAsync if we have 2 points
   if (finish)
     finish = await base.FinishSketchAsync();

   return finish;
}

Build the sample. Debug the add-in and start ArcGIS Pro. Open the SampleAnno.aprx project. Activate an annotation template and ensure that the additional construction tool exists. Activate the Advanced Construction tool and verify that the sketch completes after digitizing two points.

AdvancedAnnoTool

Step 7

The final step is to modify the OnSketchCompleteAsync method. Rather than using the default implementation, use the EditOperation.Create method which uses an Inspector object. In this example we will change 4 text attribute values; TextString, Color, VerticalAlignment and Underline in addition to setting the AnnotationClassID, SymbolID and Shape.

In order to supply AnnotationClassID and SymbolID values, the valid labels and symbols for the annotation feature class need to be determined. The label and symbol collections can be retrieved by accessing the CIM definition of the annotation feature class. These routines need to run on the MCT, so wrap in a QueuedTask.Run. The following code illustrates this

   bool result = await QueuedTask.Run(() =>
   {
     // get the anno layer
     AnnotationLayer annoLayer = CurrentTemplate.Layer as AnnotationLayer;
     if (annoLayer == null)
       return false;

     // get the anno feature class
     var fc = annoLayer.GetFeatureClass() as 
                       ArcGIS.Core.Data.Mapping.AnnotationFeatureClass;
     if (fc == null)
       return false;

     // get the featureclass CIM definition which contains the labels, symbols
     var cimDefinition = fc.GetDefinition() as 
                        ArcGIS.Core.Data.Mapping.AnnotationFeatureClassDefinition;
     var labels = cimDefinition.GetLabelClassCollection();
     var symbols = cimDefinition.GetSymbolCollection();

     // make sure there are labels, symbols
     if ((labels.Count == 0) || (symbols.Count == 0))
       return false;
   });

Find the label class required. Typically this should match a particular subtype name or some other characteristic. Each label class has an associated symbol. This example attempts to find the label class that matches the current template name. Note this is not a very robust solution as user generated templates or templates which have been renamed will cause this to default to the first label class in the collection which may not be the expected result.

   bool result = await QueuedTask.Run(() =>
   {
       ....

     // find the label class required
     //   typically you would use a subtype name or some other characteristic

     // use the first label class
     var label = labels[0];
     if (labels.Count > 1)
     {
       // find a label class based on template name 
       foreach (var LabelClass in labels)
       {
         if (LabelClass.Name == CurrentTemplate.Name)
         {
           label = LabelClass;
           break;
         }
       }
     }

     // each label has a textSymbol
     // the symbolName *should* be the symbolID to be used
     var symbolName = label.TextSymbol.SymbolName;
     int symbolID = -1;
     if (!int.TryParse(symbolName, out symbolID))
     {
       // int.TryParse fails - attempt to find the symbolName in the symbol collection
       foreach (var symbol in symbols)
       {
         if (symbol.Name == symbolName)
         {
           symbolID = symbol.ID;
           break;
         }
       }
     }
     // no symbol?
     if (symbolID == -1)
       return false;

   });

Using the inspector object obtained from the CurrentTemplate, assign the AnnotationClassID and SymbolID fields with the appropriate values. This code should also be within the QueuedTask.Run.

   // use the template's inspector object
   var inspector = CurrentTemplate.Inspector;

   // AnnotationClassID, SymbolID and Shape are the bare minimum for an annotation 
   // feature

   // use the inspector[fieldName] to set the annotationClassid
   // this is allowed since annotationClassID is a guaranteed field in the 
   // annotation schema
   inspector["AnnotationClassID"] = label.ID;
   // set the symbolID too
   inspector["SymbolID"] = symbolID;

In ArcGIS Pro, the only fields guaranteed to exist in an annotation schema are AnnotationClassID, SymbolID, Element, FeatureID or FeatureGlobalID (if using a GlobalID relationship), ZOrder and Status along with the system ObjectID and Shape fields. All other fields which store text formatting attributes (such as TextString, FontName, VerticalAlignment, HorizontalAlignment etc) are optional. They are not guaranteed to exist (in the physical schema).

For this reason, the AnnotationProperties class was introduced at ArcGIS Pro 2.2 which provides access to the text attributes of an annotation feature. Obtain the AnnotationProperties class using the Inspector.GetAnnotationProperties method. Set the individual text attributes and CIMTextGraphic shape as appropriate and then use the Inspector.SetAnnotationProperties method to assign the values back on to the Inspector object.

   // get the annotation properties
   var annoProperties = inspector.GetAnnotationProperties();
                                               
   // use the annotation properties to set the other attributes
   annoProperties.TextString = "My annotation feature";
   annoProperties.Color = ColorFactory.Instance.GreenRGB;
   annoProperties.VerticalAlignment = ArcGIS.Core.CIM.VerticalAlignment.Top;
   annoProperties.Underline = true;

   // set the geometry to be the sketched line
   // when creating annotation features the shape to be passed 
   //    in the create operation is the CIMTextGraphic shape
   annoProperties.Shape = geometry;

   // set the annotation properties back on the inspector
   inspector.SetAnnotationProperties(annoProperties);

Finally create the EditOperation class, and execute the feature creation.

   // Create an edit operation
   var createOperation = new EditOperation();
   createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
   createOperation.SelectNewFeatures = true;

   // create and execute using the inspector
   createOperation.Create(CurrentTemplate.Layer, inspector);
   return createOperation.Execute();

So the complete OnSketchCompleteAsync method looks like below.

/// <summary>
/// Called when the sketch finishes. This is where we will create the edit operation and 
/// then execute it.</summary>
/// <param name="geometry">The geometry created by the sketch.</param>
/// <returns>A Task returning a Boolean indicating if the sketch complete event was 
/// successfully handled.</returns>
protected override async Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  if (CurrentTemplate == null || geometry == null)
    return false;

  bool result = await QueuedTask.Run(() =>
  {
    // get the anno layer
    AnnotationLayer annoLayer = CurrentTemplate.Layer as AnnotationLayer;
    if (annoLayer == null)
      return false;

    // get the anno feature class
    var fc = annoLayer.GetFeatureClass() 
                as ArcGIS.Core.Data.Mapping.AnnotationFeatureClass;
    if (fc == null)
      return false;

    // get the featureclass CIM definition which contains the labels, symbols
    var cimDefinition = fc.GetDefinition() as 
                  ArcGIS.Core.Data.Mapping.AnnotationFeatureClassDefinition;
    var labels = cimDefinition.GetLabelClassCollection();
    var symbols = cimDefinition.GetSymbolCollection();

   // make sure there are labels, symbols
   if ((labels.Count == 0) || (symbols.Count == 0))
     return false;

   // find the label class required
   //   typically you would use a subtype name or some other characteristic

   // use the first label class
   var label = labels[0];
   if (labels.Count > 1)
   {
     // find a label class based on template name 
     foreach (var LabelClass in labels)
     {
       if (LabelClass.Name == CurrentTemplate.Name)
       {
         label = LabelClass;
         break;
       }
     }
   }

   // each label has a textSymbol
   // the symbolName *should* be the symbolID to be used
   var symbolName = label.TextSymbol.SymbolName;
   int symbolID = -1;
   if (!int.TryParse(symbolName, out symbolID))
   {
     // int.TryParse fails - attempt to find the symbolName in the symbol collection
     foreach (var symbol in symbols)
     {
       if (symbol.Name == symbolName)
       {
         symbolID = symbol.ID;
         break;
       }
     }
   }
   // no symbol?
   if (symbolID == -1)
     return false;

   // use the template's inspector object
   var inspector = CurrentTemplate.Inspector;

   // AnnotationClassID, SymbolID and Shape are the bare minimum for an 
   // annotation feature

   // use the inspector[fieldName] to set the annotationClassid
   //   this is allowed since annotationClassID is a guaranteed field in the 
   //   annotation schema
   inspector["AnnotationClassID"] = label.ID;
   // set the symbolID too
   inspector["SymbolID"] = symbolID;

   // get the annotation properties
   var annoProperties = inspector.GetAnnotationProperties();

   // use the annotation properties to set the other attributes
   annoProperties.TextString = "My annotation feature";
   annoProperties.Color = ColorFactory.Instance.GreenRGB;
   annoProperties.VerticalAlignment = ArcGIS.Core.CIM.VerticalAlignment.Top;
   annoProperties.Underline = true;

   // set the geometry to be the sketched line
   // when creating annotation features the shape to be passed 
   //    in the create operation is the CIMTextGraphic shape
   annoProperties.Shape = geometry;

   // set the annotation properties back on the inspector
   inspector.SetAnnotationProperties(annoProperties);

   // Create an edit operation
   var createOperation = new EditOperation();
   createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
   createOperation.SelectNewFeatures = true;

   // create and execute using the inspector
   createOperation.Create(CurrentTemplate.Layer, inspector);
   return createOperation.Execute();
  });

  return result;
}

Build the sample. Debug the add-in and start ArcGIS Pro. Open the SampleAnno.aprx project. Use the second custom construction tool to create some new annotation features. Note the text, color, vertical alignment and underlining of the created annotation text. Note also that the orientation of your digitized line changes the angle of the annotation text of the created feature.

Close ArcGIS Pro and return to Visual Studio.

Continue to the ProGuide Annotation Editing Tools for a walkthrough and examples of building editing tools for annotation features.

⚠️ **GitHub.com Fallback** ⚠️