
Viewshed analysis determines the visibility of terrain, buildings, and other 3D objects from an observer’s location within a scene (using a specified field of view). The result indicates which areas are visible and which are obstructed when viewed from the observer’s perspective.
In this tutorial, you will perform and display a viewshed analysis in a web scene. Your viewshed analysis will show visibility (visible or obstructed) and can be used to determine which hotspots in the Yosemite Valley are visible from a specified observer’s perspective.
Prerequisites
Before starting this tutorial, you need the following:
-
An ArcGIS Location Platform or ArcGIS Online account.
-
A development and deployment environment that meets the system requirements.
-
An IDE for Android development in Kotlin.
Develop or download
You have two options for completing this tutorial:
Option 1: Develop the code
Open an Android Studio project
-
Open the project you created by completing the Display a scene tutorial.
-
Continue with the following instructions to perform and display a viewshed analysis in a 3D scene.
-
Modify the old project for use in this new tutorial.
-
On your file system, delete the .idea folder, if present, at the top level of your project.
-
In the Android view, open app > res > values > strings.xml.
In the
<string name="app_name">element, change the text content to Display a viewshed.strings.xml<resources><string name="app_name">Display a viewshed</string></resources> -
In the Android view, open Gradle Scripts > settings.gradle.kts.
Change the value of
rootProject.nameto “Display a viewshed”.settings.gradle.kts14 collapsed linespluginManagement {repositories {google {content {includeGroupByRegex("com\\.android.*")includeGroupByRegex("com\\.google.*")includeGroupByRegex("androidx.*")}}mavenCentral()gradlePluginPortal()}}dependencyResolutionManagement {repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)repositories {google()mavenCentral()maven { url = uri("https://esri.jfrog.io/artifactory/arcgis") }}}rootProject.name = "Display a viewshed"include(":app") -
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
-
In
libs.versions.toml, add a [libraries] entry for the dependency. In the module-levelbuild.gradle.kts (app), add the dependency for the lifecycle view model.19 collapsed lines[versions]arcgisMapsKotlin = "300.0.0"# Version numbers added by Android Studio New Project Wizardagp = "8.12.1"kotlin = "2.2.10"coreKtx = "1.17.0"junit = "4.13.2"junitVersion = "1.3.0"espressoCore = "3.7.0"lifecycleRuntimeKtx = "2.9.2"activityCompose = "1.10.1"composeBom = "2025.08.00"# Other version numberscompileSdk = "36"minSdk = "28"targetSdk = "36"[libraries]arcgis-maps-kotlin = { group = "com.esri", name = "arcgis-maps-kotlin", version.ref = "arcgisMapsKotlin" }arcgis-maps-kotlin-toolkit-bom = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-bom", version.ref = "arcgisMapsKotlin" }arcgis-maps-kotlin-toolkit-geoview-compose = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-geoview-compose" }arcgis-maps-kotlin-toolkit-authentication = { group = "com.esri", name = "arcgis-maps-kotlin-toolkit-authentication" }androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }junit = { group = "junit", name = "junit", version.ref = "junit" }androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }15 collapsed linesandroidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }androidx-ui = { group = "androidx.compose.ui", name = "ui" }androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }androidx-material3 = { group = "androidx.compose.material3", name = "material3" }[plugins]android-application = { id = "com.android.application", version.ref = "agp" }kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }47 collapsed linesplugins {alias(libs.plugins.android.application)alias(libs.plugins.kotlin.android)alias(libs.plugins.kotlin.compose)}android {namespace = "com.example.app"compileSdk = libs.versions.compileSdk.get().toInt()defaultConfig {applicationId = "com.example.app"minSdk = libs.versions.minSdk.get().toInt()targetSdk = libs.versions.targetSdk.get().toInt()versionCode = 1versionName = "1.0"testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"vectorDrawables {useSupportLibrary = true}}buildTypes {release {isMinifyEnabled = falseproguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")}}compileOptions {sourceCompatibility = JavaVersion.VERSION_17targetCompatibility = JavaVersion.VERSION_17}kotlinOptions {jvmTarget = "17"}buildFeatures {compose = true}packaging {resources {excludes += "/META-INF/{AL2.0,LGPL2.1}"}}}dependencies {implementation(libs.androidx.core.ktx)implementation(libs.androidx.lifecycle.runtime.ktx)implementation(libs.androidx.activity.compose)implementation(platform(libs.androidx.compose.bom))implementation(libs.androidx.lifecycle.viewmodel.compose)implementation(libs.androidx.ui)implementation(libs.androidx.ui.graphics)19 collapsed linesimplementation(libs.androidx.ui.tooling.preview)implementation(libs.androidx.material3)testImplementation(libs.junit)androidTestImplementation(libs.androidx.junit)androidTestImplementation(libs.androidx.espresso.core)androidTestImplementation(platform(libs.androidx.compose.bom))androidTestImplementation(libs.androidx.ui.test.junit4)debugImplementation(libs.androidx.ui.tooling)debugImplementation(libs.androidx.ui.test.manifest)// ArcGIS Maps for Kotlin - SDK dependencyimplementation(libs.arcgis.maps.kotlin)// Toolkit dependenciesimplementation(platform(libs.arcgis.maps.kotlin.toolkit.bom))implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose)implementation(libs.arcgis.maps.kotlin.toolkit.authentication)} -
Click File > Sync Project with Gradle files.
Add imports
Modify import statements to reference the packages and classes required for this tutorial.
@file:OptIn(ExperimentalMaterial3Api::class)
package com.example.app.screens
import android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntGet the web scene item ID
You can use ArcGIS tools
- Go to the Yosemite Valley Hotspots web scene in the Scene Viewer in ArcGIS Online
ArcGIS Online is a GIS mapping, analytics, data hosting, and content management software as a service (SaaS) product. It includes applications, tools, APIs, and location services for users and developers. It is subscription-based and requires an ArcGIS Online account. . This web scene displays terrain and hotspots in the Yosemite Valley. - Make a note of the item ID
An item ID is a unique identifier representing a single item stored, managed, and accessed in a portal, such as a web map, hosted layer, or file. at the end of the browser’s URL. The item ID should be 7558ee942b2547019f66885c44d4f0b1.
Create a view model
Modern app architecture uses a scene view model to hold the business logic and the mutual state of your app.
-
In Android Studio: in the Android view, open app > kotlin+java > com.example.app > screens > MainScreen.kt
Create a scene view model that extends
ViewModel.MainScreen.kt42 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {}5 collapsed lines@Composablefun MainScreen() {} -
In the
MainScreencomposable, delete the entire function body. Leave just the function declaration as shown below. Then delete the top-level code for creating a scene that is part of the Display a scene tutorial. In this tutorial, you will create the scene within the view model.MainScreen.kt109 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}}@Composablefun MainScreen() {}MainScreen.kt25 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.rememberimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.ArcGISTiledElevationSourceimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Surfaceimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.example.app.Rfun createScene(): ArcGISScene {// add base surface for elevation dataval elevationSource = ArcGISTiledElevationSource("https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer")val surface = Surface().apply {elevationSources.add(elevationSource)// add an exaggeration factor to increase the 3D effect of the elevation.elevationExaggeration = 2.5f}val cameraLocation = Point(x = -118.794,y = 33.909,z = 5330.0,spatialReference = SpatialReference.wgs84())val camera = Camera(locationPoint = cameraLocation,heading = 355.0,pitch = 72.0,roll = 0.0)return ArcGISScene(BasemapStyle.ArcGISImagery).apply {baseSurface = surfaceinitialViewpoint = Viewpoint(cameraLocation, camera)}}20 collapsed lines@Composablefun MainScreen() {val scene = remember {createScene()}Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) {SceneView(modifier = Modifier.fillMaxSize().padding(it),arcGISScene = scene)}} -
In the view model, create an
ArcGISScenefrom aPortalItem. Then create aSceneViewProxy.MainScreen.kt44 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()8 collapsed lines}@Composablefun MainScreen() {}
Create a viewshed analysis
Visual analyses are used to help you make sense of complex 3D data contained by a scene. Use an ExploratoryLocationViewshed to perform and display a viewshed
-
In the view model, create an
AnalysisOverlaynamedanalysisOverlayto contain and display the viewshed analyses.An analysis overlay is a container for
Analysisobjects. It is used with a scene viewA scene view is a user interface that displays scene layers and graphics in 3D. It uses a camera to control the visible area of the scene and supports user interactions such as pan, zoom, tilt, and rotate. to display visual analyses on a sceneA scene is a collection of layers that are displayed in 3D. It is typically composed of a basemap layer, data layers, and 3D data. . You can add more than one analysis overlay to a scene view. Analysis overlays are displayed on top of all other layersA layer is a reference to a collection of geographic data that is used to access and display data. The data for layers are typically provided by the basemap layer service and data services. and graphics overlaysA graphics overlay is a client-side, temporary container of graphics to display on a map view or scene view. .SceneViewModel class58 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()8 collapsed lines}@Composablefun MainScreen() {} -
Create an
ExploratoryLocationViewshednamedviewshed.The viewshed analysis is added to a scene view
A scene view is a user interface that displays scene layers and graphics in 3D. It uses a camera to control the visible area of the scene and supports user interactions such as pan, zoom, tilt, and rotate. using an analysis overlay. An analysis overlay is a container for analyses. It can be used to display visual analyses in a scene view. You can add more than one analysis overlay, and they are displayed on top of all other layersA layer is a reference to a collection of geographic data that is used to access and display data. The data for layers are typically provided by the basemap layer service and data services. .SceneViewModel class61 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)8 collapsed lines}@Composablefun MainScreen() {} -
In the
initblock, make the viewshed not visible upon launch and add the viewshed to the analysis overlay. Then load the scene. Sinceload()is a suspend function, call it fromviewModelScope.launch.SceneViewModel class71 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}8 collapsed lines}@Composablefun MainScreen() {} -
Define a function named
setViewshedLocation()that takes aPointas a parameter. This function sets the location of the viewshed and makes it visible, if it is not visible already.SceneViewModel class80 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}8 collapsed lines}@Composablefun MainScreen() {} -
Define a function named
hideViewshed(), which hides the viewshed without modifying or deleting it. Then create a function namedsetViewshedDistance()to set a new maximum distance value for the viewshed.SceneViewModel class85 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}8 collapsed lines}@Composablefun MainScreen() {}
Display the viewshed analysis with touch events
Touch events determine where to place the observer for the viewshed analysis. A user will long-press and drag to reveal and move the observer’s location.
-
Define a function named
handleLongPressEvent()to add or move the viewshed analysis when the user long-presses on the scene. To obtain the new location (Point) in the scene, callscreenToLocation()on theSceneViewProxy, passing in the screen coordinate of the long-press. Then call yoursetViewshedLocation()with the scene point.SceneViewModel class93 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}8 collapsed lines}@Composablefun MainScreen() {} -
Define a function to log errors.
SceneViewModel class102 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}8 collapsed lines}@Composablefun MainScreen() {}
Add a UI to control the viewshed analysis
To control the viewshed analysis, some UI elements are required.
-
Create variables in the
MainScreencomposable.- Create
sceneViewModelusingviewModel(). - Create
sliderValueof typeMutableFloatState. Sinceviewshed.maxDistancein the view model is aDouble, but thevaluepassed to Jetpack ComposeSlidercomposable is aFloat, you must convert the max distance to aFloat.
MainScreen()109 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}}@Composablefun MainScreen() {val sceneViewModel: SceneViewModel = viewModel()// If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0var sliderValue by remember {mutableFloatStateOf(sceneViewModel.viewshed.maxDistance?.toFloat() ?: 1000.0f)}} - Create
-
Add a
Scaffoldand pass aTopAppBar. TheScaffold’s content lambda contains aColumn.MainScreen()109 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}}@Composablefun MainScreen() {val sceneViewModel: SceneViewModel = viewModel()// If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0var sliderValue by remember {mutableFloatStateOf(sceneViewModel.viewshed.maxDistance?.toFloat() ?: 1000.0f)}Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.spacedBy(12.dp)) {}}} -
In the
Column’s content lambda, add theSceneViewcomposable. For thearcGISScene,sceneViewProxy, andanalysisOverlaysparameters, pass the corresponding properties fromsceneViewModel. ForonLongPress, pass a lambda that callssceneViewModel.handleOnLongPress().MainScreen()118 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}}@Composablefun MainScreen() {val sceneViewModel: SceneViewModel = viewModel()// If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0var sliderValue by remember {mutableFloatStateOf(sceneViewModel.viewshed.maxDistance?.toFloat() ?: 1000.0f)}Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.spacedBy(12.dp)) {SceneView(modifier = Modifier.fillMaxSize().weight(1f),arcGISScene = sceneViewModel.scene,sceneViewProxy = sceneViewModel.sceneViewProxy,analysisOverlays = listOf(sceneViewModel.analysisOverlay),onLongPress = { longPressEvent ->sceneViewModel.handleOnLongPress(longPressEvent.screenCoordinate)})}}2 collapsed lines} -
Add a
Textto display the current value of the slider. -
Add a
Rowthat will contain aSliderand aTextButton.The slider allows the user to adjust the maximum distance of the viewshed in the range of
0f..20999f. Thevalueparameter is the current value of the slider, which is held in thesliderValuevariable of theMainScreencomposable. For theonValuechangeparameters pass a lambda that saves the new value of the slider in thesliderValuevariable and then callssetViewshedDistance()in the view model. Changing the maximum distance of the viewshed expands or contracts the size of the observer’s field of view.The text button says “Clear” and calls the view model’s
hideViewshed()when clicked. This allows the user a fresh start to make more analyses.MainScreen()139 collapsed lines@file:OptIn(ExperimentalMaterial3Api::class)package com.example.app.screensimport android.util.Logimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.PortalItemimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.ScreenCoordinateimport com.arcgismaps.portal.Portalimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.example.app.Rimport kotlinx.coroutines.launchimport kotlin.math.roundToIntclass SceneViewModel : ViewModel() {private val portal = Portal(url = "https://www.arcgis.com",connection = Portal.Connection.Anonymous)private val portalItem = PortalItem(portal = portal,itemId = "7558ee942b2547019f66885c44d4f0b1")var scene = ArcGISScene(portalItem)val sceneViewProxy = SceneViewProxy()// The analysis overlay to be added to the scene.val analysisOverlay = AnalysisOverlay()var viewshed = ExploratoryLocationViewshed(location = Point(x = 0.0, y = 0.0),heading = 0.0,pitch = 90.0,horizontalAngle = 360.0,verticalAngle = 180.0,minDistance = 10.0,maxDistance = 12_000.0)init {viewshed.isVisible = falseanalysisOverlay.analyses.add(viewshed)viewModelScope.launch {scene.load().getOrElse { logError(it) }}}private fun setViewshedLocation(point: Point) {viewshed.location = pointviewshed.isVisible = true}fun hideViewshed() {viewshed.isVisible = false}fun setViewshedDistance(maxDistance: Double) {viewshed.maxDistance = maxDistance}fun handleOnLongPress(screenCoordinate: ScreenCoordinate) {viewModelScope.launch {val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error ->return@launch logError(error)}setViewshedLocation(scenePoint)}}private fun logError(error: Throwable) {Log.e(this.javaClass.simpleName, error.message.toString(), error.cause)}}@Composablefun MainScreen() {val sceneViewModel: SceneViewModel = viewModel()// If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0var sliderValue by remember {mutableFloatStateOf(sceneViewModel.viewshed.maxDistance?.toFloat() ?: 1000.0f)}Scaffold(topBar = { TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) }) { innerPadding ->Column(modifier = Modifier.padding(innerPadding),horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.spacedBy(12.dp)) {SceneView(modifier = Modifier.fillMaxSize().weight(1f),arcGISScene = sceneViewModel.scene,sceneViewProxy = sceneViewModel.sceneViewProxy,analysisOverlays = listOf(sceneViewModel.analysisOverlay),onLongPress = { longPressEvent ->sceneViewModel.handleOnLongPress(longPressEvent.screenCoordinate)})Text(text = "Viewshed max distance: "+sliderValue.roundToInt().toString())Row {Slider(modifier = Modifier.weight(1f),value = sliderValue,valueRange = 0f..20999f,onValueChange = {sliderValue = itsceneViewModel.setViewshedDistance(sliderValue.toDouble())},)TextButton(onClick = { sceneViewModel.hideViewshed() }) {Text(text = "Clear")}}5 collapsed lines}}}
Click Run > Run > app to run the app.
You should see a scene of hotspots in the Yosemite Valley. Long-press and drag to display and move a viewshed analysis to explore the visibility of terrain from various locations.
Alternatively, you can download the tutorial solution, as follows.
Option 2: Download the solution
-
Click the Download solution link in the right-hand side of this page.
-
Unzip the file to a location on your machine.
-
Run Android Studio.
-
Go to File > Open…. Navigate to the solution folder and click Open.
On Windows: If you are in the Welcome to Android Studio dialog, click Open and navigate to the solution folder. Then click Open.
Since the downloaded solution does not contain authentication credentials, you must first set up authentication to create credentials, and then add the developer credentials to the solution.
Set up authentication
To access the secure ArcGIS location services
You can implement API key authentication or user authentication in this tutorial. Compare the differences below:
API key authentication
- Users are not required to sign in.
- Requires creating an API key credential
API key credentials are an item that contains the parameters used to create and manage long-lived access tokens for API key authentication. They are a type of developer credential. with the correct privileges. - API keys
An API key is a long-lived access token created using API key credentials. They are valid for up to one year and are typically embedded directly into client applications. are long-lived access tokens. - Service usage is billed to the API key owner/developer.
- Simplest authentication method to implement.
- Recommended approach for new ArcGIS developers.
Learn more in API key authentication.
User authentication
- Users are required to sign in with an ArcGIS account
An ArcGIS account is an identity with a user type and set of privileges that can access specific ArcGIS products, tools, APIs, services, and resources. The main account types that can be used for development are an ArcGIS Location Platform account, ArcGIS Online account, and ArcGIS Enterprise account. ArcGIS Location Platform and ArcGIS Online accounts are also associated with a subscription. . - User accounts must have privilege
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. to access the ArcGIS servicesA service, also known as an ArcGIS service, is software that supports an ArcGIS REST API and provides geospatial functionality or data. A service can be hosted by Esri or in ArcGIS Enterprise. used in application. - Requires creating OAuth credentials
OAuth credentials are an item that contains parameters required to implement user authentication or app authentication, including a .client_id,client_secret, and redirect URIs. They are a type of developer credential. - Application uses a redirect URL and client ID.
- Service usage is billed to the organization of the user signed into the application.
Learn more in User authentication.
To complete this tutorial, click on the tab in the switcher below for your authentication type of choice, either API key authentication or User authentication.
Create a new API key access token
-
Complete the Create an API key tutorial and create an API key with the following privilege(s)
Privileges are a set of permissions assigned to ArcGIS accounts, developer credentials, and applications that grant access to secure resources and functionality in ArcGIS. :- Privileges
- Location services > Basemaps
- Privileges
-
Copy and paste the API key access token into a safe location. It will be used in a later step.
Create new OAuth credentials to access the secure resources used in this tutorial.
-
Complete the Create OAuth credentials for user authentication tutorial to obtain a Client ID and Redirect URL.
A
Client IDuniquely identifies your app on the authenticating server. If the server cannot find an app with the provided Client ID, it will not proceed with authentication.The
Redirect URL(also referred to as a callback url) is used to identify a response from the authenticating server when the system returns control back to your app after an OAuth login. Since it does not necessarily represent a valid endpoint that a user could navigate to, the redirect URL can use a custom scheme, such asmy-app://auth. It is important to make sure the redirect URL used in your app’s code matches a redirect URL configured on the authenticating server. -
Copy and paste the Client ID and Redirect URL into a safe location. They will be used in a later step.
All users that access this application need account privileges
Set developer credentials in the solution
To allow your app users to access ArcGIS location services
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.API_KEY.MainActivity.kt14 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEY42 collapsed linesprivate val authenticatorState = AuthenticatorState()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)when (authenticationMode) {AuthenticationMode.API_KEY -> {ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")}AuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL"))}}enableEdgeToEdge()setContent {TutorialTheme {MainScreen()if (authenticationMode == AuthenticationMode.USER_AUTH) {DialogAuthenticator(authenticatorState)}}}}} -
Set the
apiKeyproperty with your API key access token.MainActivity.kt22 collapsed linespackage com.example.appimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.arcgismaps.httpcore.authentication.OAuthUserConfigurationimport com.arcgismaps.toolkit.authentication.AuthenticatorStateimport com.arcgismaps.toolkit.authentication.DialogAuthenticatorimport com.example.app.screens.MainScreenimport com.example.app.ui.theme.TutorialThemeclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.API_KEYprivate val authenticatorState = AuthenticatorState()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)when (authenticationMode) {AuthenticationMode.API_KEY -> {ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN")}30 collapsed linesAuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL"))}}enableEdgeToEdge()setContent {TutorialTheme {MainScreen()if (authenticationMode == AuthenticationMode.USER_AUTH) {DialogAuthenticator(authenticatorState)}}}}}
Best Practice: The access token is stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
-
In the Android view of Android Studio, open app > kotlin+java > com.example.app > MainActivity. Set the
AuthenticationModeto.USER_AUTH.MainActivity.ktclass MainActivity : ComponentActivity() {private enum class AuthenticationMode { API_KEY, USER_AUTH }private val authenticationMode = AuthenticationMode.USER_AUTH -
Set your
clientIDandredirectURLvalues. You must use the RedirectURL that you supplied for your app in theuser authenticationpart of the Set up authentication step.MainActivity.ktAuthenticationMode.USER_AUTH -> {authenticatorState.oAuthUserConfigurations = listOf(OAuthUserConfiguration(portalUrl = "https://www.arcgis.com",clientId = "YOUR_CLIENT_ID",redirectUrl = "YOUR_REDIRECT_URL")) -
Open app > manifests > AndroidManifest.xml.
-
Set the
android:schemeandandroid:hostusing the scheme and host from your RedirectURL.A redirectURL is composed of a scheme and a host component. The format for the redirect url is
scheme://host. For example, if the redirect url ismyscheme://myhostthen the scheme ismyschemeand the host ismyhost.AndroidManifest.xml41 collapsed lines<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.INTERNET" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Tutorial"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"android:label="@string/app_name"android:theme="@style/Theme.Tutorial"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activityandroid:name="com.arcgismaps.toolkit.authentication.AuthenticationActivity"android:configChanges="keyboard|keyboardHidden|orientation|screenSize"android:exported="true"android:launchMode="singleTop" ><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:scheme="your_redirect_url_scheme"android:host="your_redirect_url_host" />6 collapsed lines</intent-filter></activity></application></manifest>
Best Practice: The OAuth credentials are stored directly in the code as a convenience for this tutorial. Do not store credentials directly in source code in a production environment.
Run the app
Click Run > Run > app to run the app.
You should see a scene of trails, trailheads and parks in the Santa Monica Mountains. Drag, swipe, or pinch on the scene view to explore the scene.
What’s next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: