library(sf) # Simple Features in R
library(terra) # Handling rasters in R
library(tidyterra) # For plotting rasters in ggplot2
library(magrittr) # Using pipes with raster objects
library(tidyverse) # All things tidy; Data Wrangling
library(spData) # Spatial Datasets
library(patchwork) # Composing plots
Chapter 5: Geometry operations
Key Learnings from, and Solutions to the exercises in Chapter 4 of the book Geocomputation with R by Robin Lovelace, Jakub Nowosad and Jannes Muenchow.
5.1 Introduction
- Previous chapters introduced geographic datasets’ structure (Chapter 2), attribute-based manipulation (Chapter 3), and spatial relations (Chapter 4).
- Focus of this chapter: Manipulating geographic elements of spatial objects.
- Examples: Creating buffers, simplifying/converting vector geometries, and raster aggregation/resampling.
- Section 5.2: Transforming vector geometries using:
- Unary operations: Simplifications, buffers, centroids, and affine transformations (Sections 5.2.1–5.2.4).
- Binary operations: Modifying geometries through clipping and unions (Sections 5.2.5–5.2.7).
- Type transformations: Converting geometry types, e.g., polygons to lines (Section 5.2.8).
- Section 5.3: Raster transformations:
- Alter pixel size, resolution, extent, and origin.
- Align raster datasets for map algebra.
5.2 Geometric operations on vector data
- Focus: Operations that modify the geometry of vector (
sf
) objects. - Key distinction: Works directly on geometry-level objects of class
sfc
, in addition tosf
objects. - Examples: Drilling into geometry to transform, simplify, or reshape vector data.
5.2.1 Simplification
- Generalizes vector geometries (lines/polygons) for smaller scale maps, reducing memory, disk space, and bandwidth usage. Useful for publishing interactive maps by simplifying complex geometries.
- Key Functions and Algorithms:
st_simplify()
from the sf package (Pebesma and Bivand 2023):Implements the Douglas-Peucker algorithm (Douglas and Peucker 1973).
Controlled by
dTolerance
(generalization level in metres, or map units).Simplifies individual geometries but does not preserve topology, leading to overlaps or holes.
topology (noun): the way in which parts of something are organized, arranged or connected
ms_simplify()
from the rmapshaper package (Teucher and Russell 2023):- Uses the Visvalingam algorithm (Visvalingam and Whyatt 1993).
- Retains topology by default (
keep_shapes = TRUE
) and allows fine control over the vertex retention (keep
: the % of vertices that are to be retained, given as a proportion).
smooth()
from the smoothr package:- Smooths edges using techniques like Gaussian kernel regression, Chaikin’s algorithm, or spline interpolation.
- Does not reduce vertex count and does not preserve topology.
- Key parameter:
smoothness
(controls Gaussian bandwidth).
- Examples of Simplification are shown in Figure 1
- Applications of Smoothing:
- Suitable for geometries derived from raster vectorization (e.g., Chapter 6).
Code
# Download India's Official states' map from
# https://github.com/Aditya-Dahiya/projects_presentations/tree/main/data/india_map
#### Base Map
<- read_sf("India_State_Boundary.shp")
india_states
<- india_states |>
g ggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "Base Map of India: in full detail",
subtitle = "Source: Census of India"
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_1.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
#### st_simplify()
<- india_states |>
g st_simplify(dTolerance = 100000) |> # 100 km tolerance
ggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "India: st_simplify(dTolerance = 100000)",
subtitle = "Douglas-Peucker algorithm. Topology is lost."
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_2.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
#### rmapshaper::ms_simplify()
<- india_states |>
g ::ms_simplify(keep = 0.0001, keep_shapes = TRUE) |>
rmapshaperggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "India: rmapshaper::ms_simplify(keep_shapes = TRUE)",
subtitle = "Visvalingam algorithm. Topology is retained."
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_3.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
#### smoothr::smooth() - 3 methods
<- india_states |>
g st_simplify(dTolerance = 10000) |> # To save computing time
::smooth(method = "ksmooth",
smoothrsmoothness = 5) |>
ggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "smoothr::smooth(method = \"ksmooth\")",
subtitle = "Gaussian kernel regression. Topology is lost."
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_4.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
<- india_states |>
g st_simplify(dTolerance = 50000) |> # To save computing time
::smooth(method = "chaikin") |>
smoothrggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "smoothr::smooth(method = \"chaikin\")",
subtitle = "Chaikin’s corner cutting algorithm. Topology is lost."
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_5.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
<- india_states |>
g st_simplify(dTolerance = 50000) |> # To save computing time
::smooth(method = "spline") |>
smoothrggplot() +
geom_sf() +
theme_minimal() +
labs(
title = "smoothr::smooth(method = \"spline\")",
subtitle = "Spline interpolation. Topology is lost."
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_6.png"),
plot = g,
height = 1600,
width = 1200,
units = "px"
)
smoothr
A short note on the {smoothr} package (Strimas-Mackey 2023), which uses three different types of algorithms:-.
Code
library(sf)
library(smoothr)
library(tidyverse)
# Smooth polygons using different methods
<- smooth(jagged_polygons, method = "chaikin")
p_smooth_chaikin <- smooth(jagged_polygons, method = "ksmooth")
p_smooth_ksmooth <- smooth(jagged_polygons, method = "spline")
p_smooth_spline
# Combine data for plotting
<- bind_rows(
plot_data mutate(st_as_sf(p_smooth_chaikin), method = "chaikin"),
mutate(st_as_sf(p_smooth_ksmooth), method = "ksmooth"),
mutate(st_as_sf(p_smooth_spline), method = "spline"),
mutate(st_as_sf(jagged_polygons), method = "original")
)
# Assign colors to methods
<- c(
method_colors chaikin = "#E41A1C",
ksmooth = "#4DAF4A",
spline = "#377EB8"
)
# Convert geometry for plotting
<- plot_data |>
plot_data mutate(geometry = st_sfc(geometry)) |>
st_as_sf()
<- plot_data |>
p2 filter(method != "original")
<- plot_data |>
p1 filter(method == "original") |>
select(-method)
# Plot with ggplot2
<- ggplot(data = p2) +
g geom_sf(aes(geometry = geometry,
color = method),
size = 0.7,
linewidth = 0.5,
fill = "transparent") +
geom_sf(
data = p1,
fill = alpha("grey50", 0.5),
colour = "transparent"
+
) scale_color_manual(values = method_colors) +
facet_grid(id ~ method) +
guides(fill = "none") +
theme_void() +
theme(
legend.position = "none",
plot.title.position = "plot",
strip.text.y = element_blank()
+
) labs(
title = "Simplification with {smoothr}",
colour = "Method"
)
ggsave(
filename = here::here("book_solutions", "images",
"ch5-2-1_7.png"),
plot = g,
height = 2000,
width = 1200,
units = "px"
)
5.2.2 Centroids
Identify the center of geographic objects, creating single-point representations of complex geometries using
st_centroid()
Types of Centroids: (shown in Figure 2)
- Geographic Centroid (center of mass):
- Balances a spatial object (like balancing a plate).
- Useful for creating simple point representations or estimating distances between polygons.
- Calculated with
st_centroid()
from the sf package. - Limitation: Centroids may fall outside the object (e.g., doughnut-shaped polygons).
- Point on Surface:
- Ensures the point lies within the object boundaries.
- Useful for labeling irregular polygons, such as islands or multipolygon geometries.
- Calculated with
st_point_on_surface()
.
- Geographic Centroid (center of mass):
Other Centroid Types: Chebyshev center and visual center
Code
::font_add_google("Saira Extra Condensed", "caption_font")
sysfonts::showtext_auto()
showtext
::theme_set(
ggplot2theme_minimal(
base_size = 30,
base_family = "caption_font"
+
) theme(
text = element_text(
lineheight = 0.3,
hjust = 0.5
),plot.title.position = "plot"
)
)
# Focussing on the Island Chains of India
<- india_states |>
andaman filter(
== "Andaman & Nicobar"
State_Name
)
<- ggplot(andaman) +
g1 geom_sf() +
labs(
title = "Base Map",
subtitle = "Andaman & Nicobar\nIslands (India)"
)
<- ggplot() +
g2 geom_sf(
data = andaman
+
) geom_sf(
data = st_centroid(andaman),
colour = "red",
size = 4,
pch = 1,
stroke = 2
+
) labs(
title = "st_centroid()",
subtitle = "Andaman & Nicobar\nIslands (India)"
)
<- ggplot() +
g3 geom_sf(
data = andaman
+
) geom_sf(
data = st_centroid(andaman, of_largest_polygon = TRUE),
colour = "red",
size = 4,
pch = 1,
stroke = 2,
fill = "transparent"
+
) labs(
title = "st_centroid\n(of_largest_polygon = TRUE)",
subtitle = "Andaman & Nicobar\nIslands (India)"
)
<- ggplot() +
g4 geom_sf(
data = andaman
+
) geom_sf(
data = st_point_on_surface(andaman),
colour = "red",
size = 4,
pch = 1,
stroke = 2,
fill = "transparent"
+
) labs(
title = "st_point_on_surface()",
subtitle = "Andaman & Nicobar\nIslands (India)"
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-2_1.png"),
plot = patchwork::wrap_plots(g1, g2, g3, g4, nrow = 1),
height = 1900,
width = 2400,
units = "px"
)
5.2.3 Buffers
- Buffers are polygons representing areas within a specified distance from a geometric feature (point, line, or polygon).
- Purpose: Used for geographic data analysis (not just visualization). Examples:
- How many points are within a given distance of a line?
- Which demographic groups are within travel range of a new shop?
st_buffer()
from the sf package. Example Visualization is shown in Figure 3- Input: Geometry and
dist
(distance in CRS units, e.g., meters). - Output: One buffer polygon per geometry feature.
- Input: Geometry and
- Other Key Arguments in
st_buffer()
:nQuadSegs
(GEOS engine):- Number of segments per quadrant (default: 30).
- Adjust: Decrease for memory concerns, or increase for high resolution output.
max_cells
(S2 engine):- Higher values create smoother buffers (slower computation).
endCapStyle
andjoinStyle
(GEOS engine):- Control buffer edge appearance (useful for lines).
singleSide
(GEOS engine):- Buffer on one or both sides of the geometry.
Code
<- andaman |>
a1 st_cast("POLYGON")
<- st_buffer(a1, dist = 20000) |>
a2 mutate(id = as_factor(row_number()))
<- st_buffer(a1, dist = 20000, nQuadSegs = 0.5) |>
a3 mutate(id = as_factor(row_number()))
<- ggplot() +
g1 geom_sf(data = a1) +
labs(
title = "Base Map with\nst_cast(\"POLYGON\")",
subtitle = "Nicobar Islands"
+
) coord_sf(
ylim = c(6.5, 9.5),
default_crs = 4326
)
<- ggplot() +
g2 geom_sf(
data = a2,
mapping = aes(fill = id),
alpha = 0.2,
colour = "transparent"
+
) geom_sf(
data = a1
+
) labs(
title = "With 20 km buffer\n around each island",
subtitle = "Nicobar Islands; each\nbuffer in separate colour"
+
) theme(legend.position = "none") +
coord_sf(
ylim = c(6.5, 9.5),
default_crs = 4326
)
<- ggplot() +
g3 geom_sf(
data = a3,
mapping = aes(fill = id),
alpha = 0.2,
colour = "transparent"
+
) geom_sf(
data = a1
+
) labs(
title = "With 20 km buffer\n around each island",
subtitle = "Nicobar Islands;\narugment (nQuadSegs = 1)"
+
) theme(legend.position = "none") +
coord_sf(
ylim = c(6.5, 9.5),
default_crs = 4326
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-3_1.png"),
plot = patchwork::wrap_plots(g1, g2, g3, nrow = 1),
height = 1200,
width = 2000,
units = "px"
)
5.2.4 Affine Transformations
- Definition: Transformations that preserve lines and parallelism but not necessarily angles or lengths.
- Types of Affine Transformations:
Shifting (Translation): Moves every point by a constant distance.
Example: Adding a vector to shift all y-coordinates north by 400 km distance while keeping x-coordinates unchanged using:
<- n1 |> n_shift add(c(0, 400000)) |> st_set_crs(st_crs(n1))
Note: This converts the CRS of the new sfc object to
NA
and thus needsst_set_crs()
to return it back to the original CRS.
Scaling: Enlarges or shrinks geometries.
- Global Scaling:
- Multiplies all coordinates relative to the origin, preserving topological relations.
- Local Scaling:
Scales geometries around specific points (e.g., centroids).
Steps:
- Shift geometries so the centroid becomes (0,0).
- Scale by a factor.
- Shift back to original centroid coordinates.
Example: Enlarge the geometries by a factor of 2.5.
<- (n1 - n1_centroid) |> n1_scale multiply_by(2.5) |> add(n1_centroid) |> st_set_crs(st_crs(n1))
- Global Scaling:
Rotation: Rotates coordinates using a rotation matrix.
- Rotation matrix: Define a function to create the rotation matrix and apply it to the geometry. R=[cosθ/sinθ − sinθ/cosθ]
- Replacing Old Geometry: Use
st_set_geometry()
from the sf package to finally Replace original geometry with scaled versions (shifted, rotated or scaled) - Applications:
- Shifting: For label placement.
- Scaling: In non-contiguous cartograms (see Section 9.6).
- Rotation: Correcting distorted geometries during re-projection.
Code
::font_add_google("Saira Extra Condensed", "caption_font")
sysfonts::showtext_auto()
showtext
::theme_set(
ggplot2theme_minimal(
base_size = 30,
base_family = "caption_font"
+
) theme(
text = element_text(
lineheight = 0.3,
hjust = 0.5
),plot.title.position = "plot",
plot.title = element_text(hjust = 1),
plot.subtitle = element_text(hjust = 1),
panel.grid = element_line(
linewidth = 0.2
)
)
)
<- andaman |>
df1 st_cast("POLYGON") |>
mutate(id = row_number()) |>
filter(id < 10) |>
mutate(
name = case_when(
%in% c(4,8, 9, 7) ~ "Nicobar Islands",
id .default = "Andaman Islands"
)
)
# Pull out only sfc class (i.e. geometry for Andaman Islands)
<- df1 |>
a1 filter(name == "Andaman Islands") |>
st_geometry()
# Pull out only sfc class (i.e. geometry for Nicobar Islands)
<- df1 |>
n1 filter(name == "Nicobar Islands") |>
st_geometry()
<- df1 |>
g1 ggplot(aes(fill = name)) +
geom_sf(colour = "transparent") +
geom_sf_text(aes(label = id)) +
coord_sf(
ylim = c(7, 13.5),
default_crs = 4326
+
) labs(
title = "Base Map",
subtitle = "10 Largest Islands amongst\nAndamand and Nicobar Island chain",
fill = NULL, x = NULL, y = NULL
+
) scale_fill_manual(values = c("#89973DFF", "#E8B92FFF")) +
theme(
legend.position = "left"
)
<- ggplot() +
g2 geom_sf(data = a1, fill = "#89973DFF", colour = "transparent") +
geom_sf(data = n1, fill = "#E8B92FFF", colour = "transparent") +
coord_sf(
ylim = c(7, 13.5),
default_crs = 4326
+
) labs(
title = "Plotting as separate\nsfc objects",
subtitle = "10 Largest Islands"
)
#################### Shifting #########################
<- n1 |>
n_shift add(c(0, 400000)) |>
st_set_crs(st_crs(n1))
<- ggplot() +
g3 geom_sf(data = a1, fill = "#89973DFF", colour = "transparent") +
geom_sf(
data = n_shift,
fill = "#E8B92FFF",
colour = "transparent"
+
) coord_sf(
ylim = c(7, 13.5),
default_crs = 4326
+
) labs(
title = "Shifting sfc objects",
subtitle = "Bring Nicobar Islands\ncloser to the Andamans"
)
#################### Scaling ##########################
<- st_centroid(n1)
n1_centroid
<- (n1 - n1_centroid) |>
n1_scale multiply_by(2.5) |>
add(n1_centroid) |>
st_set_crs(st_crs(n1))
<- ggplot() +
g4 geom_sf(data = a1, fill = "#89973DFF", colour = "transparent") +
geom_sf(
data = n1_scale,
fill = "#E8B92FFF",
colour = "transparent"
+
) coord_sf(
ylim = c(7, 13.5),
default_crs = 4326
+
) labs(
title = "Scaling sfc objects",
subtitle = "Enlarging Nicobar Islands\nby 2.5 times."
)
##################### Rotation ########################
= function(a){
rotation = a * pi / 180 #degrees to radians
r matrix(c(cos(r), sin(r), -sin(r), cos(r)), nrow = 2, ncol = 2)
}
<- (n1 - n1_centroid) |>
n1_rotate multiply_by(rotation(90)) |>
add(n1_centroid) |>
st_set_crs(st_crs(n1))
<- ggplot() +
g5 geom_sf(data = a1, fill = "#89973DFF", colour = "transparent") +
geom_sf(
data = n1_rotate,
fill = "#E8B92FFF",
colour = "transparent"
+
) coord_sf(
ylim = c(7, 13.5),
default_crs = 4326
+
) labs(
title = "Rotating sfc objects",
subtitle = "Rotating Nicobar Islands\nclockwise by 90 degrees"
)
<- patchwork::wrap_plots(g1, g3, g4, g5) +
g ::plot_layout(widths = c(1,1,1,1,1), nrow = 1)
patchwork
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-4_1.png"),
plot = g,
height = 1200,
width = 2500,
units = "px"
)
Data Viz demonstration
Here’s a more visually appealing version of the same graphic, produced using complete code given on this page.
5.2.5 Clipping
Definition: A form of spatial subsetting that modifies the geometry column of affected features. Applies to lines, polygons, and their multi equivalents (not points).
Purpose: Identifies or extracts areas of overlap or subsets of spatial features. Commonly used in geographic data analysis to focus on regions of interest.
Logical Operations and Spatial Equivalents. Inspired by Figure 12.1 of R for Data Science (2e). Spatial equivalents to logical operators (e.g., AND, OR, NOT) allow flexible geometry subsetting (as shown in Figure 4)
Intersection (AND):
st_intersection()
Union (OR):
st_union()
Difference (NOT):
st_difference()
Exclusive OR (XOR):
st_sym_difference()
Applications:
Identifying overlapping regions.
Creating subsets of spatial data for specific analysis or visualization.
Code
::font_add_google("Nova Mono", "body_font")
sysfonts::showtext_auto()
showtext
<- function(...) {
theme_custom ::theme_map(
ggthemesbase_size = 20,
base_family = "body_font"
+
) labs(
x = NULL, y = NULL
+
) theme(
plot.title = element_text(
hjust = 0.5,
lineheight = 0.3,
margin = margin(5,0,2,0, "mm")
),plot.margin = margin(0,0,0,0, "mm"),
...
)
}# Define the center points of the circles
<- tibble(
sf_circles x = c(0, 2, 4, 1, 3),
y = c(0, 0, 0, -1, -1),
colour_var = c("blue", "black", "red", "yellow", "green"),
label_var = LETTERS[1:5]
|>
) # Convert the points to an sf object
st_as_sf(
coords = c("x", "y"),
crs = NA
|>
) # Create circular geometries using st_buffer
mutate(geometry = st_buffer(geometry, dist = 1))
<- sf_circles |>
g1 ggplot(
mapping = aes(
label = label_var
)+
) geom_sf(
fill = "transparent",
linewidth = 0.5,
colour = "grey10"
+
) geom_sf_text(
colour = "grey10",
fontface = "bold",
size = 16,
family = "body_font"
+
) labs(
title = "5 overlapping circles plotted with {sf}"
+
) theme_custom()
# Naming the individual circles
<- function(ch_pick){
pull_a_circle |>
sf_circles filter(label_var == ch_pick)
}<- pull_a_circle("A")
a1 <- pull_a_circle("B")
b1 <- pull_a_circle("C")
c1 <- pull_a_circle("D")
d1 <- pull_a_circle("E")
e1
<- g1 +
g2 geom_sf(
data = a1 |> st_difference(d1),
fill = alpha("grey", 0.7)
+
) ggtitle("A |> st_difference(D)")
<- g1 +
g3 geom_sf(
data = d1 |> st_difference(a1),
fill = alpha("grey", 0.7)
+
) ggtitle("D |> st_difference(A)")
<- g1 +
g4 geom_sf(
data = d1 |> st_difference(a1) |> st_difference(b1),
fill = alpha("grey", 0.7)
+
) ggtitle("D |> st_difference(A) |>\nst_difference(B)")
<- g1 +
g5 geom_sf(
data = a1 |> st_union(d1),
fill = alpha("grey", 0.7)
+
) ggtitle("st_union(A, D)")
<- g1 +
g6 geom_sf(
data = a1 |> st_intersection(d1),
fill = alpha("grey", 0.7)
+
) ggtitle("st_intersection(A, D)")
<- g1 +
g7 geom_sf(
data = st_sym_difference(a1, d1),
fill = alpha("grey", 0.7)
+
) ggtitle("st_sym_difference(A, D)")
<- a1 |>
non_overlap st_sym_difference(d1) |>
st_sym_difference(b1) |>
st_sym_difference(c1) |>
st_sym_difference(e1)
<- g1 +
g8 geom_sf(
data = non_overlap,
fill = alpha("grey", 0.7)
+
) labs(title = "An st_sym_difference() chain")
<- a1 |>
overlap st_intersection(d1) |>
st_union(st_intersection(d1, b1)) |>
st_union(st_intersection(b1, e1)) |>
st_union(st_intersection(e1, c1))
<- g1 +
g9 geom_sf(
data = overlap,
fill = alpha("grey", 0.7)
+
) labs(title = "A st_union() and\nst_interaction() chain")
<- "
custom_layout_design AAAAAA
AAAAAA
BBCCDD
BBCCDD
EEFFGG
EEFFGG
HHHIII
HHHIII
"
<- patchwork::wrap_plots(
g
g1, g2, g3, g4, g5, g6, g7, g8, g9+
) ::plot_layout(
patchworkdesign = custom_layout_design
+
) ::plot_annotation(
patchworktitle = "Clipping {sf} objects\n(Boolean Algebra examples)",
subtitle = "Using functions like st_intersection(), st_union(), st_difference()\n& st_sym_difference()",
theme = theme(
plot.title = element_text(
family = "body_font",
size = 54,
lineheight = 0.3,
hjust = 0.5,
face = "bold"
),plot.subtitle = element_text(
family = "body_font",
size = 30,
lineheight = 0.3,
hjust = 0.5
)
)
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-5_1.png"),
plot = g,
height = 2000,
width = 1600,
units = "px",
bg = "white"
)
5.2.6 Sub-setting and Clipping
- Clipping: Modifies geometry to match a subsetting object. Subsetting: Selects features that intersect or partly intersect with a clipping object. An example: Points randomly distributed within the bounding box of the five concentric circles. Some points are inside one circle, some inside two circles, or neither. Then, we subset points intersecting with one, two or no circles.
- Key Functions:
st_sample()
: Generates random points within a geometry.- Clipping and Subsetting Approaches:
- Way #1: Use the intersection of
x
andy
(x_and_y
) as a direct subsetting object:p[x_and_y]
- Way #2: Find the intersection between points (
p
) andx_and_y
, modifying overlapping geometries:st_intersection(p, x_and_y)
, or, usingst_interesects()
when working in a pipe (|>
) chain. - Way #3: Use
st_intersects()
to determine logical overlap betweenp
and the subsetting objects:sel_p_xy = st_intersects(p, x, sparse = FALSE)[, 1] & st_intersects(p, y, sparse = FALSE)[, 1]
and then subset, usingp_xy3 = p[sel_p_xy]
- Way #1: Use the intersection of
- Preferred Implementation:
- Way #2 (concise and efficient) and it is the tidyverse approach with
|>
compatibility. Example shown in Figure 5
- Way #2 (concise and efficient) and it is the tidyverse approach with
Code
::font_add_google("Fira Sans Condensed", "body_font")
sysfonts::showtext_auto()
showtext
<- function(...) {
theme_custom ::theme_map(
ggthemesbase_size = 20,
base_family = "body_font"
+
) labs(
x = NULL, y = NULL
+
) theme(
plot.title = element_text(
hjust = 0.5,
lineheight = 0.3,
margin = margin(5,0,2,0, "mm")
),plot.margin = margin(0,0,0,0, "mm"),
...
)
}# Define the center points of the circles
<- tibble(
sf_circles x = c(0, 2, 4, 1, 3),
y = c(0, 0, 0, -1, -1),
colour_var = c("blue", "black", "red", "yellow", "green"),
label_var = LETTERS[1:5]
|>
) # Convert the points to an sf object
st_as_sf(
coords = c("x", "y"),
crs = NA
|>
) # Create circular geometries using st_buffer
mutate(geometry = st_buffer(geometry, dist = 1))
# Naming the individual circles
<- function(ch_pick){
pull_a_circle |>
sf_circles filter(label_var == ch_pick)
}<- pull_a_circle("A")
a1 <- pull_a_circle("B")
b1 <- pull_a_circle("C")
c1 <- pull_a_circle("D")
d1 <- pull_a_circle("E")
e1
<- a1 |>
one_circle st_sym_difference(d1) |>
st_sym_difference(b1) |>
st_sym_difference(c1) |>
st_sym_difference(e1)
<- a1 |>
overlap st_intersection(d1) |>
st_union(st_intersection(d1, b1)) |>
st_union(st_intersection(b1, e1)) |>
st_union(st_intersection(e1, c1))
rm(a1, b1, c1, d1, e1)
set.seed(42)
<- sf_circles |>
random_points # Get a bounding box
st_bbox() |>
# Covert it into a polygon
st_as_sfc() |>
# Get a sample of points within this polygon
st_sample(size = 100) |>
# Convert into a sf object
st_as_sf() |>
# Add identifiers for where the points fall
mutate(
colour_var = case_when(
st_intersects(x, overlap, sparse = F) ~ "Two Circles",
st_intersects(x, one_circle, sparse = F) ~ "One Circle",
.default = "Outside"
),colour_var = fct(
colour_var,levels = c(
"Outside",
"One Circle",
"Two Circles"
)
)
)
<- ggplot() +
g geom_sf(
data = sf_circles,
fill = "transparent",
linewidth = 0.2,
colour = "grey10"
+
) geom_sf(
data = random_points,
mapping = aes(
geometry = x,
colour = colour_var
),alpha = 0.75,
size = 0.7,
stroke = 0.1
+
) labs(
title = "Clipping and Subsetting",
subtitle = "Subsetting random points into those that overlap none, one or two circles",
colour = "Point lies within"
+
) ::scale_colour_paletteer_d("khroma::highcontrast") +
paletteer::theme_map(
ggthemesbase_family = "body_font",
base_size = 16
+
) theme(
plot.title = element_text(
size = 24,
hjust = 0.5,
margin = margin(0,0,0,0, "mm")
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.3,
margin = margin(0,0,0,0, "mm")
),legend.position = "inside",
legend.position.inside = c(0.5, 0),
legend.justification = c(0.5, 1),
legend.direction = "horizontal",
legend.text = element_text(
margin = margin(0,0,0,0, "mm")
),legend.title = element_text(
margin = margin(0,0,0,0, "mm"),
hjust = 0.5
),legend.margin = margin(0,0,0,0, "mm"),
legend.key.size = unit(5, "pt"),
legend.title.position = "top"
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-6_1.png"),
plot = g,
height = 500,
width = 800,
units = "px",
bg = "white"
)
5.2.7 Geometry Unions
- Spatial Aggregation and Geometry Dissolution:
- Spatial aggregation dissolves boundaries of touching polygons within the same group automatically. Example: Aggregating 22 districts of the State of Haryana into an overall boundary map with
aggregate()
(base R approach) orsummarize()
(tidyverse approach), for example in Figure 6 - Geometric Operation Behind the Scenes: Functions
aggregate()
andsummarize()
internally callst_union()
from the sf package to dissolve boundaries and merge geometries.
- Spatial aggregation dissolves boundaries of touching polygons within the same group automatically. Example: Aggregating 22 districts of the State of Haryana into an overall boundary map with
- Union of Geometries: Visualization Insight example is shown below.
Code
::font_add_google("Fira Sans Condensed", "body_font")
sysfonts::showtext_auto()
showtext
<- read_sf(here::here(
haryana1 "data", "haryana_map",
"HARYANA_DISTRICT_BDY.shp"
))
<- haryana1 |>
g1 ggplot() +
geom_sf(
aes(fill = District)
+
) ::theme_map(
ggthemesbase_family = "body_font"
+
) coord_sf(expand = FALSE) +
labs(
title = "Districts of Haryana State (India)",
subtitle = "The raw map, showing each district."
+
) theme(
plot.title = element_text(
hjust = 0.5
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.35
),legend.position = "none"
)
<- haryana1 |>
g2 st_union() |>
ggplot() +
geom_sf() +
::theme_map(
ggthemesbase_family = "body_font"
+
) coord_sf(expand = FALSE) +
labs(
title = "Boundary of Haryana State (India)",
subtitle = "Generated using sf::st_union()"
+
) theme(
plot.title = element_text(
hjust = 0.5
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.35
),legend.position = "none"
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-7_2.png"),
plot = (g1 + g2),
height = 400,
width = 600,
units = "px",
bg = "white"
)
Code
::font_add_google("Fira Sans Condensed", "body_font")
sysfonts::showtext_auto()
showtext
<- read_sf(
haryana ::here(
here"data", "haryana_map",
"HARYANA_SUBDISTRICT_BDY.shp"
)|>
) ::clean_names() |>
janitorst_simplify(dTolerance = 100) |>
mutate(
district = str_replace_all(district, ">", "A"),
district = str_replace_all(district, "\\|", "I"),
district = str_to_title(district),
tehsil = str_to_title(str_replace_all(tehsil, ">", "A"))
)
<- ggplot() +
g1 geom_sf(data = haryana, linewidth = 0.1) +
::theme_map(
ggthemesbase_family = "body_font"
+
) coord_sf(expand = FALSE) +
labs(
title = "Tehsils of Haryana State (India)",
subtitle = "The raw map, showing each tehsil /\nsubdivision of Haryana."
+
) theme(
plot.title = element_text(
hjust = 0.5
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.35
)
)
<- haryana |>
g2 group_by(district) |>
summarise() |>
ggplot() +
geom_sf(
linewidth = 0.1
+
) ::theme_map(
ggthemesbase_family = "body_font"
+
) coord_sf(expand = FALSE) +
labs(
title = "Districts of Haryana State (India)",
subtitle = "Aggregating tehsils with group_by(..) |> summarise()\nwhich uses st_union() at the backend."
+
) theme(
plot.title = element_text(
hjust = 0.5
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.35
)
)
<- haryana |>
g3 group_by(district) |>
summarise() |>
st_cast("LINESTRING") |>
ggplot() +
geom_sf(
linewidth = 0.1
+
) ::theme_map(
ggthemesbase_family = "body_font"
+
) coord_sf(expand = FALSE) +
labs(
title = "Casting geometry into LINESTRING",
subtitle = "group_by(district) |> summarise() |> st_cast(\"LINESTRING\").\nLoss of some geometries, but internal slivers are removed."
+
) theme(
plot.title = element_text(
hjust = 0.5
),plot.subtitle = element_text(
hjust = 0.5,
lineheight = 0.35
)
)
ggsave(
filename = here::here("book_solutions",
"images",
"chapter5-2-7_1.png"),
plot = (g1 + g2 + g3),
height = 500,
width = 900,
units = "px",
bg = "white"
)
When using the st_union()
function in spatial analysis, “inside small lines” or “slivers” appearing in the resulting combined geometry often indicates slight discrepancies in the input geometries, particularly at boundary points, causing the union operation to create small, extra line segments where the geometries nearly overlap but don’t perfectly align, as shown in Figure 7
summarise()
[which basically uses st_union()
only] produces an imperfect result with internal slivers, as the boundary nodes are not overlapping amongst the sub-districts / tehsils. This depicts an inherent (limitation? / caution?) of sf::st_union()
.