This post introduces a user-defined function for casting sf objects of class LINESTRING or POLYGON into substrings.
Required R packages
library(sf)
library(rnaturalearth)
library(dplyr)
The problem
The sf package includes st_cast, a powerful function that transforms geometries into other types of geometries (i.e. LINESTRING to POLYGON, etc.).
italy <- ne_countries(country = "italy", returnclass = "sf")
italy_pol <- italy %>% st_cast("POLYGON")
italy_lin <- italy_pol %>% st_cast("LINESTRING")
italy_pt <- italy_lin %>% st_cast("POINT")
par(mfrow = c(2, 2), mar = c(1, 1, 1, 1), bg = NA)
plot(st_geometry(italy), col = c("red", "yellow", "blue"), main = "MULTIPOLYGON")
plot(st_geometry(italy_pol), col = c("red", "yellow", "blue"), main = "POLYGON")
plot(st_geometry(italy_lin), col = c("red", "yellow", "blue"), main = "LINE")
plot(st_geometry(italy_pt), col = c("red", "yellow", "blue"), main = "POINT")
What I missed when using st_cast was the possibility of breaking LINESTRING objects into subsegments:
An approach
One possible solution is to create LINESTRING objects for each consecutive pair of POINT objects across the original geometry. Let’s check it:
par(mfrow = c(1, 2), mar = c(1, 1, 1, 1))
test <- ne_countries(country = "spain", returnclass = "sf") %>%
st_cast("POLYGON") %>%
st_cast("LINESTRING")
plot(st_geometry(test), col = c("red", "yellow", "blue"), main = "LINESTRING")
geom <- lapply(
1:(length(st_coordinates(test)[, 1]) - 1),
function(i) {
rbind(
as.numeric(st_coordinates(test)[i, 1:2]),
as.numeric(st_coordinates(test)[i + 1, 1:2])
)
}
) %>%
st_multilinestring() %>%
st_sfc(crs = st_crs(test)) %>%
st_cast("LINESTRING")
plot(st_geometry(geom), col = c("red", "yellow", "blue"), main = "AFTER FUNCTION")
The function stdh_cast_substring
Finally, I wrapped the solution into a function and extended it a little bit:
When the input is not a
LINESTRINGor aPOLYGON, it returns an error and stops.The function accepts sf objects with several rows or
sfcobjects with several geometries, and returns the same class as the input. In the case of sf objects, the inputdata.frameis added.By default, the output is a
MULTILINESTRINGgeometry. This has the benefit that the output has the same number of geometries as the input. This can be modified by setting thetoparameter toLINESTRING, which only casts theMULTILINESTRINGobject intoLINESTRING.
stdh_cast_substring <- function(x, to = "MULTILINESTRING") {
ggg <- st_geometry(x)
if (!unique(st_geometry_type(ggg)) %in% c("POLYGON", "LINESTRING")) {
stop("Input should be LINESTRING or POLYGON")
}
for (k in 1:length(st_geometry(ggg))) {
sub <- ggg[k]
geom <- lapply(
1:(length(st_coordinates(sub)[, 1]) - 1),
function(i) {
rbind(
as.numeric(st_coordinates(sub)[i, 1:2]),
as.numeric(st_coordinates(sub)[i + 1, 1:2])
)
}
) %>%
st_multilinestring() %>%
st_sfc()
if (k == 1) {
endgeom <- geom
}
else {
endgeom <- rbind(endgeom, geom)
}
}
endgeom <- endgeom %>% st_sfc(crs = st_crs(x))
if (class(x)[1] == "sf") {
endgeom <- st_set_geometry(x, endgeom)
}
if (to == "LINESTRING") {
endgeom <- endgeom %>% st_cast("LINESTRING")
}
return(endgeom)
}
The function could be improved in terms of performance. Given that it works at the coordinate level, high-resolution objects introduce some delay.
test100 <- ne_countries(
continent = "south america",
returnclass = "sf"
) %>%
st_cast("POLYGON")
test50 <- ne_countries(50,
continent = "south america",
returnclass = "sf"
) %>%
st_cast("POLYGON")
init <- Sys.time()
t1 <- stdh_cast_substring(test100, "LINESTRING")
end <- Sys.time()
kable(end - init, format = "markdown")
| x |
|---|
| 0.1729319 secs |
init <- Sys.time()
t2 <- stdh_cast_substring(test50, "LINESTRING")
end <- Sys.time()
kable(end - init, format = "markdown")
| x |
|---|
| 2.288558 secs |
par(mfrow = c(1, 1), mar = c(0, 0, 0, 0))
plot(st_geometry(test50), col = NA, bg = "#C6ECFF")
plot(st_geometry(ne_countries(50, returnclass = "sf")), col = "#F6E1B9", border = "#646464", add = T)
plot(st_geometry(test50), col = "#FEFEE9", border = "#646464", add = T)
plot(st_geometry(t2), col = c("red", "yellow", "blue"), add = T, lwd = 0.5)
There is a difference in performance: test100 has 15 polygons decomposed into 914 substrings, while test50 has 80 polygons decomposed into 8,414 substrings. In that sense, the original st_cast is much faster, although this solution may work well in most cases.




Top comments (0)