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
6 changes: 6 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@
zip
gpkg
^Doc$
^\.devcontainer$
^code$
^network_eval\.qmd$
^network_eval_file$
^plot$
^requirements\.txt$
9 changes: 8 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ Imports:
purrr,
units,
rlang,
anime,
stats,
grDevices
Suggests:
tibble,
tmaptools,
zonebuilder,
rsgeo
rsgeo,
fmsb
Remotes:
josiahparry/anime/r,
josiahparry/rsgeo
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
Expand Down
3 changes: 2 additions & 1 deletion R/anime_join.R
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ anime_join = function(
net_target_aggregated = net_source_matches |>
dplyr::group_by(row_number = target_id) |>
dplyr::summarise(
!!rlang::sym(new_name) := agg_fun(!!mult_expr, na.rm = TRUE)
!!rlang::sym(new_name) := agg_fun(!!mult_expr, na.rm = TRUE),
.groups = "drop"
)

# 6. If aggregator is max, round the result
Expand Down
34 changes: 15 additions & 19 deletions R/corenet.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
utils::globalVariables(c("edge_paths", "influence_network", "all_fastest_bicycle_go_dutch",
"weight", "to_linegraph", "edges", "group", "mean_potential", "LAD23NM",
"road_function", "grid_id", "density",
"max_value", "min_value", "arterialness", "road_score", "value", "key_attribute", "n_group",".data", "n_removeDangles", "path_type", "maxDistPts", "minDistPts","penalty_value","penalty", "use_stplanr", "group_column"))
"max_value", "min_value", "arterialness", "road_score", "value", "key_attribute", "n_group",".data", "n_removeDangles", "path_type", "maxDistPts", "minDistPts","penalty_value","penalty", "use_stplanr", "group_column",
# Variables from net_eval.R functions
"geometry", "InterZone", "TotPop2011", "StdAreaKm2", "ResPop2011", "W_i",
"has_intersection", "covered_area", "covered_area_km2", "pop_density",
"covered_population", "distance_to_network", "all", "length_m", "km",
"target_id", "id"))

