Thanks to active developer and user communities, R is becoming an increasingly popular language for interactive geographic data analysis. Large steps forward include the release and continued evolution of sf, which provides classes for representing and manipulating spatial vector data (points, lines and polygons). Unlike its predecessors, `sf`

is compatible with the popular data science oriented packages that form the tidyverse, most notably the data manipulation toolbox `dplyr`

, and more generally with the concept of tidy data.

R is also well-suited for network analysis, thanks to the R interface of the igraph library. The tidygraph package extends `igraph`

into the domain of the `tidyverse`

, enabling `dplyr`

compatibility by treating a graph as a collection of two tidy data frames describing respectively the nodes and edges.

Given the strength of R in spatial *and* network analysis, one would expect it to be an ideal language for analysis of geospatial networks. Geospatial networks are simultaneously graph and spatial objects, with nodes and edges embedded in geographic space. Well known examples include transport networks, river basins, power grids, ecological networks and geolocated social networks.

Although several R packages exist that address geospatial networks, they often focus on a specific application within the broader domain of geospatial network analysis, or complicate `tidyverse`

compatibility, for example by using S4 classes or relying on the older `sp`

package for spatial data handling. Hence, at present, no generally applicable, `tidyverse`

compatible classes exist for geospatial network data, representing a gap in R’s spatial and network analysis ecosystems (see the gRaphical Models and Spatial task views). `sfnetworks`

is created to fill this gap, and form a connecting edge between the two worlds.

The core of the package is a data structure that can be provided directly to both graph analytical functions of `tidygraph`

and spatial analytical functions of `sf`

. However, there might be problems that are so specific to spatial networks, that they can neither be solved by `tidygraph`

nor `sf`

. Therefore, `sfnetworks`

will contain several extensions to the current offer of the two “parent packages”. Think for example of a function that can snap spatial points to the edges of a network. We want to prevent that `sfnetworks`

goes too much into a single application - it should remain a lower-level package - but basic, widely used spatial network operations should be supported. See the extensions vignette for an explanation of the currently implemented extensions.

The `sfnetworks`

package is still in very active development, and this vignette only describes the first part of our work. Therefore, the package is also not on CRAN yet. Install it from GitHub using the `remotes`

package in R.

`remotes::install_github("luukvdmeer/sfnetworks")`

The core of the `sfnetworks`

package is the `sfnetwork`

data structure. This is a class that subclasses tidygraph’s `tbl_graph`

, which itself subclasses `igraph`

. Therefore, `sfnetwork`

objects are recognized by all network analysis algorithms that `igraph`

offers, as well as by the tidy wrappers that `tidygraph`

has built around them.

The philosophy of a `tbl_graph`

is best described by the following paragraph from the tidygraph introduction: “Relational data cannot in any meaningful way be encoded as a single tidy data frame. On the other hand, both node and edge data by itself fits very well within the tidy concept as each node and edge is, in a sense, a single observation. Thus, a close approximation of tidyness for relational data is two tidy data frames, one describing the node data and one describing the edge data.”

Since `sfnetwork`

subclasses `tbl_graph`

, it shares the same philosophy. However, it extends it into the domain of geospatial data analysis, where each observation has a location in geographical space. For that, it brings `sf`

into the game. An `sf`

object stores the geographical coordinates of each observation in standardized format in a geometry list column, which has a Coordinate Reference System associated with it. Thus, in `sfnetworks`

, we re-formulate the last sentence of the paragraph above to the following. “A close approximation of tidyness for relational *geospatial data* is two *sf objects*, one describing the node data and one describing the edge data.”

We do need to make a note here. In a geospatial network, the nodes *always* have coordinates in geographic space, and thus, are always described by an sf object. The edges, however, can also be described by just the indices of its end-nodes. This still makes them geospatial, because they start and end at specific points in space, but the spatial information is not *explicitly* attached to them. Both representations can be useful. In road networks, for example, it makes sense to explicitly draw a line geometry between two nodes, while in geolocated social networks, it probably does not. An `sfnetwork`

supports both types. It can either have edges with a geometry stored in a geometry list column, described by an sf object, or edges that only refer to node indices, described by a regular data frame. We refer to these types of edges as *spatially explicit edges* and *spatially implicit edges* respectively. In this vignette, we mainly focus on the first type.

