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

Use case
Terrain suitability analysis narrows 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. Those factors can determine whether an area is suitable for a given purpose, such as finding more sheltered terrain versus more exposed terrain.
How to use the sample
When the sample opens, the map shows a preconfigured terrain suitability analysis for south-facing lowland slopes on the Isle of Arran, Scotland. The matching areas are rendered in green and the non-matching areas are rendered in white. Open the settings panel to switch to the second scenario, which highlights west- to north-facing upland slopes in purple.
How it works
- Create a blank
Mapwith a spatial reference set to UTM 30N so the analysis runs in a conformal coordinate system. - Create a
ContinuousFieldfrom the elevation raster and project it to the map spatial reference. - Create a
ContinuousFieldFunctionfrom the continuous field and deriveslopeandaspectfunctions. - Build
BooleanFieldFunctionmasks for slope, aspect, elevation, and land-only areas using range checks and long-form boolean field methods. - Combine the masks with
logicalAndandlogicalOrto build a final scenario mask. - Convert the final mask to a
DiscreteFieldFunctionand create aFieldAnalysisfrom it. - Apply a
ColormapRendererwith white for non-matching areas and green or purple for matching areas. - Add the analyses to an
AnalysisOverlayand toggle their visibility from the settings panel.
Relevant API
- AnalysisOverlay
- BooleanFieldFunction
- Colormap
- ColormapRenderer
- ContinuousField
- ContinuousFieldFunction
- DiscreteFieldFunction
- FieldAnalysis
- Map
- MapQuickView
- SpatialReference
About the data
The sample uses a 10m resolution digital terrain elevation raster of the Isle of Arran, Scotland from ArcGIS Online.
The data requires this attribution to be shown in the app display somewhere:
Raster data Copyright Scottish Government and SEPA (2014)
Tags
aspect, elevation, field analysis, map algebra, raster, slope, spatial reference, terrain
Sample code
// [WriteFile Name=AnalyzeTerrainSuitabilityWithSlopeAndAspect, Category=Analysis]// [Legal]// 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.// [Legal]
#ifdef PCH_BUILD#include "pch.hpp"#endif // PCH_BUILD
// sample headers#include "AnalyzeTerrainSuitabilityWithSlopeAndAspect.h"
// ArcGIS Maps SDK headers#include "AnalysisListModel.h"#include "AnalysisOverlay.h"#include "AnalysisOverlayListModel.h"#include "BooleanFieldFunction.h"#include "Colormap.h"#include "ColormapRenderer.h"#include "ContinuousField.h"#include "ContinuousFieldFunction.h"#include "DiscreteFieldFunction.h"#include "Envelope.h"#include "FieldAnalysis.h"#include "Map.h"#include "MapQuickView.h"#include "Point.h"#include "SpatialReference.h"
// Qt headers#include <QColor>#include <QFileInfo>#include <QFuture>#include <QStandardPaths>
using namespace Esri::ArcGISRuntime;
// helper method to get cross platform data pathnamespace{ QString defaultDataPath() { QString dataPath;
#ifdef Q_OS_IOS dataPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);#else dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);#endif
return dataPath; }} // namespace
AnalyzeTerrainSuitabilityWithSlopeAndAspect::AnalyzeTerrainSuitabilityWithSlopeAndAspect(QObject* parent /* = nullptr */) : QObject(parent){}
AnalyzeTerrainSuitabilityWithSlopeAndAspect::~AnalyzeTerrainSuitabilityWithSlopeAndAspect() = default;
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::init(){ qmlRegisterType<MapQuickView>("Esri.Samples", 1, 0, "MapView"); qmlRegisterType<AnalyzeTerrainSuitabilityWithSlopeAndAspect>("Esri.Samples", 1, 0, "AnalyzeTerrainSuitabilityWithSlopeAndAspectSample");}
MapQuickView* AnalyzeTerrainSuitabilityWithSlopeAndAspect::mapView() const{ return m_mapView;}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::setMapView(MapQuickView* mapView){ if (!mapView || mapView == m_mapView) { return; }
m_mapView = mapView;
m_map = new Map(SpatialReference(32630), this); loadElevationField();
m_mapView->setMap(m_map);
emit mapViewChanged();}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::loadElevationField(){ const QString rasterPath = defaultDataPath() + QStringLiteral("/ArcGIS/Runtime/Data/raster/arran.tif"); if (!QFileInfo::exists(rasterPath)) { return; }
// Load the raster into a continuous field that can be queried for slope and aspect. const SpatialReference spatialReference(32630); ContinuousField::createFromFilesAsync({rasterPath}, 0, spatialReference, this) .then(this, [this](ContinuousField* continuousField) { onContinuousFieldCreated(continuousField); });}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::onContinuousFieldCreated(ContinuousField* continuousField){ if (!continuousField) { return; }
// Create the analysis overlay once the raster field is available. if (!m_analysisOverlay && m_mapView) { m_analysisOverlay = new AnalysisOverlay(this); m_mapView->analysisOverlays()->append(m_analysisOverlay); }
// Derive slope and aspect from the elevation field m_elevationFieldFunction = ContinuousFieldFunction::create(continuousField, this); m_slopeFunction = m_elevationFieldFunction->slope(); m_aspectFunction = m_elevationFieldFunction->aspect(); m_aboveSeaLevelSelection = m_elevationFieldFunction->isGreaterThanOrEqualTo(0.0F);
// Build both scenarios up front and toggle visibility when the user changes selection. buildAnalysisForScenario(GentleSouthFacingSlopes); buildAnalysisForScenario(SteepWestAndNorthFacingSlopes); applyFieldAnalysisVisibility();
if (m_mapView) { m_mapView->setViewpointCenterAsync(continuousField->extent().center(), 200000); }}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::buildAnalysisForScenario(SiteScenario scenario){ switch (scenario) { case GentleSouthFacingSlopes: if (m_gentleSouthFacingSlopesAnalysis) { return; }
// Green highlights sheltered, lowland south-facing areas. m_gentleSouthFacingSlopesAnalysis = createScenarioAnalysis(0.0F, 20.0F, 112.5F, 247.5F, 0.0F, 300.0F, QColor(Qt::green)); if (m_gentleSouthFacingSlopesAnalysis) { m_analysisOverlay->analyses()->append(m_gentleSouthFacingSlopesAnalysis); } break;
case SteepWestAndNorthFacingSlopes: if (m_steepWestAndNorthFacingSlopesAnalysis) { return; }
// Purple highlights steeper upland terrain facing west through north. m_steepWestAndNorthFacingSlopesAnalysis = createScenarioAnalysis(20.0F, 80.0F, 202.5F, 67.5F, 300.0F, 850.0F, QColor("purple")); if (m_steepWestAndNorthFacingSlopesAnalysis) { m_analysisOverlay->analyses()->append(m_steepWestAndNorthFacingSlopesAnalysis); } break; }}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::applyFieldAnalysisVisibility(){ buildAnalysisForScenario(GentleSouthFacingSlopes); buildAnalysisForScenario(SteepWestAndNorthFacingSlopes);
if (m_gentleSouthFacingSlopesAnalysis) { m_gentleSouthFacingSlopesAnalysis->setVisible(m_selectedScenario == GentleSouthFacingSlopes); }
if (m_steepWestAndNorthFacingSlopesAnalysis) { m_steepWestAndNorthFacingSlopesAnalysis->setVisible(m_selectedScenario == SteepWestAndNorthFacingSlopes); }}
FieldAnalysis* AnalyzeTerrainSuitabilityWithSlopeAndAspect::createScenarioAnalysis(float slopeMin, float slopeMax, float aspectStart, float aspectEnd, float elevationMin, float elevationMax, const QColor& color){ // Build the scenario mask first, then convert it into a field analysis rendered with two colors. BooleanFieldFunction* scenarioFieldFunction = createScenarioFieldFunction(slopeMin, slopeMax, aspectStart, aspectEnd, elevationMin, elevationMax);
const QList<QColor> colors{QColor(Qt::white), color}; ColormapRenderer* renderer = new ColormapRenderer(Colormap::create(colors, this), this);
FieldAnalysis* analysis = FieldAnalysis::create(scenarioFieldFunction->toDiscreteFieldFunction(), renderer, this); if (!analysis) { return nullptr; }
analysis->setVisible(false); return analysis;}
BooleanFieldFunction* AnalyzeTerrainSuitabilityWithSlopeAndAspect::createScenarioFieldFunction(float slopeMin, float slopeMax, float aspectStart, float aspectEnd, float elevationMin, float elevationMax){ // Each mask represents a terrain condition that must be satisfied. BooleanFieldFunction* slopeRangeMask = m_slopeFunction->isGreaterThanOrEqualTo(slopeMin)->logicalAnd(m_slopeFunction->isLessThanOrEqualTo(slopeMax));
BooleanFieldFunction* aspectRangeMask = nullptr; if (aspectStart <= aspectEnd) { // Normal (non-wrapping) aspect range, such as south-facing terrain. aspectRangeMask = m_aspectFunction->isGreaterThanOrEqualTo(aspectStart)->logicalAnd(m_aspectFunction->isLessThanOrEqualTo(aspectEnd)); } else { // Wrapped aspect range, such as west through north spanning 0 degrees. BooleanFieldFunction* aspectFromStartToNorth = m_aspectFunction->isGreaterThanOrEqualTo(aspectStart)->logicalAnd(m_aspectFunction->isLessThan(360.0F)); BooleanFieldFunction* aspectFromZeroToEnd = m_aspectFunction->isGreaterThanOrEqualTo(0.0F)->logicalAnd(m_aspectFunction->isLessThanOrEqualTo(aspectEnd)); aspectRangeMask = aspectFromStartToNorth->logicalOr(aspectFromZeroToEnd); }
// Elevation keeps the analysis within the intended lowland/upland bands. BooleanFieldFunction* elevationRangeMask = m_elevationFieldFunction->isGreaterThanOrEqualTo(elevationMin)->logicalAnd(m_elevationFieldFunction->isLessThanOrEqualTo(elevationMax));
return slopeRangeMask->logicalAnd(aspectRangeMask)->logicalAnd(elevationRangeMask)->logicalAnd(m_aboveSeaLevelSelection);}
void AnalyzeTerrainSuitabilityWithSlopeAndAspect::setSelectedScenario(SiteScenario scenario){ if (m_selectedScenario == scenario) { return; }
m_selectedScenario = scenario; if (m_analysisOverlay) { applyFieldAnalysisVisibility(); }
emit selectedScenarioChanged();}// [WriteFile Name=AnalyzeTerrainSuitabilityWithSlopeAndAspect, Category=Analysis]// [Legal]// 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.// [Legal]
#ifndef ANALYZETERRAINSUITABILITYWITHSLOPEANDASPECT_H#define ANALYZETERRAINSUITABILITYWITHSLOPEANDASPECT_H
// Qt headers#include <QColor>#include <QObject>
namespace Esri::ArcGISRuntime{ class AnalysisOverlay; class BooleanFieldFunction; class ContinuousField; class ContinuousFieldFunction; class FieldAnalysis; class Map; class MapQuickView;} // namespace Esri::ArcGISRuntime
Q_MOC_INCLUDE("MapQuickView.h");
class AnalyzeTerrainSuitabilityWithSlopeAndAspect : public QObject{ Q_OBJECT
Q_PROPERTY(Esri::ArcGISRuntime::MapQuickView* mapView READ mapView WRITE setMapView NOTIFY mapViewChanged) Q_PROPERTY(SiteScenario selectedScenario MEMBER m_selectedScenario WRITE setSelectedScenario NOTIFY selectedScenarioChanged)
public: enum SiteScenario { GentleSouthFacingSlopes, SteepWestAndNorthFacingSlopes }; Q_ENUM(SiteScenario)
explicit AnalyzeTerrainSuitabilityWithSlopeAndAspect(QObject* parent = nullptr); ~AnalyzeTerrainSuitabilityWithSlopeAndAspect() override;
static void init();
signals: void mapViewChanged(); void selectedScenarioChanged();
private: Esri::ArcGISRuntime::MapQuickView* mapView() const; void setMapView(Esri::ArcGISRuntime::MapQuickView* mapView); void loadElevationField(); void onContinuousFieldCreated(Esri::ArcGISRuntime::ContinuousField* continuousField); void buildAnalysisForScenario(SiteScenario scenario); void applyFieldAnalysisVisibility(); Esri::ArcGISRuntime::FieldAnalysis* createScenarioAnalysis(float slopeMin, float slopeMax, float aspectStart, float aspectEnd, float elevationMin, float elevationMax, const QColor& color); Esri::ArcGISRuntime::BooleanFieldFunction* createScenarioFieldFunction(float slopeMin, float slopeMax, float aspectStart, float aspectEnd, float elevationMin, float elevationMax);
void setSelectedScenario(SiteScenario scenario);
Esri::ArcGISRuntime::Map* m_map = nullptr; Esri::ArcGISRuntime::MapQuickView* m_mapView = nullptr; Esri::ArcGISRuntime::AnalysisOverlay* m_analysisOverlay = nullptr; Esri::ArcGISRuntime::ContinuousFieldFunction* m_elevationFieldFunction = nullptr; Esri::ArcGISRuntime::ContinuousFieldFunction* m_slopeFunction = nullptr; Esri::ArcGISRuntime::ContinuousFieldFunction* m_aspectFunction = nullptr; Esri::ArcGISRuntime::BooleanFieldFunction* m_aboveSeaLevelSelection = nullptr; Esri::ArcGISRuntime::FieldAnalysis* m_gentleSouthFacingSlopesAnalysis = nullptr; Esri::ArcGISRuntime::FieldAnalysis* m_steepWestAndNorthFacingSlopesAnalysis = nullptr;
SiteScenario m_selectedScenario = GentleSouthFacingSlopes;};
#endif // ANALYZETERRAINSUITABILITYWITHSLOPEANDASPECT_H// [WriteFile Name=AnalyzeTerrainSuitabilityWithSlopeAndAspect, Category=Analysis]// [Legal]// 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.// [Legal]
import QtQuickimport QtQuick.Controlsimport QtQuick.Layoutsimport Esri.Samples
Item { readonly property bool compactUi: width < 760
AnalyzeTerrainSuitabilityWithSlopeAndAspectSample { id: model }
MapView { id: view anchors.fill: parent objectName: "mapView" focus: true
Component.onCompleted: forceActiveFocus() }
Component.onCompleted: { model.mapView = view }
Rectangle { id: settingsPanel anchors.top: parent.top anchors.right: parent.right anchors.topMargin: compactUi ? 26 : 32 anchors.rightMargin: compactUi ? 10 : 16 radius: 12 color: palette.base border.color: palette.mid border.width: 2 opacity: 0.85 z: 5 readonly property int panelPadding: compactUi ? 10 : 14 width: Math.min(settingsColumn.implicitWidth + panelPadding * 2, parent.width - anchors.rightMargin * 2) height: settingsColumn.implicitHeight + panelPadding * 2
MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => mouse.accepted = true onDoubleClicked: mouse => mouse.accepted = true onWheel: wheel => wheel.accepted = true }
ColumnLayout { id: settingsColumn anchors.fill: parent anchors.margins: settingsPanel.panelPadding spacing: compactUi ? 4 : 6
Label { text: qsTr("Sheltered vs Exposed Terrain Suitability") font.bold: true wrapMode: Text.WordWrap Layout.fillWidth: true Layout.preferredWidth: 280 }
Label { text: qsTr("Choose a preconfigured terrain suitability scenario.") wrapMode: Text.WordWrap Layout.fillWidth: true Layout.preferredWidth: 280 }
ButtonGroup { id: scenarioGroup }
RadioButton { ButtonGroup.group: scenarioGroup checked: model.selectedScenario === AnalyzeTerrainSuitabilityWithSlopeAndAspectSample.GentleSouthFacingSlopes text: qsTr("Gentle, lowland south-facing slopes") Layout.fillWidth: true onClicked: model.selectedScenario = AnalyzeTerrainSuitabilityWithSlopeAndAspectSample.GentleSouthFacingSlopes }
RadioButton { ButtonGroup.group: scenarioGroup checked: model.selectedScenario === AnalyzeTerrainSuitabilityWithSlopeAndAspectSample.SteepWestAndNorthFacingSlopes text: qsTr("Steep, upland west- through north-facing slopes") Layout.fillWidth: true onClicked: model.selectedScenario = AnalyzeTerrainSuitabilityWithSlopeAndAspectSample.SteepWestAndNorthFacingSlopes } } }
Label { anchors.left: parent.left anchors.top: parent.top anchors.leftMargin: 8 anchors.topMargin: 8 text: qsTr("Raster data Copyright Scottish Government and SEPA (2014)") font.italic: true font.pointSize: 12 color: palette.text font.bold: true }}