Trace utility network

View on GitHubSample viewer app

Discover connected features in a utility network using connected, subnetwork, upstream, and downstream traces.

Image of trace utility network

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

  1. Create a MapView and identify the feature onSingleTap event.
  2. Create an ArcGISMap that contains FeatureLayer(s) created from the ServiceFeatureTable's.
  3. Create and load a UtilityNetwork with the same feature service URL on the ArcGISMap.
  4. Add a GraphicsOverlay with symbology that distinguishes starting locations from barriers.
  5. Identify features on the map and add a Graphic that represents its purpose (starting location or barrier) at the tapped location.
  6. Create a UtilityElement for the identified feature.
  7. Determine the type of this element using its UtilityNetworkSourceType property.
  8. 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.
  9. If an edge, set its FractionAlongEdge property using GeometryEngine.FractionAlong.
  10. Add this UtilityElement to a collection of starting locations or barriers.
  11. Create UtilityTraceParameters with the selected trace type along with the collected starting locations and barriers (if applicable).
  12. Set the UtilityTraceParameters.TraceConfiguration with the tier's UtilityTier.getDefaultTraceConfiguration() result.
  13. Run a UtilityNetwork.trace() with the specified parameters.
  14. For every FeatureLayer in the map, select the features returned with elements matching their UtilityNetworkSource.FeatureTable with the layer's FeatureTable.

Relevant API

  • FractionAlong
  • UtilityAssetType
  • UtilityDomainNetwork
  • UtilityElement
  • UtilityElementTraceResult
  • UtilityNetwork
  • UtilityNetworkDefinition
  • UtilityNetworkSource
  • UtilityTerminal
  • UtilityTier
  • UtilityTraceConfiguration
  • UtilityTraceParameters
  • UtilityTraceResult
  • UtilityTraceType
  • UtilityTraversability

About the data

The Naperville electrical network feature service 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.

A UtilityNetworkTrace toolkit component can be used for various utility network related use cases. For information about setting up the toolkit, as well as code for the underlying component, visit the toolkit repository.

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

TraceUtilityNetworkViewModel.ktTraceUtilityNetworkViewModel.ktMainActivity.ktTraceUtilityNetworkScreen.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
/* 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.data.ServiceFeatureTable
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.TokenCredential
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.PortalItem
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.portal.Portal
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.arcgismaps.utilitynetworks.UtilityDomainNetwork
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

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(
        item = PortalItem(
            portal = Portal.arcGISOnline(connection = Portal.Connection.Authenticated),
            itemId = NAPERVILLE_ELECTRICAL_NETWORK_ITEM_ID
        )
    ).apply {
        // Add the map with streets night vector basemap
        setBasemap(Basemap(BasemapStyle.ArcGISStreetsNight))
    }

    // Used to handle map view animations
    val mapViewProxy = MapViewProxy()

    // The utility network used for tracing.
    private val utilityNetwork: UtilityNetwork
        get() = arcGISMap.utilityNetworks.first()

    // Use the ElectricDistribution domain network
    private val electricDistribution: UtilityDomainNetwork?
        get() = utilityNetwork.definition?.getDomainNetwork("ElectricDistribution")

    // Use the Medium Voltage Tier
    private val mediumVoltageTier: UtilityTier?
        get() = electricDistribution?.getTier("Medium Voltage Radial")

    // 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.cyan,
                    width = 3f
                ),
                values = listOf(3)
            ),
            UniqueValue(
                description = "Medium voltage",
                label = "",
                symbol = SimpleLineSymbol(
                    style = SimpleLineSymbolStyle.Solid,
                    color = Color.cyan,
                    width = 3f
                ),
                values = listOf(5)
            )
        )
    )

    /**
     * Initializes view model by adding credentials, loading map and utility network,
     * and electrical device and distribution feature layers.
     */
    suspend fun initializeTrace() {
        // A licensed user is required to perform utility network operations
        TokenCredential.create(
            url = SAMPLE_PORTAL_URL,
            username = USERNAME,
            password = PASSWORD
        ).onSuccess { tokenCredential ->
            // Add the loaded token credential to the app session's authenticationManager
            ArcGISEnvironment.authenticationManager.arcGISCredentialStore.add(tokenCredential)
            // Load the Naperville electric web-map
            arcGISMap.load().getOrElse {
                handleError(
                    title = "Error loading the web-map: ${it.message}",
                    description = it.cause.toString()
                )
            }
            // Load the utility network associated with the web-map
            utilityNetwork.load().getOrElse {
                handleError(
                    title = "Error loading the utility network: ${it.message}",
                    description = it.cause.toString()
                )
            }

            // Once loaded, remove all operational layers
            arcGISMap.operationalLayers.clear()

            // Create the two service feature table to be added as layers
            val electricalDeviceTable = ServiceFeatureTable("$FEATURE_SERVICE_URL/0")
            val electricalDistributionTable = ServiceFeatureTable("$FEATURE_SERVICE_URL/3")

            // Create feature layers from the service feature tables.
            val electricalDeviceFeatureLayer = FeatureLayer.createWithFeatureTable(
                featureTable = electricalDeviceTable
            )
            val electricalDistributionFeatureLayer = FeatureLayer.createWithFeatureTable(
                featureTable = electricalDistributionTable
            ).apply {
                // Customize rendering for the layer
                renderer = electricalDistributionUniqueValueRenderer
            }

            // Add the two feature layers to the map's operational layers
            arcGISMap.operationalLayers.addAll(
                listOf(
                    electricalDistributionFeatureLayer,
                    electricalDeviceFeatureLayer
                )
            )

            // Update hint values to reflect trace stage changes
            viewModelScope.launch {
                _traceState.collect { updateHint(it) }
            }
        }.onFailure {
            handleError(
                title = "Error using TokenCredential: ${it.message}",
                description = it.cause.toString()
            )
        }
    }

    /**
     * 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)
    }

    companion object {
        // Public credentials for the data in this sample.
        const val USERNAME = "viewer01"
        const val PASSWORD = "I68VGU^nMurF"

        // The portal item ID for Naperville’s electrical network
        private const val NAPERVILLE_ELECTRICAL_NETWORK_ITEM_ID = "471eb0bf37074b1fbb972b1da70fb310"
        private const val SAMPLE_SERVER_7 = "https://sampleserver7.arcgisonline.com"
        private const val SAMPLE_PORTAL_URL = "$SAMPLE_SERVER_7/portal/sharing/rest"
        private const val FEATURE_SERVICE_URL =
            "$SAMPLE_SERVER_7/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer"
    }

    private fun Double.roundToThreeDecimals(): Double {
        return Math.round(this * 1000.0) / 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"
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.