Skip to content
View on GitHubSample viewer app

Apply map algebra to an elevation raster to floor, mask, and categorize the elevation values into discrete integer-based categories.

Apply map algebra sample

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

  1. Create a ContinuousField from a raster file.
  2. Create a ContinuousFieldFunction from the continuous field and mask values below sea level.
  3. Round elevation values down to the lowest 10-meter interval with map algebra operators floor(continuousFieldFunction / 10f) * 10f, and then convert the result to a DiscreteFieldFunction with .toDiscreteFieldFunction.
  4. Create BooleanFieldFunctions for each category by defining a range with map algebra operators such as isGreaterThanOrEqualTo, and, and isLessThan.
  5. Create a new DiscreteField by chaining replaceIf operations into discrete category values and evaluating the result with evaluate.
  6. Export the discrete field to files with exportToFiles and create a Raster with the result. Use it to create a RasterLayer.
  7. Apply a ColormapRenderer to 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

ApplyMapAlgebraViewModel.ktApplyMapAlgebraViewModel.ktMainActivity.ktDownloadActivity.ktApplyMapAlgebraScreen.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
/* 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
            }
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.