Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ Imports:
wk (>= 0.9),
sf (>= 1.0),
geos (>= 0.2.4),
sfnetworks (>= 0.6),
checkmate,
igraph (>= 2.0.0)
Suggests:
Expand Down
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ centerline 0.3.0 (unreleased)
* `cnt_path()` now reports when start and end points cannot be connected
through the skeleton graph instead of passing an empty path to GEOS.

### UPDATES

* Skeleton routing and anchor graphs now use igraph directly; the
dependency on sfnetworks has been removed. Public geometry APIs are
unchanged.

centerline 0.2.5 (2026-03-12)
=========================

Expand Down
107 changes: 38 additions & 69 deletions R/cnt_path.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
#' \code{start_point} parameters.
#'
#' @details
#' The following function uses the [sfnetworks::st_network_paths()] approach to
#' connect \code{start_point} with \code{end_point} by using the
#' \code{skeleton} of a closed polygon as potential routes.
#' The function connects start and end points with weighted shortest paths
#' on an undirected skeleton graph using [igraph::shortest_paths()].
#' The \code{skeleton} of a closed polygon provides the potential routes.
#'
#' It is important to note that multiple starting points are permissible,
#' but there can only be **one ending point**. Should there be two or more
Expand Down Expand Up @@ -102,17 +102,11 @@ cnt_path.geos_geometry <- function(skeleton, start_point, end_point) {
stopifnot(check_points(end_point))
check_same_class(skeleton, start_point, end_point)

if (any(get_geom_type(skeleton) == "multilinestring")) {
skeleton <- geos::geos_unnest(skeleton, keep_multi = FALSE)
}

# Transform to sf
skeleton <- sf::st_as_sf(skeleton)
start_point <- sf::st_as_sf(start_point)
end_point <- sf::st_as_sf(end_point)

# Find the paths
cnt_path_master(skeleton, start_point, end_point)
cnt_path_master(
skeleton_geos = skeleton,
start_geos = start_point,
end_geos = end_point
)
}

#' @export
Expand All @@ -123,12 +117,12 @@ cnt_path.sf <- function(skeleton, start_point, end_point) {
stopifnot(check_points(end_point))
check_same_class(skeleton, start_point, end_point)

if (any(get_geom_type(skeleton) == "MULTILINESTRING")) {
skeleton <- sf::st_cast(skeleton, "LINESTRING")
}

# Find the paths
cnt_path_master(skeleton, start_point, end_point) |>
cnt_path_master(
skeleton_geos = geos::as_geos_geometry(skeleton),
start_geos = geos::as_geos_geometry(start_point),
end_geos = geos::as_geos_geometry(end_point)
) |>
sf::st_as_sf() |>
cbind(sf::st_drop_geometry(start_point))
}
Expand All @@ -140,12 +134,13 @@ cnt_path.sfc <- function(skeleton, start_point, end_point) {
stopifnot(check_points(start_point))
stopifnot(check_points(end_point))

if (any(get_geom_type(skeleton) == "MULTILINESTRING")) {
skeleton <- sf::st_cast(skeleton, "LINESTRING")
}

# Find the paths
cnt_path_master(skeleton, start_point, end_point) |> sf::st_as_sfc()
cnt_path_master(
skeleton_geos = geos::as_geos_geometry(skeleton),
start_geos = geos::as_geos_geometry(start_point),
end_geos = geos::as_geos_geometry(end_point)
) |>
sf::st_as_sfc()
}

#' @export
Expand All @@ -159,56 +154,34 @@ cnt_path.SpatVector <- function(skeleton, start_point, end_point) {
# Save CRS
crs <- terra::crs(skeleton)

# Transform to sf objects
skeleton <- sf::st_as_sf(skeleton)
start_point <- sf::st_as_sf(start_point)
end_point <- sf::st_as_sf(end_point)

if (any(get_geom_type(skeleton) == "MULTILINESTRING")) {
skeleton <- sf::st_cast(skeleton, "LINESTRING")
}
start_sf <- sf::st_as_sf(start_point)

# Find the paths
cnt_path_master(skeleton, start_point, end_point) |>
cnt_path_master(
skeleton_geos = terra_to_geos(skeleton),
start_geos = terra_to_geos(start_point),
end_geos = terra_to_geos(end_point)
) |>
wk::as_wkt() |>
as.character() |>
terra::vect(crs = crs) |>
cbind(sf::st_drop_geometry(start_point))
cbind(sf::st_drop_geometry(start_sf))
}


