Skip to content

ProGuide Annotation Editing Tools

arcgisprosdk edited this page Jan 16, 2018 · 20 revisions
Language:      C# and Visual Basic
Subject:       Editing
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          12/27/2017
ArcGIS Pro:    2.1
Visual Studio: 2015, 2017

This ProGuide demonstrates how to build tools for modifying annotation features. Two tools will be discussed; the first map tool illustrates how to modify the baseline geometry of an annotation feature. (The baseline geometry of an annotation feature is the geometry which the annotation text sits on). The second map tool illustrates how to modify text attributes of an annotation feature.

The code used in this ProGuide can be found at Anno Tools Sample.

The accompanying ProGuide Annotation Construction Tools demonstrates how to build tools for creating annotation features.

Prerequisites

Step 1

This map tool demonstrates how to modify the baseline geometry of an annotation feature. Remember that the shape of an annotation feature is always a polygon; the bounding box of the annotation text and is maintained by the application software. Instead, it is the baseline geometry of the annotation text that is modified by developers. At ArcGIS Pro 2.1 the only way to access this baseline geometry is via the AnnotationFeature and it's CIMTextGraphic.

The EditOperation.Callback method is the principal pattern for updating annotation features. However, because this tool is only updating the geometry of the CIMTextGraphic it is possible to use the EditOperation.Modify method. The second map tool later in this guide will illustrate how to use the EditOperation.Callback pattern for updating the text attributes of an annotation feature.

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

  • Change the caption to "Modify Geometry"
  • Change the tool heading to "Modify Anno Geometry" and the ToolTip text to "Click and drag over annotation features to modify their geometry."
  <tool id="AnnoTools_AnnoModifyGeometry" caption="Modify Geometry" 
        className="AnnoModifyGeometry" 
        loadOnClick="true" condition="esri_mapping_mapPane"
        smallImage="Images\GenericButtonRed16.png" largeImage="Images\GenericButtonRed32.png" >
      <tooltip heading="Modify Anno Geometry">Click and drag over annotation features to modify their geometry.<disabledText /></tooltip>
  </tool>

Step 2

Open the AnnoModifyGeometry.cs file and review the code. The constructor sets up the map tool to sketch a rectangle on the map. There are also two methods stubbed out by default; OnToolActivateAsync and OnSketchCompleteAsync. We will be modifying the OnSketchCompleteAsync method to retrieve the annotation features that intersect the sketch geometry, obtain their baseline geometry, rotate the geometry 90 degrees and modify the features with the rotated geometry.

First, retrieve the annotation features intersecting the sketch using the MapView.GetFeatures method. Refine the features returned to be only features from annotation layers using the AnnotationLayer class. Iterate through the remaining features. The following code accomplishes this.

  // execute on the MCT
  return QueuedTask.Run(() =>
  {

    // find features under the sketch 
    var features = MapView.Active.GetFeatures(geometry);
    if (features.Count == 0)
      return false;

    EditOperation op = null;

    // for each layer in the features retrieved
    foreach (var layer in features.Keys)
    {
      // is it an anno layer?
      if (!(layer is AnnotationLayer))
        continue;

      // are there features?
      var featOids = features[layer];
      if (featOids.Count == 0)
        continue;

      foreach (var oid in featOids)
      {
        ...
      }
    }
  });

Step 3

Now the results have been restricted to annotation layers, the individual AnnotationFeature objects and their CIMTextGraphic need to be obtained in order to retrieve the text baseline geometry. Achieve this using the BasicFeatureLayer.Search using the objectID of the feature; this method uses a recycling cursor. It is acceptable to use this method in this situation because we are only retrieving the geometry rather than updating it directly. For each cursor result from the search, cast it to an ArcGIS.Core.Data.Mapping.AnnotationFeature object. Use the GetGraphic method to obtain the CIMTextGraphic object of the annotation feature, followed by the Shape property to retrieve the CIMTextGraphic geometry. Review the following code which achieves this. Copy it to the interior of the for loop.

       Geometry textGraphicGeometry = null;

       // query for each feature
       QueryFilter qf = new QueryFilter();
       qf.WhereClause = "OBJECTID = " + oid.ToString();

       // use BasicFeatureLayer.Search 
       using (var rowCursor = layer.Search(qf))
       {
         rowCursor.MoveNext();
         if (rowCursor.Current != null)
         {
           // cast to AnnotationFeature
           ArcGIS.Core.Data.Mapping.AnnotationFeature annoFeature = rowCursor.Current as ArcGIS.Core.Data.Mapping.AnnotationFeature;
           if (annoFeature != null)
           {                  
             // get the graphic
             var graphic = annoFeature.GetGraphic();
             // cast to a CIMTextGraphic
             var textGraphic = graphic as CIMTextGraphic;
             // get the shape
             textGraphicGeometry = textGraphic.Shape;
           }
         }
       }

