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 "Start" or "Barrier" 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. Tap "Type" to select the type of trace using the action sheet. Tap "Trace" to initiate a trace on the network. Tap "Reset" to clear the trace parameters and start over.

How it works

  1. Create an AGSMapView and listen for didTap events on the AGSGeoViewTouchDelegate.
  2. Create and load an AGSServiceGeodatabase with a feature service URL and get tables with their layer IDs.
  3. Create an AGSMap object that contains AGSFeatureLayer(s) created from the service geodatabase's tables.
  4. Create and load an AGSUtilityNetwork with the same feature service URL and map.
  5. Add an AGSGraphicsOverlay with symbology that distinguishes starting locations from barriers.
  6. Identify tapped features on the map and add an AGSGraphic that represents its purpose (starting point or barrier) at the tapped location.
  7. Create an AGSUtilityElement for the identified feature.
  8. Determine the type of the identified feature using AGSUtilityNetworkSource.sourceType.
  9. If the type is junction, display a terminal picker when more than one terminal is found and create an AGSUtilityElement using the selected terminal, or the single terminal if there is only one.
  10. If the type is edge, create an AGSUtilityElement from the identified feature and compute how far along the edge the user tapped using class AGSGeometryEngine.fraction(alongLine:to:tolerance:).
  11. Add this AGSUtilityElement to a collection of starting locations or barriers.
  12. Create AGSUtilityTraceParameters with the selected trace type along with the collected starting locations and barriers (if applicable).
  13. Set the AGSUtilityTraceConfiguration with the utility tier's makeDefaultTraceConfiguration method.
  14. Run AGSUtilityNetwork.trace(with:completion:) with the specified starting points and (optionally) barriers.
  15. Group the AGSUtilityElementTraceResult.elements by their networkSource.name.
  16. For every AGSFeatureLayer in this map with trace result elements, select features by converting AGSUtilityElement(s) to AGSArcGISFeature(s) using AGSUtilityNetwork.features(for:completion:).

Relevant API

  • AGSServiceGeodatabase
  • AGSUtilityAssetType
  • AGSUtilityDomainNetwork
  • AGSUtilityElement
  • AGSUtilityElementTraceResult
  • AGSUtilityNetwork
  • AGSUtilityNetworkDefinition
  • AGSUtilityNetworkSource
  • AGSUtilityTerminal
  • AGSUtilityTier
  • AGSUtilityTraceConfiguration
  • AGSUtilityTraceParameters
  • AGSUtilityTraceResult
  • AGSUtilityTraceType
  • AGSUtilityTraversability
  • class AGSGeometryEngine.fraction(alongLine:to:tolerance:)

About the data

The Naperville electrical network feature service, hosted on ArcGIS Online, contains a utility network used to run the subnetwork-based trace shown in this sample.

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

condition barriers, downstream trace, network analysis, subnetwork trace, trace configuration, traversability, upstream trace, utility network, validate consistency

Sample Code

TraceUtilityNetworkViewController.swift
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
// Copyright 2019 Esri.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit
import ArcGIS

class TraceUtilityNetworkViewController: UIViewController, AGSGeoViewTouchDelegate {
    @IBOutlet weak var mapView: AGSMapView!

    @IBOutlet weak var traceNetworkButton: UIBarButtonItem!
    @IBOutlet weak var resetButton: UIBarButtonItem!
    @IBOutlet weak var typeButton: UIBarButtonItem!
    @IBOutlet weak var modeLabel: UILabel!
    @IBOutlet weak var statusLabel: UILabel!
    @IBOutlet weak var modeControl: UISegmentedControl!

