akajanus-esristaff

Showing data from custom data source on map with ArcGIS Runtime for .NET

Blog Post created by akajanus-esristaff Employee on Oct 27, 2014

Sometimes you have to access data that is stored in a format that is not supported in ArcGIS Runtime for .NET out-of-the-box. In this post, I'll go through one way to tackle this issue using Graphics and GraphicsOverlay. This sample demonstrates only reading data from the custom data source and showing results on the map. Sample uses POCO class Mountain as a domain object and data access is encapsulated into MountainService class.

 

Here is general workflow that is used in the sample.

PocoGraphics.jpg

 

User interface

This sample renders items as a graphics, symbols mountains based if user has climbed them or not (green / red) and shows the some labeling information from attributes (Name and Height).

 

Untitled.png

 

 

<Window x:Class="CreateGraphicsFromExternalDataSource.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:esri="http://schemas.esri.com/arcgis/runtime/2013"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
       <esri:SimpleMarkerSymbol x:Key="doneSymbol" Color="Green" Size="12"/>
       <esri:SimpleMarkerSymbol x:Key="notDoneSymbol" Color="Red" Size="12"/>
  </Window.Resources>
  <Grid>
       <esri:MapView x:Name="MyMapView" SpatialReferenceChanged="MyMapView_SpatialReferenceChanged">
            <esri:Map>
                <esri:ArcGISTiledMapServiceLayer 
                    ServiceUri="http://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer"/>
            </esri:Map>
            <esri:MapView.GraphicsOverlays>
            <!-- Renderer is created after graphcis are loaded in code-->
            <esri:GraphicsOverlay ID="MountainsOverlay">
                 <esri:GraphicsOverlay.Labeling>
                      <esri:LabelProperties IsEnabled="True">
                           <esri:AttributeLabelClassCollection>
                                <!-- Labeling based on the name attributes ie. "Ben Nevis - 1344 m" -->
                                <esri:AttributeLabelClass 
                                        TextExpression="[Name] CONCAT &quot; - &quot; CONCAT [Height] CONCAT &quot; m &quot;">
                                     <esri:AttributeLabelClass.Symbol>
                                          <esri:TextSymbol Color="Blue">
                                               <esri:TextSymbol.Font>
                                                    <esri:SymbolFont FontFamily="Arial" 
                                                            FontSize="12" FontStyle="Italic" FontWeight="Bold" />
                                                    </esri:TextSymbol.Font>
                                               </esri:TextSymbol>
                                          </esri:AttributeLabelClass.Symbol>
                                     </esri:AttributeLabelClass>
                                </esri:AttributeLabelClassCollection>
                           </esri:LabelProperties>
                      </esri:GraphicsOverlay.Labeling>
                 </esri:GraphicsOverlay>
            </esri:MapView.GraphicsOverlays>
         </esri:MapView>        
    </Grid>
</Window>







 

MapView gets SpatialReference

 

This is used to invoke loading data and transforming that to the format that map can consume.

 

        /// <summary>
        /// Loading data after Spatial Refernce is defined.
        /// </summary>
        private async void MyMapView_SpatialReferenceChanged(object sender, EventArgs e)
        {
            // In this sample, data is loaded only once so remove handler
            MyMapView.SpatialReferenceChanged -= MyMapView_SpatialReferenceChanged;


            try
            {
                // Get mountains as graphics
                var mountains = await _mountainService.GetMountainsAsGraphicsAsync();
    
                // Get overlay from MapView and set source
                var overlayForMountains = MyMapView.GraphicsOverlays["MountainsOverlay"];
                
                // Creating unique value renderer for visualization
                overlayForMountains.Renderer = CreateUniqueValueRenderer();                
                
                // Set graphics to the overview
                overlayForMountains.GraphicsSource = mountains;


                // Create extent from graphics
                Envelope extent = null;
                foreach (var mountain in mountains)
                {
                    if (Esri.ArcGISRuntime.Geometry.Geometry.IsNullOrEmpty(extent))
                        extent = new Envelope(mountain.Geometry as MapPoint, mountain.Geometry as MapPoint);
                    else
                        extent = extent.Union(mountain.Geometry as MapPoint);
                }


                // Set view to loaded items
                await MyMapView.SetViewAsync(extent.Expand(1.2d));
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), "Error occured");
            }
        }







 

First we have to get some objects to show. MountainService.GetMountainsAsGraphicAsync queries/reads/gets data from custom data source and creates Graphics that are added to the GraphicsOverlay GraphicsOverlays are drawn on top of the layers in the map. After graphics are returned, a Renderer is created dynamically. Renderer defines how the graphics are drawn in the GraphicsOverlay. After GraphicSource is set for the GraphicsOverlay, an envelope that contains all the graphics is created and MapView is zoomed into that.

 

Create Graphics from POCOs

 