Step 4

If the geometry retrieved is a polyline, rotate it 90 degrees around it's centroid using the GeometryEngine.Instance.Centroid and GeometryEngine.Instance.Rotate methods.

       // if cimTextGraphic geometry is not a polyline, ignore
       Polyline baseLine = textGraphicGeometry as Polyline;
       if (baseLine = null)
         continue;

       // rotate the baseline 90 degrees
       var origin = GeometryEngine.Instance.Centroid(baseLine);
       Geometry rotatedBaseline = GeometryEngine.Instance.Rotate(baseLine, origin, System.Math.PI / 2);

Step 5

Next create the edit operation and call the Modify method. In the same way that calling Modify for a normal point, line or polygon feature updates it's shape, calling Modify for an annotation feature and passing the rotated geometry will cause the CIMTextGraphic geometry of the feature to be updated.

       // create the edit operation
       if (op == null)
       {
         op = new EditOperation();
         op.Name = "Update annotation baseline";
         op.SelectModifiedFeatures = true;
         op.SelectNewFeatures = false;
       }

       op.Modify(layer, oid, rotatedBaseline);

As an alternative the EditOperation.Modify method which uses a Dictionary of values could be used.

       Dictionary<string, object> newAtts = new Dictionary<string, object>();
       newAtts.Add("SHAPE", rotatedBaseline);
       op.Modify(layer, oid, newAtts);

Do not use the EditOperation.Modify method which uses the inspector object. Accessing or updating the Shape of the Inspector object will always reference the annotation polygon geometry.

Finally after iterating through all the features returned from MapView.Active.GetFeatures execute the edit operation.

   // execute the operation
   if ((op != null) && !op.IsEmpty)
     return op.Execute();
   return
     false;

The entire OnSketchCompleteAsync method should look like the following

protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  // execute on the MCT
  return QueuedTask.Run(() =>
  {
    // find features under the sketch 
    var features = MapView.Active.GetFeatures(geometry);
    if (features.Count == 0)
      return false;

    EditOperation op = null;

    // for each layer in the features retrieved
    foreach (var layer in features.Keys)
    {
      // is it an anno layer?
      if (!(layer is AnnotationLayer))
        continue;

      // are there features?
      var featOids = features[layer];
      if (featOids.Count == 0)
        continue;

      foreach (var oid in featOids)
      {
        // Remember - the shape of an annotation feature is a polygon; the 
        //     bounding box of the annotation text. We need to update the 
        //     CIMTextGraphic geometry.  Use the GetGraphic method from the 
        //     AnnotationFeature to obtain the CIMTextGraphic. 

        Geometry textGraphicGeometry = null;

        // query for each feature
        QueryFilter qf = new QueryFilter();
        qf.WhereClause = "OBJECTID = " + oid.ToString();
        using (var rowCursor = layer.Search(qf))
        {
          rowCursor.MoveNext();
          if (rowCursor.Current != null)
          {
            ArcGIS.Core.Data.Mapping.AnnotationFeature annoFeature = rowCursor.Current as ArcGIS.Core.Data.Mapping.AnnotationFeature;
            if (annoFeature != null)
            {                  
              var graphic = annoFeature.GetGraphic();
              // cast to a CIMTextGraphic
              var textGraphic = graphic as CIMTextGraphic;
              // get the shape
              textGraphicGeometry = textGraphic.Shape;
            }
          }
        }

        // if cimTextGraphic geometry is not a polyline, ignore
        Polyline baseLine = textGraphicGeometry as Polyline;
        if (baseLine == null)
           continue;

        // rotate the baseline 90 degrees
        var origin = GeometryEngine.Instance.Centroid(baseLine);
        Geometry rotatedBaseline = GeometryEngine.Instance.Rotate(baseLine, origin, System.Math.PI / 2);

        // create the edit operation
        if (op == null)
        {
          op = new EditOperation();
          op.Name = "Update annotation baseline";
          op.SelectModifiedFeatures = true;
          op.SelectNewFeatures = false;
        }

        op.Modify(layer, oid, rotatedBaseline);

        // OR 
        // use the Dictionary methodology

        //Dictionary<string, object> newAtts = new Dictionary<string, object>();
        //newAtts.Add("SHAPE", rotatedBaseline);
        //op.Modify(layer, oid, newAtts);

        // OR
        // use the pattern in AnnoModifySymbol (EditOperation.Callback)

      }
    }

    // execute the operation
    if ((op != null) && !op.IsEmpty)
      return op.Execute();
    return
      false;

  });
}

Step 6

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the SampleAnno.aprx project. Validate the UI by activating the Add-In tab.

annoModifyGeometry

