Label features the easy way

2479
2
03-28-2018 06:21 PM
Labels (1)
ThadTilton
Esri Contributor
4 2 2,479

The good newsFeatures and graphics in your ArcGIS Runtime SDK for .NET app can be labeled using a combination of attribute values, text strings, and values calculated with an expression. You can set font characteristics, determine how labels are positioned and prioritized, and how conflicts between overlapping labels are automatically and dynamically resolved. By creating one or more label classes, you can define distinct labels for groups of features.

The bad news: As of the 100.2.1 release, there's no API exposed for this. To implement labeling for a feature layer, graphics overlay, or map image sub-layer, you need to craft the appropriate JSON definition (text) to define your label classes (as described here). Bummer.

But don't worry, a simple JSON serializable class is all it takes to simplify your labeling code. I'll walk you through the basic process in the steps below. If that's still too much work for you, feel free to simply download the attached code and try it out. The labeling class in the project (LabelClassInfo.cs) can be brought into any ArcGIS Runtime for .NET project and used to create labels for your layers.

  1. Start Visual Studio and create a new ArcGIS Runtime SDK for .NET app. 
  2. Add a new class to the project. Let's name it LabelClassInfo.cs. Add the following using statements at the top of the class.
    using Esri.ArcGISRuntime.Geometry;
    using Esri.ArcGISRuntime.Symbology;
    using System;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Json;‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  3. Decorate the class with the DataContract attribute. This indicates that the class will be serializable. 
        [DataContract]
        public class LabelClassInfo
        {‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  4. Create properties for everything expected in the JSON definition of the label class. This information is defined under labelingInfo in the Web map specification. The specification defines an "allowOverrun" value, for example. This could be implemented with the following simple bool property. Note that the name of the property in the class does not need to match the name in the specification.
            public bool AllowOverrun { get; set; }‍‍‍‍‍
    Based on the specification, here are the properties (and corresponding data type) needed for the label class: allowOverrun (bool), deconflictionStrategy (string), labelExpression (string), labelExpressionInfo (object), labelPlacement (string), minScale (int), maxScale (int), name (string), priority (int), removeDuplicates (string), removeDuplicatesDistance (int), repeatLabel (bool), repeatLabelDistance (int), stackLabel (bool), stackAlignment (string), symbol (TextSymbol), useCodedValues (bool), where (string).
  5. OK, the list above looks pretty straightforward, except for the two properties that don't return primitive data types. Go ahead and create properties for everything except labelExpressionInfo and symbol (we'll circle back to those). 
  6. After you've defined the properties, in order to make them serializable you'll need to decorate them with the DataMember attribute. The string you supply for "Name" will be used in the output JSON and must match the name used in the Web map specification. Add the appropriate DataMember attribute to all properties.
            [DataMember(Name = "deconflictionStrategy")]
            public LabelPosition LabelPosition { get; set; }‍‍‍‍‍‍

  7. The labelExpressionInfo property, returns object. What kind of object do we need here? In the specification, the JSON describes a class with a single property (not counting the deprecated "value" property). You'll therefore need to create a new private class (nested under your main class) with a single property that holds a (string) expression. This class will also be serializable, so you need the DataContract and DataMember attributes. 
            [DataContract]
            private class ExpressionInfo
            {
                [DataMember(Name = "expression")]
                public string Expression { get; set; }
    
                public ExpressionInfo(string expression)
                {
                    Expression = expression;
                }
            }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  8. The labelExpressionInfo property should be based on the new ExpressionInfo type. To simplify the API, you might want to expose just the expression property (ArcadeExpression in the example below) and build the ExpressionInfo object that contains it internally. 
            [DataMember(Name ="labelExpressionInfo")]
            private ExpressionInfo _labelExpressionInfo = new ExpressionInfo("");
    
            public string ArcadeExpression
            {
                get { return _labelExpressionInfo.Expression; }
                set { _labelExpressionInfo = new ExpressionInfo(value); }
            }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  9. What about the symbol property that returns TextSymbol? You can handle this is much the same way: expose a public Symbol property of type TextSymbol, then use an internal property to get its JSON representation (yes, Esri.ArcGISRuntime.Symbology.Symbol is serializable!).
            public TextSymbol Symbol { get; set; }
    
            [DataMember(Name = "symbol")]
            private string SymbolString
            {
                get { return this.Symbol.ToJson(); }
                set { this.Symbol = (TextSymbol)TextSymbol.FromJson(value); }
            }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  10. Finally, create a method on LabelClassInfo to return the JSON representation of the object.
            public string GetJson()
            {
                // Create a JSON serializer for the label class.
                DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(LabelClassInfo));
    
                // Write the object to a memory stream.
                MemoryStream stream = new MemoryStream();
                jsonSerializer.WriteObject(stream, this);
    
                // Read the stream to a string.
                stream.Position = 0;
                StreamReader reader = new StreamReader(stream);
                string json = reader.ReadToEnd();
    
                // HACK: clean up the json a bit.
                json = json.Replace("\"{", "{").Replace("}\"", "}").Replace("\\", "\"").Replace("\"\"", "\"");
    
                // Return the serialized string.
                return json;
            }‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

  11. You should now be able to use the new LabelClassInfo class to write code like the following to apply labels to a feature layer!
                TextSymbol textSymbol = new TextSymbol
                {
                    FontFamily = "Arial",
                    FontWeight = weight,
                    Size = (int)TextSizeComboBox.SelectedValue,
                    HorizontalAlignment = Esri.ArcGISRuntime.Symbology.HorizontalAlignment.Left,
                    VerticalAlignment = Esri.ArcGISRuntime.Symbology.VerticalAlignment.Bottom,
                    Color = (Color)ColorComboBox.SelectedValue
                };
    
                // Create a new LabelInfo object and set the relevant properties (including the text symbol).
                LabelClassInfo labelInfo = new LabelClassInfo()
                {
                    LabelPosition = "static",
                    ArcadeExpression = "return $feature['" + LabelFieldComboBox.SelectedItem + "'];",
                    MinScale = 0,
                    MaxScale = 0,
                    Priority = 30,
                    RemoveDuplicateLabels = "featureType",
                    StackLabel = true,
                    Symbol = textSymbol
                };
    
                // Get the raw JSON from the label info object.
                labelJson = labelInfo.GetJson();
    
                // Create a new label definition from the JSON string.
                LabelDefinition labelDef = LabelDefinition.FromJson(labelJson);
    
                // Clear existing label definitions.
                _parcelsLayer.LabelDefinitions.Clear();
    
                // Add the new label definition.
                _parcelsLayer.LabelDefinitions.Add(labelDef);‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

OK, so it might not be the easiest thing to code, but once you have it you can reuse this class anytime you need to add labels for a layer.

Apologies if the steps above were too vague or if you got stuck because I left something out! I've attached a project with the LabelClassInfo class and a WPF app for testing. The attached example also includes some enums for defining label position, placement, and handling of stacking and duplicates.

Update (2018 March 13):

  • I've attached a much cleaner version of the LabelClassInfo class ("LabelClassInfo2.cs") that uses Newtonsoft.Json (thanks to Matvei Stefarov on the ArcGIS Runtime SDK for .NET team).
  • Including a link to a a utility that helps create your serializable class from JSON: Instantly parse JSON in any language | quicktype 
2 Comments
About the Author
I'm a product engineer on the ArcGIS Maps SDKs for Native Apps team, where I help create and maintain developer guides and API reference documentation.