Creating and using 'dynamic map layers'

4071
0
05-12-2021 06:51 AM
Labels (1)
DonKemlage
Esri Contributor
0 0 4,071

Introduction

The ArcGIS Runtime provides developers a suite SDKs to create mapping apps on native devices such as  phones, tablets, and laptops. A map's layers can be located on the native device for an offline experience when internet connectivity is not available. Additionally, map layers can be accessed via a service like ArcGIS Server (aka ArcGIS Enterprise Server), ArcGIS Portal, and ArcGIS Online when the internet is readily available.

There are some circumstances when it is desired to have a Runtime app access map layers via ArcGIS Server for the following reasons:
- It is centrally controlled are administered by an authority to control data quality and integrity.
- The hardware for the server provide fast and efficient map layer rendering.
- GIS professionals make frequent edits to the data and real-time updates being shown on the client is important.

ArcGIS Server can deliver pre-cached map layers as image tiles (or static maps) to the client. These static maps are performant in that they render on the client app fast. They can provide reference for browsing around to learn about a particular place. They can serve as the backdrop for a navigation app. But what if you wanted to dynamically change the rendering of an ArcGIS Server provided map layer in your client app on the fly? You could ask "what if"  questions to help bring your map alive and provide useful analysis.

ArcGIS Server has the concept of a REST layer source object that enables serving up 'dynamic layers' for client apps. There are three types of `layerSource` objects:
- Dynamic map layer - Refers to an existing map service layer
- Dynamic data layer - Refers to a layer created on-the-fly from a dataSource
- Dynamic workspace layer - Refers to a pre-authored Layer File (.lyrx) from a registered folder.

In this blog post, I will demonstrate how to use ArcGIS Pro to author and publish a map service on ArcGIS Server to create a dynamic map layer. Then we'll create an ArcGIS Runtime application using the WPF framework in the C# programming language to consume the dynamic map layer. This will let us do some  interesting things, like modify the rendering on the fly and perform identify operations to look at underlying attribute information.

The ArcGIS Server 'layerSource' object of  dynamic data layer is the subject of a different blog post titled: Creating and using 'dynamic data layers'.

The ArcGIS Server 'layerSource' object of dynamic workspace layer might be subject of a future blog post showing how they are created and consumed by an ArcGIS Runtime app.

For now, let's concentrate on the ArcGIS Server concept of a dynamic map layer, how it is created and consumed.

 

ArcGIS Pro

Using ArcGIS Pro, I created a shapefile (named COVID_Counties.shp) based on the USA county boundaries. The attribute data contained the following fields for each USA county:

FIELD NAMETYPE

NOTES

FIDObject IDObject ID for each record
CNTY_NAMEStringCounty name
ST_NAMEStringState name
CNTY_FIPSStringCounty FIPS code
ST_FIPSStringState FIPS code
ALL_FIPSStringConcatenated State and County FIPS codes
POP_2020Integer2020 Population estimate for the county
MAR_04_20IntegerTotal COVID-19 count on Mar 4, 2020
MAR_11_20IntegerCOVID-19 count on Mar 11, 2020
MAR_18_20IntegerTotal COVID-19 count on Mar 20, 2020
MAR_25_20IntegerTotal COVID-19 count on Mar 25, 2020
APR_01_20IntegerTotal COVID-19 count on Apr 1, 2021

 

I set the fill for the county boundaries to be transparent and the outline to be 0.5 pixel wide. Then with with the Catalog tab active, I right clicked on the name of the ArcGIS Server I had administrative access to and chose Publish > Map Service.

image.png

In the first dialog that appears in the publish process, Select Map, I expanded the left Project > Maps TOC item and chose Map (which is the view that had my data COVID_Counties.shp layer) . Then, I clicked OK.

Afterward, the Publish Map Service tab became active and I proceeded to fill in all of the necessary elements to publish my map as a service. In particular, I specified the following options:

Publish Map Service dialog options

General tab

NameCOVID19_Counties
SummaryTotal weekly count of COVID-19 occurrences by county from Mar 4, 2020 thru Apr 1, 2020
TagsCOVID-19 USA Counties
DataCopy all data
Locationhttps://[your.server].com/server/rest/services
Folder[you choose]

 

Configuration tab

CapabilitiesMap **

**NOTE: Click the Configure Service Properties icon.

image.png

Then, I made sure the following Map Service Properties were set:

Operations