Activate the tool and drag a rectangle on the map around one or more of the annotation features. Verify that the annotation text is rotated 90 degrees.

In Visual Studio, add a breakpoint in the OnSketchCompleteAsync method after the CIMTextGraphic is retrieved.

    var textGraphic = graphic as CIMTextGraphic;
    // get the shape
    textGraphicGeometry = textGraphic.Shape;       <-- add breakpoint here

Run the tool again and investigate the xml of the textGraphic object using the textGraphic.ToXml() method in a watch variable. Note the numerous properties available to alter in the CIM object. We will explore this further in the second map tool of this guide.

Stop debugging and return to Visual Studio.

Step 7

The second map tool in this ProGuide will demonstrate how to update text attributes of an annotation feature. The recommended pattern to modify text attributes is to use the EditOperation.Callback method to update the CIMTextGraphic object of the annotation feature. Because the CIMTextGraphic object is being updated directly, it also requires a non-recycling cursor result from a search. This is achieved using the Table.Search method.

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

  • Change the caption to "Modify Symbol"
  • Change the tool heading to "Modify Anno Symbol" and the ToolTip text to "Click and drag over annotation features to modify their text and symbol."
  <tool id="AnnoTools_AnnoModifySymbol" caption="Modify Symbol" 
        className="AnnoModifySymbol" 
        loadOnClick="true" condition="esri_mapping_mapPane"
        smallImage="Images\GenericButtonRed16.png" largeImage="Images\GenericButtonRed32.png" >
      <tooltip heading="Modify Anno Symbol">Click and drag over annotation features to modify their text and symbol.<disabledText /></tooltip>
  </tool>

Compile the add-in. Debug and start ArcGIS Pro. Open the SampleAnno.aprx project. Validate the UI by activating the Add-In tab. You should now see two tools on the ribbon.

annoModifySymbol

Close ArcGIS Pro and return to Visual Studio.

Step 8

Open the AnnoModifySymbol.cs file in Visual Studio. Examine the code. As per the previous map tool all updates will be made in the OnSketchCompleteAsync method. Replace the contents of the method with the following code.

  // execute on the MCT
  return QueuedTask.Run(() =>
  {
    // find features under the sketch 
    var features = MapView.Active.GetFeatures(geometry);
    if (features.Count == 0)
      return false;

    EditOperation op = null;
    foreach (var annoLayer in features.Keys)
    {
      // is it an anno layer?
      if (!(annoLayer is AnnotationLayer))
        continue;

      // are there features?
      var featOids = features[annoLayer];
      if (featOids.Count == 0)
        continue;

      foreach (var oid in featOids)
      {
         .....
      }
    }

    // execute the operation
    if ((op != null) && !op.IsEmpty)
      return op.Execute();
    return
      false;
  });

This should look familiar to you from the previous map tool as we are doing exactly the same thing; finding the annotation features under the sketch. The contents of the for loop will be a little different as we will be using the EditOperation.Callback method. Add the following to the interior of the loop

        // create the edit operation
        if (op == null)
        {
          op = new EditOperation();
          op.Name = "Update annotation symbol";
          op.SelectModifiedFeatures = true;
          op.SelectNewFeatures = false;
        }

        // use the callback method
        op.Callback(context =>
        {
           .....
        }, annoLayer.GetTable());

Step 9

We require a non-recycling cursor via the Table.Search method to update the CIMTextGraphic. Cast each of the search results to an AnnotationFeature and then use the GetGraphic method to retrieve the CIMTextGraphic.

          // find the feature
          QueryFilter qf = new QueryFilter();
          qf.WhereClause = "OBJECTID = " + oid.ToString();

          // use the table
          using (var table = annoLayer.GetTable())
          {
            // make sure you use a non-recycling cursor
            using (var rowCursor = table.Search(qf, false))
            {
              rowCursor.MoveNext();
              if (rowCursor.Current != null)
              {
                // cast to an AnnotationFeature
                ArcGIS.Core.Data.Mapping.AnnotationFeature annoFeature = rowCursor.Current as ArcGIS.Core.Data.Mapping.AnnotationFeature;
                if (annoFeature != null)
                {
                  // get the CIMTextGraphic
                  var textGraphic = annoFeature.GetGraphic() as CIMTextGraphic;

                  ....
                }
              }
            }
          }