An `sfnetwork`

object can be constructed with the `sfnetwork()`

function. This function expects the nodes and edges data as arguments, meeting the following requirements:

- The nodes should be an object of class
`sf`

, solely containing features with`POINT`

geometries. - When creating a network with spatially explicit edges, the edges should be an object of class
`sf`

, solely containing features with`LINESTRING`

geometries. Otherwise, they can be a`data.frame`

or`tibble`

. In any case, they should contain the indices of their end-nodes in a to and from column, or in the two first columns, as integers. Every node index refers to the*position*(i.e. the rownumber) of the node in the nodes table. - When creating a network with spatially explicit edges, the coordinates of the boundary points of the edge linestrings should match with the coordinates of the respective end-nodes. Otherwise, the network structure is not valid. Note that this also depends on the precision of the coordinates.
- When creating a network with spatially explicit edges, the two provided sf objects should have the same CRS.

Additionally, one can set the `directed`

argument, defining if the created network should be directed or not. This defaults to `TRUE`

. The `edges_to_lines`

argument defines if the edges of the resulting network should be spatially explicit or not.

See below a small toy example.

```
p1 = st_point(c(7, 51))
p2 = st_point(c(7, 52))
p3 = st_point(c(8, 52))
nodes = st_as_sf(st_sfc(p1, p2, p3, crs = 4326))
e1 = st_cast(st_union(p1,p2), "LINESTRING")
e2 = st_cast(st_union(p1,p3), "LINESTRING")
e3 = st_cast(st_union(p2,p3), "LINESTRING")
edges = st_as_sf(st_sfc(e1, e2, e3, crs = 4326))
edges$from = c(1, 1, 2)
edges$to = c(2, 3, 3)
sfnetwork(nodes, edges, directed = FALSE)
#> Checking validity of network structure... Use force=TRUE to force construction without checks
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 3 nodes and 3 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # An undirected simple graph with 1 component with spatially explicit edges
#> #
#> # Node Data: 3 x 1 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7 ymin: 51 xmax: 8 ymax: 52
#> x
#> <POINT [°]>
#> 1 (7 51)
#> 2 (7 52)
#> 3 (8 52)
#> #
#> # Edge Data: 3 x 3
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7 ymin: 51 xmax: 8 ymax: 52
#> from to x
#> <int> <int> <LINESTRING [°]>
#> 1 1 2 (7 51, 7 52)
#> 2 1 3 (7 51, 8 52)
#> 3 2 3 (7 52, 8 52)
```

Converting foreign objects into an `sfnetwork`

can be done with the `as_sfnetwork()`

function. Every object that fulfills both of the following requirements is supported by default:

- The object is a
`tbl_graph`

or convertible to a`tbl_graph`

with`as_tbl_graph()`

. See the tidygraph documentation for all objects that are supported. - At least the nodes are convertible to an
`sf`

object with`st_as_sf()`

.

Additionally, `as_sfnetwork()`

has a method that directly converts an `sf`

object into a network. Of course, a single `sf`

object does not contain any explicit network information, so assumptions have to be made. This functionality should therefore be seen as a convenience function, and will not be suited for all possible use-cases. It works as follows:

- When given an
`sf`

object with`LINESTRING`

geometries: the lines are considered edges, and nodes are created at the endpoints of the lines. Identical endpoints become one single node. This addresses the use-case that was presented in this r-spatial blogpost. - When given an
`sf`

object with`POINT`

geometries: the points are considered nodes, and edges are created between them, in a sequential way. With that we mean: node 1 will be connected to node 2, node 2 to node 3, et cetera. This addresses the use-case that was presented in this stackoverflow thread. More construction algorithms for point data are planned to be implemented, see here.

See below an example with the Roxel dataset (which comes with `sfnetworks`

and is a cleaned extract of OpenStreetMap roads in Münster-Roxel).

