Skip to content

Commit c0a5088

Browse files
author
Developer
committed
Robustify graph build and path guess: direct coords, multiline handling
- build_graph_geos: extract start/end coordinates directly from linestrings via wk::wk_coords instead of geos_point_start/geos_point_end to avoid any precision drift; filter non-linestrings at entry - cnt_path_master: unnest multilinestrings before graph build - cnt_path_guess_master: add edge-count filter (>1 edge) to match old sfnetworks behaviour; if geos_line_merge returns multilinestring, keep the longest component
1 parent a07f016 commit c0a5088

3 files changed

Lines changed: 62 additions & 15 deletions

File tree

R/cnt_path.R

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,12 @@ cnt_path.SpatVector <-
187187

188188
cnt_path_master <-
189189
function(skeleton_sf, start_point_sf, end_point_sf) {
190-
# Build graph from geos linestrings
190+
# Convert to geos and ensure only linestrings
191191
skeleton_geos <- geos::as_geos_geometry(skeleton_sf)
192+
if (any(geos::geos_type(skeleton_geos) == "multilinestring")) {
193+
skeleton_geos <- geos::geos_unnest(skeleton_geos, keep_multi = FALSE)
194+
}
195+
192196
graph <- build_graph_geos(skeleton_geos)
193197

194198
# Find nearest graph nodes for start and end points

R/cnt_path_guess.R

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ cnt_path_guess.SpatVector <-
238238

239239
cnt_path_guess_master <-
240240
function(skeleton_geos) {
241-
if (geos::geos_type(skeleton_geos) == "multilinestring") {
241+
if (any(geos::geos_type(skeleton_geos) == "multilinestring")) {
242242
skeleton_geos <-
243243
geos::geos_unnest(skeleton_geos, keep_multi = FALSE)
244244
}
@@ -273,7 +273,7 @@ cnt_path_guess_master <-
273273
all_dist <- dijkstra(graph, from = closest_end_points)
274274

275275
other_dists <- all_dist$dist[other_nodes]
276-
valid <- is.finite(other_dists)
276+
valid <- is.finite(other_dists) & other_dists > 0
277277

278278
if (!any(valid)) {
279279
return(
@@ -283,7 +283,23 @@ cnt_path_guess_master <-
283283
)
284284
}
285285

286-
target_node <- other_nodes[which.max(other_dists)]
286+
# Filter to paths with > 1 edge (matching old sfnetworks behaviour)
287+
# and pick the longest among them
288+
candidate_nodes <- other_nodes[valid]
289+
candidate_dists <- other_dists[valid]
290+
291+
# Count edges in each path by reconstructing it
292+
edge_counts <- vapply(candidate_nodes, function(t) {
293+
p <- dijkstra(graph, from = closest_end_points, to = t)
294+
if (is.null(p)) 0L else length(p$edges)
295+
}, FUN.VALUE = integer(1))
296+
297+
long_enough <- edge_counts > 1L
298+
if (!any(long_enough)) {
299+
long_enough <- edge_counts > 0L
300+
}
301+
302+
target_node <- candidate_nodes[long_enough][which.max(candidate_dists[long_enough])]
287303

288304
# Reconstruct shortest path to the farthest outer node
289305
path <- dijkstra(graph, from = closest_end_points, to = target_node)
@@ -301,5 +317,13 @@ cnt_path_guess_master <-
301317
geos::geos_make_collection() |>
302318
geos::geos_line_merge()
303319

320+
# If line_merge couldn't produce a single linestring (e.g. tiny coord
321+
# gaps), keep the longest component
322+
if (geos::geos_type(longest_path_geos) == "multilinestring") {
323+
pieces <- geos::geos_unnest(longest_path_geos, keep_multi = FALSE)
324+
lens <- geos::geos_length(pieces)
325+
longest_path_geos <- pieces[which.max(lens)]
326+
}
327+
304328
longest_path_geos
305329
}

R/graph.R

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,37 @@
66
#' Endpoints are deduplicated using coordinate hashing (tolerance ~1e-10).
77
#' @noRd
88
build_graph_geos <- function(lines) {
9-
starts <- geos::geos_point_start(lines)
10-
ends <- geos::geos_point_end(lines)
11-
all_points <- c(starts, ends)
9+
# Keep only linestrings
10+
is_line <- geos::geos_type(lines) == "linestring"
11+
if (!all(is_line)) {
12+
lines <- lines[is_line]
13+
}
1214
n_edges <- length(lines)
15+
if (n_edges == 0) {
16+
stop("No linestring edges in skeleton.")
17+
}
18+
19+
# Extract start/end coordinates directly from the linestrings.
20+
# This avoids any precision drift from geos_point_start / geos_point_end.
21+
coords <- wk::wk_coords(lines)
22+
23+
# First coordinate of each linestring = start, last = end
24+
first_rows <- !duplicated(coords$feature_id)
25+
last_rows <- !duplicated(coords$feature_id, fromLast = TRUE)
1326

14-
# Deduplicate nodes via rounded coordinates.
15-
# Exact geos_equals can fail for shared Voronoi vertices due to tiny
16-
# floating-point differences introduced during intersection/clipping.
17-
coords <- wk::wk_coords(all_points)
18-
rounded <- paste(
19-
format(round(coords$x, 10), scientific = FALSE, trim = TRUE),
20-
format(round(coords$y, 10), scientific = FALSE, trim = TRUE),
27+
start_key <- paste(
28+
format(round(coords$x[first_rows], 10), scientific = FALSE, trim = TRUE),
29+
format(round(coords$y[first_rows], 10), scientific = FALSE, trim = TRUE),
2130
sep = ","
2231
)
23-
node_ids <- match(rounded, unique(rounded))
32+
end_key <- paste(
33+
format(round(coords$x[last_rows], 10), scientific = FALSE, trim = TRUE),
34+
format(round(coords$y[last_rows], 10), scientific = FALSE, trim = TRUE),
35+
sep = ","
36+
)
37+
38+
all_key <- c(start_key, end_key)
39+
node_ids <- match(all_key, unique(all_key))
2440
n_nodes <- max(node_ids)
2541

2642
from <- node_ids[seq_len(n_edges)]
@@ -37,6 +53,9 @@ build_graph_geos <- function(lines) {
3753
}
3854

3955
# One representative point per unique node
56+
starts <- geos::geos_point_start(lines)
57+
ends <- geos::geos_point_end(lines)
58+
all_points <- c(starts, ends)
4059
first_idx <- match(seq_len(n_nodes), node_ids)
4160
node_points <- all_points[first_idx]
4261

0 commit comments

Comments
 (0)