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.
Steps
Open an Android Studio project
-
To start this tutorial, complete the Display a scene tutorial, or download and unzip the Display a scene solution in a new folder.
-
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
element, change the text content to Display a viewshed._name" > strings.xmlUse dark colors for code blocks <resources> <string name="app_name">Display a viewshed</string> </resources>
-
In the Android view, open Gradle Scripts > settings.gradle.kts.
Change the value of
root
to "Display a viewshed".Project.name settings.gradle.ktsUse dark colors for code blocks 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")
-
The UI theme composable in Display a map tutorial was
Display
. Rename the theme composable throughout the tutorial by refactoringA Map Theme Display
.A Map Theme In the Android view, open app > kotlin+java > com.exmple.app > ui.theme > Theme.kt.
Right-click the function name
Display
and select Refactor -> Rename. Replace the name withA Map Theme Display
.A Viewshed Theme Theme.ktUse dark colors for code blocks Copy @Composable fun DisplayAMapTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> DarkColorScheme else -> LightColorScheme }
-
Click File > Sync Project with Gradle files. Android Studio will recognize your changes and create a new .idea folder.
-
-
If you downloaded the solution, get an access token and set the API key in MainActivity.kt.
An API Key gives your app access to secure resources used in this tutorial.
-
Go to the Create an API key tutorial to obtain a new API key access token using your ArcGIS Location Platform or ArcGIS Online account. Ensure that the following privilege is enabled: Location services > Basemaps > Basemap styles service. Copy the access token as it will be used in the next step.
-
In Android Studio: in the Android view, open app > java > com.example.app > MainActivity.
-
In the
set
function, find theApi Key() ApiKey.create()
call and paste your access token inside the double quotes, replacing YOUR_ACCESS_TOKEN.MainActivity.ktUse dark colors for code blocks Copy private fun setApiKey() { ArcGISEnvironment.apiKey = ApiKey.create("YOUR_ACCESS_TOKEN") }
-
-
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.Use dark colors for code blocks [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" } # Additional modules from Toolkit, if needed, such as: # 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" }
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.Log
import androidx.compose.foundation.layout.Arrangement
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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.arcgismaps.analysis.LocationViewshed
import com.arcgismaps.geometry.Point
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.PortalItem
import com.arcgismaps.mapping.view.AnalysisOverlay
import com.arcgismaps.mapping.view.ScreenCoordinate
import com.arcgismaps.portal.Portal
import com.arcgismaps.toolkit.geoviewcompose.SceneView
import com.arcgismaps.toolkit.geoviewcompose.SceneViewProxy
import com.example.app.R
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
Get the web scene item ID
You can use ArcGIS tools to create and view web scenes. Use the Scene Viewer to identify the web scene item ID. This item ID will be used later in the tutorial.
- Go to the Yosemite Valley Hotspots web scene in the Scene Viewer in ArcGIS Online. This web scene displays terrain and hotspots in the Yosemite Valley.
- Make a note of the item ID at the end of the browser's URL. The item ID should be 7558ee942b2547019f66885c44d4f0b1.
Create a view model
Modern app architecture uses a map 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 map view model that extends
View
.Model MainScreen.ktUse dark colors for code blocks class SceneViewModel : ViewModel() { }
-
In the
Main
composable, delete the entire function body. Leave just the function declaration as shown below. Then delete the top-level code for creating a map that is part of the Display a map tutorial. In this tutorial, you will create the map within the view model.Screen MainScreen.ktUse dark colors for code blocks Copy @Composable fun MainScreen() { }
MainScreen.ktUse dark colors for code blocks fun createMap(): ArcGISMap { return ArcGISMap(BasemapStyle.ArcGISTopographic).apply { initialViewpoint = Viewpoint( latitude = 34.0270, longitude = -118.8050, scale = 72000.0 ) } }
-
In the view model, create an
ArcGISScene
from aPortalItem
. Then create aSceneViewProxy
.MainScreen.ktUse dark colors for code blocks 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()
Create a viewshed analysis
Visual analyses are used to help you make sense of complex 3D data contained by a scene. Use a LocationViewshed
to perform and display a viewshed analysis using a 3D point to define the observer's location.
-
In the view model, create an
AnalysisOverlay
namedanalysis
to contain and display the viewshed analyses.Overlay An analysis overlay is a container for
Analysis
objects. It is used with a scene view to display visual analyses on a scene. You can add more than one analysis overlay to a scene view. Analysis overlays are displayed on top of all other layers and graphics overlays.SceneViewModel classUse dark colors for code blocks // The analysis overlay to be added to the scene. val analysisOverlay = AnalysisOverlay()
-
Create a
LocationViewshed
namedviewshed
.The viewshed analysis is added to a scene view 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 layers.
SceneViewModel classUse dark colors for code blocks var viewshed = LocationViewshed( 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 )
-
In the
init
block, 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 fromview
.Model Scope.launch SceneViewModel classUse dark colors for code blocks init { viewshed.isVisible = false analysisOverlay.analyses.add(viewshed) viewModelScope.launch { scene.load().getOrElse { logError(it) } } }
-
Define a function named
set
that takes aViewshed Location() Point
as a parameter. This function sets the location of the viewshed and makes it visible, if it is not visible already.SceneViewModel classUse dark colors for code blocks private fun setViewshedLocation(point: Point) { viewshed.location = point viewshed.isVisible = true }
-
Define a function named
hide
, which hides the viewshed without modifying or deleting it. Then create a function namedViewshed() set
to set a new maximum distance value for the viewshed.Viewshed Distance() SceneViewModel classUse dark colors for code blocks fun hideViewshed() { viewshed.isVisible = false } fun setViewshedDistance(maxDistance: Double) { viewshed.maxDistance = maxDistance }
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
handle
to add or move the viewshed analysis when the user long-presses on the scene. To obtain the new location (Long Press Event() Point
) in the scene, callscreen
on theTo Location() SceneViewProxy
, passing in the screen coordinate of the long-press. Then call yourset
with the scene point.Viewshed Location() SceneViewModel classUse dark colors for code blocks fun handleOnLongPress(screenCoordinate: ScreenCoordinate) { viewModelScope.launch { val scenePoint = sceneViewProxy.screenToLocation(screenCoordinate).getOrElse { error -> return@launch logError(error) } setViewshedLocation(scenePoint) } }
-
Define a function to log errors.
SceneViewModel classUse dark colors for code blocks private fun logError(error: Throwable) { Log.e(this.javaClass.simpleName, error.message.toString(), error.cause) }
Add a UI to control the viewshed analysis
To control the viewshed analysis, some UI elements are required.
-
Create variables in the
Main
composable.Screen - Create
scene
usingView Model view
.Model() - Create
slider
of typeValue Mutable
. SinceFloat State viewshed.max
in the view model is aDistance Double
, but thevalue
passed to Jetpack ComposeSlider
composable is aFloat
, you must convert the max distance to aFloat
.
MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { val sceneViewModel: SceneViewModel = viewModel() // If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0 var sliderValue by remember { mutableFloatStateOf(sceneViewModel.viewshed.maxDistance?.toFloat() ?: 1000.0f) } }
- Create
-
Add a
Scaffold
and pass aTop
. TheApp Bar Scaffold
's content lambda contains aColumn
.MainScreen()Use dark colors for code blocks @Composable fun MainScreen() { val sceneViewModel: SceneViewModel = viewModel() // If viewshed.maxDistance is not null, assign it to maxDistance. Otherwise, assign default of 1000.0 var 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 theSceneView
composable. For thearc
,GIS Scene scene
, andView Proxy analysis
parameters, pass the corresponding properties fromOverlays scene
. ForView Model on
, pass a lambda that callsLong Press scene
.View Model.handle On Long Press() MainScreen()Use dark colors for code blocks 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) } ) } }
-
Add a
Text
to display the current value of the slider. -
Add a
Row
that will contain aSlider
and aText
.Button The slider allows the user to adjust the maximum distance of the viewshed in the range of
0f..20999f
. Thevalue
parameter is the current value of the slider, which is held in theslider
variable of theValue Main
composable. For theScreen on
parameters pass a lambda that saves the new value of the slider in theValuechange slider
variable and then callsValue set
in the view model. Changing the maximum distance of the viewshed expands or contracts the size of the observer's field of view.Viewshed Distance() The text button says "Clear" and calls the view model's
hide
when clicked. This allows the user a fresh start to make more analyses.Viewshed() MainScreen()Use dark colors for code blocks Text(text = "Viewshed max distance: "+sliderValue.roundToInt().toString()) Row { Slider( modifier = Modifier.weight(1f), value = sliderValue, valueRange = 0f..20999f, onValueChange = { sliderValue = it sceneViewModel.setViewshedDistance(sliderValue.toDouble()) }, ) TextButton(onClick = { sceneViewModel.hideViewshed() }) { Text(text = "Clear") } }
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.
What's next?
Learn how to use additional API features, ArcGIS location services, and ArcGIS tools in these tutorials: