Skip to content

Augment reality to show hidden infrastructure

View on GitHubSample viewer app

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

Image of augment reality to show hidden infrastructure

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 offset 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. Add a WorldScaleSceneView composable to the augmented reality screen, available in the ArcGIS Maps SDK for Kotlin toolkit.
    • The component is available both in World tracking and Geospatial tracking modes. Geospatial tracking uses street view data to calibrate augmented reality positioning and is available with an ARCORE API key.
  3. Pass a SceneView into the world scale scene view and set the base surface background grid to not be visible and the base surface opacity to 0.0.
  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.

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

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).

This sample requires a device that is compatible with ARCore.

Unlike other scene samples, there's no need for a basemap while navigating, because context is provided by the camera feed showing the real environment. The base surface's opacity is set to zero to prevent it from interfering with the AR experience.

This sample uses the WorldScaleSceneView toolkit component. For information about setting up the toolkit, as well as code for the underlying component, visit the toolkit docs.

Note that apps using ARCore must comply with ARCore's user privacy requirements. See this page for more information.

Tags

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

Sample Code

AugmentedRealityViewModel.ktAugmentedRealityViewModel.ktMapViewModel.ktMainActivity.ktAugmentRealityToShowHiddenInfrastructureRouteNavGraph.ktSharedRepository.ktMapScreen.ktAugmentedRealityScreen.kt
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
/* Copyright 2025 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.
 *
 */

package com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components

import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.Polyline
import com.arcgismaps.geometry.PolylineBuilder
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.ElevationSource
import com.arcgismaps.mapping.NavigationConstraint
import com.arcgismaps.mapping.symbology.MultilayerPolylineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.SolidStrokeSymbolLayer
import com.arcgismaps.mapping.symbology.StrokeSymbolLayerLineStyle3D
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.SurfacePlacement
import com.arcgismaps.toolkit.ar.WorldScaleSceneViewProxy
import com.arcgismaps.toolkit.ar.WorldScaleVpsAvailability
import kotlinx.coroutines.launch

class AugmentedRealityViewModel(app: Application) : AndroidViewModel(app) {

    val worldScaleSceneViewProxy = WorldScaleSceneViewProxy()

    var isVpsAvailable by mutableStateOf(false)

    // Graphics overlay for the 3D pipes
    val pipeGraphicsOverlay = GraphicsOverlay().apply {
        sceneProperties.surfacePlacement = SurfacePlacement.Absolute
    }

    // Graphics overlay for the shadow of pipes underground
    val pipeShadowGraphicsOverlay = GraphicsOverlay().apply {
        opacity = 0.6f
    }

    // Graphics overlay for the leaders
    val leaderGraphicsOverlay = GraphicsOverlay().apply {
        sceneProperties.surfacePlacement = SurfacePlacement.Absolute
    }

    // Create a scene with an elevation source and grid and surface hidden
    val arcGISScene = ArcGISScene().apply {
        baseSurface.apply {
            elevationSources.add(ElevationSource.fromTerrain3dService())
            backgroundGrid.isVisible = false
            opacity = 0.0f
            navigationConstraint = NavigationConstraint.None
        }
    }

    // Define a red 3D stroke symbol to show the pipe
    private val pipeStrokeSymbol = SolidStrokeSymbolLayer(
        width = 0.3,
        color = Color.red,
        lineStyle3D = StrokeSymbolLayerLineStyle3D.Tube
    )
    val pipeSymbol = MultilayerPolylineSymbol(listOf(pipeStrokeSymbol))

    // Define a red 2D stroke symbol to show the pipe shadow
    private val pipeShadowSymbol = SimpleLineSymbol(
        style = SimpleLineSymbolStyle.Solid,
        color = Color.yellow,
        width = 0.3f
    )

    val leaderSymbol = SimpleLineSymbol(
        style = SimpleLineSymbolStyle.Dash,
        color = Color.red,
        width = 0.1f
    )

    init {
        // For each pipe in the shared repository
        SharedRepository.pipeInfoList.forEach {
            viewModelScope.launch {
                // Densify the polyline to ensure it has enough points for elevation sampling
                val densifiedPolyline = GeometryEngine.densifyGeodeticOrNull(
                    geometry = it.polyline,
                    maxSegmentLength = 1.0,
                    lengthUnit = LinearUnit.meters,
                    curveType = GeodeticCurveType.Geodesic
                ) as Polyline
                // Add Z values to the polyline using the base surface elevation and elevation offset
                val densifiedPolylineWithZ = addZValues(densifiedPolyline, it.elevationOffset)
                // Add the 3D pipe to the pipe graphics overlay
                pipeGraphicsOverlay.graphics.add(Graphic(densifiedPolylineWithZ, pipeSymbol))
                // Only add the shadow if the pipe is underground
                if (it.elevationOffset < 0) {
                    // Add the 2D pipe shadow to the shadow graphics overlay
                    pipeShadowGraphicsOverlay.graphics.add(Graphic(it.polyline, pipeShadowSymbol))
                    // Get the original polyline with Z values
                    val originalPolylineWithZ = addZValues(it.polyline, it.elevationOffset)
                    // Add leader lines connecting pipe vertices to shadow vertices
                    addLeaderLines(originalPolylineWithZ, it.elevationOffset)
                }
            }
        }
    }

    /**
     * Adds Z values to the polyline by getting the elevation from the base surface.
     */
    private suspend fun addZValues(polyline: Polyline, elevationOffset: Float): Polyline {
        // Create a new polyline builder to construct the polyline with Z values
        val polylineBuilder = PolylineBuilder(SpatialReference(3857))
        // For each point in each part of the densified polyline
        polyline.parts.forEach { part ->
            part.points.forEach { point ->
                arcGISScene.baseSurface.elevationSources.first().load().onSuccess {
                    arcGISScene.baseSurface.getElevation(point).let { elevationResult ->
                        // Get the elevation at the point
                        elevationResult.getOrNull()?.let { elevation ->
                            // Add the point with the elevation offset to the polyline builder
                            polylineBuilder.addPoint(
                                GeometryEngine.createWithZ(
                                    point,
                                    elevation + elevationOffset
                                )
                            )
                        }
                    }
                }
            }
        }
        return polylineBuilder.toGeometry()
    }

    /**
     * Adds leader lines from the pipe vertices to the shadow vertices.
     */
    private fun addLeaderLines(pipePolyline: Polyline, elevationOffset: Float) {
        // For each point in each part of the densified polyline
        pipePolyline.parts.forEach { part ->
            part.points.forEach { point ->
                // Create a line from the 3D pipe vertex to a pont offset by the elevation offset
                val offsetPoint = GeometryEngine.createWithZ(
                    point,
                    point.z?.minus(elevationOffset)
                )
                val leaderLine = Polyline(listOf(point, offsetPoint))
                leaderGraphicsOverlay.graphics.add(Graphic(leaderLine, leaderSymbol))
            }
        }
    }

    /**
     * Checks if the current viewpoint camera location is within the VPS availability area.
     */
    fun onCurrentViewpointCameraChanged(location: Point) {
        viewModelScope.launch {
            worldScaleSceneViewProxy.checkVpsAvailability(location.y, location.x).onSuccess {
                isVpsAvailable = it == WorldScaleVpsAvailability.Available
            }
        }
    }
}

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