    private static let featureServiceURL = URL(string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleElectric/FeatureServer")!

    private let map: AGSMap
    private let serviceGeodatabase = AGSServiceGeodatabase(url: featureServiceURL)
    private let utilityNetwork = AGSUtilityNetwork(url: featureServiceURL)
    private var utilityTier: AGSUtilityTier?
    private var traceType = (name: "Connected", type: AGSUtilityTraceType.connected)
    private var traceParameters = AGSUtilityTraceParameters(traceType: .connected, startingLocations: [])
    // Create electrical distribution line layer ./3 and electrical device layer ./0.
    private let featureLayerURLs = [
        featureServiceURL.appendingPathComponent("3"),
        featureServiceURL.appendingPathComponent("0")
    ]

    private let parametersOverlay: AGSGraphicsOverlay = {
        let barrierPointSymbol = AGSSimpleMarkerSymbol(style: .X, color: .red, size: 20)
        let barrierUniqueValue = AGSUniqueValue(
            description: "Barriers",
            label: InteractionMode.addingBarriers.toString(),
            symbol: barrierPointSymbol,
            values: [InteractionMode.addingBarriers.traceLocationType])
        let startingPointSymbol = AGSSimpleMarkerSymbol(style: .cross, color: .green, size: 20)
        let renderer = AGSUniqueValueRenderer(
            fieldNames: ["TraceLocationType"],
            uniqueValues: [barrierUniqueValue],
            defaultLabel: InteractionMode.addingStartLocation.toString(),
            defaultSymbol: startingPointSymbol)
        let overlay = AGSGraphicsOverlay()
        overlay.renderer = renderer

        return overlay
    }()

    // MARK: Initialize map, utility network, and service geodatabase
    required init?(coder aDecoder: NSCoder) {
        // Create the map
        map = AGSMap(basemapStyle: .arcGISStreetsNight)

        // Add the utility network to the map's array of utility networks.
        map.utilityNetworks.add(utilityNetwork)
        // NOTE: Never hardcode login information in a production application. This is done solely for the sake of the sample.
        utilityNetwork.credential = AGSCredential(user: "viewer01", password: "I68VGU^nMurF")
        super.init(coder: aDecoder)
        // Load the service geodatabase.
        serviceGeodatabase.load { [weak self] _ in
            guard let self = self else { return }
            let layers = self.featureLayerURLs.map { url -> AGSFeatureLayer in
                let featureTable = AGSServiceFeatureTable(url: url)
                let layer = AGSFeatureLayer(featureTable: featureTable)
                if featureTable.serviceLayerID == 3 {
                    // Define a solid line for medium voltage lines and a dashed line for low voltage lines.
                    let darkCyan = UIColor(red: 0, green: 0.55, blue: 0.55, alpha: 1)
                    let mediumVoltageValue = AGSUniqueValue(
                        description: "N/A",
                        label: "Medium voltage",
                        symbol: AGSSimpleLineSymbol(style: .solid, color: darkCyan, width: 3),
                        values: [5]
                    )
                    let lowVoltageValue = AGSUniqueValue(
                        description: "N/A",
                        label: "Low voltage",
                        symbol: AGSSimpleLineSymbol(style: .dash, color: darkCyan, width: 3),
                        values: [3]
                    )
                    layer.renderer = AGSUniqueValueRenderer(
                        fieldNames: ["ASSETGROUP"],
                        uniqueValues: [mediumVoltageValue, lowVoltageValue],
                        defaultLabel: "",
                        defaultSymbol: AGSSimpleLineSymbol()
                    )
                }
                return layer
            }
            // Add the utility network feature layers to the map for display.
            self.map.operationalLayers.addObjects(from: layers)
        }
    }

    // MARK: Initialize user interface
    override func viewDidLoad() {
        super.viewDidLoad()

        // add the source code button item to the right of navigation bar
        (self.navigationItem.rightBarButtonItem as! SourceCodeBarButtonItem).filenames = ["TraceUtilityNetworkViewController"]

        // Initialize the UI
        setUIState()

        // Set up the map view
        mapView.map = map
        let extent = AGSEnvelope(
            xMin: -9813547.35557238,
            yMin: 5129980.36635111,
            xMax: -9813185.0602376,
            yMax: 5130215.41254146,
            spatialReference: .webMercator()
        )
        mapView.setViewpoint(AGSViewpoint(targetExtent: extent))
        mapView.graphicsOverlays.add(parametersOverlay)
        mapView.touchDelegate = self
        // Set the selection color for features in the map view.
        mapView.selectionProperties = AGSSelectionProperties(color: .yellow)

        // Load the Utility Network to be ready for us to run a trace against it.
        setStatus(message: "Loading Utility Network…")
        utilityNetwork.load { [weak self] error in
            guard let self = self else { return }
            if let error = error {
                self.setStatus(message: "Loading Utility Network failed.")
                self.presentAlert(error: error)
            } else {
                // Update the UI to allow network traces to be run.
                self.setUIState()
                self.setInstructionMessage()
                // Get the utility tier used for traces in this network.
                // For this data set, the "Medium Voltage Radial" tier from the "ElectricDistribution" domain network is used.
                let domainNetwork = self.utilityNetwork.definition.domainNetwork(withDomainNetworkName: "ElectricDistribution")
                self.utilityTier = domainNetwork?.tier(withName: "Medium Voltage Radial")
            }
        }
    }

    // MARK: Set trace start points and barriers
    var identifyAction: AGSCancelable?

    func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
        if let identifyAction = identifyAction {
            identifyAction.cancel()
        }

        setStatus(message: "Identifying trace locations…")
        identifyAction = mapView.identifyLayers(atScreenPoint: screenPoint, tolerance: 10, returnPopupsOnly: false) { [weak self] (result, error) in
            guard let self = self else { return }
            if let error = error {
                self.setStatus(message: "Error identifying trace locations.")
                self.presentAlert(error: error)
                return
            }

            guard let feature = result?.first?.geoElements.first as? AGSArcGISFeature else { return }

            self.addStartElementOrBarrier(for: feature, at: mapPoint)
        }
    }