Once the CIMTextGraphic is retrieved, update the required attributes. In this scenario change the text to "Hello World" and the symbol color to Red. Because the CIMSymbol has been modified, a new CIMSymbolReference is required. Use the SetGraphic method on the annotation feature to set the feature with the modified CIMTextGraphic, Store to commit the change and invalidate the context to refresh the display cache. Because the symbol color has been altered, the symbol is now referred to as 'bloated'; that is the symbol color now needs to be stored because it is different from the color of the symbol referenced in the SymbolID field.

                  if (textGraphic != null)
                  {
                    // change the text 
                    textGraphic.Text = "Hello World";
                                                 
                    // get the symbol reference
                    var cimSymbolReference = textGraphic.Symbol;
                    // get the symbol
                    var cimSymbol = cimSymbolReference.Symbol;

                    // change the color to red
                    cimSymbol.SetColor(ColorFactory.Instance.RedRGB);

                    // update the symbol
                    textGraphic.Symbol = cimSymbol.MakeSymbolReference();

                    // update the graphic
                    annoFeature.SetGraphic(textGraphic);

                    // store
                    annoFeature.Store();

                    // refresh the cache
                    context.Invalidate(annoFeature);
                  }

The entire OnSketchCompleteAsync method should look like the following

protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
{
  // execute on the MCT
  return QueuedTask.Run(() =>
  {
    // find features under the sketch 
    var features = MapView.Active.GetFeatures(geometry);
    if (features.Count == 0)
      return false;

    EditOperation op = null;
    foreach (var annoLayer in features.Keys)
    {
      // is it an anno layer?
      if (!(annoLayer is AnnotationLayer))
        continue;

      // are there features?
      var featOids = features[annoLayer];
      if (featOids.Count == 0)
        continue;

      // for each feature
      foreach (var oid in featOids)
      {
        // create the edit operation
        if (op == null)
        {
          op = new EditOperation();
          op.Name = "Update annotation symbol";
          op.SelectModifiedFeatures = true;
          op.SelectNewFeatures = false;
        }

        // use the callback method
        op.Callback(context =>
        {
          // find the feature
          QueryFilter qf = new QueryFilter();
          qf.WhereClause = "OBJECTID = " + oid.ToString();

          // use the table
          using (var table = annoLayer.GetTable())
          {
            // make sure you use a non-recycling cursor
            using (var rowCursor = table.Search(qf, false))
            {
              rowCursor.MoveNext();
              if (rowCursor.Current != null)
              {
                ArcGIS.Core.Data.Mapping.AnnotationFeature annoFeature = rowCursor.Current as ArcGIS.Core.Data.Mapping.AnnotationFeature;
                if (annoFeature != null)
                {
                  // get the CIMTextGraphic
                  var textGraphic = annoFeature.GetGraphic() as CIMTextGraphic;
                  if (textGraphic != null)
                  {
                    // change the text 
                    textGraphic.Text = "Hello World";
                                                 
                    // get the symbol reference
                    var cimSymbolReference = textGraphic.Symbol;
                    // get the symbol
                    var cimSymbol = cimSymbolReference.Symbol;

                    // change the color to red
                    cimSymbol.SetColor(ColorFactory.Instance.RedRGB);

                    // update the symbol
                    textGraphic.Symbol = cimSymbol.MakeSymbolReference();

                    // update the graphic
                    annoFeature.SetGraphic(textGraphic);

                    // store
                    annoFeature.Store();

                    // refresh the cache
                    context.Invalidate(annoFeature);
                  }
                }
              }
            }
          }
        }, annoLayer.GetTable());
            
      }
    }

    // execute the operation
    if ((op != null) && !op.IsEmpty)
      return op.Execute();
    return
      false;
  });
}

Step 10

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the SampleAnno.aprx project. Activate the Modify Anno Symbol tool and drag a rectangle on the map around one or more of the annotation features. Verify that the annotation text is altered and the symbol color changes to red.

In Visual Studio, add a breakpoint in the OnSketchCompleteAsync method after the CIMTextGraphic is retrieved.

     // get the CIMTextGraphic
     var textGraphic = annoFeature.GetGraphic() as CIMTextGraphic;
     if (textGraphic != null)     <-- add breakpoint here

Run the tool again and when the breakpoint hits, explore other properties you might wish to alter. Further investigation and code modifications are left to the developer.

Stop debugging and return to Visual Studio.

The Anno Tools Sample illustrating this ProGuide contains additional tools demonstrating how to add leader lines and callouts to the CIMTextGraphic.

Developing with ArcGIS Pro

    Migration


Framework

    Add-ins

    Configurations

    Customization

    Styling


Arcade


Content


CoreHost


DataReviewer


Editing


Geodatabase

    3D Analyst Data

    Plugin Datasources

    Topology

    Linear Referencing

    Object Model Diagram


Geometry

    Relational Operations


Geoprocessing


Knowledge Graph


Layout

    Reports

    Presentations


Map Authoring

    3D Analyst

    CIM

    Graphics

    Scene

    Stream

    Voxel


Map Exploration

    Map Tools


Networks

    Network Diagrams


Parcel Fabric


Raster


Sharing


Tasks


Workflow Manager


Reference

Clone this wiki locally