Skip to content

Commit 93c2cc0

Browse files
committed
refactor: Align with igraph updates 🚧
1 parent 9213a71 commit 93c2cc0

File tree

6 files changed

+180
-67
lines changed

6 files changed

+180
-67
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ importFrom(igraph,is_dag)
229229
importFrom(igraph,is_directed)
230230
importFrom(igraph,is_simple)
231231
importFrom(igraph,k_shortest_paths)
232+
importFrom(igraph,make_graph)
232233
importFrom(igraph,mst)
233234
importFrom(igraph,reverse_edges)
234235
importFrom(igraph,shortest_paths)

R/ids.R

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,97 @@ evaluate_edge_query = function(data, query) {
188188
edges
189189
}
190190

191+
#' Extract for a given node in a spatial network the indices of adjacent nodes
192+
#'
193+
#' @param x An object of class \code{\link{sfnetwork}}.
194+
#'
195+
#' @param node The integer index of the node for which adjacent nodes should be
196+
#' queried.
197+
#'
198+
#' @param direction The direction of travel. Defaults to \code{'out'}, meaning
199+
#' that the direction given by the network is followed and a node is adjacent
200+
#' if it can be reached by an outgoing edge. May be set to \code{'in'}, meaning
201+
#' that the opposite direction is followed. May also be set to \code{'all'},
202+
#' meaning that the network is considered to be undirected. This argument is
203+
#' ignored for undirected networks.
204+
#'
205+
#' @returns A vector of integer indices specifying the adjacent nodes to the
206+
#' given node.
207+
#'
208+
#' @importFrom igraph adjacent_vertices igraph_opt igraph_options
209+
#' @noRd
210+
node_adjacent_ids = function(x, node, direction = "out") {
211+
# Change default igraph options.
212+
# This prevents igraph returns node or edge indices as formatted sequences.
213+
# We only need the "raw" integer indices.
214+
# Changing this option can lead to quite a performance improvement.
215+
default_igraph_opt = igraph_opt("return.vs.es")
216+
if (default_igraph_opt) {
217+
igraph_options(return.vs.es = FALSE)
218+
on.exit(igraph_options(return.vs.es = default_igraph_opt))
219+
}
220+
# Query adjacent nodes and correct for zero-based indexing if needed.
221+
adjacent_vertices(x, node, mode = direction)[[1]] + get_igraph_offset()
222+
}
223+
224+
#' Extract for each node in a spatial network the indices of incident edges
225+
#'
226+
#' @param x An object of class \code{\link{sfnetwork}}.
227+
#'
228+
#' @param nodes A vector of integer indices specifying the nodes for which
229+
#' incident edges should be queried.
230+
#'
231+
#' @returns A list in which each element is a vector of integer indices
232+
#' specifying the incident edges to one of the given nodes.
233+
#'
234+
#' @importFrom igraph incident_edges igraph_opt igraph_options
235+
#' @noRd
236+
node_incident_ids = function(x, nodes) {
237+
# Change default igraph options.
238+
# This prevents igraph returns node or edge indices as formatted sequences.
239+
# We only need the "raw" integer indices.
240+
# Changing this option can lead to quite a performance improvement.
241+
default_igraph_opt = igraph_opt("return.vs.es")
242+
if (default_igraph_opt) {
243+
igraph_options(return.vs.es = FALSE)
244+
on.exit(igraph_options(return.vs.es = default_igraph_opt))
245+
}
246+
# Query incident edges and correct for zero-based indexing if needed.
247+
ids = incident_edges(x, nodes, mode = "all")
248+
ids = lapply(ids, `+`, get_igraph_offset())
249+
ids
250+
}
251+
252+
#' Extract for a node pair in a spatial network the indices of connecting edges
253+
#'
254+
#' @param x An object of class \code{\link{sfnetwork}}.
255+
#'
256+
#' @param nodes A vector of two integer indices specifying the node pair
257+
#' between which edges should be found.
258+
#'
259+
#' @note If the network is directed, this function will only return the
260+
#' edges that go from the first node of the given pair to the second node
261+
#' of the given pair.
262+
#'
263+
#' @returns A vector of integer indices specifying the edges between the
264+
#' given nodes.
265+
#'
266+
#' @importFrom igraph get_edge_ids igraph_opt igraph_options
267+
#' @noRd
268+
node_connector_ids = function(x, nodes) {
269+
# Change default igraph options.
270+
# This prevents igraph returns node or edge indices as formatted sequences.
271+
# We only need the "raw" integer indices.
272+
# Changing this option can lead to quite a performance improvement.
273+
default_igraph_opt = igraph_opt("return.vs.es")
274+
if (default_igraph_opt) {
275+
igraph_options(return.vs.es = FALSE)
276+
on.exit(igraph_options(return.vs.es = default_igraph_opt))
277+
}
278+
# Query edge indices.
279+
get_edge_ids(x, nodes, error = TRUE)
280+
}
281+
191282
#' Extract for each edge in a spatial network the indices of incident nodes
192283
#'
193284
#' @param x An object of class \code{\link{sfnetwork}}.

