In my GIS 2 class, I have students complete a simple site suitability analysis using Model Builder and ArcGIS. I’d like to have them do this through scripting, but at the time of assignment I think it’s a bit above their comfort level. I created this post as a demonstration of how to do site suitability/terrain analysis in R.
Suppose a client wants to build a cabin in Rusk County, Wisconsin and requests that it be situated situated
- Below 1200 ft.
- On a slope of less than 3 degrees
- Facing northeast, east, or southeast (angle between 22.5 and 157.5 degrees)
First, we will load packages then download and unzip the DEM data.
library(here)
library(tigris)
library(raster)
library(dplyr)
library(sf)
library(measurements)
library(leaflet)
## download dem and use the "here" package to set a "relative" working directory
download.file("https://gitlab.com/mhaffner/data/raw/master/DEM_30m.zip",
destfile = here("data/DEM_30m.zip"))
## unzip dem data
unzip(here("data/DEM_30m.zip"), exdir = here("data"))
## load dem
wi.dem <- raster(here("data/DEM_30m/demgw930/"))
plot(wi.dem)
Next, we will download county data for Wisconsin, extract just Rusk County, and transform the CRS into that of the DEM.
## use sf instead of sp, use cartographic boundary files since they look nicer
rusk.co <- counties("55", class = "sf", cb = TRUE, progress_bar = FALSE) %>%
filter(NAME == "Rusk") %>%
st_transform(., crs(wi.dem))
After this, we can crop the DEM to the county boundary since everything outside of Rusk County is irrelevant.
## crop dem to country boundaries
rusk.dem <- crop(wi.dem, rusk.co)
plot(rusk.dem, main = "Rusk County DEM")
Next, we are ready to implement terrain analysis functions and complete the
binary site suitability analysis. We can simply use the <
operator to get all
cells under 1200 ft. Then, we can “chain” the other functions together using the
&
operator. Here, we can use the terrain
function and specify the opt
argument.
rusk.suit <-
## elevation; under 1200 ft, convert to m (units of the dem)
rusk.dem < conv_unit(1200, "ft", "m") &
## aspect; between 22.5 and 157.5 degrees
terrain(rusk.dem, opt = "aspect", unit = "degrees") > 22.5 &
terrain(rusk.dem, opt = "aspect", unit = "degrees") < 157.5 &
## slope; less than 3 degrees
terrain(rusk.dem, opt = "slope", unit = "degrees") < 3
plot(rusk.suit)
Of course, the individual criteria may be of interest on their own. If that is
the case, then we can create a function that completes each operation within
its own plot
function and displays each criteria individually.
plot_criteria <- function(elev, asp.min, asp.max, slope) {
## get plot parameters so we can reset them after plotting is done
opar <- par()
## set plot parameters
par(mfrow = c(2,2))
## plot each step
plot(rusk.dem, main = "DEM")
plot(rusk.dem < conv_unit(elev, "ft", "m"), main = "Elevation criteria",
col = c("#ffffff", "#0000ff"))
plot(terrain(rusk.dem, opt = "aspect", unit = "degrees") > asp.min &
terrain(rusk.dem, opt = "aspect", unit = "degrees") < asp.max, main = "Aspect criteria")
plot(terrain(rusk.dem, opt = "slope", unit = "degrees") < slope, main = "Slope criteria",
col = c("#ffffff", "#ff9900"))
## reset plot parameters
par(opar)
}
plot_criteria(1200, 22.5, 157.5, 3)
It would also be nice to visualize the final result with some context. To do
this, we can use a leaflet
basemap.
leaflet(width = "100%") %>%
addTiles() %>%
addRasterImage(rusk.suit, opacity = 0.5, col = c("#ffffff", "#ff0000"))
From here, it’s clear that a significant portion of the suitable area is in water which is probably not ideal. Then again, maybe so if the client is open to a boat cabin!