Map algebra provides data-driven analyses that derive new outputs by evaluating one or more expressions. An expression is applied to an input field of spatial information and returns a new field as a result. Expressions can be chained together to build more complex analyses, each one building on the previous results. Because map algebra expressions are not evaluated until the final result is requested, you can compose complex expressions without creating intermediate outputs.

Map algebra operates on the underlying data (not only what is currently visible in the view), so results are consistent and can be reused in follow-on workflows. You can visualize results dynamically, export them for persistence, and chain multiple operations to build more advanced analysis models.

NDVI raster highlighting vegetation intensity across agricultural land.

Use map algebra when you need to derive a new surface or category map from one or more input datasets. Common examples include:

  • creating suitability or risk surfaces from weighted criteria
  • classifying continuous values into discrete ranges
  • masking analysis to include only areas that meet a condition
  • combining intermediate analysis outputs into a final model

Map algebra expressions

In ArcGIS Maps SDK for Qt, you create map algebra expressions with ContinuousFieldFunction, DiscreteFieldFunction, and BooleanFieldFunction objects, then evaluate the final function to produce a new field.

The following code creates an NDVI (Normalized Difference Vegetation Index) field from the red and near-infrared bands of a multispectral image. The expression calculates the NDVI value for each pixel.

// Create a continuous field from the red band of the multispectral image.
QFuture<ContinuousField*> redFieldFuture = ContinuousField::createFromFilesAsync(
QStringList{multispectralRasterPath}, 0, this);
redFieldFuture.then(this, [this, multispectralRasterPath](ContinuousField* redField)
{
// Create the near-infrared field after the red field is available.
QFuture<ContinuousField*> nearInfraredFieldFuture = ContinuousField::createFromFilesAsync(
QStringList{multispectralRasterPath}, 1, this);
nearInfraredFieldFuture.then(this, [this, redField](ContinuousField* nearInfraredField)
{
// Create continuous field functions for the red and near-infrared bands.
ContinuousFieldFunction* redFunction = ContinuousFieldFunction::create(redField, this);
ContinuousFieldFunction* nearInfraredFunction = ContinuousFieldFunction::create(nearInfraredField, this);
// Compute the NDVI using the formula: (NIR - Red) / (NIR + Red).
ContinuousFieldFunction* ndviNumerator = nearInfraredFunction->subtract(redFunction);
ContinuousFieldFunction* ndviDenominator = nearInfraredFunction->add(redFunction);
ContinuousFieldFunction* ndviFunction = ndviNumerator->divide(ndviDenominator);
// Evaluate the expression and process the NDVI field only when evaluation has completed.
QFuture<ContinuousField*> ndviFieldFuture = ndviFunction->evaluateAsync(this);
ndviFieldFuture.then(this, [](ContinuousField* ndviField)
{
// TODO: Add further analysis to make use of the ndvifield.
Q_UNUSED(ndviField);
}).onFailed(this, [](const ErrorException& exception)
{
qWarning() << "Unable to evaluate the NDVI field:" << exception.error();
});
}).onFailed(this, [](const ErrorException& exception)
{
qWarning() << "Unable to create the near-infrared field:" << exception.error();
});
}).onFailed(this, [](const ErrorException& exception)
{
qWarning() << "Unable to create the red field:" << exception.error();
});

When designing map algebra expressions, it may help to break the logic into intermediate functions rather than composing everything in one statement. This can make complex models easier to read, test, and update.

A common pattern is:

  • normalize or scale input values so they can be compared consistently
  • mask out cells that should be excluded from analysis
  • derive intermediate boolean or discrete categories from thresholds
  • combine intermediate outputs into the final classified or continuous result

Map algebra operations

The tables below list many of the available operations for each type of field function. Each operation is implemented as a method on the field function class. See the ContinuousFieldFunction, DiscreteFieldFunction, and BooleanFieldFunction for details.

MethodComposes a function that
ContinuousFieldFunction::add()
DiscreteFieldFunction::Add()
Adds, point-wise, another compatible field function or a constant value.
ContinuousFieldFunction::subtract()
DiscreteFieldFunction::subtract()
Subtracts, point-wise, another compatible field function or a constant value.
ContinuousFieldFunction::multiply()
DiscreteFieldFunction::multiply()
Multiplies, point-wise, by another compatible field function or a constant value.
ContinuousFieldFunction::divide()
DiscreteFieldFunction::divide()
Divides, point-wise, by another compatible field function or a constant value.
ContinuousFieldFunction::remainder()
DiscreteFieldFunction::remainder()
Computes the remainder, point-wise, when dividing by another compatible field function or a constant value.
ContinuousFieldFunction::abs()
DiscreteFieldFunction::abs()
Returns the absolute value of the function’s field result across its extent.
ContinuousFieldFunction::pow()Raises each value to a specified exponent, point-wise.
ContinuousFieldFunction::exp()
ContinuousFieldFunction::exp10()
Computes the natural exponential (base $e$) or base-10 exponential of each value, point-wise.
ContinuousFieldFunction::log()
ContinuousFieldFunction::log10()
Computes the natural logarithm or base-10 logarithm of each value, point-wise.
ContinuousFieldFunction::sqrt()Computes the square root of each value, point-wise.
ContinuousFieldFunction::reciprocal()Computes the reciprocal ($1/x$) of each value, point-wise.
ContinuousFieldFunction::ceil()
ContinuousFieldFunction::floor()
ContinuousFieldFunction::round()
Rounds continuous values up, down, or to the nearest integer across the function’s extent.

