Perform valve isolation trace

View on GitHubSample viewer app

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

Image of perform valve isolation trace with category comparison Image of perform valve isolation trace with filter barriers

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

  1. Create an AGSMapView object.

  2. Create and load an AGSServiceGeodatabase with a feature service URL and get tables with their layer IDs.

  3. Create an AGSMap object that contains AGSFeatureLayer(s) created from the service geodatabase's tables.

  4. Create and load an AGSUtilityNetwork with the same feature service URL and map. Use AGSGeoViewTouchDelegate.geoView(_:didTapAtScreenPoint:mapPoint:) to get the mapPoint where a user tapped on the map.

  5. Create AGSUtilityTraceParameters with isolation trace type and a default starting location from a given asset type and global ID.

  6. Get a default AGSUtilityTraceConfiguration from a given tier in a domain network. Set its filter property with an AGSUtilityTraceFilter object.

  7. Add an AGSGraphicsOverlay for showing starting location and filter barriers.

  8. Populate the choice list for the filter barriers from the categories property of AGSUtilityNetworkDefinition.

  9. When the map view is tapped, identify which feature is at the tap location, and add an AGSGraphic to represent a filter barrier.

  10. Create an AGSUtilityElement 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 using AGSGeometryEngine.fraction(alongLine:to:tolerance:) method.
  11. If "Trace" is tapped without filter barriers:

    • Create a new AGSUtilityCategoryComparison with the selected category and AGSUtilityCategoryComparisonOperator.exists.
    • Assign this condition to AGSUtilityTraceFilter.barriers from the default configuration from step 6.
    • Update the configuration's includeIsolatedFeatures property.
    • Set this configuration to the parameters' traceConfiguration property.
    • Run AGSUtilityNetwork.trace(with:completion:) with the specified parameters.

    If "Trace" is tapped with filter barriers:

    • Update includeIsolatedFeatures property of the default configuration from step 6.
    • Run AGSUtilityNetwork.trace(with:completion:) with the specified parameters.
  12. For every AGSFeatureLayer in this map with trace result elements, select features by converting AGSUtilityElement(s) to AGSArcGISFeature(s) using AGSUtilityNetwork.features(for:completion:).

Relevant API

  • AGSGeometryEngine.fraction(alongLine:to:tolerance:)
  • AGSServiceGeodatabase
  • AGSUtilityCategory
  • AGSUtilityCategoryComparison
  • AGSUtilityCategoryComparisonOperator
  • AGSUtilityDomainNetwork
  • AGSUtilityElement
  • AGSUtilityElementTraceResult
  • AGSUtilityNetwork
  • AGSUtilityNetworkDefinition
  • AGSUtilityTerminal
  • AGSUtilityTier
  • AGSUtilityTraceFilter
  • AGSUtilityTraceParameters
  • AGSUtilityTraceResult
  • AGSUtilityTraceType

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

PerformValveIsolationTraceViewController.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
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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
// 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

class PerformValveIsolationTraceViewController: UIViewController {
    // MARK: Storyboard views

    /// The map view managed by the view controller.
    @IBOutlet var mapView: AGSMapView! {
        didSet {
            mapView.map = makeMap()
            mapView.graphicsOverlays.add(parametersOverlay)
        }
    }

    /// The button to start or reset the trace.
    @IBOutlet var traceResetBarButtonItem: UIBarButtonItem! {
        didSet {
            traceResetBarButtonItem.possibleTitles = ["Trace", "Reset"]
        }
    }
    /// The button to choose a utility category for filter barriers.
    @IBOutlet var categoryBarButtonItem: UIBarButtonItem!
    /// The switch to control whether to include isolated features in the
    /// trace results when used in conjunction with an isolation trace.
    @IBOutlet var isolationSwitch: UISwitch!
    /// The label to display trace status.
    @IBOutlet var statusLabel: UILabel!

    // MARK: Constant

    /// The URL to the feature service for running the isolation trace.
    static let featureServiceURL = URL(
        string: "https://sampleserver7.arcgisonline.com/server/rest/services/UtilityNetwork/NapervilleGas/FeatureServer"
    )!

