#' Create ridgelines from a digital elevation model (dhm)
#'
#' dhm: path to a dhm that can be imported using terra::rast
#' n_lines: how many lines / polygons do you want to draw? Default is 50
#' vspace: vertical space between lines, in units provided by the dhm. This overrides n_lines
#' fac: How much of the space between the lines should be occupied by the hightest elevation?
#' point_density: Density of the point samples used to extract elevation. Defaults to the inverse of the raster resolution
#' geom_type: What should the output geometry type be? Can be LINESTRING or POLYGON
<- function(dhm, n_lines = 50, vspace = NULL, fac = 2, point_density = NULL, geom_type = "LINESTRING"){
create_ridges
library(sf)
library(terra)
library(purrr)
# extract the extent of the dhm as a vector
<- ext(dhm) %>%
ex as.vector()
# If vspace is NULL (default), then vspace is calculated using n_lines
if(is.null(vspace)){
<- (ex["ymax"] - ex["ymin"])/n_lines
vspace
}
<- if(is.null(point_density)){1/terra::res(dhm)[2]}
point_density
# Defines at what y-coordinates elevation should be extracted
<- seq(ex["ymin"], ex["ymax"], vspace)
heights
# calculates the x/y coordinates to extract points from the dhm
<- map(heights, function(height){
mypoints_mat matrix(c(ex["xmin"], height, ex["xmax"], height), ncol = 2, byrow = TRUE) %>%
st_linestring()
%>%
}) st_as_sfc() %>%
st_line_sample(density = point_density,type = "regular") %>%
st_as_sf() %>%
st_cast("POINT") %>%
st_coordinates()
# extracts the elevation from the dhm
<- terra::extract(dhm, mypoints_mat) %>%
extracted cbind(mypoints_mat) %>%
as_tibble()
# calculates the factor with which to multiply elevation, based on "fac" and the maximum elevation value
<- vspace*fac/max(extracted[,1], na.rm = TRUE)
fac
# calculates the coordinats of the ridge lines
<-extracted %>%
coords filter(!is.na(extracted[,1])) %>%
split(.$Y) %>%
imap(function(df, hig){
<- as.numeric(hig)
hig <- hig+pull(df[,1])*fac
Y_new matrix(c(df$X, Y_new), ncol = 2)
})
# creates LINESTRING or POLYGON, based on the "geom_type"
<- if(geom_type == "LINESTRING"){
geoms map(coords, ~st_linestring(.x))
else if(geom_type == "POLYGON"){
} imap(coords, function(x, hig){
<- as.numeric(hig)
hig
<- head(x, 1)
first 2] <- hig
first[,<- tail(x, 1)
last 2] <- hig
last[,
st_polygon(list(rbind(first, x, last, first)))
})else{
} stop(paste0("This geom_type is not implemented:",geom_type,". geom_type must be 'LINESTRING' or 'POLYGON'"))
}
# adds the CRS to the output sfc
<- crs(dhm)
dhm_crs
if(dhm_crs == "") warning("dhm does not seem to have a CRS, therefore the output does not have a CRS assigned either.")
%>%
geoms st_sfc() %>%
st_set_crs(dhm_crs)
}
# A helper function to creteate a polygon from the extent of a (dhm) raster
<- function(rast_obj){
st_bbox_rast
library(terra)
library(sf)
<- ext(rast_obj) %>%
ex as.vector()
matrix(c(ex[1],ex[3],ex[1], ex[4],ex[2], ex[4],ex[2],ex[3],ex[1],ex[3]),ncol = 2, byrow = TRUE) %>%
list() %>%
st_polygon() %>%
st_sfc(crs = crs(rast_obj))
}
“So beautiful that it hurts”1 Bauhasaurus wrote in his tweet, posting an image by Carla Martínez Sastre. The artist had used a beautiful, clever and minimalistic way to visualize the topography of South America.
The way I understand it, Carla drew “horizontal” (latitudanal) elevation profiles at equal intervals over the continent and filled these elevation profiles to visualize not only the continent’s topography, but also implicitly showing it’s borders.
I found this a very nice approach and tried recreating this idea with R for my home country, Switzerland. I’m quite happy with the result, however there is still a lot of room for improvement. I’ve packed the approach into generic functions, see below for the complete source code. Check below to see the source code.
Create some generic functions
Import data and use the functions
Code
library(sf)
library(terra)
library(dplyr)
library(purrr)
library(ggplot2)
# library(ragg)
<- terra::rast("data-git-lfs/DHM25/DHM200.asc")
dhm crs(dhm) <- "epsg:21781"
<- sf::read_sf("data-git-lfs/swissboundaries/swissBOUNDARIES3D_1_3_TLM_LANDESGEBIET.shp") %>%
switzerland_21781 st_union() %>%
st_transform(21781)
<- st_bbox_rast(dhm) %>%
mymask st_buffer(5000) %>%
st_difference(switzerland_21781)
<- create_ridges(dhm,n_lines = 35, fac = 1.1,geom_type = "POLYGON")
sf_obj
# bg_color <- "#27363B"
<- Sys.getenv("plot_bg_col")
bg_color <- "#EB4960"
fg_color <- "FreeMono"
family
<- st_bbox(switzerland_21781)
bbox_switz <- st_buffer(st_as_sfc(bbox_switz),50000)
bbox_switz_enlarge <- st_bbox(bbox_switz_enlarge)
lims = lims[c("xmin","xmax")]
xlims = lims[c("ymin","ymax")]
ylims
<- diff(ylims)/diff(xlims) asp
Code
<- ggplot(sf_obj) +
myplot geom_sf(color = "NA", fill = fg_color) +
geom_sf(data = mymask, color = "NA", fill = bg_color) +
# geom_sf(data = bbox_switz_enlarge, fill = "NA") +
::geom_richtext(aes(x = median(xlims), y = quantile(ylims,0.95), label = "Topography of Switzerland"), family = family, fill = NA, label.color = NA, hjust = 0.5, size = 6, color = fg_color)+
ggtext::geom_richtext(aes(x = median(xlims), y = ylims["ymin"], label = "Data from ©swisstopo<br>visualized by Nils Ratnaweera"), family = family, fill = NA, label.color = NA, hjust = 0.5, size = 3.5, color = fg_color)+
ggtexttheme_void() +
theme(plot.background = element_rect(fill = bg_color,color = NA)) +
coord_sf(datum = 21781,xlim = xlims, ylim = ylims);
Footnotes
Original (Esp): “Tan linda que duele.”↩︎