Mapchecked on
Datachecked on
Querychecked on


Dynamic Workpaces

Allow per request modification of layer order and symbologychecked on


Caching

Draw this map serviceDynamically from data

 

image.png

As a good practice it is recommended to click the Analyze button before trying to Publish your service, to fix any errors or warnings. After I had everything the way I wanted, I clicked the Publish button and waited for the "Successfully published map service....." notification. So far so good.

 

ArcGIS Server

Now let's move to ArcGIS Server to ensure that I could see what I was expecting. I logged in with my username and password. I navigated to the correct location where my newly publish map was. In my case the COVID19_Counties (Map Server) was there and had the following configuration:

image.png

The most important thing I was looking for was the Supports Dynamic Layers: true setting. This ensures that we can generate a dynamic map layer in ArcGIS Server.

TIP: In general, when you see the Support Dynamic Layers: true setting on any ArcGIS Server service REST page out on the open internet, you can create a dynamic map layer in your custom app. It does not matter who is the author or publisher of these services; they are public and you can explore and tinker as you wish.

 

ArcGIS Runtime

Using Visual Studio 2019, I created a new C# application based on the WPF Framework. Using the NugetPackage Manager dialog, I added the most recent Esri.ArcGISRuntime.WPF package from nuget.org. Three files in the Visual Studio project require modification to create the app. This section names those files and describes the code changes needed. 

App.xaml.cs

Modify this file is to enable the API Key so that you can view the basemap hosted on ArcGIS Online service. Provide your own API Key by going to the ArcGIS developer dashboard at: https://developers.arcgis.com/dashboard/

 

using System.Windows;

namespace DynamicLayerCovid19
{
  public partial class App : Application
  {
   protected override void OnStartup(StartupEventArgs e)
   {
    base.OnStartup(e);

    // Note: it is not best practice to store API keys in source code.
    // The API key is referenced here for the convenience of this tutorial.
    //
    // Get your open API key from: https://developers.arcgis.com/dashboard/
    Esri.ArcGISRuntime.ArcGISRuntimeEnvironment.ApiKey = "PROVIDE YOUR API KEY";
   }
  }
}

 

 

MainWindow.xaml

The purpose of modifying this file is to define the GUI for the app.

 

<Window x:Class="DynamicLayerCovid19.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DynamicLayerCovid19"
        xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
        mc:Ignorable="d"
        Title="Dynamic layer Covid-19" Height="800" Width="1200">
    <Grid>
        <esri:MapView x:Name="MyMapView"/>
        <Border
            Background="White" BorderBrush="Black" BorderThickness="1"
            HorizontalAlignment="Left" VerticalAlignment="Bottom"
            Margin="10,0,0,30" Padding="10" Width="400">
            <StackPanel Orientation="Vertical">
                <TextBlock x:Name="TextBlockInstructions" TextWrapping="Wrap" Width="350" Height="130" Margin="0,0,0,10"/>
                <TextBox x:Name="TextBox_SQL" Width="350" Margin="0,0,0,10"/>
                <Button x:Name="ButtonExecute" Click="Button_Execute_Click" Content="Execute" Width="350"/>
            </StackPanel>
        </Border>
    </Grid>
</Window>

 

 

MainWindows.xamls.cs

This file contains the main logic for the running of the app. There are 4 main sections worth discussing to provide insight on how the app works:

(1) The using statements at the top of the file shorten the syntax for writing the code.

(2) The IntializeMap() function sets up the initial look of the GUI. It adds the world light gray base map zoomed to the area of the continental USA to provide orientation for the operational layers. An ArcGISMapImageLayer is added by default to show the county boundaries (via transparent fill) used to interrogate COVID-19 cases for few dates in time. A default GraphicsOverlay is created to aid in the identify process. Finally, the GeoViewTapped event is wired up to handle the user map taps to obtain the identify results.   

(3) The Button_Execute_Click() function provides the logic to add a dynamic map layer to the application showing USA county COVID-19 cases for selected dates are shown with a red solid fill. The dynamic map layer is created via the MapSubLayerSource object to create an ArcGISMapImageSubLayer object. The user provided SQL syntax is used for the DefinitionExpression to restrict which counties are provided by ArcGIS Server back to the client app.  A SimpleRenderer is used to define a solid red fill for those polygons returned. The dynamic map layer is attached as an ArcGISSubLayer of the ArcGISMapImageLayer when the app was first loaded. The function provides logic to remove an prior added dynamic map layers for multiple SQL queries executed by the user.