```
roxel
#> Simple feature collection with 851 features and 2 fields
#> geometry type: LINESTRING
#> dimension: XY
#> bbox: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geographic CRS: WGS 84
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # A tibble: 851 x 3
#> name type geometry
#> <fct> <fct> <LINESTRING [°]>
#> 1 Havixbecker S… residen… (7.533722 51.95556, 7.533461 51.95576)
#> 2 Pienersallee seconda… (7.532442 51.95422, 7.53236 51.95377, 7.53209 51.953…
#> 3 Schulte-Bernd… residen… (7.532709 51.95209, 7.532823 51.95239, 7.532869 51.9…
#> 4 NA path (7.540063 51.94468, 7.539696 51.94479, 7.539466 51.9…
#> 5 Welsingheide residen… (7.537673 51.9475, 7.537614 51.94562)
#> 6 NA footway (7.543791 51.94733, 7.54369 51.94686, 7.543751 51.94…
#> 7 NA footway (7.54012 51.94478, 7.539931 51.94514)
#> 8 NA path (7.53822 51.94546, 7.538131 51.94549, 7.538027 51.94…
#> 9 NA track (7.540063 51.94468, 7.540338 51.94468, 7.540591 51.9…
#> 10 NA track (7.5424 51.94599, 7.54205 51.94629, 7.541967 51.9463…
#> # … with 841 more rows
```

```
(net = as_sfnetwork(roxel, directed = FALSE))
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # An undirected multigraph with 14 components with spatially explicit edges
#> #
#> # Node Data: 701 x 1 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7.522622 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geometry
#> <POINT [°]>
#> 1 (7.533722 51.95556)
#> 2 (7.533461 51.95576)
#> 3 (7.532442 51.95422)
#> 4 (7.53209 51.95328)
#> 5 (7.532709 51.95209)
#> 6 (7.532869 51.95257)
#> # … with 695 more rows
#> #
#> # Edge Data: 851 x 5
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [°]>
#> 1 1 2 Havixbecker S… residen… (7.533722 51.95556, 7.533461 51.95576)
#> 2 3 4 Pienersallee seconda… (7.532442 51.95422, 7.53236 51.95377, 7.5…
#> 3 5 6 Schulte-Bernd… residen… (7.532709 51.95209, 7.532823 51.95239, 7.…
#> # … with 848 more rows
```

```
class(net)
#> [1] "sfnetwork" "tbl_graph" "igraph"
```

More methods for `as_sfnetwork`

exists, such that other spatial network objects that exist in R can be directly converted into an `sfnetwork`

. These includes `SpatialLinesNetwork`

from the `stplanr`

package and `linnet`

from the `spatstat`

package.

Just as with a `tbl_graph`

, one of the graph elements of an `sfnetwork`

(i.e. nodes or edges) can be activated, and all subsequent computations will be performed on that active element. By default, the nodes are the active element. This can be changed with the `activate()`

verb, that is borrowed from `tidygraph`

.

Thanks to `activate()`

, a lot of data manipulation tools can be directly applied to the network. If you want to do something that goes further, you can always extract the active element as an `sf`

object, simply by calling `st_as_sf()`

- in the case of spatially implicit edges, extract them with `as_tibble()`

instead.

```
net %>%
activate("edges") %>%
st_as_sf()
#> Simple feature collection with 851 features and 4 fields
#> geometry type: LINESTRING
#> dimension: XY
#> bbox: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geographic CRS: WGS 84
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # A tibble: 851 x 5
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [°]>
#> 1 1 2 Havixbecker… reside… (7.533722 51.95556, 7.533461 51.95576)
#> 2 3 4 Pienersallee second… (7.532442 51.95422, 7.53236 51.95377, 7.532…
#> 3 5 6 Schulte-Ber… reside… (7.532709 51.95209, 7.532823 51.95239, 7.53…
#> 4 7 8 NA path (7.540063 51.94468, 7.539696 51.94479, 7.53…
#> 5 9 10 Welsingheide reside… (7.537673 51.9475, 7.537614 51.94562)
#> 6 11 12 NA footway (7.543791 51.94733, 7.54369 51.94686, 7.543…
#> 7 13 14 NA footway (7.54012 51.94478, 7.539931 51.94514)
#> 8 8 10 NA path (7.53822 51.94546, 7.538131 51.94549, 7.538…
#> 9 7 15 NA track (7.540063 51.94468, 7.540338 51.94468, 7.54…
#> 10 16 17 NA track (7.5424 51.94599, 7.54205 51.94629, 7.54196…
#> # … with 841 more rows
```

