Apply map algebra to an elevation raster to floor, mask, and categorize the elevation values into discrete integer-based categories.
Use case
Categorizing raster data, such as elevation values, into distinct categories is a common spatial analysis workflow. This often involves applying threshold‑based logic or algebraic expressions to transform continuous numeric fields into discrete, integer‑based categories suitable for downstream analytical or computational operations. These operations can be specified and applied using map algebra.
How to use the sample
When the sample opens, it displays the source elevation raster. Tap the Categorize button to generate a raster with three distinct ice age related geomorphological categories (raised shore line areas in blue, ice free high ground in brown and areas covered by ice in teal). After processing completes, switch between the map algebra results raster and the original elevation raster.
How it works
- Create a
ContinuousFieldfrom a raster file. - Create a
ContinuousFieldFunctionfrom the continuous field and mask values below sea level. - Round elevation values down to the lowest 10-meter interval with map algebra operators
floor(continuousFieldFunction / 10f) * 10f, and then convert the result to aDiscreteFieldFunctionwith.toDiscreteFieldFunction. - Create
BooleanFieldFunctions for each category by defining a range with map algebra operators such asisGreaterThanOrEqualTo,and, andisLessThan. - Create a new
DiscreteFieldby chainingreplaceIfoperations into discrete category values and evaluating the result withevaluate. - Export the discrete field to files with
exportToFilesand create aRasterwith the result. Use it to create aRasterLayer. - Apply a
ColormapRendererto the raster and display it in the map view.
Relevant API
- BooleanFieldFunction
- Colormap
- ColormapRenderer
- ColorRamp
- ContinuousField
- ContinuousFieldFunction
- DiscreteField
- DiscreteFieldFunction
- Raster
- RasterLayer
- StretchRenderer
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)).
Additional information
This sample requires an ArcGIS Maps SDK Analysis extension license key. Without this license, the map algebra analysis will fail at runtime.
Tags
elevation, map algebra, raster, spatial analysis, terrain
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.applymapalgebra.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.ContinuousField
import com.arcgismaps.analysis.ContinuousFieldFunction
import com.arcgismaps.analysis.floor
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.BasemapStyle
import com.arcgismaps.mapping.Viewpoint
import com.arcgismaps.mapping.layers.RasterLayer
import com.arcgismaps.mapping.symbology.raster.ColorRamp
import com.arcgismaps.mapping.symbology.raster.Colormap
import com.arcgismaps.mapping.symbology.raster.ColormapRenderer
import com.arcgismaps.mapping.symbology.raster.StretchRenderer
import com.arcgismaps.mapping.symbology.raster.MinMaxStretchParameters
import com.arcgismaps.mapping.symbology.raster.PresetColorRampType
import com.arcgismaps.raster.Raster
import com.esri.arcgismaps.sample.applymapalgebra.R
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.launch
import java.io.File
import kotlin.io.path.absolutePathString
import kotlin.io.path.createTempDirectory
private const val ORIGINAL_ELEVATION_LAYER_NAME = "Original elevation"
private const val MAP_ALGEBRA_RESULTS_LAYER_NAME = "Map algebra results"
/**
* ViewModel for the Apply map algebra sample.
*
* The sample starts by showing the original elevation raster. After the user runs
* categorization, it adds a results layer and lets the user switch between the two layers.
*/
class ApplyMapAlgebraViewModel(app: Application) : AndroidViewModel(app) {
// Path where sample data would be provisioned if available.
private val provisionPath: String by lazy {
app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(
R.string.apply_map_algebra_app_name
)
}
// Local raster expected by the sample workflow.
private val elevationRasterPath = provisionPath + File.separator + "arran.tif"
// The map displayed by the MapView.
val arcGISMap =
ArcGISMap(BasemapStyle.ArcGISHillshadeDark).apply {
initialViewpoint = Viewpoint(
55.584612,
-5.234218,
500_000.0
)
}
// Controls whether the UI should show the layer switcher.
var hasMapAlgebraResults by mutableStateOf(false)
private set
// UI state for running analysis
var isPerformingAnalysis by mutableStateOf(false)
private set
// Currently selected visible raster layer name.
var selectedRasterLayerName by mutableStateOf(ORIGINAL_ELEVATION_LAYER_NAME)
private set
// Show the results option only after processing creates the output raster.
val availableLayerNames: List<String>
get() = if (!hasMapAlgebraResults) {
listOf(ORIGINAL_ELEVATION_LAYER_NAME)
} else {
listOf(ORIGINAL_ELEVATION_LAYER_NAME, MAP_ALGEBRA_RESULTS_LAYER_NAME)
}
// Used to surface errors to the Compose UI
val messageDialogVM = MessageDialogViewModel()
init {
// Load the source elevation raster at startup so users see the original layer first.
if (File(elevationRasterPath).exists()) {
val elevationLayer = RasterLayer(Raster.createWithPath(elevationRasterPath)).apply {
name = ORIGINAL_ELEVATION_LAYER_NAME
}
arcGISMap.operationalLayers += elevationLayer
selectedRasterLayerName = ORIGINAL_ELEVATION_LAYER_NAME
// Create a stretch renderer to visualize elevation values
val stretchParams = MinMaxStretchParameters(minValues = listOf(0.0), maxValues = listOf(874.0))
val colorRamp = ColorRamp.create(
type = PresetColorRampType.Surface,
size = 256
)
val stretchRenderer = StretchRenderer(
parameters = stretchParams,
gammas = listOf(1.0),
estimateStatistics = false,
colorRamp = colorRamp
)
// Load the layer and apply the renderer
viewModelScope.launch {
elevationLayer.load()
.onSuccess {
elevationLayer.renderer = stretchRenderer
elevationLayer.opacity = 0.5f
}
.onFailure { messageDialogVM.showMessageDialog(it) }
}
} else {
messageDialogVM.showMessageDialog(
title = "Elevation raster not found",
description = "Place arran.tif into the sample's provisioned folder: $provisionPath"
)
}
viewModelScope.launch {
// Load the map
arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) }
}
}
/**
* Runs map algebra on the source elevation raster and creates the
* "Map algebra results" layer used by the sample's layer switcher.
*/
fun categorizeElevation() {
// Ensure we have a raster to analyze
if (!File(elevationRasterPath).exists()) {
return messageDialogVM.showMessageDialog(
title = "Elevation raster not found",
description = "Place arran.tif into the sample's provisioned folder: $provisionPath"
)
}
viewModelScope.launch {
isPerformingAnalysis = true
try {
// Build a continuous field from the source elevation raster.
val elevationField = ContinuousField.createFromFiles(
filePaths = listOf(elevationRasterPath),
band = 0
).getOrThrow()
// Mask out values below sea level before classifying the terrain.
val continuousFieldFunction = ContinuousFieldFunction.create(elevationField)
val elevationFieldFunction = continuousFieldFunction.mask(
selection = continuousFieldFunction.isGreaterThanOrEqualTo(0.0f)
)
// Group the elevation values into 10-meter bins.
val tenMeterBin = (floor(elevationFieldFunction / 10f) * 10f).toDiscreteFieldFunction()
// Build the three geomorphic categories used by the sample.
val isRaisedShoreline = tenMeterBin.isGreaterThanOrEqualTo(0) and tenMeterBin.isLessThan(10)
val isIceCovered = tenMeterBin.isGreaterThanOrEqualTo(10) and tenMeterBin.isLessThan(600)
val isIceFreeHighGround = tenMeterBin.isGreaterThanOrEqualTo(600)
// Create and evaluate a function that replaces each matching range with a category value.
val geomorphicCategoryFieldFunction = tenMeterBin
.replaceIf(isRaisedShoreline, 1)
.replaceIf(isIceCovered, 2)
.replaceIf(isIceFreeHighGround, 3)
val geomorphicCategoryField = geomorphicCategoryFieldFunction.evaluate().getOrThrow()
// Export the processed data and read it back as a Raster.
val exportedFiles = geomorphicCategoryField.exportToFiles(
outputDirectory = createTempDirectory().absolutePathString(),
filenamesPrefix = "geomorphicCategorization"
).getOrThrow()
val resultRaster = Raster.createWithPath(exportedFiles.first())
// Create a colormap renderer for the geomorphic categories.
val colormap = Colormap.create(
mapOf(
1 to Color.fromRgba(82, 158, 235, 255), // Raised shoreline - blue
2 to Color.fromRgba(102, 204, 204, 255), // Ice covered - teal
3 to Color.fromRgba(140, 100, 65, 255), // Ice-free high ground - brown
)
)
val colormapRenderer = ColormapRenderer(colormap)
// Create a RasterLayer from the result Raster and apply the colormap renderer.
val resultLayer = RasterLayer(resultRaster).apply {
name = MAP_ALGEBRA_RESULTS_LAYER_NAME
renderer = colormapRenderer
opacity = 0.5f
}
// Load the result layer and add to the map's operational layers.
resultLayer.load().onSuccess {
arcGISMap.operationalLayers += resultLayer
hasMapAlgebraResults = true
selectRasterLayer(MAP_ALGEBRA_RESULTS_LAYER_NAME)
}.getOrThrow()
} catch (throwable: Throwable) {
messageDialogVM.showMessageDialog(
title = "Error during analysis",
description = throwable.message.toString()
)
}
finally {
isPerformingAnalysis = false
}
}
}
/**
* Helper to toggle visibility between the original elevation raster and the results raster.
*/
fun selectRasterLayer(layerName: String) {
arcGISMap.operationalLayers
.filterIsInstance<RasterLayer>()
.forEach { layer ->
val isSelected = layer.name == layerName
layer.isVisible = isSelected
if (isSelected) selectedRasterLayerName = layerName
}
}
}