    static let filterBarrierIdentifier = "filter barrier"

    // MARK: Instance properties

    let utilityNetwork = AGSUtilityNetwork(url: featureServiceURL)
    let serviceGeodatabase = AGSServiceGeodatabase(url: featureServiceURL)
    var traceCompleted = false
    var identifyAction: AGSCancelable?

    /// The base trace parameters.
    let traceParameters = AGSUtilityTraceParameters(traceType: .isolation, startingLocations: [])
    /// The utility category selected for running the trace.
    var selectedCategory: AGSUtilityCategory?
    /// An array of available utility categories with current network definition.
    var filterBarrierCategories = [AGSUtilityCategory]()
    /// An array to hold the gas line and gas device feature layers created from
    /// the service geodatabase.
    var layers = [AGSFeatureLayer]()
    /// The point geometry of the starting location.
    var startingLocationPoint: AGSPoint!

    /// The graphic overlay to display starting location and filter barriers.
    let parametersOverlay: AGSGraphicsOverlay = {
        let barrierPointSymbol = AGSSimpleMarkerSymbol(style: .X, color: .red, size: 20)
        let barrierUniqueValue = AGSUniqueValue(
            description: "Filter Barrier",
            label: "Filter Barrier",
            symbol: barrierPointSymbol,
            values: [filterBarrierIdentifier]
        )
        let startingPointSymbol = AGSSimpleMarkerSymbol(style: .cross, color: .green, size: 20)
        let renderer = AGSUniqueValueRenderer(
            fieldNames: ["TraceLocationType"],
            uniqueValues: [barrierUniqueValue],
            defaultLabel: "Starting Location",
            defaultSymbol: startingPointSymbol
        )
        let overlay = AGSGraphicsOverlay()
        overlay.renderer = renderer
        return overlay
    }()

    // MARK: Initialize map and utility network

    /// Create a map with a utility network.
    func makeMap() -> AGSMap {
        let map = AGSMap(basemapStyle: .arcGISStreetsNight)
        // Add the not yet loaded utility network to the map.
        map.utilityNetworks.add(utilityNetwork)
        return map
    }

    /// Load the service geodatabase and initialize the layers.
    func loadServiceGeodatabase() {
        UIApplication.shared.showProgressHUD(message: "Loading service geodatabase…")
        // NOTE: Never hardcode login information in a production application.
        // This is done solely for the sake of the sample.
        serviceGeodatabase.credential = AGSCredential(user: "viewer01", password: "I68VGU^nMurF")
        serviceGeodatabase.load { [weak self] error in
            guard let self = self else { return }
            // The gas device layer ./0 and gas line layer ./3 are created
            // from the service geodatabase.
            if let gasDeviceLayerTable = self.serviceGeodatabase.table(withLayerID: 0),
               let gasLineLayerTable = self.serviceGeodatabase.table(withLayerID: 3) {
                let layers = [gasLineLayerTable, gasDeviceLayerTable].map(AGSFeatureLayer.init)
                // Add the utility network feature layers to the map for display.
                self.mapView.map?.operationalLayers.addObjects(from: layers)
                self.layers = layers
                self.loadUtilityNetwork()
            } else if let error = error {
                UIApplication.shared.hideProgressHUD()
                self.presentAlert(error: error)
            } else {
                UIApplication.shared.hideProgressHUD()
            }
        }
    }

    /// Load the utility network.
    func loadUtilityNetwork() {
        UIApplication.shared.showProgressHUD(message: "Loading utility network…")
        // Load the utility network to be ready to run a trace against it.
        utilityNetwork.load { [weak self] error in
            guard let self = self else { return }
            let errorMessage = "Failed to load utility network."
            if let error = error {
                UIApplication.shared.hideProgressHUD()
                self.presentAlert(error: error)
                self.setStatus(message: errorMessage)
            } else if let startingLocation = self.makeStartingLocation() {
                self.utilityNetworkDidLoad(startingLocation: startingLocation)
            } else {
                UIApplication.shared.hideProgressHUD()
                self.presentAlert(message: "Failed to create starting location.")
                self.setStatus(message: errorMessage)
            }
        }
    }