R/igraph.R

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#' Run an igraph function on an sfnetwork object
2+
#'
3+
#' Since \code{\link{sfnetwork}} objects inherit \code{\link[igraph]{igraph}}
4+
#' objects, any igraph function can be called on a sfnetwork. However, if this
5+
#' function returns a network, it will be an igraph object rather than a
6+
#' sfnetwork object. With \code{\link{wrap_igraph}}, such a function will
7+
#' preserve the sfnetwork class, after checking if the network returned by
8+
#' igraph still has a valid spatial network structure.
9+
#'
10+
#' @param .data An object of class \code{\link{sfnetwork}}.
11+
#'
12+
#' @param .f An function from the \code{\link[igraph]{igraph}} package that
13+
#' accepts a graph as its first argument, and returns a graph.
14+
#'
15+
#' @param ... Arguments passed on to \code{.f}.
16+
#'
17+
#' @param .force Should network validity checks be skipped? Defaults to
18+
#' \code{FALSE}, meaning that network validity checks are executed when
19+
#' returning the new network. These checks guarantee a valid spatial network
20+
#' structure. For the nodes, this means that they all should have \code{POINT}
21+
#' geometries. In the case of spatially explicit edges, it is also checked that
22+
#' all edges have \code{LINESTRING} geometries, nodes and edges have the same
23+
#' CRS and boundary points of edges match their corresponding node coordinates.
24+
#' These checks are important, but also time consuming. If you are already sure
25+
#' your input data meet the requirements, the checks are unnecessary and can be
26+
#' turned off to improve performance.
27+
#'
28+
#' @param .message Should informational messages (those messages that are
29+
#' neither warnings nor errors) be printed when constructing the network?
30+
#' Defaults to \code{TRUE}.
31+
#'
32+
#' @return An object of class \code{\link{sfnetwork}}.
33+
#'
34+
#' @examples
35+
#' oldpar = par(no.readonly = TRUE)
36+
#' par(mar = c(1,1,1,1), mfrow = c(1,2))
37+
#'
38+
#' net = as_sfnetwork(mozart, "delaunay", directed = FALSE)
39+
#' mst = wrap_igraph(net, igraph::mst, .message = FALSE)
40+
#' mst
41+
#'
42+
#' plot(net)
43+
#' plot(mst)
44+
#'
45+
#' par(oldpar)
46+
#'
47+
#' @export
48+
wrap_igraph = function(.data, .f, ..., .force = FALSE, .message = TRUE) {
49+
out = .f(.data, ...) %preserve_all_attrs% .data
50+
if (! .force) validate_network(out, message = .message)
51+
out
52+
}
53+
54+
#' Get the offset of node and edge indices returned by igraph
55+
#'
56+
#' The functions \code{\link[igraph]{adjacent_vertices}} and
57+
#' \code{\link[igraph]{incident_edges}} used to return zero-based indices.
58+
#' Since v2.1.2, it returns one-based indices instead. To not fix the required
59+
#' igraph version to the latest release, this utility function finds the offset
60+
#' of returned indices compared to one-based indexing.
61+
#'
62+
#' @note This function assumes that the igraph option \code{return.vs.es} is
63+
#' set to \code{FALSE}!
64+
#'
65+
#' @returns An integer, 1 if zero-based indexing is used, and 0 if one-based
66+
#' indexing is used.
67+
#'
68+
#' @importFrom igraph adjacent_vertices make_graph
69+
#' @noRd
70+
get_igraph_offset = function() {
71+
if (! is.null(igraph_offset$offset)) return(igraph_offset$offset)
72+
net = make_graph(edges = c(1L, 2L))
73+
idx = as.integer(adjacent_vertices(net, v = 1L, mode = "out"))
74+
off = 2L - idx
75+
igraph_offset$offset = off
76+
off
77+
}
78+
79+
igraph_offset = new.env(parent = emptyenv())

