ArcGIS Blog

Developers

Developers

Learn how to calculate slope and aspect with Native Maps SDKs

By Rachael Ellen and Isaac Hall

We’re excited to share that, with the release of ArcGIS Maps SDKs for Native Apps 300.1, you can now calculate slope and aspect from a raster.

Slope and aspect are the two latest features to join the growing library of tools available in the spatial analysis APIs (first introduced at 300.0). This means you can now use Native Maps SDKs to build apps that calculate, visualize, and analyze terrain using slope and aspect derived from digital elevation models.

In this blog, we’ll walk through what slope and aspect are, how you can use the new features in your Native Maps SDKs app, and how to get started.

The code snippets, API reference links, and documentation links shown throughout this blog post mostly use ArcGIS Maps SDK for Flutter, but the same concepts apply across all Native Maps SDKs.

What are the spatial analysis APIs?

Before we jump into slope and aspect, here’s a quick overview of what the spatial analysis APIs are. They provide a collection of analysis tools that operate on your source data at full resolution. The APIs use the GPU for fast execution, enabling high-precision, high-performance analysis across the range of platforms supported by Native Maps SDKs.

The spatial analysis APIs follow a functional approach to analysis. You start with a ContinuousField representing your source data, such as a digital elevation model (DEM). You then apply one or more ContinuousFieldFunction operations, such as slope, aspect, or map algebra expressions, to define the analysis you want to perform before evaluating the result when needed. Because functions can be chained together before evaluation, you can build complex analysis workflows from a series of simple operations.

In 300.0, we introduced the initial set of functions, including viewshed and line-of-sight analysis, map algebra, and an analysis overlay for interactive workflows. With 300.1, slope, aspect, and more map algebra tools have joined this growing library of spatial analysis capabilities. Check out the latest release blog to learn more about other new features.

Slope

Slope describes how steep or shallow a surface is. It represents the rate of change in elevation across a digital elevation model (DEM). Slope is one of the most commonly used terrain derivatives in GIS, as it helps answer practical questions on how people and natural processes interact with the landscape.

For example, imagine a search and rescue team responding in an area that’s off the beaten track, inaccessible to road vehicles. Using the spatial analysis APIs to calculate slope from a DEM stored on the device, rescuers could quickly identify routes suitable for off-road vehicles whilst avoiding terrain that is too steep to traverse safely. If the analysis were then to show there is no way to use even an off-road vehicle, the new tools can help identify places to safely drop off/pick up teams via helicopter.

To calculate slope, create a ContinuousField from your DEM, create a ContinuousFieldFunction from it, and then call slope to create a new function representing the slope calculation. When evaluated, the spatial analysis APIs use the GPU to efficiently calculate terrain gradients across the elevation surface, potentially processing millions of cells depending on the size of your dataset. These gradients are then used to derive slope values, producing a field containing values in degrees from 0° (flat) to 90° (vertical). You can learn more about inputs and results for slope in the API reference documentation.

Once evaluated, the result is returned as a field that can be displayed, exported, or used as input to further analysis – see the documentation and samples link at the end of this blog to learn more.

The code below steps through the workflow.


Future<RasterLayer> buildSlopeRasterLayer(Uri elevationFilePath) async {
  final continuousField = await ContinuousField.createFromFiles(
    filePaths: [elevationFilePath],
    band: 0,
  );

  final elevationFunction = ContinuousFieldFunction.create(continuousField);
  final slopeFunction = elevationFunction.slope();
  final evaluatedField = await slopeFunction.evaluate();

  final exportedFiles = await evaluatedField .exportToFiles(
    outputDirectory: outputDirectoryUri,
    filenamesPrefix: 'slope_result',
  );

  final raster = Raster.withFileUri(exportedFiles.first);
  final layer = RasterLayer.withRaster(raster)
    ..renderer = StretchRenderer(
      stretchParameters: MinMaxStretchParameters(
        minValues: [0.0],
        maxValues: [90.0],
      ),
      gammas: [1.0],
      estimateStatistics: false,
      colorRamp: ColorRamp(
        type: PresetColorRampType.viridis,
        size: 256,
      ),
    );
  await layer.load();
  return layer;
}

A color-coded slope map showing steep volcanic crater walls and flatter valley floors.
Slope visualization calculated from a DEM of Tongariro National Park, New Zealand. Terrain steepness is rendered using a `viridis` color ramp, with darker colours indicating low-angle terrain (such as floors of volcanic craters) and brighter colours indicating steeper slopes around volcanic landforms and valley walls.

