GithubHelp home page GithubHelp logo

luukvdmeer / sfnetworks Goto Github PK

View Code? Open in Web Editor NEW
330.0 16.0 20.0 66.63 MB

Tidy Geospatial Networks in R

Home Page: https://luukvdmeer.github.io/sfnetworks/

License: Other

R 100.00%
geospatial-networks spatial-networks network-analysis spatial-analysis spatial-data-science simple-features rspatial tidygraph tidyverse

sfnetworks's Introduction

Tidy Geospatial Networks in R

R build status Codecov test coverage CRAN Downloads

sfnetworks is an R package for analysis of geospatial networks. It connects the functionalities of the tidygraph package for network analysis and the sf package for spatial data science.

Background

Geospatial networks are graphs embedded in geographical space. That means that both the nodes and edges in the graph can be represented as geographic features: the nodes most commonly as points, and the edges as linestrings. They play an important role in many different domains, ranging from transportation planning and logistics to ecology and epidemiology. The structure and characteristics of geospatial networks go beyond standard graph topology, and therefore it is crucial to explicitly take space into account when analyzing them.

We created sfnetworks to facilitate such an integrated workflow. It combines the forces of two popular R packages: sf for spatial data science and tidygraph for standard graph analysis. The core of the package is a dedicated data structure for geospatial networks, that can be provided as input to both the graph analytical functions of tidygraph as well as the spatial analytical functions of sf, without the need for conversion. Additionally, we implemented a set of geospatial network specific functions, such as routines for shortest path calculation, network cleaning and topology modification. sfnetworks is designed as a general-purpose package suitable for usage across different application domains, and can be seamlessly integrated in tidyverse workflows.

Installation

You can install the latest stable version of sfnetworks from CRAN with:

install.packages("sfnetworks")

You can install the development version from GitHub with:

remotes::install_github("luukvdmeer/sfnetworks")

Note: Two important dependencies of sfnetworks, the sf package for spatial data science and the igraph package for network analysis (which is the main "analysis backend" of tidygraph), require some low-level software libraries to be installed on your system. Depending on which operating system you use, this can mean that you have to install these system requirements first, before you can install sfnetworks. See the installation guides of sf and igraph for details.

Usage

The main goal of sfnetworks is to connect the tidygraph package for network analysis and the sf package for spatial data science. To make the most out of it, it is recommended to make yourself familiar with these two 'parent packages' if you don't know them yet.

There are currently five vignettes that guide you through the functionalities of sfnetworks:

(GIF (c) by Lore Abad)

Contribution

We look very much forward to contributions to the package. See the contributing guide for further details.

This project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Acknowledgment

This project gratefully acknowledges financial support from the

sfnetworks's People

Contributors

agila5 avatar ccb2n19 avatar loreabad6 avatar luukvdmeer avatar robinlovelace avatar romainfrancois avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sfnetworks's Issues

Plot method for sfnetwork objects

Currently a plot method for an object of class sfnetworks does not exists, and hence it defaults to the plot method of a igraph object.

Suggestion: create a plot method that shows edges and nodes at once, on base R. To handle geometry, explore options of plotting GEOMETRY COLLECTIONS. Consider the case where edges are not sf lines, how to handle that? Should they be converted to an sf object just for plotting?

A simple plot method would be useful to have a quick overview of how the network looks like. However, for any advanced plotting, the user can export edges and nodes as sf objects and use their preferred plotting package and settings. Or in another case, plotting methods specific for network analysis with igraph or tidygraph for example. This should be emphasized on the description of the plot method.

Problems with spatial and graph operations on a Spatial Lines Network

This issue shows the problems with the current implementations of spatial and graph operations on a network.

The problem is illustrated using code from stplanr, the challenge will be to solve this problem in future versions of sfnetworks.

We start this example by creating a spatial lines network object with the function SpatialLinesNetwork of the R package stplanr.

# packages
library(sf)
library(stplanr)
library(tmap)
library(leaflet)
library(ggspatial)
library(tidygraph)

sln_sf <- SpatialLinesNetwork(route_network_sf)

First of all, it should be noted that, at the moment, the plotting of spatial lines network objects with ggplot2/tmap/leaflet is unnecessary complicated. For example, all the following approaches fail.

tm_shape(sln_sf) +
  tm_lines()
#> Error in get_projection(mshp_raw, output = "crs"): shp is neither a sf, sp, nor a raster object
leaflet(sln_sf) %>%
  addTiles() %>%
  addPolylines()
#> Error in polygonData.default(data): Don't know how to get path data from object of class sfNetwork
ggplot() +
  annotation_map_tile() +
  geom_sf(data = sln_sf, size = 0.75)
#> Error: `data` must be a data frame, or other object coercible by `fortify()`, not an S4 object with class sfNetwork

As we can see from the error messages, the user first has to extract the sf data and then plot it. For example,

ggplot() +
  annotation_map_tile() +
  geom_sf(data = sln_sf@sl, size = 0.75)
#> Zoom: 13

The same idea can be applied to igraph functions, like

igraph::edge_betweenness(sln_sf)
#> Error in igraph::edge_betweenness(sln_sf): Not a graph object
igraph::is_connected(sln_sf)
#> Error in igraph::is_connected(sln_sf): Not a graph object

which work only if we extract the graph component from the spatial network object,

igraph::edge_betweenness(sln_sf@g)
#>  [1]  51 100   0   0 147 108  84 129  56 116 151   0 220  16  49 102 121
#> [18] 182 100 161  95  57  48   6  37 135 375 386   0 289 106  51 135  10
#> [35] 245  43   4 184 200  97   0 162 121   0  78  33 198  51  57 120  77
#> [52] 117 150  85  51 127 118   0  79  44 135  98  34   0 119  37 101 120
#> [69]  99  25  41  17 153  36 226 151  40 164  20  11
igraph::is_connected(sln_sf@g)
#> [1] TRUE

This separation between the spatial and the graph components of a spatial network is counterintuituve and complicated.

Moreover, we cannot apply to that network object any spatial or graph function that implies a modification of the spatial or graph structure. For example, if we create a buffer of 400m around Chapteltown, which is one of the neighborhoods in the center of the network object,

chapeltown_geocode <- tmaptools::geocode_OSM("chapeltown leeds")
chapteltown_buffer <- geo_projected(
  st_sf(st_sfc(st_point(c(chapeltown_geocode$coords[1], chapeltown_geocode$coords[2])), crs = 4326)),
  st_buffer,
  dist = 400
)

and filter all the edges inside the buffer, we get an error:

sln_sf[chapteltown_buffer, ]
#> Error in sln_sf[chapteltown_buffer, ]: object of type 'S4' is not subsettable

This example is even more problematic than the others, since, in this case, we cannot simply work on the sf component of the spatial lines network object. In fact, even if we modify it,

sln_sf2 <- sln_sf
sln_sf2@sl <- sln_sf2@sl[chapteltown_buffer, ]
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar

this does not change the graph component since they are not strictly linked:

nrow(sln_sf2@sl)
#> [1] 9
igraph::E(sln_sf@g)
#> + 80/80 edges from 6e154b9:
#>  [1]  1-- 2  1-- 3  1-- 4  3-- 5  3-- 6  6-- 7  6-- 8  5-- 8  8-- 9  5--10
#> [11]  7-- 9  7--11  9--12  9--11 10--13 10--14 13--14 12--14 14--15 13--16
#> [21] 16--17  4--16  4--18 18--19 17--18 17--20 12--21 21--22 21--22 22--23
#> [31] 22--24 19--20 15--20 19--25 15--26 15--25 25--27 23--28 23--29 24--29
#> [41] 24--30 24--30 30--31 31--32 31--32 32--33 29--34 11--33 33--35 28--36
#> [51] 28--37 34--38 34--39 37--38 38--40 37--41 41--42 41--43 36--44 26--36
#> [61] 39--45 35--39 39--46 35--46 45--47 42--45 43--44 42--43 47--48 47--49
#> [71] 48--49 46--49 44--50 26--27 26--50 48--51 50--52 50--51 51--52 27--52
igraph::E(sln_sf2@g)
#> + 80/80 edges from 6e154b9:
#>  [1]  1-- 2  1-- 3  1-- 4  3-- 5  3-- 6  6-- 7  6-- 8  5-- 8  8-- 9  5--10
#> [11]  7-- 9  7--11  9--12  9--11 10--13 10--14 13--14 12--14 14--15 13--16
#> [21] 16--17  4--16  4--18 18--19 17--18 17--20 12--21 21--22 21--22 22--23
#> [31] 22--24 19--20 15--20 19--25 15--26 15--25 25--27 23--28 23--29 24--29
#> [41] 24--30 24--30 30--31 31--32 31--32 32--33 29--34 11--33 33--35 28--36
#> [51] 28--37 34--38 34--39 37--38 38--40 37--41 41--42 41--43 36--44 26--36
#> [61] 39--45 35--39 39--46 35--46 45--47 42--45 43--44 42--43 47--48 47--49
#> [71] 48--49 46--49 44--50 26--27 26--50 48--51 50--52 50--51 51--52 27--52