    /// Called in response to the utility network load operation completing.
    /// - Parameter startingLocation: The utility element to start the trace from.
    func utilityNetworkDidLoad(startingLocation: AGSUtilityElement) {
        traceParameters.startingLocations.append(startingLocation)
        UIApplication.shared.showProgressHUD(message: "Getting starting location feature…")
        // Get the feature for the starting location element.
        utilityNetwork.features(for: traceParameters.startingLocations) { [weak self] features, error in
            UIApplication.shared.hideProgressHUD()
            guard let self = self else { return }
            if let features = features,
               let feature = features.first,
               let startingLocationPoint = feature.geometry as? AGSPoint {
                // Get the geometry of the starting location as a point.
                // Then draw the starting location on the map.
                self.startingLocationPoint = startingLocationPoint
                self.addGraphic(for: startingLocationPoint, traceLocationType: "starting point")
                self.mapView.setViewpointCenter(startingLocationPoint, scale: 3_000)
                // Get available utility categories.
                self.filterBarrierCategories = self.utilityNetwork.definition.categories
                self.categoryBarButtonItem.isEnabled = true
                // Enable touch event detection on the map view.
                self.mapView.touchDelegate = self
                self.setStatus(
                    message: """
                    Utility network loaded.
                    Tap on the map to add filter barriers or run the trace directly without filter barriers.
                    """
                )
            } else if let error = error {
                self.presentAlert(error: error)
                self.setStatus(message: "Failed to load starting location features.")
            }
        }
    }

    // MARK: Factory methods

    /// When the utility network is loaded, create an `AGSUtilityElement`
    /// from the asset type to use as the starting location for the trace.
    func makeStartingLocation() -> AGSUtilityElement? {
        // 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(withName: networkSourceName),
           let assetType = networkSource.assetGroup(withName: assetGroupName)?.assetType(withName: assetTypeName),
           let startingLocation = utilityNetwork.createElement(with: 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
        }
    }

    /// Get the utility tier's trace configuration and apply category comparison.
    func makeTraceConfiguration(category: AGSUtilityCategory?) -> AGSUtilityTraceConfiguration? {
        // Get a default trace configuration from a tier in the network.
        guard let configuration = utilityNetwork
                .definition
                .domainNetwork(withDomainNetworkName: "Pipeline")?
                .tier(withName: "Pipe Distribution System")?
                .makeDefaultTraceConfiguration()
        else {
            return nil
        }
        if let category = category {
            // Note: `AGSUtilityNetworkAttributeComparison` or `AGSUtilityCategoryComparison`
            // with `AGSUtilityCategoryComparisonOperator.doesNotExist` can also be used.
            // These conditions can be joined with either `AGSUtilityTraceOrCondition`
            // or `AGSUtilityTraceAndCondition`.
            // See more in the README.
            let comparison = AGSUtilityCategoryComparison(category: category, comparisonOperator: .exists)
            // Create a trace filter.
            let filter = AGSUtilityTraceFilter()
            filter.barriers = comparison
            configuration.filter = filter
        } else {
            configuration.filter = nil
        }
        configuration.includeIsolatedFeatures = isolationSwitch.isOn
        return configuration
    }

    // MARK: UI and feedback

    /// Select to highlight the features in the feature layers.
    /// - Parameters:
    ///   - elements: The utility elements from the trace result that correspond to `AGSArcGISFeature` objects.
    ///   - completion: Completion closure to execute after all selections are done.
    func selectFeatures(in elements: [AGSUtilityElement], completion: @escaping () -> Void) {
        let groupedElements = Dictionary(grouping: elements) { $0.networkSource.name }
        let selectionGroup = DispatchGroup()

        groupedElements.forEach { (networkName, elements) in
            guard let layer = layers.first(where: { $0.featureTable?.tableName == networkName }) else { return }

            selectionGroup.enter()
            utilityNetwork.features(for: elements) { [weak self, layer] (features, error) in
                defer {
                    selectionGroup.leave()
                }
                if let features = features {
                    layer.select(features)
                } else if let error = error {
                    self?.presentAlert(error: error)
                }
            }
        }

        selectionGroup.notify(queue: .main) {
            completion()
        }
    }

