Run a filtered trace to locate operable features that will isolate an area from the flow of network resources.

Use case
Determine the set of operable features required to stop a network’s resource, effectively isolating an area of the network. For example, you can choose to return only accessible and operable valves: ones that are not paved over or rusted shut.
How to use the sample
Tap on one or more features to use as filter barriers or create and set the configuration’s filter barriers by selecting a utility category. Toggle “Isolated Features” to update trace configuration. Tap “Trace” to run a subnetwork-based isolation trace. Tap “Reset” to clear filter barriers and trace results.
How it works
-
Create a
MapViewinstance. -
Create and load a
Mapwith a web map portal item that contains aUtilityNetwork. -
Create a
Mapobject that containsFeatureLayer(s) created from the service geodatabase’s tables. -
Create and load a
UtilityNetworkwith the same feature service URL and map. Use theonSingleTapGesturemodifier to listen for tap events on the map view. -
Create
UtilityTraceParameterswithisolationtrace type and a default starting location from a given asset type and global ID. -
Get a default
UtilityTraceConfigurationfrom a given tier in a domain network. Set itsfilterproperty with anUtilityTraceFilterobject. -
Add a
GraphicsOverlayfor showing starting location and filter barriers. -
Populate the choice list for the filter barriers from the
categoriesproperty ofUtilityNetworkDefinition. -
When the map view is tapped, identify which feature is at the tap location, and add a
Graphicto represent a filter barrier. -
Create a
UtilityElementfor the identified feature and add this element to the trace parameters’filterBarriersproperty.- If the element is a junction with more than one terminal, display a terminal picker. Then set the junction’s
terminalproperty with the selected terminal. - If it is an edge, set its
fractionAlongEdgeproperty usingGeometryEngine.polyline(_:fractionalLengthClosestTo:tolerance:)method.
- If the element is a junction with more than one terminal, display a terminal picker. Then set the junction’s
-
If “Trace” is tapped without filter barriers:
- Create a new
UtilityCategoryComparisonwith the selected category andUtilityCategoryComparison.Operator.exists. - Assign this condition to
UtilityTraceFilter.barriersfrom the default configuration from step 6. - Update the configuration’s
includesIsolatedFeaturesproperty. - Set this configuration to the parameters’
traceConfigurationproperty. - Run
UtilityNetwork.trace(parameters:)with the specified parameters.
If “Trace” is tapped with filter barriers:
- Update
includesIsolatedFeaturesproperty of the default configuration from step 6. - Run
UtilityNetwork.trace(parameters:)with the specified parameters.
- Create a new
-
For every
FeatureLayerin this map with trace result elements, select features by convertingUtilityElement(s) toArcGISFeature(s) usingUtilityNetwork.features(for:).
Relevant API
- GeometryEngine.polyline(_:fractionalLengthClosestTo:tolerance:)
- ServiceGeodatabase
- UtilityCategory
- UtilityCategoryComparison
- UtilityCategoryComparison.Operator
- UtilityDomainNetwork
- UtilityElement
- UtilityElementTraceResult
- UtilityNetwork
- UtilityNetworkDefinition
- UtilityTerminal
- UtilityTier
- UtilityTraceFilter
- UtilityTraceParameters
- UtilityTraceParameters.TraceType
- UtilityTraceResult
About the data
The Naperville gas network feature service, hosted on ArcGIS Online, contains a utility network used to run the isolation 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, filter barriers, isolated features, network analysis, subnetwork trace, trace configuration, trace filter, utility network
Sample code
// Copyright 2023 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//// https://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 ArcGISimport SwiftUI
struct RunValveIsolationTraceView: View { /// The view model for the sample. @StateObject private var model = Model()
/// The last locations in the screen and map where a tap occurred. @State private var lastSingleTap: (screenPoint: CGPoint, mapPoint: Point)?
/// A Boolean value indicating if the configuration sheet is presented. @State private var isConfigurationPresented = false
/// A Boolean value indicating whether to include isolated features in the /// trace results when used in conjunction with an isolation trace. @State private var includeIsolatedFeatures = true
var body: some View { MapViewReader { mapViewProxy in MapView( map: model.map, graphicsOverlays: [model.parametersOverlay] ) .onSingleTapGesture { screenPoint, mapPoint in lastSingleTap = (screenPoint, mapPoint) } .overlay(alignment: .top) { Text(model.statusText) .padding(10) .frame(maxWidth: .infinity, alignment: .center) .background(.ultraThinMaterial, ignoresSafeAreaEdges: .horizontal) .multilineTextAlignment(.center) } .task { await model.setup() if let point = model.startingLocationPoint { await mapViewProxy.setViewpointCenter(point, scale: 3_000) } } .task(id: lastSingleTap?.mapPoint) { guard let lastSingleTap else { return } if let feature = try? await mapViewProxy.identifyLayers( screenPoint: lastSingleTap.screenPoint, tolerance: 10 ).first?.geoElements.first as? ArcGISFeature { model.addFilterBarrier(for: feature, at: lastSingleTap.mapPoint) } } .toolbar { ToolbarItemGroup(placement: .bottomBar) { Button("Configuration") { isConfigurationPresented.toggle() } .disabled(model.tracingActivity == .runningTrace || model.tracingActivity == .loadingNetwork) Spacer() Button("Trace") { Task { await model.trace(includeIsolatedFeatures: includeIsolatedFeatures) } } .disabled(!model.traceEnabled) Spacer() Button("Reset") { model.reset() if let point = model.startingLocationPoint { Task { await mapViewProxy.setViewpointCenter(point, scale: 3_000) } } } .disabled(!model.resetEnabled || model.tracingActivity == .runningTrace) } } .sheet(isPresented: $isConfigurationPresented) { NavigationStack { configurationView } } .overlay(alignment: .center) { if let tracingActivity = model.tracingActivity { VStack { Text(tracingActivity.label) ProgressView() .progressViewStyle(.circular) } .padding() .background(.thinMaterial) .clipShape(.rect(cornerRadius: 10)) } } .alert( "Select Terminal", isPresented: $model.terminalSelectorIsOpen, actions: { terminalPickerButtons } ) .onTeardown { model.tearDown() } } }
/// Buttons for each the available terminals on the last added utility element. @ViewBuilder private var terminalPickerButtons: some View { if let lastAddedElement = model.lastAddedElement, let terminalConfiguration = lastAddedElement.assetType.terminalConfiguration { ForEach(terminalConfiguration.terminals) { terminal in Button(terminal.name) { lastAddedElement.terminal = terminal model.terminalSelectorIsOpen = false model.addTerminal(to: lastSingleTap!.mapPoint) } } } }
@ViewBuilder private var configurationView: some View { Form { Section { List(model.filterBarrierCategories, id: \.name) { category in HStack { Text(category.name) Spacer() if category === model.selectedCategory { Image(systemName: "checkmark") .foregroundStyle(Color.accentColor) } } // Allows the whole row to be tapped. Without this only the text is // tappable. .contentShape(Rectangle()) .onTapGesture { if category.name == model.selectedCategory?.name { model.unselectCategory(category) } else { model.selectCategory(category) } } } } header: { Text("Category") } footer: { Text("Choose a category to run the valve isolation trace. The selected utility category defines constraints and conditions based upon specific characteristics of asset types in the utility network.") } Section { Toggle(isOn: $includeIsolatedFeatures) { Text("Include Isolated Features") } } header: { Text("Other Options") } footer: { Text("Choose whether or not the trace should include isolated features. This means that isolated features are included in the trace results when used in conjunction with an isolation trace.") } .toggleStyle(.switch) } .navigationTitle("Configuration") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { isConfigurationPresented = false } } } }}
private extension RunValveIsolationTraceView.Model.TracingActivity { /// A human-readable label for the tracing activity. var label: String { switch self { case .loadingNetwork: return "Loading utility network" case .startingLocation: return "Getting starting location feature" case .runningTrace: return "Running isolation trace" } }}
#Preview { NavigationStack { RunValveIsolationTraceView() }}// Copyright 2023 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//// https://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 ArcGISimport Combineimport Foundation
extension RunValveIsolationTraceView { /// The view model for this sample. @MainActor class Model: ObservableObject { /// A web map with a utility network used to run the isolation trace. let map = Map(item: PortalItem.napervilleGasNetwork())
/// The utility network for this sample. private var utilityNetwork: UtilityNetwork { map.utilityNetworks.first! }
/// The feature layers of the web map. private var layers: [FeatureLayer] { map.operationalLayers.compactMap { $0 as? FeatureLayer } }
/// The last element that was added to the list of filter barriers. /// /// When an element contains more than one terminal, the user should be presented with the /// option to select a terminal. Keeping a reference to the last added element provides ease /// of access to save the user's choice. @Published private(set) var lastAddedElement: UtilityElement?
/// The current tracing related activity. @Published private(set) var tracingActivity: TracingActivity? = .loadingNetwork
/// The base trace parameters. private let traceParameters = UtilityTraceParameters(traceType: .isolation, startingLocations: [])
/// The point geometry of the starting location. private(set) var startingLocationPoint: Point?
/// The filter barrier categories. private(set) var filterBarrierCategories: [UtilityCategory] = []
/// The selected filter barrier category. @Published private(set) var selectedCategory: UtilityCategory?
/// A Boolean value indicating if tracing is enabled. @Published private(set) var traceEnabled = false
/// A Boolean value indicating if the reseting the trace is enabled. @Published private(set) var resetEnabled = false
/// A Boolean value indicating if the user has added filter barriers to the trace parameters. private var hasFilterBarriers: Bool { !traceParameters.filterBarriers.isEmpty }
/// A Boolean value indicating whether the terminal selection menu is open. @Published var terminalSelectorIsOpen = false
/// The status text to display to the user. @Published private(set) var statusText = "Loading Utility Network…"
/// The filter barrier identifier. private static let filterBarrierIdentifier = "filter barrier"
/// The graphic overlay to display starting location and filter barriers. let parametersOverlay: GraphicsOverlay = { let barrierPointSymbol = SimpleMarkerSymbol(style: .x, color: .red, size: 20) let barrierUniqueValue = UniqueValue( description: "Filter Barrier", label: "Filter Barrier", symbol: barrierPointSymbol, values: [filterBarrierIdentifier] ) let startingPointSymbol = SimpleMarkerSymbol(style: .cross, color: .green, size: 20) let renderer = UniqueValueRenderer( fieldNames: ["TraceLocationType"], uniqueValues: [barrierUniqueValue], defaultLabel: "Starting Location", defaultSymbol: startingPointSymbol ) let overlay = GraphicsOverlay() overlay.renderer = renderer return overlay }()
init() { // Updates the URL session challenge handler to use the // specified credentials and tokens for any challenges. ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = ChallengeHandler() }
/// Loads the map and utility network. func setup() async { do { // Load the map to get the utility network. try await map.load() try await loadUtilityNetwork() } catch { statusText = error.localizedDescription } }
/// Cleans up the model's setup. func tearDown() { // Resets the URL session challenge handler to use default handling // and removes all credentials. ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = nil ArcGISEnvironment.authenticationManager.arcGISCredentialStore.removeAll() }
/// Loads the utility network. private func loadUtilityNetwork() async throws { tracingActivity = .loadingNetwork defer { tracingActivity = nil } try await utilityNetwork.load() statusText = """ Tap on the map to add filter barriers or run the trace \ directly without filter barriers by specifying a category \ through the Configuration menu. """ tracingActivity = .startingLocation guard let startingLocation = makeStartingLocation() else { return } traceParameters.addStartingLocation(startingLocation) if let feature = try await utilityNetwork.features(for: traceParameters.startingLocations).first, let point = feature.geometry as? Point { // Get the geometry of the starting location as a point. // Then draw the starting location on the map.
addGraphic(for: point, traceLocationType: "starting point") startingLocationPoint = point tracingActivity = nil
// Get available utility categories. if let definition = utilityNetwork.definition { filterBarrierCategories = definition.categories } } }
/// When the utility network is loaded, create a `UtilityElement` /// from the asset type to use as the starting location for the trace. private func makeStartingLocation() -> UtilityElement? { // Constants for creating the default starting location. let networkSourceName = "Gas Device" let assetGroupName = "Meter" let assetTypeName = "Customer" let terminalName = "Load" let globalID = UUID(uuidString: "98A06E95-70BE-43E7-91B7-E34C9D3CB9FF")!
// Create a default starting location. if let networkSource = utilityNetwork.definition?.networkSource(named: networkSourceName), let assetType = networkSource.assetGroup(named: assetGroupName)?.assetType(named: assetTypeName), let startingLocation = utilityNetwork.makeElement(assetType: assetType, globalID: globalID) { // Set the terminal for the location. (For our case, use the "Load" terminal.) startingLocation.terminal = assetType.terminalConfiguration?.terminals.first(where: { $0.name == terminalName }) return startingLocation } else { return nil } }
/// Adds a graphic to the graphics overlay. /// - Parameters: /// - location: The `Point` location to place the graphic. /// - traceLocationType: The textual description of the trace location type. private func addGraphic(for location: Point, traceLocationType: String) { let graphic = Graphic( geometry: location, attributes: ["TraceLocationType": traceLocationType] ) parametersOverlay.addGraphic(graphic) }
/// Sets the selected filter barrier category and updates the status text. func selectCategory(_ category: UtilityCategory) { selectedCategory = category statusText = "\(category.name) selected." traceEnabled = true }
/// Unselects the selected filter barrier category and updates the status text. func unselectCategory(_ category: UtilityCategory) { selectedCategory = nil traceEnabled = hasFilterBarriers statusText = "Tap on the map to add filter barriers, or run the trace directly without filter barriers." }
/// Runs a trace with the pending trace configuration and selects features in the map that /// correspond to the element results. func trace(includeIsolatedFeatures: Bool) async { // Clear previous trace results. layers.forEach { $0.clearSelection() }
tracingActivity = .runningTrace traceEnabled = false
let configuration = makeTraceConfiguration(category: selectedCategory, includeIsolatedFeatures: includeIsolatedFeatures) traceParameters.traceConfiguration = configuration do { let traceResults = try await utilityNetwork .trace(using: traceParameters) .compactMap { $0 as? UtilityElementTraceResult } try await handleTraceResults(traceResults) } catch { statusText = "Trace failed." traceEnabled = true tracingActivity = nil return } tracingActivity = nil traceEnabled = true resetEnabled = true }
func handleTraceResults(_ traceResults: [UtilityElementTraceResult]) async throws { let elements = traceResults.flatMap(\.elements) guard !elements.isEmpty else { statusText = "Trace completed with no output." return }
let groups = Dictionary(grouping: elements, by: \.networkSource.name) if groups.isEmpty { statusText = "Trace completed with no output." return } do { for (networkName, elements) in groups { guard let layer = layers.first( where: { $0.featureTable?.tableName == networkName } ) else { continue } let features = try await utilityNetwork.features(for: elements) layer.selectFeatures(features) } } catch { statusText = error.localizedDescription } if !hasFilterBarriers, let selectedCategory { statusText = "Trace with \(selectedCategory.name.lowercased()) category completed." } else { statusText = "Trace with filter barriers completed." } }
/// Removes all added filter barriers. func reset() { layers.forEach { $0.clearSelection() } traceParameters.removeAllFilterBarriers() parametersOverlay.removeAllGraphics() // Add back the starting location. if let startingLocationPoint { addGraphic(for: startingLocationPoint, traceLocationType: "starting point") } statusText = "Tap on the map to add filter barriers, or run the trace directly without filter barriers." resetEnabled = false }
/// Gets the utility tier's trace configuration and apply category comparison. private func makeTraceConfiguration(category: UtilityCategory?, includeIsolatedFeatures: Bool) -> UtilityTraceConfiguration { // Get a default trace configuration from a tier in the network. guard let configuration = utilityNetwork .definition? .domainNetwork(named: "Pipeline")? .tier(named: "Pipe Distribution System")? .defaultTraceConfiguration else { fatalError("Utility network does not have a default trace configuration.") } if let category = category { // Note: `UtilityNetworkAttributeComparison` or `UtilityCategoryComparison` // with `UtilityCategoryComparisonOperator.doesNotExist` can also be used. // These conditions can be joined with either `UtilityTraceOrCondition` // or `UtilityTraceAndCondition`. // See more in the README. let comparison = UtilityCategoryComparison(category: category, operator: .exists) // Create a trace filter. let filter = UtilityTraceFilter() filter.barriers = comparison configuration.filter = filter } configuration.includesIsolatedFeatures = includeIsolatedFeatures return configuration }
/// Adds a graphic at the tapped location for the filter barrier. /// - Parameters: /// - feature: The geo element retrieved as a `Feature`. /// - location: The `Point` used to identify utility elements in the utility network. func addFilterBarrier(for feature: ArcGISFeature, at location: Point) { guard let geometry = feature.geometry, let element = utilityNetwork.makeElement(arcGISFeature: feature) else { return }
switch element.networkSource.kind { case .junction: lastAddedElement = element if let terminals = element.assetType.terminalConfiguration?.terminals { if terminals.count > 1 { terminalSelectorIsOpen.toggle() return } else { if let terminal = terminals.first { statusText = "Junction element with terminal \(terminal.name) added to the filter barriers." } } } case .edge: if let line = GeometryEngine.makeGeometry(from: geometry, z: nil) as? Polyline { element.fractionAlongEdge = GeometryEngine.polyline( line, fractionalLengthClosestTo: location, tolerance: -1 ) statusText = String(format: "Edge element at distance %.3f along edge added to the filter barriers.", element.fractionAlongEdge) } @unknown default: return }
traceParameters.addFilterBarrier(element) lastAddedElement = element let point = geometry as? Point ?? location addGraphic(for: point, traceLocationType: RunValveIsolationTraceView.Model.filterBarrierIdentifier) resetEnabled = true traceEnabled = true }
/// Adds the filter barrier of the user selected terminal to the trace parameters. func addTerminal(to point: Point) { guard let lastAddedElement, let terminal = lastAddedElement.terminal else { return } traceParameters.addFilterBarrier(lastAddedElement) statusText = "Junction element with terminal \(terminal.name) added to the filter barriers." resetEnabled = true addGraphic( for: point, traceLocationType: RunValveIsolationTraceView.Model.filterBarrierIdentifier ) } }}
extension RunValveIsolationTraceView.Model { /// The different states of a utility network trace. enum TracingActivity: CaseIterable { case loadingNetwork, startingLocation, runningTrace }}
/// The authentication model used to handle challenges and credentials.private struct ChallengeHandler: ArcGISAuthenticationChallengeHandler { func handleArcGISAuthenticationChallenge( _ challenge: ArcGISAuthenticationChallenge ) async throws -> ArcGISAuthenticationChallenge.Disposition { // NOTE: Never hardcode login information in a production application. // This is done solely for the sake of the sample. return .continueWithCredential( // Credentials for sample server 7 services. try await TokenCredential.credential(for: challenge, username: "viewer01", password: "I68VGU^nMurF") ) }}
private extension PortalItem { /// A web map portal item for the Naperville Gas Device and Line layers. static func napervilleGasNetwork() -> PortalItem { PortalItem( // Sample server 7 authentication required. portal: Portal( url: URL(string: "https://sampleserver7.arcgisonline.com/portal")!, connection: .authenticated ), id: .init("f439b4724bb54ac088a2c21eaf70da7b")! ) }}