(4) The MyMapView_GeoViewTapped() function provides attribute information on the dynamic map layer in the map where the user taps. The existing GraphicsOverlay is obtained and cleared from previous user interaction. Using the IdentifyLayersAsync method the IdentifyLayerResult object is returned and SubLayerResults are obtained. While iterating over the results, the GeoElement is obtained and used to construct a yellow solid fill. It  is then added to the GraphicsOverlay. Additionally while iterating over the results, the attribute information is obtained. By iterating over the Attributes the Key/Value pairs for each identified county, the results are present back to the user in a MessageBox.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using Esri.ArcGISRuntime.Mapping;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Symbology;
using Esri.ArcGISRuntime.Data;
using Esri.ArcGISRuntime.UI;
using Esri.ArcGISRuntime.UI.Controls;

namespace DynamicLayerCovid19
{
 public partial class MainWindow : Window
 {
   public MainWindow()
   {
    InitializeComponent();

    // Function to start up the mapping app
    InitializeMap();
   }

   private async void InitializeMap()
   {
    // Provide instructions on how to use the app
    TextBlockInstructions.Text = "Enter a valid SQL statement in the textbox and click Execute." +
     System.Environment.NewLine + 
     "Some valid examples are:" + System.Environment.NewLine +
     "MAR_04_20 > 0" + System.Environment.NewLine +
     "MAR_11_20 > 0" + System.Environment.NewLine +
     "MAR_04_20 = 0  AND MAR_11_20 > 0" + System.Environment.NewLine +
     System.Environment.NewLine + 
     "NOTE: You can also tap on a county to perfom an identify showing the attribute values.";

    // Give a starting example of a valid SQL syntax
    TextBox_SQL.Text = "MAR_04_20 = 0  AND MAR_11_20 > 0";

    // Create new map
    Map myMap = new Map();

    // Get the base map from the map
    Basemap myBasemap = myMap.Basemap;

    // Get the layer collection for the base map 
    LayerCollection myLayerCollection_BaseLayers = myBasemap.BaseLayers;

    // Create uri for the reference base layer
    Uri myUri_BaseLayer = new Uri("http://services.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer");

    // Create new reference map image layer from the uri
    ArcGISMapImageLayer myArcGISMapImageLayer_BaseLayer = new ArcGISMapImageLayer(myUri_BaseLayer);

    // Provide a base layer name
    myArcGISMapImageLayer_BaseLayer.Name = "WorldLightGrayBase";

    // Add the reference base layer to the layer collection
    myLayerCollection_BaseLayers.Add(myArcGISMapImageLayer_BaseLayer);

    // Assign the map to the MapView
    MyMapView.Map = myMap;

    // Create an envelope that focuses on the continental United States
    Envelope myEnvelope = new Envelope(-14131181.98, 2324882.74, -7251750.27, 6636148.22, SpatialReferences.WebMercator);

    // Set the view point of the map view to that of the envelope
    await MyMapView.SetViewpointGeometryAsync(myEnvelope, 20);

    // Create uri for the ArcGIS map image layer
    var myUri = new Uri("https://[Your.Server].com/server/rest/services/NA/COVID19_Counties/MapServer");

    // Create new ArcGIS map image layer from the url
    ArcGISMapImageLayer myArcGISMapImageLayer = new ArcGISMapImageLayer(myUri);
    myArcGISMapImageLayer.Name = "COVID19 Counties";

    // Get the list of sub layers from the map image layer
    IList<ArcGISSublayer> myListOfSublayers = myArcGISMapImageLayer.Sublayers;

    // Get the layer collection for the operational layers
    LayerCollection myLayerCollection_OperatioalLayers = myMap.OperationalLayers;

    // Add the ArcGIS map image layer to the operation layer collection. The default rendering from the
    // ArcGIS Server map service for the counties layer will be used.
    myLayerCollection_OperatioalLayers.Add(myArcGISMapImageLayer);

    // Create a graphics overlay used to highlight identified counties
    MyMapView.GraphicsOverlays.Add(new GraphicsOverlay());

    // Wire up the GeoView tapped event for the identify operation
    MyMapView.GeoViewTapped += MyMapView_GeoViewTapped;
   }

