View on GitHub Sample viewer app

Analyze terrain suitability from an elevation raster by deriving slope and aspect.

Analyze terrain suitability from slope and aspect sample

Use case

Terrain suitability analysis is a common way to narrow a larger elevation surface down to areas that match a specific set of conditions. Slope and aspect are derived from elevation datasets to show how steep the terrain is and which direction it faces. Both of these factors can determine whether an area is suitable for a given purpose, for example, finding areas which are more sheltered from weather versus areas with more exposed terrain.

How to use the sample

When the sample opens, the map shows the results of a preconfigured terrain suitability analysis which finds southward facing lowland slopes on the Isle of Arran, Scotland. The areas matching the criteria are rendered in green, and those not, in white. Open the settings panel to choose another preconfigured scenario, that of a west to north facing slope in upland terrains. Areas matching these criteria are rendered in purple.

How it works

  1. Create a ContinuousField from a raster file.
  2. Create a ContinuousFieldFunction from the continuous field.
  3. Derive a slope function and an aspect function from the continuous field function.
  4. Create BooleanFieldFunction masks for slope, aspect, and elevation using range checks with map algebra.
  5. Combine the masks using the infix and function and apply a land-only mask to exclude areas below sea level.
  6. Create a FieldAnalysis from the resultant BooleanFieldFunction.
  7. Apply a ColormapRenderer with a color for areas not matching the terrain suitability criteria, and a color for matching areas.
  8. Add the analysis to an AnalysisOverlay.

Relevant API

  • AnalysisOverlay
  • BooleanFieldFunction
  • Colormap
  • ColormapRenderer
  • ContinuousField
  • ContinuousFieldFunction
  • FieldAnalysis

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

aspect, elevation, field analysis, map algebra, raster, slope, spatial reference, terrain

Sample code