Aspect

Aspect describes the compass direction that a slope faces. To put it another way, it identifies the downslope direction of the steepest descent of a slope. Knowing the direction a slope faces could be important for a variety of workflows. For example, south-facing slopes in the northern hemisphere typically receive more solar energy than north-facing slopes, influencing factors such as vegetation growth, snow persistence, and local microclimates.

Just like with slope, calculating aspect is straightforward using the spatial analysis APIs. Create a ContinuousField from your DEM, create a ContinuousFieldFunction from the field, and call aspect to derive the result. The resulting field contains aspect values from 0º up to but not including 360º, where north is represented by 0º, east by 90º, south by 180º and west by 270º. You can learn more about inputs and results for aspect in the API reference documentation.

Again, just like with slope, once evaluated the result is returned as a field that can be displayed, exported, or used as input to further analysis.


_continuousField = await ContinuousField.createFromFiles(
  filePaths: [elevationFilePath],
  band: 0,
);

_elevationFieldFunction = ContinuousFieldFunction.create(_continuousField);

_aspectFunction = _elevationFieldFunction.aspect();

While the continuous aspect values can be visualized directly using a stretch renderer, many applications benefit from reclassifying them into compass-direction bands such as north, northeast, east, and so on. This makes the results easier to interpret at a glance and allows each direction to be represented using a distinct color. Click the plus icon below to see the code you can use to achieve this:

⊕ Click or tap to view code…

DiscreteFieldFunction _createCompassDirectionField(
  ContinuousFieldFunction aspectFunction,
) {
  final compassClassifiedAspect = aspectFunction
      // North wraps around 360 degrees, so it needs two angle ranges.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanOrEqualToValue(0)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(22.5))
            .logicalOr(
              aspectFunction
                  .isGreaterThanValue(337.5)
                  .logicalAnd(aspectFunction.isLessThanValue(360)),
            ),
        value: 0,
      )
      // Northeast.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(22.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(67.5)),
        value: 1,
      )
      // East.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(67.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(112.5)),
        value: 2,
      )
      // Southeast.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(112.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(157.5)),
        value: 3,
      )
      // South.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(157.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(202.5)),
        value: 4,
      )
      // Southwest.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(202.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(247.5)),
        value: 5,
      )
      // West.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(247.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(292.5)),
        value: 6,
      )
      // Northwest.
      .replaceWithValueIf(
        selection: aspectFunction
            .isGreaterThanValue(292.5)
            .logicalAnd(aspectFunction.isLessThanOrEqualToValue(337.5)),
        value: 7,
      );

  // Round any remaining fractional results and convert the classified surface
  // into a discrete field that can be symbolized categorically.
  return compassClassifiedAspect.toDiscreteFieldFunction();
}

Aspect map of Tongariro National Park showing slope direction classified into eight compass directions. Red, orange, yellow, green, cyan, blue, dark blue, and purple colours represent different slope orientations across volcanic craters, ridges, and valleys.
Aspect visualization of Tongariro National Park, New Zealand. Aspect values derived from a DEM have been reclassified into eight compass-direction categories and rendered with a colormap.

Aspect-Slope Rendering

Now that we’ve looked at slope and aspect individually, let’s see what happens when we work with them together.

While rendering of each provides useful context for the terrain in your area of interest, combining them creates a rich terrain visualization that can effectively communicate the character of a landscape. Using a combination of the spatial analysis APIs tool suite alongside raster or field analysis renderers, you can use color hue to represent the direction a slope faces and color intensity to represent how steep that slope is. The result simultaneously reveals terrain orientation and steepness in a single layer, helping users interpret ridges, valleys, and exposed slopes without needing to switch between multiple visualizations.

Here’s how you can do it. After deriving slope and aspect from a DEM, reclassify both slope and aspect fields into a set of discrete categories with map algebra. Categorize slope into broad steepness bands, and aspect into eight compass directions. Then, combine these classifications into a single DiscreteFieldFunction, creating a unique category for every aspect-slope combination. A color map can then be used to assign a distinct color to each aspect-slope combination, with hue representing aspect and color intensity indicating increasing steepness.

The following code and rendering have been inspired by the Aspect-Slope function rendering available in ArcGIS Pro. Click on the plus sign below to view the code for an aspect-slope rendering.

⊕ Click or tap to view code…

