Augment reality to show hidden infrastructure

View on GitHub

Visualize hidden infrastructure in its real-world location using augmented reality.

Image of Augment reality to show hidden infrastructure 1 Image of Augment reality to show hidden infrastructure 2

Use case

You can use AR to "x-ray" the ground to see pipes, wiring, or other infrastructure that isn't otherwise visible. For example, you could use this feature to trace the flow of water through a building to help identify the source of a leak.

How to use the sample

When you open the sample, you'll see a map centered on your current location. Tap on the map to draw pipes around your location. After drawing the pipes, input an elevation value to place the drawn infrastructure above or below ground. When you are ready, tap the camera button to view the infrastructure you drew in AR.

How it works

  1. Draw pipes on the map. See the "Create and edit geometries" sample to learn how to use the geometry editor for creating graphics.
  2. When you start the AR visualization experience, create and show the WorldScaleSceneView.
  3. Pass a SceneView into the world scale scene view and set the space effect transparent and the atmosphere effect to off.
  4. Create an ArcGISTiledElevationSource and add it to the scene's base surface. Set the navigation constraint to unconstrained to allow going underground if needed.
  5. Configure a graphics overlay and renderer for showing the drawn pipes. This sample uses a SolidStrokeSymbolLayer with a MultilayerPolylineSymbol to draw the pipes as tubes. Add the drawn pipes to the overlay.

Relevant API

  • GeometryEditor
  • GraphicsOverlay
  • MultilayerPolylineSymbol
  • SolidStrokeSymbolLayer
  • Surface
  • WorldScaleSceneView

About the data

This sample uses Esri's world elevation service to ensure that the infrastructure you create is accurately placed beneath the ground.

Real-scale AR relies on having data in real-world locations near the user. It isn't practical to provide pre-made data like other ArcGIS Maps SDKs for Native Apps samples, so you must draw your own nearby sample "pipe infrastructure" prior to starting the AR experience.

Additional information

Note that unlike other scene samples, a basemap isn't shown most of the time, because the real world provides the context. Only while calibrating is the basemap displayed at 50% opacity, to give the user a visual reference to compare to.

You may notice that pipes you draw underground appear to float more than you would expect. That floating is a normal result of the parallax effect that looks unnatural because you're not used to being able to see underground/obscured objects. Compare the behavior of underground pipes with equivalent pipes drawn above the surface - the behavior is the same, but probably feels more natural above ground because you see similar scenes day-to-day (e.g. utility wires).

World-scale AR is one of three main patterns for working with geographic information in augmented reality. Augmented reality is made possible with the ArcGIS Maps SDK Toolkit. See Augmented reality in the guide for more information about augmented reality and adding it to your app.

Tags

augmented reality, full-scale, infrastructure, lines, mixed reality, pipes, real-scale, underground, visualization, visualize, world-scale

Sample Code

AugmentRealityToShowHiddenInfrastructureView.swiftAugmentRealityToShowHiddenInfrastructureView.swiftAugmentRealityToShowHiddenInfrastructureView.ARSceneView.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
// Copyright 2024 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 CoreLocation
import SwiftUI

struct AugmentRealityToShowHiddenInfrastructureView: View {
    /// The view model for the map view in the sample.
    @StateObject private var model = MapModel()

    /// The status message in the overlay.
    @State private var statusMessage = "Tap the map to add pipe points."

    /// A Boolean value indicating whether there are graphics to be deleted.
    @State private var canDelete = false

    /// A Boolean value indicating whether the current geometry edits can be added as a pipe.
    @State private var canApplyEdits = false

    /// A Boolean value indicating whether the geometry editor can undo.
    @State private var geometryEditorCanUndo = false

    /// A Boolean value indicating whether the alert for entering an elevation offset is showing.
    @State private var elevationAlertIsPresented = false

    /// The error shown in the error alert.
    @State private var error: Error?

    var body: some View {
        MapView(map: model.map, graphicsOverlays: [model.pipesGraphicsOverlay])
            .locationDisplay(model.locationDisplay)
            .geometryEditor(model.geometryEditor)
            .toolbar {
                ToolbarItemGroup(placement: .bottomBar) {
                    toolbarButtons
                }
            }
            .overlay(alignment: .top) {
                instructionText
            }
            .elevationOffsetAlert(isPresented: $elevationAlertIsPresented) { elevationOffset in
                model.addPipe(elevationOffset: elevationOffset)
                canDelete = true

                if elevationOffset < 0 {
                    statusMessage = "Pipe added \(elevationOffset.formatted()) meter(s) below surface."
                } else if elevationOffset.isZero {
                    statusMessage = "Pipe added at ground level."
                } else {
                    statusMessage = "Pipe added \(elevationOffset.formatted()) meter(s) above surface."
                }
                statusMessage.append("\nTap the camera to view the pipe(s) in AR.")

                model.geometryEditor.start(withType: Polyline.self)
            }
            .task {
                do {
                    try await model.startLocationDisplay()
                } catch {
                    self.error = error
                }

                // Start the geometry editor and listen for its geometry updates.
                model.geometryEditor.start(withType: Polyline.self)

                for await geometry in model.geometryEditor.$geometry {
                    let polyline = geometry as? Polyline
                    canApplyEdits = polyline?.parts.contains { $0.points.count >= 2 } ?? false
                    if canApplyEdits {
                        statusMessage = "Tap the check mark to add the pipe."
                    }

                    geometryEditorCanUndo = model.geometryEditor.canUndo
                }
            }
            .errorAlert(presentingError: $error)
    }