First we have to load some POCOs but before that here is the Mountain class that is used as a domain object.

 

    /// <summary>
    /// Model class to mock POCO domain object
    /// </summary>
    public class Mountain
    {
        /// <summary>
        /// X in WGS84 (MapView uses WebMercator so we project this to it when creating graphic)
        /// </summary>
        public double X { get; set; }


        /// <summary>
        /// Y in WGS84 (MapView uses WebMercator so we project this to it when creating graphic)
        /// </summary>
        public double Y { get; set; }


        /// <summary>
        /// The name of the mountain.
        /// </summary>
        public string Name { get; set; }


        /// <summary>
        /// The height of the mountain in meters.
        /// </summary>
        public double Height { get; set; }


        /// <summary>
        /// Defines if user has climbed the mountain.
        /// </summary>
        public bool IsDone { get; set; }
    }





 

I created super simple MockMountainService that wraps all "communication" functionality with the data source. This kind of approach is fairly common when you are implementing Repository / Service data access patterns or similar. GetMountainsAsGraphicsAsync executes first query (GetMountainsAsync) to custom data source (SQL server, txt file, custom binary etc...) and gets set of Mountains. This sample returns hard coded values, this would be the place where you need to implement your own data access logic. It doesn't really matter how and where the data is stored as long you know how to access it and how to convert that into .NET objects. Each of the returned items are used to create Graphic that presents that single domain object on the map. In the data, location is stored in WGS84 so we need to get that to the projection that MapView is using - in this case WebMercator. Projections are handled by using GeometryEngine that provides a lot of different spatial operations out-of-the-box so in the most cases there is no need to roll your own.

 

After getting the geometry in the correct format a set of Attributes is created. Attributes are basically just list of objects with a key value that is associated with Graphic. After adding needed (can be subset from domain objects data or maybe you need to calculate some extra attributes based on other values that are not needed on the map) attributes to the collection, actual Graphic is created.

 

       /// <summary>
        /// Gets all mountains from the data source as a Graphics
        /// </summary>
        /// <returns></returns>
        public async Task<GraphicCollection> GetMountainsAsGraphicsAsync()
        {
            // Load mountain models from your data source
            var mountains = await GetMountainsAsync();


            var results = new GraphicCollection();


            foreach (var mountain in mountains)
            {
                // Data source stores items in WGS84
                var locationInWSG84 = new MapPoint(mountain.X, mountain.Y, SpatialReferences.Wgs84);


                // Map uses WebMercator so project WGS84 to Webmercator. 
                var locationInWebMercator = GeometryEngine.Project(locationInWSG84, SpatialReferences.WebMercator);


                // Create attributes
                var attributes = new Dictionary<string, object>();
                attributes.Add("Name", mountain.Name);
                attributes.Add("Height", mountain.Height);
                attributes.Add("IsDone", mountain.IsDone);


                // Create graphic
                var mountainGraphic = new Graphic(locationInWebMercator, attributes);


                // Add to results
                results.Add(mountainGraphic);
            }


            return results;
        }







 

As you can see, converting graphics from POCOs are very straight forward. It gets a bit more complicated if you need to support CRUD operations but basically you just do conversion another way around and execute your custom data access logic.

 

Create Renderer

Renderer defines how graphics are drawn on the map. Here it is created dynamically after graphics are loaded and you could create easily use max value, min value and/or some other logic to create optimal visual presentation.

 

        /// <summary>
        /// Create renderer. This could create renderer dynamically based on some attributes(data).
        /// </summary>
        private Renderer CreateUniqueValueRenderer()
        {
            var doneSymbol = TryFindResource("doneSymbol") as SimpleMarkerSymbol;
            var notDoneSymbol = TryFindResource("notDoneSymbol") as SimpleMarkerSymbol;


            var mountainRenderer = new UniqueValueRenderer()
            {
                DefaultLabel = "Not climbed",
                DefaultSymbol = notDoneSymbol,
                Fields = new ObservableCollection<string> { "IsDone" },
                Infos = new UniqueValueInfoCollection() { 
                    new UniqueValueInfo() {
                        Values = new ObservableCollection<object> { true }, 
                        Symbol = doneSymbol}}
            };


            return mountainRenderer;
        }






 

Zoom to the content

After everything is ready, map can zoom to the content area by using MapView.SetViewAsync method. To use that, we need to create an envelope that contains all the features and expand it a bit to get them nicely into the view. This can be easily done by looping through all graphics.

 

 // Create extent from graphics
                Envelope extent = null;
                foreach (var mountain in mountains)
                {
                    if (Esri.ArcGISRuntime.Geometry.Geometry.IsNullOrEmpty(extent))
                        extent = new Envelope(mountain.Geometry as MapPoint, mountain.Geometry as MapPoint);
                    else
                        extent = extent.Union(mountain.Geometry as MapPoint);
                }


                // Set view to loaded items
                await MyMapView.SetViewAsync(extent.Expand(1.2d));







 

And that's pretty much it.

 

Resources:

Outcomes