--- title: "geometries" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{geometries} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#" ) ``` ```{r setup } library(geometries) library(Rcpp) ``` ### Header ``` #include "geometries/geometries.hpp" ``` ### Functions ``` SEXP make_geometries() ``` ## data as geometries When one thinks of geometries in `R`, one of the most common data structures is the matrix. For example, in the `sf` world, an POINT is a single-row matrix (i.e, a vector) ``` sf::st_point( 1:2 ) POINT (1 2) ``` A LINESTRING is a matrix ``` sf::st_linestring( matrix( c(1,1,1,2,2,2,2,1,1,1), ncol = 2, byrow = T) ) LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1) ``` and a POLYGON is a list of matrices ``` sf::st_polygon( list( matrix( c(1,1,1,2,2,2,2,1,1,1), ncol = 2, byrow = T) ) ) POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)) ``` And to group these into a **collection** you would put each geometry inside a list ``` sf::st_sfc( list( sf::st_linestring( matrix( c(1,1,1,2,2,2,2,1,1,1), ncol = 2, byrow = T) ) , sf::st_polygon( list( matrix( c(1,1,1,2,2,2,2,1,1,1), ncol = 2, byrow = T) ) ) ) ) Geometry set for 2 features geometry type: GEOMETRY dimension: XY bbox: xmin: 1 ymin: 1 xmax: 2 ymax: 2 CRS: NA LINESTRING (1 1, 1 2, 2 2, 2 1, 1 1) POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)) ``` --- From my limited research (i.e., practically none), I estimate most users will have a `data.frame` and will want to convert it into a collection of geometries. For example, take a `data.frame` of `x` and `y` coordinates, and two `id` columns. ```{r} df <- data.frame( id1 = c( rep(1,12), rep(2, 12) ) , id2 = c( rep(1:4, each = 3), rep(1:4, each = 3) ) , x = 1:24 , y = 24:1 ) df ``` You can think of the ID columns in this example as - `id1` defines a polygon - `id2` is each ring inside the polygon Calling `geometries::make_geometries()` will split this data.frame by these ID columns and put the resulting matrices inside list elements. ```r cppFunction( depends = "geometries" , includes = '#include "geometries/geometries.hpp"' , code = ' SEXP my_shape( SEXP df, SEXP id_cols, SEXP geometry_cols ) { return geometries::make_geometries( df, id_cols, geometry_cols ); } ' , plugins = "cpp11" ) my_shape( df, c(0L,1L), c(2L,3L) ) # [[1]] # [[1]][[1]] # [,1] [,2] # [1,] 1 24 # [2,] 2 23 # [3,] 3 22 # # [[1]][[2]] # [,1] [,2] # [1,] 4 21 # [2,] 5 20 # [3,] 6 19 # # [[1]][[3]] # [,1] [,2] # [1,] 7 18 # [2,] 8 17 # [3,] 9 16 # # [[1]][[4]] # [,1] [,2] # [1,] 10 15 # [2,] 11 14 # [3,] 12 13 # # # [[2]] # [[2]][[1]] # [,1] [,2] # [1,] 13 12 # [2,] 14 11 # [3,] 15 10 # # [[2]][[2]] # [,1] [,2] # [1,] 16 9 # [2,] 17 8 # [3,] 18 7 # # [[2]][[3]] # [,1] [,2] # [1,] 19 6 # [2,] 20 5 # [3,] 21 4 # # [[2]][[4]] # [,1] [,2] # [1,] 22 3 # [2,] 23 2 # [3,] 24 1 ``` Notice here there are no class attributes on the shapes. In `{geometries}` I only want to provide the tools to build these structures, then each user can define what they mean. For example, if you want to define a class for each geometry you can supply a list containing a "class" vector as the 4th argument ```r cppFunction( depends = "geometries" , includes = '#include "geometries/geometries.hpp"' , code = ' SEXP my_shape( Rcpp::DataFrame df, Rcpp::IntegerVector id_cols, Rcpp::IntegerVector geometry_cols, Rcpp::List class_attributes ) { return geometries::make_geometries( df, id_cols, geometry_cols, class_attributes ); } ' , plugins = "cpp11" ) my_shape( df, c(0,1), c(2,3), list(class = "my_polygon") ) # [[1]] # [[1]] # [,1] [,2] # [1,] 1 24 # [2,] 2 23 # [3,] 3 22 # # [[2]] # [,1] [,2] # [1,] 4 21 # [2,] 5 20 # [3,] 6 19 # # [[3]] # [,1] [,2] # [1,] 7 18 # [2,] 8 17 # [3,] 9 16 # # [[4]] # [,1] [,2] # [1,] 10 15 # [2,] 11 14 # [3,] 12 13 # # attr(,"class") # [1] "my_polygon" # # [[2]] # [[1]] # [,1] [,2] # [1,] 13 12 # [2,] 14 11 # [3,] 15 10 # # [[2]] # [,1] [,2] # [1,] 16 9 # [2,] 17 8 # [3,] 18 7 # # [[3]] # [,1] [,2] # [1,] 19 6 # [2,] 20 5 # [3,] 21 4 # # [[4]] # [,1] [,2] # [1,] 22 3 # [2,] 23 2 # [3,] 24 1 # # attr(,"class") # [1] "my_polygon" ``` Notice here that each list element now has a `"my_polygon"` class. And if you have `library(sf)` loaded, setting the class as `sfg` `POLYGON`, you should see each element printed in the usual `sf` way ``` library(sf) my_shape( df, c(0,1), c(2,3), list( class = c("XY", "POLYGON","sfg") ) ) # [[1]] # POLYGON ((1 24, 2 23, 3 22), (4 21, 5 20, 6 19), (7 18, 8 17, 9 16), (10 15, 11 14, 12 13)) # # [[2]] # POLYGON ((13 12, 14 11, 15 10), (16 9, 17 8, 18 7), (19 6, 20 5, 21 4), (22 3, 23 2, 24 1)) ``` You can use this function to define any shape you want. The number of `id` columns you supply will determine how deeply nested the matrices are. If I add two more `id` columns, this will nest each matrix 2-levels deeper ```r df$id0 <- 1 df$id00 <- 1 head( df ) my_shape( df, c(0,1,4,5), c(2,3), list(class = "my_new_shape") ) # [[1]] # [[1]] # [[1]][[1]] # [[1]][[1]][[1]] # [,1] [,2] # [1,] 1 24 # [2,] 2 23 # [3,] 3 22 # # # # [[2]] # [[2]][[1]] # [[2]][[1]][[1]] # [,1] [,2] # [1,] 4 21 # [2,] 5 20 # [3,] 6 19 # # # # [[3]] # [[3]][[1]] # [[3]][[1]][[1]] # [,1] [,2] # [1,] 7 18 # [2,] 8 17 # [3,] 9 16 # # # # [[4]] # [[4]][[1]] # [[4]][[1]][[1]] # [,1] [,2] # [1,] 10 15 # [2,] 11 14 # [3,] 12 13 # # # # attr(,"class") # [1] "my_new_shape" # # [[2]] # [[1]] # [[1]][[1]] # [[1]][[1]][[1]] # [,1] [,2] # [1,] 13 12 # [2,] 14 11 # [3,] 15 10 # # # # [[2]] # [[2]][[1]] # [[2]][[1]][[1]] # [,1] [,2] # [1,] 16 9 # [2,] 17 8 # [3,] 18 7 # # # # [[3]] # [[3]][[1]] # [[3]][[1]][[1]] # [,1] [,2] # [1,] 19 6 # [2,] 20 5 # [3,] 21 4 # # # # [[4]] # [[4]][[1]] # [[4]][[1]][[1]] # [,1] [,2] # [1,] 22 3 # [2,] 23 2 # [3,] 24 1 # # # # attr(,"class") # [1] "my_new_shape" ```