Discover connected features in a utility network using connected, subnetwork, upstream, and downstream traces.
Use case
You can use a trace to visualize and validate the network topology of a utility network for quality assurance. Subnetwork traces are used for validating whether subnetworks, such as circuits or zones, are defined or edited appropriately.
How to use the sample
Tap on one or more features while 'Add starting locations' or 'Add barriers' is selected. When a junction feature is identified, you may be prompted to select a terminal. When an edge feature is identified, the distance from the tapped location to the beginning of the edge feature will be computed. Select the type of trace using the drop down menu. Click 'Trace' to initiate a trace on the network. Click 'Reset' to clear the trace parameters and start over.
How it works
- Create a
MapViewand identify the featureonSingleTapevent. - Create and load an
ArcGISMapwith a web map item URL that contains anUtilityNetwork. - Get and load the first
UtilityNetworkfrom the web map. - Get and load the
ServiceGeodatabasefrom the utility network and fetch the lineFeatureLayerfrom theServiceGeodatabase's tables. - Add a
GraphicsOverlaywith symbology that distinguishes starting locations from barriers. - Identify features on the map and add a
Graphicthat represents its purpose (starting location or barrier) at the tapped location. - Create a
UtilityElementfor the identified feature. - Determine the type of this element using its
UtilityNetworkSourceTypeproperty. - If the element is a junction with more than one terminal, display a terminal picker. Then set the junction's
UtilityTerminalproperty with the selected terminal. - If an edge, set its
FractionAlongEdgeproperty usingGeometryEngine.FractionAlong. - Add this
UtilityElementto a collection of starting locations or barriers. - Create
UtilityTraceParameterswith the selected trace type along with the collected starting locations and barriers (if applicable). - Set the
UtilityTraceParameters.TraceConfigurationwith the tier'sUtilityTier.getDefaultTraceConfiguration()result. - Run a
UtilityNetwork.trace()with the specified parameters. - For every
FeatureLayerin the map, select the features returned with elements matching theirUtilityNetworkSource.FeatureTablewith the layer'sFeatureTable.
Relevant API
- FractionAlong
- UtilityAssetType
- UtilityDomainNetwork
- UtilityElement
- UtilityElementTraceResult
- UtilityNetwork
- UtilityNetworkDefinition
- UtilityNetworkSource
- UtilityTerminal
- UtilityTier
- UtilityTraceConfiguration
- UtilityTraceParameters
- UtilityTraceResult
- UtilityTraceType
- UtilityTraversability
About the data
The Naperville Electric Map web map contains a utility network used to run the subnetwork-based trace shown in this sample. Authentication is required and handled within the sample code.
Additional information
Using utility network on ArcGIS Enterprise 10.8 requires an ArcGIS Enterprise member account licensed with the Utility Network user type extension. Please refer to the utility network services documentation.
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable MapView. Use the UtilityNetworkTrace tool to help configure, run, and visualize UtilityNetworkTraces on a composable MapView.
Tags
condition barriers, downstream trace, geoview-compose, network analysis, subnetwork trace, toolkit, trace configuration, traversability, upstream trace, utility network, validate consistency
Sample Code
/* Copyright 2025 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. * */ package com.esri.arcgismaps.sample.traceutilitynetwork.components import android.app.Application import android.widget.Toast import androidx.compose.ui.unit.dp import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.Color import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.data.QueryParameters import com.arcgismaps.geometry.Geometry import com.arcgismaps.geometry.GeometryEngine import com.arcgismaps.geometry.Point import com.arcgismaps.geometry.Polyline import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallengeHandler import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallengeResponse import com.arcgismaps.httpcore.authentication.TokenCredential import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.Viewpoint import com.arcgismaps.mapping.layers.FeatureLayer import com.arcgismaps.mapping.layers.SelectionMode import com.arcgismaps.mapping.symbology.SimpleLineSymbol import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle import com.arcgismaps.mapping.symbology.SimpleMarkerSymbol import com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyle import com.arcgismaps.mapping.symbology.UniqueValue import com.arcgismaps.mapping.symbology.UniqueValueRenderer import com.arcgismaps.mapping.view.Graphic import com.arcgismaps.mapping.view.GraphicsOverlay import com.arcgismaps.mapping.view.IdentifyLayerResult import com.arcgismaps.mapping.view.ScreenCoordinate import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy import com.arcgismaps.utilitynetworks.UtilityElement import com.arcgismaps.utilitynetworks.UtilityElementTraceResult import com.arcgismaps.utilitynetworks.UtilityNetwork import com.arcgismaps.utilitynetworks.UtilityNetworkSource import com.arcgismaps.utilitynetworks.UtilityNetworkSourceType import com.arcgismaps.utilitynetworks.UtilityTerminal import com.arcgismaps.utilitynetworks.UtilityTier import com.arcgismaps.utilitynetworks.UtilityTraceParameters import com.arcgismaps.utilitynetworks.UtilityTraceType import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.math.roundToLong class TraceUtilityNetworkViewModel(application: Application) : AndroidViewModel(application) { // The textual hint shown to the user private val _hint = MutableStateFlow<String?>(null) val hint = _hint.asStateFlow() // Is trace utility network enabled private val _canTrace = MutableStateFlow(false) val canTrace = _canTrace.asStateFlow() // The trace state used for the sample private val _traceState = MutableStateFlow(TraceState.ADD_STARTING_POINT) val traceState = _traceState.asStateFlow() // Currently selected utility trace type private val _selectedTraceType = MutableStateFlow<UtilityTraceType>(UtilityTraceType.Connected) val selectedTraceType = _selectedTraceType.asStateFlow() // Currently selected point type (start/barrier) private val _selectedPointType = MutableStateFlow(PointType.Start) val selectedPointType = _selectedPointType.asStateFlow() // Terminal configuration options (high/low) private val _terminalConfigurationOptions = MutableStateFlow<List<UtilityTerminal>>(listOf()) val terminalConfigurationOptions = _terminalConfigurationOptions.asStateFlow() // Currently selected terminal configuration private var _selectedTerminalConfigurationIndex = MutableStateFlow<Int?>(null) // ArcGISMap holding the UtilityNetwork and operational layers val arcGISMap = ArcGISMap("https://sampleserver7.arcgisonline.com/portal/home/item.html?id=be0e4637620a453584118107931f718b") // Used to handle map view animations val mapViewProxy = MapViewProxy() // The utility network used for tracing private var utilityNetwork: UtilityNetwork? = null // The medium voltage tier used for the electric distribution domain network private var mediumVoltageTier: UtilityTier? = null // Create lists for starting locations and barriers private val utilityElementStartingLocations: MutableList<UtilityElement> = mutableListOf() private val utilityElementBarriers: MutableList<UtilityElement> = mutableListOf() // Graphics overlay for the starting locations and barrier graphics val graphicsOverlay = GraphicsOverlay() // Create symbols for the starting point and barriers private val startingPointSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Cross, color = Color.green, size = 25f ) private val barrierPointSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.X, color = Color.red, size = 25f ) // Add custom unique renderer values for the electrical distribution layer private val electricalDistributionUniqueValueRenderer = UniqueValueRenderer( fieldNames = listOf("ASSETGROUP"), uniqueValues = listOf( UniqueValue( description = "Low voltage", label = "", symbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Dash, color = Color.green, width = 3f ), values = listOf(3) ), UniqueValue( description = "Medium voltage", label = "", symbol = SimpleLineSymbol( style = SimpleLineSymbolStyle.Solid, color = Color.green, width = 3f ), values = listOf(5) ) ) ) /** * Returns a [ArcGISAuthenticationChallengeHandler] to access the utility network URL. */ private fun getAuthenticationChallengeHandler(): ArcGISAuthenticationChallengeHandler { return ArcGISAuthenticationChallengeHandler { challenge -> val result: Result<TokenCredential> = runBlocking { TokenCredential.create(challenge.requestUrl, "viewer01", "I68VGU^nMurF", 0) } if (result.getOrNull() != null) { val credential = result.getOrNull() return@ArcGISAuthenticationChallengeHandler ArcGISAuthenticationChallengeResponse.ContinueWithCredential( credential!! ) } else { val ex = result.exceptionOrNull() return@ArcGISAuthenticationChallengeHandler ArcGISAuthenticationChallengeResponse.ContinueAndFailWithError( ex!! ) } } } /** * Initializes view model by adding credentials, loading map and utility network, * and electrical device and distribution feature layers. */ suspend fun initializeTraceViewModel() { ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = getAuthenticationChallengeHandler() // Load the map arcGISMap.load().onSuccess { // The utility network used for tracing utilityNetwork = arcGISMap.utilityNetworks.first() utilityNetwork?.let { utilityNetwork -> // Load the utility network utilityNetwork.load().onSuccess { // Get the service geodatabase from the utility network val serviceGeodatabase = utilityNetwork.serviceGeodatabase // Use the ElectricDistribution domain network val electricDistribution = utilityNetwork.definition?.getDomainNetwork("ElectricDistribution") // Use the Medium Voltage Tier mediumVoltageTier = electricDistribution?.getTier("Medium Voltage Radial") (serviceGeodatabase?.getTable(3)?.layer as FeatureLayer).apply { // Customize rendering for the layer renderer = electricalDistributionUniqueValueRenderer } // Update hint values to reflect trace stage changes viewModelScope.launch { _traceState.collect { updateHint(it) } } } } } } /** * Performs an identify operation to obtain the [ArcGISFeature] nearest to the * tapped [screenCoordinate]. The selected feature is then used to [identifyUtilityElement]. */ fun identifyNearestArcGISFeature( mapPoint: Point, screenCoordinate: ScreenCoordinate ) { viewModelScope.launch { // Identify the feature on the tapped location val identifyResults: List<IdentifyLayerResult> = mapViewProxy.identifyLayers( screenCoordinate = screenCoordinate, tolerance = 4.dp, returnPopupsOnly = false, maximumResults = 1 ).getOrElse { return@launch messageDialogVM.showMessageDialog( title = it.message.toString(), description = it.cause.toString() ) } // If the identify returns a result, retrieve the geoelement as an ArcGISFeature identifyResults.firstOrNull()?.geoElements?.firstOrNull()?.let { identifiedFeature -> (identifiedFeature as? ArcGISFeature)?.let { arcGISFeature -> // Identify the utility element associated with the selected feature identifyUtilityElement( identifiedFeature = arcGISFeature, mapPoint = mapPoint ) } } } } /** * Uses the [mapPoint] to identify any utility elements in the utility network. * Based on the [UtilityNetworkSourceType] create an element for a junction or an edge. */ private fun identifyUtilityElement( identifiedFeature: ArcGISFeature, mapPoint: Point ) { // Get the network source of the identified feature val utilityNetworkSource = utilityNetwork?.definition?.networkSources?.value?.firstOrNull { it.featureTable.tableName == identifiedFeature.featureTable?.tableName } ?: return handleError("Selected feature does not contain a Utility Network Source.") // Check if the network source is a junction or an edge when (utilityNetworkSource.sourceType) { UtilityNetworkSourceType.Junction -> { // Create a junction element with the identified feature createJunctionUtilityElement( identifiedFeature = identifiedFeature, utilityNetworkSource = utilityNetworkSource ) } UtilityNetworkSourceType.Edge -> { // Create an edge element with the identified feature createEdgeUtilityElement( identifiedFeature = identifiedFeature, mapPoint = mapPoint ) } } } /** * Create a [UtilityElement] of the [identifiedFeature]. */ private fun createJunctionUtilityElement( identifiedFeature: ArcGISFeature, utilityNetworkSource: UtilityNetworkSource ) { // Find the code matching the asset group name in the feature's attributes val assetGroupCode = identifiedFeature.attributes["assetgroup"] as Int // Find the network source's asset group with the matching code utilityNetworkSource.assetGroups.first { it.code == assetGroupCode }.assetTypes // Find the asset group type code matching the feature's asset type code .first { it.code == identifiedFeature.attributes["assettype"].toString().toInt() }.let { utilityAssetType -> // Get the list of terminals for the feature val terminals = utilityAssetType.terminalConfiguration?.terminals ?: return handleError("Error retrieving terminal configuration") // If there is only one terminal, use it to create a utility element when (terminals.size) { 1 -> { // Create a utility element utilityNetwork?.createElementOrNull( arcGISFeature = identifiedFeature, terminal = terminals.first() )?.let { utilityElement -> // Add the utility element to the map addUtilityElementToMap( identifiedFeature = identifiedFeature, mapPoint = identifiedFeature.geometry as Point, utilityElement = utilityElement ) } } // If there is more than one terminal, prompt the user to select one else -> { // Reset the index, as the user would need to make a choice _selectedTerminalConfigurationIndex.value = null // Get a list of terminal names from the terminal configuration val terminalConfiguration = utilityAssetType.terminalConfiguration ?: return // Update the list of available terminal options _terminalConfigurationOptions.value = terminalConfiguration.terminals // Show the dialog to choose a terminal configuration _traceState.value = TraceState.TERMINAL_CONFIGURATION_REQUIRED viewModelScope.launch { _selectedTerminalConfigurationIndex.collect { selectedIndex -> if (selectedIndex != null) { // Create a utility element val element = utilityNetwork?.createElementOrNull( arcGISFeature = identifiedFeature, terminal = terminals[selectedIndex] ) ?: return@collect handleError( "Error creating utility element" ) // Add the utility element graphic to the map addUtilityElementToMap( identifiedFeature = identifiedFeature, mapPoint = identifiedFeature.geometry as Point, utilityElement = element ) // Dismiss the dialog to choose another point _traceState.value = TraceState.ADD_STARTING_POINT } } } } } } } /** * Create a [UtilityElement] of the [identifiedFeature]. */ private fun createEdgeUtilityElement( identifiedFeature: ArcGISFeature, mapPoint: Point ) { // Create a utility element with the identified feature val element = (utilityNetwork?.createElementOrNull( arcGISFeature = identifiedFeature, terminal = null ) ?: return handleError("Error creating element")) // Calculate the fraction along these the map point is located element.fractionAlongEdge = GeometryEngine.fractionAlong( line = GeometryEngine.createWithZ( geometry = identifiedFeature.geometry!!, z = null // Remove the z-coordinate value from the identified geometry ) as Polyline, point = mapPoint, tolerance = -1.0 ).roundToThreeDecimals() // Add the utility element graphic to the map addUtilityElementToMap( identifiedFeature = identifiedFeature, mapPoint = mapPoint, utilityElement = element ) // Update the hint text updateHint("Fraction along the edge: ${element.fractionAlongEdge}") } /** * Add [utilityElement] to either the starting locations or barriers list * and add a graphic representing it to the [graphicsOverlay]. */ private fun addUtilityElementToMap( identifiedFeature: ArcGISFeature, mapPoint: Point, utilityElement: UtilityElement ) { graphicsOverlay.graphics.add( Graphic( geometry = GeometryEngine.nearestCoordinate( geometry = identifiedFeature.geometry!!, point = mapPoint )?.coordinate ).apply { // Add the element to the appropriate list (starting locations or barriers), // and add the appropriate symbol to the graphic when (_selectedPointType.value) { PointType.Start -> { utilityElementStartingLocations.add(utilityElement) symbol = startingPointSymbol _canTrace.value = true } PointType.Barrier -> { utilityElementBarriers.add(utilityElement) symbol = barrierPointSymbol } } }) } /** * Uses the elements selected as starting locations and (optionally) barriers * to perform a connected trace, then selects all connected elements * found in the trace to highlight them. */ fun traceUtilityNetwork() { // Check that the utility trace parameters are valid if (utilityElementStartingLocations.isEmpty()) { return handleError("No starting locations provided for trace.") } val traceType = _selectedTraceType.value // Create utility trace parameters for the given trace type val traceParameters = UtilityTraceParameters( traceType = traceType, startingLocations = utilityElementStartingLocations ).apply { // If any barriers have been created, add them to the parameters barriers.addAll(utilityElementBarriers) // Set the trace configuration using the tier from the utility domain network traceConfiguration = mediumVoltageTier?.getDefaultTraceConfiguration() } // Run the utility trace and get the results viewModelScope.launch { // Update the trace state _traceState.value = TraceState.RUNNING_TRACE_UTILITY_NETWORK // Perform the trace with the above parameters, and obtain the results list val traceResults = utilityNetwork?.trace(traceParameters)?.getOrElse { return@launch handleError( title = "Error performing trace", description = it.message.toString() ) } // Get the utility trace result's first result as a utility element trace result (traceResults?.first() as? UtilityElementTraceResult)?.let { utilityElementTraceResult -> // Ensure the result is not empty if (utilityElementTraceResult.elements.isEmpty()) return@launch handleError("No elements found in the trace result") arcGISMap.operationalLayers.filterIsInstance<FeatureLayer>().forEach { featureLayer -> // Clear previous selection featureLayer.clearSelection() val params = QueryParameters().apply { returnGeometry = true // Used to calculate the viewpoint result } // Create query parameters to find features who's network source name matches the layer's feature table name utilityElementTraceResult.elements.filter { it.networkSource.name == featureLayer.featureTable?.tableName }.forEach { utilityElement -> params.objectIds.add(utilityElement.objectId) } // Check if any trace results were added from the above filter if (params.objectIds.isNotEmpty()) { // Select features that match the query val featureQueryResult = featureLayer.selectFeatures( parameters = params, mode = SelectionMode.New ).getOrElse { return@launch handleError( title = it.message.toString(), description = it.cause.toString() ) } // Create list of all the feature result geometries val resultGeometryList = mutableListOf<Geometry>() featureQueryResult.iterator().forEach { feature -> feature.geometry?.let { resultGeometryList.add(it) } } // Obtain the union geometry of all the feature geometries GeometryEngine.unionOrNull(resultGeometryList)?.let { unionGeometry -> // Set the map's viewpoint to the union result geometry mapViewProxy.setViewpointAnimated(Viewpoint(boundingGeometry = unionGeometry)) } } else { Toast.makeText( getApplication(), "Trace result found 0 elements", Toast.LENGTH_SHORT ).show() } } // Update the trace state _traceState.value = TraceState.TRACE_COMPLETED } } } /** * Resets the trace, removing graphics and clearing selections. */ fun reset() { arcGISMap.operationalLayers.filterIsInstance<FeatureLayer>().forEach { it.clearSelection() } utilityElementBarriers.clear() utilityElementStartingLocations.clear() graphicsOverlay.graphics.clear() _traceState.value = TraceState.ADD_STARTING_POINT _canTrace.value = false _selectedTraceType.value = UtilityTraceType.Connected _selectedTerminalConfigurationIndex.value = null _selectedPointType.value = PointType.Start _terminalConfigurationOptions.value = listOf() } /** * Update the [utilityTraceType] selected by the user */ fun updateTraceType(utilityTraceType: UtilityTraceType) { _selectedTraceType.value = utilityTraceType _traceState.value = TraceState.ADD_STARTING_POINT } /** * Switch from adding .start points to adding .barrier, or vice versa. */ fun updatePointType(pointType: PointType) { _selectedPointType.value = pointType when (pointType) { PointType.Start -> { _traceState.value = TraceState.ADD_STARTING_POINT } PointType.Barrier -> { _traceState.value = TraceState.ADD_BARRIER_POINT } } } /** * Update the index used to select the [terminalConfigurationOptions] */ fun updateTerminalConfigurationOption(index: Int) { _selectedTerminalConfigurationIndex.value = index } /** * Update the hint flow to display new [message]. */ private fun updateHint(message: String) { _hint.value = message } // Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel() private fun handleError(title: String, description: String = "") { reset() _traceState.value = TraceState.TRACE_FAILED messageDialogVM.showMessageDialog(title, description) } private fun Double.roundToThreeDecimals(): Double { return (this * 1000.0).roundToLong() / 1000.0 } } enum class PointType { Start, Barrier } object TraceState { const val ADD_STARTING_POINT = "Tap on map to add a stating location point(s)" const val ADD_BARRIER_POINT = "Tap on map to add a barrier point(s)" const val TERMINAL_CONFIGURATION_REQUIRED = "Select Terminal Configuration" const val RUNNING_TRACE_UTILITY_NETWORK = "Evaluating trace utility network" const val TRACE_COMPLETED = "Trace completed" const val TRACE_FAILED = "Fail to run trace" }