   private void Button_Execute_Click(object sender, RoutedEventArgs e)
   {
    // Create a new map sub layer source object; this will be the basis for the new 'dynamic map layer'
    // generated by ArcGIS Server. NOTE: The input parameter is the Id (or index number)
    // of the sub layer that is listed in the ArcGIS REST Services Directory documentation for the
    // ArcGIS Server; in this particular case 0 = the 'COVID19_Counties' sub layer.
    MapSublayerSource myMapSubLayerSource = new MapSublayerSource(0);

    // Create a new map image sub layer (aka 'dynamic map layer') based on the map sub layer source;
    // provide an id value as well
    ArcGISMapImageSublayer myArcGISMapImageSubLayer = new ArcGISMapImageSublayer(99, myMapSubLayerSource);

    // Set a definition expression for a SQL query that limits the counties being drawn
    myArcGISMapImageSubLayer.DefinitionExpression = TextBox_SQL.Text;
			
    // Define a new simple renderer for the counties sub layer
    SimpleRenderer mySimpleRenderer = new SimpleRenderer();

    // Define a simple fill symbol (red and solid)
    SimpleFillSymbol mySimpleFillSymbol = new SimpleFillSymbol();
    mySimpleFillSymbol.Color = System.Drawing.Color.Red;
    mySimpleFillSymbol.Style = SimpleFillSymbolStyle.Solid;

    // Set the symbol of the counties sub layer to the newly defined symbol
    mySimpleRenderer.Symbol = mySimpleFillSymbol;

    // Set the renderer for the map image sub layer
    myArcGISMapImageSubLayer.Renderer = mySimpleRenderer;

    // Create uri for the existing ArcGIS map image layer
    var myUri = new Uri("https://[Your.Server].com/server/rest/services/NA/COVID19_Counties/MapServer");

    // Create new ArcGIS map image layer from the url
    ArcGISMapImageLayer myArcGISMapImageLayer = new ArcGISMapImageLayer(myUri);
    myArcGISMapImageLayer.Name = "COVID19_Counties_Dynamic";

    // Get the list of sub layers from the map image layer
    IList<ArcGISSublayer> myListOfSublayers = myArcGISMapImageLayer.Sublayers;

    // Add the map image sub layer (aka 'dynamic map layer') to the ArcGIS map image layer
    myListOfSublayers.Add(myArcGISMapImageSubLayer);

    // Get the map from the map view
    Map myMap = MyMapView.Map;

    // Get the layer collection from the map
    LayerCollection myLayerCollection = myMap.OperationalLayers;

    // Remove any previous operational layers (aka 'dynamic map layers') that was added previously
    if (myLayerCollection.Count() > 1)
    {
     myLayerCollection.RemoveAt(1);
    }

    // Add the ArcGIS map image layer to the layer collection
    myLayerCollection.Add(myArcGISMapImageLayer);
   }

