::opts_chunk$set(error = TRUE, message = TRUE) knitr
Packages
library(sf)
library(tmap)
library(dplyr)
library(wk)
::sf_use_s2(FALSE) sf
A simple square
Just to get things set up let’s make a simple square.
<- (st_polygon(list(matrix(c(-1, -1, 1, -1, 1, 1, -1, 1, -1, -1),
square 5, 2, byrow = TRUE))) * 0.5 + c(1, 0)) |>
st_sfc()
tm_shape(square) +
tm_borders(col = "red") +
tm_grid()
Simple transformations
In the code above, we made a polygon and multipled it by 0.5, then added c(1,0)
to it. This had the effect of scaling it by 0.5 andthen translating it by the vector \[\left[\begin{array}{c}1\\0\end{array}\right]\]
These unlikely looking operations are perfectly valid, although they feel a bit ‘off’.
Even more unlikely is that you can multiply an sf
object by a matrix…
<- pi / 6
ang <- matrix(c(cos(ang), -sin(ang),
mat sin(ang), cos(ang)), 2, 2, byrow = TRUE)
* mat) |>
(square plot()
This is very handy… but probably also a bad idea! Because you have to post-multiply by the matrix, the sense of many affine transformations is reversed and construction of the matrix is not ‘by the book’. Usually the affine transformation matrix \(\mathbf{A}\) for an anti-clockwise rotation by angle \(\theta\) around the origin, would be
\[ \mathbf{A} = \left[\begin{array}{cc} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{array}\right] \]
Here, because we are post-multiplying the rotation will be in the other direction… and to rotate anti-clockwise, you use the \(-\mathbf{A}=\mathbf{A}^T\)
\[ -\mathbf{A} = \left[\begin{array}{cc} -\cos\theta & \sin\theta \\ -\sin\theta & -\cos\theta \end{array}\right] = \left[\begin{array}{cc} \cos\theta & \sin\theta \\ -\sin\theta & \cos\theta \end{array}\right] = \mathbf{A}^\mathrm{T} \]
This means that if you are doing any serious affine transforming of sf
shapes at a low-level in R spatial, I recommend either writing some wrapper functions that generate and apply the necessary matrices on the fly, or, probably better yet, using the wk
package which has proper support for affine transformations.
Wrapper functions for the ‘native’ matrix operations
Taking the wk
approach, I will show what you can do below. Making similar functions that just post-multiply shapes or add vectors to them instead is left as an exercise for the reader…
For example a rotation function might look something like
<- function(shp, angle) {
rotate_sf ::wk_transform(shp, wk::wk_affine_rotate(angle))
wk }
and this can be applied like this
<- st_polygon(list(matrix(c(.25, 0.25,
base_s 1.5, 0.25,
1.5, 1.5,
25, 1.5,
.25, .25),
.nrow = 5, ncol = 2, byrow = TRUE)))
plot(base_s, xlim = c(-2, 2), ylim = c(-2, 2),
col = "lightgrey", border = NA)
for (a in seq(0, 330, 30)) {
plot(rotate_sf(base_s, a), add = TRUE)
}
Or you might want to make multiple copies of a basic unit at a series of locations on a grid. First, make a function that will translate a shape by a vector.
<- function(shape, translation) {
translate_shape ::wk_transform(shape, wk::wk_affine_translate(translation[1], translation[2]))
wk }
Generate a set of translations
<- expand.grid(x = 0:19 * 1.2 + 1, y = 0:19 * 1.2 + 1)
grid <- list()
squares for (i in seq(nrow(grid))) {
<- append(squares,
squares list(translate_shape(square, c(grid$x[i], grid$y[i]))))
}|> sapply("[") |> st_sfc() |>
squares plot()
The wk
functions also allow you to compose complex transformations from several steps. For example a function to rotate a shape around its own centre, i.e., not around the origin at \((0,0)\) requires moving the shape so that its centroid is at the origin, performing the rotation, then moving it back:
<- function(shape, angle) {
rotate_around_centroid <- st_centroid(shape) |>
centroid st_coordinates() |>
c()
<- wk::wk_affine_compose(
transformation ::wk_affine_translate(-centroid[1], -centroid[2]),
wk::wk_affine_rotate(angle),
wk::wk_affine_translate(centroid[1], centroid[2])
wk
)::wk_transform(shape, transformation)
wk }
And here’s that in action
plot(square, xlim = c(-1, 1), ylim = c(-1, 1))
for (a in seq(30, 90, 30)) {
plot(rotate_around_centroid(square, a), add = TRUE)
}