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
MapView
instance. -
Create and load a
ServiceGeodatabase
with a feature service URL and get tables with their layer IDs. -
Create a
Map
object that containsFeatureLayer
(s) created from the service geodatabase's tables. -
Create and load a
UtilityNetwork
with the same feature service URL and map. Use theonSingleTapGesture
modifier to listen for tap events on the map view. -
Create
UtilityTraceParameters
withisolation
trace type and a default starting location from a given asset type and global ID. -
Get a default
UtilityTraceConfiguration
from a given tier in a domain network. Set itsfilter
property with anUtilityTraceFilter
object. -
Add a
GraphicsOverlay
for showing starting location and filter barriers. -
Populate the choice list for the filter barriers from the
categories
property ofUtilityNetworkDefinition
. -
When the map view is tapped, identify which feature is at the tap location, and add a
Graphic
to represent a filter barrier. -
Create a
UtilityElement
for the identified feature and add this element to the trace parameters'filterBarriers
property.- If the element is a junction with more than one terminal, display a terminal picker. Then set the junction's
terminal
property with the selected terminal. - If it is an edge, set its
fractionAlongEdge
property 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
UtilityCategoryComparison
with the selected category andUtilityCategoryComparison.Operator.exists
. - Assign this condition to
UtilityTraceFilter.barriers
from the default configuration from step 6. - Update the configuration's
includesIsolatedFeatures
property. - Set this configuration to the parameters'
traceConfiguration
property. - Run
UtilityNetwork.trace(parameters:)
with the specified parameters.
If "Trace" is tapped with filter barriers:
- Update
includesIsolatedFeatures
property of the default configuration from step 6. - Run
UtilityNetwork.trace(parameters:)
with the specified parameters.
- Create a new
-
For every
FeatureLayer
in 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 ArcGIS
import 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 == .loadingServiceGeodatabase ||
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 }
)
}
}
/// 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 .loadingServiceGeodatabase: return "Loading service geodatabase…"
case .loadingNetwork: return "Loading utility network…"
case .startingLocation: return "Getting starting location feature…"
case .runningTrace: return "Running isolation trace…"
}
}
}
#Preview {
NavigationStack {
RunValveIsolationTraceView()
}
}