    /// Based on the selection mode, the tapped utility element is added either to the starting locations or barriers for the trace parameters.
    /// An appropriate graphic is created at the tapped location to mark the element as either a starting location or barrier.
    ///
    /// - Parameters:
    ///   - feature: The geoelement retrieved as an `AGSArcGISFeature`.
    ///   - location: The `AGSPoint` used to identify utility elements in the utility network.
    private func addStartElementOrBarrier(for feature: AGSArcGISFeature, at location: AGSPoint) {
        guard let featureTable = feature.featureTable as? AGSArcGISFeatureTable,
              let networkSource = utilityNetwork.definition.networkSource(withName: featureTable.tableName) else {
            self.setStatus(message: "Could not identify location.")
            return
        }

        switch networkSource.sourceType {
        case .junction:
            // If the user tapped on a junction, get the asset's terminal(s).
            if let assetGroupField = featureTable.field(forName: featureTable.subtypeField),
               let assetGroupCode = feature.attributes[assetGroupField.name] as? Int,
               let assetGroup = networkSource.assetGroups.first(where: { $0.code == assetGroupCode }),
               let assetTypeField = featureTable.field(forName: "ASSETTYPE"),
               let assetTypeCode = feature.attributes[assetTypeField.name] as? Int,
               let assetType = assetGroup.assetTypes.first(where: { $0.code == assetTypeCode }),
               let terminals = assetType.terminalConfiguration?.terminals {
                selectTerminal(from: terminals, at: feature.geometry as? AGSPoint ?? location) { [weak self, currentMode] terminal in
                    guard let self = self,
                          let element = self.utilityNetwork.createElement(with: feature, terminal: terminal),
                          let location = feature.geometry as? AGSPoint else { return }

                    self.add(element: element, for: location, mode: currentMode)
                    self.setStatus(message: "terminal: \(terminal.name)")
                }
            }
        case .edge:
            // If the user tapped on an edge, determine how far along that edge.
            if let geometry = feature.geometry,
               let line = AGSGeometryEngine.geometryByRemovingZ(from: geometry) as? AGSPolyline,
               let element = utilityNetwork.createElement(with: feature, terminal: nil) {
                element.fractionAlongEdge = AGSGeometryEngine.fraction(alongLine: line, to: location, tolerance: -1)

                add(element: element, for: location, mode: currentMode)
                setStatus(message: String(format: "fractionAlongEdge: %.3f", element.fractionAlongEdge))
            }
        @unknown default:
            presentAlert(message: "Unexpected Network Source type!")
        }
    }

    private func add(element: AGSUtilityElement, for location: AGSPoint, mode: InteractionMode) {
        switch mode {
        case .addingStartLocation:
            traceParameters.startingLocations.append(element)
        case .addingBarriers:
            traceParameters.barriers.append(element)
        }

        setUIState()

        let traceLocationGraphic = AGSGraphic(geometry: location, symbol: nil, attributes: ["TraceLocationType": mode.traceLocationType])
        parametersOverlay.graphics.add(traceLocationGraphic)
    }

    // MARK: Perform Trace
    @IBAction func traceNetwork(_ sender: Any) {
        UIApplication.shared.showProgressHUD(message: "Running \(traceType.name.lowercased()) trace…")
        let parameters = AGSUtilityTraceParameters(traceType: traceType.type, startingLocations: traceParameters.startingLocations)
        parameters.barriers.append(contentsOf: traceParameters.barriers)

        // Set the trace configuration using the tier from the utility domain network.
        parameters.traceConfiguration = utilityTier?.makeDefaultTraceConfiguration()

        utilityNetwork.trace(with: parameters) { [weak self] (traceResult, error) in
            if let error = error {
                self?.setStatus(message: "Trace failed.")
                UIApplication.shared.hideProgressHUD()
                self?.presentAlert(error: error)
                return
            }

            guard let self = self else { return }

            guard let elementTraceResult = traceResult?.first as? AGSUtilityElementTraceResult,
                  !elementTraceResult.elements.isEmpty else {
                self.setStatus(message: "Trace completed with no output.")
                UIApplication.shared.hideProgressHUD()
                return
            }

            self.clearSelection()

            UIApplication.shared.showProgressHUD(message: "Trace completed. Selecting features…")

            let groupedElements = Dictionary(grouping: elementTraceResult.elements) { $0.networkSource.name }

            let selectionGroup = DispatchGroup()

            for (networkName, elements) in groupedElements {
                guard let layer = self.map.operationalLayers.first(where: { ($0 as? AGSFeatureLayer)?.featureTable?.tableName == networkName }) as? AGSFeatureLayer else { continue }

                selectionGroup.enter()
                self.utilityNetwork.features(for: elements) { [weak self, layer] (features, error) in
                    defer {
                        selectionGroup.leave()
                    }

                    if let error = error {
                        self?.presentAlert(error: error)
                        return
                    }

                    guard let features = features else { return }

                    layer.select(features)
                }
            }

            selectionGroup.notify(queue: .main) { [weak self] in
                self?.setStatus(message: "Trace completed.")
                UIApplication.shared.hideProgressHUD()
            }
        }
    }