Implement a map algebra workflow

You can implement map algebra using a pattern like the following:

  1. Create one or more input fields from input spatial data.

    You can load raster data into ContinuousField, DiscreteField, or BooleanField objects. Use API like ContinuousField::createFromFilesAsync() to create fields from source raster datasets.

  2. Create field functions from the input fields and prepare data for analysis.

    Create ContinuousFieldFunction, DiscreteFieldFunction, or BooleanFieldFunction objects from the input fields. Prepare the data for analysis by masking out areas that should be excluded, normalizing values, or applying other transformations to the input fields.

  3. Compose map algebra expressions.

    Build expressions from arithmetic, trigonometric, logical, relational, and conditional operations to transform values and produce derived outputs. You can also convert between field types, for example with ContinuousFieldFunction::toDiscreteFieldFunction() and DiscreteFieldFunction::toContinuousFieldFunction().

  4. Evaluate the final function.

    Evaluation of an expression is deferred until it is explicitly requested (lazy evaluation). Call ContinuousFieldFunction::evaluateAsync() (or the equivalent evaluate method for the function type you are working with) on the final expression to create the output field.

  5. Visualize and optionally persist the result.

    Display continuous results with a StretchRenderer and categorical results with a ColormapRenderer. You can add analyses to an AnalysisOverlay to display results. An AnalysisOverlay allows you to group related analyses and control visibility for all members of the collection. A GeoView can contain many analysis overlays. You can also export output fields for reuse by writing GeoTIFF files, for example with ContinuousField::exportToFilesAsync().

Example

Categorize an elevation raster with map algebra

