diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..0b7caca --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,37 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "ghcr.io/geocompx/docker:rust", + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["reditorsupport.r", + "GitHub.copilot-chat", + "GitHub.copilot-labs", + "GitHub.copilot", + "yzhang.markdown-all-in-one", + "quarto.quarto" + ] + } + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" + + // Run the script install-additional-dependencies.sh: + "postCreateCommand": "apt-get update && apt-get install -y dos2unix && dos2unix ./.devcontainer/postCreateCommand.sh && bash ./.devcontainer/postCreateCommand.sh" + +} + diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100644 index 0000000..15d22d6 --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,47 @@ +# Check the Linux distro we're running: +cat /etc/os-release + +# Add cargo to the path both temporarily and permanently: +export PATH="$HOME/.cargo/bin:$PATH" +echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.profile + +# Ensure cargo command is available +command -v cargo + +# Install odjitter using cargo +cargo install --git https://github.com/dabreegster/odjitter --rev 32fb58bf7f0d68afd3b76b88cf6b1272c5c66828 +# Add local instance of odjitter to the /usr/local/bin directory: +which odjitter +sudo ln -s ~/.cargo/bin/odjitter /usr/local/bin/odjitter + +# Ensure R is installed and execute the R script +Rscript code/install.R + +# Ensure apt repository is up-to-date and install Python packages +apt-get update +apt-get install -y software-properties-common python3 python3-pip + +# Install Python dependencies: +pip install -r requirements.txt + +# Clone and install tippecanoe if not already installed +cd /tmp +if [ ! -d "tippecanoe" ]; then + git clone https://github.com/felt/tippecanoe.git +fi +cd tippecanoe +make -j$(nproc) +sudo make install +tippecanoe --version + +# Install git and GitHub CLI (gh) +apt-get install -y git +apt-get install -y gh + +# Configure git settings +git config --global core.autocrlf input +git config --global core.fileMode false +git update-index --refresh + +# Make sure there's a newline at the end of the script +echo "Script execution completed successfully." diff --git a/R/anime_join.R b/R/anime_join.R new file mode 100644 index 0000000..5b3afa1 --- /dev/null +++ b/R/anime_join.R @@ -0,0 +1,68 @@ +# Define a helper function that runs anime and aggregates an attribute. +anime_join = function(source_data, + target_data, + attribute, # character name of the attribute to aggregate + new_name, # character name for the output column + agg_fun = sum, # aggregation function (e.g., sum, mean, max) + weights, # character vector of weight columns (e.g., "target_weighted") + angle_tolerance = 35, + distance_tolerance = 15) { + library(dplyr) + library(sf) + library(rlang) + library(purrr) + library(anime) + + get_fun_name = function(fn) { + deparse(substitute(fn)) + } + + cat("Aggregating attribute:", attribute, "using function:", get_fun_name(sum), "\n") + + # Run anime matching between source and target + matches = anime::anime( + source = source_data, + target = target_data, + angle_tolerance = angle_tolerance, + distance_tolerance = distance_tolerance + ) + matches_df = anime::get_matches(matches) + + # Join the source data (with a source_id) to the matches. + net_source_matches = source_data %>% + mutate(source_id = row_number()) %>% + st_drop_geometry() %>% + left_join(matches_df, by = "source_id") + + # Convert the attribute and weight names to symbols. + attr_sym = sym(attribute) + weight_syms = syms(weights) + + # For max aggregatio + if (identical(agg_fun, max)) { + mult_expr = purrr::reduce(weight_syms, function(acc, w) expr((!!acc)), .init = attr_sym) + } else { + mult_expr = purrr::reduce(weight_syms, function(acc, w) expr((!!acc) * (!!w)), .init = attr_sym) + } + + # Group by target id (assumed to be provided by anime as "target_id") and aggregate. + net_target_aggregated = net_source_matches %>% + group_by(row_number = target_id) %>% + summarise(!!sym(new_name) := agg_fun(!!mult_expr, na.rm = TRUE)) + + # For max, we also want to round the final result. + if (identical(agg_fun, max)) { + net_target_aggregated = net_target_aggregated %>% + mutate(!!sym(new_name) := round(!!sym(new_name))) + } + + # Join the aggregated values back to the target data. + net_target_joined = target_data %>% + mutate(row_number = row_number()) %>% + st_drop_geometry() %>% + select(-any_of(new_name)) %>% # Remove the original column (if it exists) + left_join(net_target_aggregated, by = "row_number") %>% + select(id, !!sym(new_name)) + + return(net_target_joined) +} diff --git a/R/corenet.R b/R/corenet.R index caeedb5..848544d 100644 --- a/R/corenet.R +++ b/R/corenet.R @@ -33,8 +33,7 @@ utils::globalVariables(c("edge_paths", "influence_network", "all_fastest_bicycle #' NPT_demo_3km = sf::st_set_crs(NPT_demo_3km, 27700) #' base_network = sf::st_transform(os_edinburgh_demo_3km, crs = 27700) #' influence_network = sf::st_transform(NPT_demo_3km, crs = 27700) -#' target_zone = zonebuilder::zb_zone("Edinburgh", n_circles = 2) |> -#' sf::st_transform(crs = "EPSG:27700") +#' target_zone = zonebuilder::zb_zone("Edinburgh", n_circles = 3) |> sf::st_transform(crs = "EPSG:27700") #' #' # Prepare the cohesive network #' OS_NPT_demo = cohesive_network_prep( base_network = base_network, @@ -46,7 +45,7 @@ utils::globalVariables(c("edge_paths", "influence_network", "all_fastest_bicycle #' -cohesive_network_prep = function(base_network, influence_network, target_zone, crs = "EPSG:27700", key_attribute = "road_function", attribute_values = c("A Road", "B Road", "Minor Road"), use_stplanr = TRUE) { +cohesive_network_prep = function(base_network, influence_network, target_zone, crs = "EPSG:27700", key_attribute = "road_function", attribute_values = c("A Road", "B Road", "Minor Road"), use_stplanr = FALSE) { base_network = sf::st_transform(base_network, crs) influence_network = sf::st_transform(influence_network, crs) target_zone = sf::st_transform(target_zone, crs) @@ -62,35 +61,69 @@ cohesive_network_prep = function(base_network, influence_network, target_zone, c dplyr::filter(!!rlang::sym(key_attribute) %in% attribute_values ) |> sf::st_transform(crs) |> sf::st_zm() - if (use_stplanr) { - # Assign functions for data aggregation based on network attribute values - name_list = names(NPT_zones) + # if (use_stplanr) { + # # Assign functions for data aggregation based on network attribute values + # name_list = names(NPT_zones) - funs = list() - for (name in name_list) { - if (name == "geometry") { - next # Correctly skip the current iteration if the name is "geometry" - } else if (name %in% c("gradient", "quietness")) { - funs[[name]] = mean # Assign mean function for specified fields - } else { - funs[[name]] = mean # Assign sum function for all other fields - } - } + # funs = list() + # for (name in name_list) { + # if (name == "geometry") { + # next # Correctly skip the current iteration if the name is "geometry" + # } else if (name %in% c("gradient", "quietness")) { + # funs[[name]] = mean # Assign mean function for specified fields + # } else { + # funs[[name]] = mean # Assign sum function for all other fields + # } + # } - # Merge road networks with specified parameters - filtered_OS_NPT_zones = stplanr::rnet_merge(filtered_OS_zones, NPT_zones, dist = 10, funs = funs, segment_length = 20, max_angle_diff = 10) - } else { - print("Using sf::st_join") - filtered_OS_NPT_zones = sf::st_join(filtered_OS_zones, NPT_zones, join = sf::st_intersects) - } + # # Merge road networks with specified parameters + # filtered_OS_NPT_zones = stplanr::rnet_merge(filtered_OS_zones, NPT_zones, dist = 10, funs = funs, segment_length = 20, max_angle_diff = 10) + + # } else { + # print("Using sf::st_join") + # filtered_OS_NPT_zones = sf::st_join(filtered_OS_zones, NPT_zones, join = sf::st_intersects) + # } + NPT_zones = sf::st_cast(NPT_zones, "LINESTRING") + filtered_OS_zones = sf::st_cast(filtered_OS_zones, "LINESTRING") + NPT_zones$id = 1:nrow(NPT_zones) + filtered_OS_zones$id = 1:nrow(filtered_OS_zones) + + params = list( + list( + source = NPT_zones, + target = filtered_OS_zones, + attribute = "all_fastest_bicycle_go_dutch", + new_name = "all_fastest_bicycle_go_dutch", + agg_fun = sum, + weights = c("target_weighted") + ) + ) + + results_list = purrr::map(params, function(p) { + anime_join( + source_data = p$source, + target_data = p$target, + attribute = p$attribute, + new_name = p$new_name, + agg_fun = p$agg_fun, + weights = p$weights, + angle_tolerance = 35, + distance_tolerance = 15 + ) + }) + + + filtered_OS_NPT_zones = reduce(results_list, function(x, y) { + left_join(x, y, by = "id") + }, .init = filtered_OS_zones) + filtered_OS_NPT_zones = filtered_OS_NPT_zones |> dplyr::mutate(dplyr::across(dplyr::where(is.numeric), as.integer)) - - print("Finished preparing the network data") - - return(filtered_OS_NPT_zones) + print("Finished preparing the network data") + + return(filtered_OS_NPT_zones) } @@ -160,9 +193,11 @@ corenet = function(influence_network, cohesive_base_network, target_zone, key_at # Perform DBSCAN clustering coordinates = sf::st_coordinates(centroids) - clusters = dbscan::dbscan(coordinates, eps = 18, minPts = 1) - centroids$cluster = clusters$cluster - unique_centroids = centroids[!duplicated(centroids$cluster), ] + coordinates_clean = coordinates[complete.cases(coordinates), ] + clusters = dbscan::dbscan(coordinates_clean, eps = 18, minPts = 1) + centroids_clean = centroids[complete.cases(coordinates), ] + centroids_clean$cluster = clusters$cluster + unique_centroids = centroids_clean[!duplicated(centroids_clean$cluster), ] # create a buffer of 10 meters around the cohesive_base_network cohesive_base_network_buffer = sf::st_buffer(cohesive_base_network, dist = 20) diff --git a/R/pkgs.R b/R/pkgs.R new file mode 100644 index 0000000..a95e12e --- /dev/null +++ b/R/pkgs.R @@ -0,0 +1,27 @@ +get_pkgs = function() { + c( + "crew", # For targets with workers + "collapse", # Needed for bind_sf + "cyclestreets", # For routing + "geojsonsf", # For converting geojson to sf + "geos", # For geometric operations + "gert", # For interactive with git + "glue", # For string interpolation + "lubridate", # For working with dates and times + "lwgeom", # For working with spatial data + "nngeo", # Nearest neighbour functions + "osmextract", # For extracting OpenStreetMap data + "pct", # PCT interface + "remotes", # For installing packages from remote sources + "sf", # For working with spatial data + "simodels", # For spatial interaction models + "snakecase", # For converting strings to snake case + "stplanr", # For sustainable transport planning + "targets", # For managing targets in a workflow + "tidyverse", # Includes dplyr, ggplot2, tidyr, stringr etc. + "zonebuilder", # For creating zones for spatial analysis + "iterators", # For creating iterators + "doParallel", # For parallel processing + "httr" + ) +} diff --git a/code/install.R b/code/install.R new file mode 100644 index 0000000..b21c6f6 --- /dev/null +++ b/code/install.R @@ -0,0 +1,28 @@ +# Run as part of _targets.R to install packages +# Do you want to reinstall github packages, set to TRUE for first run +update_github_packages = TRUE +options(Ncpus = 4) +source("R/pkgs.R") +pkgs = get_pkgs() +remotes::install_cran(pkgs) + +# rsgeo +install.packages( + 'rsgeo', + repos = c('https://josiahparry.r-universe.dev', 'https://cloud.r-project.org') +) + +# Repeated builds can it GitHub API limit, set to TRUE in _targets.R to check for package updates +if (update_github_packages) { + remotes::install_dev("cyclestreets") + remotes::install_github("dabreegster/odjitter", subdir = "r") + remotes::install_github("robinlovelace/ukboundaries") + remotes::install_github("robinlovelace/simodels") + remotes::install_version("rgeos", version = "0.6-3") + remotes::install_dev("od") + remotes::install_dev("osmextract") + remotes::install_github("nptscot/corenet") + remotes::install_github("nptscot/osmactive") + install.packages("pak") + pak::pak("JosiahParry/anime/r") +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a3d93c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +geopandas +shapely +pandas \ No newline at end of file