Skip To Content ArcGIS for Developers Sign In Dashboard

ArcGIS Runtime SDK for .NET

Find connected features in a utility network

This code sample is available for these platforms:
View Sample on GitHub

Find all features connected to a set of starting points in a utility network.

Use case

This is useful to visualize and validate the network topology of a utility network for quality assurance.

How to use the sample

To add a starting point, select 'Add starting location(s)' and tap on one or more features. To add a barrier, select 'Add barrier(s)' and tap on one or more features. Depending on the type of feature, you may be prompted to select a terminal or the distance from the tapped location will be computed. Click 'Trace' to highlight all features connected to the specified starting locations and not positioned beyond the barriers. Click 'Reset' to clear parameters and start over.

How it works

  1. Create a MapView and subscribe to its GeoViewTapped event.
  2. Create and load a Map that contains FeatureLayer(s) that are part of a utility network.
  3. Create and load a UtilityNetwork with the same feature service URL and map.
  4. Add a GraphicsOverlay with symbology that distinguishes starting points from barriers.
  5. Identify features on the map and add a Graphic that represents its purpose (starting point or barrier) at the location of each identified feature.
  6. Determine the type of the identified feature using UtilityNetwork.Definition.GetNetworkSource passing its table name.
  7. If a junction, display a terminal picker when more than one terminal is found and create a UtilityElement with the selected terminal or the single terminal if there is only one.
  8. If an edge, create a UtilityElement from the identified feature and compute its FractionAlongLine using GeometryEngine.FractionAlong.
  9. Run a UtilityNetwork.TraceAsync with the specified parameters.
  10. Group the UtilityElementTraceResult.Elements by NetworkSource.Name
  11. For every FeatureLayer in this map with elements, select features by converting UtilityElement(s) to ArcGISFeature(s) using UtilityNetwork.GetFeaturesAsync

Relevant API

  • UtilityNetworks.UtilityNetwork
  • UtilityNetworks.UtilityTraceParameters
  • UtilityNetworks.UtilityTraceResult
  • UtilityNetworks.UtilityElementTraceResult
  • UtilityNetworks.UtilityNetworkDefinition
  • UtilityNetworks.UtilityNetworkSource
  • UtilityNetworks.UtilityAssetType
  • UtilityNetworks.UtilityTerminal
  • GeometryEngine.FractionAlong

About the data

The sample uses a dark vector basemap. It includes a subset of feature layers from a feature service that contains the same utility network used to run the connected trace.

Tags

connected trace, utility network, network analysis

Sample Code

<UserControl
    x:Class="ArcGISRuntime.UWP.Samples.FindFeaturesUtilityNetwork.FindFeaturesUtilityNetwork"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:esri="using:Esri.ArcGISRuntime.UI.Controls">
    <Grid>
        <esri:MapView x:Name="MyMapView" GeoViewTapped="OnGeoViewTapped" />
        <Border
            x:Name="MainUI"
            Width="500"
            HorizontalAlignment="Right"
            VerticalAlignment="Top"
            Style="{StaticResource BorderStyle}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                    <RowDefinition Height="auto" />
                </Grid.RowDefinitions>

                <Grid Grid.Row="0">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.5*" />
                        <ColumnDefinition Width="0.5*" />
                    </Grid.ColumnDefinitions>
                    <RadioButton
                        x:Name="IsAddingStartingLocations"
                        Grid.Column="0"
                        Margin="0,5,0,5"
                        HorizontalAlignment="Stretch"
                        Content="Add starting location(s)"
                        GroupName="AddState"
                        IsChecked="True" />
                    <RadioButton
                        x:Name="IsAddingBarriers"
                        Grid.Column="1"
                        Margin="0,5,0,5"
                        HorizontalAlignment="Stretch"
                        Content="Add barrier(s)"
                        GroupName="AddState"
                        IsChecked="False" />
                </Grid>
                <Grid Grid.Row="1">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.5*" />
                        <ColumnDefinition Width="0.5*" />
                    </Grid.ColumnDefinitions>
                    <Button
                        Grid.Column="0"
                        Margin="0,5,5,5"
                        HorizontalAlignment="Stretch"
                        Click="OnReset"
                        Content="Reset" />
                    <Button
                        Grid.Column="1"
                        Margin="5,5,0,5"
                        HorizontalAlignment="Stretch"
                        Click="OnTrace"
                        Content="Trace" />
                </Grid>

                <TextBlock
                    x:Name="Status"
                    Grid.Row="2"
                    Grid.Column="2"
                    Grid.ColumnSpan="2"
                    Margin="0,5,0,5"
                    Text="Loading sample..." />
                <ProgressBar
                    x:Name="IsBusy"
                    Grid.Row="3"
                    Grid.Column="2"
                    Grid.ColumnSpan="2"
                    Height="15"
                    VerticalAlignment="Center"
                    IsIndeterminate="True"
                    Visibility="Collapsed" />
            </Grid>
        </Border>
        <Border
            Name="TerminalPicker"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Style="{StaticResource BorderStyle}"
            Visibility="Collapsed">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition />
                    <RowDefinition />
                </Grid.RowDefinitions>
                <TextBlock
                    Grid.Row="0"
                    Margin="0,0,0,5"
                    HorizontalAlignment="Center"
                    Text="Choose the terminal for this junction." />
                <ComboBox
                    x:Name="Picker"
                    Grid.Row="1"
                    Margin="0,5,0,5"
                    HorizontalAlignment="Stretch"
                    ItemsSource="{Binding}">
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Name}" />
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                </ComboBox>
                <Button
                    Grid.Row="2"
                    Margin="0,5,0,0"
                    HorizontalAlignment="Stretch"
                    Click="Choose_Click"
                    Content="Select" />
            </Grid>
        </Border>
    </Grid>