   private async void MyMapView_GeoViewTapped(object sender, GeoViewInputEventArgs e)
   {
    // Get the graphics overlay collection
    GraphicsOverlayCollection myGraphicsOverlayerCollection = MyMapView.GraphicsOverlays;

    // Get the first graphics overlay in the collection
    GraphicsOverlay myGraphicsOverlay = myGraphicsOverlayerCollection[0];

    // Get the graphic collection 
    GraphicCollection myGraphicCollection = myGraphicsOverlay.Graphics;

    // Clear out all of the graphics in the graphic collection
    myGraphicCollection.Clear();

    // Set the maximum number of features to be selected/returned from the identify operation
    long myMaxSelectedFeatures = 50;

    // Set the search tolerance in pixels for the identify to find features from where the user
    // touched on the map 
    double myTolerance = 3;

    try
    {
     // Get the location on the map where the user touched (tapped, mouse click, etc.)
     var myScreenPoint = e.Position;

     // Get the map from the map view
     Map myMap = MyMapView.Map;

     // Get the collection of all layers in the map
     IReadOnlyList<Layer> myReadOnlyListOfLayers = myMap.AllLayers;

     // Get the second item in the collection of layers (it should be the 'dynamic map layer')
     Layer myLayer = myReadOnlyListOfLayers[1];

     // Get the identify layer result from where the user touched on the map subject to the
     // parameters entered
     IdentifyLayerResult myIdentifyLayerResult = await MyMapView.IdentifyLayerAsync(myLayer, myScreenPoint, myTolerance, false, myMaxSelectedFeatures);

     // Get the collection of identify layer result values from the sub layers
     IReadOnlyList<IdentifyLayerResult> mySublayerResults = myIdentifyLayerResult.SublayerResults;

     // Proceed if we have at least one result
     if (mySublayerResults.Count > 0)
     {
      // Loop through each identify layer result
      foreach (var oneIdentifyLayerResult in mySublayerResults)
      {
       // Get the geo element (shape + attributes) for the identify layer result
       IReadOnlyList<GeoElement> myGeoElements = oneIdentifyLayerResult.GeoElements;

       // Loop through each geo element
       foreach (var oneGeoElement in myGeoElements)
       {
        // Get the geometry from the geo element
        Esri.ArcGISRuntime.Geometry.Geometry myGeometry = oneGeoElement.Geometry;

        // Create symbol for the polygon
        SimpleFillSymbol mySimpleFillSymbol = new SimpleFillSymbol(SimpleFillSymbolStyle.Solid, System.Drawing.Color.Yellow, null);

        // Create new graphic using the geometry and symbol to display to the user
        Graphic myPolygonGraphic = new Graphic(myGeometry, mySimpleFillSymbol);

        // Create overlay to where graphics are shown
        MyMapView.GraphicsOverlays[0].Graphics.Add(myPolygonGraphic);

        string keyValues = "";
       
        // Construct the identify information string to be presented to the
        // user based upon what is found in the key/value pairs of the geo elements
        foreach (KeyValuePair<string, object> element in oneGeoElement.Attributes)
        {
         keyValues += element.Key + " = " + (element.Value ?? "Null").ToString() + Environment.NewLine;
        }
        // Display the identify information to the user
        MessageBox.Show(keyValues, "Identify Results:");
       }
      }
     }
    }
    catch (Exception ex)
    {
     // If there was a problem display it to the user
     MessageBox.Show(ex.Message);
    }
   }
  }
}

 

 

After the code complied without any errors, it was time to test out the app. When the app opens you can start out by clicking the Execute button and a dynamic map layer will be generated for the SQL query 'MAR_04_20 = 0 AND MAR_11_20 > 0'. Those USA counties that had 1 or more new COVID-19 cases between March 4, 2020 and March 11, 2020 were displayed in red. This can be verified by tapping on a red county (aka an identify operation) to see the attribute information displayed in a MessageBox as well as that county being highlighted in yellow.

image.png

You can experiment further by performing additional 'What if' type of SQL queries by entering them in the TextBox and clicking the Execute button.

 

Additional Resources

ArcGIS Server
[Dynamic layers] https://enterprise.arcgis.com/en/server/latest/publish-services/windows/about-dynamic-layers.htm
[Supported functionality in map services] https://enterprise.arcgis.com/en/server/latest/publish-services/windows/supported-functionality-in-m...

ArcMap
[About dynamic layers] https://desktop.arcgis.com/en/arcmap/10.3/map/publish-map-services/about-dynamic-layers.htm
[Enabling dynamic layers on a map service in Manager] https://desktop.arcgis.com/en/arcmap/10.3/map/publish-map-services/enabling-dynamic-layers-on-a-map-...

ArcGIS Pro
[Configure a map service] https://pro.arcgis.com/en/pro-app/latest/help/sharing/overview/configure-a-map-service.htm
[Publish a map service] https://pro.arcgis.com/en/pro-app/latest/help/sharing/overview/publish-a-map-service.htm

ArcGIS Runtime .NET Samples that demonstrate ArcGISMapImageLayer.Sublayers capabilities
[Change sublayer renderer] https://developers.arcgis.com/net/wpf/sample-code/change-sublayer-renderer/
[Map image layer sublayer visibility] https://developers.arcgis.com/net/wpf/sample-code/map-image-layer-sublayer-visibility/
[Identify layers] https://developers.arcgis.com/net/wpf/sample-code/identify-layers/
[Query map image sublayer] https://developers.arcgis.com/net/wpf/sample-code/query-map-image-sublayer/

Source for COVID-19 county level data
[University of Virginia] https://nssac.bii.virginia.edu/covid-19/dashboard/

About the Author
I have been with Esri for over 21 years and I had the privilege of working on/with various software products including: ArcGIS Maps SDK for Qt, ArcGIS Maps SDK for .NET, ArcGIS Desktop, Arc Pro, ArcGIS Server, Map Objects, ArcView GIS, and even PC Arc/Info. Currently, I author content on the https://developers.arcgis.com web site helping developers be successful with Esri software.