    func addGraphic(for location: AGSPoint, traceLocationType: String) {
        let traceLocationGraphic = AGSGraphic(geometry: location, symbol: nil, attributes: ["TraceLocationType": traceLocationType])
        parametersOverlay.graphics.add(traceLocationGraphic)
    }

    func setStatus(message: String) {
        statusLabel.text = message
    }

    /// Clear all the feature selections from previous trace.
    func clearLayersSelection() {
        layers.forEach { $0.clearSelection() }
    }

    // MARK: Actions

    func trace(completion: @escaping () -> Void) {
        guard let configuration = makeTraceConfiguration(category: selectedCategory) else {
            setStatus(message: "Failed to get trace configuration.")
            return
        }
        traceParameters.traceConfiguration = configuration

        utilityNetwork.trace(with: traceParameters) { [weak self] traceResults, error in
            guard let self = self else { return }
            if let elementTraceResult = traceResults?.first as? AGSUtilityElementTraceResult,
               !elementTraceResult.elements.isEmpty {
                self.selectFeatures(in: elementTraceResult.elements) {
                    if let categoryName = self.selectedCategory?.name.lowercased() {
                        self.setStatus(message: "Trace with \(categoryName) category completed.")
                    } else {
                        self.setStatus(message: "Trace with filter barriers completed.")
                    }
                }
            } else if let error = error {
                self.setStatus(message: "Trace failed.")
                self.presentAlert(error: error)
            } else {
                self.setStatus(message: "Trace completed with no output.")
            }
            completion()
        }
    }

    @IBAction func traceResetButtonTapped(_ button: UIBarButtonItem) {
        if traceCompleted {
            // Reset the trace if it is already completed
            clearLayersSelection()
            traceParameters.filterBarriers.removeAll()
            parametersOverlay.graphics.removeAllObjects()
            traceCompleted = false
            selectedCategory = nil
            // Add back the starting location.
            addGraphic(for: startingLocationPoint, traceLocationType: "starting point")
            mapView.setViewpointCenter(startingLocationPoint, scale: 3_000)
            // Set UI state.
            setStatus(message: "Tap on the map to add filter barriers, or run the trace directly without filter barriers.")
            traceResetBarButtonItem.title = "Trace"
            traceResetBarButtonItem.isEnabled = false
            categoryBarButtonItem.isEnabled = true
            isolationSwitch.isEnabled = true
        } else {
            UIApplication.shared.showProgressHUD(message: "Running isolation trace…")
            // Run the trace.
            trace { [weak self] in
                UIApplication.shared.hideProgressHUD()
                guard let self = self else { return }
                self.traceResetBarButtonItem.title = "Reset"
                self.categoryBarButtonItem.isEnabled = false
                self.isolationSwitch.isEnabled = false
                self.traceCompleted = true
            }
        }
    }

    @IBAction func categoryButtonTapped(_ button: UIBarButtonItem) {
        let alertController = UIAlertController(
            title: "Choose a category for filter barrier.",
            message: nil,
            preferredStyle: .actionSheet
        )
        filterBarrierCategories.forEach { category in
            let action = UIAlertAction(title: category.name, style: .default) { [self] _ in
                selectedCategory = category
                setStatus(message: "\(category.name) selected.")
                traceResetBarButtonItem.isEnabled = true
            }
            alertController.addAction(action)
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
        alertController.addAction(cancelAction)
        alertController.popoverPresentationController?.barButtonItem = categoryBarButtonItem
        present(alertController, animated: true)
    }

    // MARK: UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        // Add the source code button item to the right of navigation bar.
        (navigationItem.rightBarButtonItem as? SourceCodeBarButtonItem)?.filenames = ["PerformValveIsolationTraceViewController"]
        // Load the service geodatabase and utility network.
        setStatus(message: "Loading utility network…")
        loadServiceGeodatabase()
    }
}

