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()).")
}
}