The `sfnetworks`

package does not (yet?) include advanced visualization options. However, a simple plot method is provided, which gives a quick view of how the network looks like.

For advanced visualization, we encourage to extract nodes and edges as `sf`

objects, and use one of the many ways to map those in R, either statically or interactively. Think of sf’s default plot method, ggplots `geom_sf()`

, `tmap`

, `mapview`

, et cetera.

*Note: it would be great to see this change in the future, for example by good integration with ggraph. Contributions are very welcome regarding this!*

Since they subclass `tbl_graph`

, `sfnetwork`

objects are recognized by all network analysis algorithms that `igraph`

offers, as well as by the tidy wrappers that `tidygraph`

has built around them*.

A small example is the calculation of the betweenness centrality for each node:

```
library(ggplot2)
net_c = net %>%
activate("nodes") %>%
mutate(bc = centrality_betweenness())
ggplot() +
geom_sf(data = activate(net_c, "edges") %>% st_as_sf(), col = 'grey50') +
geom_sf(data = activate(net_c, "nodes") %>% st_as_sf(), aes(col = bc, size = bc))
```

There is a large amount of available algorithms, ranging from simple type queries to advanced community detection. To keep this vignette within an acceptable length, we will not cover all graph algorithms that can be used, but instead refer to the tidygraph introduction.

*Note: due to the internal handling of nodes and edges as sf objects with a sticky geometry list column, there are still some tidygraph and igraph functions that result in errors when applying them to sfnetwork objects. In that case, you can try to first run as_tbl_graph(), and then apply the algorithm.*

Not only can `sfnetwork`

objects be used in graph algorithms, they are also recognized by various spatial analytical functions of `sf`

. Internally, this works as follows: the active element is converted to an `sf`

object, on which the function is applied. Depending on the function, the results are merged back into the network, and an `sfnetwork`

object is returned, or simply the raw output of the sf function is returned.

Some of the functions have effects also outside of the active element: when nodes are removed from the network, the edges terminating at those nodes will be removed too. This behavior is *not* symmetric: when removing edges, the nodes that are at the start or end of those edges remain, even if they are not adjacent to any other edge. This is because in graph theory, edges can never exist without end-nodes, while nodes can exist without having adjacent edges.

All supported `sf`

functions that are of the form `f(x,y)`

also accept an `sfnetwork`

as `y`

parameter. Just as for object `x`

, the active element of the network `y`

will internally be converted to an `sf`

object, and used as such inside the function.

Networks can be filtered by a spatial predicate, using `st_filter`

. For example, we want to only keep those nodes that intersect with a given polygon:

```
p1 = st_point(c(7.53173, 51.95662))
p2 = st_point(c(7.53173, 51.95190))
p3 = st_point(c(7.53778, 51.95190))
p4 = st_point(c(7.53778, 51.95662))
rect = st_multipoint(c(p1, p2, p3, p4)) %>%
st_cast('POLYGON') %>%
st_sfc(crs = 4326)
net_f = net %>%
st_filter(rect, .pred = st_intersects)
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar
plot(net)
plot(rect, border = "Red", lwd = 2, add = TRUE)
plot(net_f)
```

Additional attributes can be joined spatially into the network with `st_join`

, which is also based on spatial predicates. By default, this join is a left join, meaning that all features (i.e. nodes or edges) of `x`

remain. If changed to an inner join (by adding `left = FALSE`

), features get removed if they don’t have a match with any row in `y`

.

The `sfnetwork`

method for `st_join`

has an important restriction. When the nodes are the active element of the network, only joins where there is at most one match per feature are allowed. Allowing multiple matches is a problem when joining on the nodes. For example, if node 1 in `x`

has two matches in `y`

, this creates two rows for node 1 in the resulting object. Firstly, this messes up the network structure: if the end-nodes of edge A where node 1 and node 2, these indices now suddenly refer to the same node, while the original node 2 becomes node 3. Secondly, if we manage to update the `from`

and `to`

columns of the edges accordingly, should edge A go from the new node 1 to the new node 3, or from the new node 2 to the new node 3? In the future we might remove the restriction, but only after we decide on a good way to deal with multiple matches per node.