This means that, after the spatial filtering, some edges in the graph component are linked to LINESTRINGS that do no exist in the spatial component. The same will happen if we modify the graph component. The only procedure that works is to filter the original sf data and, then, rebuild the network from scratch, i.e.

sln_sf3 <- SpatialLinesNetwork(route_network_sf[chapteltown_buffer, ])
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar

nrow(sln_sf3@sl)
#> [1] 9
igraph::E(sln_sf3@g)
#> + 9/9 edges from 700d8f9:
#> [1] 1--2 1--3 1--4 4--5 4--5 5--6 5--7 6--8 6--9

Created on 2019-10-13 by the reprex package (v0.3.0)

Calculating shortest path from and to any spatial point

Is your feature request related to a problem? Please describe.
In tidygraph and igraph, you have to provide a node index as from and to in shortest path calculation. This requires you to know your node indices. In spatial networks, it is common to calculate the shortest path from and to a spatial point with X and Y coordinates, no matter if this point is a node in the network or not.

Describe the solution you'd like
Enable spatial shortest path calculation as described above. The function will find either the nearest node (with st_snap_point_to_node) or the nearest point on the nearest edge (with st_snap_point_to_edge followed by st_split_edge), and then calculate the shortest path as normal.

The future of sfnetworks

Building on the r-spatial blogpost on tidy spatial networks and the related issue in the spnethack repository, it seems to be time to discuss how to proceed with the sfnetworks package.

Firstly, it is important to say again that this package was originally developed as a homework assignment for an R class. As you may know, when doing a master study, there are a lot of parallel courses that all have homework assignments, and you don't always have (or make..) enough time for each of them ;-) Therefore, some parts of the package were created as easy ways to meet the requirements of the assignment (which included a minimum number of classes, and a minimum number of functions). For example, the sfn_route class, and some of the functions, are not really an added value in my point of view, but just had to be there. Long story short: there is a need to clean this up, and remove everything that is unnecessary.

Secondly, during the time I wrote the package, I was experimenting with the tidyverse, and therefore, all the source code is written in tidyverse style, including dplyr verbs and pipes. I think this is bad practice, and would like to rewrite this as much as possible in just base R.

Thirdly, the package was written before I knew the tidygraph package. In the blogpost, we showed that tidygraph can be a nice way to work with tidy spatial networks. That connection between sf and tidygraph should be the main building block of the package. One of the questions that arises then, is if there is still a need for a sfnetwork class, or that using tbl_graph for network operations, and 'escaping' to sf for spatial operations, is enough.

I would argue that a sfnetwork class that subclasses tbl_graph is the best way to go. Now, tidygraph works in a way that you use the activate() verb to specify if you want to work on the nodes or the edges, and then, you can use most of the dplyr verbs as if your nodes/edges were tibbles (and also use columns from the other table if needed). For example (Note: I will use the graph object from the blogpost in the examples):

graph %>%
  activate(nodes) %>%
  mutate(nodeIDplusOne = nodeID + 1)
  activate(edges) %>%
  mutate(edgeIDplusNodeID = edgeID + .N()$nodeID)

Some verbs that reduce the number of nodes/edges, also have effects outside of the currently active data. For example, when using filter() and slice() on the nodes, it will also remove the edges terminating at the removed nodes. The same thing happens when doing any kind of dplyr *_join(). For example, if you have a subset of your nodes as a tibble, you can do a right join with the nodes table of the graph. Then, only the nodes of your subset remain, and also only the edges that connect to these nodes.

This behaviour would be very useful when doing geometric operations on the network. For example, I could think of 'escaping' the nodes to an sf object, intersecting them with a polygon, joining the result back in, and then also keep only those edges that connect to the 'new' set of nodes. Something like this:

