diff --git a/R/stroke.R b/R/stroke.R index 7851bc1..c26a629 100644 --- a/R/stroke.R +++ b/R/stroke.R @@ -141,18 +141,18 @@ best_link <- function(nodes, segments, links, edge_ids, flow_mode, return(linked_nodes) } + get_link_on_same_edge <- function(linked_segs, current_segment) { + is_same_edge <- edge_ids[linked_segs] == edge_ids[current_segment] + link_on_same_edge <- linked_segs[is_same_edge] + return(link_on_same_edge) + } + find_best_link <- function(node, opposite_node, current_segment) { linked_segs <- get_linked_segments(current_segment, node) - get_link_on_same_edge <- function(current_segment, edge_ids) { - is_same_edge <- edge_ids[linked_segs] == edge_ids[current_segment] - link_on_same_edge <- linked_segs[is_same_edge] - return(link_on_same_edge) - } - # if in flow mode, we look for a link on the same edge if (flow_mode) { - best_link <- get_link_on_same_edge(current_segment, edge_ids) + best_link <- get_link_on_same_edge(linked_segs, current_segment) } # if not in flow mode or if no link is found on the same edge, we look for # the best link by calculating the interior angles with all connections @@ -210,20 +210,6 @@ get_best_link <- function(angles, links, angle_threshold = 0) { return(best_link) } -#' @noRd -check_reciprocal <- function(best_links, side) { - # find the best link of the best links - bl <- best_links[best_links[, side], ] - # we check both ends to see whether the best link is reciprocal - is_best_link <- bl == seq_len(nrow(bl)) - # if we have a match on either of the sides, we keep the link - is_reciprocal <- apply(is_best_link, 1, any) - # fix for NA values - is_reciprocal[is.na(is_reciprocal)] <- FALSE - - return(is_reciprocal) -} - #' @noRd cross_check_links <- function(best_links) { links <- array(integer(), dim = dim(best_links)) @@ -240,6 +226,21 @@ cross_check_links <- function(best_links) { return(links) } +#' @noRd +check_reciprocal <- function(best_links, side) { + # find the best link of the best links + bl <- best_links[best_links[, side], ] + # we check both ends to see whether the best link is reciprocal + is_best_link <- bl == seq_len(nrow(bl)) + # if we have a match on either of the sides, we keep the link + is_reciprocal <- apply(is_best_link, 1, any) + # fix for NA values + is_reciprocal[is.na(is_reciprocal)] <- FALSE + + return(is_reciprocal) +} + +#' @noRd merge_lines <- function(nodes, segments, links, edge_ids, from_edge = NULL, attributes = FALSE, crs = NULL) { is_segment_used <- array(FALSE, dim = nrow(segments)) @@ -254,66 +255,31 @@ merge_lines <- function(nodes, segments, links, edge_ids, can_reuse_segments <- TRUE } - traverse_segments <- function(node, link, stroke_label) { - get_next <- function() { - # find node and segment connected to the current ones via the given link - # 1. get the nodes and segments connected to the given link - nodes <- segments[link, ] - segs <- links[link, ] - # 2. identify the position of the current node in the arrays (the current - # segment will be in the same position - is_current <- nodes == node - # 3. exclude current node and segment from the respective lists to find - # the new elements - return(list(node = nodes[!is_current], link = segs[!is_current])) - } - stroke <- c() - while (TRUE) { - if (is.na(link) || (is_segment_used[link] && !can_reuse_segments)) break - stroke_labels[edge_ids[link]] <- stroke_label - new <- get_next() - is_segment_used[link] <- TRUE - node <- new$node - link <- new$link - stroke <- c(node, stroke) - } - return(list(stroke = stroke, is_segment_used = is_segment_used, - stroke_labels = stroke_labels)) - } - - to_linestring <- function(node_id) { - points <- nodes[node_id, ] - linestring <- sfheaders::sfc_linestring(points, x = "x", y = "y") - return(linestring) - } - istroke <- 1 for (iseg in segment_ids) { if (is_segment_used[iseg]) next - stroke <- segments[iseg, ] - is_segment_used[iseg] <- TRUE - stroke_labels[edge_ids[iseg]] <- istroke - - # traverse forwards from the start node - node <- segments[iseg, "start"] - link <- links[iseg, "start"] - forward_result <- traverse_segments(node, link, istroke) - forward_stroke <- forward_result$stroke - is_segment_used <- forward_result$is_segment_used - stroke_labels <- forward_result$stroke_labels + stroke <- c(iseg) - # traverse backwards from the end node + # traverse forwards from the end node of the current segment node <- segments[iseg, "end"] link <- links[iseg, "end"] - backward_result <- traverse_segments(node, link, istroke) - backward_stroke <- rev(backward_result$stroke) - is_segment_used <- backward_result$is_segment_used - stroke_labels <- backward_result$stroke_labels - - # combine strokes and add to results - stroke <- c(forward_stroke, stroke, backward_stroke) - strokes <- c(strokes, to_linestring(stroke)) + stroke <- traverse_segments(stroke, node, link, can_reuse_segments, + segments, links, is_segment_used) + + # revert the stroke to traverse backwards from the start node of the + # current segment, then revert it back to the original direction + node <- segments[iseg, "start"] + link <- links[iseg, "start"] + stroke <- rev(stroke) + stroke <- traverse_segments(stroke, node, link, can_reuse_segments, + segments, links, is_segment_used) + stroke <- rev(stroke) + + # keep track of edge ids of strokes and add current stroke to strokes + is_segment_used[stroke] <- TRUE + stroke_labels[edge_ids[stroke]] <- istroke + strokes <- c(strokes, to_linestring(stroke, segments, nodes)) istroke <- istroke + 1 } @@ -325,3 +291,66 @@ merge_lines <- function(nodes, segments, links, edge_ids, return(strokes) } } + +#' @noRd +to_linestring <- function(stroke, segments, nodes) { + # extract the sequence of the nodes forming the stroke (with duplicates) + segs <- segments[stroke, , drop = FALSE] + if (nrow(segs) > 1) { + # get all but the last segment in the stroke (i.e. the "target" segments) + targets <- segs[-nrow(segs), , drop = FALSE] + # get the stroke segments that immediately follow the targets (these are all + # but the first segment in the stroke) + linked <- segs[-1, , drop = FALSE] + # for each pair of target/linked segments, find the duplicate nodes (we + # check both ends of the linked segments). The sequence of repeated nodes + # makes up the body of the stroke + is_duplicate <- targets == linked[, 1] | targets == linked[, 2] + # transpose both `targets` and `is_duplicate`, so that the duplicate nodes + # appear in the correct order when applying the mask (the "segment" + # dimension should run along the rows) + repeated_nodes <- t(targets)[t(is_duplicate)] + # we now have the body of the stroke, identify the first and last nodes + first_seg <- segs[1, ] + start <- first_seg[first_seg != repeated_nodes[1]] + last_seg <- segs[nrow(segs), ] + end <- last_seg[last_seg != repeated_nodes[length(repeated_nodes)]] + # concatenate the sequence of nodes + node_ids <- c(start, repeated_nodes, end) + } else { + # if we have a single segment, its two nodes already makes up the stroke + node_ids <- segs + } + # build the linestring geometry from the sequence of nodes + points <- nodes[node_ids, ] + linestring <- sfheaders::sfc_linestring(points, x = "x", y = "y") + return(linestring) +} + +#' @noRd +traverse_segments <- function(stroke, node, link, can_reuse_segments, + segments, links, is_segment_used) { + + get_next <- function(node, link) { + # find node and segment connected to the current ones via the given link + # 1. get the nodes and segments connected to the given link + nodes <- segments[link, ] + segs <- links[link, ] + # 2. identify the position of the current node in the arrays (the current + # segment will be in the same position + is_current <- nodes == node + # 3. exclude current node and segment from the respective lists to find + # the new elements + return(list(node = nodes[!is_current], link = segs[!is_current])) + } + + while (TRUE) { + if (is.na(link) || (link %in% stroke) || + (is_segment_used[link] && !can_reuse_segments)) break + stroke <- c(stroke, link) + new <- get_next(node, link) + node <- new$node + link <- new$link + } + return(stroke) +} diff --git a/tests/testthat/test-stroke.R b/tests/testthat/test-stroke.R index bb2921f..1cf05cc 100644 --- a/tests/testthat/test-stroke.R +++ b/tests/testthat/test-stroke.R @@ -24,7 +24,6 @@ test_that("a stroke is found in a very simple network", { # p4 # / # p1 - p2 - p3 - expected <- sf::st_sfc(sf::st_linestring(c(p1, p2, p3)), l3) actual <- stroke(sfc) expect_setequal(actual, expected) @@ -32,7 +31,7 @@ test_that("a stroke is found in a very simple network", { test_that("a ring is recognized as a stroke", { sfc <- sf::st_sfc(l2, l4, l6, l7) - expected <- sf::st_sfc(sf::st_linestring(c(p3, p6, p5, p2, p3))) + expected <- sf::st_sfc(sf::st_linestring(c(p2, p3, p6, p5, p2))) actual <- stroke(sfc) expect_setequal(actual, expected) }) @@ -144,6 +143,13 @@ test_that("edges are not split if flow_mode is true", { expect_setequal(actual, expected) }) +test_that("a ring is recognized as a stroke also in flow_mode", { + sfc <- sf::st_sfc(l2, l4, l6, l7) + expected <- sf::st_sfc(sf::st_linestring(c(p2, p3, p6, p5, p2))) + actual <- stroke(sfc, flow_mode = TRUE) + expect_setequal(actual, expected) +}) + test_that("strokes can be formed starting from a given edge", { new_l1 <- sf::st_linestring(c(p1, p2, p3)) sfc <- sf::st_sfc(new_l1, l4, l7) @@ -182,3 +188,10 @@ test_that("attributes can't be returned if edge is specified", { expect_error(stroke(sfc, attribute = TRUE, flow_mode = TRUE, from_edge = 3), "from_edge is not compatible with attributes or flow_mode") }) + +test_that("a ring is recognized when from_edge is specified", { + sfc <- sf::st_sfc(l2, l4, l6, l7) + expected <- sf::st_sfc(sf::st_linestring(c(p2, p3, p6, p5, p2))) + actual <- stroke(sfc, from_edge = 1) + expect_setequal(actual, expected) +})