// Step 1: Classify slope into broad bands.
// This turns continuous slope values into simple numeric buckets.
final slopeBuckets = _slopeFunction
    .replaceWithValueIf(
      selection: _slopeFunction
          .isGreaterThanOrEqualToValue(0)
          .logicalAnd(_slopeFunction.isLessThanOrEqualToValue(5)),
      value: 10,
    )
    .replaceWithValueIf(
      selection: _slopeFunction
          .isGreaterThanValue(5)
          .logicalAnd(_slopeFunction.isLessThanOrEqualToValue(20)),
      value: 20,
    )
    .replaceWithValueIf(
      selection: _slopeFunction
          .isGreaterThanValue(20)
          .logicalAnd(_slopeFunction.isLessThanOrEqualToValue(40)),
      value: 30,
    )
    .replaceWithValueIf(
      selection: _slopeFunction.isGreaterThanValue(40),
      value: 40,
    );

// Step 2: Classify aspect into compass-direction bands.
// Each direction gets a bucket value so it can be combined with slope.
final aspectBuckets = _aspectFunction
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanOrEqualToValue(-1) // include flat areas
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(22.5)),
      value: 1,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(22.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(67.5)),
      value: 2,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(67.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(112.5)),
      value: 3,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(112.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(157.5)),
      value: 4,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(157.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(202.5)),
      value: 5,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(202.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(247.5)),
      value: 6,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(247.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(292.5)),
      value: 7,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(292.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(337.5)),
      value: 8,
    )
    .replaceWithValueIf(
      selection: _aspectFunction
          .isGreaterThanValue(337.5)
          .logicalAnd(_aspectFunction.isLessThanOrEqualToValue(360)),
      value: 1,
    );

// Step 3: Combine the slope and aspect classes into one discrete field.
// The numeric codes create a combined aspect-slope category per cell.
final slopeAspectMap = (slopeBuckets + aspectBuckets).toDiscreteFieldFunction();

// Step 4: Render the combined categories with a fixed colormap.
// Each combined class code gets a specific color in the legend.
// Values of 10 or under are relatively flat.
final colors = <int, Color>{
  11: const Color.fromARGB(255, 161, 161, 161),
  12: const Color.fromARGB(255, 161, 161, 161),
  13: const Color.fromARGB(255, 161, 161, 161),
  14: const Color.fromARGB(255, 161, 161, 161),
  15: const Color.fromARGB(255, 161, 161, 161),
  16: const Color.fromARGB(255, 161, 161, 161),
  17: const Color.fromARGB(255, 161, 161, 161),
  18: const Color.fromARGB(255, 161, 161, 161),
  21: const Color.fromARGB(255, 152, 181, 129),
  22: const Color.fromARGB(255, 114, 168, 144),
  23: const Color.fromARGB(255, 124, 142, 173),
  24: const Color.fromARGB(255, 140, 117, 160),
  25: const Color.fromARGB(255, 180, 123, 161),
  26: const Color.fromARGB(255, 203, 139, 143),
  27: const Color.fromARGB(255, 197, 165, 138),
  28: const Color.fromARGB(255, 189, 191, 137),
  31: const Color.fromARGB(255, 141, 196, 88),
  32: const Color.fromARGB(255, 61, 171, 113),
  33: const Color.fromARGB(255, 80, 120, 182),
  34: const Color.fromARGB(255, 119, 71, 157),
  35: const Color.fromARGB(255, 192, 77, 156),
  36: const Color.fromARGB(255, 231, 111, 122),
  37: const Color.fromARGB(255, 226, 166, 108),
  38: const Color.fromARGB(255, 214, 219, 94),
  41: const Color.fromARGB(255, 132, 214, 0),
  42: const Color.fromARGB(255, 0, 171, 68),
  43: const Color.fromARGB(255, 0, 104, 192),
  44: const Color.fromARGB(255, 108, 0, 163),
  45: const Color.fromARGB(255, 202, 0, 156),
  46: const Color.fromARGB(255, 255, 85, 104),
  47: const Color.fromARGB(255, 255, 171, 71),
  48: const Color.fromARGB(255, 244, 250, 0),
};

// Step 5: Build the renderer and apply it to the raster layer.
// The colormap makes each class visually distinct in the scene.
final renderer = ColormapRenderer.withColormap(
  Colormap.fromValuesAndColors(colors),
);

 

As a visualization, aspect-slope results work well as a contextual layer, providing intuitive terrain rendering without obscuring operational data. The combined classification can also serve as the foundation for further spatial analysis, making it easy to identify terrain with combinations of slope and aspect, support suitability modelling, or highlight areas that meet specific environmental conditions.

