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
MapView
and identify the featureonSingleTap
event. - Create and load an
ArcGISMap
with a web map item URL that contains anUtilityNetwork
. - Get and load the first
UtilityNetwork
from the web map. - Get and load the
ServiceGeodatabase
from the utility network and fetch the lineFeatureLayer
from theServiceGeodatabase
's tables. - Add a
GraphicsOverlay
with symbology that distinguishes starting locations from barriers. - Identify features on the map and add a
Graphic
that represents its purpose (starting location or barrier) at the tapped location. - Create a
UtilityElement
for the identified feature. - Determine the type of this element using its
UtilityNetworkSourceType
property. - If the element is a junction with more than one terminal, display a terminal picker. Then set the junction's
UtilityTerminal
property with the selected terminal. - If an edge, set its
FractionAlongEdge
property usingGeometryEngine.FractionAlong
. - Add this
UtilityElement
to a collection of starting locations or barriers. - Create
UtilityTraceParameters
with the selected trace type along with the collected starting locations and barriers (if applicable). - Set the
UtilityTraceParameters.TraceConfiguration
with the tier'sUtilityTier.getDefaultTraceConfiguration()
result. - Run a
UtilityNetwork.trace()
with the specified parameters. - For every
FeatureLayer
in the map, select the features returned with elements matching theirUtilityNetworkSource.FeatureTable
with 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"
}