---
title: "Landform classification with the Topographic Position Index"
author: "Gianmarco Alberti (University of Malta)"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Landform classification with the Topographic Position Index}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7.5,
  fig.height = 5.5,
  out.width = "100%",
  dpi = 72
)
```

## What the package does

The Topographic Position Index (TPI) compares the elevation of each cell of a
digital terrain model (DTM) with the mean elevation of the cells around it
(Guisan et al. 1999; Weiss 2001). Positive values mean that the cell is higher
than its surroundings (a ridge or a hilltop), negative values that it is lower
(a valley bottom), and values near zero that it is either on flat ground or
half-way up a slope; slope is used to tell these two apart.

`landform` implements the two classifications proposed by Weiss (2001) and
made popular by Jenness's TPI extension for ArcView (Jenness 2006):

* the **Slope Position Classification**, six classes from one TPI scale;
* the **Landform Classification**, ten classes from a small and a large TPI
  scale.

The package has three functions: `landform()`, which performs the
classifications; `tpi()`, which returns the TPI surface itself; and
`malta_dtm()`, which loads the sample DTM used below.

## The sample data

The DTM covers the island of Malta at 40 m resolution, with the sea coded as
`NA`.

```{r data}
library(landform)
library(terra)

dem <- malta_dtm()
dem
plot(dem, col = hcl.colors(50, "Terrain 2"), main = "Malta DTM (m)")
```

## The Topographic Position Index

The TPI depends entirely on the size of the neighbourhood: the same point can
be a ridge with respect to its immediate surroundings and part of a valley
floor at a broader scale. The neighbourhood is set by `scale`, interpreted
either as the width of a square window in cells (`units = "cells"`, the
default) or as a radius in map units (`units = "map"`; metres for
longitude/latitude rasters). The window can be square, circular or annular
(`shape`).

```{r tpi, fig.height = 4}
t_small <- tpi(dem, scale = 120, units = "map", shape = "circle", std = "none")
t_large <- tpi(dem, scale = 1000, units = "map", shape = "circle", std = "none")
rng <- c(-30, 30)
plot(c(t_small, t_large), range = rng,
     col = hcl.colors(100, "Blue-Red 3"),
     main = c("TPI (m), 120 m radius", "TPI (m), 1000 m radius"))
```

### Standardisation

Weiss's thresholds are expressed in standard deviations, so the TPI has to be
standardised first. Two approaches are available, and they can lead to
noticeably different maps.

* **Global** (`std = "global"`, the default). The TPI is turned into z-scores
  using the mean and standard deviation of the TPI over the whole raster.
  This is the method of Weiss (2001) and Jenness (2006): a value of 1 means
  "one standard deviation above the typical TPI of this landscape".
* **Local** (`std = "local"`). The TPI of each cell is divided by the
  standard deviation of elevation in its own neighbourhood, which is the
  "deviation from mean elevation" (DEV) of Wilson and Gallant (2000). De Reu
  et al. (2013) found it better suited to landscapes in which relief varies
  greatly from place to place. Its drawback is that in nearly flat areas the
  local standard deviation is tiny, so that small irregularities of the DTM
  (or plain noise) become large standardised values. This was the only method
  available in `landform` up to version 0.2.

The difference is easiest to see on the plateaus and plains of eastern Malta:

```{r std, fig.height = 4}
g <- landform(dem, scale = 9, plot = FALSE)
l <- landform(dem, scale = 9, std = "local", plot = FALSE)
par(mfrow = c(1, 2))
plot(g$classes, main = "Global standardisation", legend = FALSE,
     mar = c(1, 1, 2, 1), axes = FALSE)
plot(l$classes, main = "Local standardisation", legend = FALSE,
     mar = c(1, 1, 2, 1), axes = FALSE)
par(mfrow = c(1, 1))
```

```{r std-table}
data.frame(class = g$stats$class,
           global = round(g$stats$percent, 1),
           local = round(l$stats$percent, 1))
