Elevation Data in R

Posted by David D. Hofmann on Tuesday, March 23, 2021

Introduction

Elevation is often an important predictor variable in ecological studies. In this blog post, I will show you different ways in which you can easily download and import elevation data directly into R. I found this ability to be very useful, especially when considering how tedious it can be to obtain and clean similar data from online portals such as from USGS. A workflow that does not require you to manully download data vastly increases the reproducibility of your work and therefore the value of your analysis. Besides showing you how to download elevation data, I’ll also outline how digital elevation models (DEMs) can be used to create beautiful background maps for spatial plots and maps. Finally, we’ll have some fun with rayshader, a brilliant r-package to plot stuff in 3D. So let’s get started and download some elevation data!

Downloading DEM Data

First of all, let’s decide on an area of interest. For the purpose of this tutorial, we will focus on the country of Switzerland. We can quickly get a polygon of the country’s borders using the getData() function from the raster package. I will also use the opportunity to load some additional packages that we’ll use later.

# Load required packages
library(raster)     # To manipulate raster data
library(elevatr)    # To download elevation data
library(viridis)    # To get nice color palettes
library(rayshader)  # To plot maps in 3D

# Download polygon for country (only on the national borders -> level = 0)
swiss <- getData("GADM", country = "CHE", level = 0)
## Warning in getData("GADM", country = "CHE", level = 0): getData will be removed in a future version of raster
## . Please use the geodata package instead
plot(swiss)

Now that we decided on an area of interest, we have two options to download a DEM:

  • Option 1: Use the getData() function from the raster package
  • Option 2: Use the get_elev_raster() function from the elevatr package

If we use the raster package, we can either download elevation data around a desired coordinate or for a desired country. Since we already decided for a country, we can go for the latter and download data for the country of Switzerland (country code CHE).

# Download DEM data for a desired country
elev <- getData("alt", country = "CHE")

# Visualize it
plot(elev, col = viridis(50), box = F, axes = F, horizontal = T)
plot(swiss, add = T, border = "red", lwd = 2)

The download does not take long at all! While the downloaded raster has a rather low resolution of 925 by 925 meters, this should suffice for most purposes (you canactually calculate the resolution in meters using the approximation that 1 degree = 111’000 meters).

# Calculate resolution of raster (roughly in meters)
res(elev) * 111000
## [1] 925 925

Due to the limited options in the raster::getData() function, I personally prefer to use the function get_elev_raster() from the elevatr package. This function gives a bit more flexibility and enables us to change the resolution and extent of the downloaded data. As the name suggests, the function retrieves an elevation-raster for a desired area. In contrast to the raster::getData() function, we can specify the area of interest using a polygon. In this case, we can use the previously downloaded shapefile for Switzerland. Furthermore, we can provide a zoom factor z that determines the resolution of the downloaded data. The higher the zoom-factor, the higher the resolution. Of course the download and stitching will take longer if we decide for a higher resolution. Because the package will download data beyond the exact shapefile, we’ll need crop the downloaded DEM to the exact area of Switzerland.

# Download DEM data and crop downloaded data
elev <- get_elev_raster(swiss, z = 7)
elev <- crop(elev, swiss)

# Visualize it
plot(elev, col = viridis(50), box = F, axes = F, horizontal = T)
plot(swiss, add = T, border = "red", lwd = 2)

# Check resolution (could be increased even further)
res(elev) * 111000
## [1] 521.7562 521.7562

Once we downloaded the DEM in raster format, it’s straight forward to conduct further analyses. For instance, let’s assume that we collected species occurence data and that we now want to know the exact elevation at the sampled locations. For this, we first randomly place some in Switzerland (the points represent our hypothetical species occurences). Then, we can use the raster::extract() function to determine the elevation at those locations. Finally, we’ll visualize the locations and their respective elevation on a nice map.

# Randomly distribute locations (species occurences)
locs <- spsample(swiss, n = 20, type = "random")

# Identify elevation at each location
locs$Elevation <- extract(elev, locs)
locs$Elevation <- round(locs$Elevation)

# Visualize points and add indicate their height using labels
plot(elev, col = viridis(50), box = F, axes = F, horizontal = T)
plot(swiss, add = T, border = "red", lwd = 2)
plot(locs, add = T, col = "orange", pch = 20, cex = 0.5)
text(locs, label = "Elevation", col = "orange", cex = 0.5, pos = 3, offset = 0.2)

Deriving Terrain Features

Besides using the raw elevation data, we can also further process the DEM and calculate derived metrics such as the slope (steepness of the terrain) and the aspect (direction that a slope faces). From these two, we can finally compute the hillshade, which we will use to create some nice relief plots.

# Calculate "aspect" and "slope"
aspect <- terrain(elev, "aspect")
slope <- terrain(elev, "slope")

# And now we can calculate the hillshade
hillshade <- hillShade(slope = slope, aspect = aspect)

# Visualize it
plot(hillshade, col = gray(60:100/100), box = F, axes = F, legend = F)
plot(swiss, add = T, border = "red", lwd = 2)