    /// The buttons in the bottom toolbar.
    @ViewBuilder private var toolbarButtons: some View {
        Button {
            if geometryEditorCanUndo {
                model.geometryEditor.undo()
            } else {
                model.removeAllGraphics()
                canDelete = false
                statusMessage = "Tap the map to add pipe points."
            }
        } label: {
            Image(systemName: geometryEditorCanUndo ? "arrow.uturn.backward" : "trash")
        }
        .disabled(!geometryEditorCanUndo && !canDelete)
        Spacer()

        NavigationLink {
            ARPipesSceneView(model: model.sceneModel)
        } label: {
            Image(systemName: "camera")
        }
        .disabled(geometryEditorCanUndo || !canDelete)
        Spacer()

        Button("Done", systemImage: "checkmark") {
            elevationAlertIsPresented = true
        }
        .disabled(!canApplyEdits)
    }

    /// The instruction text in the overlay.
    private var instructionText: some View {
        Text(statusMessage)
            .multilineTextAlignment(.center)
            .frame(maxWidth: .infinity, alignment: .center)
            .padding(8)
            .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
    }
}

private extension AugmentRealityToShowHiddenInfrastructureView {
    // MARK: Map Model

    /// The view model for the map view in the sample.
    @MainActor
    class MapModel: ObservableObject {
        /// A map with an imagery basemap style.
        let map = Map(basemapStyle: .arcGISImagery)

        /// The graphics overlay for the 2D pipe graphics.
        let pipesGraphicsOverlay: GraphicsOverlay = {
            let graphicsOverlay = GraphicsOverlay()
            let redLineSymbol = SimpleLineSymbol(style: .solid, color: .red, width: 2)
            graphicsOverlay.renderer = SimpleRenderer(symbol: redLineSymbol)
            return graphicsOverlay
        }()

        /// The location display for showing the user's current location.
        let locationDisplay: LocationDisplay = {
            let locationDisplay = LocationDisplay(dataSource: SystemLocationDataSource())
            locationDisplay.autoPanMode = .recenter
            locationDisplay.initialZoomScale = 1000
            return locationDisplay
        }()

        /// The geometry editor for creating polylines representing pipes.
        let geometryEditor = GeometryEditor()

        /// The view model for scene view in the sample.
        let sceneModel = SceneModel()

        /// Starts the location display to show user's location on the map.
        func startLocationDisplay() async throws {
            // Request location permission if it has not yet been determined.
            let locationManager = CLLocationManager()
            if locationManager.authorizationStatus == .notDetermined {
                locationManager.requestWhenInUseAuthorization()
            }

            // Start the location display to zoom to the user's current location.
            try await locationDisplay.dataSource.start()
        }

        /// Adds pipe graphics to the map and scene using the current geometry editor edits.
        /// - Parameter elevationOffset: The elevation to offset the pipe with in the scene.
        func addPipe(elevationOffset: Double) {
            guard let polyline = geometryEditor.stop() as? Polyline else { return }

            let pipeGraphic = Graphic(geometry: polyline)
            pipesGraphicsOverlay.addGraphic(pipeGraphic)

            Task {
                await sceneModel.addGraphics(for: polyline, elevationOffset: elevationOffset)
            }
        }

        /// Removes the graphics from the map and scene graphics overlays.
        func removeAllGraphics() {
            pipesGraphicsOverlay.removeAllGraphics()

            sceneModel.pipeGraphicsOverlay.removeAllGraphics()
            sceneModel.shadowGraphicsOverlay.removeAllGraphics()
            sceneModel.leaderGraphicsOverlay.removeAllGraphics()
        }
    }

    // MARK: Elevation Alert

    /// An alert that allows the user to enter an elevation offset for a pipe.
    struct ElevationOffsetAlert: ViewModifier {
        /// A binding to a Boolean value that determines whether to present the alert.
        @Binding var isPresented: Bool

        /// The action to perform when the user presses "Done".
        let action: (Double) -> Void

        /// The text in the text field.
        @State private var text = ""

        /// A Boolean value indicating whether the invalid elevation alert is showing.
        @State private var invalidAlertIsPresented = false

        func body(content: Content) -> some View {
            content
                .alert("Enter an Elevation", isPresented: $isPresented) {
                    TextField("Enter elevation", text: $text)
                        .keyboardType(.numbersAndPunctuation)

                    Button("Cancel", role: .cancel, action: {})

                    Button("Done") {
                        if let elevationOffset = Double(text),
                           -10...10 ~= elevationOffset {
                            action(elevationOffset)
                            text.removeAll()
                        } else {
                            invalidAlertIsPresented = true
                        }
                    }
                } message: {
                    Text("Enter a pipe elevation offset in meters between -10 and 10.")
                }
                .alert("Invalid Elevation", isPresented: $invalidAlertIsPresented) {
                    Button("OK") {
                        isPresented = true
                    }
                } message: {
                    Text("\"\(text)\" is not a valid elevation offset.\nEnter a value between -10 and 10.")
                }
        }
    }
}

private extension View {
    /// Presents an alert that allows the user to enter an elevation offset for a pipe.
    /// - Parameters:
    ///   - isPresented: A binding to a Boolean value that determines whether to present the alert.
    ///   - action: The action to perform when the user presses "Done".
    /// - Returns: A new `View`.
    func elevationOffsetAlert(
        isPresented: Binding<Bool>,
        action: @escaping (Double) -> Void
    ) -> some View {
        self.modifier(
            AugmentRealityToShowHiddenInfrastructureView.ElevationOffsetAlert(
                isPresented: isPresented,
                action: action
            )
        )
    }
}

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