</UserControl>
// Copyright 2019 Esri.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
// language governing permissions and limitations under the License.

using Esri.ArcGISRuntime.Data;
using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.Mapping;
using Esri.ArcGISRuntime.Symbology;
using Esri.ArcGISRuntime.UI;
using Esri.ArcGISRuntime.UI.Controls;
using Esri.ArcGISRuntime.UtilityNetworks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.UI.Popups;
using Windows.UI.Xaml;

namespace ArcGISRuntime.UWP.Samples.FindFeaturesUtilityNetwork
{
    [ArcGISRuntime.Samples.Shared.Attributes.Sample(
        "Find connected features in utility networks",
        "Network Analysis",
        "Find all features connected to a given set of starting point(s) and barrier(s) in your network using the Connected trace type.",
        "")]
    public partial class FindFeaturesUtilityNetwork
    {
        private const string FeatureServiceUrl = "https://sampleserver7.arcgisonline.com/arcgis/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer";

        private UtilityNetwork _utilityNetwork;
        private UtilityTraceParameters _parameters;

        private TaskCompletionSource<UtilityTerminal> _terminalCompletionSource = null;

        private Viewpoint _startingViewpoint = new Viewpoint(new Envelope(-9813547.35557238, 5129980.36635111, -9813185.0602376, 5130215.41254146, SpatialReferences.WebMercator));

        private SimpleMarkerSymbol _startingPointSymbol;
        private SimpleMarkerSymbol _barrierPointSymbol;

        public FindFeaturesUtilityNetwork()
        {
            InitializeComponent();
            Initialize();
        }

        private async void Initialize()
        {
            try
            {
                IsBusy.Visibility = Visibility.Visible;
                Status.Text = "Loading Utility Network...";

                // Setup Map with Feature Layer(s) that contain Utility Network.
                MyMapView.Map = new Map(Basemap.CreateStreetsNightVector())
                {
                    InitialViewpoint = _startingViewpoint
                };

                // Add the layer with electric distribution lines.
                FeatureLayer lineLayer = new FeatureLayer(new Uri($"{FeatureServiceUrl}/115"));
                lineLayer.Renderer = new SimpleRenderer(new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, System.Drawing.Color.DarkCyan, 3));
                MyMapView.Map.OperationalLayers.Add(lineLayer);

                // Add the layer with electric devices.
                FeatureLayer electricDevicelayer = new FeatureLayer(new Uri($"{FeatureServiceUrl}/100"));
                MyMapView.Map.OperationalLayers.Add(electricDevicelayer);

                // Create and load the utility network.
                _utilityNetwork = await UtilityNetwork.CreateAsync(new Uri(FeatureServiceUrl), MyMapView.Map);

                Status.Text = "Click on the network lines or points to add a utility element.";

                // Create symbols for starting points and barriers.
                _startingPointSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbolStyle.Cross, System.Drawing.Color.Green, 20d);
                _barrierPointSymbol = new SimpleMarkerSymbol(SimpleMarkerSymbolStyle.X, System.Drawing.Color.Red, 20d);