If you want to increase the “strength” of the hillshade, you can introduce a modifier that artificially increases the variability in elevation as follows.

# Calculate "aspect" and "slope" using a modified DEM
modifier <- 2
aspect2 <- terrain(elev ^ modifier, "aspect")
slope2 <- terrain(elev ^ modifier, "slope")

# And now we can calculate the hillshade
hillshade2 <- hillShade(slope = slope2, aspect = aspect2)

# Visualize it
plot(hillshade2, col = gray(60:100/100), box = F, axes = F, legend = F)
plot(swiss, add = T, border = "red", lwd = 2)

You may also be interested in the ruggedness of the terrain. For this, there’s the terrain ruggedness index (TRI). The TRI is defined as the square root of the average of the squared differences in elevation of a focal cell and its eight neighbors (I know, this sounds very complicated but it’s actually a very simple way to quantify how substantially the elevation varies within a few cells). We can calculate the TRI by hand using the focal function. The focal function basically creates a moving window and applies the function for terrain ruggedness to this window. For our purposes, a 3x3 cell window will suffice.

# Define weights (size of moving window)
weights <- matrix(1, nrow = 3, ncol = 3)

# Function to calculate the terrain ruggedness index
ruggedness <- function(x){sum(abs(x[-5] - x[5])) / 8}

# Apply the function to the moving window
tri <- focal(elev, w = weights , fun = ruggedness, pad = TRUE, padValue = NA)

# Visualize
plot(tri, col = viridis(50), box = F, axes = F, horizontal = T)
plot(swiss, add = T, border = "red", lwd = 2)

These are just a few examples that should get you started with elevation data. We’ll now turn to more fun things and try to get some nice plots using the rayshader package.

Having some Fun with Rayshader

Rayshader is a nice little package developed by Tyler Morgan-Wall. The package is mainly intended to turn your elevation maps into beautiful 3D plots, yet it also has some 3d plotting capabilities for ggplot! There is a fantastic walk-through of the package here. Here, I will use the package to add a bit of depth to our elevation map of Switzerland. As a first step, we need to convert the elevation raster into a matrix. We can do this using the function raster_to_matrix(). We can then already generate a first plot.

# Convert the DEM-raster to a matrix
elmat <- raster_to_matrix(elev)

# Prepare a simple plot
elmat %>%
  sphere_shade(., texture = "imhof1") %>%
  plot_map()

At this stage, the map looks somewhat boring. We can make it a bit more interesting using two modifications. First, we can apply a mask and pretend that anything outside of Switzerland has an elevation of 0. This will make the country literally pop.

# Set elevation outside Switzerland to NA
elev <- mask(elev, swiss)

# Convert the raster to a matrix
elmat <- raster_to_matrix(elev)

# Visualize
elmat %>%
  sphere_shade(., texture = "imhof1") %>%
  plot_map()

This already looks much better. As a second modification, we can add waterbodies to our plot. This should add a bit of colour and really help to sell the map. While the rayshader package comes with a function called detect_water(elev) to detect waterbodies, I found the results to be unsatisfactory. Thus, I’ve prepared a shapefile containing all major water-bodies of Switzerland. You can download the shapefile from github and load it into R as follows.

# Load watermaps
url <- "https://github.com/DavidDHofmann/MajorWaters/archive/master.zip"

# Specify filename and download directory (we'll use a temporary directory)
dir <- tempdir()
file <- file.path(dir, "water.zip")

# Download the data
download.file(url = url, destfile = file)

# Extract the zip file
unzip(file, exdir = dir)

# Filepath to the extracted file
file <- file.path(dir, "MajorWaters-main", "geo_che_water.shp")

# Load the data and reproject it
water <- shapefile(file)
water <- spTransform(water, crs(hillshade))

# Visualize them on top of the hillshade map
plot(mask(hillshade, swiss), col = gray(60:100/100), box = F, axes = F, legend = F)
plot(water, add = T, col = "cornflowerblue", border = NA)

Rayshader requires that waterbodies are represented in raster format, hence we’ll have to rasterize them. In addition (and don’t ask me why) we need to “flip” the resulting raster (mirror it on the x axis) so that it aligns with the elevation raster.

# Rasterize waterbodies
water <- rasterize(water, elev, field = 1, background = 0)

# For whatever reason we need to flip the water raster
water <- flip(water, "x")

# Convert water to a matrix
wamat <- raster_to_matrix(water)

# Visualize
elmat %>%
  sphere_shade(., texture = "imhof1") %>%
  add_water(., wamat, color = "lightblue") %>%
  plot_map()

Awesome! This already looks much cooler. But it’s not really in 3d yet. So let’s give it a final twist by putting everything into proper 3d. We’ll also add some shadows, make the water slightly transparent, and add bit of depth of field.

# Prepare a nice map in 3D
elmat %>%
  sphere_shade(., texture = "imhof1") %>%
  add_water(., wamat, color = "lightblue") %>%
  add_shadow(., ray_shade(elmat), 0.7) %>%
  add_shadow(., ambient_shade(elmat), 0.7) %>%
  plot_3d(.
    , elmat
    , zscale         = 200
    , fov            = 0
    , theta          = 0
    , phi            = 30
    , zoom           = 0.40
    , windowsize     = c(2000, 1000)
    , wateralpha     = 0.5
    , waterlinecolor = "white"
    , waterlinealpha = 0.5
  )