    func clearSelection() {
        map.operationalLayers.lazy
            .compactMap { $0 as? AGSFeatureLayer }
            .forEach { $0.clearSelection() }
    }

    // MARK: Terminal Selection UI

    /// Presents an action sheet to select one from multiple terminals, or return if there is only one.
    ///
    /// - Parameters:
    ///   - terminals: An array of terminals.
    ///   - mapPoint: The location tapped on the map.
    ///   - completion: Completion closure to pass the selected terminal.
    private func selectTerminal(from terminals: [AGSUtilityTerminal], at mapPoint: AGSPoint, completion: @escaping (AGSUtilityTerminal) -> Void) {
        if terminals.count > 1 {
            // Show a terminal picker
            let terminalPicker = UIAlertController(title: "Select a terminal.", message: nil, preferredStyle: .actionSheet)

            for terminal in terminals {
                let action = UIAlertAction(title: terminal.name, style: .default) { [terminal] _ in
                    completion(terminal)
                }

                terminalPicker.addAction(action)
            }

            terminalPicker.addAction(UIAlertAction(title: "Cancel", style: .cancel))
            present(terminalPicker, animated: true, completion: nil)

            if let popoverController = terminalPicker.popoverPresentationController {
                // If we're presenting in a split view controller (e.g. on an iPad),
                // provide positioning information for the alert view.
                popoverController.sourceView = mapView
                let tapPoint = mapView.location(toScreen: mapPoint)
                popoverController.sourceRect = CGRect(origin: tapPoint, size: .zero)
            }
        } else if let terminal = terminals.first {
            completion(terminal)
        }
    }

    // MARK: Interaction Mode
    private enum InteractionMode: Int {
        case addingStartLocation = 0
        case addingBarriers = 1

        var traceLocationType: String {
            switch self {
            case .addingStartLocation:
                return "starting point"
            case .addingBarriers:
                return "barrier"
            }
        }

        func toString() -> String {
            switch self {
            case .addingStartLocation:
                return "Start Location"
            case .addingBarriers:
                return "Barrier"
            }
        }
    }

    private var currentMode: InteractionMode = .addingStartLocation {
        didSet {
            setInstructionMessage()
        }
    }

    @IBAction func setMode(_ modePickerControl: UISegmentedControl) {
        if let mode = InteractionMode(rawValue: modePickerControl.selectedSegmentIndex) {
            currentMode = mode
        }
    }

    // MARK: Set trace type
    @IBAction func setTraceType(_ sender: Any) {
        let alertController = UIAlertController(title: "Select a trace type.", message: nil, preferredStyle: .actionSheet)
        let types: [(name: String, type: AGSUtilityTraceType)] = [
            ("Connected", .connected),
            ("Subnetwork", .subnetwork),
            ("Upstream", .upstream),
            ("Downstream", .downstream)
        ]
        types.forEach { (name, type) in
            let action = UIAlertAction(title: name, style: .default) { [unowned self] _ in
                self.traceType = (name, type)
                self.setStatus(message: "Trace type \(name) selected.")
            }
            alertController.addAction(action)
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)
        alertController.popoverPresentationController?.barButtonItem = typeButton
        present(alertController, animated: true)
    }

    // MARK: Reset trace
    @IBAction func reset(_ sender: Any) {
        clearSelection()
        traceParameters.startingLocations.removeAll()
        traceParameters.barriers.removeAll()
        parametersOverlay.graphics.removeAllObjects()
        traceType = (name: "Connected", type: .connected)
        setInstructionMessage()
    }

    // MARK: UI and Feedback
    private func setStatus(message: String) {
        statusLabel.text = message
    }

    func setUIState() {
        let utilityNetworkIsReady = utilityNetwork.loadStatus == .loaded
        modeControl.isEnabled = utilityNetworkIsReady
        modeLabel.isEnabled = modeControl.isEnabled

        let canTrace = utilityNetworkIsReady && !traceParameters.startingLocations.isEmpty
        traceNetworkButton.isEnabled = canTrace
        resetButton.isEnabled = traceNetworkButton.isEnabled
    }

    func setInstructionMessage() {
        setStatus(message: "Tap on the map to add a \(currentMode.toString()).")
    }
}

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