polygon = st_sfc(
  st_polygon(list(rbind(c(7.62, 51.95), c(7.63, 51.95), c(7.63, 51.96), c(7.62, 51.96), c(7.62, 51.95))),
  crs = st_crs(graph %>% activate(nodes) %>% as_tibble() %>% st_as_sf()
)

intersection = graph %>%
  activate(nodes) %>%
  as_tibble() %>%
  st_as_sf() %>%
  st_intersection(polygon)

new_graph = graph %>%
  activate(nodes) %>%
  st_join(intersection)

However, this does not work, because st_join does not know what to do with an object of class tbl_graph. Using a dplyr right_join with the nodeID column also fails, even when converting the intersection object to a tibble first.

new_graph = graph %>%
  activate(nodes) %>%
  right_join(intersection, by = "nodeID")

new_graph = graph %>%
  activate(nodes) %>%
  right_join(as_tibble(intersection), by = "nodeID")

Only when we remove the geometry list column from both the nodes table as the intersection object, the join works. But of course, we don't want to do that ;-)

The solution here would be that when activating the nodes or edges, you don't analyse them as being a tibble, but as being an sf object. Then, things like this would work, without any need to escape first to an sf object and (if it even works) merging the resuts back in:

graph %>%
  activate(nodes) %>%
  st_intersection(polygon) # This will also keep only those edges that connect to the 'intersecting nodes'

graph %>%
  activate(edges) %>%
  mutate(length = st_length(.))

Also, the dplyr verbs like select() will use their sf method. Hence, sticky geometry will also work in the graph structure. A sfnetwork class that subclasses tbl_graph could implement this. We could also think of, for example, special methods for st_transform, that transforms both the nodes and edges at once, and for st_as_sf , that enables to escape to an sf object without having to go through a tibble first.

For now, I will release the current version of sfnetworks as v0.1.0, such that people already using it can keep using it in the way it is, and then (try to) implement the things mentioned above in a new version 0.2.0, before considering uploading it to CRAN.

Discussion and feedback is welcome! @Robinlovelace @loreabad6

Functions to snap points to nodes or edges

Is your feature request related to a problem? Please describe.
It would be nice to have a function that takes a (set of) spatial point(s), either as sf, sfc or sfg, and snaps them to the nearest node or nearest edge. In that way you can provide any spatial point to a shortest path query, no matter if it lies exactly on the network or not.

Describe the solution you'd like
A function st_snap_point_to_node that replaces the geometry of the given point by the geometry of the nearest node, and a function st_snap_point_to_edge that replaces the geometry of the given point by the nearest point on the nearest edge. The first can simply use sf's st_nearest_feature (or st_nearest_node if we implement that), the latter can use sf's st_nearest_points to find the nearest point on the nearest edge.

Probably these functions will need a tolerance argument, like st_snap in sf.

Sf methods for sfnetwork objects

Documenting what sf functions should have a method for an object of class sfnetwork:

Function Implemented Comment
geometric binary predicates yes Output: (sparse) matrix
st_crs yes *
st_set_crs yes *
st_bbox yes **
st_make_grid yes **
st_geometry yes **
st_transform no *
st_crop no *
st_join, st_filter no ?
geometric unary operations no ***, all these functions should be applied to sf objects BEFORE transforming to an sfnetwork
geometric operations no *, If an edge is cut after an st_intersection should a new node be created?
geometric measurements no ***, st_length only for edges, tidygraph::morph method?
combine functions no ?
tidyverse methods yes

* Should the function be applied to both edges and nodes?
** When edges are active, and are not LINESTRINGs, shows sf error: no geometry column detected.
*** Not relevant for an sfnetwork object?

Check if endpoints of edges match nodes when constructing an sfnetwork

The construction function of sfnetwork objects now accepts an sf object (with POINT geometry) as nodes, and an sf object (with LINESTRING geometry) as edges. The edges should have a to and from column referring to the indices of their terminate nodes. However, the construction function does not check if the coordinates of the endpoints of each edge match with the coordinates of the terminate nodes of that edge. Such a check should be implemented.

Snap spatial points to the nearest node

Is your feature request related to a problem? Please describe.
This is already implemented internally in the st_shortest_paths function. It uses st_nearest_feature to find the nearest node.

However, there can be some issues, which could be addressed by allowing users to tune some parameters.

For example:

  • What if the nearest node is in a very small, unconnected component? Then you would probably prefer to snap it to a node that might be a little bit further, but at least part of the largest connected component.
  • Maybe you want some threshold distance x, and return a message that no node could be found when distance(point, node) > x.

Describe the solution you'd like
More arguments for the functionality to snap points to their nearest node, that can be tuned by users.

Standard creation functions for spatial graphs from only points

Is your feature request related to a problem? Please describe.
You can now create an sfnetwork from only a set of spatial linestrings. The linestrings will be the edges, and the endpoints of the linestrings the nodes. Also, you can create an sfnetwork from only a set of spatial points. The points will be the nodes, and edges will be created in a sequential order (i.e. from node 1 to 2, node 2 to 3, etc). This was created as a solution for this StackOverflow issue.

However, there are much more ways to create a spatial graph from only a set of points. Dale (2017) lists the most common ones:

sn

Describe the solution you'd like
More options to create an sfnetwork from only a set of spatial points. This could be implemented with an extra argument to as_sfnetwork.sf, specifying which creation algorithm to use, and also in the same style as tidygraphs create_* family of functions. That is: create_mnn, create_nn, create_mst, et cetera.

CRS of sfnetworks objects

Is your feature request related to a problem? Please describe.
The philosophy of tbl_graph objects, and thus also of sfnetwork objects, is that you apply functions to either nodes or edges, by activating one of them before applying the functions. For most sf functions that we will write methods for (see #18) this will be the appropriate workflow as well. However, it probably better if the complete sfnetwork object has always only one CRS, so it is never possible to have nodes and edges that are in a different CRS. That also means that the CRS functions of sf (like st_crs, st_transform, etc) are always applied to the network as a whole, and not to the nodes or edges separately.

Describe the solution you'd like
I have to think of how to exactly implement it, but the solution should in any case make sure that nodes and edges are always in the same CRS, and that crs functions of sf are always applied to the whole network.

Note
Relates to #18

Spatial join between two networks

Is your feature request related to a problem? Please describe.
In tidygraph, the graph_join function can join two graphs A and B, resulting in a new graph C where nodes that were shared between A and B, only occur once in C. That is, identical nodes of the two input graphs are detected as being identical, and merged accordingly. Additionally, it updates the edges in the joining graph so they match the new indexes of the nodes in the resulting graph.

Describe the solution you'd like
A function st_graph_join that does exactly what graph_join does, but based on a spatial join. That is, it will detect when nodes of A and B have the same coordinates, merge them accordingly, and update the edges.

Morphing of sfnetwork objects

Is your feature request related to a problem? Please describe.
The tidygraph package uses 'morphing' to create temporary alternative representations of a graph. See here. (Warning: it takes some thought to understand how this exactly works, but after that it is useful). sfnetworks does not support morphing yet.

Describe the solution you'd like
Since morphing is an important part of tidygraph, especially when doing more complicated graph analysis, we should support this also. The sfnetwork class simply subclasses tbl_graph, so all of tidygraphs morphers should already work, but along the way, there will probably be the need to add dedicated spatial morphers. Or at least, to enable users to create their own spatial morphers if they need them.

A morphed_tbl_graph is a separate class in tidygraph. Therefore, we need a morphed_sfnetwork class as well, which subclasses the morphed_tbl_graph.

One spatial morpher I can think of would be to_spatially_explicit_edges(). This will come in handy when working with an sfnetwork that has an edges table containing from and to columns referring to its source and target nodes, but no geometry column. Of course, implicitly, these edges are spatial, because they refer to nodes which are spatial points. An alternative representation of this graph could then be a morphed_sfnetwork where a geometry column is added to the edges (as a LINESTRING between the source and target nodes). This enables to calculate the length of the edges with st_length(), and to plot the edges on a map (see #17).

Describe alternatives you've considered
Simply rely on tidygraphs morphing functions. However, as said, dedicated spatial morphers might be needed.

Note
Adding a morphed_sfnetwork class will probably also create the need to write extra methods for sf functions. See #18.

Use st_boundary to get endpoints of lines

When defining the endpoints of edges, sfnetworks now gets all coordinates of the LINESTRING, and then selects the first and last one. At the moment that was coded, I did not now that applying st_boundary on a LINESTRING already returns the two endpoints directly. The code would be cleaner and simpler when using st_boundary for finding the endpoints of an edge.

Fine-tune pkgdown website

  • Group functions on reference tab
    Group and describe sfnetworks functions accordingly. Reference here.

  • Check auto-linking to documentation
    Although pkgdown does this automatically, some links might not be working properly. Reference here.

  • Add news tab and changelog
    Add news tab to specify new features for each release. Reference here.

Reroute edges of sfnetwork objects

Is your feature request related to a problem? Please describe.
In tidygraph, the reroute() verb enables to change the terminal nodes of edges. For example, you can reverse edges with reroute(to = from, from = to). When having spatially explicit edges in an sfnetwork object, rerouting should not just mean changing the from and to columns, but also the geometry column. In the case of reversion, this would mean applying st_reverse() to the geometries.

Describe the solution you'd like
An sfnetwork method for tidygraphs reroute() function, that takes into account the above (only when edges are spatially explicit).

Fine-tune continuous integration with GH actions

GH actions seems to work good now, but we should still fine-tune it to make it better.

Consider Linux as OS for pkgdown deployment

Right now pkgdown.yaml runs on macOS latest. For the similar R-CMD-Check.yaml (which does both macOS and Linux), the Linux check finishes twice as fast as the macOS one. It might be worth considering to run the pkgdown deployment on a Linux VE instead?

(Not sure if this will cause other errors, but let's see. Deploying pkgdown with GH actions is still a very new thing that seems to be not so often used yet. Most use Travis.)

Check if it is possible to prevent installing dependencies twice

Right now, both pkgdown.yaml and R-CMD-check.yaml install all system dependencies (gdal and friends) and all R package dependencies, before doing the core of their job (deploying pkgdown and running an R CMD check, respectively). The difference is that pkgdown.yaml runs only at pushes to master, and R-CMD-check.yaml at pushes to master, pushes to develop and pull requests. That makes sense. However, it also means that when pushing to master, the whole dependency installation, which takes up to almost half an hour, happens twice. Maybe we can explore if this double work can somehow be prevented.

Use new pkgdown release

Right now we use the deploy_local() internal function of pkgdown in combination with a GitHub Personal Access Token and GitHub Secret to deploy the pkgdown website at every push to master. See here why this is needed. Although in this issue of pkgdown it is stated that this complicated way should not be needed anymore, I could not get it to work without.

As I understand, we should now be able to use the deploy_to_branch() function of pkgdown, which is there since version 1.5. However, it did not work for me yet., for the same reasons as described here. That issue is solved for Travis, but with GH actions it still occurred (at least in my situation). According to what @Nowosad said here, it is better to wait for the new CRAN-release of pkgdown?

Things work good as we have it now, but when the new pkgdown is released on CRAN, it might be worth considering to update the pkgdown.yaml file and make the process cleaner and simpler.

Regularly update sf related GH actions

As said here, GH actions for sf is still actively developed and evolving rapidly. We should regularly update the yaml files to keep up with the most recent versions.

Relates to #2

Setting edge geometry to NULL changes the active element of the network

Describe the bug
When setting the geometry of the edges to NULL, the active element of the network gets changed to the nodes. Changing the activated element of the network should never happen without calling activate.

Reproducible example

library(sfnetworks)
library(sf)

as_sfnetwork(roxel) %>%
  activate('edges') %>%
  st_set_geometry(NULL) %>%
  active()
[1] "nodes"

Expected behavior
Keeping the edges active after setting the edge geometry to NULL.

Estimation of basic exploratory data analysis measures

Is your feature request related to a problem? Please describe.
I think we should implement a few functions for the estimation of basic exploratory data analysis measures, such as Moran's I or Geary's C. AFAIK the existing methods are really too complicated for something so simple.

Describe the solution you'd like
Ad-hoc functions for the estimation of those statistics for network data. They could also be summarized in one function, I'm not sure what's the best approach.

Describe alternatives you've considered
This is my workflow at the moment.

# create a very simple network
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, PROJ 4.9.3
library(tmap)

network_column <- st_sfc(
  st_linestring(matrix(c(0, -2, 0, -1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(0, 2, 0, 1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(-2, 0, -1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, -2, 2, -1, 2, 0), ncol = 2, byrow = TRUE)),
  st_linestring(matrix(c(2, 2, 2, 1, 2, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 3, 0, 4, 0), ncol = 2, byrow = TRUE))
)

# assign some random measurements to all edges
set.seed(1)
network_data <- st_as_sf(
  data.frame(x = runif(7)), 
  geometry = network_column
)

tm_shape(network_data) + 
  tm_lines(col = "x", palette = "viridis")
#> Warning: Currect projection of shape network_data unknown. Long-lat (WGS84) is
#> assumed.

# estimate neighborhoods matrix
network_neigh <- st_touches(network_data)

# use spdep for Moran's I and Geary's C
library(spdep)
#> Loading required package: sp
#> Loading required package: spData

# define ad-hoc function to translate sgbp into nb (as documented in 
# https://r-spatial.github.io/spdep/articles/nb_sf.html#creating-neighbours-using-sf-objects)
as.nb.sgbp <- function(x) {
  attrs <- attributes(x)
  x <- lapply(x, function(i) { if(length(i) == 0L) 0L else i } )
  attributes(x) <- attrs
  class(x) <- "nb"
  x
}

network_nb <- as.nb.sgbp(network_neigh)
network_lw <- nb2listw(network_nb)

moran(
  x = network_data$x, 
  listw = network_lw, 
  n = nrow(network_data), 
  S0 = Szero(network_lw)
)
#> $I
#> [1] -0.1479862
#> 
#> $K
#> [1] 1.272941

geary(
  x = network_data$x, 
  listw = network_lw, 
  n = nrow(network_data), 
  n1 = nrow(network_data) - 1,
  S0 = Szero(network_lw)
)
#> $C
#> [1] 0.9910445
#> 
#> $K
#> [1] 1.272941

Created on 2020-02-05 by the reprex package (v0.3.0)

Function that returns junction points

Is your feature request related to a problem? Please describe.
I think we should include a function that returns junction points of a street network. As far as I know, there is no clear definition of junction point but I think that a reasonable definition (for OSM data) is the following: a function point is a point that is repeated three or more times in the city network.

Describe the solution you'd like
We should create a function that, given an object of class sfnetwork, returns an sf object with a POINT geometry with the coordinates of the junction points.

Describe alternatives you've considered
This is a reprex of the problem with a possible solution:

# packages
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, PROJ 4.9.3
library(tmap)

# create a very simple network
network_column <- st_sfc(
  st_linestring(matrix(c(0, -2, 0, -1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(0, 2, 0, 1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(-2, 0, -1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, -2, 2, -1, 2, 0), ncol = 2, byrow = TRUE)),
  st_linestring(matrix(c(2, 2, 2, 1, 2, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 3, 0, 4, 0), ncol = 2, byrow = TRUE)), 
  crs = 3003
)

# find junction points
points_coordinates <- st_coordinates(network_column)[, c(1, 2)]
# points that occur two or more times
duplicated_points <- points_coordinates[duplicated(points_coordinates), ]
# points that occur three or more times
duplicated_duplicated_points <- duplicated_points[duplicated(duplicated_points), ]
# exclude further duplicates
duplicated_duplicated_points <- unique(duplicated_duplicated_points)

# transform back into an sf object
junction_points <- st_sfc(st_multipoint(duplicated_duplicated_points), crs = st_crs(network_column))

# plot
tm_shape(network_column) + 
  tm_lines() +
tm_shape(junction_points) + 
  tm_dots(col = "red", size = 0.5)

Created on 2020-02-17 by the reprex package (v0.3.0)

I've already discussed this problem with @Robinlovelace here: ropensci/stplanr#357 (and maybe we can also adopt the same approach of that reprex to close that issue but I would just wait for sfnetwork) and I think this is the first step for #12.

Benchmarking

For another project I've done some benchmarks and it seems that sfnetworks is already pretty fast. Wonder if we can make it even faster!

# Aim: benchmark the performance of different spatial network packages

library(magrittr)
library(stplanr)
library(sf)
#> Linking to GEOS 3.7.1, GDAL 2.4.2, PROJ 5.2.0
piggyback::pb_download("chapeltown_leeds_key_roads.Rds", repo = "ropensci/stplanr", dest = ".", show_progress = FALSE)
chapeltown_leeds_key_roads <- readRDS("chapeltown_leeds_key_roads.Rds")
x <- chapeltown_leeds_key_roads %>% 
  st_transform(crs = geo_select_aeq(.))
x_sp = as(x, "Spatial")

# spatial network creation ------------------------------------------------

stplanr <- function() stplanr::SpatialLinesNetwork(x)
sfnetworks <- function() sfnetworks::sfn_asnetwork(x)
dodgr <- function() dodgr::weight_streetnet(x)
shp2graph <- function() shp2graph::readshpnw(x_sp)

bench::mark(check = FALSE, stplanr(), sfnetworks(), dodgr(), shp2graph())
#> Warning in SpatialLinesNetwork.sf(x): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().

#> Warning in SpatialLinesNetwork.sf(x): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().

#> Warning in SpatialLinesNetwork.sf(x): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().

#> Warning in SpatialLinesNetwork.sf(x): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().

#> Warning in SpatialLinesNetwork.sf(x): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 4 x 6
#>   expression        min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>   <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 stplanr()     137.2ms 142.87ms     7.03     7.85MB     5.27
#> 2 sfnetworks()  64.69ms  69.22ms    14.3      4.49MB     7.15
#> 3 dodgr()         1.41s    1.41s     0.709   68.29MB     2.84
#> 4 shp2graph()  310.25ms 324.86ms     3.08   473.86MB    27.7

Created on 2019-11-29 by the reprex package (v0.3.0)

Allow node_key argument in network creation

Is your feature request related to a problem? Please describe.
Last month, tidygraph got an update. Where before the to and from columns of the edges always contained integers referring to the row number of the respective nodes in the node table, it is now also possible to have those columns containing character indices referring to a specific ID column in the nodes table (which has to contain characters). Which column to use for that, can then be specified with the node_key argument.

node_key
The name of the column inΒ nodesΒ that character representedΒ toΒ andΒ fromΒ columns should be matched against. IfΒ NAΒ the first column is always chosen. This setting has no effect ifΒ toΒ andΒ fromΒ are given as integers.

Describe the solution you'd like
Include the node_key argument also in sfnetwork construction functions.

Function to calculate the circuity of an edge

Is your feature request related to a problem? Please describe.
There are graph measures so specific to spatial networks, that neither sf, neither tidygraph has functions to calculate them. One such measure is the circuity of an edge, as described in this paper:

Circuity, the ratio of network to Euclidean distances, describes the directness of trips and the efficiency of transportation networks.

Describe the solution you'd like
An edge measurement function, in the same style as tidygraphs "edge measures" group of functions, that calculates the circuity of a spatially explicit edge: edge_circuity.

Print method for sfnetwork objects

Currently the print method for an object of class sfnetworks does not exists, and hence it defaults to the print method of a tbl_graph object.

I would suggest, to keep it simple the same structure for the new print method, with a header indicating the class sfnetworks for the object, and with the properties of an sf object.

Check on any potential problems when creating the method regarding the new sf version, mainly for PROJ issues

Check also how tbl_graph prints. At the top it includes a general overview, e.g.:

A tbl_graph: 701 nodes and 851 edges

But then it gets specific depending on what operation was performed:

A directed multigraph with 14 components
A rooted forest with 3 trees, after an st_filter that reduces the network
An empty graph when the result is 0 nodes and 0 edges

Function to split an edge by a point

Is your feature request related to a problem? Please describe.
It would be nice to have a function that splits an edge by a point on that edge. That is, the given point (which should lie on the edge) becomes a node, and the edge will be split in two.

Describe the solution you'd like
A function st_split_edge(x,y) that accepts a point (as sf, sfc or sfg) as x and an edge ID as y. It will split edge y in two, and add x as node to the nodes table.

This can be used nicely with st_nearest_edge. First, you will find the ID of the nearest edge to your point with st_nearest_edge, and then apply st_split_edge.

Slope-sensitive routing

Please describe the problem.

It could be useful to be able to create weighting profiles that are sensitive to the slope, gradient, of route segments encoded as linestrings. I've recently developed a way to quickly estimate the slope of 1000s of routes based on a raster dataset with elevations: https://github.com/ITSLeeds/slopes

It could be a fun hackathon challenge. Heads-up @tempospena and @mpadge.

Describe how you currently solve the problem, or how attempted to solve it

I would like to calculate shortest paths as below, but one that responds to steepness:

remotes::install_github("itsleeds/slopes")
remotes::install_github("itsleeds/od")
remotes::install_github("ropensci/stplanr")
library(sfnetworks)
library(sf)

r = slopes::lisbon_road_segments
v = sf::st_coordinates(r)
nrow(v)
set.seed(5)
p = v[sample(nrow(v), size = 3), ]
head(p)
l = od::points_to_odl(p[, 1:2], crs = st_crs(r), interzone_only = TRUE)

net = as_sfnetwork(r)
net_t = net %>%
  activate("edges") %>%
  dplyr::mutate(length = sf::st_length(.))
igraph::shortest_paths(graph = net_t, 1, 200)$vpath

Currently I can do routing on a network as follows:

l_start_points = lwgeom::st_startpoint(l)
l_end_points = lwgeom::st_endpoint(l)
r1 = stplanr::route_local(sln = sln, from = l_start_points[1], to = l_end_points[2])
plot(r1$geometry, col = "red", lwd = 5)

# calculate shortest paths
sp = stplanr::route(
  l = l,
  route_fun = stplanr::route_local,
  sln = sln
)

Describe the desired functionality of sfnetworks
A clear and concise description of what functionalities you would like to see in the sfnetworks package such that your problem can be solved more conveniently.

To be able to incorporate slope in the route estimate in a controlled way. Some demonstration code, showing 3 shortest routes calculated with the route() function in stplanr are shown below.

remotes::install_github("itsleeds/slopes")
#> Using github PAT from envvar GITHUB_PAT
#> Skipping install of 'slopes' from a github remote, the SHA1 (aaf4d36b) has not changed since last install.
#>   Use `force = TRUE` to force installation
remotes::install_github("itsleeds/od")
#> Using github PAT from envvar GITHUB_PAT
#> Skipping install of 'od' from a github remote, the SHA1 (d9e37c4a) has not changed since last install.
#>   Use `force = TRUE` to force installation
remotes::install_github("ropensci/stplanr")
#> Using github PAT from envvar GITHUB_PAT
#> Skipping install of 'stplanr' from a github remote, the SHA1 (b2b95040) has not changed since last install.
#>   Use `force = TRUE` to force installation
library(sfnetworks)
library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 7.0.0

r = slopes::lisbon_road_segments
r = stplanr::overline(r, "Avg_Slope")
#> 2020-05-02 14:05:12 constructing segments
#> 2020-05-02 14:05:12 building geometry
#> 2020-05-02 14:05:12 simplifying geometry
#> 2020-05-02 14:05:12 aggregating flows
#> 2020-05-02 14:05:12 rejoining segments into linestrings
sln = stplanr::SpatialLinesNetwork(r)
#> Warning in SpatialLinesNetwork.sf(r): Graph composed of multiple subgraphs,
#> consider cleaning it with sln_clean_graph().
sln = stplanr::sln_clean_graph(sln)
#> Input sln composed of 4 graphs. Selecting the largest.
nrow(r)
#> [1] 219
nrow(sln@sl) # simple graph
#> [1] 207
v = sf::st_coordinates(sln@sl)
nrow(v)
#> [1] 2367
set.seed(8)
p = v[sample(nrow(v), size = ), ]
p = st_sample(st_convex_hull(st_union(sln@sl)), size = 3)
l = od::points_to_odl(st_coordinates(p), crs = st_crs(r), interzone_only = TRUE)
l$v = 1
l = od::od_oneway(l)
plot(sln@sl$geometry)
plot(p, add = TRUE)

net = as_sfnetwork(sln@sl)
net_t = net %>%
  activate("edges") %>%
  dplyr::mutate(length = sf::st_length(.))
igraph::shortest_paths(graph = net_t, 1, 9)$vpath
#> Warning in igraph::shortest_paths(graph = net_t, 1, 9): At
#> structural_properties.c:745 :Couldn't reach some vertices
#> [[1]]
#> + 0/187 vertices, from 05a320d:

# rnet
r_test = stplanr::sum_network_routes(sln = sln, start = 1, end = 9)
plot(r_test)

# calculate shortest paths
# test with route_local
l_start_points = lwgeom::st_startpoint(l)
l_end_points = lwgeom::st_endpoint(l)
r1 = stplanr::route_local(sln = sln, from = l_start_points[1], to = l_end_points[2])
plot(r1$geometry, col = "red", lwd = 5)

# calculate shortest paths
sp = stplanr::route(
  l = l,
  route_fun = stplanr::route_local,
  sln = sln
)
#> Most common output is sf
#> Loading required namespace: data.table
plot(st_geometry(sln@sl))
plot(st_geometry(l), add = TRUE, lwd = 5)
plot(sp["route_number"], add = TRUE, lwd = rev(sp$route_number) * 3)

Created on 2020-05-02 by the reprex package (v0.3.0)

Retrieving or editing attributes of sf objects inside sfnetwork objects

Is your feature request related to a problem? Please describe.
Inside the network structure, nodes and edges are not sf objects anymore. This means that it is not straightforward to access sf object attributes like $sf_column and $agr without first escaping the network structure and then recreating the network. Therefore, it is not supported yet to provide a string to st_set_geometry and to change the attribute-geometry-relationships of columns (without escaping the network structure).

Describe the solution you'd like
It would be nice to access the attributes without escaping the network structure. I think tidygraph has a solution for this already (see here), but I have to look into that.

add_edges and add_vertices break with geometry columns

Describe the bug
Before, the bind functionalities of tidygraph (i.e. bind_graphs, bind_nodes and bind_edges) did not work for sfnetwork objects because bind_rows of dplyr did not work for sf objects. With the development versions of dplyr (1.0.0.9000), vctrs (0.3.1.9000) and sf (0.9-4), this problem is solved. That makes that bind_graphs now works good for sfnetwork objects:

library(sfnetworks)

sn = as_sfnetwork(roxel, directed = FALSE)

c(igraph::vcount(sn), igraph::ecount(sn))
#> 701 851

snb = tidygraph::bind_graphs(sn, sn)

c(igraph::vcount(snb), igraph::ecount(snb))
#> 1402 1702

However, bind_nodes still fails, because of a new, weird error:

nsf = sf::st_as_sf(sn)

tidygraph::bind_nodes(sn, nsf)
#> Error in UseMethod("st_bbox") : 
#>  no applicable method for 'st_bbox' applied to an object of class "logical"

The error is caused by igraph::add_vertices, which is used internally in bind_nodes. It is a mystery for me why that function would call st_bbox, but I did not look into it yet. What I did find is that the same error occurs when using as_tibble(nsf) instead of nsf, so it is really the geometry list column that causes the problem, not just the sf class.

For edges (i.e. using tidygraph::bind_edges) RStudio even crashes completely (i.e. aborted because of fatal error). Here, the error is caused by igraph::add_edges.

The add_vertices and add_edges functions are also used inside right joins and full joins, which also fail.

R Session Info

sessionInfo()
#> R version 3.6.3 (2020-02-29)
#> Platform: x86_64-pc-linux-gnu (64-bit)
#> Running under: Ubuntu 16.04.6 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/libblas/libblas.so.3.6.0
#> LAPACK: /usr/lib/lapack/liblapack.so.3.6.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=de_AT.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=de_AT.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=de_AT.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=de_AT.UTF-8 LC_IDENTIFICATION=C       
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] igraph_1.2.5     sf_0.9-4         tidygraph_1.2.0  dplyr_1.0.0.9000
#> [5] sfnetworks_0.2.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] Rcpp_1.0.4.6       rstudioapi_0.11    magrittr_1.5      
#>  [4] units_0.6-6        tidyselect_1.1.0   R6_2.4.1          
#>  [7] rlang_0.4.6.9000   fansi_0.4.1        tools_3.6.3       
#> [10] grid_3.6.3         utf8_1.1.4         KernSmooth_2.23-17
#> [13] cli_2.0.2          e1071_1.7-3        DBI_1.1.0         
#> [16] ellipsis_0.3.1     class_7.3-16       assertthat_0.2.1  
#> [19] tibble_3.0.1       lwgeom_0.2-4       lifecycle_0.2.0   
#> [22] crayon_1.3.4       tidyr_1.0.2        purrr_0.3.4       
#> [25] vctrs_0.3.1.9000   glue_1.4.1         compiler_3.6.3    
#> [28] pillar_1.4.4       generics_0.0.2     classInt_0.4-3    
#> [31] pkgconfig_2.0.3  

Option to create linestrings for edges during construction

Is your feature request related to a problem? Please describe.
Right now, if you provide an sf objects as nodes, but a regular data frame with a to and from column as edges, the sfnetwork construction function simply creates the network with those two objects, and does not create linestrings for the edges. This is probably good for some use-cases, but in other use-cases, it might be nicer if the construction function would already create the spatially explicit edges, instead of the user having to do this himself in either pre or post processing.

Describe the solution you'd like
An option edges_as_lines in the sfnetwork() construction function. If TRUE, the returned network will always be spatially explicit, no matter if you provided the edges as sf object or not. If FALSE, the edges will always be spatially implicit.

Cannot install package

Hi @luukvdmeer,
Thanks for writing this package. I had installed it and was using it for a project. I changed computers and when I tried to reinstall I cannot do it anymore. I am assuming an R update broke something here.
The error code is shown below. I would appreciate any help.
Syed

  • installing source package 'sfnetworks' ...
    ** using staged installation
    ** R
    ** data
    *** moving datasets to lazyload DB
    ** byte-compile and prepare package for lazy loading
    Error in findpack(package, lib.loc) :
    there is no package called 'sfnetworks'
    Calls: -> findpack
    Execution halted
    ERROR: lazy loading failed for package 'sfnetworks'
  • removing '\DOMAIN/User$/UserID/documents/R/win-library/3.6/sfnetworks'
    Error: Failed to install 'sfnetworks' from GitHub:
    (converted from warning) installation of package β€˜C:/Users/UserID/AppData/Local/Temp/2/RtmpiAcKRV/file2d8c3de4708/sfnetworks_0.0.0.9000.tar.gz’ had non-zero exit status

Not all tidygraph functions work on sfnetwork objects

Although sfnetwork subclasses tbl_graph, not all of tidygraph functions work on sfnetwork objects. This is caused by the geometry list column, and the nodes and edges being sf objects.

The problems occur because of two reasons:

1. Usage of igraph::add_vertices and/or igraph::add_edges inside functions

For some reason these functions break when there are list columns in the nodes and/or edges. The error given when using igraph::add_vertices is:

#> Error in UseMethod("st_bbox") : 
#>  no applicable method for 'st_bbox' applied to an object of class "logical"

It is a mystery for me why that function would call sf::st_bbox, but I did not look into it yet. When using igraph::add_edges RStudio even crashes completely (i.e. aborted because of fatal error).

The affected functions are:

  • bind_nodes
  • bind_edges
  • right_join
  • full_join

The latter two are generics and the issue could be solved by writing a sfnetwork method for them. The first two are not generics.

2. Recreating a tbl_graph object inside a function

Creating a tbl_graph with tidygraph::tbl_graph does not work when the edges are an sf object, because of the sticky geometry column.

The affected functions are:

  • graph_join
  • reroute

The first is not a generic. The latter is a generic so we can solve the issue by writing a sfnetwork method for it.

Website for sfnetworks documentation

  • Get continuous integration service working, I suggest GH actions
  • Create gh-pages branch and add website created by pkgdown
  • Make website update with every commit to master

Snap spatial points to the nearest point on the nearest edge

Please describe the problem.
It would be nice to snap any given point to its nearest point on the network, no matter if that is an existing node or not. Then, that nearest network point can be added as a new node to the network, by splitting the edge it is located on (using for example lwgeom::st_split).

In sf, it is already possible to find the nearest point on a feature to another given feature. A quick function I wrote to do this:

# x is a set of input points.
# y is the edges element of the sfnetwork, containing lines.
st_snap_to_network = function(x, y) {
  # Function to find the nearest edge point to a single input point.
  f = function(z) {
    # Convert sfg to sfc.
    z = sf::st_sfc(z, crs = sf::st_crs(x))
    # Run st_nearest_points.
    # This returns for each combination (z, y) a linestring from z to the
    # .. nearest point on y.
    np = sf::st_nearest_points(z, y)
    # Then, the endpoint of the np-line to the nearest feature y to z
    # .. is the nearest edge point to z.
    lwgeom::st_endpoint(np[sf::st_nearest_feature(z, y),])
  }
  # Run f for all input points.
  geoms = do.call("c", lapply(sf::st_geometry(x), f))
  # Replace the geometries of x with the corresponding nearest edge points.
  if (inherits(x, "sf")) st_geometry(x) = geoms
  if (inherits(x, "sfc")) x = geoms
  x
}

However, there is a problem with this approach. The returned point does not always intersect with the network.. Sometimes it is located a few millimetres away from the edge. This has to do with floating point precision.

See:
https://gis.stackexchange.com/questions/288988/nearest-point-does-not-intersect-line-in-sf-r-package
And:
r-spatial/sf#790

This reply does not sound hopeful:

I think this problem cannot be solved. Setting precision essentially rounds coordinates to a (scaled) integer grid. If you want to represent points on lines connecting points on an integer grid exactly, you need to store them as rational numbers, i.e. as a ratio of two (possibly very large) integers. What R (or GEOS) does is approximating this ratio when storing it as an eight byte double. By that, we're back at R FAQ 7.31.

Describe how you currently solve the problem, or how attempted to solve it
There is a recent blogpost addressing this problem, which uses a solution of drawing a small buffer around the returned nearest network point:
https://ryanpeek.org/mapping-in-R-workshop/03_vig_snapping_points_to_line.html

Another option would be to include the returned nearest network point in the edge, by moving the edge slightly.

Describe the desired functionality of sfnetworks
A function snap_to_network that replaces the geometry of the input point by the geometry of its nearest network point, that is guaranteed to intersect with the network. This can then also be used in shortest paths calculations between any set of given points.

Smoothing on a linear network

Is your feature request related to a problem? Please describe.
I know that this is a very difficult problem with no general solution and thousands of different approaches (see the references at the bottom of the issue), but I think we should code a few functions that implement basic smoothing approaches on a linear network. AFAIK the idea of smoothing spatial estimates for areal data is very common in epidemiology using bayesian methods (e.g. here, here and here). I will explore other R packages that already implemented this procedure for polygonal areas.

Describe the solution you'd like
Given a fixed spatial network and corresponding measurements, return the same network data after smoothing the measurements. I'm not 100% sure that we can straightforwardly apply the same methods as for polygonal areas.

Describe alternatives you've considered
For example this is the code to implement the simplest (and ugliest) spatial smoother.

# create a very simple network
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, PROJ 4.9.3
library(tmap)

network_column <- st_sfc(
  st_linestring(matrix(c(0, -2, 0, -1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(0, 2, 0, 1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(-2, 0, -1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, -2, 2, -1, 2, 0), ncol = 2, byrow = TRUE)),
  st_linestring(matrix(c(2, 2, 2, 1, 2, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 3, 0, 4, 0), ncol = 2, byrow = TRUE))
)

# assign some random measurements to all edges
set.seed(1)
network_data <- st_as_sf(
  data.frame(measurements = rpois(7, runif(7, 0, 4))), 
  geometry = network_column
)

tm_shape(network_data) + 
  tm_lines(col = "measurements", palette = "viridis", style = "cat")
#> Warning: Currect projection of shape network_data unknown. Long-lat (WGS84) is
#> assumed.

# estimate neighborhoods matrix
network_neigh <- st_touches(network_data)

# each measure is smoothed using a weighted average between its value and the
# mean of the values in it's neighborhood:
# alpha * x_i + (1 - alpha) * mean(x_i)
# where the mean is estimated wrt to the neighboord of the ith observation and
# alpha is the mixing proportion. For simplicity I set alpha = 0.5. For
# examples:

neighborhood_means <- vapply(
  X = network_neigh, 
  FUN = function(x) mean(network_data[x, "measurements", drop = TRUE]), 
  FUN.VALUE = numeric(1)
)

network_data$smoothed_measurements <- 0.5 * network_data[["measurements"]] + 0.5 * neighborhood_means

tm_shape(network_data) + 
  tm_lines(col = "smoothed_measurements", palette = "viridis", style = "cont")
#> Warning: Currect projection of shape network_data unknown. Long-lat (WGS84) is
#> assumed.

Created on 2020-02-05 by the reprex package (v0.3.0)

I'm not saying that this is a valid spatial smoother (and it's not) but I think it should give the right idea of what I'm talking about.

References

  • Banerjee, S., Carlin, B. P., & Gelfand, A. E. (2014). Hierarchical modeling and analysis for spatial data. Chapman and Hall/CRC.
  • Besag J, York J, Molli ́e A (1991). β€œBayesian image restoration with applications in spatialstatistics (with discussion).”Annals of the Institute of Statistical Mathematics,43, 1–59
  • Lawson AB (2009).Bayesian Disease Mapping: Hierarchical Modeling In Spatial Epidemiol-ogy. Chapman and Hall/CRC.

Calculating City Circulation Plans

Circulation plans are grids of Low Traffic Neighbourhoods used to keep motorised traffic on main roads.

β€œLow traffic neighbourhoods” are groups of residential streets, bordered by main or β€œdistributor” roads (the places where buses, lorries, non-local traffic should be), where β€œthrough” motor vehicle traffic is discouraged or removed.”

Many cities and towns are now implementing circulation plans. Is there a way to automatically identify main roads (e.g. google traffic), identify LTNs, and then identify the optimal central placements of a road blocks/bus gates for each LTN?

If not can an area be defined as an LTN and the optimal road blocks identified?
EQfDDYFXUAAh-Nh

Creating circulation plans for cities and towns is going to be a key for urban planning for the next 10+ years.

More methods for as_sfnetwork

Is your feature request related to a problem? Please describe.
Right now, you can already convert a lot of graph objects to an sfnetwork object, if 1) they are a tbl_graph or can be converted to a tbl_graph with as_tbl_graph AND 2) at least the nodes can be converted to an sf object with st_as_sf. The as_tbl_graph function supports a large amount of graph data structures in R.

However, in sfnetwork we also want to support the most important spatial network data structures that already exist.

Describe the solution you'd like
More methods for as_sfnetwork, addressing the most important spatial network data structures in R.

An overview (this list will be updated):

  • tbl_graph from tidygraph
  • sf from sf
  • sfc from sf
  • dodgr_streetnet from dodgr
  • SpatialLinesNetwork from stplanr
  • linnet from spatstat (https://rdrr.io/cran/spatstat/man/linnet.html).
  • mgNetwork from rmangal
  • SpatialNetwork from spnetwork

@Robinlovelace @agila5 @loreabad6 and others, please feel free to add more classes in the comments!

Shortest path distance between edges and other sets of points

Is your feature request related to a problem? Please describe.
At the moment I'm working on statistical models for count data on linear networks and one of the most difficult part of the analysis is the estimation of several network measurements that represent the distance of each edge wrt other points in the network (such as road intersections, important buildings and so on). I think that this is an important part any road safety analysis (as documented here and in several other papers) based on network data.

Describe the solution you'd like
A function or a set of functions that estimate the shortest path distance of each edge in the road network wrt a fixed set of points/nodes.

Describe alternatives you've considered
It was much much much much more difficult than I expected but this is a reprex of the result.

# packages
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, PROJ 4.9.3
library(tmap)
library(igraph)
#> 
#> Attaching package: 'igraph'
#> The following objects are masked from 'package:stats':
#> 
#>     decompose, spectrum
#> The following object is masked from 'package:base':
#> 
#>     union
library(tidygraph)
#> 
#> Attaching package: 'tidygraph'
#> The following object is masked from 'package:igraph':
#> 
#>     groups
#> The following object is masked from 'package:stats':
#> 
#>     filter
library(stplanr)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:igraph':
#> 
#>     as_data_frame, groups, union
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(purrr)
#> 
#> Attaching package: 'purrr'
#> The following objects are masked from 'package:igraph':
#> 
#>     compose, simplify

# create a very simple network
network_column <- st_sfc(
  st_linestring(matrix(c(0, -2, 0, -1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(0, 2, 0, 1, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(-2, 0, -1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 1, 0, 0, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, -2, 2, -1, 2, 0), ncol = 2, byrow = TRUE)),
  st_linestring(matrix(c(2, 2, 2, 1, 2, 0), ncol = 2, byrow = TRUE)), 
  st_linestring(matrix(c(2, 0, 3, 0, 4, 0), ncol = 2, byrow = TRUE)), 
  crs = 3003
)
network <- st_as_sf(
  data.frame(ID = seq(7)), 
  geometry = network_column
)

# add two points to the network
road_junctions <- st_sfc(
  st_point(c(-2, 0)), 
  st_point(c(4, 0)), 
  crs = 3003
)

# plot
tm_shape(network) + 
  tm_lines() + 
tm_shape(road_junctions) + 
  tm_dots(col = "red", size = 0.5)

# Transform network into sf-network. The function is inspired by the blogpost.
my_transform_into_sf_network <- function(network) {
  # 1. Give each edge a unique index
  edges <- network
  edges[["edges_ID"]] <- seq_len(nrow(edges))
  
  # 2. Creates nodes at the start and end point of each edge
  nodes <- data.frame(st_coordinates(edges))
  indexes <- lapply(
    split(1:nrow(nodes), nodes[, "L1", drop = TRUE]),
    function(x) c(x[1], x[length(x)])
  )
  nodes <- nodes[do.call("c", indexes), ]
  nodes[, "stard_end"] <- rep(c('start', 'end'), times = nrow(nodes)/2)
  names(nodes)[names(nodes) == "L1"] <- "edgeID"
  
  # 3. Give each node a unique index
  xy <- paste(nodes$X, nodes$Y)
  nodes$nodeID <- as.numeric(factor(xy))
  
  # 4. Combine the node indices with the edges
  edges$from <- subset(nodes, stard_end == "start", nodeID, drop = TRUE)
  edges$to <- subset(nodes, stard_end == "end", nodeID, drop = TRUE)
  
  # 5. Remove duplicate nodes and transform as sf
  nodes <- st_as_sf(
    x = nodes[!duplicated(nodes$nodeID), ],
    coords = c("X", "Y"),
    crs = st_crs(edges)
  )
  
  # 6. Convert to tbl graph
  graph <- tbl_graph(
    nodes = nodes,
    edges = as.data.frame(edges),
    directed = FALSE
  )
  
  # 7. Add edges lenghts
  graph <- graph %>% 
    activate(edges) %>% 
    mutate(length = st_length(geometry))
  
  graph
}
network_graph <- my_transform_into_sf_network(network)

# Calculate nodeID for road junction points. We should work on some ways to
# automate this feature and match rows_ids with nodes_ids. 
graph_nearest_nodes <- function(points, network_graph) {
  rows_ids <- st_nearest_feature(
    points, 
    network_graph %>% activate(nodes) %>% as_tibble() %>% st_as_sf(crs = 3003)
  )
  igraph_nodes_ids <- (network_graph %>% activate(nodes) %>% pull(nodeID))[rows_ids]
  igraph_nodes_ids
}
ids_of_junctions <- graph_nearest_nodes(road_junctions, network_graph)
ids_of_junctions
#> [1] 1 8

# The idea here is to calculate the matrix of distances between all pairs of
# nodes that identify each edge and the junction nodes and take the minimum
# distance. We could also consider the max distance, the mean distance and so on. 
result <- network_graph %>%
  activate(edges) %>%
  as_tibble() %>%
  st_as_sf() %>%
  mutate(graph_distance = pmap_dbl(list(from, to), 
    ~ distances(network_graph, v = c(...), to = ids_of_junctions, 
                weights = network_graph %>% activate(edges) %>% pull(length)) %>% 
      min()
  ))


tm_shape(result) + 
  tm_lines(col = "graph_distance", style = "cat", palette = "viridis") + 
  tm_shape(road_junctions) + 
  tm_dots(col = "red", size = 0.5)

Created on 2020-02-11 by the reprex package (v0.3.0)

Multi-modal routing with dodgr and gtfsrouter

The hackathon description claims that,

no mature, well tested and β€˜tidy’ class exists for spatial network data.

The sfnetworks package represents just one approach to this issue. An alternative representation is provided by the package dodgr, which stands for "Distances on Directed GRaphs". dodgr represents networks as single rectangular data.frames in which each row is an edge. The package demands that each vertex be given a unique identifier, enabling for example the representation of overpasses in which two edges may cross at geometrically identical points yet may not intersect. This insistence that each vertex have a unique identifier makes the transition to a point-based representation (analogous to the tidygraph approach of activating vertices) entirely trivial, enabling a single, coherent edge-based representation to be used at all times.

Not only is dodgr designed to represent and process directed graphs, it also represents dual-weighted graphs, in which each edge has two weights. Dual-weighted representations are essential for realistic routing through street networks, because the geometrically shortest path will often not be the best. Paths generally taken through street networks depend on preferences which differ for different modes of transport. Dual weighted routing systems calculate paths based on one set of weights reflective of typical preferences for a given mode of transport, while resultant distances are calculated using a different set of edge properties (for example direct geometric distances).

dodgr is primarily intended to perform "heavy lifting" tasks typically necessary to understand city-scale transport problems. The package can also calculate fastest rather than shortest paths. The resultant routing times reflect realistic temporal effects such as waiting times at traffic lights, for turning across oncoming traffic, and - for pedestrians and cyclists - the effects of incline. The ability to perform realistic routing is a pre-requisite for multi-modal routing, an ability currently only available in R through accessing an external service via the opentripplanner package.

A great hackathon task would be to combine the routing abilities of dodgr with the public transport routing offered by the gtfsrouter package to generate multi-modal routing between any two chosen points. It would be particularly useful to analyse how one might get between pairs of points by foot, on a bicycle, or combining either of these modes with public transport, comparing both times and distances.

Function that finds the nearest nodes or edges to a spatial feature

Is your feature request related to a problem? Please describe.
Especially when calculating shortest paths, it should be possible to enter any point in space as to and from. Then, a function should find the nearest node in the sfnetwork object, for both this to and from point, and calculate the shortest path in the network accordingly. Also, when you have additional data that you want to join with the network, but the coordinates are not exactly matching, this functionality would be useful.

Describe the solution you'd like
A function st_nearest_node that takes an sfnetwork object and an sf/sfg/sfc object. Then, it will find for each feature in the sf/sfg/sfc object the nearest node in the sfnetwork object, and replace the geometry of the feature with the geometry of its nearest node.

st_segmentize changes start/end point coordinates of edges for lat/long data

Describe the bug
When working with an sfnetwork object with activated edges and a CRS = 4326, running st_segmentize will change the start and end points of the LINESTRING geometry, from the 7th decimal point. This would break the network since the start and end nodes geometry will not correspond to the geometry of the extreme points of the edges.

Reproducible example

library(sf)
library(sfnetworks)
library(tidygraph)

net = as_sfnetwork(roxel) %>% 
  slice(1:7)

st_crs(net)
#> Coordinate Reference System:
#>   User input: EPSG:4326 
#>   wkt:
#> GEOGCS["WGS 84",
#>     DATUM["WGS_1984",
#>         SPHEROID["WGS 84",6378137,298.257223563,
#>             AUTHORITY["EPSG","7030"]],
#>         AUTHORITY["EPSG","6326"]],
#>     PRIMEM["Greenwich",0,
#>         AUTHORITY["EPSG","8901"]],
#>     UNIT["degree",0.0174532925199433,
#>         AUTHORITY["EPSG","9122"]],
#>     AUTHORITY["EPSG","4326"]]

geom1 = net %>% 
  activate('edges') %>% 
  st_segmentize(dfMaxLength = 2) %>% 
  st_geometry()
  
geom2 = net %>% 
  activate('edges') %>% 
  st_geometry()

print(st_coordinates(st_boundary(geom1)),digits = 22)
#>                       X                  Y L1
#> [1,] 7.5337216000000069 51.955558499999995  1
#> [2,] 7.5334608999999944 51.955761800000005  1
#> [3,] 7.5324423000000138 51.954220800000002  2
#> [4,] 7.5320901999999990 51.953283400000004  2
#> [5,] 7.5327090000000112 51.952087300000017  3
#> [6,] 7.5328685000000064 51.952565600000014  3

print(st_coordinates(st_boundary(geom2)),digits = 22)
#>                       X                  Y L1
#> [1,] 7.5337215999999998 51.955558500000002  1
#> [2,] 7.5334608999999997 51.955761799999998  1
#> [3,] 7.5324422999999996 51.954220800000002  2
#> [4,] 7.5320901999999998 51.953283399999997  2
#> [5,] 7.5327089999999997 51.952087300000002  3
#> [6,] 7.5328685000000002 51.952565600000000  3

st_boundary(geom1) == st_boundary(geom2)
#> [1] FALSE FALSE FALSE

According to r-spatial/sf#1078:

This is due to st_segmentize operating with respect to geographic distance and assuming great circle traversal.

So, the solution is:

you can avoid the problem by forcing it to work in the coordinate system you are actually using - what follows is effectively the same as your second example because you segmentize with a hardcoded value before setting CRS.

Giving:

geom3 = net %>% 
  activate('edges') %>% 
  st_set_crs(NA) %>%
  st_segmentize(dfMaxLength = 2) %>% 
  st_set_crs(4326) %>%
  st_geometry()

geom4 = net %>% 
  activate('edges') %>% 
  st_geometry()

print(st_coordinates(st_boundary(geom3)),digits = 22)
#>                       X                  Y L1
#> [1,] 7.5337215999999998 51.955558500000002  1
#> [2,] 7.5334608999999997 51.955761799999998  1
#> [3,] 7.5324422999999996 51.954220800000002  2
#> [4,] 7.5320901999999998 51.953283399999997  2
#> [5,] 7.5327089999999997 51.952087300000002  3
#> [6,] 7.5328685000000002 51.952565600000000  3

print(st_coordinates(st_boundary(geom4)),digits = 22)
#>                       X                  Y L1
#> [1,] 7.5337215999999998 51.955558500000002  1
#> [2,] 7.5334608999999997 51.955761799999998  1
#> [3,] 7.5324422999999996 51.954220800000002  2
#> [4,] 7.5320901999999998 51.953283399999997  2
#> [5,] 7.5327089999999997 51.952087300000002  3
#> [6,] 7.5328685000000002 51.952565600000000  3

st_boundary(geom3) == st_boundary(geom4)
#> [1] TRUE TRUE TRUE

NOTE:
This is not an issue for projected data:

net = as_sfnetwork(roxel) %>% 
  st_transform(5684) %>% 
  slice(1:7)

geom5 = net %>% 
  activate('edges') %>% 
  st_segmentize(dfMaxLength = 2) %>% 
  st_geometry()

geom6 = net %>% 
  activate('edges') %>% 
  st_geometry()

print(st_coordinates(st_boundary(geom5)),digits = 22)
#>                       X                  Y L1
#> [1,] 4193073.4805748016 5767235.7895007376  1
#> [2,] 4193056.9641923946 5767259.4923774172  1
#> [3,] 4192976.4583431017 5767092.4805315016  2
#> [4,] 4192945.8600853733 5766989.7559105381  2
#> [5,] 4192980.1846303302 5766854.1674023187  3
#> [6,] 4192994.4125140691 5766906.6669925889  3

print(st_coordinates(st_boundary(geom6)),digits = 22)
#>                       X                  Y L1
#> [1,] 4193073.4805748016 5767235.7895007376  1
#> [2,] 4193056.9641923946 5767259.4923774172  1
#> [3,] 4192976.4583431017 5767092.4805315016  2
#> [4,] 4192945.8600853733 5766989.7559105381  2
#> [5,] 4192980.1846303302 5766854.1674023187  3
#> [6,] 4192994.4125140691 5766906.6669925889  3

st_boundary(geom5) == st_boundary(geom6)
#> [1] TRUE TRUE TRUE

Expected behavior
I think st_segmentize should not be supported by sfnetworks. If a user needs to do a segmentize procedure, they can do so before turning it into a network.

R Session Info

Session info
devtools::session_info()
#> - Session info ---------------------------------------------------------------
#>  setting  value                       
#>  version  R version 3.6.3 (2020-02-29)
#>  os       Windows 10 x64              
#>  system   x86_64, mingw32             
#>  ui       RTerm                       
#>  language (EN)                        
#>  collate  English_United States.1252  
#>  ctype    English_United States.1252  
#>  tz       Europe/Berlin               
#>  date     2020-04-03                  
#> 
#> - Packages -------------------------------------------------------------------
#>  package     * version date       lib source        
#>  assertthat    0.2.1   2019-03-21 [1] CRAN (R 3.5.3)
#>  backports     1.1.5   2019-10-02 [1] CRAN (R 3.5.3)
#>  callr         3.4.3   2020-03-28 [1] CRAN (R 3.6.3)
#>  class         7.3-15  2019-01-01 [1] CRAN (R 3.5.2)
#>  classInt      0.4-2   2019-10-17 [1] CRAN (R 3.6.3)
#>  cli           2.0.2   2020-02-28 [1] CRAN (R 3.6.3)
#>  crayon        1.3.4   2017-09-16 [1] CRAN (R 3.5.0)
#>  DBI           1.1.0   2019-12-15 [1] CRAN (R 3.6.3)
#>  desc          1.2.0   2018-05-01 [1] CRAN (R 3.5.0)
#>  devtools      2.2.2   2020-02-17 [1] CRAN (R 3.6.3)
#>  digest        0.6.25  2020-02-23 [1] CRAN (R 3.6.3)
#>  dplyr         0.8.5   2020-03-07 [1] CRAN (R 3.6.3)
#>  e1071         1.7-3   2019-11-26 [1] CRAN (R 3.6.3)
#>  ellipsis      0.3.0   2019-09-20 [1] CRAN (R 3.5.3)
#>  evaluate      0.14    2019-05-28 [1] CRAN (R 3.5.3)
#>  fansi         0.4.1   2020-01-08 [1] CRAN (R 3.5.3)
#>  fs            1.3.2   2020-03-05 [1] CRAN (R 3.6.3)
#>  glue          1.3.2   2020-03-12 [1] CRAN (R 3.6.3)
#>  highr         0.8     2019-03-20 [1] CRAN (R 3.5.3)
#>  htmltools     0.4.0   2019-10-04 [1] CRAN (R 3.5.3)
#>  igraph        1.2.4.2 2019-11-27 [1] CRAN (R 3.6.3)
#>  KernSmooth    2.23-16 2019-10-15 [2] CRAN (R 3.6.3)
#>  knitr         1.28    2020-02-06 [1] CRAN (R 3.6.3)
#>  lifecycle     0.2.0   2020-03-06 [1] CRAN (R 3.6.3)
#>  lwgeom        0.2-1   2020-01-31 [1] CRAN (R 3.6.3)
#>  magrittr      1.5     2014-11-22 [1] CRAN (R 3.5.1)
#>  memoise       1.1.0   2017-04-21 [1] CRAN (R 3.5.0)
#>  pillar        1.4.3   2019-12-20 [1] CRAN (R 3.5.3)
#>  pkgbuild      1.0.6   2019-10-09 [1] CRAN (R 3.6.3)
#>  pkgconfig     2.0.3   2019-09-22 [1] CRAN (R 3.5.3)
#>  pkgload       1.0.2   2018-10-29 [1] CRAN (R 3.5.1)
#>  prettyunits   1.1.1   2020-01-24 [1] CRAN (R 3.6.3)
#>  processx      3.4.2   2020-02-09 [1] CRAN (R 3.6.3)
#>  ps            1.3.2   2020-02-13 [1] CRAN (R 3.6.3)
#>  purrr         0.3.3   2019-10-18 [1] CRAN (R 3.5.3)
#>  R6            2.4.1   2019-11-12 [1] CRAN (R 3.5.3)
#>  Rcpp          1.0.4   2020-03-17 [1] CRAN (R 3.6.3)
#>  remotes       2.1.1   2020-02-15 [1] CRAN (R 3.6.3)
#>  rlang         0.4.5   2020-03-01 [1] CRAN (R 3.6.3)
#>  rmarkdown     2.1     2020-01-20 [1] CRAN (R 3.5.3)
#>  rprojroot     1.3-2   2018-01-03 [1] CRAN (R 3.5.0)
#>  sessioninfo   1.1.1   2018-11-05 [1] CRAN (R 3.5.2)
#>  sf          * 0.9-0   2020-03-24 [1] CRAN (R 3.6.3)
#>  sfnetworks  * 0.2.0   2020-04-03 [1] local         
#>  stringi       1.4.6   2020-02-17 [1] CRAN (R 3.6.2)
#>  stringr       1.4.0   2019-02-10 [1] CRAN (R 3.5.2)
#>  testthat      2.3.2   2020-03-02 [1] CRAN (R 3.6.3)
#>  tibble        3.0.0   2020-03-30 [1] CRAN (R 3.6.3)
#>  tidygraph   * 1.1.2   2019-02-18 [1] CRAN (R 3.5.3)
#>  tidyr         1.0.2   2020-01-24 [1] CRAN (R 3.5.3)
#>  tidyselect    1.0.0   2020-01-27 [1] CRAN (R 3.5.3)
#>  units         0.6-6   2020-03-16 [1] CRAN (R 3.6.3)
#>  usethis       1.5.1   2019-07-04 [1] CRAN (R 3.6.3)
#>  vctrs         0.2.4   2020-03-10 [1] CRAN (R 3.6.3)
#>  withr         2.1.2   2018-03-15 [1] CRAN (R 3.5.0)
#>  xfun          0.12    2020-01-13 [1] CRAN (R 3.5.3)
#>  yaml          2.2.1   2020-02-01 [1] CRAN (R 3.5.2)
#> 
#> [1] C:/Users/Lore/Documents/R/win-library/3.6
#> [2] C:/Program Files/R/R-3.6.3/library

Licence

Hello there!
First of all, thanks for this library.
I was wondering, what's its licence?
Regards

Visualization of sfnetwork objects

The visualisation of the geographic components of sfnetwork objects is taken care of by sf and mapping packages such as leaflet, mapview, tmap and mapview and the graph component can be plotted by the igraph package. Worth considering other ways of visualising graphs? Just saw this and played with the interactive examples on the website and thought it may be of interest: https://github.com/JohnCoene/sigmajs

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.