R/smooth.R

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@
3838
#'
3939
#' @importFrom cli cli_abort
4040
#' @importFrom dplyr distinct slice
41-
#' @importFrom igraph adjacent_vertices decompose degree delete_vertices
42-
#' edge_attr get_edge_ids igraph_opt igraph_options incident_edges
43-
#' induced_subgraph is_directed vertex_attr
41+
#' @importFrom igraph decompose degree delete_vertices edge_attr get_edge_ids
42+
#' igraph_opt igraph_options induced_subgraph is_directed vertex_attr
4443
#' @importFrom sf st_as_sf st_cast st_combine st_crs st_drop_geometry
4544
#' st_equals st_is st_line_merge
4645
#' @export
@@ -88,8 +87,8 @@ smooth_pseudo_nodes = function(x, protect = NULL,
8887
pseudo_ids = which(pseudo)
8988
edge_attrs = st_drop_geometry(edges)
9089
edge_attrs = edge_attrs[, names(edge_attrs) %in% require_equal]
91-
incident_ids = incident_edges(x, pseudo_ids, mode = "all")
92-
check_equality = function(i) nrow(distinct(slice(edge_attrs, i + 1))) < 2
90+
incident_ids = node_incident_ids(x, pseudo_ids)
91+
check_equality = function(i) nrow(distinct(slice(edge_attrs, i))) < 2
9392
pass = do.call("c", lapply(incident_ids, check_equality))
9493
pseudo[pseudo_ids[!pass]] = FALSE
9594
}
@@ -147,15 +146,13 @@ smooth_pseudo_nodes = function(x, protect = NULL,
147146
# --> The index of the edge that comes in to the pseudo node set.
148147
# --> The index of the non-pseudo node at the other end of that edge.
149148
# We'll call this the source node and source edge of the set.
150-
# Note the + 1 since adjacent_vertices returns indices starting from 0.
151-
source_node = adjacent_vertices(x, n_i, mode = "in")[[1]] + 1
149+
source_node = node_adjacent_ids(x, n_i, direction = "in")
152150
source_edge = get_edge_ids(x, c(source_node, n_i))
153151
# Find the following:
154152
# --> The index of the edge that goes out of the pseudo node set.
155153
# --> The index of the non-pseudo node at the other end of that edge.
156154
# We'll call this the sink node and sink edge of the set.
157-
# Note the + 1 since adjacent_vertices returns indices starting from 0.
158-
sink_node = adjacent_vertices(x, n_o, mode = "out")[[1]] + 1
155+
sink_node = node_adjacent_ids(x, n_o, direction = "out")
159156
sink_edge = get_edge_ids(x, c(n_o, sink_node))
160157
# List indices of all edges that will be merged into the replacement edge.
161158
edge_idxs = c(source_edge, E, sink_edge)
@@ -181,8 +178,7 @@ smooth_pseudo_nodes = function(x, protect = NULL,
181178
if (length(N) == 1) {
182179
# When we have a single pseudo node that forms a set:
183180
# --> It will be adjacent to both adjacent nodes of the set.
184-
# Note the + 1 since adjacent_vertices returns indices starting from 0.
185-
adjacent = adjacent_vertices(x, N)[[1]] + 1
181+
adjacent = node_adjacent_ids(x, N)
186182
if (length(adjacent) == 1) {
187183
# If there is only one adjacent node to the pseudo node:
188184
# --> The two adjacent nodes of the set are the same node.
@@ -217,9 +213,8 @@ smooth_pseudo_nodes = function(x, protect = NULL,
217213
# We find them iteratively for the two boundary nodes of the set:
218214
# --> A boundary connects to one pseudo node and one non-pseudo node.
219215
# --> The non-pseudo node is the one not present in the pseudo set.
220-
# Note the + 1 since adjacent_vertices returns indices starting from 0.
221216
get_set_neighbour = function(n) {
222-
all = adjacent_vertices(x, n)[[1]] + 1
217+
all = node_adjacent_ids(x, n)
223218
all[!(all %in% N)]
224219
}
225220
adjacent = do.call("c", lapply(N_b, get_set_neighbour))

R/utils.R

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,3 @@
1-
#' Run an igraph function on an sfnetwork object
2-
#'
3-
#' Since \code{\link{sfnetwork}} objects inherit \code{\link[igraph]{igraph}}
4-
#' objects, any igraph function can be called on a sfnetwork. However, if this
5-
#' function returns a network, it will be an igraph object rather than a
6-
#' sfnetwork object. With \code{\link{wrap_igraph}}, such a function will
7-
#' preserve the sfnetwork class, after checking if the network returned by
8-
#' igraph still has a valid spatial network structure.
9-
#'
10-
#' @param .data An object of class \code{\link{sfnetwork}}.
11-
#'
12-
#' @param .f An function from the \code{\link[igraph]{igraph}} package that
13-
#' accepts a graph as its first argument, and returns a graph.
14-
#'
15-
#' @param ... Arguments passed on to \code{.f}.
16-
#'
17-
#' @param .force Should network validity checks be skipped? Defaults to
18-
#' \code{FALSE}, meaning that network validity checks are executed when
19-
#' returning the new network. These checks guarantee a valid spatial network
20-
#' structure. For the nodes, this means that they all should have \code{POINT}
21-
#' geometries. In the case of spatially explicit edges, it is also checked that
22-
#' all edges have \code{LINESTRING} geometries, nodes and edges have the same
23-
#' CRS and boundary points of edges match their corresponding node coordinates.
24-
#' These checks are important, but also time consuming. If you are already sure
25-
#' your input data meet the requirements, the checks are unnecessary and can be
26-
#' turned off to improve performance.
27-
#'
28-
#' @param .message Should informational messages (those messages that are
29-
#' neither warnings nor errors) be printed when constructing the network?
30-
#' Defaults to \code{TRUE}.
31-
#'
32-
#' @return An object of class \code{\link{sfnetwork}}.
33-
#'
34-
#' @examples
35-
#' oldpar = par(no.readonly = TRUE)
36-
#' par(mar = c(1,1,1,1), mfrow = c(1,2))
37-
#'
38-
#' net = as_sfnetwork(mozart, "delaunay", directed = FALSE)
39-
#' mst = wrap_igraph(net, igraph::mst, .message = FALSE)
40-
#' mst
41-
#'
42-
#' plot(net)
43-
#' plot(mst)
44-
#'
45-
#' par(oldpar)
46-
#'
47-
#' @export
48-
wrap_igraph = function(.data, .f, ..., .force = FALSE, .message = TRUE) {
49-
out = .f(.data, ...) %preserve_all_attrs% .data
50-
if (! .force) validate_network(out, message = .message)
51-
out
52-
}
53-
541
#' Determine duplicated geometries
552
#'
563
#' @param x An object of class \code{\link[sf]{sf}} or \code{\link[sf]{sfc}}.

man/wrap_igraph.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)