For the edges this problem does not exist, because the same nodes can be connected by multiple edges. Multiple matches will cause edges to be duplicated, which might not be so useful, but at least it does not break the network structure.

An example of a spatial join:

```
net_a = net %>% slice(1:3)
net_b = net %>% slice(2:4) %>% mutate(foo = "bar")
net_a %>%
st_join(net_b, join = st_equals)
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 3 nodes and 1 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # An unrooted forest with 2 trees with spatially explicit edges
#> #
#> # Node Data: 3 x 2 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7.532442 ymin: 51.95422 xmax: 7.533722 ymax: 51.95576
#> foo geometry
#> <chr> <POINT [°]>
#> 1 NA (7.533722 51.95556)
#> 2 bar (7.533461 51.95576)
#> 3 bar (7.532442 51.95422)
#> #
#> # Edge Data: 1 x 5
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7.533461 ymin: 51.95556 xmax: 7.533722 ymax: 51.95576
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [°]>
#> 1 1 2 Havixbecker Stras… residenti… (7.533722 51.95556, 7.533461 51.955…
```

Spatial predicates are very useful for filtering and joining, but can also directly be applied to an `sfnetwork`

. This allows for example to create new attribute columns based on such predicates.

```
net %>%
mutate(in_rect = lengths(st_intersects(., rect)) > 0)
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # An undirected multigraph with 14 components with spatially explicit edges
#> #
#> # Node Data: 701 x 2 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7.522622 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geometry in_rect
#> <POINT [°]> <lgl>
#> 1 (7.533722 51.95556) TRUE
#> 2 (7.533461 51.95576) TRUE
#> 3 (7.532442 51.95422) TRUE
#> 4 (7.53209 51.95328) TRUE
#> 5 (7.532709 51.95209) TRUE
#> 6 (7.532869 51.95257) TRUE
#> # … with 695 more rows
#> #
#> # Edge Data: 851 x 5
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [°]>
#> 1 1 2 Havixbecker S… residen… (7.533722 51.95556, 7.533461 51.95576)
#> 2 3 4 Pienersallee seconda… (7.532442 51.95422, 7.53236 51.95377, 7.5…
#> 3 5 6 Schulte-Bernd… residen… (7.532709 51.95209, 7.532823 51.95239, 7.…
#> # … with 848 more rows
```

Also the CRS functions technically are applied to the active element of the network. However, when changing the CRS of the active element (either by setting or transforming), the same operation is applied to the other element (of course this does not apply for spatially implicit edges). This is because `sfnetwork`

objects have the restriction that nodes and edges should always have the same CRS.

```
st_crs(net)
#> Coordinate Reference System:
#> User input: EPSG:4326
#> wkt:
#> GEOGCRS["WGS 84",
#> DATUM["World Geodetic System 1984",
#> ELLIPSOID["WGS 84",6378137,298.257223563,
#> LENGTHUNIT["metre",1]]],
#> PRIMEM["Greenwich",0,
#> ANGLEUNIT["degree",0.0174532925199433]],
#> CS[ellipsoidal,2],
#> AXIS["geodetic latitude (Lat)",north,
#> ORDER[1],
#> ANGLEUNIT["degree",0.0174532925199433]],
#> AXIS["geodetic longitude (Lon)",east,
#> ORDER[2],
#> ANGLEUNIT["degree",0.0174532925199433]],
#> USAGE[
#> SCOPE["unknown"],
#> AREA["World"],
#> BBOX[-90,-180,90,180]],
#> ID["EPSG",4326]]
```

```
st_transform(net, 3035)
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:3035
#> #
#> # An undirected multigraph with 14 components with spatially explicit edges
#> #
#> # Node Data: 701 x 1 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 4150707 ymin: 3206375 xmax: 4152367 ymax: 3208565
#> geometry
#> <POINT [m]>
#> 1 (4151491 3207923)
#> 2 (4151474 3207946)
#> 3 (4151398 3207777)
#> 4 (4151370 3207673)
#> 5 (4151408 3207539)
#> 6 (4151421 3207592)
#> # … with 695 more rows
#> #
#> # Edge Data: 851 x 5
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 4150707 ymin: 3206375 xmax: 4152367 ymax: 3208565
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [m]>
#> 1 1 2 Havixbecker St… resident… (4151491 3207923, 4151474 3207946)
#> 2 3 4 Pienersallee secondary (4151398 3207777, 4151390 3207727, 4151…
#> 3 5 6 Schulte-Bernd-… resident… (4151408 3207539, 4151417 3207573, 4151…
#> # … with 848 more rows
```