Sys.sleep(0.2)
render_depth(focus = 0.6, focallength = 80, clear = TRUE)

Bathymetry Data

In some cases, you may be interested in bathymetric data, i.e. the topography of seas. For this, there’s a brilliant r-package called marmap which enables you to access such data from the NOAA database. For instance, we can easily download bathymetry data for the Mariana Trench. Note that the downloaded file will not be in raster format. Still, we can easily coerce it using marmap::as.raster(). Afterwards, you can do all the fany things we’ve been doing so far with it.

# Load the marmap package
library(marmap)

# Download bathymetry data for the Mariana Trench
bathy <- getNOAA.bathy(
    lon1       = 140
  , lon2       = 155
  , lat1       = -13
  , lat2       = 0
  , resolution = 4
)

# Coerce to a raster
bathy <- marmap::as.raster(bathy)

# Visualize it
plot(bathy, col = rev(viridis(50)), box = F, axes = F, horizontal = T)

I won’t go into further details but the package offers many more useful functions and has great plotting capabilities that allow you to generate publication ready plots. You can find out about all the nifty little tricks and percs of this package in its beautiful vignette.

Summary

In this blog post, we learned about different R-packages, such as raster, elevatr, and marmapr that allo us to directly import elevation data into R. In addition, we explored various ways in which such data can be displayed. Ultimately, we took a glimpse at the rayshader package to visualize elevation data in 3d.

Further Reading

Session Information

## R version 4.1.2 (2021-11-01)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Linux Mint 21.3
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.10.0
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.10.0
## 
## locale:
##  [1] LC_CTYPE=de_CH.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=de_CH.UTF-8        LC_COLLATE=de_CH.UTF-8    
##  [5] LC_MONETARY=de_CH.UTF-8    LC_MESSAGES=de_CH.UTF-8   
##  [7] LC_PAPER=de_CH.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=de_CH.UTF-8 LC_IDENTIFICATION=C       
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] marmap_1.0.10     rayshader_0.35.5  viridis_0.6.3     viridisLite_0.4.2
## [5] elevatr_0.4.4     raster_3.6-20     sp_1.6-1         
## 
## loaded via a namespace (and not attached):
##  [1] httr_1.4.6          sass_0.4.9          bit64_4.0.5        
##  [4] jsonlite_1.8.8      foreach_1.5.2       bslib_0.6.2        
##  [7] highr_0.10          blob_1.2.4          yaml_2.3.8         
## [10] progress_1.2.2      RSQLite_2.3.1       Rttf2pt1_1.3.12    
## [13] pillar_1.9.0        lattice_0.20-45     glue_1.7.0         
## [16] extrafontdb_1.0     digest_0.6.35       colorspace_2.1-0   
## [19] plyr_1.8.8          htmltools_0.5.7     Matrix_1.5-4.1     
## [22] slippymath_0.3.1    pkgconfig_2.0.3     magick_2.7.4       
## [25] bookdown_0.34       purrr_1.0.2         scales_1.2.1       
## [28] terra_1.7-74        tibble_3.2.1        proxy_0.4-27       
## [31] generics_0.1.3      ggplot2_3.4.4       cachem_1.0.8       
## [34] cli_3.6.2           magrittr_2.0.3      crayon_1.5.2       
## [37] memoise_2.0.1       evaluate_0.23       adehabitatMA_0.3.16
## [40] ncdf4_1.21          fansi_1.0.5         doParallel_1.0.17  
## [43] class_7.3-20        progressr_0.13.0    blogdown_1.17      
## [46] tools_4.1.2         prettyunits_1.2.0   hms_1.1.3          
## [49] lifecycle_1.0.4     stringr_1.5.1       munsell_0.5.0      
## [52] gdistance_1.6.2     compiler_4.1.2      jquerylib_0.1.4    
## [55] e1071_1.7-13        rlang_1.1.3         classInt_0.4-9     
## [58] units_0.8-2         grid_4.1.2          iterators_1.0.14   
## [61] htmlwidgets_1.6.4   igraph_1.4.3        base64enc_0.1-3    
## [64] rmarkdown_2.26      gtable_0.3.3        codetools_0.2-18   
## [67] DBI_1.1.3           curl_5.0.1          reshape2_1.4.4     
## [70] rayimage_0.9.1      R6_2.5.1            gridExtra_2.3      
## [73] knitr_1.45          dplyr_1.1.3         rgdal_1.6-6        
## [76] bit_4.0.5           fastmap_1.1.1       extrafont_0.19     
## [79] utf8_1.2.4          shape_1.4.6         KernSmooth_2.23-20 
## [82] stringi_1.8.1       parallel_4.1.2      Rcpp_1.0.11        
## [85] vctrs_0.6.4         sf_1.0-13           png_0.1-8          
## [88] rgl_1.3.3           tidyselect_1.2.0    xfun_0.42