Skip to content

Commit

Permalink
Merge pull request #27 from nptscot/use_anime_join
Browse files Browse the repository at this point in the history
add .devcontainer and replace rnet_merge with anime_join
  • Loading branch information
wangzhao0217 authored Feb 19, 2025
2 parents e1bd1d7 + 4f917f0 commit f6da227
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 29 deletions.
37 changes: 37 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"

}

47 changes: 47 additions & 0 deletions .devcontainer/postCreateCommand.sh
Original file line number Diff line number Diff line change
@@ -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."
68 changes: 68 additions & 0 deletions R/anime_join.R
Original file line number Diff line number Diff line change
@@ -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)
}
93 changes: 64 additions & 29 deletions R/corenet.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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)
}


Expand Down Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions R/pkgs.R
Original file line number Diff line number Diff line change
@@ -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"
)
}
28 changes: 28 additions & 0 deletions code/install.R
Original file line number Diff line number Diff line change
@@ -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")
}
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
geopandas
shapely
pandas

0 comments on commit f6da227

Please sign in to comment.