Replacing the geometry of nodes or edges comes with restrictions. First, there is for now a general restriction for `sfnetwork`

objects where nodes can only be points, and edges (if spatially explicit) can only be lines. This is to keep things simple at first. If there are use-cases where other geometry types as nodes or edges make sense, we can always loosen this restriction further down the road.

Additionally:

- You cannot replace the geometry of the nodes when edges are spatially explicit. This would mean that the coordinates of the nodes don’t match anymore with the endpoints of the edges. Hence, the network structure is broken.
- You can only replace the geometry of spatially explicit edges when 1) the CRS of the geometries doesn’t change (otherwise you would create a network with different CRS for nodes and edges, which is restricted) and 2) the endpoints of the geometries don’t change (otherwise you would have endpoints of edges that don’t match the node coordinates anymore, and thus break the network structure).

Geometries can be replaced also by using geometry unary operations, as long as they don’t break the restrictions mentioned above. This basically means that only `st_reverse`

and `st_simplify`

are supported. When calling `st_reverse`

on the edges of a directed network, not only the geometries will be reversed, but the `to`

and `from`

columns of the edges will also be swapped. In the case of undirected networks the geometries are not reversed at all.

```
st_geometry(net)
#> Geometry set for 701 features
#> geometry type: POINT
#> dimension: XY
#> bbox: xmin: 7.522622 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geographic CRS: WGS 84
#> First 5 geometries:
#> POINT (7.533722 51.95556)
#> POINT (7.533461 51.95576)
#> POINT (7.532442 51.95422)
#> POINT (7.53209 51.95328)
#> POINT (7.532709 51.95209)
```

```
class(st_set_geometry(activate(net, "nodes"), NULL))
#> [1] "tbl_graph" "igraph"
```

```
class(st_set_geometry(activate(net, "edges"), NULL))
#> [1] "sfnetwork" "tbl_graph" "igraph"
```

```
as_sfnetwork(roxel, directed = TRUE) %>%
activate("edges") %>%
st_reverse()
#> Warning in st_reverse.sfnetwork(.): For directed networks st_reverse swaps
#> columns 'to' and 'from'
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # A directed multigraph with 14 components with spatially explicit edges
#> #
#> # Edge Data: 851 x 5 (active)
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [°]>
#> 1 2 1 Havixbecker… reside… (7.533461 51.95576, 7.533722 51.95556)
#> 2 4 3 Pienersallee second… (7.53209 51.95328, 7.53236 51.95377, 7.53244…
#> 3 6 5 Schulte-Ber… reside… (7.532869 51.95257, 7.532823 51.95239, 7.532…
#> 4 8 7 NA path (7.53822 51.94546, 7.538353 51.94542, 7.5385…
#> 5 10 9 Welsingheide reside… (7.537614 51.94562, 7.537673 51.9475)
#> 6 12 11 NA footway (7.543751 51.94677, 7.54369 51.94686, 7.5437…
#> # … with 845 more rows
#> #
#> # Node Data: 701 x 1
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7.522622 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geometry
#> <POINT [°]>
#> 1 (7.533722 51.95556)
#> 2 (7.533461 51.95576)
#> 3 (7.532442 51.95422)
#> # … with 698 more rows
```

An `sfnetwork`

object preserves the sf-specific attributes of the nodes and edge data. They can be queried using the `sf_attr`

function.

For the same reason as `tidygraph`

does not support all `dplyr`

functions, `sfnetwork`

does not support all `sf`

functions: there is a clear limitation in the relational data structure that requires rows to maintain their identity. For `sfnetwork`

objects, this also means that the geometries of features should be maintained, or at least the endpoints of these geometries, in the case of the edges. That is, functions that summarise geometries, or (may) change their *type*, *shape* or *position*, are not supported. These are for example the geometric binary operations, most of the geometric unary operations, `st_union`

, `st_combine`

, `st_cast`

, and `st_jitter`

.

These functions cannot be directly applied to an `sfnetwork`

