Perform an interactive viewshed analysis to determine visible and non-visible areas from a given observer position.

Use case
A viewshed analysis calculates the visible and non-visible areas from an observer’s location, based on factors such as elevation and topographic features. For example, an interactive viewshed analysis can be used to identify which areas can be seen from a helicopter moving along a given flight path for monitoring wildfires while taking parameters such as height, field of view, and heading into account to give immediate visual feedback. A user could further extend their viewshed analysis calculations by using map algebra, for example to only return viewshed results in geographical areas not covered in forest if they have an additional land cover raster dataset.
Note: This analysis is a form of “data-driven analysis”, which means the analysis is calculated at the resolution of the data rather than the resolution of the display.
How to use the sample
The sample loads with a viewshed analysis initialized from an elevation raster covering the Isle of Arran, Scotland. Translucent green shows the area visible from the observer position, and gray shows the non-visible areas. Move the observer position by long-pressing and dragging over the island to interactively evaluate the viewshed result and display it in the analysis overlay. Alternatively, tap on the map to see the viewshed from the tapped location. Use the sliders and radio buttons to explore how the viewshed analysis results change when adjusting the observer elevation, target height, maximum radius, field of view, heading and elevation sampling interval. As you move the observer and update the viewshed parameters, the analysis overlay refreshes to show the evaluated viewshed result.
How it works
- Create an
ArcGISMapand set it on aMapView. - Add a
GraphicsOverlayto draw the observer point and anAnalysisOverlayto the map view. - Create a
ContinuousFieldfrom a raster file containing elevation data. - Create a
ContinuousFieldFunctionfrom theContinuousField. - Create and configure
ViewshedParameters, passing in aPointas the observer position for the viewshed. - Create a
ViewshedFunctionusing theContinuousFieldFunctionandViewshedParameters, then convert it to aDiscreteFieldFunction. - Create a
ColormapRendererfrom aColormapwith colors that represent visible and non-visible results. - Create a
FieldAnalysisfrom theDiscreteFieldFunctionandColormapRenderer, then add it to theAnalysisOverlay’s collection of analysis objects to display the results. As parameter values change, the result is recalculated and redrawn automatically.
Relevant API
- AnalysisOverlay
- Colormap
- ColormapRenderer
- ContinuousField
- ContinuousFieldFunction
- FieldAnalysis
- ViewshedFunction
- ViewshedParameters
About the data
The sample uses a 10m resolution digital terrain elevation raster of the Isle of Arran, Scotland (Data Copyright Scottish Government and SEPA (2014)).
Tags
analysis overlay, elevation, field analysis, interactive, raster, spatial analysis, terrain, viewshed, visibility
Sample code
/* Copyright 2026 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.showinteractiveviewshedwithanalysisoverlay.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.analysis.ContinuousFieldimport com.arcgismaps.analysis.ContinuousFieldFunctionimport com.arcgismaps.analysis.interactive.FieldAnalysisimport com.arcgismaps.analysis.visibility.ViewshedFunctionimport com.arcgismaps.analysis.visibility.ViewshedParametersimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.symbology.raster.Colormapimport com.arcgismaps.mapping.symbology.raster.ColormapRendererimport com.arcgismaps.mapping.view.AnalysisOverlayimport com.arcgismaps.mapping.view.AnalysisViewStatusimport com.arcgismaps.mapping.view.GeoViewimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.LongPressEventimport com.arcgismaps.mapping.view.PanChangeEventimport com.arcgismaps.mapping.view.PanChangeEvent.PanStatusimport com.arcgismaps.mapping.view.SingleTapConfirmedEventimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.Rimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.ViewshedUiState.Companion.initialViewshedUiStateimport kotlinx.coroutines.flow.MutableSharedFlowimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asSharedFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.flow.updateimport kotlinx.coroutines.launchimport java.io.Fileimport kotlin.io.path.Path
class ShowInteractiveViewshedWithAnalysisOverlayViewModel(app: Application) : AndroidViewModel(app) { // Initialize and keep track of UI state private val _viewshedUiState = MutableStateFlow(initialViewshedUiState) val viewshedUiState = _viewshedUiState.asStateFlow()
// Create a MapViewProxy, used to convert screen points to map points val mapViewProxy = MapViewProxy()
// Initialize and keep track of the ArcGISMap & the overlays it uses val arcGISMap by mutableStateOf( ArcGISMap(BasemapStyle.ArcGISImagery).apply { initialViewpoint = Viewpoint(latitude = 55.610000, longitude = -5.200346, scale = 150000.0) } ) var analysisOverlay by mutableStateOf(AnalysisOverlay()) var graphicsOverlay by mutableStateOf(GraphicsOverlay())
// Create and keep track of ViewshedParameters private val viewshedParameters by mutableStateOf(ViewshedParameters())
// Setup initial observer position, and a symbol and Graphic to draw at the observer position private val initialObserverPosition = Point( x = -579246.504, y = 7479619.947, z = initialViewshedUiState.observerElevation, spatialReference = SpatialReference.webMercator() ) private val observerSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Circle, color = Color.blue, size = 10.0f ) private val observerGraphic = Graphic(geometry = initialObserverPosition, symbol = observerSymbol)
// Indicates if observer position is currently being dragged across the map var isDragging by mutableStateOf(false)
// Keep track of haptic feedback events, used when dragging the observer position private val _dragHapticEvents = MutableSharedFlow<DragHapticEvent>(extraBufferCapacity = 1) val dragHapticEvents = _dragHapticEvents.asSharedFlow()
// Location of file containing elevation data private val provisionPath: String by lazy { app.getExternalFilesDir(null)?.path + File.separator + app.getString( R.string.show_interactive_viewshed_with_analysis_overlay_app_name ) } private val filePath = Path(provisionPath, app.getString(R.string.elevation_data_filename))
// Used to surface errors to the Compose UI val messageDialogVM = MessageDialogViewModel()
init { // Configure the ViewshedParameters using initial values from the UI state viewshedParameters.apply { observerPosition = initialObserverPosition targetHeight = initialViewshedUiState.targetHeight maxRadius = initialViewshedUiState.maxRadius fieldOfView = initialViewshedUiState.fieldOfView heading = initialViewshedUiState.heading }
viewModelScope.launch { // Display a symbol to mark the observer position graphicsOverlay.graphics.add(observerGraphic)
// Create a ContinuousField from a raster file containing elevation data val filePaths = listOf(filePath.toString()) ContinuousField.createFromFiles(filePaths = filePaths, band = 0) .onFailure { messageDialogVM.showMessageDialog(it) }.onSuccess { continuousField -> // Create a ContinuousFieldFunction from the ContinuousField val continuousFieldFunction = ContinuousFieldFunction.create(continuousField)
// Create a ViewshedFunction using the ContinuousFieldFunction and // ViewshedParameters, then convert it to a DiscreteFieldFunction val viewshedFunction = ViewshedFunction( elevation = continuousFieldFunction, parameters = viewshedParameters ) val discreteViewshed = viewshedFunction.toDiscreteFieldFunction()
// Create a ColormapRenderer from a Colormap with colors that represent visible // and non-visible results val areaNotVisibleColor = Color.gray val areaVisibleColor = Color.fromRgba(r = 136, g = 204, b = 132, a = 100) val colors = listOf(areaNotVisibleColor, areaVisibleColor) val colormap = Colormap.create(colors) val colormapRenderer = ColormapRenderer(colormap)
// Create a FieldAnalysis from the DiscreteFieldFunction and ColormapRenderer, // then add it to the AnalysisOverlay's collection of analysis objects to // display the results val analysis = FieldAnalysis(discreteFieldFunction = discreteViewshed, colormapRenderer) analysisOverlay.analyses.add(analysis) } } }
/** * Sets a new observer elevation. */ fun setObserverElevation(observerElevation: Float) { viewshedParameters.observerPosition?.let { oldPos -> val observerPosition = Point( x = oldPos.x, y = oldPos.y, z = observerElevation.toDouble(), spatialReference = oldPos.spatialReference ) syncObserverPosition(observerPosition) _viewshedUiState.update { it.copy(observerElevation = observerElevation.toDouble()) } } }
/** * Sets a new target height. */ fun setTargetHeight(targetHeight: Float) { viewshedParameters.targetHeight = targetHeight.toDouble() _viewshedUiState.update { it.copy(targetHeight = targetHeight.toDouble()) } }
/** * Sets a new maximum radius. */ fun setMaxRadius(maxRadius: Float) { viewshedParameters.maxRadius = maxRadius.toDouble() _viewshedUiState.update { it.copy(maxRadius = maxRadius.toDouble()) } }
/** * Sets a new field of view. */ fun setFieldOfView(fieldOfView: Float) { viewshedParameters.fieldOfView = fieldOfView.toDouble() _viewshedUiState.update { it.copy(fieldOfView = fieldOfView.toDouble()) } }
/** * Sets a new heading. */ fun setHeading(heading: Float) { viewshedParameters.heading = heading.toDouble() _viewshedUiState.update { it.copy(heading = heading.toDouble()) } }
/** * Sets a new elevation sampling interval. */ fun setElevationSamplingInterval(elevationSamplingInterval: Double) { viewshedParameters.elevationSamplingInterval = when (elevationSamplingInterval) { 0.0 -> null else -> elevationSamplingInterval } _viewshedUiState.update { it.copy(elevationSamplingInterval = elevationSamplingInterval) } }
/** * Sets the observer position to the location of the given tap [event]. */ fun onTap(event: SingleTapConfirmedEvent) { setNewObserverPosition(event.mapPoint) }
/** * Acts on a long press [event] by setting the observer position to the location of the long * press and allowing it to be dragged across the map. */ fun onLongPress(event: LongPressEvent) { observerGraphic.isSelected = true isDragging = true _dragHapticEvents.tryEmit(DragHapticEvent.Start) setNewObserverPosition(event.mapPoint) }
/** * Acts on a pan [event]. If the observer position is currently being dragged, the new position * is set to match the current screen coordinate. Dragging is terminated when panning ends. */ fun onPan(event: PanChangeEvent) { if (isDragging) { setNewObserverPosition(mapViewProxy.screenToLocationOrNull(event.screenCoordinate)) if (event.status == PanStatus.End) { observerGraphic.isSelected = false isDragging = false _dragHapticEvents.tryEmit(DragHapticEvent.End) } } }
/** * Sets the observer position to the given [mapPoint]. */ private fun setNewObserverPosition(mapPoint: Point?) { if (mapPoint != null) { viewshedParameters.observerPosition?.let { oldPos -> val observerPosition = when (oldPos.z) { null -> Point(x = mapPoint.x, y = mapPoint.y, mapPoint.spatialReference) else -> Point( x = mapPoint.x, y = mapPoint.y, z = oldPos.z!!, mapPoint.spatialReference ) } syncObserverPosition(observerPosition) } } }
/** * Synchronizes setting of a new [observerPosition]. This needs to be set in the * [viewshedParameters] and also as the geometry of the [observerGraphic]. */ private fun syncObserverPosition(observerPosition: Point) { // Update the observer graphic geometry to the current observer position observerGraphic.geometry = observerPosition
// Update the viewshed parameters to the current observer position, which triggers analysis viewshedParameters.observerPosition = observerPosition }
/** * Display dialog if there is an error with analysis. */ fun analysisViewStatusListener(event: GeoView.GeoViewAnalysisViewStatusChanged) { if (event.analysisViewStatus is AnalysisViewStatus.Error) { messageDialogVM.showMessageDialog( throwable = (event.analysisViewStatus as AnalysisViewStatus.Error).details ) } }}
data class ViewshedUiState( val observerElevation: Double, val targetHeight: Double, val maxRadius: Double, val fieldOfView: Double, val heading: Double, val elevationSamplingInterval: Double) { companion object { // Initial viewshed parameters to drive the UI on launch val initialViewshedUiState = ViewshedUiState( observerElevation = 20.0, targetHeight = 20.0, maxRadius = 8000.0, fieldOfView = 150.0, heading = 10.0, elevationSamplingInterval = 0.0 ) }}
enum class DragHapticEvent { Start, End,}/* Copyright 2026 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.showinteractiveviewshedwithanalysisoverlay
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), // get the app name of the sample getString(R.string.show_interactive_viewshed_with_analysis_overlay_app_name), listOf( "https://www.arcgis.com/home/item.html?id=aa97788593e34a32bcaae33947fdc271" ) ) }}/* Copyright 2026 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.showinteractiveviewshedwithanalysisoverlay
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.screens.ShowInteractiveViewshedWithAnalysisOverlayScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
enableEdgeToEdge() setContent { SampleAppTheme { Surface(color = MaterialTheme.colorScheme.background) { ShowInteractiveViewshedWithAnalysisOverlayScreen() } } } }}/* Copyright 2026 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.showinteractiveviewshedwithanalysisoverlay.screens
import android.view.HapticFeedbackConstantsimport androidx.compose.foundation.layout.BoxScopeimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalViewimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.text.style.TextAlignimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.mapping.view.MapViewInteractionOptionsimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleDeviceLightDarkPreviewimport com.esri.arcgismaps.sample.sampleslib.components.SamplePreviewSurfaceimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.sampleslib.components.adaptive.AdaptiveThreePaneimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.Rimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.DragHapticEventimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.ShowInteractiveViewshedWithAnalysisOverlayViewModelimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.ViewshedUiStateimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.ViewshedUiState.Companion.initialViewshedUiStateimport kotlinx.coroutines.flow.Flow
/** * Main screen layout for the sample app. */@Composablefun ShowInteractiveViewshedWithAnalysisOverlayScreen() { val viewModel: ShowInteractiveViewshedWithAnalysisOverlayViewModel = viewModel() val uiState by viewModel.viewshedUiState.collectAsStateWithLifecycle()
// Set up a LaunchedEffect to perform haptic feedback whenever a drag of the observer position // starts or ends InteractiveViewshedHapticFeedback(viewModel.dragHapticEvents)
MainScreenScaffold( uiState = uiState, onObserverElevationChanged = viewModel::setObserverElevation, onTargetHeightChanged = viewModel::setTargetHeight, onMaxRadiusChanged = viewModel::setMaxRadius, onFieldOfViewChanged = viewModel::setFieldOfView, onHeadingChanged = viewModel::setHeading, onElevationSamplingIntervalChanged = viewModel::setElevationSamplingInterval, mainPaneContent = { Column(modifier = Modifier.fillMaxSize()) { DisplayMessagesAboutMap() MapView( modifier = Modifier.fillMaxSize(), arcGISMap = viewModel.arcGISMap, mapViewProxy = viewModel.mapViewProxy, mapViewInteractionOptions = MapViewInteractionOptions(isEnabled = !viewModel.isDragging), analysisOverlays = listOf(viewModel.analysisOverlay), graphicsOverlays = listOf(viewModel.graphicsOverlay), onSingleTapConfirmed = viewModel::onTap, onLongPress = viewModel::onLongPress, onPan = viewModel::onPan, onAnalysisViewStatusChanged = viewModel::analysisViewStatusListener ) // Show a message dialog if the viewmodel reported an error viewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } } )}
@Composableprivate fun MainScreenScaffold( uiState: ViewshedUiState, onObserverElevationChanged: (Float) -> Unit = {}, onTargetHeightChanged: (Float) -> Unit = {}, onMaxRadiusChanged: (Float) -> Unit = {}, onFieldOfViewChanged: (Float) -> Unit = {}, onHeadingChanged: (Float) -> Unit = {}, onElevationSamplingIntervalChanged: (Double) -> Unit = {}, mainPaneContent: @Composable BoxScope.() -> Unit,) { Scaffold( topBar = { SampleTopAppBar(title = stringResource(R.string.show_interactive_viewshed_with_analysis_overlay_app_name)) }, content = { paddingValues -> AdaptiveThreePane( modifier = Modifier.fillMaxSize().padding(paddingValues), supportingPaneTitle = "Viewshed Parameters", mainPane = { _, _ -> mainPaneContent() }, supportingPane = { _, _ -> ViewshedSupportingContent( uiState = uiState, onObserverElevationChanged = onObserverElevationChanged, onTargetHeightChanged = onTargetHeightChanged, onMaxRadiusChanged = onMaxRadiusChanged, onFieldOfViewChanged = onFieldOfViewChanged, onHeadingChanged = onHeadingChanged, onElevationSamplingIntervalChanged = onElevationSamplingIntervalChanged ) } ) } )}
/** * Display messages about the map: copyright text for the raster data we are using, and instructions * for changing the observer position. */@Composablefun DisplayMessagesAboutMap() { Text( text = "Raster data copyright Scottish Government and SEPA (2014)", style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) Text( text = "Tap on map, or long-press and drag, to change observer position", style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) )}
@Composablefun InteractiveViewshedHapticFeedback(dragHapticEvents: Flow<DragHapticEvent>) { val view = LocalView.current LaunchedEffect(dragHapticEvents, view) { // Perform haptic feedback whenever a drag of the observer position starts or ends dragHapticEvents.collect { event -> val hapticFeedbackConstant = when (event) { DragHapticEvent.Start -> HapticFeedbackConstants.LONG_PRESS DragHapticEvent.End -> HapticFeedbackConstants.CONTEXT_CLICK } view.performHapticFeedback(hapticFeedbackConstant) } }}
@SampleDeviceLightDarkPreview@Composablefun MainScreenPreview() { SamplePreviewSurface { MainScreenScaffold( uiState = initialViewshedUiState, mainPaneContent = {} ) }}/* Copyright 2026 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.showinteractiveviewshedwithanalysisoverlay.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.selection.selectableimport androidx.compose.foundation.selection.selectableGroupimport androidx.compose.material3.RadioButtonimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.style.TextAlignimport androidx.compose.ui.unit.dpimport com.esri.arcgismaps.sample.showinteractiveviewshedwithanalysisoverlay.components.ViewshedUiState
/** * Screen containing UI controls to modify the viewshed parameters. */@Composablefun ViewshedSupportingContent( uiState: ViewshedUiState, onObserverElevationChanged: (Float) -> Unit, onTargetHeightChanged: (Float) -> Unit, onMaxRadiusChanged: (Float) -> Unit, onFieldOfViewChanged: (Float) -> Unit, onHeadingChanged: (Float) -> Unit, onElevationSamplingIntervalChanged: (Double) -> Unit) { Column { ObserverElevationSlider( sliderValue = uiState.observerElevation.toFloat(), onObserverElevationChanged ) TargetHeightSlider(sliderValue = uiState.targetHeight.toFloat(), onTargetHeightChanged) MaxRadiusSlider(sliderValue = uiState.maxRadius.toFloat(), onMaxRadiusChanged) FieldOfViewSlider(sliderValue = uiState.fieldOfView.toFloat(), onFieldOfViewChanged) HeadingSlider(sliderValue = uiState.heading.toFloat(), onHeadingChanged) ElevationSamplingIntervalButtons( initialValue = uiState.elevationSamplingInterval, onElevationSamplingIntervalChanged ) }}
/** * Custom slider implementation, used for several viewshed parameter controls. */@Composablefun ViewshedSlider( title: String, sliderValue: Float, sliderRangeValue: ClosedFloatingPointRange<Float>, units: String, onSliderValueChanged: (Float) -> Unit) { Column(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = title) Text(text = sliderValue.toInt().toString() + units) } Slider( modifier = Modifier.fillMaxWidth(), value = sliderValue, onValueChange = onSliderValueChanged, valueRange = sliderRangeValue ) }}
@Composableprivate fun ObserverElevationSlider(sliderValue: Float, onObserverElevationChanged: (Float) -> Unit) { ViewshedSlider( title = "Observer Elevation", sliderValue = sliderValue, sliderRangeValue = 2f..200f, units = " m", onSliderValueChanged = onObserverElevationChanged )}
@Composableprivate fun TargetHeightSlider(sliderValue: Float, onTargetHeightChanged: (Float) -> Unit) { ViewshedSlider( title = "Target Height", sliderValue = sliderValue, sliderRangeValue = 2f..1000f, units = " m", onSliderValueChanged = onTargetHeightChanged )}
@Composableprivate fun MaxRadiusSlider(sliderValue: Float, onMaxRadiusChanged: (Float) -> Unit) { ViewshedSlider( title = "Maximum Radius", sliderValue = sliderValue, sliderRangeValue = 2500f..20000f, units = " m", onSliderValueChanged = onMaxRadiusChanged )}
@Composableprivate fun FieldOfViewSlider(sliderValue: Float, onFieldOfViewChanged: (Float) -> Unit) { ViewshedSlider( title = "Field of View", sliderValue = sliderValue, sliderRangeValue = 5f..360f, units = "°", onSliderValueChanged = onFieldOfViewChanged )}
@Composableprivate fun HeadingSlider(sliderValue: Float, onHeadingChanged: (Float) -> Unit) { ViewshedSlider( title = "Heading", sliderValue = sliderValue, sliderRangeValue = 0f..360f, units = "°", onSliderValueChanged = onHeadingChanged )}
/** * Use radio buttons to allow one of 3 values to be selected for Elevation Sampling Interval. */@Composableprivate fun ElevationSamplingIntervalButtons( initialValue: Double?, onElevationSamplingIntervalChanged: (Double) -> Unit) { val radioOptions = listOf("0", "10", "20") val initialIndex = when (initialValue) { 10.0 -> 1 20.0 -> 2 else -> 0 } val selectedOption = radioOptions[initialIndex] Column(modifier = Modifier.fillMaxWidth()) { Text(text = "Elevation Sampling Interval (m)") Row( Modifier.fillMaxWidth().selectableGroup(), horizontalArrangement = Arrangement.SpaceEvenly ) { radioOptions.forEach { text -> Row( Modifier.selectable( selected = (text == selectedOption), onClick = { onElevationSamplingIntervalChanged(text.toDouble()) }) ) { RadioButton( selected = (text == selectedOption), onClick = { onElevationSamplingIntervalChanged(text.toDouble()) } ) Text( modifier = Modifier.padding(top = 10.dp, end = 10.dp), text = text, textAlign = TextAlign.Left ) } } } }}