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)
getData("GADM", country = "CHE", level = 0) swiss <-
## 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 theraster
package - Option 2: Use the
get_elev_raster()
function from theelevatr
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
getData("alt", country = "CHE")
elev <-
# 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
get_elev_raster(swiss, z = 7)
elev <- crop(elev, swiss)
elev <-
# 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)
spsample(swiss, n = 20, type = "random")
locs <-
# Identify elevation at each location
$Elevation <- extract(elev, locs)
locs$Elevation <- round(locs$Elevation)
locs
# 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"
terrain(elev, "aspect")
aspect <- terrain(elev, "slope")
slope <-
# And now we can calculate the hillshade
hillShade(slope = slope, aspect = aspect)
hillshade <-
# 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
2
modifier <- terrain(elev ^ modifier, "aspect")
aspect2 <- terrain(elev ^ modifier, "slope")
slope2 <-
# And now we can calculate the hillshade
hillShade(slope = slope2, aspect = aspect2)
hillshade2 <-
# 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)
matrix(1, nrow = 3, ncol = 3)
weights <-
# Function to calculate the terrain ruggedness index
function(x){sum(abs(x[-5] - x[5])) / 8}
ruggedness <-
# Apply the function to the moving window
focal(elev, w = weights , fun = ruggedness, pad = TRUE, padValue = NA)
tri <-
# 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
raster_to_matrix(elev)
elmat <-
# 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
mask(elev, swiss)
elev <-
# Convert the raster to a matrix
raster_to_matrix(elev)
elmat <-
# 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
"https://github.com/DavidDHofmann/MajorWaters/archive/master.zip"
url <-
# Specify filename and download directory (we'll use a temporary directory)
tempdir()
dir <- file.path(dir, "water.zip")
file <-
# Download the data
download.file(url = url, destfile = file)
# Extract the zip file
unzip(file, exdir = dir)
# Filepath to the extracted file
file.path(dir, "MajorWaters-main", "geo_che_water.shp")
file <-
# Load the data and reproject it
shapefile(file)
water <- spTransform(water, crs(hillshade))
water <-
# 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
rasterize(water, elev, field = 1, background = 0)
water <-
# For whatever reason we need to flip the water raster
flip(water, "x")
water <-
# Convert water to a matrix
raster_to_matrix(water)
wamat <-
# 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(.
, elmatzscale = 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
getNOAA.bathy(
bathy <-lon1 = 140
lon2 = 155
, lat1 = -13
, lat2 = 0
, resolution = 4
,
)
# Coerce to a raster
marmap::as.raster(bathy)
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
- Great tutorials on how to use the rayshader package (https://www.rayshader.com/)
- marmapr vignette (https://cran.r-project.org/web/packages/marmap/vignettes/marmap-DataAnalysis.pdf)
- Some further information on terrain characteristics (http://search.r-project.org/library/raster/html/terrain.html)
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