object, but of course you can always escape the network structure, do some work, and merge changes back into the network. For example, when you want to know the area of the voronoi polygon of each node:

```
net_t = st_transform(net, 3035)
y = net_t %>%
activate("nodes") %>%
st_as_sf() %>%
mutate(aoi = st_area(st_collection_extract(st_voronoi(do.call(c, st_geometry(.))))))
net_t %>%
activate("nodes") %>%
st_join(y)
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:3035
#> #
#> # An undirected multigraph with 14 components with spatially explicit edges
#> #
#> # Node Data: 701 x 2 (active)
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 4150707 ymin: 3206375 xmax: 4152367 ymax: 3208565
#> aoi geometry
#> <dbl> <POINT [m]>
#> 1 1837540. (4151491 3207923)
#> 2 2563698. (4151474 3207946)
#> 3 36462. (4151398 3207777)
#> 4 359624. (4151370 3207673)
#> 5 12954. (4151408 3207539)
#> 6 11089. (4151421 3207592)
#> # … with 695 more rows
#> #
#> # Edge Data: 851 x 5
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 4150707 ymin: 3206375 xmax: 4152367 ymax: 3208565
#> from to name type geometry
#> <int> <int> <fct> <fct> <LINESTRING [m]>
#> 1 1 2 Havixbecker St… resident… (4151491 3207923, 4151474 3207946)
#> 2 3 4 Pienersallee secondary (4151398 3207777, 4151390 3207727, 4151…
#> 3 5 6 Schulte-Bernd-… resident… (4151408 3207539, 4151417 3207573, 4151…
#> # … with 848 more rows
```

In addition to those functions mentioned above, there are also no `sfnetwork`

methods for functions related to reading and writing data. Use `sf`

for this, and then convert to an `sfnetwork`

.

*Note: Maybe there are specific spatial network data formats that the package should be able to read from and write to? If yes, please let us know.*

As already shown in some of the examples above, a lot of `dplyr`

functions work directly on `sfnetwork`

objects, thanks to `tidygraph`

. Since the active elements (if spatially explicit) are internally handled as being `sf`

objects, automatically the `sf`

methods for these functions are used. That means, for example, that the `geometry`

column is ‘sticky’. Note: also the `to`

and `from`

column in the edges table are sticky, which does not come from `sf`

, but from `tidygraph`

. So now, we have three sticky columns!

```
net %>%
activate("edges") %>%
select(name)
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
#> Warning: `...` is not empty.
#>
#> We detected these problematic arguments:
#> * `needs_dots`
#>
#> These dots only exist to allow future extensions and should be empty.
#> Did you misspecify an argument?
```

```
#> # An sfnetwork with 701 nodes and 851 edges
#> #
#> # CRS: EPSG:4326
#> #
#> # An undirected multigraph with 14 components with spatially explicit edges
#> #
#> # Edge Data: 851 x 4 (active)
#> # Geometry type: LINESTRING
#> # Dimension: XY
#> # Bounding box: xmin: 7.522594 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> from to geometry name
#> <int> <int> <LINESTRING [°]> <fct>
#> 1 1 2 (7.533722 51.95556, 7.533461 51.95576) Havixbecker …
#> 2 3 4 (7.532442 51.95422, 7.53236 51.95377, 7.53209 51.95… Pienersallee
#> 3 5 6 (7.532709 51.95209, 7.532823 51.95239, 7.532869 51.… Schulte-Bern…
#> 4 7 8 (7.540063 51.94468, 7.539696 51.94479, 7.539466 51.… NA
#> 5 9 10 (7.537673 51.9475, 7.537614 51.94562) Welsingheide
#> 6 11 12 (7.543791 51.94733, 7.54369 51.94686, 7.543751 51.9… NA
#> # … with 845 more rows
#> #
#> # Node Data: 701 x 1
#> # Geometry type: POINT
#> # Dimension: XY
#> # Bounding box: xmin: 7.522622 ymin: 51.94151 xmax: 7.546705 ymax: 51.9612
#> geometry
#> <POINT [°]>
#> 1 (7.533722 51.95556)
#> 2 (7.533461 51.95576)
#> 3 (7.532442 51.95422)
#> # … with 698 more rows
```