Display content of utility network container

View on GitHub
Sample viewer app

A utility network container allows a dense collection of features to be represented by a single feature, which can be used to reduce map clutter.

Image of display content of utility network container

Use case

Offering a container view for features aids in the review for valid structural attachment and containment relationships and helps determine if a dataset has an association role set. Container views often model a cluster of electrical devices on a pole top or inside a cabinet or vault.

How to use the sample

Tap on a container feature to show all features inside the container. The container is shown as a polygon graphic with the content features contained within. The viewpoint and scale of the map are also changed to the container's extent. Connectivity and attachment associations inside the container are shown as red and blue dotted lines respectively.

How it works

  1. Load a web map that includes ArcGIS Pro Subtype Group Layers with only container features visible (i.e. fuse bank, switch bank, transformer bank, hand hole and junction box).
  2. Add a GraphicsOverlay for displaying a container view.
  3. Create and load a UtilityNetwork with the same feature service URL as the layers in the Map.
  4. Add an event handler for the onTouchListener listener of the MapView.
  5. Identify a feature and create a UtilityElement from it.
  6. Get the associations for this element using GetAssociationsAsync(UtilityElement, UtilityAssociationType.CONTAINMENT).
  7. Turn-off the visibility of all OperationalLayers.
  8. Get the features for the UtilityElement(s) from the associations using fetchFeaturesForElementsAsync(List<UtilityElement>)
  9. Add a Graphic with the same geometry and symbol as these features.
  10. Add another Graphic that represents this extent and zoom to this extent with some buffer.
  11. Get associations for this extent using GetAssociationsAsync(Envelope)
  12. Add a Graphic to represent the association geometry between them using a symbol that distinguishes between Attachment and Connectivity association type.
  13. Turn-on the visibility of all OperationalLayers, clear the Graphics and zoom out to the previous extent to exit the container view.

Relevant API

  • SubtypeFeatureLayer
  • UtilityAssociation
  • UtilityAssociationType
  • UtilityElement
  • UtilityNetwork

About the data

The Naperville electric feature service contains a utility network layer used to find associations shown in this sample. The Naperville electric containers webmap uses the same feature service endpoint and displays only container features. 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.

Tags

associations, connectivity association, containment association, structural attachment associations, utility network

Sample Code

