Perform a viewshed analysis from a defined vantage point.

Use case
A 3D viewshed analysis is a type of visual analysis you can perform on a scene. The viewshed shows what can be seen from a given location. The output is an overlay with two different colors - one representing the visible areas (green) and the other representing the obstructed areas (red). Viewshed analysis is a form of “exploratory analysis”, which means the results are calculated on the current scale of the data, and the results are generated very quickly. If more “conclusive” results are required, consider using a GeoprocessingTask to perform a viewshed instead.
How to use the sample
Use the sliders to change the properties (heading, pitch, etc.), of the viewshed and see them updated in real time.
How it works
- Create a
LocationViewshedpassing in the observer location, heading, pitch, horizontal/vertical angles, and min/max distances. - Set the property values on the viewshed instance for location, direction, range, and visibility properties.
Relevant API
- AnalysisOverlay
- ArcGISSceneLayer
- ArcGISTiledElevationSource
- LocationViewshed
- Viewshed
About the data
The scene shows a buildings layer in Brest, France hosted on ArcGIS Online.
Additional information
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable SceneView.
Tags
3D, frustum, geoview-compose, scene, viewshed, visibility analysis
Sample Code
/* Copyright 2023 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.showviewshedfrompointinscene
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showviewshedfrompointinscene.screens.MainScreen
class MainActivity : ComponentActivity() {
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)
enableEdgeToEdge() setContent { SampleAppTheme { ViewshedLocationApp() } } }
@Composable private fun ViewshedLocationApp() { Surface(color = MaterialTheme.colorScheme.background) { MainScreen(sampleName = getString(R.string.show_viewshed_from_point_in_scene_app_name)) } }}/* Copyright 2023 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.showviewshedfrompointinscene.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport com.arcgismaps.analysis.LocationViewshedimport com.arcgismaps.geometry.Pointimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.ArcGISTiledElevationSourceimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Surfaceimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.layers.ArcGISSceneLayerimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.mapping.view.OrbitLocationCameraControllerimport com.esri.arcgismaps.sample.showviewshedfrompointinscene.R
class SceneViewModel(private val application: Application) : AndroidViewModel(application) {
// initialize location viewshed parameters private var viewShed: LocationViewshed private val initHeading = 82.0 private val initPitch = 60.0 private val initHorizontalAngle = 75.0 private val initVerticalAngle = 90.0 private val initMinDistance = 0.0 private val initMaxDistance = 1500.0
private val initLocation = Point( x = -4.50, y = 48.4, z = 1000.0 ) private val camera = Camera( lookAtPoint = initLocation, distance = 20000000.0, heading = 0.0, pitch = 55.0, roll = 0.0 ) val cameraController = OrbitLocationCameraController( targetPoint = initLocation, distance = 5000.0 ) var scene by mutableStateOf(ArcGISScene(BasemapStyle.ArcGISNavigationNight)) var analysisOverlay by mutableStateOf(AnalysisOverlay())
init { // create a surface for elevation data val surface = Surface().apply { elevationSources.add(ArcGISTiledElevationSource(application.getString(R.string.elevation_service))) }
// create a layer of buildings val buildingsSceneLayer = ArcGISSceneLayer(application.getString(R.string.buildings_layer))
// create a scene and add imagery basemap, elevation surface, and buildings layer to it val buildingsScene = ArcGISScene(BasemapStyle.ArcGISImagery).apply { baseSurface = surface operationalLayers.add(buildingsSceneLayer) }
val initLocation = Point(-4.50, 48.4, 1000.0) // create viewshed from the initial location viewShed = LocationViewshed( location = initLocation, heading = initHeading, pitch = initPitch, horizontalAngle = initHorizontalAngle, verticalAngle = initVerticalAngle, minDistance = initMinDistance, maxDistance = initMaxDistance ).apply { frustumOutlineVisible = true }
// add the buildings scene to the sceneView scene = buildingsScene.apply { baseSurface = surface initialViewpoint = Viewpoint(initLocation, camera) } // add the viewshed to the analysisOverlay of the scene view analysisOverlay.apply { analyses.add(viewShed) isVisible = true } }
fun setHeading(sliderHeading: Float) { viewShed.heading = sliderHeading.toDouble() }
fun setMaximumDistanceSlider(sliderValue: Float) { viewShed.maxDistance = sliderValue.toDouble()
}
fun setMinimumDistanceSlider(sliderValue: Float) { viewShed.minDistance = sliderValue.toDouble() }
fun setVerticalAngleSlider(sliderValue: Float) { viewShed.verticalAngle = sliderValue.toDouble() }
fun setHorizontalAngleSlider(sliderValue: Float) { viewShed.horizontalAngle = sliderValue.toDouble() }
fun setPitch(sliderValue: Float) { viewShed.pitch = sliderValue.toDouble() }
fun frustumVisibility(checkedValue: Boolean) { viewShed.frustumOutlineVisible = checkedValue }
fun analysisVisibility(checkedValue: Boolean) { viewShed.isVisible = checkedValue }}/* Copyright 2023 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.showviewshedfrompointinscene.screens
import android.app.Applicationimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showviewshedfrompointinscene.components.SceneViewModel
/** * Main screen layout for the sample app */@Composablefun MainScreen(sampleName: String) { // get the application context val application = LocalContext.current.applicationContext as Application // create a ViewModel to handle SceneView interactions val sceneViewModel = SceneViewModel(application)
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Box { Column( modifier = Modifier .fillMaxSize() .padding(it) ) { // composable function that wraps the SceneView SceneView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISScene = sceneViewModel.scene, cameraController = sceneViewModel.cameraController, analysisOverlays = listOf(sceneViewModel.analysisOverlay) ) // display list of options to modify viewshed properties ViewshedOptionsScreen( onHeadingChanged = sceneViewModel::setHeading, onPitchChanged = sceneViewModel::setPitch, onHorizontalAngleChanged = sceneViewModel::setHorizontalAngleSlider, onVerticalAngleChanged = sceneViewModel::setVerticalAngleSlider, onMinDistanceChanged = sceneViewModel::setMinimumDistanceSlider, onMaxDistanceChanged = sceneViewModel::setMaximumDistanceSlider, isFrustumVisible = sceneViewModel::frustumVisibility, isAnalysisVisible = sceneViewModel::analysisVisibility ) } } } )}/* Copyright 2023 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.showviewshedfrompointinscene.screens
import android.content.res.Configurationimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Checkboximport androidx.compose.material3.Surfaceimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.ui.Modifierimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
/** * Viewshed options screen for sliders and checkboxes */@Composablefun ViewshedOptionsScreen( onHeadingChanged: (Float) -> Unit = {}, onPitchChanged: (Float) -> Unit = {}, onHorizontalAngleChanged: (Float) -> Unit = {}, onVerticalAngleChanged: (Float) -> Unit = {}, onMinDistanceChanged: (Float) -> Unit = {}, onMaxDistanceChanged: (Float) -> Unit = {}, isFrustumVisible: (Boolean) -> Unit = {}, isAnalysisVisible: (Boolean) -> Unit = {}) { Column { // sliders HeadingSlider(onHeadingChanged) PitchSlider(onPitchChanged) HorizontalAngleSlider(onHorizontalAngleChanged) VerticalAngleSlider(onVerticalAngleChanged) MinimumDistanceSlider(onMinDistanceChanged) MaximumDistanceSlider(onMaxDistanceChanged) // checkbox Row { FrustumCheckBox(isFrustumVisible) AnalysisCheckBox(isAnalysisVisible) } }}
@Composableprivate fun HeadingSlider(onHeadingChanged: (Float) -> Unit) { ViewshedSlider( title = "Heading", initialSliderValue = 82f, sliderRangeValue = 0f..360f, functionChanged = onHeadingChanged )}
@Composableprivate fun PitchSlider(onPitchChanged: (Float) -> Unit) { ViewshedSlider( title = "Pitch", initialSliderValue = 60f, sliderRangeValue = 0f..180f, functionChanged = onPitchChanged )}
@Composableprivate fun HorizontalAngleSlider(onHorizontalAngleChanged: (Float) -> Unit) { ViewshedSlider( title = "Horizontal Angle", initialSliderValue = 75f, sliderRangeValue = 1f..120f, functionChanged = onHorizontalAngleChanged )}
@Composableprivate fun VerticalAngleSlider(onVerticalAngleChanged: (Float) -> Unit) { ViewshedSlider( title = "Vertical Angle", initialSliderValue = 90f, sliderRangeValue = 1f..120f, functionChanged = onVerticalAngleChanged )}
@Composableprivate fun MinimumDistanceSlider(onMinDistanceChanged: (Float) -> Unit) { ViewshedSlider( title = "Minimum Distance", initialSliderValue = 0f, sliderRangeValue = 0f..8999f, functionChanged = onMinDistanceChanged )}
@Composableprivate fun MaximumDistanceSlider(onMaxDistanceChanged: (Float) -> Unit) { ViewshedSlider( title = "Maximum Distance", initialSliderValue = 1500f, sliderRangeValue = 0f..9999f, functionChanged = onMaxDistanceChanged )}
@Composablefun FrustumCheckBox(isFrustumVisible: (Boolean) -> Unit) { // set the state of the checkbox val checkedState = remember { mutableStateOf(true) } // display a row and create a checkbox and text in a row Row { Checkbox( checked = checkedState.value, onCheckedChange = { checkedState.value = it isFrustumVisible(checkedState.value) }, ) Text(modifier = Modifier.padding(top = 10.dp), text = "Frustum Outline") }}
@Composablefun AnalysisCheckBox(isAnalysisVisible: (Boolean) -> Unit) { // set the state of the checkbox val checkedState = remember { mutableStateOf(true) } // display a row and create a checkbox and text in a row Row { Checkbox( checked = checkedState.value, onCheckedChange = { checkedState.value = it isAnalysisVisible(checkedState.value) }, ) Text(modifier = Modifier.padding(top = 10.dp), text = "Analysis Overlay") }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun PreviewViewshedOptions() { SampleAppTheme { Surface { ViewshedOptionsScreen() } }}package com.esri.arcgismaps.sample.showviewshedfrompointinscene.screens
import androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.widthimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dp
/** * Custom slider implementation to be used by various viewshed slider options */@Composablefun ViewshedSlider( title: String, initialSliderValue: Float, sliderRangeValue: ClosedFloatingPointRange<Float>, functionChanged: (Float) -> Unit) { var sliderValue by remember { mutableFloatStateOf(initialSliderValue) } Row { Text( modifier = Modifier.padding(start = 10.dp, top = 10.dp, end = 10.dp).width(150.dp), text = title ) Slider( modifier = Modifier.weight(1f), value = sliderValue, onValueChange = { sliderValue = it // update view model viewshed value functionChanged(sliderValue) }, valueRange = sliderRangeValue ) Text( modifier = Modifier.padding(start = 10.dp, top = 10.dp, end = 10.dp).size(40.dp), text = sliderValue.toInt().toString() ) }}