cnt_path_master <- function(skeleton_sf, start_point_sf, end_point_sf) {
# Convert skeleton sf object to sfnetworks
pol_network <- sfnetworks::as_sfnetwork(
x = skeleton_sf,
directed = FALSE,
length_as_weight = TRUE,
edges_as_lines = TRUE
)

# Convert sfnetworks to igraph
df_graph <- igraph::as_data_frame(pol_network)
names(df_graph)[3] <- "geometry"
df_graph <- df_graph[, c("weight", "geometry")]
df_graph$weight <- as.numeric(df_graph$weight)
cnt_path_master <- function(skeleton_geos, start_geos, end_geos) {
g <- skeleton_graph_build(skeleton_geos, crs = wk::wk_crs(skeleton_geos))

# Find indices of nearest nodes for start ...
start_nodes <- sf::st_nearest_feature(start_point_sf, pol_network)
# ... and end points
end_nodes <- sf::st_nearest_feature(end_point_sf, pol_network)
start_nodes <- skeleton_graph_nearest_nodes(g, start_geos)
end_nodes <- skeleton_graph_nearest_nodes(g, end_geos)

# Check if there are several end nodes
stopifnot("Only one end point is allowed" = length(end_nodes) == 1)

# Find the shortest path between two points
paths <- base::suppressWarnings(sfnetworks::st_network_paths(
pol_network,
from = end_nodes,
to = start_nodes,
weights = "weight"
))
# Route from the single end ID to all start IDs
edge_paths <- skeleton_graph_paths(g, from = end_nodes, to = start_nodes)

failed_start_indices <- which(lengths(paths$edge_paths) == 0L)
failed_start_indices <- which(lengths(edge_paths) == 0L)
if (length(failed_start_indices) > 0L) {
if (length(start_nodes) > 1L) {
stop(
Expand All @@ -225,17 +198,13 @@ cnt_path_master <- function(skeleton_sf, start_point_sf, end_point_sf) {
)
}

# Convert to GEOS geometries and create a GEOS collection
lines_list_geos <- lapply(paths$edge_paths, function(x) {
df_graph[x, "geometry"]
}) |>
lapply(geos::as_geos_geometry) |>
lapply(geos::geos_make_collection) |>
lapply(geos::geos_line_merge)
lines_list_geos <- lapply(edge_paths, function(epath) {
skeleton_graph_path_line(g, epath)
})

# Check if we need to reverse the lines
rev_lines_list <- reverse_lines_if_needed(lines_list_geos, end_point_sf)
rev_lines_list <- reverse_lines_if_needed(lines_list_geos, end_geos)

# Return pathes binded together as GEOS geometry
# Return paths binded together as GEOS geometry
do.call(c, rev_lines_list)
}
86 changes: 28 additions & 58 deletions R/cnt_path_guess.R
Original file line number Diff line number Diff line change
Expand Up @@ -244,74 +244,44 @@ cnt_path_guess.SpatVector <- function(
}

cnt_path_guess_master <- function(skeleton_geos) {
if (geos::geos_type(skeleton_geos) == "multilinestring") {
skeleton_geos <- geos::geos_unnest(skeleton_geos, keep_multi = FALSE)
}
g <- skeleton_graph_build(skeleton_geos, crs = wk::wk_crs(skeleton_geos))

skeleton_sf <- sf::st_as_sf(skeleton_geos)
# Convert skeleton to sfnetworks
pol_network <- sfnetworks::as_sfnetwork(
x = skeleton_sf,
directed = FALSE,
length_as_weight = TRUE,
edges_as_lines = TRUE
)
# Convert sfnetworks to igraph
# pol_graph <- igraph::as.igraph(pol_network)
df_graph <- igraph::as_data_frame(pol_network)
names(df_graph)[3] <- "geometry"
df_graph <- df_graph[, c("weight", "geometry")]
df_graph$weight <- as.numeric(df_graph$weight)
# find_outer_nodes expects unnested linework
outer_lines <- skeleton_geos
if (any(geos::geos_type(outer_lines) == "multilinestring")) {
outer_lines <- geos::geos_unnest(outer_lines, keep_multi = FALSE)
}

# Find border points of skeleton
closest_points <- find_closest_nodes(
pol_network,
find_outer_nodes(skeleton_geos)
)
terminal_ids <- find_closest_nodes(g, find_outer_nodes(outer_lines))

# Find the most distant point from center
# Find the most distant point from center (lowest closeness).
# It will serve as the end point
closest_end_points <- closest_points[which.min(igraph::closeness(
pol_network,
vid = closest_points
closest_end_points <- terminal_ids[which.min(igraph::closeness(
g$graph,
vids = terminal_ids,
weights = g$edges$weight
))]

# Find paths
paths <- base::suppressWarnings(sfnetworks::st_network_paths(
pol_network,
to = closest_points[closest_points != closest_end_points],
remaining <- terminal_ids[terminal_ids != closest_end_points]
edge_paths <- skeleton_graph_paths(
g,
from = closest_end_points,
weights = "weight"
))

# Paths lengths in counts
paths_length <- lengths(paths$edge_paths)

# Filter non-zero paths
paths_length_flag <- paths_length > 1
paths_length_nonzero <- paths_length[paths_length_flag]
edge_paths_nonzero <- paths$edge_paths[paths_length_flag]

# Estimate paths lengths
edge_paths_vec <- unlist(edge_paths_nonzero, use.names = FALSE)
edge_paths_groups <- rep(
seq_along(edge_paths_nonzero),
times = paths_length_nonzero
to = remaining
)
edge_paths_length <- df_graph[edge_paths_vec, "weight"]

# Sum paths lengths in meters
true_paths_igraph <- tapply(edge_paths_length, edge_paths_groups, FUN = sum)
# Filter non-zero multi-edge paths
paths_length <- lengths(edge_paths)
paths_length_flag <- paths_length > 1L
edge_paths_nonzero <- edge_paths[paths_length_flag]

# Return the longest path
longest_path_igraph <- which.max(true_paths_igraph)
longest_path_geos <- df_graph[
edge_paths_nonzero[[longest_path_igraph]],
"geometry"
] |>
geos::as_geos_geometry() |>
geos::geos_make_collection() |>
geos::geos_line_merge()
true_paths_igraph <- vapply(
edge_paths_nonzero,
function(epath) sum(g$edges$weight[epath]),
numeric(1),
USE.NAMES = FALSE
)

longest_path_geos
longest_path_igraph <- which.max(true_paths_igraph)
skeleton_graph_path_line(g, edge_paths_nonzero[[longest_path_igraph]])
}
Loading