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 "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
- Create an
AGSMapView
and listen fordidTap
events on theAGSGeoViewTouchDelegate
. - Create and load an
AGSServiceGeodatabase
with a feature service URL and get tables with their layer IDs. - Create an
AGSMap
object that containsAGSFeatureLayer
(s) created from the service geodatabase's tables. - Create and load an
AGSUtilityNetwork
with the same feature service URL and map. - Add an
AGSGraphicsOverlay
with symbology that distinguishes starting locations from barriers. - Identify tapped features on the map and add an
AGSGraphic
that represents its purpose (starting point or barrier) at the tapped location. - Create an
AGSUtilityElement
for the identified feature. - Determine the type of the identified feature using
AGSUtilityNetworkSource.sourceType
. - If the type is
junction
, display a terminal picker when more than one terminal is found and create anAGSUtilityElement
using the selected terminal, or the single terminal if there is only one. - If the type is
edge
, create anAGSUtilityElement
from the identified feature and compute how far along the edge the user tapped usingclass AGSGeometryEngine.fraction(alongLine:to:tolerance:)
. - Add this
AGSUtilityElement
to a collection of starting locations or barriers. - Create
AGSUtilityTraceParameters
with the selected trace type along with the collected starting locations and barriers (if applicable). - Set the
AGSUtilityTraceConfiguration
with the utility tier'smakeDefaultTraceConfiguration
method. - Run
AGSUtilityNetwork.trace(with:completion:)
with the specified starting points and (optionally) barriers. - Group the
AGSUtilityElementTraceResult.elements
by theirnetworkSource.name
. - For every
AGSFeatureLayer
in this map with trace result elements, select features by convertingAGSUtilityElement
(s) toAGSArcGISFeature
(s) usingAGSUtilityNetwork.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
// 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()).")
}
}