View on GitHub Sample viewer app

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

  1. Create a blank Map with a spatial reference set to UTM 30N so the analysis runs in a conformal coordinate system.
  2. Create a ContinuousField from the elevation raster and project it to the map spatial reference.
  3. Create a ContinuousFieldFunction from the continuous field and derive slope and aspect functions.
  4. Build BooleanFieldFunction masks for slope, aspect, elevation, and land-only areas using range checks and long-form boolean field methods.
  5. Combine the masks with logicalAnd and logicalOr to build a final scenario mask.
  6. Convert the final mask to a DiscreteFieldFunction and create a FieldAnalysis from it.
  7. Apply a ColormapRenderer with white for non-matching areas and green or purple for matching areas.
  8. Add the analyses to an AnalysisOverlay and 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

AnalyzeTerrainSuitabilityWithSlopeAndAspect.cpp AnalyzeTerrainSuitabilityWithSlopeAndAspect.cpp AnalyzeTerrainSuitabilityWithSlopeAndAspect.h AnalyzeTerrainSuitabilityWithSlopeAndAspect.qml
// [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 path
namespace
{
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();
}