The following example follows the pattern described above to apply map algebra to an elevation raster and categorize values into classes. The workflow creates a field function expression, evaluates it to produce a result field, and then renders the categories in a map view.

  1. Create a source field from raster data.

    Create a ContinuousField from an elevation raster. Create a Map to display the original raster so users can compare inputs and outputs.

    // Create a map with a dark hillshade basemap and set its initial viewpoint.
    Map* elevationMap = new Map(BasemapStyle::ArcGISHillshadeDark, this);
    const Point elevationCenter(-5.234218, 55.584612, SpatialReference::wgs84());
    const Viewpoint elevationViewpoint(elevationCenter, 300000);
    elevationMap->setInitialViewpoint(elevationViewpoint);
    // Add the elevation raster layer to the map and display the map in the view.
    Raster* elevationRaster = new Raster(elevationRasterPath, this);
    RasterLayer* elevationRasterLayer = new RasterLayer(elevationRaster, this);
    LayerListModel* layerListModel = elevationMap->operationalLayers();
    layerListModel->append(elevationRasterLayer);
    mapView->setMap(elevationMap);
    // Create the elevation field and continue the classification only when the field is ready.
    QFuture<ContinuousField*> elevationFieldFuture = ContinuousField::createFromFilesAsync(
    QStringList{elevationRasterPath}, 0, this);
    elevationFieldFuture.then(this, [elevationMap](ContinuousField* elevationField)
    {
    // TODO: Add further analysis to make use of the elevationMap and elevationField.
    Q_UNUSED(elevationMap);
    Q_UNUSED(elevationField);
    }).onFailed(this, [](const ErrorException& exception)
    {
    qWarning() << "Unable to create the elevation field:" << exception.error();
    });
  2. Create a field function from the raster field and mask out values at or below sea level.

    Create a ContinuousFieldFunction from the ContinuousField of elevation values. Mask out values at or below sea level so that only land areas are categorized in the final output.

    // Create a continuous field function from the elevation field.
    ContinuousFieldFunction* elevationFunction = ContinuousFieldFunction::create(elevationField, this);
    // Mask out values below sea level to categorize only land.
    BooleanFieldFunction* isAboveSeaLevel = elevationFunction->isGreaterThanOrEqualTo(0.0f);
    ContinuousFieldFunction* landElevationFunction = elevationFunction->mask(isAboveSeaLevel);
  3. Build expressions that create categories of elevation values.

    Compose map algebra expressions that use mathematical and relational operations to create categorized output fields based on elevation.

    // Round elevation values down to the lower 10-meter interval, then convert to a discrete field function.
    ContinuousFieldFunction* elevationIntervals = landElevationFunction->divide(10.0f);
    ContinuousFieldFunction* flooredElevationIntervals = elevationIntervals->floor();
    ContinuousFieldFunction* tenMeterIntervals = flooredElevationIntervals->multiply(10.0f);
    DiscreteFieldFunction* tenMeterBins = tenMeterIntervals->toDiscreteFieldFunction();
    // Create boolean fields for raised shoreline, ice-covered, and ice-free high-ground categories.
    BooleanFieldFunction* isRaisedShoreline = tenMeterBins->isGreaterThanOrEqualTo(0);
    BooleanFieldFunction* isBelowTenMeters = tenMeterBins->isLessThan(10);
    BooleanFieldFunction* raisedShoreline = isRaisedShoreline->logicalAnd(isBelowTenMeters);
    // Create boolean fields for ice-covered and ice-free high-ground categories.
    BooleanFieldFunction* isIceCoveredAboveTenMeters = tenMeterBins->isGreaterThanOrEqualTo(10);
    BooleanFieldFunction* isBelowSixHundredMeters = tenMeterBins->isLessThan(600);
    BooleanFieldFunction* iceCovered = isIceCoveredAboveTenMeters->logicalAnd(isBelowSixHundredMeters);
    BooleanFieldFunction* iceFreeHighGround = tenMeterBins->isGreaterThanOrEqualTo(600);
    // Assign geomorphic categories: raised shoreline is 1, ice covered is 2, and ice-free high ground is 3.
    DiscreteFieldFunction* geomorphicCategoryFunction = tenMeterBins->replaceIf(raisedShoreline, 1);
    geomorphicCategoryFunction = geomorphicCategoryFunction->replaceIf(iceCovered, 2);
    geomorphicCategoryFunction = geomorphicCategoryFunction->replaceIf(iceFreeHighGround, 3);
  4. Evaluate the final expression to produce a discrete field of geomorphic categories.

    None of the intermediate functions are evaluated until the final expression is evaluated. Call ContinuousFieldFunction::evaluateAsync() to produce a DiscreteField of geomorphic categories.

    // Evaluate the final expression to create a discrete field of geomorphic categories.
    QFuture<DiscreteField*> geomorphicCategoryFieldFuture = geomorphicCategoryFunction->evaluateAsync(this);
    geomorphicCategoryFieldFuture.then(this, [elevationMap](DiscreteField* geomorphicCategoryField)
    {
    // TODO: Add further analysis to make use of the elevationMap and geomorphicCategoryField.
    Q_UNUSED(elevationMap);
    Q_UNUSED(geomorphicCategoryField);
    }).onFailed(this, [](const ErrorException& exception)
    {
    qWarning() << "Unable to evaluate the geomorphic categories:" << exception.error();
    });
  5. Render the output categories.

    Export the evaluated field if needed, create a RasterLayer from the result, and apply a ColormapRenderer so each category is symbolized with a distinct color.

    // Create a unique temporary folder for the exported geomorphic category raster.
    const QString outputDirectory = QDir::tempPath() + "/geomorphic_map_algebra_"
    + QUuid::createUuid().toString(QUuid::WithoutBraces);
    if (!QDir().mkpath(outputDirectory))
    {
    qWarning() << "Unable to create the map algebra output directory:" << outputDirectory;
    return;
    }
    // Export the evaluated field to raster files using the requested filename prefix.
    QFuture<QStringList> exportedFilesFuture = geomorphicCategoryField->exportToFilesAsync(
    outputDirectory, "geomorphicCategorization");
    exportedFilesFuture.then(this, [this, elevationMap](const QStringList& exportedFiles)
    {
    if (exportedFiles.isEmpty())
    {
    qWarning() << "Map algebra did not export any geomorphic category raster files.";
    return;
    }
    // Create a raster layer from the exported geomorphic categorization raster.
    Raster* categorizedRaster = new Raster(exportedFiles.first(), this);
    RasterLayer* geomorphicRasterLayer = new RasterLayer(categorizedRaster, this);
    // Create a color for each category, where each color index corresponds to its pixel value.
    const QList<QColor> categoryColors{
    QColor(0, 0, 0, 0),
    QColor(25, 118, 210, 255),
    QColor(128, 203, 196, 255),
    QColor(121, 85, 72, 255)
    };
    ColormapRenderer* categoryRenderer = new ColormapRenderer(categoryColors, this);
    geomorphicRasterLayer->setRenderer(categoryRenderer);
    // Set the layer opacity to allow the basemap hillshade to show through.
    geomorphicRasterLayer->setOpacity(0.5);
    // Add the geomorphic categorization raster layer to the map.
    elevationMap->operationalLayers()->append(geomorphicRasterLayer);
    }).onFailed(this, [](const ErrorException& exception)
    {
    qWarning() << "Unable to export the geomorphic categories:" << exception.error();
    });

This pattern can be adapted to many use cases beyond elevation classification, such as vegetation index thresholding, terrain suitability modeling, and multi-criteria risk mapping.