Perform an exploratory viewshed analysis from a defined vantage point.

Use case
An exploratory viewshed analysis is a type of visual analysis you can perform at the current rendered resolution of a scene. The exploratory 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).
Note: This 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 but not persisted. If persisted analysis performed at the full resolution of the data is required, consider using a ViewshedFunction to perform a viewshed calculation instead.
How to use the sample
Use the sliders to change the properties (heading, pitch, etc.), of the exploratory viewshed and see them updated in real time.
How it works
- Create an
ExploratoryLocationViewshedpassing in the observer location, heading, pitch, horizontal/vertical angles, and min/max distances. - Set the property values on the exploratory viewshed instance for location, direction, range, and visibility properties.
Relevant API
- AnalysisOverlay
- ArcGISSceneLayer
- ArcGISTiledElevationSource
- ExploratoryLocationViewshed
- ExploratoryViewshed
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, exploratory viewshed, frustum, geoview-compose, scene, 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.showexploratoryviewshedfrompointinscene
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.showexploratoryviewshedfrompointinscene.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_exploratory_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.showexploratoryviewshedfrompointinscene.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport com.arcgismaps.analysis.interactive.ExploratoryLocationViewshedimport 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.esri.arcgismaps.sample.showexploratoryviewshedfrompointinscene.R
class SceneViewModel(private val application: Application) : AndroidViewModel(application) {
// initialize location viewshed parameters private var viewShed: ExploratoryLocationViewshed 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
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 ) 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 = ExploratoryLocationViewshed( 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.showexploratoryviewshedfrompointinscene.screens
import androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.mapping.view.OrbitLocationCameraControllerimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showexploratoryviewshedfrompointinscene.components.SceneViewModel
/** * Main screen layout for the sample app */@Composablefun MainScreen(sampleName: String) { var isBottomSheetVisible by remember { mutableStateOf(true) } // create a ViewModel to handle SceneView interactions val sceneViewModel: SceneViewModel = viewModel()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, floatingActionButton = { if (!isBottomSheetVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 24.dp), onClick = { isBottomSheetVisible = true } ) { Icon(Icons.Filled.Settings, contentDescription = "Viewshed options") } } }, content = { Box { Column( modifier = Modifier .fillMaxSize() .padding(it) ) {
val cameraController = remember { OrbitLocationCameraController( targetPoint = sceneViewModel.initLocation, distance = 5000.0 ) } // composable function that wraps the SceneView SceneView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISScene = sceneViewModel.scene, onDown = { isBottomSheetVisible = false }, cameraController = cameraController, analysisOverlays = listOf(sceneViewModel.analysisOverlay) ) } } BottomSheet( sheetTitle = "Viewshed Options", isVisible = isBottomSheetVisible, onDismissRequest = { isBottomSheetVisible = false } ) { // 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.showexploratoryviewshedfrompointinscene.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.showexploratoryviewshedfrompointinscene.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() ) }}