MainActivity.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
/* Copyright 2022 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.arcgisruntime.sample.displaycontentofutilitynetworkcontainer

import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.esri.arcgisruntime.data.ArcGISFeature
import com.esri.arcgisruntime.geometry.Geometry
import com.esri.arcgisruntime.geometry.GeometryEngine
import com.esri.arcgisruntime.geometry.Point
import com.esri.arcgisruntime.layers.SubtypeFeatureLayer
import com.esri.arcgisruntime.loadable.LoadStatus
import com.esri.arcgisruntime.mapping.ArcGISMap
import com.esri.arcgisruntime.mapping.Viewpoint
import com.esri.arcgisruntime.mapping.view.*
import com.esri.arcgisruntime.sample.displaycontentofutilitynetworkcontainer.databinding.ActivityMainBinding
import com.esri.arcgisruntime.sample.displaycontentofutilitynetworkcontainer.databinding.UtilityNetworkLegendBinding
import com.esri.arcgisruntime.security.AuthenticationChallengeHandler
import com.esri.arcgisruntime.security.AuthenticationChallengeResponse
import com.esri.arcgisruntime.security.AuthenticationManager
import com.esri.arcgisruntime.security.UserCredential
import com.esri.arcgisruntime.symbology.SimpleLineSymbol
import com.esri.arcgisruntime.symbology.Symbol
import com.esri.arcgisruntime.utilitynetworks.UtilityAssociation
import com.esri.arcgisruntime.utilitynetworks.UtilityAssociationType
import com.esri.arcgisruntime.utilitynetworks.UtilityElement
import com.esri.arcgisruntime.utilitynetworks.UtilityNetwork

class MainActivity : AppCompatActivity() {

    private val TAG = MainActivity::class.java.simpleName

    private val activityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    private val mapView: MapView by lazy {
        activityMainBinding.mapView
    }

    private val exitButton: Button by lazy {
        activityMainBinding.exitButton
    }

    private val legendButton: Button by lazy {
        activityMainBinding.legendButton
    }

    private val progressBar: ProgressBar by lazy {
        activityMainBinding.progressBar
    }

    // create graphic overlay to display the utility network associations
    private val graphicsOverlay: GraphicsOverlay = GraphicsOverlay()

    // instance of the legend alert dialog
    private var legendDialog: AlertDialog? = null

    // create three new simple line symbols for displaying container view features
    private val boundingBoxSymbol: SimpleLineSymbol =
        SimpleLineSymbol(SimpleLineSymbol.Style.DASH, Color.YELLOW, 3F)
    private val attachmentSymbol: SimpleLineSymbol =
        SimpleLineSymbol(SimpleLineSymbol.Style.DOT, Color.BLUE, 3F)
    private val connectivitySymbol: SimpleLineSymbol =
        SimpleLineSymbol(SimpleLineSymbol.Style.DOT, Color.RED, 3F)

    // the feature service url contains a utility network used to find associations shown in this sample
    private val utilityNetwork: UtilityNetwork =
        UtilityNetwork("https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")

    // use the previous viewpoint when exiting the container view
    private var previousViewpoint: Viewpoint? = null

    // needed to avoid the DefaultMapViewOnTouchListener accessibility warning
    @SuppressLint("ClickableViewAccessibility")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(activityMainBinding.root)

        // set user credentials to authenticate with the feature service and webmap url
        // NOTE: a licensed user is required to perform utility network operations
        // NOTE: Never hardcode login information in a production application. This is done solely for the sake of the sample.
        val authenticationChallengeHandler =
            AuthenticationChallengeHandler {
                AuthenticationChallengeResponse(
                    AuthenticationChallengeResponse.Action.CONTINUE_WITH_CREDENTIAL,
                    UserCredential("viewer01", "I68VGU^nMurF")
                )
            }
        AuthenticationManager.setAuthenticationChallengeHandler(authenticationChallengeHandler)

        // create a new map from the web map URL (includes ArcGIS Pro subtype group layers with only container features visible)
        val map =
            ArcGISMap("https://sampleserver7.arcgisonline.com/portal/home/item.html?id=813eda749a9444e4a9d833a4db19e1c8")
                .apply {
                    // add the utility network to the map's collection of utility networks, and load it
                    utilityNetworks.add(utilityNetwork)
                }

        // loads the features elements and the associations of the utility network
        utilityNetwork.apply {
            addDoneLoadingListener {
                // handle error if the utility network did not load
                if (utilityNetwork.loadStatus != LoadStatus.LOADED) {
                    onError("Error loading the utility network. Check URL used")
                }
            }
            loadAsync()
        }

        mapView.apply {
            // set the map to the MapView and set the MapView's viewpoint
            this.map = map
            setViewpoint(Viewpoint(41.801504, -88.163718, 4e3))
            // hide the progress indicator once the map view is done loading
            map.addDoneLoadingListener {
                if (map.loadStatus == LoadStatus.LOADED) {
                    progressBar.visibility = View.GONE
                } else if (map.loadStatus == LoadStatus.FAILED_TO_LOAD) {
                    progressBar.visibility = View.GONE
                    // handle error if map failed to load
                    map.loadError.message?.let { onError("Map failed to load: $it") }
                }
            }
            // add graphics overlay to display container view contents
            graphicsOverlays.add(graphicsOverlay)
        }

        // handle when map is clicked by retrieving the point
        mapView.onTouchListener =
            object : DefaultMapViewOnTouchListener(this@MainActivity, mapView) {
                override fun onSingleTapConfirmed(event: MotionEvent?): Boolean {
                    if (event != null) {
                        // handle map click only if progressBar is not loading
                        if (progressBar.visibility == View.GONE) {
                            // display the progressBar
                            progressBar.visibility = View.VISIBLE
                            // create a point from where the user clicked
                            val screenPoint =
                                android.graphics.Point(event.x.toInt(), event.y.toInt())
                            // identify and handle the feature of the clicked at point
                            handleMapViewClicked(screenPoint)
                        }
                    }
                    return super.onSingleTapConfirmed(event)
                }
            }
        // set up the attachmentSymbol, connectivitySymbol and
        // the boundingBoxSymbol bitmaps for the legend view
        setUpLegendView()
        // sets the viewpoint, layers, and the graphics when user exits the container view
        exitButton.setOnClickListener {
            handleExitButtonClick()
        }
        // display the legend alert dialog when the button is clicked
        legendButton.setOnClickListener {
            legendDialog?.show()
        }
    }

    /**
     * Called when the user exits the container view.
     * Clears graphics, resets the viewpoint
     * and displays the operationalLayers
     */
    private fun handleExitButtonClick() {
        graphicsOverlay.graphics.clear()
        mapView.setViewpointAsync(previousViewpoint)
        mapView.map.operationalLayers.forEach { layer ->
            layer.isVisible = true
        }
        // exits from the container view
        handleContainerView(false)
    }

    /**
     * Display legend, exit button and disable interaction
     * when container view [isVisible]
     */
    private fun handleContainerView(isVisible: Boolean) {
        if (isVisible) {
            // enable buttons
            exitButton.isEnabled = true
            legendButton.isEnabled = true
            // disable map interactions
            mapView.interactionOptions.apply {
                isPanEnabled = false
                isZoomEnabled = false
                isRotateEnabled = false
            }
        } else {
            // disable button
            exitButton.isEnabled = false
            legendButton.isEnabled = false
            // enable map interactions
            mapView.interactionOptions.apply {
                isPanEnabled = true
                isZoomEnabled = true
                isRotateEnabled = true
            }
        }
    }

    /**
     * Set up the attachmentSymbol, connectivitySymbol and
     * the boundingBoxSymbol bitmaps for the legend view
     */
    private fun setUpLegendView() {
        // inflate the layout and get references to each of its components
        val dialogBinding = UtilityNetworkLegendBinding.inflate(LayoutInflater.from(this))
        // set the image bitmaps using the line symbols to the ImageViews
        dialogBinding.apply {
            attachmentImageView.setImageBitmap(
                attachmentSymbol.createSwatchAsync(0x00000000, 1F).get()
            )
            connectivityImageView.setImageBitmap(
                connectivitySymbol.createSwatchAsync(0x00000000, 1F).get()
            )
            boundingImageView.setImageBitmap(
                boundingBoxSymbol.createSwatchAsync(0x00000000, 1F).get()
            )
        }

        // create the alert dialog and bind it to the inflated layout
        legendDialog = AlertDialog.Builder(this)
            .setView(dialogBinding.root)
            .setTitle("Legend")
            .create()
    }

    /**
     * Identifies the feature of the clicked [mapPoint], gets the selected feature's sublayerResults
     * and retrieves the utility network associations of the selected ArcGISFeature
     */
    private fun handleMapViewClicked(mapPoint: android.graphics.Point) {
        try {
            // identify the feature clicked on
            val identifyLayerResultsFuture = mapView.identifyLayersAsync(mapPoint, 10.0, false)
            identifyLayerResultsFuture.addDoneListener {
                // get the result of the query
                val identifyLayerResults = identifyLayerResultsFuture.get()
                // if no layer identified then return
                if (identifyLayerResults.isEmpty()) {
                    progressBar.visibility = View.GONE
                    return@addDoneListener
                }
                // finds the first layer where the LayerContent is a SubtypeFeatureLayer
                val layerResult = identifyLayerResults.find { layerResult -> layerResult.layerContent is SubtypeFeatureLayer }
                // user clicked on an empty space on map with no feature
                if(layerResult == null){
                    progressBar.visibility = View.GONE
                    return@addDoneListener
                }
                // filter the sublayer result's elements to find the first one which is an ArcGIS feature
                val selectedContainerFeature =
                    layerResult.sublayerResults.first().elements.filterIsInstance<ArcGISFeature>()
                        .first()
                // create a container element using the selected feature
                val containerElement = utilityNetwork.createElement(selectedContainerFeature)
                // get the containment associations from this element to display its content
                val containmentAssociationsFuture = utilityNetwork.getAssociationsAsync(
                    containerElement,
                    UtilityAssociationType.CONTAINMENT
                )
                containmentAssociationsFuture.addDoneListener {
                    // displays the features, symbols and the container element's associations
                    handleContainmentAssociations(
                        containmentAssociationsFuture.get(),
                        containerElement
                    )
                }
            }
        } catch (e: Exception) {
            onError("Error getting result: ${e.message}")
        }
    }

    /**
     * Displays the features, symbols and the container element's associations
     */
    private fun handleContainmentAssociations(
        containmentAssociations: List<UtilityAssociation>,
        containerElement: UtilityElement
    ) {
        // get and store a list of elements from the result of the query
        val contentElements: MutableList<UtilityElement> = mutableListOf()
        // get the list of containment associations and loop through them to get their elements
        containmentAssociations.forEach { association ->
            val utilityElement =
                if (association.fromElement.objectId == containerElement.objectId) association.toElement
                else association.fromElement
            contentElements.add(utilityElement)
        }

        // save the viewpoint and hide the current operationalLayers
        previousViewpoint = mapView.getCurrentViewpoint(Viewpoint.Type.BOUNDING_GEOMETRY)
        mapView.map.operationalLayers.forEach { layer -> layer.isVisible = false }

        // fetch the features from the elements
        val fetchFeaturesFuture = utilityNetwork.fetchFeaturesForElementsAsync(contentElements)
        fetchFeaturesFuture.addDoneListener {
            // get the content features and give them each a symbol, and add them as a graphic to the graphics overlay
            fetchFeaturesFuture.get().forEach { content ->
                val symbol: Symbol =
                    content.featureTable.layerInfo.drawingInfo.renderer.getSymbol(content)
                graphicsOverlay.graphics.add(Graphic(content.geometry, symbol))
            }
            val firstGraphic = graphicsOverlay.graphics[0].geometry
            val containerViewScale = containerElement.assetType.containerViewScale
            if (graphicsOverlay.graphics.size == 1 && firstGraphic is Point) {
                mapView.setViewpointCenterAsync(firstGraphic, containerViewScale)
                    .addDoneListener {
                        // the bounding box, which defines the container view, may be computed using the extent of the features
                        // it contains or centered around its geometry at the container's view scale
                        val boundingBox =
                            mapView.getCurrentViewpoint(Viewpoint.Type.BOUNDING_GEOMETRY).targetGeometry
                        // identifies and displays associations for the specified geometry
                        identifyAssociationsWithExtent(boundingBox)
                        // since size is 1, no associations found
                        Toast.makeText(this, "This feature has no associations", Toast.LENGTH_SHORT)
                            .show()
                    }
            } else {
                // associations found, create a bounding box around them
                val boundingBox: Geometry =
                    GeometryEngine.buffer(graphicsOverlay.extent, 0.05)
                // get associations for the specified geometry and display its associations
                identifyAssociationsWithExtent(boundingBox)
            }
        }
    }

    /**
     * Get associations for the specified geometry and display its associations
     * using [boundingBox] the geometry from which to get associations.
     */
    private fun identifyAssociationsWithExtent(boundingBox: Geometry?) {
        // display container view and hide progressBar
        handleContainerView(true)
        progressBar.visibility = View.GONE
        // adds a graphic representing the bounding box of the associations identified and zooms to its extent
        graphicsOverlay.graphics.add(Graphic(boundingBox, boundingBoxSymbol))
        mapView.setViewpointGeometryAsync(GeometryEngine.buffer(graphicsOverlay.extent, 0.05))
        // get the associations for this extent to display how content features are attached or connected.
        val extentAssociations = utilityNetwork.getAssociationsAsync(graphicsOverlay.extent)
        extentAssociations.addDoneListener {
            try {
                extentAssociations.get().forEach { association ->
                    // assign the appropriate symbol if the association is an attachment or connectivity type
                    val symbol: Symbol =
                        if (association.associationType === UtilityAssociationType.ATTACHMENT) attachmentSymbol
                        else connectivitySymbol
                    // add the symbol for each association in the container
                    graphicsOverlay.graphics.add(Graphic(association.geometry, symbol))
                }
            } catch (e: Exception) {
                onError("Error getting extent associations")
            }
        }
    }

    private fun onError(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
        Log.e(TAG, message)
        progressBar.visibility = View.GONE
    }

    override fun onPause() {
        mapView.pause()
        super.onPause()
    }

    override fun onResume() {
        super.onResume()
        mapView.resume()
    }

    override fun onDestroy() {
        mapView.dispose()
        super.onDestroy()
    }
}

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