Aspect-slope map of Tongariro National Park showing terrain classified by both slope and aspect. Different colours represent slope direction, while colour intensity indicates increasing steepness.
Aspect-slope visualization of Tongariro National Park, New Zealand. Hue represents aspect (compass direction), while color intensity represents slope (terrain steepness). Grey areas represent relatively flat terrain.

While aspect-slope maps are useful visualizations, the real power of the spatial analysis APIs comes from treating the results as inputs to further analysis.

Convert slope result to percent rise with map algebra

Now you’ve learned the basics of calculating and visualizing slope and aspect from a DEM, let’s take it one step further by applying map algebra to filter for areas that are of interest to the user of your app.

For example, the units for slope are returned in degrees, but what if you want the result in percent rise for your app? Well, you can easily convert degrees to percent rise using the map algebra tools included with the spatial analysis APIs. Percent rise is defined as the ratio of rise over run multiplied by 100 to convert it to a percentage. You can achieve this with the following code:


// convert slope degrees to radians
final slopeRadians = _slopeFunction * (math.pi / 180.0);
// take the tangent and multiply by 100 to get the percentage
final slopePercentRise = slopeRadians.tan() * 100.0;

This creates a new ContinuousFieldFunction representing percent rise values derived from the input DEM. However, you may not want to display results for the entire DEM, and instead focus the analysis on terrain that is relevant to your application.  For example, if you were building an app that allows skiers to explore the variation in slope (in percent rise) across their local resort, a raster representing ski-run footprints could be loaded as a field and used as a mask to restrict the analysis to designated skiing areas rather than the surrounding landscape. This can be achieved using the mask function, ensuring that only terrain within the ski-run boundary is included in the result:


// Keep only raster cells where the ski-run raster has data.
final skiRunSelectionMask = _skiRunFieldFunction.hasData();
// Apply the ski-run mask to the percent rise surface.
final maskedPercentRise = slopePercentRise.mask(skiRunSelectionMask);

Oblique 3D view of a mountain landscape with ski runs highlighted by percent-rise categories. Ski-run corridors are shaded light, medium, and dark blue to represent increasing terrain steepness, while surrounding terrain is shown in shaded grey relief. A legend indicates low (0-20%), moderate (20-40%), and steep (>40%) percent rise.
Percent-rise analysis of ski terrain around Col de la Loze, France. Slope values derived from a DEM were converted to percent rise using map algebra and masked to ski-run footprints, allowing only the skiable terrain to be visualized in a color map. Different shades of blue representing different percent-rise bands highlight variations in slope across the resort. Ski run data from OpenSkiData/OpenSkiMap.org, © OpenStreetMap contributors (ODbL) and Skimap.org.

From there, you can refine the analysis further using additional suitability criteria. For example, a BooleanFieldFunction could identify areas where percent rise falls within a user-defined skiing range (flat areas for beginners, steeper areas for experienced skiers). A DiscreteFieldFunction could reclassify terrain into categories such as beginner, intermediate, and advanced runs. Additional raster layers, such as snow depth or land cover, could also be incorporated to support specialized workflows, such as cross-country ski planning. For an example of this type of filtering and reclassification workflow in action, see the Analyze Terrain Suitability from Slope and Aspect sample.

Percent Rise Caveats

Percent rise can provide a more intuitive measure of terrain steepness than slope in degrees, but the range and distribution of values differ significantly between the two. While slope angles are limited to a range of 0° to 90°, percent rise increases non-linearly and can become very large on steep terrain. As a result, a small number of steep locations may dominate the value range of a raster. Rather than using a single visualization strategy everywhere, consider tailoring your RasterRenderer to the terrain and your audience. You can adjust the stretch range of the minimum and maximum values in MinMaxStretchParameters or instead use PercentClipStretchParameters to help highlight the differences that matter most for your analysis.

Summary

In this blog post, we’ve covered what slope and aspect are, why you might want to calculate and visualize them either individually or together, and how you would do that. We’ve also explored how map algebra can be used to transform, classify and combine terrain analysis results into more advanced workflows. If you want to learn more about how to use slope, aspect, or the spatial analysis APIs more generally, check out the spatial analysis APIs documentation and samples most related to this blog, Apply map algebra and Analyze terrain suitability from slope and aspect.

If you’d like to learn more about color ramp and renderer options, check out the New Color Ramps and Color Maps in Native Maps SDK blog post. You can also learn how to use a renderer in a field analysis in the Show interactive viewshed with analysis overlay sample.

We would love to hear how you are using the spatial analysis APIs on the Esri Community ArcGIS Maps SDKs for Native Apps forums.

Share this article