Configure subnetwork trace

View on GitHubSample viewer app

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.

Configure subnetwork trace screenshot

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

  1. Create and load an AGSUtilityNetwork with a feature service URL, then get an asset type and a tier by their names.
  2. 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 from AGSUtilityAttributeComparisonOperator.
  3. Create an AGSUtilityElement from this asset type to use as the starting location for the trace.
  4. Update the selected barrier expression and the checked options in the UI using this tier's AGSTraceConfiguration.
  5. When an attribute has been selected, if its AGSDomain is an AGSCodedValueDomain, populate the choice list for the comparison value with its AGSCodedValues. Otherwise, display a UITextField for entering an attribute value.
  6. When "Add" is tapped, create a new AGSUtilityNetworkAttributeComparison using the selected comparison source, operator, and selected or typed value. Use the selected source's dataType to convert the comparison value to the correct data type.
  7. If the traversability's list of barriers is not empty, create an AGSUtilityTraceOrCondition with the existing barriers and the new comparison from step 6.
  8. When "Trace" is tapped, create AGSUtilityTraceParameters passing in subnetwork and the default starting location. Set its traceConfiguration with the modified options, selections, and expression; then trace the utility network with AGSUtilityNetwork.trace(with:completion:).
  9. When "Reset" is tapped, set the trace configurations expression back to its original value.
  10. 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

ConfigureSubnetworkTraceOptionsViewController.swiftConfigureSubnetworkTraceOptionsViewController.swiftConfigureSubnetworkTraceViewController.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
// 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"
        }
    }
}

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