diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 74c0feb71..2f65254c2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,8 +26,10 @@
-
+ android:exported="true" />
+
+
-
\ No newline at end of file
diff --git a/build-logic/convention/src/main/java/com/esri/arcgismaps/kotlin/build_logic/convention/KotlinAndroid.kt b/build-logic/convention/src/main/java/com/esri/arcgismaps/kotlin/build_logic/convention/KotlinAndroid.kt
index 1e6d69482..5dcdab882 100644
--- a/build-logic/convention/src/main/java/com/esri/arcgismaps/kotlin/build_logic/convention/KotlinAndroid.kt
+++ b/build-logic/convention/src/main/java/com/esri/arcgismaps/kotlin/build_logic/convention/KotlinAndroid.kt
@@ -19,6 +19,7 @@ internal fun Project.configureKotlinAndroid(
defaultConfig {
buildConfigField("String", "ACCESS_TOKEN", project.properties["ACCESS_TOKEN"].toString())
+ manifestPlaceholders["GOOGLE_API_KEY"] = project.properties["GOOGLE_API_KEY"].toString()
minSdk = libs.findVersion("minSdk").get().toString().toInt()
}
diff --git a/samples/augment-reality-to-navigate-route/src/main/AndroidManifest.xml b/samples/augment-reality-to-navigate-route/src/main/AndroidManifest.xml
index 5da89d1bb..1dab86214 100644
--- a/samples/augment-reality-to-navigate-route/src/main/AndroidManifest.xml
+++ b/samples/augment-reality-to-navigate-route/src/main/AndroidManifest.xml
@@ -7,22 +7,15 @@
-
-
-
+
+
-
-
-
-
+ android:exported="true"
+ android:label="@string/augment_reality_to_navigate_route_app_name" />
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/README.md b/samples/augment-reality-to-show-hidden-infrastructure/README.md
new file mode 100644
index 000000000..32d8d66e0
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/README.md
@@ -0,0 +1,53 @@
+# Augment reality to show hidden infrastructure
+
+Visualize hidden infrastructure in its real-world location using augmented reality.
+
+
+
+## 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](https://github.com/Esri/arcgis-maps-sdk-kotlin-toolkit/tree/main/microapps/ArWorldScaleApp).
+ * 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](https://developers.google.com/ar/develop/authorization?platform=android#api-key-android).
+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](https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer) 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](https://developers.google.com/ar/devices).
+
+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.
+
+**World-scale AR** is one of two main patterns for working with geographic information in augmented reality currently available in the [toolkit](https://github.com/Esri/arcgis-maps-sdk-kotlin-toolkit/tree/main).
+
+Note that apps using ARCore must comply with ARCore's user privacy requirements. See [this page](https://developers.google.com/ar/develop/privacy-requirements) for more information.
+
+## Tags
+
+augmented reality, full-scale, infrastructure, lines, mixed reality, pipes, real-scale, underground, visualization, visualize, world-scale
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/README.metadata.json b/samples/augment-reality-to-show-hidden-infrastructure/README.metadata.json
new file mode 100644
index 000000000..5735c8d7b
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/README.metadata.json
@@ -0,0 +1,48 @@
+{
+ "category": "Augmented Reality",
+ "description": "Visualize hidden infrastructure in its real-world location using augmented reality.",
+ "formal_name": "AugmentRealityToShowHiddenInfrastructure",
+ "ignore": false,
+ "images": [
+ "augment-reality-to-show-hidden-infrastructure.png"
+ ],
+ "keywords": [
+ "augmented reality",
+ "full-scale",
+ "infrastructure",
+ "lines",
+ "mixed reality",
+ "pipes",
+ "real-scale",
+ "underground",
+ "visualization",
+ "visualize",
+ "world-scale",
+ "GeometryEditor",
+ "GraphicsOverlay",
+ "MultilayerPolylineSymbol",
+ "SolidStrokeSymbolLayer",
+ "Surface",
+ "WorldScaleSceneView"
+ ],
+ "language": "kotlin",
+ "redirect_from": "",
+ "relevant_apis": [
+ "GeometryEditor",
+ "GraphicsOverlay",
+ "MultilayerPolylineSymbol",
+ "SolidStrokeSymbolLayer",
+ "Surface",
+ "WorldScaleSceneView"
+ ],
+ "snippets": [
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/AugmentedRealityViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/MapViewModel.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/MainActivity.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/navigation/AugmentRealityToShowHiddenInfrastructureRouteNavGraph.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/SharedRepository.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/MapScreen.kt",
+ "src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/AugmentedRealityScreen.kt"
+ ],
+ "title": "Augment reality to show hidden infrastructure"
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/augment-reality-to-show-hidden-infrastructure.png b/samples/augment-reality-to-show-hidden-infrastructure/augment-reality-to-show-hidden-infrastructure.png
new file mode 100644
index 000000000..357798e56
Binary files /dev/null and b/samples/augment-reality-to-show-hidden-infrastructure/augment-reality-to-show-hidden-infrastructure.png differ
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/build.gradle.kts b/samples/augment-reality-to-show-hidden-infrastructure/build.gradle.kts
new file mode 100644
index 000000000..af03ba775
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/build.gradle.kts
@@ -0,0 +1,25 @@
+plugins {
+ alias(libs.plugins.arcgismaps.android.library)
+ alias(libs.plugins.arcgismaps.android.library.compose)
+ alias(libs.plugins.arcgismaps.kotlin.sample)
+ alias(libs.plugins.gradle.secrets)
+}
+
+secrets {
+ // this file doesn't contain secrets, it just provides defaults which can be committed into git.
+ defaultPropertiesFileName = "secrets.defaults.properties"
+}
+
+android {
+ namespace = "com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure"
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+dependencies {
+ // Only module specific dependencies needed here
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.ar.core)
+ implementation(libs.arcgis.maps.kotlin.toolkit.ar)
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/AndroidManifest.xml b/samples/augment-reality-to-show-hidden-infrastructure/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..aafc9e267
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/MainActivity.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/MainActivity.kt
new file mode 100644
index 000000000..e29a401d0
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/MainActivity.kt
@@ -0,0 +1,78 @@
+/* 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
+
+import android.Manifest
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.navigation.compose.rememberNavController
+import com.arcgismaps.ApiKey
+import com.arcgismaps.ArcGISEnvironment
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components.SharedRepository
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.navigation.AugmentRealityToShowHiddenInfrastructureNavGraph
+import com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
+
+class MainActivity : ComponentActivity() {
+
+ private var isLocationPermissionGranted = false
+
+ private val requestPermissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ isLocationPermissionGranted = true
+ } else {
+ Toast.makeText(this, "Location permission is required to run this sample!", Toast.LENGTH_SHORT).show()
+ }
+
+ setContent {
+ SampleAppTheme {
+ AugmentRealityToNavigateRoute(isLocationPermissionGranted)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // authentication with an API key or named user is
+ // required to access basemaps and other location services
+ ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
+ ArcGISEnvironment.applicationContext = applicationContext
+
+ requestLocationPermission()
+
+ SharedRepository.pipeInfoList.clear()
+ }
+
+ private fun requestLocationPermission() {
+ requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+
+}
+
+@Composable
+fun AugmentRealityToNavigateRoute(isLocationPermissionGranted: Boolean) {
+ val navController = rememberNavController()
+ AugmentRealityToShowHiddenInfrastructureNavGraph(
+ navController = navController,
+ isLocationPermissionGranted = isLocationPermissionGranted,
+ )
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/AugmentedRealityViewModel.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/AugmentedRealityViewModel.kt
new file mode 100644
index 000000000..c94739547
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/AugmentedRealityViewModel.kt
@@ -0,0 +1,163 @@
+/* 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.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.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 kotlinx.coroutines.launch
+
+class AugmentedRealityViewModel(app: Application) : AndroidViewModel(app) {
+
+ // 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))
+ }
+ }
+ }
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/MapViewModel.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/MapViewModel.kt
new file mode 100644
index 000000000..5d60edeef
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/MapViewModel.kt
@@ -0,0 +1,127 @@
+/* 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.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+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.GeometryType
+import com.arcgismaps.geometry.Polyline
+import com.arcgismaps.mapping.ArcGISMap
+import com.arcgismaps.mapping.BasemapStyle
+import com.arcgismaps.mapping.symbology.SimpleLineSymbol
+import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
+import com.arcgismaps.mapping.view.Graphic
+import com.arcgismaps.mapping.view.GraphicsOverlay
+import com.arcgismaps.mapping.view.LocationDisplay
+import com.arcgismaps.mapping.view.geometryeditor.GeometryEditor
+import kotlinx.coroutines.launch
+
+class MapViewModel(app: Application) : AndroidViewModel(app) {
+
+ val arcGISMap = ArcGISMap(BasemapStyle.ArcGISImagery)
+ val graphicsOverlay = GraphicsOverlay()
+ val geometryEditor = GeometryEditor()
+
+ private var _statusText by mutableStateOf("Tap on map or use current location to create start point")
+ val statusText get() = _statusText
+
+ private var _showElevationDialog by mutableStateOf(false)
+ val showElevationDialog get() = _showElevationDialog
+
+ private var _elevationInput by mutableFloatStateOf(0f)
+ val elevationInput get() = _elevationInput
+
+ var graphic = Graphic()
+
+ var polyline: Polyline? = null
+
+ private val _isGeometryBeingEdited = mutableStateOf(false)
+ val isGeometryBeingEdited: MutableState = _isGeometryBeingEdited
+
+ init {
+ startPolylineEditing()
+ }
+
+ /**
+ * Initialize the location display.
+ */
+ fun initialize(locationDisplay: LocationDisplay) {
+ with(viewModelScope) {
+ launch {
+ locationDisplay.dataSource.start()
+ }
+ launch {
+ geometryEditor.geometry.collect {
+ (it as? Polyline)?.let { polyline ->
+ _isGeometryBeingEdited.value =
+ geometryEditor.isStarted.value && (polyline.parts.firstOrNull()?.points?.toList()?.size
+ ?: 0) > 1
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts the GeometryEditor for creating a polyline.
+ */
+ fun startPolylineEditing() {
+ geometryEditor.start(GeometryType.Polyline)
+ _statusText = "Polyline editing started. Tap to add points."
+ }
+
+ /**
+ * Completes the polyline and adds it as a graphic to the map.
+ */
+ fun completePolyline() {
+ polyline = geometryEditor.stop() as? Polyline
+ if (polyline != null) {
+ graphic = Graphic(
+ polyline, symbol = SimpleLineSymbol(style = SimpleLineSymbolStyle.Solid, color = Color.red, width = 2f)
+ )
+ _showElevationDialog = true
+ graphicsOverlay.graphics.add(graphic)
+
+ _statusText = "Polyline completed. Tap again to continue adding polylines or proceed to rendering in AR."
+ } else {
+ _statusText = "No geometry created. Try again."
+ }
+ }
+
+ /**
+ * Adds the pipe information to the shared repository and resets the UI state.
+ */
+ fun onElevationConfirmed(elevation: Float) {
+ polyline?.let { pipelineGeometry ->
+ _elevationInput = elevation
+ _showElevationDialog = false
+ _isGeometryBeingEdited.value = false
+ SharedRepository.pipeInfoList.add(PipeInfo(pipelineGeometry, _elevationInput))
+ }
+ }
+}
+
+data class PipeInfo(
+ val polyline: Polyline, val elevationOffset: Float
+)
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/SharedRepository.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/SharedRepository.kt
new file mode 100644
index 000000000..b0f344cb1
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/components/SharedRepository.kt
@@ -0,0 +1,30 @@
+/* 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
+
+/**
+ * Shared repository to hold the route result generated in the route view model and passed to the augmented reality view
+ * model.
+ */
+object SharedRepository {
+
+ private var _pipeInfoList: MutableList = mutableListOf()
+ val pipeInfoList
+ get() = _pipeInfoList
+}
+
+
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/navigation/AugmentRealityToShowHiddenInfrastructureRouteNavGraph.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/navigation/AugmentRealityToShowHiddenInfrastructureRouteNavGraph.kt
new file mode 100644
index 000000000..da8ed2be2
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/navigation/AugmentRealityToShowHiddenInfrastructureRouteNavGraph.kt
@@ -0,0 +1,54 @@
+/* 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.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.R
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.screens.AugmentedRealityScreen
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.screens.MapScreen
+
+@Composable
+fun AugmentRealityToShowHiddenInfrastructureNavGraph(
+ navController: NavHostController,
+ modifier: Modifier = Modifier,
+ isLocationPermissionGranted: Boolean
+) {
+ NavHost(
+ navController = navController,
+ startDestination = "route_screen",
+ modifier = modifier
+ ) {
+ composable("route_screen") {
+ MapScreen(
+ sampleName = stringResource(R.string.augment_reality_to_show_hidden_infrastructure_app_name),
+ locationPermissionGranted = isLocationPermissionGranted,
+ onNavigateToARScreen = { navController.navigate("ar_screen") }
+ )
+
+ }
+ composable("ar_screen") {
+ AugmentedRealityScreen(
+ sampleName = stringResource(R.string.augment_reality_to_show_hidden_infrastructure_app_name)
+ )
+ }
+ }
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/AugmentedRealityScreen.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/AugmentedRealityScreen.kt
new file mode 100644
index 000000000..728b5fb85
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/AugmentedRealityScreen.kt
@@ -0,0 +1,400 @@
+/* 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.screens
+
+import android.content.Context
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withLink
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.core.content.edit
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.arcgismaps.LoadStatus
+import com.arcgismaps.toolkit.ar.WorldScaleSceneView
+import com.arcgismaps.toolkit.ar.WorldScaleSceneViewStatus
+import com.arcgismaps.toolkit.ar.WorldScaleTrackingMode
+import com.arcgismaps.toolkit.ar.rememberWorldScaleSceneViewStatus
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.R
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components.AugmentedRealityViewModel
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+
+private const val KEY_PREF_ACCEPTED_PRIVACY_INFO = "ACCEPTED_PRIVACY_INFO"
+
+@Composable
+fun AugmentedRealityScreen(sampleName: String) {
+ val augmentedRealityViewModel: AugmentedRealityViewModel = viewModel()
+
+ var displayCalibrationView by remember { mutableStateOf(false) }
+ var initializationStatus by rememberWorldScaleSceneViewStatus()
+ var trackingMode by remember { mutableStateOf(WorldScaleTrackingMode.Geospatial()) }
+
+ var showDropdownMenu by remember { mutableStateOf(false) }
+ var isPipeShadowVisible by remember { mutableStateOf(true) }
+ var isLeaderVisible by remember { mutableStateOf(true) }
+
+ val sharedPreferences = LocalContext.current.getSharedPreferences("", Context.MODE_PRIVATE)
+ var acceptedPrivacyInfo by rememberSaveable {
+ mutableStateOf(
+ sharedPreferences.getBoolean(
+ KEY_PREF_ACCEPTED_PRIVACY_INFO,
+ false
+ )
+ )
+ }
+ var showPrivacyInfo by rememberSaveable { mutableStateOf(!acceptedPrivacyInfo) }
+
+ Scaffold(
+ topBar = {
+ SampleTopAppBar(title = sampleName, actions = {
+ var actionsExpanded by remember { mutableStateOf(false) }
+ IconButton(onClick = { actionsExpanded = !actionsExpanded }) {
+ Icon(Icons.Default.MoreVert, "More")
+ }
+ DropdownMenu(
+ expanded = actionsExpanded, onDismissRequest = { actionsExpanded = false }) {
+ DropdownMenuItem(text = { Text("World tracking") }, onClick = {
+ trackingMode = WorldScaleTrackingMode.World()
+ actionsExpanded = false
+ })
+ DropdownMenuItem(text = { Text("Geospatial tracking") }, onClick = {
+ trackingMode = WorldScaleTrackingMode.Geospatial()
+ actionsExpanded = false
+ })
+ }
+ })
+ },
+ content = {
+ if (showPrivacyInfo) {
+ PrivacyInfoDialog(
+ hasCurrentlyAccepted = acceptedPrivacyInfo,
+ onUserResponse = { accepted ->
+ acceptedPrivacyInfo = accepted
+ sharedPreferences.edit { putBoolean(KEY_PREF_ACCEPTED_PRIVACY_INFO, accepted) }
+ showPrivacyInfo = false
+ }
+ )
+ } else if (!acceptedPrivacyInfo) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = "Privacy Info not accepted")
+ Button(onClick = { showPrivacyInfo = true }) {
+ Text(text = "Show Privacy Info")
+ }
+ }
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ ) {
+ WorldScaleSceneView(
+ modifier = Modifier.fillMaxSize(),
+ arcGISScene = augmentedRealityViewModel.arcGISScene,
+ graphicsOverlays = listOf(
+ augmentedRealityViewModel.pipeGraphicsOverlay,
+ augmentedRealityViewModel.pipeShadowGraphicsOverlay,
+ augmentedRealityViewModel.leaderGraphicsOverlay
+ ),
+ worldScaleTrackingMode = trackingMode,
+ onInitializationStatusChanged = { status ->
+ initializationStatus = status
+ }) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (trackingMode is WorldScaleTrackingMode.World) {
+ if (displayCalibrationView) {
+ CalibrationView(
+ onDismiss = { displayCalibrationView = false },
+ modifier = Modifier.align(Alignment.BottomCenter),
+ )
+ }
+ }
+ }
+ }
+ WorldScaleSceneViewStatusHandler(
+ initializationStatus = initializationStatus,
+ trackingMode = trackingMode,
+ arcGISSceneLoadStatus = augmentedRealityViewModel.arcGISScene.loadStatus.collectAsStateWithLifecycle().value
+ )
+ }
+ }
+ },
+ floatingActionButton = {
+ if (!displayCalibrationView) {
+ FloatingActionButtonOptions(
+ showDropdownMenu = showDropdownMenu,
+ isPipeShadowVisible = isPipeShadowVisible,
+ isLeaderVisible = isLeaderVisible,
+ trackingMode = trackingMode,
+ onToggleDropdownMenu = { showDropdownMenu = !showDropdownMenu },
+ onTogglePipeShadowVisibility = {
+ isPipeShadowVisible = !isPipeShadowVisible
+ augmentedRealityViewModel.pipeShadowGraphicsOverlay.isVisible = isPipeShadowVisible
+ },
+ onToggleLeaderVisibility = {
+ isLeaderVisible = !isLeaderVisible
+ augmentedRealityViewModel.leaderGraphicsOverlay.isVisible = isLeaderVisible
+ },
+ onShowCalibrationView = { displayCalibrationView = true }
+ )
+ }
+ }
+ )
+}
+
+@Composable
+private fun PrivacyInfoDialog(
+ hasCurrentlyAccepted: Boolean,
+ onUserResponse: (accepted: Boolean) -> Unit
+) {
+ Dialog(onDismissRequest = {
+ onUserResponse(hasCurrentlyAccepted)
+ }) {
+ Card {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ LegalTextArCore()
+ Spacer(Modifier.height(16.dp))
+ LegalTextGeospatial()
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ TextButton(onClick = {
+ onUserResponse(false)
+ }) {
+ Text(text = "Decline")
+ }
+
+ TextButton(onClick = {
+ onUserResponse(true)
+ }) {
+ Text(text = "Accept")
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun WorldScaleSceneViewStatusHandler(
+ initializationStatus: WorldScaleSceneViewStatus,
+ trackingMode: WorldScaleTrackingMode,
+ arcGISSceneLoadStatus: LoadStatus
+) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ when (initializationStatus) {
+ is WorldScaleSceneViewStatus.Initializing -> {
+ TextWithScrim(
+ if (trackingMode is WorldScaleTrackingMode.Geospatial) {
+ "Initializing AR in geospatial mode..."
+ } else {
+ "Initializing AR in world mode..."
+ }
+ )
+ }
+
+ is WorldScaleSceneViewStatus.Initialized -> {
+ when (arcGISSceneLoadStatus) {
+ is LoadStatus.Loading, LoadStatus.NotLoaded -> {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+
+ is LoadStatus.FailedToLoad -> {
+ TextWithScrim("Failed to load world scale AR scene: " + arcGISSceneLoadStatus.error)
+ }
+
+ else -> {}
+ }
+ }
+
+ is WorldScaleSceneViewStatus.FailedToInitialize -> {
+ TextWithScrim(
+ text = "World scale AR failed to initialize: " + (initializationStatus.error.message
+ ?: initializationStatus.error)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FloatingActionButtonOptions(
+ showDropdownMenu: Boolean,
+ isPipeShadowVisible: Boolean,
+ isLeaderVisible: Boolean,
+ trackingMode: WorldScaleTrackingMode,
+ onToggleDropdownMenu: () -> Unit,
+ onTogglePipeShadowVisibility: () -> Unit,
+ onToggleLeaderVisibility: () -> Unit,
+ onShowCalibrationView: () -> Unit
+) {
+ Column {
+ FloatingActionButton(
+ modifier = Modifier.padding(bottom = 18.dp),
+ onClick = onToggleDropdownMenu
+ ) {
+ Icon(Icons.Default.Settings, contentDescription = "Toggle visibility of shadows and leaders")
+ }
+ DropdownMenu(
+ expanded = showDropdownMenu,
+ onDismissRequest = onToggleDropdownMenu
+ ) {
+ DropdownMenuItem(
+ text = { Text("Shadows") },
+ leadingIcon = {
+ if (isPipeShadowVisible) {
+ Icon(Icons.Default.Done, contentDescription = "Visible")
+ }
+ },
+ onClick = onTogglePipeShadowVisibility
+ )
+ DropdownMenuItem(
+ text = { Text("Leaders") },
+ leadingIcon = {
+ if (isLeaderVisible) {
+ Icon(Icons.Default.Done, contentDescription = "Visible")
+ }
+ },
+ onClick = onToggleLeaderVisibility
+ )
+ }
+ if (trackingMode is WorldScaleTrackingMode.World) {
+ FloatingActionButton(
+ modifier = Modifier
+ .align(Alignment.End)
+ .padding(bottom = 16.dp),
+ onClick = onShowCalibrationView
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.baseline_straighten_24),
+ contentDescription = "Show calibration view"
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Displays the provided [text] on top of a half-transparent gray background.
+ */
+@Composable
+private fun TextWithScrim(text: String) {
+ Column(
+ modifier = Modifier
+ .background(androidx.compose.ui.graphics.Color.Gray.copy(alpha = 0.5f))
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = text)
+ }
+}
+
+/**
+ * Displays the required privacy information for use of ARCore
+ */
+@Composable
+private fun LegalTextArCore() {
+ val textLinkStyle =
+ TextLinkStyles(style = SpanStyle(color = androidx.compose.ui.graphics.Color.Blue))
+ Text(text = buildAnnotatedString {
+ append("This application runs on ")
+ withLink(
+ LinkAnnotation.Url(
+ "https://play.google.com/store/apps/details?id=com.google.ar.core",
+ textLinkStyle
+ )
+ ) {
+ append("Google Play Services for AR")
+ }
+ append(" (ARCore), which is provided by Google and governed by the ")
+ withLink(
+ LinkAnnotation.Url(
+ "https://policies.google.com/privacy",
+ textLinkStyle
+ )
+ ) {
+ append("Google Privacy Policy.")
+ }
+ })
+}
+
+/**
+ * Displays the required privacy information for use of the Geospatial API
+ */
+@Composable
+private fun LegalTextGeospatial() {
+ Text(text = buildAnnotatedString {
+ append("To power this session, Google will process sensor data (e.g., camera and location).")
+ appendLine()
+ withLink(
+ LinkAnnotation.Url(
+ "https://support.google.com/ar?p=how-google-play-services-for-ar-handles-your-data",
+ TextLinkStyles(style = SpanStyle(color = androidx.compose.ui.graphics.Color.Blue))
+ )
+ ) {
+ append("Learn more")
+ }
+ })
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/MapScreen.kt b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/MapScreen.kt
new file mode 100644
index 000000000..ba964446e
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/java/com/esri/arcgismaps/sample/augmentrealitytoshowhiddeninfrastructure/screens/MapScreen.kt
@@ -0,0 +1,194 @@
+/* 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.screens
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.arcgismaps.location.LocationDisplayAutoPanMode
+import com.arcgismaps.toolkit.geoviewcompose.MapView
+import com.arcgismaps.toolkit.geoviewcompose.rememberLocationDisplay
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.R
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components.MapViewModel
+import com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components.SharedRepository
+import com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
+import kotlin.math.roundToInt
+
+/**
+ * Main screen layout for the sample app
+ */
+@Composable
+fun MapScreen(sampleName: String, locationPermissionGranted: Boolean, onNavigateToARScreen: () -> Unit) {
+
+ val mapViewModel: MapViewModel = viewModel()
+
+ // Initialize the location display with auto pan mode set to recenter
+ val locationDisplay = rememberLocationDisplay().apply {
+ setAutoPanMode(LocationDisplayAutoPanMode.Recenter)
+ }
+ var isViewmodelInitialized by remember { mutableStateOf(false) }
+ LaunchedEffect(isViewmodelInitialized) {
+ if (!isViewmodelInitialized && locationPermissionGranted) {
+ mapViewModel.initialize(locationDisplay)
+ isViewmodelInitialized = true
+ }
+ }
+
+ val isGeometryBeingEdited by remember { mutableStateOf(mapViewModel.isGeometryBeingEdited) }
+ val canUndo by mapViewModel.geometryEditor.canUndo.collectAsState()
+ val showElevationDialog = mapViewModel.showElevationDialog
+ val pipeInfoList = SharedRepository.pipeInfoList
+
+ Scaffold(
+ topBar = { SampleTopAppBar(title = sampleName) }, content = {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(it),
+ ) {
+ MapView(
+ modifier = Modifier.fillMaxSize(),
+ arcGISMap = mapViewModel.arcGISMap,
+ locationDisplay = locationDisplay,
+ geometryEditor = mapViewModel.geometryEditor,
+ graphicsOverlays = listOf(mapViewModel.graphicsOverlay),
+ onSingleTapConfirmed = {
+ if (!mapViewModel.geometryEditor.isStarted.value) {
+ mapViewModel.startPolylineEditing()
+ }
+ }
+ )
+ if (mapViewModel.statusText != "") {
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(8.dp)
+ .clip(RoundedCornerShape(10.dp))
+ .background(Color.Black.copy(alpha = 0.8f))
+ .padding(16.dp)
+ ) {
+ Text(
+ text = mapViewModel.statusText, color = Color.White
+ )
+ }
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(bottom = 24.dp),
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // Allow undoing of last vertex while editing geometry
+ if (canUndo) {
+ Button (onClick = { mapViewModel.geometryEditor.undo() }) {
+ Icon(
+ painter = painterResource(R.drawable.baseline_undo_24),
+ contentDescription = "Close button"
+ )
+ }
+ }
+ // Complete polyline once enough vertices have been added
+ if (isGeometryBeingEdited.value) {
+ Button(onClick = { mapViewModel.completePolyline() }) {
+ Text("Complete polyline")
+ }
+ }
+ // Clear all polyline graphics and start over
+ if (mapViewModel.graphicsOverlay.graphics.isNotEmpty()) {
+ Button(onClick = {
+ mapViewModel.graphicsOverlay.graphics.clear()
+ mapViewModel.startPolylineEditing()
+ SharedRepository.pipeInfoList.clear()
+ }) {
+ Text("Clear polylines")
+ }
+ }
+ }
+ if (pipeInfoList.isNotEmpty()) {
+ Button(
+ onClick = onNavigateToARScreen,
+ modifier = Modifier.padding(top = 8.dp)
+ ) {
+ Text("Show hidden infrastructure in AR")
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+
+ if (showElevationDialog) {
+ var elevationInput by remember { mutableFloatStateOf(mapViewModel.elevationInput) }
+ AlertDialog(
+ onDismissRequest = { },
+ title = { Text("Elevation offset") },
+ text = {
+ Column {
+ Text("Enter a pipe elevation offset in meters between -10 and 10.")
+ Slider(
+ value = elevationInput,
+ onValueChange = { elevationInput = it },
+ valueRange = -10f..10f,
+ steps = 19
+ )
+ Text(modifier = Modifier.align(Alignment.End), text = "${elevationInput.roundToInt()} m")
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ mapViewModel.onElevationConfirmed(elevationInput)
+ }) {
+ Text("Confirm")
+ }
+ },
+ )
+ }
+}
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_straighten_24.xml b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_straighten_24.xml
new file mode 100644
index 000000000..235bc3e9f
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_straighten_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_undo_24.xml b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_undo_24.xml
new file mode 100644
index 000000000..34788b326
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/drawable/baseline_undo_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/values/strings.xml b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/values/strings.xml
new file mode 100644
index 000000000..a75fd316d
--- /dev/null
+++ b/samples/augment-reality-to-show-hidden-infrastructure/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Augment reality to show hidden infrastructure
+
diff --git a/secrets.defaults.properties b/secrets.defaults.properties
index 690696b65..b2a75c44c 100644
--- a/secrets.defaults.properties
+++ b/secrets.defaults.properties
@@ -18,5 +18,8 @@
# This properties file contains default values for runtime secrets.
# Actual secrets should be placed in `local.properties` to avoid committing sensitive information.
# This file is tracked by git; `local.properties` is not.
-# To obtain a new API key access token, visit: https://links.esri.com/create-an-api-key.
+# To obtain a new API key access token for ArcGIS Maps SDKs, visit: https://links.esri.com/create-an-api-key.
ACCESS_TOKEN="DEFAULT_ACCESS_TOKEN"
+
+# To access Google's Geospatial API obtain an API key, visit: https://developers.google.com/ar/develop/authorization?platform=android#api-key-android
+GOOGLE_API_KEY="DEFAULT_GOOGLE_API_KEY"