#' Prepare a cohesive cycling network using NPT data
#'
Expand Down Expand Up @@ -146,6 +151,7 @@ cohesive_network_prep = function(base_network, influence_network, target_zone, c
#' @param n_removeDangles Number of iterations to remove dangles from the network, default is 6.
#' @param penalty_value The penalty value for roads with low values, default is 1.
#' @param group_column The column name to group the network by edge betweenness, default is "name_1".
#' @param max_path_weight Maximum weight allowed for paths in network calculations, default is 10.
#' @return A spatial object representing the largest cohesive component of the network, free of dangles.
#' @export
#' @examples
Expand Down Expand Up @@ -193,9 +199,9 @@ corenet = function(influence_network, cohesive_base_network, target_zone, key_at

# Perform DBSCAN clustering
coordinates = sf::st_coordinates(centroids)
coordinates_clean = coordinates[complete.cases(coordinates), ]
coordinates_clean = coordinates[stats::complete.cases(coordinates), ]
clusters = dbscan::dbscan(coordinates_clean, eps = 18, minPts = 1)
centroids_clean = centroids[complete.cases(coordinates), ]
centroids_clean = centroids[stats::complete.cases(coordinates), ]
centroids_clean$cluster = clusters$cluster
unique_centroids = centroids_clean[!duplicated(centroids_clean$cluster), ]

Expand Down Expand Up @@ -479,8 +485,10 @@ prepare_network = function(network, key_attribute = "all_fastest_bicycle_go_dutc
#' @param minDistPts The minimum distance (in meters) to consider for path calculations.
#' @param centroids An sf object containing centroids to which paths are calculated.
#' @param path_type A character string indicating the type of path calculation: 'shortest', 'all_shortest', or 'all_simple'.
#' @param max_path_weight
#' @param max_path_weight Maximum weight allowed for paths in network calculations.
#' @param crs_transform The coordinate reference system to transform to, default is 27700 (British National Grid).
#'
#' @return A list containing the computed paths and their associated weights.
#' - 'shortest': Calculates the shortest path considering weights.
#' - 'all_shortest': Calculates all paths that tie for the shortest distance, considering weights.
#' - 'all_simple': Calculates all simple paths, ignoring weights.
Expand All @@ -506,16 +514,7 @@ calculate_paths_from_point_dist = function(
network = sf::st_transform(network, crs = crs_transform)
}

# 2) Generate a unique key for caching based on the point's coordinates
# You can modify how this key is generated if your 'point' is already sf-compatible
point_key = paste(sort(as.character(point)), collapse = "_")

# 3) Check if result is already cached
if (exists(point_key, envir = path_cache_env)) {
return(get(point_key, envir = path_cache_env))
}

# 4) Convert the point and centroids to sfc if not already
# 2) Convert the point and centroids to sfc if not already
point_geom = sf::st_as_sfc(point)
centroids_geom = sf::st_as_sfc(centroids)

Expand All @@ -529,9 +528,8 @@ calculate_paths_from_point_dist = function(
distances_m <= units::set_units(maxDistPts, "m")
)

# If no centroids qualify, cache NULL and return
# If no centroids qualify, return NULL
if (!length(valid_idx)) {
assign(point_key, NULL, envir = path_cache_env)
return(NULL)
}

Expand Down Expand Up @@ -568,7 +566,6 @@ calculate_paths_from_point_dist = function(
# 9) Filter out paths whose total weight exceeds threshold
valid_paths = paths_from_point[total_weights <= max_path_weight, ]
if (!nrow(valid_paths)) {
assign(point_key, NULL, envir = path_cache_env)
return(NULL)
}

Expand All @@ -580,8 +577,7 @@ calculate_paths_from_point_dist = function(
dplyr::slice(edges_in_paths) |>
sf::st_as_sf()

# 12) Store in cache and return
assign(point_key, result, envir = path_cache_env)
# 12) Return the result
return(result)
}

Expand Down
76 changes: 39 additions & 37 deletions R/net_eval.R
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
### Function: Compute Spatial Coverage
compute_spatial_coverage = function(rnet_core, lads, city_name = "City of Edinburgh", buffer_distance = 500) {

city_boundary = lads |> filter(LAD23NM == city_name)
city_boundary = lads |> dplyr::filter(LAD23NM == city_name)
rnet_core_zone = sf::st_intersection(rnet_core, city_boundary)
# Create a buffer around the network
network_buffer = st_buffer(rnet_core_zone, dist = buffer_distance)
network_buffer = sf::st_buffer(rnet_core_zone, dist = buffer_distance)
# Union all buffer polygons
buffer_union = st_union(network_buffer)
buffer_union = sf::st_union(network_buffer)
# Intersect with city boundary
buffer_intersection = sf::st_intersection(buffer_union, city_boundary)

# Compute areas
area_buffered = st_area(buffer_intersection)
area_city = st_area(city_boundary)
area_buffered = sf::st_area(buffer_intersection)
area_city = sf::st_area(city_boundary)

# Spatial coverage ratio
spatial_coverage = as.numeric(area_buffered / area_city)
Expand All @@ -25,24 +25,24 @@ compute_spatial_coverage = function(rnet_core, lads, city_name = "City of Edinbu
### Function: Compute Zone Connectivity
compute_zone_connectivity = function(intermediate_zone, lads, city_name = "City of Edinburgh", buffer_intersection, density_quantile = 0.3) {
# Filter zones by density threshold
city_boundary = lads |> filter(LAD23NM == city_name)
city_boundary = lads |> dplyr::filter(LAD23NM == city_name)
intermediate_zone = sf::st_intersection(intermediate_zone, city_boundary)
intermediate_zone$density = intermediate_zone$ResPop2011 / intermediate_zone$StdAreaKm2
density_threshold = quantile(intermediate_zone$density, density_quantile, na.rm = TRUE)
intermediate_zone = intermediate_zone |> filter(density > density_threshold)
density_threshold = stats::quantile(intermediate_zone$density, density_quantile, na.rm = TRUE)
intermediate_zone = intermediate_zone |> dplyr::filter(density > density_threshold)

zones = intermediate_zone |>
select(InterZone, geometry) |>
st_make_valid()
dplyr::select(InterZone, geometry) |>
sf::st_make_valid()

# W = B_union ∩ A_city (already computed outside)
W = buffer_intersection

# Compute intersections W_i
zones = zones |>
rowwise() |>
mutate(W_i = list(st_intersection(geometry, W))) |>
ungroup()
dplyr::rowwise() |>
dplyr::mutate(W_i = list(sf::st_intersection(geometry, W))) |>
dplyr::ungroup()

# Check intersections
zones = zones |> mutate(has_intersection = lengths(W_i) > 0)
Expand All @@ -61,8 +61,8 @@ compute_zone_connectivity = function(intermediate_zone, lads, city_name = "City
geom_i = zones$W_i[[i]]
geom_j = zones$W_i[[j]]

intersects = st_intersects(geom_i, geom_j, sparse = FALSE)
touches = st_touches(geom_i, geom_j, sparse = FALSE)
intersects = sf::st_intersects(geom_i, geom_j, sparse = FALSE)
touches = sf::st_touches(geom_i, geom_j, sparse = FALSE)

if (any(intersects) | any(touches)) {
adj_matrix[i, j] = 1
Expand All @@ -75,10 +75,10 @@ compute_zone_connectivity = function(intermediate_zone, lads, city_name = "City
all_connected = all(adj_matrix == 1)
cat("Are all zones inter-connected within W? ", all_connected, "\n")

g = graph_from_adjacency_matrix(adj_matrix, mode = "undirected")
comp = components(g)
g = igraph::graph_from_adjacency_matrix(adj_matrix, mode = "undirected")
comp = igraph::components(g)
largest_comp_size = max(comp$csize)
total_zones = length(V(g))
total_zones = length(igraph::V(g))
fraction_connected = largest_comp_size / total_zones
return(list(
graph = g,
Expand All @@ -92,13 +92,13 @@ compute_zone_connectivity = function(intermediate_zone, lads, city_name = "City
compute_cycling_potential_coverage = function(rnet_npt, lads, city_name = "City of Edinburgh", rnet_core, crs_target, buffer_distance = 20) {

# Filter city network to within the city boundary
city_boundary = lads |> filter(LAD23NM == city_name)
city_boundary = lads |> dplyr::filter(LAD23NM == city_name)
rnet_core_zone = sf::st_intersection(rnet_core, city_boundary)
rnet_city = sf::st_intersection(rnet_npt, city_boundary)

# Compute length of each segment
rnet_city = rnet_city |>
mutate(length_m = as.numeric(st_length(geometry)))
dplyr::mutate(length_m = as.numeric(sf::st_length(geometry)))

# Total city potential (sum of all_fastest_bicycle_go_dutch)
P_total = sum(rnet_city$all_fastest_bicycle_go_dutch, na.rm = TRUE)
Expand All @@ -110,10 +110,10 @@ compute_cycling_potential_coverage = function(rnet_npt, lads, city_name = "City
D_city = P_total / L_city

# Create a buffer around the core network
rnet_core_buffer = st_buffer(rnet_core_zone, buffer_distance)
rnet_core_buffer = sf::st_buffer(rnet_core_zone, buffer_distance)

# Extract segments within the buffer
rnet_city_buffer = rnet_city[st_union(rnet_core_buffer), , op = st_within]
rnet_city_buffer = rnet_city[sf::st_union(rnet_core_buffer), , op = sf::st_within]

# Buffered potential sum
P_U_city = sum(rnet_city_buffer$all_fastest_bicycle_go_dutch, na.rm = TRUE)
Expand All @@ -139,24 +139,24 @@ compute_cycling_potential_coverage = function(rnet_npt, lads, city_name = "City

### Function: Compute Population Coverage
compute_population_coverage = function(intermediate_zone, lads, city_name = "City of Edinburgh", rnet_core, dist_threshold = 500) {
city_boundary = lads |> filter(LAD23NM == city_name)
city_boundary = lads |> dplyr::filter(LAD23NM == city_name)
intermediate_zone = sf::st_intersection(intermediate_zone, city_boundary)
rnet_core_zone = sf::st_intersection(rnet_core, city_boundary)

zones = intermediate_zone |>
select(InterZone, TotPop2011, StdAreaKm2, geometry) |>
st_make_valid() |>
mutate(pop_density = TotPop2011 / StdAreaKm2)
dplyr::select(InterZone, TotPop2011, StdAreaKm2, geometry) |>
sf::st_make_valid() |>
dplyr::mutate(pop_density = TotPop2011 / StdAreaKm2)

rnet_core_buffer = st_buffer(rnet_core_zone, dist_threshold)
W = st_intersection(st_union(rnet_core_buffer), city_boundary)
rnet_core_buffer = sf::st_buffer(rnet_core_zone, dist_threshold)
W = sf::st_intersection(sf::st_union(rnet_core_buffer), city_boundary)

zones_coverage = st_intersection(zones, W)
zones_coverage$covered_area = st_area(zones_coverage)
zones_coverage = sf::st_intersection(zones, W)
zones_coverage$covered_area = sf::st_area(zones_coverage)
zones_coverage$covered_area_km2 = units::set_units(zones_coverage$covered_area, km^2)

zones_coverage = zones_coverage |>
mutate(covered_population = pop_density * as.numeric(covered_area_km2))
dplyr::mutate(covered_population = pop_density * as.numeric(covered_area_km2))

P_covered = sum(zones_coverage$covered_population, na.rm = TRUE)
P_total = sum(zones$TotPop2011, na.rm = TRUE)
Expand Down Expand Up @@ -272,7 +272,9 @@ generate_radar_chart = function(city_name,
dist_threshold = 500, buffer_distance = 500,
save_path = NULL) {

library(fmsb)
if (!requireNamespace("fmsb", quietly = TRUE)) {
stop("Package 'fmsb' is required for this function. Please install it with install.packages('fmsb')")
}

# 1. Spatial Coverage
sp_cov_result = compute_spatial_coverage(rnet_core, lads, city_name = city_name, buffer_distance = buffer_distance)
Expand Down Expand Up @@ -364,15 +366,15 @@ generate_radar_chart = function(city_name,
# Generate radar chart
if (!is.null(save_path)) {
# Open a PNG device to save the plot
png(filename = save_path, width = 800, height = 800)
grDevices::png(filename = save_path, width = 800, height = 800)
}

radarchart(
fmsb::radarchart(
df_radar,
axistype = 1,
seg = 5,
pcol = rgb(0.2, 0.5, 0.5, 0.9),
pfcol = rgb(0.2, 0.5, 0.5, 0.5),
pcol = grDevices::rgb(0.2, 0.5, 0.5, 0.9),
pfcol = grDevices::rgb(0.2, 0.5, 0.5, 0.5),
plwd = 2,
cglcol = "grey",
cglty = 1,
Expand All @@ -387,7 +389,7 @@ generate_radar_chart = function(city_name,
)

if (!is.null(save_path)) {
dev.off() # Close the PNG device
grDevices::dev.off() # Close the PNG device
}
}

16 changes: 10 additions & 6 deletions man/calculate_paths_from_point_dist.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions man/corenet.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes
File renamed without changes