Get a server-defined trace configuration for a given tier and modify its traversability scope, add new condition barriers, and control what is included in the subnetwork trace result.
Use case
While some traces are built from an ad-hoc group of parameters, many are based on a variation of the trace configuration taken from the subnetwork definition. For example, an electrical trace will be based on the trace configuration of the subnetwork, but may add additional clauses to constrain the trace along a single phase. Similarly, a trace in a gas or electric design application may include features with a status of "In Design" that are normally excluded from trace results.
How to use the sample
The sample loads with a server-defined trace configuration from a tier. Use the switches to toggle which options to include in the trace - such as containers or barriers. Tap the middle button on the bottom toolbar to create a new condition to add to the list. Swipe left on a condition under "List of conditions" to delete it or tap "Reset" to delete the whole list. Tap "Trace" to run a subnetwork trace with this modified configuration from a default starting location.
Example barrier conditions for the default dataset:
- 'Transformer Load' equal '15'
- 'Phases Current' doesNotIncludeTheValues 'A'
- 'Generation KW' lessThan '50'
How it works
- Create and load an
AGSUtilityNetwork
with a feature service URL, then get an asset type and a tier by their names. - Populate the choice list for the comparison source with the non-system defined
AGSUtilityNetworkDefinition.networkAttributes
. Populate the choice list for the comparison operator with the enum values fromAGSUtilityAttributeComparisonOperator
. - Create an
AGSUtilityElement
from this asset type to use as the starting location for the trace. - Update the selected barrier expression and the checked options in the UI using this tier's
AGSTraceConfiguration
. - When an attribute has been selected, if its
AGSDomain
is anAGSCodedValueDomain
, populate the choice list for the comparison value with itsAGSCodedValues
. Otherwise, display aUITextField
for entering an attribute value. - When "Add" is tapped, create a new
AGSUtilityNetworkAttributeComparison
using the selected comparison source, operator, and selected or typed value. Use the selected source'sdataType
to convert the comparison value to the correct data type. - If the traversability's list of
barriers
is not empty, create anAGSUtilityTraceOrCondition
with the existingbarriers
and the new comparison from step 6. - When "Trace" is tapped, create
AGSUtilityTraceParameters
passing insubnetwork
and the default starting location. Set itstraceConfiguration
with the modified options, selections, and expression; then trace the utility network withAGSUtilityNetwork.trace(with:completion:)
. - When "Reset" is tapped, set the trace configurations expression back to its original value.
- Display the count of returned
AGSUtilityElementTraceResult.elements
.
Relevant API
- AGSCodedValueDomain
- AGSUtilityAssetType
- AGSUtilityAttributeComparisonOperator
- AGSUtilityCategory
- AGSUtilityCategoryComparison
- AGSUtilityCategoryComparisonOperator
- AGSUtilityDomainNetwork
- AGSUtilityElement
- AGSUtilityElementTraceResult
- AGSUtilityNetwork
- AGSUtilityNetworkAttribute
- AGSUtilityNetworkAttributeComparison
- AGSUtilityNetworkDefinition
- AGSUtilityTerminal
- AGSUtilityTier
- AGSUtilityTraceAndCondition
- AGSUtilityTraceConfiguration
- AGSUtilityTraceOrCondition
- AGSUtilityTraceParameters
- AGSUtilityTraceResult
- AGSUtilityTraceType
- AGSUtilityTraversability
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
category comparison, condition barriers, network analysis, network attribute comparison, subnetwork trace, trace configuration, traversability, utility network, validate consistency
Sample Code
// Copyright 2020 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
protocol ConfigureSubnetworkTraceOptionsViewControllerDelegate: AnyObject {
func optionsViewController(_ controller: ConfigureSubnetworkTraceOptionsViewController, didCreate: AGSUtilityTraceConditionalExpression)
}
class ConfigureSubnetworkTraceOptionsViewController: UITableViewController {
// MARK: Storyboard views
/// The cell for attribute options.
@IBOutlet var attributesCell: UITableViewCell!
/// The cell for comparison operator options.
@IBOutlet var comparisonCell: UITableViewCell!
/// The cell for value to compare with.
@IBOutlet var valueCell: UITableViewCell!
/// A button to add the conditional expression to the trace configuration.
@IBOutlet var doneBarButtonItem: UIBarButtonItem!
// MARK: Properties
/// A delegate to notify other view controllers.
weak var delegate: ConfigureSubnetworkTraceOptionsViewControllerDelegate?
/// An array of possible network attributes.
var possibleAttributes = [AGSUtilityNetworkAttribute]()
/// The attribute selected by the user.
var selectedAttribute: AGSUtilityNetworkAttribute? {
didSet {
// Set the selected attribute name.
attributesCell.detailTextLabel?.text = selectedAttribute?.name
// Reset the selected value.
selectedValue = nil
valueCell.detailTextLabel?.text = nil
updateCellStates()
}
}
/// The comparison selected by the user.
var selectedComparison: AGSUtilityAttributeComparisonOperator? {
didSet {
if let selectedComparisonString = selectedComparison?.title {
comparisonCell.detailTextLabel?.text = selectedComparisonString
} else {
comparisonCell.detailTextLabel?.text = nil
}
doneBarButtonItem.isEnabled = selectedComparison != nil && selectedValue != nil
}
}
/// The value selected by the user.
var selectedValue: Any? {
didSet {
doneBarButtonItem.isEnabled = selectedComparison != nil && selectedValue != nil
}
}
// MARK: Actions
@IBAction func addConditionBarButtonItemTapped(_ sender: UIBarButtonItem) {
if let attribute = selectedAttribute, let comparison = selectedComparison, let value = selectedValue {
let convertedValue: Any
if let codedValue = value as? AGSCodedValue, attribute.domain is AGSCodedValueDomain {
// The value is a coded value.
convertedValue = convertToDataType(value: codedValue.code!, dataType: attribute.dataType)
} else {
// The value is from user input.
convertedValue = convertToDataType(value: value, dataType: attribute.dataType)
}
if let expression = AGSUtilityNetworkAttributeComparison(networkAttribute: attribute, comparisonOperator: comparison, value: convertedValue) {
// Create and pass the valid expression back to the main view controller.
delegate?.optionsViewController(self, didCreate: expression)
}
}
dismiss(animated: true)
}
@IBAction func cancelBarButtonItemTapped(_ sender: UIBarButtonItem) {
dismiss(animated: true)
}
// MARK: UI and data binding methods
/// Convert the values to matching data types.
///
/// - Note: The input value can either be an `AGSCodedValue` populated from the left hand side
/// attribute's domain, or a numeric value entered by the user.
/// - Parameters:
/// - value: The right hand side value used in the conditional expression.
/// - dataType: An `AGSUtilityNetworkAttributeDataType` enum case.
/// - Returns: Converted value.
func convertToDataType(value: Any, dataType: AGSUtilityNetworkAttributeDataType) -> Any {
switch dataType {
case .integer:
return value as! Int64
case .float:
return value as! Float
case .double:
return value as! Double
case .boolean:
return value as! Bool
default:
return value
}
}
func updateCellStates() {
// Disable the value cell when attribute is unspecified.
if let selectedAttribute = selectedAttribute {
if selectedAttribute.domain is AGSCodedValueDomain {
// Indicate that a new view controller will display.
valueCell.accessoryType = .disclosureIndicator
} else {
// Indicate that an alert will show.
valueCell.accessoryType = .none
}
valueCell.textLabel?.isEnabled = true
valueCell.isUserInteractionEnabled = true
} else {
// Enable the value cell when an attribute is specified.
valueCell.textLabel?.isEnabled = false
valueCell.isUserInteractionEnabled = false
}
}
// Transition to the attribute options view controller.
func showAttributePicker() {
let selectedIndex = possibleAttributes.firstIndex { $0 == selectedAttribute }
let optionsViewController = OptionsTableViewController(labels: possibleAttributes.map { $0.name }, selectedIndex: selectedIndex) { newIndex in
self.selectedAttribute = self.possibleAttributes[newIndex]
self.navigationController?.popViewController(animated: true)
}
optionsViewController.title = "Attributes"
show(optionsViewController, sender: self)
}
// Transition to the comparison options view controller.
func showComparisonPicker() {
let selectedIndex = selectedComparison?.rawValue
// An array of `AGSUtilityAttributeComparisonOperator`s.
let attributeComparisonOperators = AGSUtilityAttributeComparisonOperator.allCases
let optionsViewController = OptionsTableViewController(labels: attributeComparisonOperators.map { $0.title }, selectedIndex: selectedIndex) { newIndex in
self.selectedComparison = attributeComparisonOperators[newIndex]
self.navigationController?.popViewController(animated: true)
}
optionsViewController.title = "Comparison"
show(optionsViewController, sender: self)
}
// Transition to the value options view controller.
func showValuePicker(values: [AGSCodedValue]) {
let selectedIndex: Int?
if let selectedValue = selectedValue as? AGSCodedValue {
selectedIndex = values.firstIndex { $0 == selectedValue }
} else {
selectedIndex = nil
}
let valueLabels = values.map { $0.name }
let optionsViewController = OptionsTableViewController(labels: valueLabels, selectedIndex: selectedIndex) { newIndex in
self.selectedValue = values[newIndex]
self.valueCell.detailTextLabel?.text = valueLabels[newIndex]
self.navigationController?.popViewController(animated: true)
}
optionsViewController.title = "Value"
show(optionsViewController, sender: self)
}
// Prompt an alert to allow the user to input custom values.
func showValueInputField(completion: @escaping (NSNumber?) -> Void) {
// Create an object to observe if text field input is empty.
var textFieldObserver: NSObjectProtocol!
let alertController = UIAlertController(title: "Provide a comparison value", message: nil, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
// Remove observer when canceled.
NotificationCenter.default.removeObserver(textFieldObserver!)
}
let doneAction = UIAlertAction(title: "Done", style: .default) { [unowned alertController] _ in
let textField = alertController.textFields!.first!
// Remove the observer when done button is no longer in use.
NotificationCenter.default.removeObserver(textFieldObserver!)
// Convert the string to a number.
completion(NumberFormatter().number(from: textField.text!))
}
// Add the done action to the alert controller.
doneAction.isEnabled = false
alertController.addAction(doneAction)
// Add a text field to the alert controller.
alertController.addTextField { textField in
textField.keyboardType = .numbersAndPunctuation
textField.placeholder = "e.g. 15"
// Add an observer to ensure the user does not input an empty string.
textFieldObserver = NotificationCenter.default.addObserver(
forName: UITextField.textDidChangeNotification,
object: textField,
queue: .main
) { [ unowned doneAction ] _ in
if let text = textField.text {
// Enable the done button if the textfield is not empty and is a valid number.
doneAction.isEnabled = NumberFormatter().number(from: text) != nil
} else {
doneAction.isEnabled = false
}
}
}
// Add a cancel action to alert controller.
alertController.addAction(cancelAction)
alertController.preferredAction = doneAction
present(alertController, animated: true)
}
// MARK: UITableViewController
override func viewDidLoad() {
super.viewDidLoad()
updateCellStates()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let cell = tableView.cellForRow(at: indexPath)
switch cell {
case attributesCell:
showAttributePicker()
case comparisonCell:
showComparisonPicker()
case valueCell:
if let domain = selectedAttribute?.domain as? AGSCodedValueDomain {
showValuePicker(values: domain.codedValues)
} else {
showValueInputField { [weak self] value in
guard let self = self else { return }
// Assign an `NSNumber?` to selected value so that it can cast to numbers.
self.selectedValue = value
self.valueCell.detailTextLabel?.text = value?.stringValue
// Mitigate the Apple's UI bug in right detail cell.
tableView.reloadRows(at: [indexPath], with: .none)
}
}
default:
fatalError("Unknown cell type")
}
}
}
private extension AGSUtilityAttributeComparisonOperator {
static let allCases: [AGSUtilityAttributeComparisonOperator] = [.equal, .notEqual, .greaterThan, .greaterThanEqual, .lessThan, .lessThanEqual, .includesTheValues, .doesNotIncludeTheValues, .includesAny, .doesNotIncludeAny]
/// An extension of `AGSUtilityAttributeComparisonOperator` that returns a human readable description.
var title: String {
switch self {
case .equal: return "Equal"
case .notEqual: return "Not Equal"
case .greaterThan: return "Greater Than"
case .greaterThanEqual: return "Greater Than Equal"
case .lessThan: return "Less Than"
case .lessThanEqual: return "Less Than Equal"
case .includesTheValues: return "Includes The Values"
case .doesNotIncludeTheValues: return "Does Not Include The Values"
case .includesAny: return "Includes Any"
case .doesNotIncludeAny: return "Does Not Include Any"
@unknown default: return "Unknown"
}
}
}