diff --git a/DESCRIPTION b/DESCRIPTION index 3d42fa5..cd036b1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,7 +26,7 @@ Imports: dbscan, purrr, units, - rlang, + rlang Suggests: tibble, tmaptools, @@ -35,4 +35,6 @@ Suggests: Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 +Additional_repositories: https://josiahparry.r-universe.dev + diff --git a/R/corenet.R b/R/corenet.R index caeedb5..e8f33bf 100644 --- a/R/corenet.R +++ b/R/corenet.R @@ -580,6 +580,22 @@ removeDangles = function(network, tolerance = 0.001) { } +removeDangles_geos = function(network, tolerance = 0.001, tol_distance) { + network = geos::as_geos_geometry(central_leeds_osm) + network_linestring = geos::geos_unnest(network, keep_multi = FALSE) + coordinates_start = geos::geos_point_start(network_linestring) + coordinates_end = geos::geos_point_end(network_linestring) + coordinates = c(coordinates_start, coordinates_end) + coordinates_matrix = data.frame( + x = geos::geos_x(coordinates), + y = geos::geos_y(coordinates) + ) + # Find the unique coordinates: + c_txt = paste0(coordinates_matrix$x, coordinates_matrix$y) + c_tbl = table(c_txt) +} + + #' Create coherent network PMtiles #' @@ -637,3 +653,134 @@ create_coherent_network_PMtiles = function(folder_path, city_filename, cohesive_ return(system_output) } + +#' Create a plot of the network +#' +#' This function creates a plot of the network using ggplot2 and ggspatial. +#' @param network An sf object representing the network. +#' @param color The color to use for the network. +#' @param title The title for the plot. +#' @param output_file The filename for the output plot. +#' @param base_map A logical value indicating whether to include a base map. +#' @param line_width The width of the lines in the plot. +#' @return The plot object. +#' @export +#' + + +library(ggplot2) +library(sf) +library(ggspatial) +library(dplyr) +library(patchwork) +library(viridis) # Assuming viridis is used for color scales + +plot_networks = function(networks, colors, titles, output_file, ncol = 2, width = 12, height = 6, base_map = TRUE, line_width = 1, point_size = 1) { + + plots = list() # Create a list to hold individual plots + + for (i in seq_along(networks)) { + network_plot = ggplot() # Initialize ggplot + + if (base_map) { + network_plot = network_plot + annotation_map_tile(type = "osm") # Add OSM base map if enabled + } + + # Loop through each network data set + if (is.list(networks[[i]]) && length(networks[[i]]) == 2) { + for (j in 1:2) { + data = networks[[i]][[j]] + geom_type = sf::st_geometry_type(data)[1] + size_use = if (geom_type %in% c("POINT", "MULTIPOINT")) point_size else line_width + + if (is.character(colors[[i]][j]) && any(colors[[i]][j] %in% names(data))) { + network_plot = network_plot + + geom_sf(data = data, aes(color = factor(.data[[colors[[i]][j]]])), size = size_use) + } else { + network_plot = network_plot + + geom_sf(data = data, color = colors[[i]][j], size = size_use) + } + } + } else { + data = networks[[i]] + geom_type = sf::st_geometry_type(data)[1] + size_use = if (geom_type %in% c("POINT", "MULTIPOINT")) point_size else line_width + + if (is.character(colors[[i]]) && any(colors[[i]] %in% names(data))) { + network_plot = network_plot + + geom_sf(data = data, aes(color = factor(.data[[colors[[i]]]])), size = size_use) + } else { + network_plot = network_plot + + geom_sf(data = data, color = colors[[i]], size = size_use) + } + } + + # Apply axis formatting for rounded coordinates + network_plot = network_plot + + ggtitle(titles[[i]]) + + theme_minimal() + + theme(panel.background = element_rect(fill = "gainsboro"), legend.position = "right") + + scale_color_viridis(discrete = TRUE) + # Apply discrete color scale + scale_x_continuous(labels = function(x) format(round(x, 2), nsmall = 2)) + + scale_y_continuous(labels = function(y) format(round(y, 2), nsmall = 2)) + + plots[[i]] = network_plot + } + + combined_plot = patchwork::wrap_plots(plots, ncol = ncol) + ggsave(output_file, combined_plot, width = width, height = height) + + return(combined_plot) +} + + +#' Calculate the degree of each node in a network +#' +#' This function calculates the degree of each node in a network. +#' @param network_sf An sf object representing the network. +#' @param crs_proj The CRS code for the projection. +#' @return An sf object with the node degree. +#' @export +#' + +cal_node_degree <- function(network_sf, crs_proj = 27700) { + edges = st_cast(network_sf, "LINESTRING") |> st_transform(crs_proj) + + # Extract start and end coordinates of each line segment + edge_list = st_coordinates(edges) %>% + as.data.frame() %>% + group_by(L1) %>% + summarize( + x_start = first(X), + y_start = first(Y), + x_end = last(X), + y_end = last(Y) + ) + # Create node identifiers by concatenating coordinates + edges_df = edge_list %>% + mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) %>% + select(from, to) + # Create an undirected graph + g = graph_from_data_frame(edges_df, directed = FALSE) + + node_degree = degree(g) + + # Convert node_degree to a data frame + node_degree_df = data.frame( + node = names(node_degree), + degree = node_degree + ) + + # Split the node coordinates back into separate columns + node_degree_df = node_degree_df %>% + separate(node, into = c("X", "Y"), sep = ",", convert = TRUE) + + # Convert node_degree_df back to sf object + node_degree_sf = st_as_sf(node_degree_df, coords = c("X", "Y"), crs = st_crs(crs_proj)) + + return(node_degree_sf) + +} diff --git a/R/data.R b/R/data.R index baaabba..07244f9 100644 --- a/R/data.R +++ b/R/data.R @@ -53,6 +53,33 @@ #' data(NPT_demo_6km) #' head(NPT_demo_6km) "NPT_demo_6km" +NULL +#' Central Leeds OSM Network +#' +#' See the `data-raw` folder for the code used to generate this data set. +#' +#' @docType data +#' @keywords datasets +#' @name central_leeds_osm +#' @format An object of class \code{sf} (inherits from \code{data.frame}). +#' @examples +#' head(central_leeds_osm) +#' library(sf) # for plotting +#' plot(central_leeds_osm$geometry) +NULL +#' Edinburgh off road network +#' +#' This data set contains network data for Edinburgh's off-road network. +#' +#' @docType data +#' @keywords datasets +#' @name edinburgh_offroad +#' @format An object of class \code{sf} (inherits from \code{data.frame}). +#' @examples +#' data(edinburgh_offroad) +#' head(edinburgh_offroad) +#' library(sf) # for plotting +#' plot(edinburgh_offroad$geometry) NULL \ No newline at end of file diff --git a/README.Rmd b/README.Rmd index 78e722a..8bf0449 100644 --- a/README.Rmd +++ b/README.Rmd @@ -69,6 +69,15 @@ if (!require("remotes")) { remotes::install_github("nptscot/corenet") ``` +To install `corenet`, you may need to add an additional repository to access the `rsgeo` package: + +```{r} +options(repos = c( + josiahparry = 'https://josiahparry.r-universe.dev', + CRAN = 'https://cloud.r-project.org' +)) +install.packages('corenet') +``` Load the package with the following for local development: ```{r, include=FALSE} @@ -599,4 +608,5 @@ cycle_network = function(area, NPT_network, length_threshold = 1) { return(summarized_data) } -``` \ No newline at end of file +``` + diff --git a/data-raw/central_leeds.qmd b/data-raw/central_leeds.qmd new file mode 100644 index 0000000..70d51de --- /dev/null +++ b/data-raw/central_leeds.qmd @@ -0,0 +1,26 @@ +--- +format: gfm +--- + +The aim of this script is to generate a minimal example route network dataset for central Leeds for use in the package and testing. + +```{r, eval=FALSE, echo=FALSE} +# Test removeDangles +library(tidyverse) +headrow = central_leeds_osm |> + filter(name == "The Headrow") +plot(headrow$geometry) +central_leeds = sf::st_buffer(headrow, 100) +sf::sf_use_s2(FALSE) +central_leeds_osm = osmactive::get_travel_network(central_leeds, boundary = central_leeds, boundary_type = "clipsrc") + +plot(central_leeds_osm$geometry) +central_leeds_nodangle_10 = removeDangles(central_leeds_osm, tolerance = 10) +central_leeds_nodangle_default = removeDangles(central_leeds_osm) +plot(central_leeds_nodangle_10$geometry) # it works! +plot(central_leeds_nodangle_default$geometry) + +usethis::use_data(central_leeds_osm, overwrite = TRUE) + +``` + diff --git a/data-raw/osm_edinburgh_demo.R b/data-raw/osm_edinburgh_demo.R index 5df1373..629edba 100644 --- a/data-raw/osm_edinburgh_demo.R +++ b/data-raw/osm_edinburgh_demo.R @@ -152,3 +152,57 @@ cycle_net |> plot() usethis::use_data(osm_edinburgh_demo, overwrite = TRUE) + +find_component= function(rnet, threshold = 50) { + + sf::st_crs(rnet) = 27700 + + # Calculate the distance matrix between features + dist_matrix = sf::st_distance(rnet) + + # Convert the threshold to units of meters + threshold = units::set_units(threshold, "m") + + # Create a connectivity matrix where connections are based on the threshold distance + + connectivity_matrix = Matrix::Matrix(dist_matrix < threshold, sparse = TRUE) + + # Create an undirected graph from the adjacency matrix + graph = igraph::graph_from_adjacency_matrix(connectivity_matrix, mode = "undirected", diag = FALSE) + + # Find the connected components in the graph + components = igraph::components(graph) + + # Assign component membership to the road network + rnet$component = components$membership + + # Return the updated road network with component membership + return(rnet) +} + + +lads = sf::read_sf("D:/Github/nptscot/npt/inputdata/boundaries/la_regions_2023.geojson") + +target_zone = lads |> + dplyr::filter(LAD23NM == "City of Edinburgh") |> + sf::st_transform(crs = 27700) + +osm = osmactive::get_travel_network("Scotland", boundary = target_zone, boundary_type = "clipsrc") +cycle_net = osmactive::get_cycling_network(osm) +drive_net = osmactive::get_driving_network_major(osm) +cycle_net = osmactive::distance_to_road(cycle_net, drive_net) +cycle_net = osmactive::classify_cycle_infrastructure(cycle_net) +# filter cycle_net based on column bicycle is yes dismount adn designated +cycle_net = cycle_net |> + dplyr::filter(bicycle %in% c("yes", "dismount", "designated")) |> + dplyr::filter(cycle_segregation == "Separated cycle track") |> + dplyr::mutate(length = as.numeric(sf::st_length(geometry))) |> + dplyr::filter(length > 1) |> + sf::st_transform(crs = 27700) + +cycle_net = sf::st_cast(cycle_net, "LINESTRING") +cycle_net = cycle_net |> dplyr::select(geometry) +cycle_net$length = sf::st_length(cycle_net) +edinburgh_offroad = find_component(cycle_net, threshold = 1) + +usethis::use_data(edinburgh_offroad, overwrite = TRUE) diff --git a/data-raw/remove_dangles.qmd b/data-raw/remove_dangles.qmd new file mode 100644 index 0000000..cbc8348 --- /dev/null +++ b/data-raw/remove_dangles.qmd @@ -0,0 +1,1273 @@ +--- +format: gfm +--- + +# load leeds example data + +```{r} +devtools::load_all() + +library(sf) +library(ggplot2) +library(patchwork) +library(mapview) +library(dplyr) +library(igraph) + +leeds_net = central_leeds_osm +edin_offroad_net = edinburgh_offroad |> filter(component  == 7) +``` + +# load two remove dangles functions +removeDangles_1 +- Accepts the parameters network (an sf object) and tolerance (default = 0.001). +- Directly casts the sf object to LINESTRING using sf::st_cast +- The tolerance parameter sets a distance threshold to determine whether an endpoint is "isolated" (dangle) based on spatial proximity to other points. + +removeDangles_2 +- Accepts the parameters network (an sf object) and percentile (default = 0.012). +- Converts the sf object into an igraph object using sf_to_igraph, enabling the calculation of vertex degrees. This step helps identify vertices with a degree of 1, which are potential dangles. +- The percentile determines the length threshold for classifying short dangling lines for removal. + +Dangle Detection: +removeDangles_1: +- Uses spatial operations to detect dangles. It extracts the endpoints of each line segment and creates a spatial buffer around them using the tolerance value. +- By finding points that do not overlap with other buffered points, it identifies isolated (dangling) endpoints. This is more of a spatial proximity-based method. + +removeDangles_2: +- Uses graph theory to identify dangles. It finds vertices (nodes) with degree 1 (dangles) in the graph representation of the network. +- It then extracts the first and last coordinates of each line and checks if any endpoints are dangle vertices. This approach focuses on graph-based dangle identification. + +```{r} +removeDangles_1 = function(network, tolerance = 0.001) { + geometry_types = sf::st_geometry_type(network) + network_linestring = network[geometry_types == "LINESTRING", ] + network_others = network[geometry_types != "LINESTRING", ] + network_multilinestring = network[geometry_types == "MULTILINESTRING", ] + network_multilinestring = sf::st_cast(network_multilinestring, "LINESTRING") + + # Combine with original LINESTRING geometries + network_lines = rbind(network_linestring, network_multilinestring) + # Extract and combine all end points of line segments + end_points = do.call(rbind, lapply(network_lines$geometry, function(line) { + endpoints = rbind(sf::st_coordinates(line)[1, ], sf::st_coordinates(line)[nrow(sf::st_coordinates(line)), ]) + sf::st_as_sf(data.frame(x = endpoints[, 1], y = endpoints[, 2]), coords = c("x", "y"), crs = sf::st_crs(network)) + })) + + # Identify unique end points (potential dangles) using a spatial join to find nearby points + buffer_points = sf::st_buffer(end_points, dist = tolerance) + overlaps = sf::st_intersects(buffer_points, buffer_points, sparse = FALSE) + isolated_points = end_points[rowSums(overlaps) == 1,] + + # Filter out road segments that end in these isolated points + segments_with_dangles = sapply(sf::st_geometry(network_lines), function(geom) { + ends = sf::st_sfc(sf::st_point(sf::st_coordinates(geom)[1,]), sf::st_point(sf::st_coordinates(geom)[nrow(sf::st_coordinates(geom)),]), crs = sf::st_crs(network)) + any(sf::st_intersects(ends, isolated_points, sparse = FALSE)) + }) + + network_without_dangles = network_lines[!segments_with_dangles, ] + + return(network_without_dangles) +} + +removeDangles_2 = function(network, percentile = 0.012) { + + network$length = sf::st_length(network) + network_g = sf_to_igraph(network) + vertex_degrees = degree(network_g) + + dangle_vertices = which(vertex_degrees == 1) + + points = unique(do.call(rbind, lapply(st_geometry(network), function(line) { + rbind(st_coordinates(line)[1, ], st_coordinates(line)[nrow(st_coordinates(line)), ]) + }))) + # Extract indices of LINESTRINGs to check + line_indices = sapply(st_geometry(network), function(line) { + coords = st_coordinates(line) + c(which(points[,1] == coords[1,1] & points[,2] == coords[1,2]), + which(points[,1] == coords[nrow(coords),1] & points[,2] == coords[nrow(coords),2])) + }) + + # sum of network$length + sum_value = sum(network$length) + + # Median of network$length + threshold_length = percentile* sum_value + short_dangles = network[(line_indices[1,] %in% dangle_vertices | line_indices[2,] %in% dangle_vertices) & network$length < units::set_units(threshold_length, "meters"), ] + + network_clean = network[!((line_indices[1,] %in% dangle_vertices | line_indices[2,] %in% dangle_vertices) & network$length < units::set_units(threshold_length, "meters")), ] +} + +sf_to_igraph = function(network_sf) { + # network_sf = cycle_net_components_1 + # Assuming network_sf is LINESTRING + points = unique(do.call(rbind, lapply(network_sf$geometry, function(line) { + rbind(st_coordinates(line)[1, ], st_coordinates(line)[nrow(st_coordinates(line)), ]) + }))) + points = unique(points) + edges = do.call(rbind, lapply(network_sf$geometry, function(line) { + start = which(points[,1] == st_coordinates(line)[1,1] & points[,2] == st_coordinates(line)[1,2]) + end = which(points[,1] == st_coordinates(line)[nrow(st_coordinates(line)),1] & points[,2] == st_coordinates(line)[nrow(st_coordinates(line)),2]) + c(start, end) + })) + graph = graph_from_edgelist(as.matrix(edges), directed = FALSE) + return(graph) +} + +``` + +# compare removeDangles_1 using leeds network with different tolerance values +```{r} +RD_leeds_1 = removeDangles_1(leeds_net, tolerance = 0.001) +RD_leeds_2 = removeDangles_1(leeds_net, tolerance = 10) + +# Plot for network 1 +p1 = ggplot() + + geom_sf(data = leeds_net, color = "gray", size = 1) + + geom_sf(data = RD_leeds_1, color = "blue", size = 1) + + ggtitle("Network 1") + + theme_minimal() + +# Plot for network 2 +p2 = ggplot() + + geom_sf(data = leeds_net, color = "gray", size = 1) + + geom_sf(data = RD_leeds_2, color = "red", size = 1) + + ggtitle("Network 2") + + theme_minimal() + +# Combine both plots side by side using patchwork +combined_plot = p1 + p2 + plot_layout(ncol = 2) + +# Display the plot +print(combined_plot) +``` +# compare removeDangles_1 and removeDangles_2 using leeds network +# removeDangles_2 not applicable for leeds network +```{r} +# RD1_leeds = removeDangles_1(leeds_net, tolerance = 0.001) +# RD2_leeds = removeDangles_2(leeds_net, percentile = 0.012) + +# # Plot for network 1 +# p1 = ggplot() + +# geom_sf(data = leeds_net, color = "gray", size = 2) + +# geom_sf(data = RD1_leeds, color = "blue", size = 1) + +# ggtitle("Network 1") + +# theme_minimal() + +# # Plot for network 2 +# p2 = ggplot() + +# geom_sf(data = leeds_net, color = "gray", size = 2) + +# geom_sf(data = RD2_leeds, color = "red", size = 1) + +# ggtitle("Network 2") + +# theme_minimal() + +# # Combine both plots side by side using patchwork +# combined_plot = p1 + p2 + plot_layout(ncol = 2) + +# # Display the plot +# print(combined_plot) +``` + +# compare removeDangles_1 using edinburgh offroad network with different tolerance values + +# result is pretty much same for different tolerance values +```{r} +RD_edin_1 = removeDangles_1(edin_offroad_net, tolerance = 0.012) +RD_edin_2 = removeDangles_1(edin_offroad_net, tolerance = 10) + +# Plot for network 1 +p1 = ggplot() + + geom_sf(data = edin_offroad_net, color = "gray", size = 1) + + geom_sf(data = RD_edin_1, color = "blue", size = 1) + + ggtitle("Network using removeDangles_1_0.012") + + theme_minimal() + +# Plot for network 2 +p2 = ggplot() + + geom_sf(data = edin_offroad_net, color = "gray", size = 1) + + geom_sf(data = RD_edin_2, color = "red", size = 1) + + ggtitle("using removeDangles_1_10") + + theme_minimal() + +# Combine both plots side by side using patchwork +combined_plot = p1 + p2 + plot_layout(ncol = 2) + +# Display the plot +print(combined_plot) + +mapview(RD_edin_1,color = "blue") + mapview(RD_edin_2,color = "red") + mapview(edin_offroad_net, color = "black") +``` + +# compare removeDangles_1 and removeDangles_2 using edinburgh offroad network +removeDangles_2 is better +```{r} +RD1_edin = removeDangles_1(edin_offroad_net, tolerance = 0.001) +RD2_edin = removeDangles_2(edin_offroad_net, percentile = 0.012) + +# Plot for network 1 +p1 = ggplot() + + geom_sf(data = edin_offroad_net, color = "gray", size = 2) + + geom_sf(data = RD1_edin, color = "blue", size = 1) + + ggtitle("Network using removeDangles_1") + + theme_minimal() + +# Plot for network 2 +p2 = ggplot() + + geom_sf(data = edin_offroad_net, color = "gray", size = 2) + + geom_sf(data = RD2_edin, color = "red", size = 1) + + ggtitle("Network using removeDangles_2") + + theme_minimal() + +# Combine both plots side by side using patchwork +combined_plot = p1 + p2 + plot_layout(ncol = 2) + +# Display the plot +print(combined_plot) + +mapview(RD1_edin,color = "blue") + mapview(RD2_edin,color = "red") + mapview(edin_offroad_net, color = "black") +``` + +# display the node degree of the network +```{r} +# Load necessary libraries +library(sf) +library(igraph) +library(dplyr) +library(ggplot2) +library(tidyr) # Load tidyr for the separate function + +# Extract edges (line segments) +edges = st_cast(leeds_net, "LINESTRING") +fix_edges = st_snap(edges |> st_transform(27700), st_union(edges) |> st_transform(27700), tolerance = 0.1) +edges = fix_edges +# Extract start and end coordinates of each line segment +edge_list = st_coordinates(edges) %>% + as.data.frame() %>% + group_by(L1) %>% + summarize( + x_start = first(X), + y_start = first(Y), + x_end = last(X), + y_end = last(Y) + ) +# Create node identifiers by concatenating coordinates +edges_df = edge_list %>% + mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) %>% + select(from, to) +# Create an undirected graph +g = graph_from_data_frame(edges_df, directed = FALSE) + +node_degree = degree(g) + +# Convert node_degree to a data frame +node_degree_df = data.frame( + node = names(node_degree), + degree = node_degree +) + +# Split the node coordinates back into separate columns +node_degree_df = node_degree_df %>% + separate(node, into = c("X", "Y"), sep = ",", convert = TRUE) + +# Convert node_degree_df back to sf object +node_degree_sf = st_as_sf(node_degree_df, coords = c("X", "Y"), crs = st_crs(27700)) + +mapview(node_degree_sf |> st_transform(4326), zcol = "degree") + mapview(leeds_net) + + +plot_networks = function(networks, colors, titles, output_file, ncol = 2, width = 12, height = 6, base_map = TRUE, line_width = 1, point_size = 1) { + + plots = list() # Create a list to hold individual plots + + for (i in seq_along(networks)) { + network_plot = ggplot() # Initialize ggplot + + if (base_map) { + network_plot = network_plot + annotation_map_tile(type = "osm") # Add OSM base map if enabled + } + + # Loop through each network data set + if (is.list(networks[[i]]) && length(networks[[i]]) == 2) { + for (j in 1:2) { + data = networks[[i]][[j]] + geom_type = sf::st_geometry_type(data)[1] + size_use = if (geom_type %in% c("POINT", "MULTIPOINT")) point_size else line_width + + if (is.character(colors[[i]][j]) && any(colors[[i]][j] %in% names(data))) { + network_plot = network_plot + + geom_sf(data = data, aes(color = factor(.data[[colors[[i]][j]]])), size = size_use) + } else { + network_plot = network_plot + + geom_sf(data = data, color = colors[[i]][j], size = size_use) + } + } + } else { + data = networks[[i]] + geom_type = sf::st_geometry_type(data)[1] + size_use = if (geom_type %in% c("POINT", "MULTIPOINT")) point_size else line_width + + if (is.character(colors[[i]]) && any(colors[[i]] %in% names(data))) { + network_plot = network_plot + + geom_sf(data = data, aes(color = factor(.data[[colors[[i]]]])), size = size_use) + } else { + network_plot = network_plot + + geom_sf(data = data, color = colors[[i]], size = size_use) + } + } + + network_plot = network_plot + + ggtitle(titles[[i]]) + + theme_minimal() + + theme(panel.background = element_rect(fill = "gainsboro"), legend.position = "right") + + scale_color_viridis(discrete = TRUE) # Apply discrete color scale + + plots[[i]] = network_plot + } + + combined_plot = patchwork::wrap_plots(plots, ncol = ncol) + ggsave(output_file, combined_plot, width = width, height = height) + + return(combined_plot) +} + +# Example usage +# Assuming leeds_net and node_degree_sf are properly defined sf objects +networks = list( + list(leeds_net, node_degree_sf) +) +colors = list( + c("blue", "degree") +) +titles = list( + "Network Visualization" +) + +# Call the function +plot_networks(networks, colors, titles, "networks_plot.png", ncol = 1, base_map = FALSE, line_width = 2, point_size = 5) + +``` + + +# new remove dangles function +```{r} +remove_dangles = function(network, crs_proj = 27700, tolerance = 1, iterative = TRUE) { + + # Step 1: Transform to Projected CRS + projected_crs = sf::st_crs(crs_proj) + network_proj = sf::st_transform(network, crs = projected_crs) + + # Step 2: Node the Network (Split Lines at Intersections) + network_noded = sf::st_union(network_proj) + network_noded = sf::st_line_merge(network_noded) + network_noded_sf = sf::st_cast(network_noded, "LINESTRING") + + # Step 3: Snap the Network to Itself + threshold = units::set_units(tolerance, "m") + network_snapped = sf::st_snap(network_noded_sf, sf::st_union(network_noded_sf), tolerance = threshold) + + # Step 4: Extract Edges (Line Segments) + edges = sf::st_cast(network_snapped, "LINESTRING") + + # Step 5: Create Edge List with Start and End Coordinates + edge_list = sf::st_coordinates(edges) |> + as.data.frame() |> + dplyr::group_by(L1) |> + dplyr::summarize( + x_start = dplyr::first(X), + y_start = dplyr::first(Y), + x_end = dplyr::last(X), + y_end = dplyr::last(Y) + ) + + # Step 6: Create Node Identifiers + edges_df = edge_list |> + dplyr::mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) |> + dplyr::select(from, to) + + # Step 7: Create Graph + g = igraph::graph_from_data_frame(edges_df, directed = FALSE) + + # Step 8: Remove Dangles (Iteratively or Once) + if (iterative) { + # Iterative Removal of Dangles + repeat { + # Calculate Node Degrees + node_degree = igraph::degree(g) + + # Break the loop if no nodes with degree 1 are left + if (all(node_degree > 1)) { + break + } + + # Identify Nodes with Degree 1 + node_degree_df = data.frame( + node = names(node_degree), + degree = node_degree + ) |> + tidyr::separate(node, into = c("X", "Y"), sep = ",", convert = TRUE) + + # Identify Dangle Nodes + dangle_nodes = node_degree_df |> dplyr::filter(degree == 1) + + # Create Node Info for Joining + node_info = node_degree_df + + # Merge Edge List with Node Degrees to Identify Edges Connected to Dangle Nodes + edges_with_nodes = edge_list |> + dplyr::mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) |> + dplyr::left_join( + node_info |> + dplyr::mutate(node = paste(X, Y, sep = ",")), + by = c("from" = "node") + ) |> + dplyr::rename(degree_from = degree) |> + dplyr::left_join( + node_info |> + dplyr::mutate(node = paste(X, Y, sep = ",")), + by = c("to" = "node") + ) |> + dplyr::rename(degree_to = degree) + + # Identify Edges Connected to Dangle Nodes + dangle_edges_indices = edges_with_nodes |> + dplyr::filter(degree_from == 1 | degree_to == 1) |> + dplyr::pull(L1) + + # Break if No Dangle Edges are Found + if (length(dangle_edges_indices) == 0) { + break + } + + # Remove Dangle Edges + edges = edges[-dangle_edges_indices, ] + + # Reconstruct Edge List and Graph for Next Iteration + edge_list = sf::st_coordinates(edges) |> + as.data.frame() |> + dplyr::group_by(L1) |> + dplyr::summarize( + x_start = dplyr::first(X), + y_start = dplyr::first(Y), + x_end = dplyr::last(X), + y_end = dplyr::last(Y) + ) + + edges_df = edge_list |> + dplyr::mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) |> + dplyr::select(from, to) + + g = igraph::graph_from_data_frame(edges_df, directed = FALSE) + } + } else { + # Single Removal of Dangles + node_degree = igraph::degree(g) + + node_degree_df = data.frame( + node = names(node_degree), + degree = node_degree + ) |> + tidyr::separate(node, into = c("X", "Y"), sep = ",", convert = TRUE) + + # Convert node_degree_df back to sf object + node_degree_sf = sf::st_as_sf(node_degree_df, coords = c("X", "Y"), crs = st_crs(crs_proj)) + + node_info = node_degree_df + + edges_with_nodes = edge_list |> + dplyr::mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) |> + dplyr::left_join( + node_info |> + dplyr::mutate(node = paste(X, Y, sep = ",")), + by = c("from" = "node") + ) |> + dplyr::rename(degree_from = degree) |> + dplyr::left_join( + node_info |> + dplyr::mutate(node = paste(X, Y, sep = ",")), + by = c("to" = "node") + ) |> + dplyr::rename(degree_to = degree) + + dangle_edges_indices = edges_with_nodes |> + dplyr::filter(degree_from == 1 | degree_to == 1) |> + dplyr::pull(L1) + + edges = edges[-dangle_edges_indices, ] + } + + # Step 9: Return the Cleaned Network + cleaned_network = sf::st_transform(edges, sf::st_crs(network)) + cleaned_network = sf::st_sf(cleaned_network) + return(cleaned_network) +} + +``` + +# apply to leeds network and edinburgh offroad network + +```{r} +cleaned_leeds = remove_dangles(leeds_net, crs_proj = 27700, tolerance = 1, iterative = FALSE) |> st_transform(27700) +leeds_net = leeds_net |> st_transform(27700) + +cleaned_edin = remove_dangles(edin_offroad_net, crs_proj = 27700, tolerance = 1,iterative = FALSE) +names(cleaned_edin) + +mapview(cleaned_leeds, color = "red") + mapview(leeds_net, color = "blue") + +mapview(cleaned_edin, color = "red") + mapview(edin_offroad_net, color = "blue") +``` +```{r} +remove_dangles_with_angle = function(network, crs_proj = 27700, tolerance = 1, + angle_threshold_low = 10, angle_threshold_high = 170, + return_debug_data = FALSE) { + # Load required packages + library(sf) + library(dplyr) + library(tidyr) + library(igraph) + library(units) + + # Step 1: Transform to Projected CRS + projected_crs = st_crs(crs_proj) + network_proj = st_transform(network, crs = projected_crs) + + # Step 2: Node the Network (Split Lines at Intersections) + network_noded = st_union(network_proj) + network_noded_sf = st_line_merge(network_noded) + network_noded_sf = st_cast(network_noded_sf, "LINESTRING") + + # Step 3: Snap the Network to Itself + threshold = set_units(tolerance, "m") + network_snapped = st_snap(network_noded_sf, st_union(network_noded_sf), tolerance = threshold) + + # Step 4: Extract Edges (Line Segments) + edges = st_cast(network_snapped, "LINESTRING") + + # Ensure 'edges' is an sf object with an ID column + edges = st_sf(geometry = edges) + edges$edge_id = seq_len(nrow(edges)) + + # Step 5: Create Edge List with Start and End Coordinates + edge_coords = st_coordinates(edges) + edges_coords_df = as.data.frame(edge_coords) + edges_coords_df$edge_id = edges$edge_id[edges_coords_df$L1] + + # Group by edge and get start and end points + edges_with_nodes = edges_coords_df %>% + group_by(edge_id) %>% + summarize( + x_start = first(X), + y_start = first(Y), + x_end = last(X), + y_end = last(Y) + ) %>% + ungroup() + + # Step 6: Create Node Identifiers + edges_with_nodes = edges_with_nodes %>% + mutate( + from = paste(x_start, y_start, sep = ","), + to = paste(x_end, y_end, sep = ",") + ) + + edges_df = edges_with_nodes %>% + select(edge_id, from, to) + + # Step 7: Create Graph + g = graph_from_data_frame(edges_df %>% select(from, to), directed = FALSE) + + # Step 8: Calculate Node Degrees + node_degree = degree(g) + + # Create Node Data Frame + nodes_df = data.frame( + node = names(node_degree), + degree = node_degree + ) %>% + separate(node, into = c("X", "Y"), sep = ",", convert = TRUE, remove = FALSE) %>% + mutate(node_id = row_number()) + + # Map Edges to Node IDs and Degrees + edges_with_nodes = edges_with_nodes %>% + left_join( + nodes_df %>% select(node, node_id, degree) %>% rename(degree_from = degree), + by = c("from" = "node") + ) %>% + rename(from_node_id = node_id) %>% + left_join( + nodes_df %>% select(node, node_id, degree) %>% rename(degree_to = degree), + by = c("to" = "node") + ) %>% + rename(to_node_id = node_id) + + # Step 9: Identify Candidate Edges Based on Degree + candidate_edges = edges_with_nodes %>% + filter( + degree_from >= 3 & degree_to == 1 + ) + + # Initialize columns in candidate_edges + candidate_edges$min_angle_deg = NA_real_ + candidate_edges$is_dangle = FALSE + + # Include the corrected calculate_angle_at_node function here + # (Provided above) + + # Initialize vector to store dangle edge IDs + dangle_edge_ids = c() + + # Loop over candidate edges + for (i in seq_len(nrow(candidate_edges))) { + edge = candidate_edges[i, ] + candidate_edge_id = edge$edge_id + + # The branch node is the from_node (degree >= 3) + branch_node_id = edge$from_node_id + + # Get edges connected to the branch node, excluding the candidate edge + connected_edges = edges_with_nodes %>% + filter( + (from_node_id == branch_node_id | to_node_id == branch_node_id) & + edge_id != candidate_edge_id + ) + + # Initialize min_angle_deg for this candidate edge + min_angle = NA_real_ + + # Initialize flag to determine if edge is continuous + is_continuous = FALSE + + # Calculate angle between candidate edge and each connected edge + for (j in seq_len(nrow(connected_edges))) { + other_edge_id = connected_edges$edge_id[j] + angle_deg = calculate_angle_at_node(candidate_edge_id, other_edge_id, + branch_node_id, edges_with_nodes, nodes_df) + + # Update min_angle + if (!is.na(angle_deg)) { + if (is.na(min_angle) || angle_deg < min_angle) { + min_angle = angle_deg + } + } + + # Check if angle indicates a continuous line + if (!is.na(angle_deg) && (angle_deg <= angle_threshold_low || + angle_deg >= angle_threshold_high)) { + # Edge is part of a continuous line; do not mark as dangle + is_continuous = TRUE + break # No need to check other edges + } + } + + # Store min_angle in candidate_edges + candidate_edges$min_angle_deg[i] = min_angle + + # Determine if edge is a dangle + if (!is_continuous) { + # Edge is a dangle + dangle_edge_ids = c(dangle_edge_ids, candidate_edge_id) + candidate_edges$is_dangle[i] = TRUE + } + } + + # Merge min_angle_deg and is_dangle back into edges_with_nodes + edges_with_nodes = edges_with_nodes %>% + left_join( + candidate_edges %>% select(edge_id, min_angle_deg, is_dangle), + by = "edge_id" + ) + + # Step 10: Remove Dangle Edges + cleaned_edges = edges %>% + filter(!(edge_id %in% dangle_edge_ids)) + + # Ensure cleaned_edges retains data attributes + cleaned_edges$edge_id = cleaned_edges$edge_id + + # Step 11: Return the Cleaned Network + cleaned_network = st_transform(cleaned_edges, st_crs(network)) + + # Ensure cleaned_network is an sf object with data + cleaned_network = st_sf(cleaned_network) + + if (return_debug_data) { + return(list( + cleaned_network = cleaned_network, + edges_with_nodes = edges_with_nodes, + nodes_df = nodes_df, + candidate_edges = candidate_edges + )) + } else { + return(cleaned_network) + } +} + +calculate_angle_at_node = function(edge1_id, edge2_id, node_id, edges_with_nodes, nodes_df) { + # Get edge1 + edge1 = edges_with_nodes %>% filter(edge_id == edge1_id) + # Get edge2 + edge2 = edges_with_nodes %>% filter(edge_id == edge2_id) + + # Coordinates of the node + node_coords = nodes_df %>% filter(node_id == node_id) %>% select(X, Y) %>% unlist() + + # Get the other end of edge1 + if (edge1$from_node_id == node_id) { + coords1 = c(edge1$x_end, edge1$y_end) + } else { + coords1 = c(edge1$x_start, edge1$y_start) + } + + # Get the other end of edge2 + if (edge2$from_node_id == node_id) { + coords2 = c(edge2$x_end, edge2$y_end) + } else { + coords2 = c(edge2$x_start, edge2$y_start) + } + + # Vectors from the node to the other ends + vector1 = coords1 - node_coords + vector2 = coords2 - node_coords + + # Calculate angle in degrees + dot_prod = sum(vector1 * vector2) + mag1 = sqrt(sum(vector1^2)) + mag2 = sqrt(sum(vector2^2)) + cos_theta = dot_prod / (mag1 * mag2) + # Ensure cos_theta is within [-1, 1] to avoid NaNs + cos_theta = max(min(cos_theta, 1), -1) + angle_rad = acos(cos_theta) + angle_deg = angle_rad * (180 / pi) + + # Ensure angle is between 0 and 180 degrees + if (angle_deg > 180) { + angle_deg = 360 - angle_deg + } + + return(angle_deg) +} + +``` +```{r} +library(sf) +library(dplyr) +library(mapview) + +# Define the main stem (Edge 1) +line1 = st_linestring(matrix(c(0, -5, 0, 10), ncol = 2, byrow = TRUE)) +edge1 = st_sf(edge_id = 1, geometry = st_sfc(line1, crs = 27700)) + +# Define the left branch (Edge 2) +line2 = st_linestring(matrix(c(0, 10, -5, 10), ncol = 2, byrow = TRUE)) +edge2 = st_sf(edge_id = 2, geometry = st_sfc(line2, crs = 27700)) + +# Define the right branch (Edge 3) +line3 = st_linestring(matrix(c(0, 10, 1, 20), ncol = 2, byrow = TRUE)) +edge3 = st_sf(edge_id = 3, geometry = st_sfc(line3, crs = 27700)) + +# Combine into an sf object +y_network = rbind(edge1, edge2, edge3) +mapview(y_network, color = "black") +``` +# latest remove dangles function +```{r} +# Load necessary libraries +library(sf) +library(dplyr) +library(tidyr) +library(igraph) +library(units) +library(lwgeom) +library(mapview) + +# Define the remove_dangles function +remove_dangles = function(network, + angle_threshold_low = 30, + angle_threshold_high = 170) { + + # Step 0: Assign edge_id if not present + if(!"edge_id" %in% colnames(network)) { + network = network %>% + mutate(edge_id = row_number()) + } + + # Step 1: Extract start and end points of each edge + # Extract start points using st_startpoint + start_points = st_startpoint(network$geometry) %>% + st_coordinates() %>% + as.data.frame() %>% + rename(X = X, Y = Y) %>% + mutate(edge_id = network$edge_id, + type = "start") + + # Extract end points using st_endpoint + end_points = st_endpoint(network$geometry) %>% + st_coordinates() %>% + as.data.frame() %>% + rename(X = X, Y = Y) %>% + mutate(edge_id = network$edge_id, + type = "end") + + # Combine start and end points + all_nodes = rbind(start_points, end_points) + + # Step 2: Assign unique node IDs based on unique coordinates + unique_nodes = all_nodes %>% + distinct(X, Y) %>% + mutate(node_id = row_number()) + + # Step 3: Create an Edge List with From and To Node IDs + edge_list = all_nodes %>% + left_join(unique_nodes, by = c("X", "Y")) %>% + arrange(edge_id, desc(type == "start")) %>% # Ensure 'start' comes before 'end' + group_by(edge_id) %>% + summarize( + from = first(node_id), # 'start' node + to = last(node_id) # 'end' node + ) %>% + ungroup() + + # Step 4: Build an igraph Object and Calculate Node Degrees + g = graph_from_data_frame(edge_list %>% select(from, to), directed = FALSE) + + # Compute node degrees + deg = degree(g) + + # Add degrees to unique_nodes + unique_nodes = unique_nodes %>% + mutate(degree = deg[node_id]) + + # Step 5: Identify Candidate Dangles Based on Node Degrees + dangle_edges = edge_list %>% + mutate( + degree_from = unique_nodes$degree[from], + degree_to = unique_nodes$degree[to], + is_dangle = (degree_from == 1 | degree_to == 1) + ) %>% + filter(is_dangle) + + # If no candidate dangles, return the original network + if(nrow(dangle_edges) == 0) { + message("No dangles found in the network.") + return(network) + } + + # Step 6: Define the angle calculation function + calculate_angle_at_node = function(edge1, edge2, node_id, unique_nodes) { + # Get coordinates of the common node + node_coords = c( + unique_nodes$X[unique_nodes$node_id == node_id], + unique_nodes$Y[unique_nodes$node_id == node_id] + ) + + # Function to get the other end coordinates + get_other_end = function(edge, node_id, unique_nodes) { + if (edge$from == node_id) { + c( + unique_nodes$X[unique_nodes$node_id == edge$to], + unique_nodes$Y[unique_nodes$node_id == edge$to] + ) + } else { + c( + unique_nodes$X[unique_nodes$node_id == edge$from], + unique_nodes$Y[unique_nodes$node_id == edge$from] + ) + } + } + + # Get the other end coordinates + coords1 = get_other_end(edge1, node_id, unique_nodes) + coords2 = get_other_end(edge2, node_id, unique_nodes) + + # Create vectors from the common node to the other ends + vector1 = coords1 - node_coords + vector2 = coords2 - node_coords + + # Calculate dot product and magnitudes + dot_prod = sum(vector1 * vector2) + mag1 = sqrt(sum(vector1^2)) + mag2 = sqrt(sum(vector2^2)) + + # Check for zero-length vectors + if (mag1 == 0 || mag2 == 0) { + return(NA) + } + + # Calculate cosine of the angle + cos_theta = dot_prod / (mag1 * mag2) + cos_theta = max(min(cos_theta, 1), -1) # Handle numerical precision + + # Calculate angle in degrees + angle_deg = acos(cos_theta) * (180 / pi) + + return(angle_deg) + } + + # Step 7: Identify True Dangles Based on Angle Criteria + # Initialize a vector to store dangles to remove + dangles_to_remove = c() + + # Iterate over each candidate dangle + for(i in 1:nrow(dangle_edges)) { + edge = dangle_edges[i, ] + + # Identify the common node (junction node) + if(edge$degree_from == 1) { + common_node = edge$to + } else { + common_node = edge$from + } + + # Get all other edges connected to the common node + connected_edges = edge_list %>% + filter((from == common_node | to == common_node) & edge_id != edge$edge_id) + + # Initialize a flag to determine if the edge is a true dangle + is_true_dangle = TRUE + + # Calculate angles between the current edge and all other connected edges + for(j in 1:nrow(connected_edges)) { + other_edge = connected_edges[j, ] + tryCatch({ + angle = calculate_angle_at_node(edge, other_edge, common_node, unique_nodes) + }, error = function(e) { + angle = 0 + }) + + + # If angle is NA, skip dangle identification + if(is.na(angle)) { + is_true_dangle = FALSE + break + } + + # Check if the angle is outside the threshold + if(angle < angle_threshold_low | angle > angle_threshold_high) { + is_true_dangle = FALSE + break + } + } + edge$cal_angle = angle + # If all angles are within the thresholds, mark as dangle + if(is_true_dangle) { + dangles_to_remove = c(dangles_to_remove, edge$edge_id) + } + } + + + # Step 8: Remove Dangles and Return Cleaned Network + cleaned_network = network %>% + filter(!edge_id %in% dangles_to_remove) + + # Return only the cleaned network + return(cleaned_network) +} + +``` + +```{r} +# Load necessary libraries +library(sf) +library(dplyr) +library(mapview) +library(igraph) + +# Define the sample Y-shaped network +# Edge 1: Main Stem +line1 = st_linestring(matrix(c(0, -5, 0, 10), ncol=2, byrow=TRUE)) +edge1 = st_sf(edge_id = 1, geometry = st_sfc(line1), crs = 27700) + +# Edge 2: Left Branch (Dangle) +line2 = st_linestring(matrix(c(0, 10, -5, 10), ncol=2, byrow=TRUE)) +edge2 = st_sf(edge_id = 2, geometry = st_sfc(line2), crs = 27700) + +# Edge 3: Right Branch +line3 = st_linestring(matrix(c(0, 10, 1, 20), ncol=2, byrow=TRUE)) +edge3 = st_sf(edge_id = 3, geometry = st_sfc(line3), crs = 27700) + +# Combine into a single sf object +y_network = rbind(edge1, edge2, edge3) + +# Visualize the original network +mapview(y_network, color = "red", layer.name = "Original Network") + +# Apply the remove_dangles function +cleaned_network = remove_dangles(network = y_network, + angle_threshold_low = 30, + angle_threshold_high = 170) +edin_offroad_net$edge_id = 1:nrow(edin_offroad_net) +cleaned_network_edin = remove_dangles(edin_offroad_net, angle_threshold_low = 30, angle_threshold_high = 170) +mapview(cleaned_network_edin, color = "blue", layer.name = "Cleaned Network") +# View the cleaned network +print("Cleaned Network:") +print(cleaned_network) + +# Visualize the cleaned network alongside the original network +mapview(cleaned_network_edin, color = "blue", layer.name = "Cleaned Network") + + mapview(y_network, color = "red", layer.name = "Original Network", alpha = 0.5) + +``` + +```{r} +# Load necessary libraries +library(sf) +library(dplyr) +library(tidyr) +library(igraph) +library(units) +library(lwgeom) +library(mapview) + + +# Step 1: Define the network with explicit edge_ids +# Edge 1: Main Stem +line1 = st_linestring(matrix(c(0, -5, 0, 10), ncol=2, byrow=TRUE)) +edge1 = st_sf(edge_id = 1, geometry = st_sfc(line1), crs = 27700) + +# Edge 2: Left Branch (Dangle) +line2 = st_linestring(matrix(c(0, 10, -5, 10), ncol=2, byrow=TRUE)) +edge2 = st_sf(edge_id = 2, geometry = st_sfc(line2), crs = 27700) + +# Edge 3: Right Branch +line3 = st_linestring(matrix(c(0, 10, 1, 20), ncol=2, byrow=TRUE)) +edge3 = st_sf(edge_id = 3, geometry = st_sfc(line3), crs = 27700) + +# Combine into a single sf object +y_network = edin_offroad_net + +# Step 2: Extract start and end points of each edge +# Extract start points using st_startpoint +start_points = st_startpoint(y_network$geometry) %>% + st_coordinates() %>% + as.data.frame() %>% + rename(X = X, Y = Y) %>% + mutate(edge_id = y_network$edge_id, + type = "start") + +# Extract end points using st_endpoint +end_points = st_endpoint(y_network$geometry) %>% + st_coordinates() %>% + as.data.frame() %>% + rename(X = X, Y = Y) %>% + mutate(edge_id = y_network$edge_id, + type = "end") + +# Combine start and end points +all_nodes = rbind(start_points, end_points) + +# Step 3: Assign unique node IDs +# Assign unique node IDs based on unique coordinates +unique_nodes = all_nodes %>% + distinct(X, Y) %>% + mutate(node_id = row_number()) + +# View unique_nodes +print("Unique Nodes:") +print(unique_nodes) + +# Step 4: Create an Edge List with From and To Node IDs (Corrected) +edge_list = all_nodes %>% + left_join(unique_nodes, by = c("X", "Y")) %>% + arrange(edge_id, desc(type == "start")) %>% # Ensure 'start' comes before 'end' + group_by(edge_id) %>% + summarize( + from = first(node_id), # 'start' node + to = last(node_id) # 'end' node + ) %>% + ungroup() + +# View corrected edge_list +print("Edge List:") +print(edge_list) + +# Step 5: Build an igraph Object and Calculate Node Degrees +g = graph_from_data_frame(edge_list %>% select(from, to), directed = FALSE) + +# Compute node degrees +deg = degree(g) + +# Add degrees to unique_nodes +unique_nodes = unique_nodes %>% + mutate(degree = deg[node_id]) + +# View nodes with degrees +print("Unique Nodes with Degrees:") +print(unique_nodes) + +# Step 6: Identify Candidate Dangles Based on Node Degrees +dangle_edges = edge_list %>% + mutate( + degree_from = unique_nodes$degree[from], + degree_to = unique_nodes$degree[to], + is_dangle = (degree_from == 1 | degree_to == 1) + ) %>% + filter(is_dangle) + +# View dangle_edges +print("Candidate Dangle Edges:") +print(dangle_edges) + +# Step 7: Calculate Angles to Confirm Dangles +# Corrected function with accurate vector calculations +calculate_angle_at_node = function(edge1, edge2, node_id, unique_nodes) { + # Get coordinates of the common node + node_coords = c( + unique_nodes$X[unique_nodes$node_id == node_id], + unique_nodes$Y[unique_nodes$node_id == node_id] + ) + + # Get the other end coordinates, ensuring we're getting the point away from the junction + get_other_end = function(edge, node_id, unique_nodes) { + if (edge$from == node_id) { + c( + unique_nodes$X[unique_nodes$node_id == edge$to], + unique_nodes$Y[unique_nodes$node_id == edge$to] + ) + } else { + c( + unique_nodes$X[unique_nodes$node_id == edge$from], + unique_nodes$Y[unique_nodes$node_id == edge$from] + ) + } + } + + # Get vectors from junction to other ends + coords1 = get_other_end(edge1, node_id, unique_nodes) + coords2 = get_other_end(edge2, node_id, unique_nodes) + + # Create vectors (from junction to endpoints) + vector1 = coords1 - node_coords + vector2 = coords2 - node_coords + + # Print vectors for verification + cat("Vector1:", vector1[1], vector1[2], "\n") + cat("Vector2:", vector2[1], vector2[2], "\n") + + # Calculate dot product and magnitudes + dot_prod = sum(vector1 * vector2) + mag1 = sqrt(sum(vector1^2)) + mag2 = sqrt(sum(vector2^2)) + + # Print intermediate calculations + cat("Dot Product:", dot_prod, "\n") + cat("Magnitude of Vector1:", mag1, "\n") + cat("Magnitude of Vector2:", mag2, "\n") + + # Check for zero-length vectors + if (mag1 == 0 || mag2 == 0) { + cat("Error: Zero-length vector detected.\n") + return(NA) + } + + # Calculate angle in degrees + cos_theta = dot_prod / (mag1 * mag2) + cos_theta = max(min(cos_theta, 1), -1) # Handle numerical precision + angle_deg = acos(cos_theta) * (180 / pi) + + cat("Calculated Angle:", angle_deg, "degrees\n") + + return(angle_deg) +} + +# Initialize a vector to store dangles to remove +dangles_to_remove = c() + +# Define angle thresholds +angle_threshold_low = 30 # Degrees +angle_threshold_high = 170 # Degrees + +# Define the main stem edge_id (e.g., Edge1) +main_stem_edge_id = 1 + +# Update dangle_edges to exclude the main stem +dangle_edges_filtered = dangle_edges %>% + filter(edge_id != main_stem_edge_id) + +# Iterate over each dangle edge with enhanced debugging +for (i in 1:nrow(dangle_edges_filtered)) { + edge = dangle_edges_filtered[i, ] + + # Identify the node with degree >=3 (common node) + if (unique_nodes$degree[edge$from] == 1) { + common_node = edge$to + } else { + common_node = edge$from + } + + # Print current edge and common node + cat("\nProcessing Edge ID:", edge$edge_id, "Common Node ID:", common_node, "\n") + + # Get all edges connected to the common node except the current edge + connected_edges = edge_list %>% + filter((from == common_node | to == common_node) & edge_id != edge$edge_id) + + # Check if there are any connected edges to compare angles + if (nrow(connected_edges) < 1) { + cat("No connected edges to compare. Skipping.\n") + next # Skip if no other edges are connected + } + + # Calculate angles between the current edge and all other connected edges + angles = sapply(1:nrow(connected_edges), function(j) { + other_edge = connected_edges[j, ] + angle = calculate_angle_at_node(edge, other_edge, common_node, unique_nodes) + if (!is.na(angle)) { + cat("Angle with Edge ID:", other_edge$edge_id, "is", angle, "degrees\n") + } else { + cat("Angle with Edge ID:", other_edge$edge_id, "could not be calculated.\n") + } + return(angle) + }) + + # Handle NA angles + if (any(is.na(angles))) { + cat("One or more angles are NA. Not marking as dangle.\n") + next + } + + # Determine if the edge is a dangle based on angles + # Criteria: + # - All angles >= angle_threshold_low AND <= angle_threshold_high: dangle + # - If any angle < angle_threshold_low OR > angle_threshold_high: not a dangle + if (all(angles >= angle_threshold_low & angles <= angle_threshold_high)) { + dangles_to_remove = c(dangles_to_remove, edge$edge_id) + cat("Edge ID:", edge$edge_id, "marked as dangle.\n") + } else { + cat("Edge ID:", edge$edge_id, "not marked as dangle due to angle criteria.\n") + } +} + +# View dangles to remove +cat("\nDangles to remove:", dangles_to_remove, "\n") + +# Step 8: Remove Dangles and Verify the Cleaned Network +# Remove dangle edges from the network +cleaned_network = y_network %>% + filter(!edge_id %in% dangles_to_remove) + +# View cleaned network +print("Cleaned Network:") +print(cleaned_network) + +# Step 9: Visualize the Results +# Visualize the cleaned network and original network +mapview(cleaned_network, color = "blue", layer.name = "Cleaned Network") + + mapview(y_network, color = "red", layer.name = "Original Network", alpha = 0.5) + +``` + diff --git a/data/central_leeds_osm.rda b/data/central_leeds_osm.rda new file mode 100644 index 0000000..4d570bf Binary files /dev/null and b/data/central_leeds_osm.rda differ diff --git a/data/edinburgh_offroad.rda b/data/edinburgh_offroad.rda new file mode 100644 index 0000000..021a045 Binary files /dev/null and b/data/edinburgh_offroad.rda differ diff --git a/fix_connectivity_issue.jpg b/fix_connectivity_issue.jpg new file mode 100644 index 0000000..825f441 Binary files /dev/null and b/fix_connectivity_issue.jpg differ diff --git a/leeds_network_node_degree_plot.jpg b/leeds_network_node_degree_plot.jpg new file mode 100644 index 0000000..dd1f578 Binary files /dev/null and b/leeds_network_node_degree_plot.jpg differ diff --git a/man/central_leeds_osm.Rd b/man/central_leeds_osm.Rd new file mode 100644 index 0000000..5763f9c --- /dev/null +++ b/man/central_leeds_osm.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{central_leeds_osm} +\alias{central_leeds_osm} +\title{Central Leeds OSM Network} +\format{ +An object of class \code{sf} (inherits from \code{data.frame}). +} +\description{ +See the \code{data-raw} folder for the code used to generate this data set. +} +\examples{ +head(central_leeds_osm) +library(sf) # for plotting +plot(central_leeds_osm$geometry) +} +\keyword{datasets} diff --git a/man/corenet.Rd b/man/corenet.Rd index 31c7121..44063c8 100644 --- a/man/corenet.Rd +++ b/man/corenet.Rd @@ -15,7 +15,8 @@ corenet( minDistPts = 2, road_scores = list(`A Road` = 1, `B Road` = 1, `Minor Road` = 1000), n_removeDangles = 6, - penalty_value = 1 + penalty_value = 1, + group_column = "name_1" ) } \arguments{ @@ -40,6 +41,8 @@ corenet( \item{n_removeDangles}{Number of iterations to remove dangles from the network, default is 6.} \item{penalty_value}{The penalty value for roads with low values, default is 1.} + +\item{group_column}{The column name to group the network by edge betweenness, default is "name_1".} } \value{ A spatial object representing the largest cohesive component of the network, free of dangles. diff --git a/networks_plot.png b/networks_plot.png new file mode 100644 index 0000000..09c3a60 Binary files /dev/null and b/networks_plot.png differ diff --git a/os_osm_compare-Edinburgh.jpg b/os_osm_compare-Edinburgh.jpg new file mode 100644 index 0000000..3e943c4 Binary files /dev/null and b/os_osm_compare-Edinburgh.jpg differ diff --git a/rosm.cache/osm/16_32486_21104.png b/rosm.cache/osm/16_32486_21104.png new file mode 100644 index 0000000..0059c34 Binary files /dev/null and b/rosm.cache/osm/16_32486_21104.png differ diff --git a/rosm.cache/osm/16_32487_21104.png b/rosm.cache/osm/16_32487_21104.png new file mode 100644 index 0000000..f403a6d Binary files /dev/null and b/rosm.cache/osm/16_32487_21104.png differ