AnalyzeTerrainSuitabilityFromSlopeAndAspectViewModel.kt AnalyzeTerrainSuitabilityFromSlopeAndAspectViewModel.kt MainActivity.kt DownloadActivity.kt AnalyzeTerrainSuitabilityFromSlopeAndAspectSupportingPane.kt AnalyzeTerrainSuitabilityFromSlopeAndAspectScreen.kt
/* 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.analyzeterrainsuitabilityfromslopeandaspect.components
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.analysis.BooleanFieldFunction
import com.arcgismaps.analysis.ContinuousField
import com.arcgismaps.analysis.ContinuousFieldFunction
import com.arcgismaps.analysis.interactive.FieldAnalysis
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.symbology.raster.Colormap
import com.arcgismaps.mapping.symbology.raster.ColormapRenderer
import com.arcgismaps.mapping.view.AnalysisOverlay
import com.arcgismaps.mapping.view.AnalysisViewStatus
import com.arcgismaps.mapping.view.GeoView
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import com.esri.arcgismaps.sample.analyzeterrainsuitabilityfromslopeandaspect.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.File
class AnalyzeTerrainSuitabilityFromSlopeAndAspectViewModel(app: Application) :
AndroidViewModel(app) {
// Create a state flow to hold the UI state for the supporting pane controls
private val _slopeAspectUiState = MutableStateFlow(SlopeAspectUiState.defaultState)
// Expose the state flow as read-only for the UI
val adaptiveUiState = _slopeAspectUiState.asStateFlow()
// Create a MapViewProxy, used to set viewpoint
val mapViewProxy = MapViewProxy()
// Initialize and keep track of the ArcGISMap & the AnalysisOverlay
val arcGISMap = ArcGISMap(SpatialReference(wkid = 32630)) // UTM30N spatial reference
val analysisOverlay = AnalysisOverlay()
// Indicates when the progress indicator should be displayed
var displayProgressIndicator by mutableStateOf(false)
private set
// Create a message dialog view model for handling error messages
val messageDialogVM = MessageDialogViewModel()
// Location of file containing elevation data
private val provisionPath: String by lazy {
app.getExternalFilesDir(null)?.path + File.separator + app.getString(
R.string.analyze_terrain_suitability_from_slope_and_aspect_app_name
) + File.separator
}
private val filePath = provisionPath + "arran.tif"
// FieldAnalysis objects for Sheltered and Exposed analysis scenarios
private var shelteredSlopesAnalysis: FieldAnalysis? = null
private var exposedSlopesAnalysis: FieldAnalysis? = null
init {
viewModelScope.launch {
// Create a ContinuousField from a raster file containing elevation data and project it
// to the UTM30N spatial reference
ContinuousField.createFromFiles(
filePaths = listOf(filePath),
band = 0,
spatialReference = SpatialReference(wkid = 32630)
).onFailure {
messageDialogVM.showMessageDialog(it)
}.onSuccess { continuousField ->
// Center the MapView on the data we have
mapViewProxy.setViewpointCenter(
center = continuousField.extent.center,
scale = 200000.0
)
// Create the continuous field function for the elevation data
val elevationFieldFunction =
ContinuousFieldFunction.create(result = continuousField)
// Derive slope and aspect from the elevation field
val slopeFunction = elevationFieldFunction.slope()
val aspectFunction = elevationFieldFunction.aspect()
// Keep only land areas above sea level
val aboveSeaLevelSelection = elevationFieldFunction.isGreaterThanOrEqualTo(0f)
// Create FieldAnalysis objects for the 2 scenarios to be shown
shelteredSlopesAnalysis = createScenarioAnalysis(
slopeFunction = slopeFunction,
aspectFunction = aspectFunction,
elevationFieldFunction = elevationFieldFunction,
aboveSeaLevelSelection = aboveSeaLevelSelection,
slopeMin = 0f, // flat terrain
slopeMax = 20f, // moderate slopes
aspectStart = 112.5f, // east-south-east facing aspect
aspectEnd = 247.5f, // west-south-west facing aspect
elevationMin = 0f,
elevationMax = 300f, // avoid higher elevations
color = Color.fromRgba(r = 0, g = 180, b = 0, a = 255)
)
exposedSlopesAnalysis = createScenarioAnalysis(
slopeFunction = slopeFunction,
aspectFunction = aspectFunction,
elevationFieldFunction = elevationFieldFunction,
aboveSeaLevelSelection = aboveSeaLevelSelection,
slopeMin = 20f, // moderate slopes
slopeMax = 80f, // very steep slopes
aspectStart = 202.5f, // south-south-west facing aspect
aspectEnd = 67.5f, // east-north-east facing aspect
elevationMin = 300f,
elevationMax = 850f, // higher elevations more exposed
color = Color.fromRgba(r = 180, g = 0, b = 180, a = 255)
)
// Display the progress indicator and make the initially selected scenario
// visible; calculation of the analysis starts when it is made visible
displayProgressIndicator = true
_slopeAspectUiState.value.scenarioOption.let { selectedOption ->
shelteredSlopesAnalysis?.isVisible = selectedOption == ScenarioOption.Sheltered
exposedSlopesAnalysis?.isVisible = selectedOption == ScenarioOption.Exposed
}
}
}
}
/**
* Creates a FieldAnalysis for a given scenario based on slope, aspect, and elevation ranges.
* The FieldAnalysis is added to the AnalysisOverlay, but its visibility is set false so it
* won't be displayed yet.
*/
private fun createScenarioAnalysis(
slopeFunction: ContinuousFieldFunction,
aspectFunction: ContinuousFieldFunction,
elevationFieldFunction: ContinuousFieldFunction,
aboveSeaLevelSelection: BooleanFieldFunction,
slopeMin: Float,
slopeMax: Float,
aspectStart: Float,
aspectEnd: Float,
elevationMin: Float,
elevationMax: Float,
color: Color
): FieldAnalysis {
// Create a BooleanFieldFunction for the scenario
val scenarioFieldFunction = createScenarioFieldFunction(
slopeFunction = slopeFunction,
aspectFunction = aspectFunction,
elevationFieldFunction = elevationFieldFunction,
aboveSeaLevelSelection = aboveSeaLevelSelection,
slopeMin = slopeMin,
slopeMax = slopeMax,
aspectStart = aspectStart,
aspectEnd = aspectEnd,
elevationMin = elevationMin,
elevationMax = elevationMax
)
// Create a colormap and renderer to display the results; white for areas that don't match
// the scenario results, and the given color for those that do
val colormapRenderer = ColormapRenderer(
colormap = Colormap.create(colors = listOf(Color.white, color))
)
// Create the FieldAnalysis, set its visibility to false, and add it to the AnalysisOverlay
val analysis = FieldAnalysis(
booleanFieldFunction = scenarioFieldFunction,
colormapRenderer = colormapRenderer
)
analysis.isVisible = false
analysisOverlay.analyses.add(analysis)
return analysis
}
/**
* Creates a BooleanFieldFunction for a given scenario based on slope, aspect, and elevation
* ranges.
*/
private fun createScenarioFieldFunction(
slopeFunction: ContinuousFieldFunction,
aspectFunction: ContinuousFieldFunction,
elevationFieldFunction: ContinuousFieldFunction,
aboveSeaLevelSelection: BooleanFieldFunction,
slopeMin: Float,
slopeMax: Float,
aspectStart: Float,
aspectEnd: Float,
elevationMin: Float,
elevationMax: Float,
): BooleanFieldFunction {
// Create BooleanFieldFunctions for slope, aspect and elevation that assign pixels a value
// of 1 when within the range of values provided for the scenario, and 0 when outside the
// range. Note that `and` and `or` functions can be called using the infix notation.
val slopeMinMask = slopeFunction.isGreaterThanOrEqualTo(slopeMin)
val slopeMaxMask = slopeFunction.isLessThanOrEqualTo(slopeMax)
val slopeRangeMask = slopeMinMask and slopeMaxMask
val elevationMinMask = elevationFieldFunction.isGreaterThanOrEqualTo(elevationMin)
val elevationMaxMask = elevationFieldFunction.isLessThanOrEqualTo(elevationMax)
val elevationRangeMask = elevationMinMask and elevationMaxMask
// Handle the case where the aspect range crosses the 0-degree line (e.g. 225 to 45 degrees)
val aspectRangeMask = if (aspectStart <= aspectEnd) {
val aspectMinMask = aspectFunction.isGreaterThanOrEqualTo(aspectStart)
val aspectMaxMask = aspectFunction.isLessThanOrEqualTo(aspectEnd)
aspectMinMask and aspectMaxMask
} else {
val upperAspectMinMask = aspectFunction.isGreaterThanOrEqualTo(aspectStart)
val upperAspectMaxMask = aspectFunction.isLessThan(360f)
val upperAspectBandMask = upperAspectMinMask and upperAspectMaxMask
val lowerAspectMinMask = aspectFunction.isGreaterThanOrEqualTo(0f)
val lowerAspectMaxMask = aspectFunction.isLessThanOrEqualTo(aspectEnd)
val lowerAspectBandMask = lowerAspectMinMask and lowerAspectMaxMask
upperAspectBandMask or lowerAspectBandMask
}
// Combine the slope, aspect, and elevation masks with the land-only aboveSeaLevelSelection
// to create a final BooleanFieldFunction for the scenario
val scenarioRangeMask = slopeRangeMask and aspectRangeMask and elevationRangeMask
return scenarioRangeMask.mask(aboveSeaLevelSelection)
}
/**
* An AnalysisViewStatus listener that displays the progress indicator when the status of the
* current scenario analysis is Updating and hides it when not Updating.
* Also displays details of any error that occurs when the analysis is displayed.
*/
fun analysisViewStatusListener(event: GeoView.GeoViewAnalysisViewStatusChanged) {
displayProgressIndicator = when (event.analysisViewStatus){
is AnalysisViewStatus.Error -> {
messageDialogVM.showMessageDialog(
throwable = (event.analysisViewStatus as AnalysisViewStatus.Error).details
)
false
}
AnalysisViewStatus.UpToDate -> {
false
}
AnalysisViewStatus.Updating -> {
true
}
}
}
/**
* Updates the currently selected ScenarioOption.
*/
fun updateScenarioOption(selectedScenarioOption: ScenarioOption) {
// Update the UI state
_slopeAspectUiState.update { currentState ->
currentState.copy(scenarioOption = selectedScenarioOption)
}
// Keep only the selected scenario analysis visible.
_slopeAspectUiState.value.scenarioOption.let { selectedOption ->
shelteredSlopesAnalysis?.isVisible = selectedOption == ScenarioOption.Sheltered
exposedSlopesAnalysis?.isVisible = selectedOption == ScenarioOption.Exposed
}
}
}
data class SlopeAspectUiState(val scenarioOption: ScenarioOption) {
companion object {
val defaultState = SlopeAspectUiState(
scenarioOption = ScenarioOption.Sheltered
)
}
}
enum class ScenarioOption {
Sheltered, Exposed
}