Key Learnings from, and Solutions to the exercises in Chapter 6 of the book Geocomputation with R by Robin Lovelace, Jakub Nowosad and Jannes Muenchow.
Geocomputation with R
Textbook Solutions
Author
Aditya Dahiya
Published
December 27, 2024
6.1 Introduction
Focuses on interactions between raster and vector data models, first introduced in Chapter 2.
Covers three main techniques:
Raster cropping and masking using vector objects (Section 6.2).
Extracting raster values based on vector data (Section 6.3).
Raster-vector conversions, explained in Sections 6.4 and 6.5.
Code
library(sf) # Simple Features in Rlibrary(terra) # Handling rasters in Rlibrary(tidyterra) # For plotting rasters in ggplot2library(magrittr) # Using pipes with raster objectslibrary(tidyverse) # All things tidy; Data Wranglinglibrary(spData) # Spatial Datasetslibrary(patchwork) # Composing plots
6.2 Raster Cropping
Raster cropping and masking unify spatial extents of data, reduce memory use, and optimize computational resources, especially for remote sensing rasters and vector boundaries integration.
Applications:
Crop rasters to fit an area of interest.
Mask values outside specified bounds.
Essential preprocessing for creating maps and analyses.
Projection Alignment:
Rasters and vectors must share the same projection.
Raster extraction retrieves raster values at specified locations using a geographic selector (typically vector objects like points, lines, or polygons). Uses the terra::extract() function (terra package).
Point Selector: Extracts raster cell values for specified points. Example: Extracting elevation values for 20 sample points in Zion National Park using st_sample() as shown in Figure 2
Code
sysfonts::font_add_google("Saira Condensed", "body_font")showtext::showtext_auto()theme_set(theme_minimal(base_family ="body_font" ))# Elevation Raster for Zion National Park and nearby areassrtm <-rast(system.file("raster/srtm.tif", package ="spDataLarge"))# Zion National Park Boundaries, converted into CRS of `srtm`zion <-read_sf(system.file("vector/zion.gpkg", package ="spDataLarge" ) ) |>st_transform(st_crs(srtm))# ----------------------------------------------------------------# Basic data displayg1 <-ggplot() +geom_spatraster(data = srtm) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) +coord_sf(expand =FALSE) +labs(title ="Raw Data",subtitle ="Elevation Raster and Zion National Park boundaries" ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =14 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )# ----------------------------------------------------------------# Mask to display only Zion National Park, and# Display the elevations of randomly selected 20 points# Get 20 random points inside Zion National Parkset.seed(21)random_points <-st_sample(zion, size =20) |>st_sf()g2 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = random_points,size =1,alpha =0.5,pch =19 ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) +coord_sf(expand =FALSE) +labs(title ="Sampling 20 Random Points",subtitle ="Getting rndom poitns from sf::st_sample()" ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =14 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )# --------------------------------------------------------------# Extract heights for these 20 pointsrandom_points <- random_points |>mutate(elevation = terra::extract(srtm, random_points) |>pull(srtm) )g3 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = random_points,mapping =aes(colour = elevation),stroke =0.5,size =1,alpha =1,pch =1 ) +geom_sf_text(data = random_points,mapping =aes(label = elevation),size =4,alpha =1,family ="body_font",hjust =0,vjust =1,nudge_y =-0.002 ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) + paletteer::scale_colour_paletteer_c("ggthemes::Red") +coord_sf(expand =FALSE) +guides(colour ="none") +labs(title ="Extracting elevation on points from rasters",subtitle ="Using terra::extract() to get elevations on 20 points",x =NULL, y =NULL ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,1,0, "mm"),hjust =0.5,size =14 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )g <- g1 + g2 + g3 +plot_layout(nrow =1,guides ="collect" )ggsave(filename = here::here("book_solutions", "images", "chapter6_3_1.png"),plot = g,height =600,width =1500,units ="px",bg ="white")rm(g, g1, g2, g3)
Line Selector: Extracts raster values for each cell touched by a line. Recommendation: Split lines into points (using st_segmentize() and st_cast() from sf package) for accurate extraction along transects, such as creating elevation profiles for hiking routes, as shown in Figure 3
Code
# Get Latitude and Longitude of these random pointsdf1 <- random_points |>st_as_sfc() |>st_as_sf() |>rename(geometry = x) |>cbind(st_coordinates(random_points))# Tibble of northern, sourthern, eastern and western-most pointsdf2 <-bind_rows( df1[which.min(df1$X), ] |>mutate(dir ="west"), df1[which.max(df1$X), ] |>mutate(dir ="east"), df1[which.min(df1$Y), ] |>mutate(dir ="south"), df1[which.max(df1$Y), ] |>mutate(dir ="north"),)# Get two paths: east to west, north to southpaths <-bind_rows( df2 |>filter(dir %in%c("west", "east")) |>st_union() |>st_cast("LINESTRING") |>st_as_sf() |>mutate(path ="West to East"), df2 |>filter(dir %in%c("south", "north")) |>st_union() |>st_cast("LINESTRING") |>st_as_sf() |>mutate(path ="South to North"))rm(df1, df2)# Mid-point Check: Whether the CRS'es matchst_crs(srtm) ==st_crs(paths)# Plot of the pathsg1 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = paths,mapping =aes(colour = path),linewidth =0.5,arrow =arrow(length =unit(2, "mm")) ) +geom_sf(data = random_points,size =0.5 ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) + paletteer::scale_colour_paletteer_d("nbapalettes::cavaliers_retro" ) +coord_sf(expand =FALSE) +guides(colour ="none") +labs(title ="Selecting 2 Paths",subtitle ="sing st_cast() and st_coordinates()",x =NULL, y =NULL ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,0,0,0, "mm") )# A {sf} object of various waypoints along the paths# to be able to extract elevationpath_points <- paths |># Break into line segments of 100 metres eachst_segmentize(dfMaxLength =100) |># Convert each line segment into a central point# to be able to extract its elevationst_cast("POINT") |># Group by path to be able to calculate distance# along the pathgroup_by(path) |># Extract only the first column (i.e. the distance from# first point alone). Otherwise, st_distance() # returns a distance matrix As expected, distance grows # by approx. less than dfMaxLength amount per pointsmutate(dist =st_distance(x)[, 1]) |>ungroup()# Extract the actual elevation using terra::extractpath_points <- path_points |>mutate(elevation = terra::extract(srtm, path_points) |>pull(srtm),dist =as.numeric(dist) )g2 <- path_points |>ggplot(mapping =aes(x = dist,y = elevation,colour = path ) ) +geom_point(size =0.1 ) +geom_line(linewidth =0.2 ) +facet_wrap(~path, nrow =1) +scale_x_continuous(labels = scales::label_number(scale =0.001 ) ) +scale_y_continuous(labels = scales::label_number(big.mark ="," ) ) + paletteer::scale_colour_paletteer_d("nbapalettes::cavaliers_retro" ) +labs(x ="Distance along the path (km)",y ="Elevation (m)",title ="Elevation Profile along two paths",subtitle ="Using st_segmentize() & terra::extract()" ) +guides(colour ="none") +theme(axis.text =element_text(size =12,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,0,0,0, "mm"),axis.title.y =element_text(margin =margin(0,0,0,5, "mm") ) )my_design ="ABB"g <-wrap_plots(g1, g2) +plot_layout(design = my_design, guides ="collect") ggsave(filename = here::here("book_solutions", "images", "chapter6_3_2.png"),plot = g,height =600,width =1500,units ="px",bg ="white")rm(g, g1, g2, paths, path_points)
Polygon Selector: Extracts multiple raster values per polygon. Example: Summarizing elevation statistics (min, mean, max) for Zion National Park polygons using group_by() and summarize() from dplyr as shown in Figure 4
Code
# Creating circles around the 20 random points# Each circle of 1.5 km radiuscircles <- random_points |>st_buffer(dist =1500) |>mutate(ID =row_number())g1 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = circles, fill =alpha("white", 0.2) ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) +coord_sf(expand =FALSE) +guides(colour ="none") +labs(title ="Circles of 3 km diameter",subtitle ="Around 20 random points with st_buffer()",x =NULL, y =NULL ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )# A custom function for rounded off meanmean_r <-function(x){round(mean(x), 0)}# Getting minimum, mean and maximum height for each areacircles <- circles |># Add minimum maximum and mean heights for each circleleft_join( terra::extract(srtm, circles) |>as_tibble() |># Grouping by each polygon / circlegroup_by(ID) |>summarise(across(srtm, list(min = min, mean = mean_r, max = max))) )g2 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = circles, fill =alpha("white", 0.5) ) +geom_sf_text(data = circles,mapping =aes(label =paste0( srtm_max, "\n", srtm_min ) ),lineheight =0.3,family ="body_font",size =1.5,check_overlap =TRUE ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) +coord_sf(expand =FALSE) +guides(colour ="none") +labs(title ="Maximum and Minimum elevations for each circle",subtitle ="Using st_buffer(), terra::extract() and dplyr::summarize()",x =NULL, y =NULL ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )g3 <-ggplot() +geom_spatraster(data = srtm |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = circles, fill =alpha("white", 0.5) ) +geom_sf_text(data = circles,mapping =aes(label =paste0(scales::number(srtm_mean, big.mark =","), " m") ),lineheight =0.3,family ="body_font",size =3,check_overlap =TRUE ) +scale_fill_wiki_c(name ="Elevation (m)",labels = scales::label_number(big.mark =",") ) +coord_sf(expand =FALSE) +guides(colour ="none") +labs(title ="Average elevation for each circle",subtitle ="Using summarise()",x =NULL, y =NULL ) +theme(legend.key.width =unit(2, "mm"),axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )g <- g1 + g2 + g3 +plot_layout(guides ="collect" )ggsave(filename = here::here("book_solutions", "images", "chapter6_3_3.png"),plot = g,height =600,width =1500,units ="px",bg ="white")
Categorical Raster Extraction: Counts occurrences of raster categories (e.g., land cover) within polygons. Example: Analyzing land cover types in Zion National Park using terra::extract() as shown in Figure 5
Code
nlcd <-rast(system.file("raster/nlcd.tif", package ="spDataLarge"))# Get Zion Park Boundary transformed into CRS of NLCD Rasterzion <- zion |>st_transform(crs =st_crs(nlcd))# Get polygons (circles) transformed into CRS of NLCD Rastercircles1 <- circles |>select(ID) |>st_transform(crs =st_crs(nlcd))cat_circles <- terra::extract(nlcd, circles1) |>as_tibble() |>group_by(ID) |>count(levels) |>mutate(perc = n /sum(n))g1 <-ggplot() +geom_spatraster(data = nlcd |>mask(zion)) +geom_sf(data = zion, linewidth =0.4, fill ="transparent" ) +geom_sf(data = circles, fill =alpha("white", 0.2) ) +geom_sf_text(data = circles, mapping =aes(label = ID),size =5,family ="body_font" ) + paletteer::scale_fill_paletteer_d("MoMAColors::VanGogh") +coord_sf(expand =FALSE) +guides(colour ="none", fill ="none") +labs(title ="Circles of 3 km diameter",subtitle ="Around 20 random points with st_buffer()",x =NULL, y =NULL ) +theme(axis.text =element_text(size =8,margin =margin(0,0,0,0, "mm") ),plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =18 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm") )g4 <- cat_circles |>ggplot(aes(x =0, y = n, fill = levels)) +geom_col(position =position_stack(),colour ="white",linewidth =0.05 ) +facet_wrap(~ ID) +coord_polar(theta ="y") +theme_void(base_family ="body_font" ) +labs(fill =NULL,title ="Land Cover Pie charts for 20 zones",subtitle ="Each zone of diameter 3 km, identified by an ID" ) + paletteer::scale_fill_paletteer_d("MoMAColors::VanGogh") +theme(plot.title =element_text(margin =margin(0,0,2,0, "mm"),hjust =0.5,size =18 ),plot.subtitle =element_text(margin =margin(0,0,0,0, "mm"),hjust =0.5,size =14 ),axis.ticks.length =unit(0, "mm"),plot.margin =margin(0,2,0,2, "mm"),legend.text =element_text(margin =margin(0,0,0,1, "mm"),size =14 ),legend.margin =margin(0,0,0,0, "mm"),legend.box.margin =margin(0,0,0,0, "mm"),strip.text =element_text(size =18,margin =margin(0,0,0,0, "mm") ) )g <- g1 + g4 +plot_layout(design ="ABB" )ggsave(filename = here::here("book_solutions", "images", "chapter6_3_4.png"),plot = g,height =600,width =1500,units ="px",bg ="white")
Combining the work done in this section to produce a composite graphic shown in Figure 6 for publishing a code demonstration
Computes precise polygon overlap fractions for weighted statistics.
terra::extract(exact = TRUE) adds a fraction column for detailed overlap calculations. Useful for weighted means or precise coverage estimates but computationally intensive.
6.4 Rasterization
Rasterization converts vector objects into raster format, often for analysis (e.g., terrain) or modeling. It simplifies data by standardizing spatial resolution, serving as geographic data aggregation.
Key Function: rasterize() in the {terra} package handles rasterization.
Note
terra::rasterize(): Converts vector data into raster format by transferring values from vector geometries to raster cells.
Key Arguments:
x: Input vector data: A SpatVector or a two-column matrix (coordinates for points). Defines the geometries to be rasterized.
y: Template raster: A SpatRaster that determines the extent, resolution, and CRS of the output raster.
field: Specifies the values to transfer to raster:
As a character: Variable name from x (e.g., a column in the vector dataset).
As a numeric: Direct values or indices recycled to match nrow(x).
values (for matrix input x): Numeric values used for rasterization, recycled if fewer than nrow(x). Can also be a matrix or data frame.
fun: Summarizing function for overlapping geometries in a cell:
For lines and polygons: “min”, “max”, “mean”, “count”, “sum”.
For points: Any function returning a single value (e.g., mean, length, min). The fun = "length" means a count of the number of points in that cell.
background: Default cell value for areas not covered by any features in x. Default: NA.
touches: Logical: If TRUE, includes all cells touched by a feature (lines/polygons), not just those where centroids fall.
update: Logical: If TRUE, updates existing values in the template raster.
cover: Logical: For polygons, returns the fraction of a cell covered by a feature. Uses sub-cell presence/absence checks.
by: Groups vector data into layers:
For SpatVector: Column name or index.
For matrices: Vector defining group membership.
Default Behaviour of rasterize():
If no fun is specified, the last value is used in case of overlaps (fun = "last").
When field is empty, raster cells reflect presence/absence of geometries.
Common Use Cases:
Presence/absence rasters: Determine where features exist.
Summary statistics: Aggregate multiple values in a single cell (e.g., sum, mean).
Fractional coverage: Estimate the proportion of a raster cell covered by polygons.
Points Rasterization: can be done in three further methods depicted in Figure 7: —
Presence/Absence Raster:
Raster indicates whether cells contain cycle hire points.
Count Raster:
Uses fun = "length" to count cycle hire points per grid cell.
Summary Statistic Raster:
Calculates sum of a variable (e.g., capacity) using field and fun = sum.
Code
sysfonts::font_add_google("Saira Condensed", "body_font")showtext::showtext_auto()theme_set(theme_minimal(base_family ="body_font",base_size =12 ))theme_custom <-function(...){theme(legend.key.height =unit(1, "mm"),legend.key.width =unit(6, "mm"),plot.title =element_text(size =24,margin =margin(0,0,2,0, "mm") ),plot.subtitle =element_text(size =18,margin =margin(0,0,1,0, "mm") ),axis.ticks.length =unit(0, "mm"),axis.text =element_text(margin =margin(0,0,0,0, "mm") ),panel.grid =element_line(linewidth =0.2),legend.position ="bottom",legend.title.position ="top",legend.text =element_text(margin =margin(1,0,0,0, "mm")),legend.title =element_text(margin =margin(2,0,1,0, "mm")),legend.margin =margin(0,0,0,0, "mm"),legend.box.margin =margin(0,0,0,0, "mm") )}# Get cycle hiring points data for Londoncycles <- spData::cycle_hire_osm |># Transform into CRS of EPSG:27700 - British National Gridst_transform("EPSG:27700")# Create a template raster for using in rasterizationtemplate_raster <-rast(# Spat Extent of the Vector data terra::ext(cycles),# Required resolution of the rasterresolution =500,# CRS for the rastercrs =crs(cycles))g1 <-ggplot() +geom_sf(data = cycles,mapping =aes(fill = capacity ),pch =21,size =0.5,linewidth =0.001 ) + paletteer::scale_fill_paletteer_c("ggthemes::Red",na.value ="transparent" ) +labs(title ="Cycle Hire Points (London)",subtitle ="OSM Vector data, plotted with {sf}" ) +theme_custom()g2 <-ggplot() +geom_spatraster(data = cycles |> terra::rasterize(template_raster) ) + paletteer::scale_fill_paletteer_c("ggthemes::Red",na.value ="transparent",name ="Hiring points present?" ) +labs(title ="Presence / Absence Raster",subtitle ="Default rasterize(); i.e. (fun = \"last\")" ) +theme_custom()g3 <-ggplot() +geom_spatraster(data = cycles |> terra::rasterize(template_raster,fun ="length") ) + paletteer::scale_fill_paletteer_c("ggthemes::Red",na.value ="transparent",breaks =seq(0, 10, 2),name ="Number of hiring points" ) +labs(title ="Count raster (vector points per cell)",subtitle ="rasterize(fun = \"length\")" ) +theme_custom()g4 <-ggplot() +geom_spatraster(data = cycles |> terra::rasterize( template_raster,field ="capacity",fun ="sum") ) + paletteer::scale_fill_paletteer_c("ggthemes::Red",na.value ="transparent",name ="Cycles Capacity" ) +labs(title ="Summary Statistic raster",subtitle ="rasterize(field = \"capacity\", fun = \"length\")" ) +theme_custom()g <- g1 + g2 + g3 + g4 +plot_layout(nrow =1) +plot_annotation(tag_levels ="a",title ="Vectorizing rasters into points - 3 types",theme =theme(plot.title =element_text(size =48,margin =margin(2,0,2,0, "mm"),hjust =0.5 ) ) ) &theme(plot.tag =element_text(size =24,margin =margin(0,0,-5,0, "mm"),face ="bold" ) )ggsave(filename = here::here("book_solutions", "images", "chapter6_4_1.png"),plot = g,height =700,width =2200,units ="px",bg ="white")
Line and Polygon Rasterization is shown in Figure 8. The polygon rasterization can proceed in two ways, depending on the argument touches = TRUE / FALSE as shown in Figure 8 .
touches = TRUE: Includes all cells touched by lines/polygons.
Default (touches = FALSE): Selects cells where centroids fall inside polygons.
Code
sysfonts::font_add_google("Saira Condensed", "body_font")showtext::showtext_auto()theme_set(theme_minimal(base_family ="body_font",base_size =12 ))theme_custom <-function(...){theme(legend.key.height =unit(1, "mm"),legend.key.width =unit(6, "mm"),plot.title =element_text(size =24,margin =margin(0,0,2,0, "mm"),lineheight =0.3 ),plot.subtitle =element_text(size =18,margin =margin(0,0,1,0, "mm"),lineheight =0.3 ),axis.ticks.length =unit(0, "mm"),axis.text =element_text(margin =margin(0,0,0,0, "mm") ),panel.grid =element_line(linewidth =0.2),legend.position ="none" )}#----------------------------------------------------------------# Get vector data on State of California from `us_states` datacalifornia <- us_states |>filter(NAME =="California")california_border <- california |>st_cast("MULTILINESTRING")template_raster <-rast(ext(california),crs =st_crs(california)$wkt, # Use crs(california) or st_crs(california)$wktresolution =0.25# Give resolution in degrees)crs(california)st_crs(california)$wktg1 <-ggplot() +geom_sf(data = california) +labs(title ="California Map (vector)\nplotted with {sf}") +theme_custom()g2 <-ggplot() +geom_spatraster(data = california_border |>rasterize(template_raster) ) + paletteer::scale_fill_paletteer_c("ggthemes::Classic Red",na.value ="transparent" ) +labs(title ="Multi-Linestring\nwith terra::rasterize()",subtitle ="Cells touched by Linestring are coloured") +theme_custom()g3 <-ggplot() +geom_spatraster(data = california |>rasterize(template_raster,touches =FALSE) ) + paletteer::scale_fill_paletteer_c("ggthemes::Classic Red",na.value ="transparent" ) +labs(title ="Multipolygon\nterra::rasterize(touches = FALSE)",subtitle ="Only colours raster cells whose\ncentroids are inside multi-polygon") +theme_custom()g4 <-ggplot() +geom_spatraster(data = california |>rasterize(template_raster,touches =FALSE) ) + paletteer::scale_fill_paletteer_c("ggthemes::Classic Red",na.value ="transparent" ) +labs(title ="Multipolygon\nterra::rasterize(touches = TRUE)",subtitle ="Colours all raster cells which\nare touched by the multi-polygon") +theme_custom()rast1 <- california |>rasterize( template_raster,touches =FALSE )rast2 <- california_border |>rasterize( (rast1 *0.5),update =TRUE )g5 <-ggplot() +geom_spatraster(data = rast2 ) + paletteer::scale_fill_paletteer_c("ggthemes::Classic Red",na.value ="transparent" ) +labs(title ="Polygon & Linestring\nterra::rasterize(update = TRUE)",subtitle ="Rasterized polygon updated with a rasterized linestring") +theme_custom()g <- g1 + g2 + g3 + g4 + g5 +plot_layout(nrow =1, guides ="collect") +plot_annotation(tag_levels ="a",title ="Techniques for Rasterizing lines and polygons with {terra}",theme =theme(plot.title =element_text(size =48,margin =margin(2,0,2,0, "mm"),hjust =0.5 ) ) ) &theme(plot.tag =element_text(size =24,margin =margin(0,0,-5,4, "mm"),face ="bold" ),plot.margin =margin(0,0,0,0, "mm") )ggsave(filename = here::here("book_solutions", "images", "chapter6_4_2.png"),plot = g,height =700,width =2200,units ="px",bg ="white")
6.5 Spatial Vectorization
Converts raster data (spatially continuous) into vector data (spatially discrete: points, lines, polygons). Opposite process of rasterization. It can be of three types – (1) points, (2) lines, and (3) polygons.
(1) Points from Raster Centroids:
Use terra::as.points()(Hijmans 2024) for converting raster cells (non-NA) to points. It produces a SpatVector class of object, which can then be converted into an {sf} (Pebesma and Bivand 2023) object using sf::st_as_sf() .
Example: Elevation raster visualized as points in Figure 9.
Represent lines of constant values (e.g., elevation, temperature).
Use terra::as.contour() which creates a SpatVector, which then is converted into an {sf} object using sf::st_as_sf() which shows contour lines as LINESTRING or MULTILINESTRING geometry featuers, with an additional column on level (every 100 metres)
Or, rasterVis::contourplot() for creating and overlaying contours. For example, visualize DEMs with hillshading and contours in Figure 10
Crop the srtm raster using (1) the zion_points dataset and (2) the ch dataset. Are there any differences in the output maps? Next, mask srtm using these two datasets. Can you see any difference now? How can you explain that?
The Figure 13 (b) shows the srtm raster cropped using zion_points dataset. The Figure 13 (c) shows the raster srtm cropped using ch dataset. There seem to be no differences between the two output cropped maps.
However, the Figure 14 (a) shows masking of the raster with points - showing that masking by points results in selected points on the raster, thus resulting in no visible result. The Figure 14 (b) shows masking of a raster by a polygon, a more expected result.
The distinction between masking and cropping can be better understood by examining how they interact with polygons and points:
Masking: When masking with a polygon, the output includes all the raster cells that fall within the interior or lie on the boundaries of the polygon. In contrast, masking with points produces an output that includes only the specific raster cells that align directly with those points, without considering the area around them.
Cropping: Cropping, whether by points or polygons, yields the same result. This is because cropping considers the bounding box, which is the smallest rectangle that can enclose the given geometry. For a set of points and their convex hull (a polygon that encloses the points), the bounding box remains identical. Consequently, the raster output produced by cropping is consistent regardless of whether the input geometry is a polygon or a set of points.
E2.
Firstly, extract values from srtm at the points represented in zion_points. Next, extract average values of srtm using a 90 buffer around each point from zion_points and compare these two sets of values. When would extracting values by buffers be more suitable than by points alone?
Bonus: Implement extraction using the exactextractr package and compare the results.
The Table 1 compares the two values from rasters - extracted from the points and extracted from the buffer zone (90 metres) around the points. It would be better to use the buffer around the points, since it removes the chance error or randomness.
Further, the use of exactextractr package helps us calculate even better weighted mean of the buffer zone, by calculating how much of a raster cell’s proportion fall within the buffer zone.
Subset points higher than 3100 meters in New Zealand (the nz_height object) and create a template raster with a resolution of 3 km for the extent of the new point dataset. Using these two new objects:
Count numbers of the highest points in each grid cell.
Find the maximum elevation in each grid cell.
The object above_3100 contains the 19 peaks which are above 3100 metres. Further, the template_raster is a template raster with a resolution of 3 km and the extent of the new point dataset. The results for count and maximum height in each cell os new raster are shown in Figure 15.
Code
data("nz_height")above_3100 <- nz_height |>filter(elevation >3100)# Create a template raster for using in rasterizationtemplate_raster <-rast(# Spat Extent of the Vector data terra::ext(above_3100),# Required resolution of the rasterresolution =3000,# CRS for the rastercrs =crs(above_3100))elevation_raster <- above_3100 |>rasterize(template_raster)elevation_raster_1 <- above_3100 |>rasterize(template_raster, fun ="length")elevation_raster_2 <- above_3100 |>rasterize( template_raster,field ="elevation",fun ="max")df1 <- elevation_raster_1 |>values() |>as_tibble() |>mutate(cell_id =row_number()) |>drop_na() |>rename(numer_of_peaks = V1_length,cell_ID = cell_id )df2 <- elevation_raster_2 |>values() |>as_tibble() |>mutate(cell_id =row_number()) |>drop_na() |>rename(maximum_height_of_peaks = max,cell_ID = cell_id )g1 <-ggplot() +geom_spatraster(data = elevation_raster_1) +scale_fill_viridis_c(direction =-1,na.value ="transparent" ) +labs(title ="Number of peaks per cell",subtitle ="Raster created using rasterize(fun = `length`)" ) +theme_custom() +theme(legend.position ="bottom" ) g2 <-ggplot() +geom_spatraster(data = elevation_raster_2) +scale_fill_princess_c(na.value ="transparent" ) +labs(title ="Maximum height per cell",subtitle ="Using rasterize(field = `elevation`, fun = `max`)" ) +theme_custom() +theme(legend.position ="bottom" )g <- g1 + g2 +plot_layout(nrow =1) +plot_annotation(tag_levels ="a",tag_prefix ="(",tag_suffix =")" ) &theme(plot.tag =element_text(size =36,face ="bold" ) )ggsave(filename = here::here("book_solutions", "images","chapter6-e3.png"),height =800,width =1600,units ="px")
The count of the number of highest points in each grid cell, and the maximum height in each grid cell is shown in Table 2
Table 2: Number of highest points in each grid cell.
Cell Id
Numer of Peaks
Maximum Height of Peaks
24
7
3497
25
2
3194
28
1
3199
31
6
3724
38
1
3593
50
1
3151
E4.
Aggregate the raster counting high points in New Zealand (created in the previous exercise), reduce its geographic resolution by half (so cells are 6 x 6 km) and plot the result.
Resample the lower resolution raster back to the original resolution of 3 km. How have the results changed?
Name two advantages and disadvantages of reducing raster resolution.
The code below aggregates the raster showing the count of high points into a resolution of 6 km, using terra::resample() with a template_raster_2 of resolution 6 km. The results are shown in Figure 16. Advantages of Reducing Raster Resolution
Improved Computation Efficiency: Fewer cells reduce the data size, speeding up processing and analysis.
Simplification: Provides a coarser overview for large-scale analysis, which may be sufficient for specific applications like regional studies.
Disadvantages of Reducing Raster Resolution
Loss of Detail: Fine spatial variations and features are lost, potentially affecting precision in applications requiring high detail.
Accuracy Trade-Off: Aggregated data may misrepresent localized phenomena, leading to potential inaccuracies in spatial analyses.
Code
# Create a template raster for using in rasterizationtemplate_raster_2 <-rast(# Spat Extent of the Vector data terra::ext(above_3100),# Required resolution of the rasterresolution =6000,# CRS for the rastercrs =crs(above_3100))g1 <-ggplot() +geom_spatraster(data = elevation_raster_1) +scale_fill_viridis_c(direction =-1,na.value ="transparent" ) +labs(title ="Number of peaks per cell",subtitle ="Raster created using rasterize(fun = `length`)" ) +theme_custom() +theme(legend.position ="bottom" ) g2 <-ggplot() +geom_spatraster(data = elevation_raster_1 |># terra::aggregate(fact = 2, fun = mean)resample(template_raster_2, method ="bilinear") ) +scale_fill_viridis_c(direction =-1,na.value ="transparent" ) +labs(title ="Aggregated raster (res = 6 km)",subtitle ="Using resample(method = `bilinear`)" ) +theme_custom() +theme(legend.position ="bottom" )g3 <-ggplot() +geom_spatraster(data = elevation_raster_1 |>resample(template_raster_2, method ="bilinear") |>resample(template_raster, method ="bilinear") ) +scale_fill_viridis_c(direction =-1,na.value ="transparent" ) +labs(title ="Disaggregated raster (res = 3 km)",subtitle ="Reverting back to high res. leads to loss of data" ) +theme_custom() +theme(legend.position ="bottom" )g <- g1 + g2 + g3 +plot_layout(nrow =1) +plot_annotation(tag_levels ="a",tag_prefix ="(",tag_suffix =")" ) &theme(plot.tag =element_text(size =36,face ="bold" ) )ggsave(filename = here::here("book_solutions", "images","chapter6-e4.png"),height =800,width =1800,units ="px")
E5.
Polygonize the grain dataset and filter all squares representing clay.
Name two advantages and disadvantages of vector data over raster data.
When would it be useful to convert rasters to vectors in your work?
The code chunks below show the polygonization, and filtered data of all squares representing clay (using as.polygon(aggregate = FALSE)). The Figure 17 shows the grain dataset and the polygonized grain_sf where each row represents a <POLYGON> (aggregated).
Precision in Representation: Vector data precisely represents geographic features such as points, lines, and polygons. It is ideal for applications requiring exact boundaries (e.g., property lines, administrative borders).
Efficient Storage for Sparse Data: Vectors store only the coordinates of features, making them more efficient for sparse or irregularly distributed data compared to rasters, which store values for all cells, even those with no data.
Disadvantages of Vector Data Over Raster Data
Complex Analysis: Performing spatial operations (e.g., overlay, buffering) is computationally more complex with vectors, especially with large datasets.
Inefficient for Continuous Data: Vector data struggles to efficiently represent continuous phenomena (e.g., elevation, temperature) that vary over space, for which raster data is better suited.
When to Convert Rasters to Vectors in Your Work
Feature Extraction: If you need to delineate specific features (e.g., contours from a Digital Elevation Model (DEM) or boundaries of high-value areas), converting a raster to vector allows for precise representation and further analysis.
Integration with Vector Data: For projects requiring alignment or combination with existing vector datasets (e.g., administrative boundaries or transportation networks), converting rasters to vectors ensures compatibility.
Visual Clarity in Maps: Vectors are preferred for creating clear and professional maps where discrete features need labeling or editing.
Custom Analysis: If detailed geometric operations like buffering or intersection analysis are needed, converting rasters into vectors (e.g., polygons of land cover classes) enables such workflows.