// MARK: - AGSGeoViewTouchDelegate

extension PerformValveIsolationTraceViewController: AGSGeoViewTouchDelegate {
    func geoView(_ geoView: AGSGeoView, didTapAtScreenPoint screenPoint: CGPoint, mapPoint: AGSPoint) {
        // Don't identify taps if trace has completed.
        guard !traceCompleted else { return }
        identifyAction?.cancel()
        // Turn off user interaction to avoid unintended touch during identify.
        mapView.isUserInteractionEnabled = false
        identifyAction = mapView.identifyLayers(atScreenPoint: screenPoint, tolerance: 10, returnPopupsOnly: false) { [weak self] result, error in
            guard let self = self else { return }
            if let feature = result?.first?.geoElements.first as? AGSArcGISFeature {
                self.addFilterBarrier(for: feature, at: mapPoint)
            } else if let error = error {
                self.setStatus(message: "Error identifying trace locations.")
                self.presentAlert(error: error)
            }
            self.mapView.isUserInteractionEnabled = true
        }
    }

    /// Add a graphic at the tapped location for the filter barrier.
    /// - Parameters:
    ///   - feature: The geoelement retrieved as an `AGSFeature`.
    ///   - location: The `AGSPoint` used to identify utility elements in the utility network.
    func addFilterBarrier(for feature: AGSArcGISFeature, at location: AGSPoint) {
        guard let geometry = feature.geometry,
              let element = utilityNetwork.createElement(with: feature) else {
            return
        }
        let elementDidSet = { [weak self] in
            guard let self = self else { return }
            if self.categoryBarButtonItem.isEnabled {
                self.categoryBarButtonItem.isEnabled = false
                self.selectedCategory = nil
            }
            if !self.traceResetBarButtonItem.isEnabled {
                self.traceResetBarButtonItem.isEnabled = true
            }
            self.traceParameters.filterBarriers.append(element)
            let point = geometry as? AGSPoint ?? location
            self.addGraphic(for: point, traceLocationType: Self.filterBarrierIdentifier)
        }

        switch element.networkSource.sourceType {
        case .junction:
            // If the user tapped on a junction, get the asset's terminal(s).
            if let terminals = element.assetType.terminalConfiguration?.terminals {
                selectTerminal(from: terminals, at: location) { [weak self] terminal in
                    guard let self = self else { return }
                    element.terminal = terminal
                    elementDidSet()
                    self.setStatus(message: String(format: "Juntion element with terminal %@ added to the filter barriers.", terminal.name))
                }
            }
        case .edge:
            // If the user tapped on an edge, determine how far along that edge.
            if let line = AGSGeometryEngine.geometryByRemovingZ(from: geometry) as? AGSPolyline {
                element.fractionAlongEdge = AGSGeometryEngine.fraction(alongLine: line, to: location, tolerance: -1)
                elementDidSet()
                setStatus(message: String(format: "Edge element at fractionAlongEdge %.3f added to the filter barriers.", element.fractionAlongEdge))
            }
        @unknown default:
            return
        }
    }

    /// 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.
    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
            )
            terminals.forEach { terminal in
                let action = UIAlertAction(title: terminal.name, style: .default) { [terminal] _ in
                    completion(terminal)
                }
                terminalPicker.addAction(action)
            }
            let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
            terminalPicker.addAction(cancelAction)
            if let popoverController = terminalPicker.popoverPresentationController {
                // If presenting in a split view controller (e.g. on an iPad),
                // provide positioning information for the alert controller.
                popoverController.sourceView = mapView
                let tapPoint = mapView.location(toScreen: mapPoint)
                popoverController.sourceRect = CGRect(origin: tapPoint, size: .zero)
            }
            present(terminalPicker, animated: true)
        } else if let terminal = terminals.first {
            completion(terminal)
        }
    }
}

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