                // Create a graphics overlay.
                GraphicsOverlay graphicsOverlay = new GraphicsOverlay();
                MyMapView.GraphicsOverlays.Add(graphicsOverlay);
            }
            catch (Exception ex)
            {
                Status.Text = "Loading Utility Network failed...";
                await new MessageDialog(ex.Message, ex.Message.GetType().Name).ShowAsync();
            }
            finally
            {
                IsBusy.Visibility = Visibility.Collapsed;
            }
        }

        private async void OnGeoViewTapped(object sender, GeoViewInputEventArgs e)
        {
            try
            {
                IsBusy.Visibility = Visibility.Visible;
                Status.Text = "Identifying trace locations...";

                // Set whether the user is adding a starting point or a barrier.
                bool isAddingStart = IsAddingStartingLocations.IsChecked.Value;

                // Identify the feature to be used.
                IEnumerable<IdentifyLayerResult> identifyResult = await MyMapView.IdentifyLayersAsync(e.Position, 10.0, false);

                // Check that a results from a layer were identified from the user input.
                if (!identifyResult.Any()) { return; }

                // Identify the selected feature.
                IdentifyLayerResult layerResult = identifyResult?.FirstOrDefault();
                ArcGISFeature feature = layerResult?.GeoElements?.FirstOrDefault() as ArcGISFeature;

                // Check that a feature was identified from the layer.
                if (feature == null) { return; }

                // Create element with `terminal` for junction feature or with `fractionAlong` for edge feature.
                UtilityElement element = null;

                // Select default terminal or display possible terminals for the junction feature.
                UtilityNetworkSource networkSource = _utilityNetwork.Definition.GetNetworkSource(feature.FeatureTable.TableName);

                // Check if the network source is a junction or an edge.
                if (networkSource.SourceType == UtilityNetworkSourceType.Junction)
                {
                    // Get the UtilityAssetGroup from the feature.
                    string assetGroupFieldName = ((ArcGISFeatureTable)feature.FeatureTable).SubtypeField ?? "ASSETGROUP";
                    int assetGroupCode = Convert.ToInt32(feature.Attributes[assetGroupFieldName]);
                    UtilityAssetGroup assetGroup = networkSource?.AssetGroups?.FirstOrDefault(g => g.Code == assetGroupCode);

                    // Get the UtilityAssetType from the feature.
                    int assetTypeCode = Convert.ToInt32(feature.Attributes["ASSETTYPE"]);
                    UtilityAssetType assetType = assetGroup?.AssetTypes?.FirstOrDefault(t => t.Code == assetTypeCode);

                    // Get the list of terminals for the feature.
                    IEnumerable<UtilityTerminal> terminals = assetType?.TerminalConfiguration?.Terminals;

                    // If there is more than one terminal, prompt the user to select a terminal.
                    if (terminals.Count() > 1)
                    {
                        // Ask the user to choose the terminal.
                        UtilityTerminal terminal = await WaitForTerminal(terminals);

                        // Create a UtilityElement with the terminal.
                        element = _utilityNetwork.CreateElement(feature, terminal);
                        Status.Text = $"Terminal: {terminal?.Name ?? "default"}";
                    }
                    else
                    {
                        element = _utilityNetwork.CreateElement(feature, terminals.FirstOrDefault());
                        Status.Text = $"Terminal: {element.Terminal?.Name ?? "default"}";
                    }
                }
                else if (networkSource.SourceType == UtilityNetworkSourceType.Edge)
                {
                    element = _utilityNetwork.CreateElement(feature);

                    // Compute how far tapped location is along the edge feature.
                    if (feature.Geometry is Polyline line)
                    {
                        line = GeometryEngine.RemoveZ(line) as Polyline;

                        // Set how far the element is along the edge.
                        element.FractionAlongEdge = GeometryEngine.FractionAlong(line, e.Location, -1);

                        Status.Text = $"Fraction along edge: {element.FractionAlongEdge}";
                    }
                }

                // Check that the element can be added to the parameters.
                if (element == null) { return; }

                // Build the utility trace parameters.
                if (_parameters == null)
                {
                    IEnumerable<UtilityElement> startingLocations = Enumerable.Empty<UtilityElement>();
                    _parameters = new UtilityTraceParameters(UtilityTraceType.Connected, startingLocations);
                }
                if (isAddingStart)
                {
                    _parameters.StartingLocations.Add(element);
                }
                else
                {
                    _parameters.Barriers.Add(element);
                }

                // Add a graphic for the new utility element.
                Graphic traceLocationGraphic = new Graphic(feature.Geometry as MapPoint ?? e.Location, isAddingStart ? _startingPointSymbol : _barrierPointSymbol);
                MyMapView.GraphicsOverlays.FirstOrDefault()?.Graphics.Add(traceLocationGraphic);
            }
            catch (Exception ex)
            {
                Status.Text = "Identifying locations failed...";
                await new MessageDialog(ex.Message, "Error").ShowAsync();
            }
            finally
            {
                if (Status.Text.Equals("Identifying trace locations...")) { Status.Text = "Could not identify location."; }
                IsBusy.Visibility = Visibility.Collapsed;
            }
        }

        private async Task<UtilityTerminal> WaitForTerminal(IEnumerable<UtilityTerminal> terminals)
        {
            try
            {
                // Start the UI for the user choosing the junction.
                TerminalPicker.Visibility = Visibility.Visible;
                MainUI.Visibility = Visibility.Collapsed;
                MyMapView.GeoViewTapped -= OnGeoViewTapped;

                // Load the terminals into the UI.
                Picker.DataContext = terminals;
                Picker.SelectedIndex = 0;

                // Wait for the user to select a terminal.
                _terminalCompletionSource = new TaskCompletionSource<UtilityTerminal>();
                return await _terminalCompletionSource.Task;
            }
            finally
            {
                // Make the main UI visible again.
                TerminalPicker.Visibility = Visibility.Collapsed;
                MainUI.Visibility = Visibility.Visible;
                MyMapView.GeoViewTapped += OnGeoViewTapped;
            }
        }

        private void Choose_Click(object sender, RoutedEventArgs e)
        {
            _terminalCompletionSource.TrySetResult(Picker.SelectedItem as UtilityTerminal);
        }

        private async void OnTrace(object sender, RoutedEventArgs e)
        {
            try
            {
                IsBusy.Visibility = Visibility.Visible;
                Status.Text = "Running connected trace...";

                // Verify that the parameters contain a starting location.
                if (_parameters == null || !_parameters.StartingLocations.Any()) { throw new Exception("No starting locations set."); }

                //  Get the trace result from the utility network.
                IEnumerable<UtilityTraceResult> traceResult = await _utilityNetwork.TraceAsync(_parameters);
                UtilityElementTraceResult elementTraceResult = traceResult?.FirstOrDefault() as UtilityElementTraceResult;

                if (elementTraceResult?.Elements?.Count > 0)
                {
                    // Clear previous selection from the layer.
                    MyMapView.Map.OperationalLayers.OfType<FeatureLayer>().ToList().ForEach(layer => layer.ClearSelection());

                    // Group the utility elements by their network source.
                    IEnumerable<IGrouping<string, UtilityElement>> groupedElementsResult = from element in elementTraceResult.Elements
                                                                                           group element by element.NetworkSource.Name into groupedElements
                                                                                           select groupedElements;

                    foreach (IGrouping<string, UtilityElement> elementGroup in groupedElementsResult)
                    {
                        // Get the layer for the utility element.
                        FeatureLayer layer = (FeatureLayer)MyMapView.Map.OperationalLayers.FirstOrDefault(l => l is FeatureLayer && ((FeatureLayer)l).FeatureTable.TableName == elementGroup.Key);
                        if (layer == null)
                            continue;

                        // Convert elements to features to highlight result.
                        IEnumerable<Feature> features = await _utilityNetwork.GetFeaturesForElementsAsync(elementGroup);
                        layer.SelectFeatures(features);
                    }
                }
                Status.Text = "Trace completed.";
            }
            catch (Exception ex)
            {
                Status.Text = "Trace failed...";
                await new MessageDialog(ex.Message, "Error").ShowAsync();
            }
            finally
            {
                IsBusy.Visibility = Visibility.Collapsed;
            }
        }

        private void OnReset(object sender, RoutedEventArgs e)
        {
            // Reset the UI.
            Status.Text = "Click on the network lines or points to add a utility element.";
            IsBusy.Visibility = Visibility.Collapsed;

            // Clear the utility trace parameters.
            _parameters = null;

            // Clear the map layers and graphics.
            MyMapView.GraphicsOverlays.FirstOrDefault()?.Graphics.Clear();
            MyMapView.Map.OperationalLayers.OfType<FeatureLayer>().ToList().ForEach(layer => layer.ClearSelection());
        }
    }
}