```

With local standardisation the flat areas are broken up into a mosaic of
valleys and ridges a few cells across. Which method is preferable depends on
the landscape and on the question; when in doubt, it is worth running both.

## Slope Position Classification

```{r sp}
sp <- landform(dem, scale = 9)
sp
```

The thresholds (Weiss 2001), with z the standardised TPI and s the slope
in degrees, are:

| Class          | Rule                                      |
|:---------------|:------------------------------------------|
| 1 Valley       | z ≤ -1                                |
| 2 Lower Slope  | -1 < z ≤ -0.5                         |
| 3 Flat Slope   | -0.5 < z < 0.5 and s ≤ 5            |
| 4 Middle Slope | -0.5 < z < 0.5 and s > 5              |
| 5 Upper Slope  | 0.5 ≤ z ≤ 1                         |
| 6 Ridge        | z > 1                                   |

All three thresholds can be changed with `tpi.thr` and `slope.thr`.

The classes can be drawn semi-transparently over a relief shading, which
helps relate them to the terrain. A slopeshade (`shade = "slope"`) shows the
slope in grey levels, from white for flat ground to black for the steepest
slopes; unlike a hillshade (`shade = "hill"`), it does not darken features
just because they face away from the light. The opacity of the classes is set
by `alpha`:

```{r sp-slopeshade}
plot(sp, shade = "slope")
```

Since the classification depends on the scale, a broader neighbourhood picks
out broader features:

```{r sp-scale}
sp_broad <- landform(dem, scale = 600, units = "map", shape = "circle")
```

## Landform Classification

The Landform Classification combines a small-neighbourhood TPI (`sn`) and a
large-neighbourhood TPI (`ln`), each split into low (≤ -1), middle and
high (≥ 1) values; plains and open slopes are separated by slope.

```{r lf}
lf <- landform(dem, class.type = "landform.classification",
               sn = 200, ln = 1000, units = "map", shape = "circle",
               shade = "slope")
```

| Class | SN | LN |
|:------|:---|:---|
| 1 Canyons, Deeply Incised Streams         | ≤ -1 | ≤ -1 |
| 2 Midslope Drainages, Shallow Valleys     | ≤ -1 | middle |
| 3 Upland Drainages, Headwaters            | ≤ -1 | ≥ 1 |
| 4 U-shaped Valleys                        | middle | ≤ -1 |
| 5 Plains (slope ≤ 5)                  | middle | middle |
| 6 Open Slopes (slope > 5)               | middle | middle |
| 7 Upper Slopes, Mesas                     | middle | ≥ 1 |
| 8 Local Ridges, Hills in Valleys          | ≥ 1 | ≤ -1 |
| 9 Midslope Ridges, Small Hills in Plains  | ≥ 1 | middle |
| 10 Mountain Tops, High Ridges             | ≥ 1 | ≥ 1 |

The two standardised TPI surfaces on which the classification rests can be
plotted directly, here over a slopeshade (the colour scale is capped at ±3
standard deviations):

```{r lf-tpi, fig.height = 4}
plot(lf, what = "tpi", shade = "slope")
```

## What `landform()` returns

`landform()` returns an object of class `"landform"`, a list holding:

* `classes`: a categorical `SpatRaster` of the classes, with class names and
  colours attached;
* `tpi`: the raw and standardised TPI layers;
* `slope`: the slope (degrees);
* `stats`: the descriptive statistics of each class;
* `dem` and `params`: the input DTM and the settings used.

```{r stats}
summary(lf)
plot(lf, what = "area")
```

Areas are computed from the actual size of each cell
(`terra::cellSize()`), so they are correct also for rasters in
longitude/latitude. Cells for which slope cannot be computed (the outer rim of
the raster and the cells along the coast) are left unclassified.

Individual classes can be extracted as separate presence/absence layers with
`class_layers()`, for instance to compute distances from ridges or to use the
classes as covariates:

```{r segregate}
layers <- class_layers(lf)
names(layers)
plot(layers[["Mountain Tops, High Ridges"]], legend = FALSE,
     col = c("grey90", "#6A3D9A"), main = "Mountain Tops, High Ridges")
```

Since `classes` is an ordinary `SpatRaster`, it can be saved with
`terra::writeRaster()` and opened in any GIS.

## Reproducing results from landform 0.2

Version 0.3 changed the default standardisation from local to global and,
by default, excludes the focal cell from the neighbourhood mean. To reproduce
results obtained with earlier versions, use:

```{r legacy, eval = FALSE}
landform(dem, scale = 9, std = "local", centre = TRUE)
```

The only remaining differences are that cells on the outer rim of the raster
are now always left unclassified, and that cells whose standardised TPI was
exactly 0.5 (previously left without a class) are now Upper Slope.

## References

De Reu, J., Bourgeois, J., Bats, M., Zwertvaegher, A., Gelorini, V., De Smedt,
P., Chu, W., Antrop, M., De Maeyer, P., Finke, P., Van Meirvenne, M., Verniers,
J. and Crombé, P. (2013). Application of the topographic position index to
heterogeneous landscapes. *Geomorphology*, 186, 39-49.

Guisan, A., Weiss, S. B. and Weiss, A. D. (1999). GLM versus CCA spatial
modeling of plant species distribution. *Plant Ecology*, 143, 107-122.

Jenness, J. (2006). *Topographic Position Index (tpi_jen.avx) extension for
ArcView 3.x, v. 1.2*. Jenness Enterprises.

Weiss, A. (2001). *Topographic Position and Landforms Analysis*. Poster
presentation, ESRI User Conference, San Diego, CA.

Wilson, J. P. and Gallant, J. C. (eds) (2000). *Terrain Analysis: Principles
and Applications*. Wiley, New York.
