├── .github ├── .gitignore └── workflows │ ├── pkgdown.yaml │ ├── R-CMD-check.yaml │ └── docker-build.yml ├── data-raw ├── .gitignore ├── test-omitted-ways.qmd ├── traffic_volumes_edinburgh.R ├── test-sett.qmd ├── osm_edinburgh.R ├── calculateAverageWidths.R ├── tests.Rmd └── gisruk2025.qmd ├── vignettes ├── .gitignore ├── images │ └── paste-1.png ├── classifying-cycle-infrastructure.qmd ├── references.bib ├── classify-cbd.Rmd └── gisruk2025.Rmd ├── LICENSE ├── street_space.png ├── data ├── cycle_net_f.rda ├── drive_net_f.rda ├── osm_edinburgh.rda ├── los_table_complete.rda ├── traffic_random_edinburgh.rda └── traffic_volumes_edinburgh.rda ├── _pkgdown.yml ├── edinburgh_bus_lanes.png ├── man ├── figures │ ├── README-leeds-1.png │ ├── README-bristol-1.png │ ├── README-dublin-1.png │ ├── README-lisbon-1.png │ ├── README-london-1.png │ ├── README-cambridge-1.png │ ├── README-edinburgh-1.png │ ├── README-christchurch-1.png │ ├── README-christchurch-2.png │ ├── README-unnamed-chunk-8-1.png │ ├── README-level_of_service-1.png │ └── README-minimal_plot_osm-1.png ├── et_active.Rd ├── cycle_net_f.Rd ├── drive_net_f.Rd ├── osm_edinburgh.Rd ├── count_bus_lanes.Rd ├── traffic_volumes_edinburgh.Rd ├── get_active_travel_network.Rd ├── npt_to_cbd_aadt.Rd ├── get_palette_npt.Rd ├── classify_speeds.Rd ├── get_cycling_network.Rd ├── npt_to_cbd_aadt_numeric.Rd ├── get_points.Rd ├── clean_widths.Rd ├── los_table_complete.Rd ├── get_traffic_calming.Rd ├── is_wide.Rd ├── estimate_traffic.Rd ├── clean_speeds.Rd ├── get_driving_network.Rd ├── plot_osm_tmap.Rd ├── npt_to_cbd_aadt_character.Rd ├── distance_to_road.Rd ├── get_bus_routes.Rd ├── classify_shared_use.Rd ├── get_travel_network.Rd ├── level_of_service.Rd ├── classify_cycle_infrastructure.Rd └── get_parallel_values.Rd ├── .devcontainer └── devcontainer.json ├── R ├── globals.R ├── test-code │ └── portugal.R └── get_parallel_values.R ├── .Rbuildignore ├── Dockerfile ├── app ├── Dockerfile └── app.R ├── .gitignore ├── LICENSE.md ├── DESCRIPTION ├── NAMESPACE ├── inst └── extdata │ ├── level-of-service-table.csv │ └── los_table_complete.csv ├── code ├── traffic-volumes.R └── classify-roads.R └── README.Rmd /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /data-raw/.gitignore: -------------------------------------------------------------------------------- 1 | /.quarto/ 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | 4 | /.quarto/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2024 2 | COPYRIGHT HOLDER: Robin Lovelace 3 | -------------------------------------------------------------------------------- /street_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/street_space.png -------------------------------------------------------------------------------- /data/cycle_net_f.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/cycle_net_f.rda -------------------------------------------------------------------------------- /data/drive_net_f.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/drive_net_f.rda -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://nptscot.github.io/osmactive/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /data/osm_edinburgh.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/osm_edinburgh.rda -------------------------------------------------------------------------------- /edinburgh_bus_lanes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/edinburgh_bus_lanes.png -------------------------------------------------------------------------------- /data/los_table_complete.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/los_table_complete.rda -------------------------------------------------------------------------------- /man/figures/README-leeds-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-leeds-1.png -------------------------------------------------------------------------------- /vignettes/images/paste-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/vignettes/images/paste-1.png -------------------------------------------------------------------------------- /man/figures/README-bristol-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-bristol-1.png -------------------------------------------------------------------------------- /man/figures/README-dublin-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-dublin-1.png -------------------------------------------------------------------------------- /man/figures/README-lisbon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-lisbon-1.png -------------------------------------------------------------------------------- /man/figures/README-london-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-london-1.png -------------------------------------------------------------------------------- /data/traffic_random_edinburgh.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/traffic_random_edinburgh.rda -------------------------------------------------------------------------------- /data/traffic_volumes_edinburgh.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/data/traffic_volumes_edinburgh.rda -------------------------------------------------------------------------------- /man/figures/README-cambridge-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-cambridge-1.png -------------------------------------------------------------------------------- /man/figures/README-edinburgh-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-edinburgh-1.png -------------------------------------------------------------------------------- /man/figures/README-christchurch-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-christchurch-1.png -------------------------------------------------------------------------------- /man/figures/README-christchurch-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-christchurch-2.png -------------------------------------------------------------------------------- /man/figures/README-unnamed-chunk-8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-unnamed-chunk-8-1.png -------------------------------------------------------------------------------- /man/figures/README-level_of_service-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-level_of_service-1.png -------------------------------------------------------------------------------- /man/figures/README-minimal_plot_osm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nptscot/osmactive/HEAD/man/figures/README-minimal_plot_osm-1.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osmactive Dev Container", 3 | "image": "ghcr.io/nptscot/osmactive", 4 | "extensions": [ 5 | "quarto.quarto", 6 | "reditorsupport.r" 7 | ], 8 | "remoteUser": "rstudio" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /R/globals.R: -------------------------------------------------------------------------------- 1 | utils::globalVariables(c( 2 | "cycle_pedestrian_separation", "bicycle", "surface", "smoothness", 3 | "cycleway_left", "oneway", "cycleway_right", "osm_id", 4 | "azimuth_target", "azimuth_source", "new_values", "angle_diff", 5 | ":=", "new_value", "new_value_numeric", "service", 6 | "los_table_complete", "los" 7 | )) -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^README\.Rmd$ 2 | ^LICENSE\.md$ 3 | ^_pkgdown\.yml$ 4 | ^docs$ 5 | ^pkgdown$ 6 | ^\.github$ 7 | ^data-raw$ 8 | ^.*\.Rproj$ 9 | ^\.Rproj\.user$ 10 | cache 11 | .*\.Rds$ 12 | ^\.devcontainer$ 13 | ^app$ 14 | ^code$ 15 | ^edinburgh_bus_lanes\.png$ 16 | ^street_space\.png$ 17 | ^cycle_net\.geojson$ 18 | ^osm_district\.geojson$ 19 | -------------------------------------------------------------------------------- /man/et_active.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{et_active} 4 | \alias{et_active} 5 | \title{This function returns OSM keys that are relevant for active travel} 6 | \usage{ 7 | et_active() 8 | } 9 | \description{ 10 | This function returns OSM keys that are relevant for active travel 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/geocompx/latest@sha256:00ff6dd552f2e9168488dee6ee1babb4b6bee805f0a2d35aff548d6ee2730625 2 | 3 | # Set the working directory 4 | WORKDIR /app 5 | 6 | # Copy the current directory contents into the container 7 | COPY . /app 8 | 9 | # Install the osmactive package 10 | RUN R -e 'remotes::install_local(dependencies = TRUE)' && \ 11 | R -e 'library(osmactive)' 12 | -------------------------------------------------------------------------------- /man/cycle_net_f.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{cycle_net_f} 4 | \alias{cycle_net_f} 5 | \title{Cycle network for Edinburgh, filtered around Leith Walk} 6 | \format{ 7 | An sf data frame 8 | } 9 | \description{ 10 | This dataset contains the cycle network for Edinburgh, filtered around Leith Walk. 11 | } 12 | \examples{ 13 | head(cycle_net_f) 14 | plot(cycle_net_f) 15 | } 16 | -------------------------------------------------------------------------------- /man/drive_net_f.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{drive_net_f} 4 | \alias{drive_net_f} 5 | \title{Driving network for Edinburgh, filtered around Leith Walk} 6 | \format{ 7 | An sf data frame 8 | } 9 | \description{ 10 | This dataset contains the driving network for Edinburgh, filtered around Leith Walk. 11 | } 12 | \examples{ 13 | head(drive_net_f) 14 | plot(drive_net_f) 15 | } 16 | -------------------------------------------------------------------------------- /man/osm_edinburgh.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \docType{data} 4 | \name{osm_edinburgh} 5 | \alias{osm_edinburgh} 6 | \title{Data from edinburgh's OSM network} 7 | \format{ 8 | An sf data frame 9 | } 10 | \description{ 11 | Data from edinburgh's OSM network 12 | } 13 | \examples{ 14 | library(sf) 15 | names(osm_edinburgh) 16 | head(osm_edinburgh) 17 | plot(osm_edinburgh) 18 | } 19 | \keyword{datasets} 20 | -------------------------------------------------------------------------------- /man/count_bus_lanes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{count_bus_lanes} 4 | \alias{count_bus_lanes} 5 | \title{Count how many bus lanes there are} 6 | \usage{ 7 | count_bus_lanes(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network} 11 | } 12 | \value{ 13 | The number of bus lanes 14 | } 15 | \description{ 16 | Count how many bus lanes there are 17 | } 18 | \examples{ 19 | osm = osm_edinburgh 20 | count_bus_lanes(osm) 21 | } 22 | -------------------------------------------------------------------------------- /man/traffic_volumes_edinburgh.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \docType{data} 4 | \name{traffic_volumes_edinburgh} 5 | \alias{traffic_volumes_edinburgh} 6 | \alias{traffic_random_edinburgh} 7 | \title{Data from edinburgh's OSM network with traffic volumes} 8 | \format{ 9 | A data frame 10 | } 11 | \description{ 12 | Data from edinburgh's OSM network with traffic volumes 13 | } 14 | \examples{ 15 | head(traffic_volumes_edinburgh) 16 | head(traffic_random_edinburgh) 17 | } 18 | \keyword{datasets} 19 | -------------------------------------------------------------------------------- /data-raw/test-omitted-ways.qmd: -------------------------------------------------------------------------------- 1 | The aim of the code below is to test which ways are omitted by the `get_cycling_network` function. 2 | 3 | 4 | ```{r} 5 | devtools::load_all() 6 | library(tidyverse) 7 | osm_national = get_travel_network("Scotland") 8 | osm_trinity_crescent = osm_national |> 9 | filter(name == "Trinity Crescent") 10 | mapview::mapview(osm_trinity_crescent) 11 | 12 | # Use browser() for interactive debugging in the following line: 13 | devtools::load_all() 14 | osm_trinity_filtered = get_cycling_network(osm_trinity_crescent) 15 | mapview::mapview(osm_trinity_filtered) 16 | ``` -------------------------------------------------------------------------------- /man/get_active_travel_network.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_active_travel_network} 4 | \alias{get_active_travel_network} 5 | \title{Get the OSM cycling network} 6 | \usage{ 7 | get_active_travel_network(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An OSM network object} 11 | } 12 | \value{ 13 | A sf object with the OSM network relevant for active travel 14 | with columns containing information on traffic calming measures, 15 | crossings, cycle infrastructure, and more. 16 | } 17 | \description{ 18 | Get the OSM cycling network 19 | } 20 | -------------------------------------------------------------------------------- /man/npt_to_cbd_aadt.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{npt_to_cbd_aadt} 4 | \alias{npt_to_cbd_aadt} 5 | \title{Convert AADT to CBD AADT} 6 | \usage{ 7 | npt_to_cbd_aadt(AADT) 8 | } 9 | \arguments{ 10 | \item{AADT}{A character or numeric value representing the Annual Average Daily Traffic.} 11 | } 12 | \value{ 13 | The converted CBD AADT value. 14 | } 15 | \description{ 16 | This function converts Annual Average Daily Traffic (AADT) to Central Business District (CBD) AADT. 17 | It handles both character and numeric inputs by delegating to appropriate helper functions. 18 | } 19 | -------------------------------------------------------------------------------- /man/get_palette_npt.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_palette_npt} 4 | \alias{get_palette_npt} 5 | \title{Get the palette for the NPT cycle segregation levels} 6 | \usage{ 7 | get_palette_npt() 8 | } 9 | \value{ 10 | A palette for the NPT cycle segregation levels 11 | } 12 | \description{ 13 | Get the palette for the NPT cycle segregation levels 14 | } 15 | \examples{ 16 | cols = get_palette_npt() 17 | jsonlite::toJSON(as.list(cols), pretty = TRUE) 18 | col_labs = c("OffRd", "SegW", "SegN", "Share", "Paint") 19 | barplot(seq_along(cols), col = cols, names.arg = col_labs) 20 | } 21 | -------------------------------------------------------------------------------- /man/classify_speeds.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{classify_speeds} 4 | \alias{classify_speeds} 5 | \title{Classify Speeds} 6 | \usage{ 7 | classify_speeds(speed_mph) 8 | } 9 | \arguments{ 10 | \item{speed_mph}{A numeric vector representing speeds in miles per hour.} 11 | } 12 | \value{ 13 | A character vector with the speed categories. 14 | } 15 | \description{ 16 | This function classifies speeds in miles per hour (mph) into categories. 17 | } 18 | \examples{ 19 | classify_speeds(c(15, 25, 35, 45, 55, 65)) 20 | # Returns: "<20 mph", "20 mph", "30 mph", "40 mph", "50 mph", "60+ mph" 21 | } 22 | -------------------------------------------------------------------------------- /man/get_cycling_network.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_cycling_network} 4 | \alias{get_cycling_network} 5 | \title{Get the OSM cycling network} 6 | \usage{ 7 | get_cycling_network( 8 | osm, 9 | ex_c = exclude_highway_cycling(), 10 | ex_b = exclude_bicycle_cycling() 11 | ) 12 | } 13 | \arguments{ 14 | \item{osm}{An OSM network object} 15 | 16 | \item{ex_c}{A vector of highway values to exclude} 17 | 18 | \item{ex_b}{A vector of bicycle values to exclude} 19 | } 20 | \value{ 21 | A sf object with the OSM cycling network 22 | } 23 | \description{ 24 | Get the OSM cycling network 25 | } 26 | -------------------------------------------------------------------------------- /man/npt_to_cbd_aadt_numeric.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{npt_to_cbd_aadt_numeric} 4 | \alias{npt_to_cbd_aadt_numeric} 5 | \title{Convert AADT categories to CBD AADT character ranges} 6 | \usage{ 7 | npt_to_cbd_aadt_numeric(AADT) 8 | } 9 | \arguments{ 10 | \item{AADT}{A numeric vector representing AADT} 11 | } 12 | \value{ 13 | A character vector with the converted CBD AADT ranges. Possible return values are "0 to 999", "1000 to 1999", "2000 to 3999", and "4000+". 14 | } 15 | \description{ 16 | This function takes an AADT (Annual Average Daily Traffic) category and converts it to a ranges 17 | } 18 | -------------------------------------------------------------------------------- /man/get_points.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_points} 4 | \alias{get_points} 5 | \title{Get the OSM network functions} 6 | \usage{ 7 | get_points(place, extra_tags = c("traffic_calming", "crossing"), ...) 8 | } 9 | \arguments{ 10 | \item{place}{A place name or a bounding box passed to \code{osmextract::oe_get()}} 11 | 12 | \item{extra_tags}{A vector of extra tags to be included in the OSM extract} 13 | 14 | \item{...}{Additional arguments passed to \code{osmextract::oe_get()}} 15 | } 16 | \value{ 17 | A sf object with the OSM network 18 | } 19 | \description{ 20 | Get the OSM network functions 21 | } 22 | -------------------------------------------------------------------------------- /man/clean_widths.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{clean_widths} 4 | \alias{clean_widths} 5 | \title{Clean cycleway widths (use est_widths when available and width otherwise)} 6 | \usage{ 7 | clean_widths(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network} 11 | } 12 | \value{ 13 | An sf object with the cleaned cycleway widths in the column \code{width} 14 | } 15 | \description{ 16 | Clean cycleway widths (use est_widths when available and width otherwise) 17 | } 18 | \examples{ 19 | osm = osm_edinburgh 20 | osm$width 21 | osm$est_width = NA 22 | osm$est_width[1:3] = 2 23 | osm_cleaned = clean_widths(osm) 24 | osm$width 25 | osm_cleaned$width 26 | } 27 | -------------------------------------------------------------------------------- /man/los_table_complete.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{los_table_complete} 4 | \alias{los_table_complete} 5 | \title{Complete Level of Service (LOS) table} 6 | \format{ 7 | A data frame with columns including speed limit, AADT, cycle_segregation and level_of_service 8 | } 9 | \source{ 10 | Generated from the classify-cbd vignette 11 | } 12 | \usage{ 13 | data(los_table_complete) 14 | } 15 | \description{ 16 | This dataset contains the complete level of service information, including missing categories, in a long format. 17 | } 18 | \examples{ 19 | data(los_table_complete) 20 | cols = c("Speed Limit (mph)", "Speed (85th kph)") 21 | unique(los_table_complete[cols]) 22 | head(los_table_complete) 23 | } 24 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rocker/geospatial:latest 2 | 3 | RUN /rocker_scripts/setup_R.sh https://packagemanager.posit.co/cran/__linux__/jammy/2023-01-29 4 | RUN /rocker_scripts/install_shiny_server.sh 5 | 6 | # Install any necessary R packages 7 | RUN R -e "install.packages(c('remotes'))" 8 | RUN R -e "install.packages(c('cols4all', 'geos', 'osmextract'))" 9 | RUN R -e "install.packages(c('shiny', 'tmap'))" 10 | RUN R -e "remotes::install_github('nptscot/osmactive', dependencies = 'Suggests', ask = FALSE, Ncpus = parallel::detectCores())" 11 | 12 | 13 | # Copy the application files into the container 14 | COPY app.R /app/app.R 15 | 16 | # Expose port 3838 17 | EXPOSE 3838 18 | 19 | # Start the Shiny app 20 | CMD ["R", "-e", "shiny::runApp('/app/app.R', host = '0.0.0.0', port = 3838)"] -------------------------------------------------------------------------------- /man/get_traffic_calming.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_traffic_calming} 4 | \alias{get_traffic_calming} 5 | \title{Get the OSM network functions} 6 | \usage{ 7 | get_traffic_calming(osm_points, filter = TRUE) 8 | } 9 | \arguments{ 10 | \item{osm_points}{An sf object with the OSM network} 11 | 12 | \item{filter}{Whether to filter the results to only include traffic calming measures} 13 | } 14 | \value{ 15 | A data frame with the OSM network relevant for active travel 16 | } 17 | \description{ 18 | Get the OSM network functions 19 | } 20 | \examples{ 21 | osm_points = get_points("Edinburgh") 22 | traffic_calming = get_traffic_calming(osm_points) 23 | head(traffic_calming) 24 | plot(traffic_calming["traffic_calming"]) 25 | } 26 | -------------------------------------------------------------------------------- /man/is_wide.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{is_wide} 4 | \alias{is_wide} 5 | \title{Classify Separated cycle track by width} 6 | \usage{ 7 | is_wide(x, min_width = 2) 8 | } 9 | \arguments{ 10 | \item{x}{A numeric vector with the width of the cycleway (m)} 11 | 12 | \item{min_width}{The minimum width for a cycleway to be considered wide (m)} 13 | } 14 | \value{ 15 | A logical vector indicating whether the cycleway is wide 16 | } 17 | \description{ 18 | This function classifies cycleways as wide if the width is greater than or equal to \code{min_width}. 19 | NA values are replaced with 0, meaning that ways with no measurement are considered narrow. 20 | } 21 | \examples{ 22 | x = osm_edinburgh$width 23 | x 24 | is_wide(x) 25 | } 26 | -------------------------------------------------------------------------------- /man/estimate_traffic.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{estimate_traffic} 4 | \alias{estimate_traffic} 5 | \title{Estimate traffic} 6 | \usage{ 7 | estimate_traffic(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network} 11 | } 12 | \value{ 13 | An sf object with the estimated road traffic volumes in the column \code{assumed_volume} 14 | } 15 | \description{ 16 | Estimate traffic 17 | } 18 | \examples{ 19 | osm = osm_edinburgh 20 | osm_traffic = estimate_traffic(osm) 21 | # check NAs: 22 | sel_nas = is.na(osm_traffic$assumed_volume) 23 | osm_no_traffic = osm_traffic[sel_nas, c("highway")] 24 | table(osm_no_traffic$highway) # Active travel infrastructure has no road traffic 25 | table(osm_traffic$assumed_volume, useNA = "always") 26 | } 27 | -------------------------------------------------------------------------------- /man/clean_speeds.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{clean_speeds} 4 | \alias{clean_speeds} 5 | \title{Clean speeds} 6 | \usage{ 7 | clean_speeds(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network} 11 | } 12 | \value{ 13 | An sf object with the cleaned speed values in the column \code{maxspeed_clean} 14 | } 15 | \description{ 16 | Clean speeds 17 | } 18 | \examples{ 19 | osm = osm_edinburgh 20 | osm_cleaned = clean_speeds(osm) 21 | # check NAs: 22 | sel_nas = is.na(osm_cleaned$maxspeed_clean) 23 | osm_no_maxspeed = osm_cleaned[sel_nas, c("highway")] 24 | table(osm_no_maxspeed$highway) # Active travel infrastructure has no maxspeed 25 | table(osm_cleaned$maxspeed) 26 | table(osm_cleaned$maxspeed_clean) 27 | plot(osm_cleaned[c("maxspeed", "maxspeed_clean")]) 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # History files 2 | .Rhistory 3 | .Rapp.history 4 | 5 | # Session Data files 6 | .RData 7 | .RDataTmp 8 | 9 | # User-specific files 10 | .Ruserdata 11 | 12 | # Example code in package build process 13 | *-Ex.R 14 | 15 | # Output files from R CMD build 16 | /*.tar.gz 17 | 18 | # Output files from R CMD check 19 | /*.Rcheck/ 20 | 21 | # RStudio files 22 | .Rproj.user/ 23 | *.Rproj 24 | 25 | # produced vignettes 26 | vignettes/*.html 27 | vignettes/*.pdf 28 | 29 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 30 | .httr-oauth 31 | 32 | # knitr and R markdown default cache directories 33 | *_cache/ 34 | /cache/ 35 | 36 | # Temporary files created by R markdown 37 | *.utf8.md 38 | *.knit.md 39 | 40 | # R Environment Variables 41 | .Renviron 42 | 43 | # pkgdown site 44 | docs/ 45 | 46 | # translation temp files 47 | po/*~ 48 | 49 | # RStudio Connect folder 50 | rsconnect/ 51 | *.html 52 | docs 53 | *.gpkg 54 | inst/doc 55 | *.geojson 56 | *.Rds 57 | *.pdf 58 | 59 | /.quarto/ 60 | -------------------------------------------------------------------------------- /man/get_driving_network.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_driving_network} 4 | \alias{get_driving_network} 5 | \alias{get_driving_network_major} 6 | \title{Get the OSM driving network} 7 | \usage{ 8 | get_driving_network(osm, ex_d = exclude_highway_driving()) 9 | 10 | get_driving_network_major( 11 | osm, 12 | ex_d = exclude_highway_driving(), 13 | pattern = "motorway|trunk|primary|secondary|tertiary" 14 | ) 15 | } 16 | \arguments{ 17 | \item{osm}{An OSM network object} 18 | 19 | \item{ex_d}{A character string of highway values to exclude in the form \code{value1|value2} etc} 20 | 21 | \item{pattern}{A character string of highway values to define major roads in the form \code{motorway|trunk|primary|secondary|tertiary}} 22 | } 23 | \value{ 24 | A sf object with the OSM driving network 25 | } 26 | \description{ 27 | This function returns the OSM driving network by excluding certain highway values. 28 | } 29 | \details{ 30 | \code{get_driving_network_major} returns only the major roads. 31 | } 32 | -------------------------------------------------------------------------------- /man/plot_osm_tmap.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{plot_osm_tmap} 4 | \alias{plot_osm_tmap} 5 | \title{Create a tmap object for visualizing the classified cycle network} 6 | \usage{ 7 | plot_osm_tmap( 8 | cycle_network_classified, 9 | popup.vars = c("name", "osm_id", "cycle_segregation", "distance_to_road", "maxspeed", 10 | "highway", "cycleway", "bicycle", "lanes", "width", "surface", "other_tags"), 11 | lwd = 4, 12 | palette = get_palette_npt() 13 | ) 14 | } 15 | \arguments{ 16 | \item{cycle_network_classified}{An sf object with the classified cycle network} 17 | 18 | \item{popup.vars}{A vector of variables to be displayed in the popup} 19 | 20 | \item{lwd}{The line width for the cycle network} 21 | 22 | \item{palette}{The palette to be used for the cycle segregation levels, 23 | such as "-PuBuGn" or "npt" (default)} 24 | } 25 | \value{ 26 | A tmap object for visualizing the classified cycle network 27 | } 28 | \description{ 29 | Create a tmap object for visualizing the classified cycle network 30 | } 31 | -------------------------------------------------------------------------------- /man/npt_to_cbd_aadt_character.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{npt_to_cbd_aadt_character} 4 | \alias{npt_to_cbd_aadt_character} 5 | \title{Convert AADT categories to CBD AADT character ranges} 6 | \usage{ 7 | npt_to_cbd_aadt_character(AADT) 8 | } 9 | \arguments{ 10 | \item{AADT}{A character vector representing AADT categories. Valid categories include "0 to 1000", "0 to 2000", "1000+", "All", "1000 to 2000", "2000 to 4000", "2000+", and "4000+".} 11 | } 12 | \value{ 13 | A character vector with the converted CBD AADT ranges. Possible return values are "0 to 999", "1000 to 1999", "2000 to 3999", and "4000+". 14 | } 15 | \description{ 16 | This function takes an AADT (Annual Average Daily Traffic) category and converts it to a ranges 17 | } 18 | \examples{ 19 | npt_to_cbd_aadt_character("0 to 1000") # returns "0 to 999" 20 | npt_to_cbd_aadt_character("1000 to 2000") # returns "1000 to 1999" 21 | npt_to_cbd_aadt_character("2000 to 4000") # returns "2000 to 3999" 22 | npt_to_cbd_aadt_character("4000+") # returns "4000+" 23 | } 24 | -------------------------------------------------------------------------------- /man/distance_to_road.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{distance_to_road} 4 | \alias{distance_to_road} 5 | \title{Calculate distance from route network segments to roads} 6 | \usage{ 7 | distance_to_road(rnet, roads) 8 | } 9 | \arguments{ 10 | \item{rnet}{The route network for which the distance to the road needs to be calculated.} 11 | 12 | \item{roads}{The road network to which the distance needs to be calculated.} 13 | } 14 | \value{ 15 | An sf object with the new column \code{distance_to_road} that contains the distance to the road. 16 | } 17 | \description{ 18 | This function approximates the distance from the route network to the nearest road. 19 | It does this by first computing the \code{sf::st_point_on_surface} of the route network segments 20 | and then calculating the distance to the nearest road using the \code{geos::geos_distance} function. 21 | } 22 | \examples{ 23 | osm = osm_edinburgh 24 | cycle_network = get_cycling_network(osm) 25 | driving_network = get_driving_network(osm) 26 | edinburgh_cycle_with_distance = distance_to_road(cycle_network, driving_network) 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 Robin Lovelace 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /man/get_bus_routes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_bus_routes} 4 | \alias{get_bus_routes} 5 | \title{Function to get multilinestrings representing bus routes} 6 | \usage{ 7 | get_bus_routes( 8 | place, 9 | query = "SELECT * FROM multilinestrings WHERE route == 'bus'", 10 | extra_tags = "route", 11 | ... 12 | ) 13 | } 14 | \arguments{ 15 | \item{place}{A place name or a bounding box passed to \code{osmextract::oe_get()}} 16 | 17 | \item{query}{A query to be passed to \code{osmextract::oe_get()}} 18 | 19 | \item{extra_tags}{A vector of extra tags to be included in the OSM extract} 20 | 21 | \item{...}{Additional arguments passed to \code{osmextract::oe_get()}} 22 | } 23 | \value{ 24 | An sf object with the bus routes 25 | } 26 | \description{ 27 | It implements the query 28 | } 29 | \details{ 30 | \if{html}{\out{
}}\preformatted{[out:json][timeout:25]; 31 | relation["route"="bus"](\{\{bbox\}\}); 32 | out geom; 33 | }\if{html}{\out{
}} 34 | 35 | See \href{https://overpass-turbo.eu/s/1Xaf}{overpass-turbo.eu} 36 | for an example of the query in action. 37 | } 38 | \examples{ 39 | # r = get_bus_routes("Edinburgh") 40 | # r = get_bus_routes("Isle of Wight") 41 | # plot(r["osm_id"]) 42 | } 43 | -------------------------------------------------------------------------------- /data-raw/traffic_volumes_edinburgh.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `traffic_volumes_edinburgh` dataset goes here 2 | 3 | library(tidyverse) 4 | osm = osm_edinburgh 5 | nrow(osm) 6 | table(osm$highway) 7 | osm_with_traffic_volumes_random = osm |> 8 | transmute( 9 | osm_id = osm_id, 10 | traffic_volume = case_when( 11 | highway == "primary" ~ round(runif(n(), 1000, 6000)), 12 | highway == "secondary" ~ round(runif(n(), 500, 5000)), 13 | highway == "tertiary" ~ round(runif(n(), 200, 4000)), 14 | TRUE ~ round(runif(n(), 0, 3000)) 15 | ) 16 | ) 17 | 18 | plot(osm_with_traffic_volumes_random["traffic_volume"]) 19 | 20 | # Hard-coded values 21 | osm_with_traffic_volumes = osm |> 22 | transmute( 23 | osm_id = osm_id, 24 | traffic_volume = case_when( 25 | highway == "primary" ~ 6000, 26 | highway == "secondary" ~ 5000, 27 | highway == "tertiary" ~ 3000, 28 | TRUE ~ 1000 29 | ) 30 | ) 31 | plot(osm_with_traffic_volumes["traffic_volume"]) 32 | 33 | # Save the data 34 | traffic_volumes_edinburgh = osm_with_traffic_volumes |> 35 | sf::st_drop_geometry() 36 | traffic_random_edinburgh = osm_with_traffic_volumes_random |> 37 | sf::st_drop_geometry() 38 | 39 | usethis::use_data(traffic_volumes_edinburgh, overwrite = TRUE) 40 | usethis::use_data(traffic_random_edinburgh, overwrite = TRUE) 41 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: osmactive 2 | Title: Extract Active Travel Infrastructure from OpenStreetMap 3 | Version: 0.0.0.9000 4 | Authors@R: c( 5 | person("Robin", "Lovelace", , "rob00x@gmail.com", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0001-5679-6536")), 7 | person("Joey", "Talbot", , "j.d.talbot@leeds.ac.uk", role = "aut", 8 | comment = c(ORCID = "0000-0002-6520-4560")) 9 | ) 10 | Description: This package adds value to OpenStreetMap data by extracting active travel infrastructure such as cycleways and footways. 11 | It builds on the 'osmextract' package and is designed to be extended to cover a wide range of classification systems for active travel infrastructure. 12 | License: MIT + file LICENSE 13 | URL: https://nptscot.github.io/osmactive/ 14 | BugReports: https://github.com/nptscot/osmactive/issues 15 | Imports: 16 | dplyr, 17 | forcats, 18 | geos, 19 | osmextract, 20 | sf, 21 | stringr, 22 | tidyr, 23 | rlang, 24 | stplanr 25 | Suggests: 26 | knitr, 27 | jsonlite, 28 | rmarkdown, 29 | tmap (>= 4.0.0), 30 | tidyverse, 31 | quarto, 32 | shiny 33 | Remotes: 34 | r-tmap/tmap 35 | Encoding: UTF-8 36 | Roxygen: list(markdown = TRUE) 37 | RoxygenNote: 7.3.2 38 | Depends: 39 | R (>= 3.5) 40 | LazyData: true 41 | VignetteBuilder: knitr 42 | -------------------------------------------------------------------------------- /man/classify_shared_use.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{classify_shared_use} 4 | \alias{classify_shared_use} 5 | \title{Classify ways by level of pedestrian/cyclist sharing} 6 | \usage{ 7 | classify_shared_use(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network} 11 | } 12 | \value{ 13 | An sf object with the classified cycle network 14 | } 15 | \description{ 16 | Ways on which bicycles and pedestrians share space are classified as "Shared Footway". 17 | According to 18 | } 19 | \details{ 20 | tagging includes: 21 | \itemize{ 22 | \item highway=path (signposted foot and bicycle path, no dividing line) 23 | \itemize{ 24 | \item foot=designated 25 | \item bicycle=designated 26 | \item segregated=no 27 | } 28 | \item highway=path (Signposted foot and bicycle path with dividing line.) 29 | \itemize{ 30 | \item segregated=yes 31 | } 32 | \item highway=pedestrian (A way intended for pedestrians) 33 | } 34 | } 35 | \examples{ 36 | osm = osm_edinburgh 37 | cycle_network = get_cycling_network(osm) 38 | cycle_network_shared = classify_shared_use(cycle_network) 39 | table(cycle_network_shared$cycle_pedestrian_separation) 40 | plot(cycle_network_shared["cycle_pedestrian_separation"]) 41 | # interactive map: 42 | # mapview::mapview(cycle_network_shared, zcol = "cycle_pedestrian_separation") 43 | } 44 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(classify_cycle_infrastructure) 4 | export(classify_shared_use) 5 | export(classify_speeds) 6 | export(clean_speeds) 7 | export(clean_widths) 8 | export(count_bus_lanes) 9 | export(distance_to_road) 10 | export(estimate_traffic) 11 | export(et_active) 12 | export(get_active_travel_network) 13 | export(get_bus_routes) 14 | export(get_cycling_network) 15 | export(get_driving_network) 16 | export(get_driving_network_major) 17 | export(get_palette_npt) 18 | export(get_points) 19 | export(get_traffic_calming) 20 | export(get_parallel_values) 21 | export(get_travel_network) 22 | export(is_wide) 23 | export(level_of_service) 24 | export(npt_to_cbd_aadt) 25 | export(npt_to_cbd_aadt_character) 26 | export(npt_to_cbd_aadt_numeric) 27 | export(plot_osm_tmap) 28 | importFrom(dplyr,all_of) 29 | importFrom(dplyr,case_when) 30 | importFrom(dplyr,filter) 31 | importFrom(dplyr,group_by) 32 | importFrom(dplyr,if_else) 33 | importFrom(dplyr,inner_join) 34 | importFrom(dplyr,left_join) 35 | importFrom(dplyr,mutate) 36 | importFrom(dplyr,rename) 37 | importFrom(dplyr,row_number) 38 | importFrom(dplyr,select) 39 | importFrom(dplyr,summarise) 40 | importFrom(dplyr,ungroup) 41 | importFrom(sf,st_buffer) 42 | importFrom(sf,st_drop_geometry) 43 | importFrom(sf,st_geometry) 44 | importFrom(sf,st_join) 45 | importFrom(sf,st_point_on_surface) 46 | importFrom(sf,st_sf) 47 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | # pull_request: 7 | # branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-r-dependencies@v2 34 | with: 35 | extra-packages: any::pkgdown, local::. 36 | needs: website 37 | 38 | - name: Build site 39 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 40 | shell: Rscript {0} 41 | 42 | - name: Deploy to GitHub pages 🚀 43 | if: github.event_name != 'pull_request' 44 | uses: JamesIves/github-pages-deploy-action@v4.5.0 45 | with: 46 | clean: false 47 | branch: gh-pages 48 | folder: docs 49 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master, classification] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | # - {os: macos-latest, r: 'release'} 22 | # - {os: windows-latest, r: 'release'} 23 | # - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | # - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | 36 | - uses: r-lib/actions/setup-r@v2 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::rcmdcheck 45 | needs: check 46 | 47 | - uses: r-lib/actions/check-r-package@v2 48 | with: 49 | upload-snapshots: true 50 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 51 | -------------------------------------------------------------------------------- /app/app.R: -------------------------------------------------------------------------------- 1 | message("Hello") 2 | 3 | # remotes::install_github('nptscot/osmactive', dependencies = 'Suggests', ask = FALSE, Ncpus = parallel::detectCores()) 4 | # remotes::install_github('r-tmap/tmap') 5 | 6 | library(shiny) 7 | library(tmap) 8 | library(osmextract) 9 | library(sf) 10 | library(dplyr) 11 | library(osmactive) 12 | 13 | message("Packages loaded") 14 | 15 | ui = fluidPage( 16 | titlePanel("OSM Active Travel Map"), 17 | checkboxInput("toggleMode", "Toggle Map Mode (Plot/View)", TRUE), 18 | textInput("city", "Enter City Name:", "Groningen"), 19 | tmapOutput("map", height = "600px") 20 | ) 21 | 22 | # TODO: make interactive? 23 | tmap_mode("view") 24 | server = function(input, output, session) { 25 | 26 | output$map = renderTmap({ 27 | city_name = input$city 28 | # Fetch OSM data for the city 29 | message("Getting OSM data") 30 | osm = tryCatch({ 31 | get_travel_network(city_name) 32 | }, error = function(e) { 33 | NULL # Handle cases where the city is not found 34 | }) 35 | 36 | if (is.null(osm)) { 37 | message("Loading osm data") 38 | return(tm_shape(sf::st_sf(sf::st_sfc(crs = 4326))) + 39 | tm_text("City not found")) 40 | } 41 | 42 | # Get cycling networkcycle_net = get_cycling_network(osm) 43 | drive_net = get_driving_network(osm) 44 | drive_net_major = get_driving_network(osm) 45 | cycle_net = get_cycling_network(osm) 46 | cycle_net = distance_to_road(cycle_net, drive_net) 47 | cycle_net = classify_cycle_infrastructure(cycle_net) 48 | plot_osm_tmap(cycle_net) 49 | }) 50 | } 51 | 52 | shinyApp(ui = ui, server = server) 53 | 54 | -------------------------------------------------------------------------------- /man/get_travel_network.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{get_travel_network} 4 | \alias{get_travel_network} 5 | \title{Get the OSM network functions} 6 | \usage{ 7 | get_travel_network( 8 | place, 9 | boundary = NULL, 10 | boundary_type = "clipsrc", 11 | extra_tags = et_active(), 12 | columns_to_remove = c("waterway", "aerialway", "barrier", "manmade"), 13 | ... 14 | ) 15 | } 16 | \arguments{ 17 | \item{place}{A place name or a bounding box passed to \code{osmextract::oe_get()}} 18 | 19 | \item{boundary}{An sf object used to clip the OSM data. Passed to \code{osmextract::oe_get()}} 20 | 21 | \item{boundary_type}{The clipping method for the boundary. Default is "clipsrc" which clips geometries to the boundary. See osmextract documentation for other options.} 22 | 23 | \item{extra_tags}{A vector of extra tags to be included in the OSM extract} 24 | 25 | \item{columns_to_remove}{A vector of columns to be removed from the OSM network} 26 | 27 | \item{...}{Additional arguments passed to \code{osmextract::oe_get()}} 28 | } 29 | \value{ 30 | A sf object with the OSM network 31 | } 32 | \description{ 33 | Get the OSM network functions 34 | } 35 | \examples{ 36 | # Basic usage: 37 | # osm = get_travel_network("Edinburgh") 38 | 39 | # Using a boundary to clip data: 40 | # library(sf) 41 | # boundary_poly = st_buffer(st_sfc(st_point(c(-3.2, 55.9)), crs = 4326), 0.01) 42 | # osm_clipped = get_travel_network("Edinburgh", boundary = boundary_poly) 43 | 44 | # Using different boundary types: 45 | # osm_intersect = get_travel_network("Edinburgh", boundary = boundary_poly, boundary_type = "clipsrc") 46 | } 47 | -------------------------------------------------------------------------------- /man/level_of_service.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{level_of_service} 4 | \alias{level_of_service} 5 | \title{Generate Cycle by Design Level of Service} 6 | \usage{ 7 | level_of_service(osm) 8 | } 9 | \arguments{ 10 | \item{osm}{An sf object with the road network including speed limits and traffic volumes} 11 | } 12 | \value{ 13 | An sf object with the Cycle by Design Level of Service in the column \verb{Level of Service} 14 | } 15 | \description{ 16 | Note: you need to have Annual Average Daily Traffic (AADT) values in the dataset 17 | These can be estimated using the \code{estimate_traffic()} function and converted 18 | to CbD AADT categories using the \code{npt_to_cbd_aadt()} function. 19 | } 20 | \examples{ 21 | osm = osm_edinburgh 22 | # Get infrastructure type: 23 | cycle_net = get_cycling_network(osm) 24 | # Get driving network: 25 | driving_net = get_driving_network(osm) 26 | # Get distance to road: 27 | osm = distance_to_road(cycle_net, driving_net) 28 | # Classify cycle infrastructure: 29 | osm = classify_cycle_infrastructure(osm, include_mixed_traffic = TRUE) 30 | osm = estimate_traffic(osm) 31 | osm$AADT = npt_to_cbd_aadt_numeric(osm$assumed_volume) 32 | osm$infrastructure = osm$cycle_segregation 33 | osm_los = level_of_service(osm) 34 | plot(osm_los["Level of Service"]) 35 | # mapview::mapview(osm_los, zcol = "Level of Service") 36 | # Test LoS on known road: 37 | mill_lane = data.frame( 38 | # TODO: find out why highway is needed for LoS 39 | highway = "residential", 40 | AADT = "4000+", 41 | maxspeed = "20 mph", 42 | cycle_segregation = "Mixed Traffic Street" 43 | ) 44 | # 45 | osm = sf::st_as_sf(mill_lane, geometry = osm$geometry[1]) 46 | mill_lane_los = level_of_service(osm) 47 | mill_lane_los 48 | # 49 | } 50 | -------------------------------------------------------------------------------- /man/classify_cycle_infrastructure.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/osmactive.R 3 | \name{classify_cycle_infrastructure} 4 | \alias{classify_cycle_infrastructure} 5 | \title{Segregation levels} 6 | \usage{ 7 | classify_cycle_infrastructure( 8 | osm, 9 | min_distance = 9.9, 10 | classification_type = "Scotland", 11 | include_mixed_traffic = FALSE 12 | ) 13 | } 14 | \arguments{ 15 | \item{osm}{The input dataset for which segregation levels need to be calculated.} 16 | 17 | \item{min_distance}{The minimum distance to the road for a cycleway to be considered off-road.} 18 | 19 | \item{classification_type}{The classification type to be used. Currently only "Scotland" is supported.} 20 | 21 | \item{include_mixed_traffic}{Whether to include mixed traffic segments in the results.} 22 | } 23 | \value{ 24 | A an sf object with the new column \code{cycle_segregation} that contains the segregation levels. 25 | } 26 | \description{ 27 | This function classifies OSM ways in by cycle infrastructure type levels for a given dataset. 28 | } 29 | \details{ 30 | See \href{https://wiki.openstreetmap.org/wiki/Key:cycleway}{wiki.openstreetmap.org/wiki/Key:cycleway} 31 | and \href{https://taginfo.openstreetmap.org/keys/cycleway#values}{taginfo.openstreetmap.org/keys/cycleway#values} 32 | for more information on cycleway values used to classify cycle infrastructure. 33 | 34 | Currently, only the "Scotland" classification type is supported. 35 | See the Scottish Government's \href{https://www.transport.gov.scot/publication/cycling-by-design/}{Cycling by Design} for more information. 36 | } 37 | \examples{ 38 | library(tmap) 39 | tmap_mode("plot") 40 | osm = osm_edinburgh 41 | cycle_network = get_cycling_network(osm) 42 | driving_network = get_driving_network(osm) 43 | netd = distance_to_road(cycle_network, driving_network) 44 | netc = classify_cycle_infrastructure(netd) 45 | library(sf) 46 | plot(netc["cycle_segregation"]) 47 | plot(netc["distance_to_road"]) 48 | # Interactive map: 49 | # tmap_mode("view") 50 | } 51 | -------------------------------------------------------------------------------- /inst/extdata/level-of-service-table.csv: -------------------------------------------------------------------------------- 1 | Motor Traffic Speed (85th percentile),Speed Limit (mph),Speed Limit (kph),Two-way traffic flow (pcu per day),Two-way traffic flow (pcu per hour),Mixed Traffic Street,Detached or Remote Cycle Track,Cycle Track at Carriageway Level,Stepped or Footway Level Cycle Track,Light Segregation,Cycle Lane 2 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,0 to 100,3,3,3,3,3,3 3 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,100 to 200,3,3,3,3,3,3 4 | 0 to 30 kph,<20 mph,<32 kph,2000 to 4000,200 to 400,2,3,3,3,3,3 5 | 0 to 30 kph,<20 mph,<32 kph,4000+,400+,1,3,3,3,3,2 6 | 0 to 30 kph,20 mph,32 kph,0 to 999,0 to 100,3,3,3,3,3,3 7 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,100 to 200,3,3,3,3,3,3 8 | 0 to 30 kph,20 mph,32 kph,2000 to 4000,200 to 400,2,3,3,3,3,3 9 | 0 to 30 kph,20 mph,32 kph,4000+,400+,1,3,3,3,3,2 10 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,0 to 100,3,3,3,3,3,3 11 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,100 to 200,2,3,3,3,3,2 12 | 30 kph to 50 kph,30 mph,48 kph,2000 to 4000,200 to 400,1,3,3,3,3,2 13 | 30 kph to 50 kph,30 mph,48 kph,4000+,400+,1,3,3,2,2,1 14 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,0 to 100,2,3,2,2,2,2 15 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,100 to 200,1,3,2,2,2,1 16 | 50 kph to 65 kph,40 mph,64 kph,2000 to 4000,200 to 400,0,3,2,2,1,1 17 | 50 kph to 65 kph,40 mph,64 kph,4000+,400+,0,3,2,2,1,1 18 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,0 to 100,1,3,2,2,2,1 19 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,100 to 200,0,3,1,1,1,1 20 | 65 kph to 80 kph,50 mph,80 kph,2000 to 4000,200 to 400,0,3,1,1,1,1 21 | 65 kph to 80 kph,50 mph,80 kph,4000+,400+,0,3,1,1,1,1 22 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,0 to 100,1,3,1,1,1,1 23 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,100 to 200,0,3,1,1,0,0 24 | 80 kph to 95 kph,60 mph,97 kph,2000 to 4000,200 to 400,0,3,1,1,0,0 25 | 80 kph to 95 kph,60 mph,97 kph,4000+,400+,0,3,1,1,0,0 26 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,0 to 100,0,3,1,1,0,0 27 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,100 to 200,0,3,1,1,0,0 28 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 4000,200 to 400,0,3,1,1,0,0 29 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,400+,0,3,1,1,0,0 30 | -------------------------------------------------------------------------------- /data-raw/test-sett.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test Sett sections 3 | format: gfm 4 | --- 5 | 6 | ```{r} 7 | #| message: false 8 | library(tidyverse) 9 | ``` 10 | 11 | The 2025-03-09 version of the package (commit `08ddafa`) excluded ways with 'Sett' surface, such as https://www.openstreetmap.org/way/4871777#map=19/55.959616/-3.194916&layers=N 12 | 13 | ```{r} 14 | #| label: old-version 15 | #| eval: false 16 | pak::pak("nptscot/osmactive@08ddafa") 17 | library(osmactive) 18 | osm = get_travel_network("Edinburgh") 19 | osm_sett_example = osm |> 20 | filter(osm_id %in% 4871777) 21 | cycle_net = get_cycling_network(osm) 22 | drive_net = get_driving_network(osm) 23 | cycle_net = distance_to_road(cycle_net, drive_net) 24 | cycle_net = classify_cycle_infrastructure(cycle_net) 25 | nrow(cycle_net) # 5832 26 | cycle_net_set_example = cycle_net |> 27 | filter(osm_id %in% 4871777) 28 | nrow(cycle_net_set_example) # 0 29 | cycle_net_set = cycle_net |> 30 | filter(surface %in% "sett") 31 | nrow(cycle_net_set) 32 | # 24 33 | mapview::mapview(cycle_net_set) 34 | # Unload package: 35 | detach("package:osmactive", unload = TRUE) 36 | ``` 37 | 38 | With dev version: 39 | 40 | ```{r} 41 | #| label: dev-version-sett 42 | # pak::pak("nptscot/osmactive@87-cyclable-ways-removed") 43 | # library(osmactive) 44 | devtools::load_all() 45 | osm = get_travel_network("Edinburgh") 46 | osm_sett_example = osm |> 47 | filter(osm_id %in% 4871777) 48 | cycle_net_set = get_cycling_network(osm_sett_example) 49 | nrow(cycle_net_set) # 1 50 | cycle_net = get_cycling_network(osm) 51 | nrow(cycle_net) # 65321 52 | table(cycle_net$highway, useNA = "always") 53 | drive_net = get_driving_network(osm) 54 | cycle_net = distance_to_road(cycle_net, drive_net) 55 | nrow(cycle_net) # 65321 56 | # Remove anything that is not cycle infrastructure: 57 | cycle_net = classify_cycle_infrastructure(cycle_net) 58 | nrow(cycle_net) # 7768 59 | table(cycle_net$highway, useNA = "always") 60 | # Plot the footways: 61 | cycle_net_footways = cycle_net |> 62 | filter(highway %in% "footway") 63 | # mapview::mapview(cycle_net_footways) 64 | plot_osm_tmap(cycle_net_footways) 65 | table(cycle_net_footways$footway, cycle_net_footways$bicycle, useNA = "always") 66 | # Sett example 67 | cycle_net_set_example = cycle_net |> 68 | filter(osm_id %in% 4871777) 69 | nrow(cycle_net_set_example) # 1 70 | ``` 71 | -------------------------------------------------------------------------------- /vignettes/classifying-cycle-infrastructure.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Cycle infrastructure classification systems" 3 | subtitle: "Comparing international 'best practice' guides and implementing them in open source software for reproducible research and data-driven active travel planning" 4 | 5 | # For .Rmd version: 6 | # output: rmarkdown::html_vignette 7 | # vignette: > 8 | # %\VignetteIndexEntry{Typologies of cycle infrastructure} 9 | # %\VignetteEngine{knitr::rmarkdown} 10 | # %\VignetteEncoding{UTF-8} 11 | 12 | # For .qmd version: 13 | format: html 14 | number-sections: true 15 | editor: 16 | markdown: 17 | wrap: sentence 18 | --- 19 | 20 | ```{r, include = FALSE} 21 | knitr::opts_chunk$set( 22 | collapse = TRUE, 23 | comment = "#>", 24 | echo = FALSE, 25 | message = FALSE, 26 | warning = FALSE 27 | ) 28 | ``` 29 | 30 | ```{r setup} 31 | library(osmactive) 32 | ``` 33 | 34 | # Abstract 35 | 36 | Transport networks are diverse and complex. 37 | This applies to all modes of transport, but especially to 'cycle network' which, uniquely, includes infrastructure segments that can be used both motorised and non-motorised modes. 38 | Even in places with relatively good provision of dedicated for cycling a substantial proportion of the cyclable network is also drivable, with 'fietstrase' in The Netherlands providing a classic example. 39 | In this paper we present a typology of cycle infrastructure classification systems and guidance on *what to build where*, based on official documents from TBC countries *and their implementation in open source software*. 40 | We find substantial differences between each classification system. 41 | Recent efforts to provide international guidance on how to talk about and classify cycling infrastructure has impacts on policies: measuring level of separation from motor traffic, for example, enables planners to focus on infrastructure that is safe for all. 42 | We conclude with tentative recommendations of classification systems for different use cases, with reference to our implementation in the `osmactive` package accompanies this paper. 43 | The work presented in this paper and our experience developing the package can provide a basis for open and community-driven classification systems that are modular, reproducible and extendable for different needs. 44 | The work provides a basis for more data-driven cycle traffic design guidance, that can co-evolve with changing policy, community and data-availability landscapes. 45 | 46 | # Introduction 47 | 48 | # Academic literature review 49 | 50 | # Official classification systems 51 | 52 | # Results 53 | 54 | # Discussion -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # Optional: Trigger on tags as well if you want to push tagged releases 8 | # tags: 9 | # - 'v*.*.*' 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | docker: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read # Needed for checkout 19 | packages: write # Needed to push to GHCR 20 | id-token: write # Needed for provenance attestations 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Log in to GitHub Container Registry 27 | uses: docker/login-action@v3 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Extract Docker metadata 34 | id: meta # Give this step an ID to reference its outputs 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: ghcr.io/${{ github.repository }} # Use github.repository for owner/repo 38 | tags: | 39 | # tag sha for specific commit reference 40 | type=sha 41 | # tag latest for default branch 42 | type=raw,value=latest,enable={{is_default_branch}} 43 | # Optional: tag based on git tag if you push git tags (e.g., v1.0.0) 44 | # type=ref,event=tag 45 | 46 | - name: Set up QEMU 47 | uses: docker/setup-qemu-action@v3 48 | 49 | - name: Set up Docker Buildx 50 | id: buildx # Give this step an ID if you need to reference the builder instance 51 | uses: docker/setup-buildx-action@v3 52 | 53 | - name: Build and push Docker image 54 | id: build-and-push 55 | uses: docker/build-push-action@v6 56 | with: 57 | context: . 58 | file: Dockerfile 59 | # Only push on pushes to the main branch (not PRs) 60 | # Or if you uncommented the tag trigger above, it would also push on tag events 61 | push: ${{ github.event_name == 'push' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | cache-from: type=gha # Enable cache read from GitHub Actions cache 65 | cache-to: type=gha,mode=max # Enable cache write to GitHub Actions cache (mode=max is recommended) 66 | provenance: true # Generate SLSA provenance attestation 67 | 68 | # Optional: Add a step here to output the image digest or other info if needed 69 | # - name: Print image digest 70 | # if: steps.build-and-push.outputs.digest 71 | # run: echo "Image digest: ${{ steps.build-and-push.outputs.digest }}" -------------------------------------------------------------------------------- /data-raw/osm_edinburgh.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `osm_edinburgh` dataset goes here 2 | 3 | devtools::load_all() 4 | # Or 5 | # remotes::install_github("nptscot/osmactive") 6 | # library(osmactive) 7 | # devtools::load_all() # Load local package code 8 | library(dplyr) 9 | library(tmap) 10 | library(sf) 11 | sf::sf_use_s2(TRUE) 12 | tmap_mode("plot") 13 | 14 | edinburgh_coords = stplanr::geo_code("Edinburgh, UK") 15 | edinburgh_sf = sf::st_sf( 16 | geometry = sf::st_sfc(sf::st_point(edinburgh_coords)), 17 | crs = 4326 18 | ) 19 | edinburgh_3km = edinburgh_sf |> 20 | sf::st_buffer(3000) 21 | 22 | osm = get_travel_network("Edinburgh", boundary = edinburgh_3km, boundary_type = "clipsrc") 23 | # Check names 24 | "footway" %in% names(osm_edinburgh) 25 | osm = get_travel_network( 26 | "edinburgh", 27 | boundary = edinburgh_3km, 28 | boundary_type = "clipsrc", 29 | force_download = TRUE 30 | ) 31 | names(osm) 32 | "footway" %in% names(osm) 33 | unique(osm$name) 34 | 35 | # Rename name2 to name 36 | if ("name2" %in% names(osm)) { 37 | osm$name = osm$name2 38 | osm$name2 = NULL 39 | } 40 | # rename highway2 to highway 41 | if ("highway2" %in% names(osm)) { 42 | osm$highway = osm$highway2 43 | osm$highway2 = NULL 44 | } 45 | 46 | mapview::mapview(osm) 47 | osm_york_way = osm |> 48 | filter(stringr::str_detect(name, "York Place")) 49 | 50 | mapview::mapview(osm_york_way) 51 | osm_york_way_buffer = osm_york_way |> 52 | sf::st_transform(27700) |> 53 | sf::st_buffer(100) |> 54 | sf::st_transform(4326) |> 55 | sf::st_union() 56 | 57 | plot(osm_york_way_buffer) 58 | 59 | osm = osm[osm_york_way_buffer, ] 60 | plot(osm) 61 | 62 | # # Keep only most relevant columns 63 | osm = osm |> 64 | select( 65 | osm_id, 66 | name, 67 | highway, 68 | matches("cycleway"), 69 | bicycle, 70 | lanes, 71 | foot, 72 | footway, 73 | path, 74 | sidewalk, 75 | segregated, 76 | maxspeed, 77 | width, 78 | est_width, 79 | lit, 80 | oneway, 81 | cycleway_surface, 82 | surface, 83 | smoothness, 84 | other_tags 85 | ) 86 | names(osm) 87 | table(osm$traffic_calming) 88 | 89 | cycle_network = get_cycling_network(osm) 90 | cycle_network_old = cycle_network 91 | driving_network = get_driving_network(osm) 92 | edinburgh_cycle_with_distance = distance_to_road(cycle_network, driving_network) 93 | cycleways_classified = classify_cycle_infrastructure( 94 | edinburgh_cycle_with_distance 95 | ) 96 | # # cycleways_classified_old = cycleways_classified 97 | # waldo::compare(cycleways_classified, cycleways_classified_old) 98 | 99 | plot_osm_tmap(cycleways_classified) 100 | 101 | 102 | table(cycleways_classified$cycle_segregation) 103 | m = plot_osm_tmap(cycleways_classified) 104 | m 105 | 106 | # Save the data 107 | osm_edinburgh = osm 108 | usethis::use_data(osm_edinburgh, overwrite = TRUE) 109 | -------------------------------------------------------------------------------- /man/get_parallel_values.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/get_parallel_values.R 3 | \name{get_parallel_values} 4 | \alias{get_parallel_values} 5 | \title{Get values from parallel features} 6 | \usage{ 7 | get_parallel_values( 8 | target_net, 9 | source_net, 10 | column = "maxspeed", 11 | buffer_dist = 10, 12 | angle_threshold = 20, 13 | value_pattern = " mph", 14 | value_replacement = "", 15 | add_suffix = " mph" 16 | ) 17 | } 18 | \arguments{ 19 | \item{target_net}{An sf object representing the network where values need imputation (e.g., cycle network). Must contain a unique \code{osm_id} column.} 20 | 21 | \item{source_net}{An sf object representing the network to source values from (e.g., drive network). Must contain a unique \code{osm_id} column.} 22 | 23 | \item{column}{The name of the column (as a string) in both networks to check for NAs 24 | in \code{target_net} and source values from \code{source_net}. Defaults to "maxspeed". 25 | This column should ideally be numeric or coercible to numeric after removing units.} 26 | 27 | \item{buffer_dist}{The buffer distance (in the units of the CRS) around \code{target_net} 28 | features to search for nearby \code{source_net} features. Defaults to 10.} 29 | 30 | \item{angle_threshold}{The maximum allowed absolute difference in bearing (degrees) 31 | between features in \code{target_net} and \code{source_net} for them to be considered parallel. 32 | Defaults to 20.} 33 | 34 | \item{value_pattern}{Optional regex pattern to remove from the \code{column} values in \code{source_net} 35 | before converting to numeric. Defaults to " mph". Set to NULL to skip removal.} 36 | 37 | \item{value_replacement}{Replacement string for \code{value_pattern}. Defaults to "".} 38 | 39 | \item{add_suffix}{Optional suffix to add back to the imputed numeric value before 40 | assigning it back to the \code{column}. Defaults to " mph". Set to NULL to skip adding suffix.} 41 | } 42 | \value{ 43 | An sf object, the \code{target_net} with the specified \code{column} potentially 44 | updated with imputed values from \code{source_net}. 45 | } 46 | \description{ 47 | This function finds features in a 'target' network (\code{target_net}) that are parallel 48 | and close to features in a 'source' network (\code{source_net}) and imputes missing 49 | values in a specified column of the \code{target_net} based on the median value 50 | from the nearby parallel features in the \code{source_net}. 51 | } 52 | \details{ 53 | It's a generalisation of the \code{find_nearby_speeds} function previously used 54 | to impute missing \code{maxspeed} values in a cycle network based on nearby roads. 55 | } 56 | \examples{ 57 | # Assuming cycle_net_f and drive_net_f are loaded sf objects from the package 58 | 59 | # Impute missing 'maxspeed' in cycle_net_f using drive_net_f 60 | cycle_net_updated_speed = get_parallel_values( 61 | target_net = cycle_net_f, 62 | source_net = drive_net_f, 63 | column = "maxspeed", 64 | buffer_dist = 10, 65 | angle_threshold = 20 66 | ) 67 | 68 | # Example imputing a hypothetical 'width' column (assuming it exists and needs imputation) 69 | # cycle_net_f$width = as.character(cycle_net_f$width) # Ensure character if adding suffix 70 | # cycle_net_f$width[sample(1:nrow(cycle_net_f), 5)] = NA # Add some NAs for example 71 | # cycle_net_updated_width = get_parallel_values( 72 | # target_net = cycle_net_f, 73 | # source_net = drive_net_f, # Using drive_net just for example structure 74 | # column = "width", 75 | # buffer_dist = 5, 76 | # angle_threshold = 15, 77 | # value_pattern = NULL, # Assuming width is already numeric or has no suffix 78 | # add_suffix = NULL 79 | # ) 80 | 81 | print(paste("NA maxspeed before:", sum(is.na(cycle_net_f$maxspeed)))) 82 | print(paste("NA maxspeed after:", sum(is.na(cycle_net_updated_speed$maxspeed)))) 83 | } 84 | -------------------------------------------------------------------------------- /data-raw/calculateAverageWidths.R: -------------------------------------------------------------------------------- 1 | get_pavement_widths = function( 2 | road_geojson_path, 3 | roadside_geojson_path, 4 | crs = "EPSG:27700", 5 | buffer_dist = 15, 6 | segment_length = 20 7 | ) { 8 | # Read and transform GeoJSON files using sf 9 | roads_sf = sf::st_read(road_geojson_path) |> 10 | sf::st_transform(crs) 11 | 12 | roads_sf = stplanr::line_cast(roads_sf) 13 | 14 | if (segment_length > 0) { 15 | # Check if the rsgeo package or specific functionality is installed 16 | rsgeo_installed = requireNamespace("rsgeo", quietly = TRUE) 17 | 18 | if (rsgeo_installed) { 19 | # rsgeo package is installed, use rsgeo-specific functionality 20 | print("Using rsgeo package for line segment splitting") 21 | segment_lengths = as.numeric(sf::st_length(roads_sf)) 22 | n_segments = n_segments(segment_lengths, segment_length) 23 | roads_split = line_segment_rsgeo(roads_sf, n_segments = n_segments) 24 | } else { 25 | # rsgeo package is not installed, fallback to standard functionality 26 | roads_split = stplanr::line_segment( 27 | roads_sf, 28 | segment_length = segment_length 29 | ) 30 | } 31 | # Convert the split or original roads to GEOS geometry 32 | roads_geos = geos::as_geos_geometry(roads_split) 33 | } else { 34 | # Convert the original roads to GEOS geometry without splitting 35 | roads_geos = geos::as_geos_geometry(roads_sf) 36 | } 37 | 38 | roadside_polygons_sf = sf::st_read(roadside_geojson_path) |> 39 | sf::st_transform(crs) 40 | 41 | # Convert sf objects to geos geometries for geos operations 42 | roads_geos = geos::as_geos_geometry(roads_sf) 43 | roadside_polygons_geos = geos::as_geos_geometry(sf::st_geometry( 44 | roadside_polygons_sf 45 | )) 46 | 47 | # Create buffered roads using geos 48 | roads_buffered = geos::geos_buffer( 49 | roads_geos, 50 | dist = buffer_dist, 51 | params = geos::geos_buffer_params(end_cap_style = "flat") 52 | ) 53 | roads_buffered_right = geos::geos_buffer( 54 | roads_geos, 55 | dist = buffer_dist, 56 | params = geos::geos_buffer_params( 57 | end_cap_style = "flat", 58 | single_sided = TRUE 59 | ) 60 | ) 61 | roads_buffered_left = geos::geos_difference( 62 | roads_buffered, 63 | roads_buffered_right 64 | ) 65 | 66 | # Convert geos geometries back to sf for attaching average widths and further processing 67 | average_widths_total = calculate_widths( 68 | roads_geos, 69 | roadside_polygons_geos, 70 | roads_buffered 71 | ) 72 | roads_sf$average_width = average_widths_total 73 | 74 | # Calculate average widths for right and left buffers, then attach to roads_sf as done above 75 | average_widths_right = calculate_widths( 76 | roads_geos, 77 | roadside_polygons_geos, 78 | roads_buffered_right 79 | ) 80 | roads_sf$average_widths_right = average_widths_right 81 | 82 | average_widths_left = calculate_widths( 83 | roads_geos, 84 | roadside_polygons_geos, 85 | roads_buffered_left 86 | ) 87 | roads_sf$average_widths_left = average_widths_left 88 | 89 | return(roads_sf) 90 | } 91 | 92 | calculate_widths = function( 93 | roads_geos, 94 | roadside_polygons_geos, 95 | roads_buffered 96 | ) { 97 | average_widths = numeric(length = length(roads_geos)) 98 | 99 | for (i in seq_along(roads_geos)) { 100 | intersections = geos::geos_intersects( 101 | roads_buffered[i], 102 | roadside_polygons_geos 103 | ) 104 | relevant_indices = which(intersections) 105 | 106 | if (length(relevant_indices) > 0) { 107 | relevant_polygons = roadside_polygons_geos[relevant_indices] 108 | pavements_intersection_within = geos::geos_intersection( 109 | relevant_polygons, 110 | roads_buffered[i] 111 | ) 112 | intersecting_area = sum(geos::geos_area(pavements_intersection_within)) 113 | road_length = geos::geos_length(roads_geos[i]) 114 | 115 | if (road_length > 0) { 116 | average_widths[i] = round(intersecting_area / road_length, 2) 117 | } else { 118 | average_widths[i] = NA 119 | } 120 | } else { 121 | average_widths[i] = NA 122 | } 123 | } 124 | 125 | return(average_widths) 126 | } 127 | 128 | n_segments = function(line_length, max_segment_length) { 129 | pmax(ceiling(line_length / max_segment_length), 1) 130 | } 131 | 132 | 133 | line_segment_rsgeo = function(l, n_segments) { 134 | crs = sf::st_crs(l) 135 | # Test to see if the CRS is latlon or not and provide warning if so 136 | if (sf::st_is_longlat(l)) { 137 | warning( 138 | "The CRS of the input object is latlon.\n", 139 | "This may cause problems with the rsgeo implementation of line_segment()." 140 | ) 141 | } 142 | 143 | # extract geometry and convert to rsgeo 144 | geo = rsgeo::as_rsgeo(sf::st_geometry(l)) 145 | 146 | # segmentize the line strings 147 | res_rsgeo = rsgeo::line_segmentize(geo, n_segments) 148 | 149 | # make them into sfc_LINESTRING 150 | res = sf::st_cast(sf::st_as_sfc(res_rsgeo), "LINESTRING") 151 | 152 | # give them them CRS 153 | res = sf::st_set_crs(res, crs) 154 | 155 | # calculate the number of original geometries 156 | n_lines = length(geo) 157 | # create index ids to grab rows from 158 | ids = rep.int(seq_len(n_lines), n_segments) 159 | 160 | # index the original sf object 161 | res_tbl = sf::st_drop_geometry(l)[ids, , drop = FALSE] 162 | 163 | # assign the geometry column 164 | nrow(res_tbl) 165 | 166 | res_tbl[[attr(l, "sf_column")]] = res 167 | 168 | # convert to sf and return 169 | res_sf = sf::st_as_sf(res_tbl) 170 | res_sf 171 | } 172 | -------------------------------------------------------------------------------- /data-raw/tests.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tests of known OSM ways" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Tests of known OSM ways} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup} 18 | library(osmactive) 19 | library(dplyr) 20 | # devtools::load_all() # for development 21 | ``` 22 | 23 | The following test checks to see if a known OSM way in Leeds, a footpath in Leeds in a path on which cycling is allowed and is common, is correctly identified, as Shared Use. 24 | 25 | ```{r} 26 | osm = get_travel_network("Leeds") 27 | potternewton_path = osm |> 28 | filter(osm_id == 306849934) 29 | cycle_net = get_cycling_network(potternewton_path) 30 | nrow(cycle_net) 31 | drive_net = get_driving_network(osm) 32 | cycle_net = distance_to_road(cycle_net, drive_net) 33 | cycle_net_classified = classify_cycle_infrastructure(cycle_net) 34 | cycle_net_classified |> 35 | pull(cycle_segregation) 36 | ``` 37 | 38 | Let's check out 'highway=path' tagged segments in the network in Leeds: 39 | 40 | ```{r} 41 | cycle_net = get_cycling_network(osm) 42 | cycle_net = distance_to_road(cycle_net, drive_net) 43 | cycle_net = classify_cycle_infrastructure(cycle_net) 44 | tmap::tmap_mode("view") 45 | cycle_net_path = cycle_net |> 46 | filter(highway == "path") 47 | m = plot_osm_tmap(cycle_net_path) 48 | m 49 | # tmap::tmap_save(m, "path.html") 50 | # browseURL("path.html") 51 | ``` 52 | 53 | Leith Walk, should be Segregated Track (narrow). 54 | It is on part of it, with the following tags for way [22587411](https://www.openstreetmap.org/way/22587411). 55 | 56 | ```{r} 57 | osm = get_travel_network("Edinburgh", force_download = TRUE) 58 | nrow(osm) 59 | sum(osm$cycleway_left == "separate", na.rm = TRUE) 60 | # Full cycle network: 61 | cycle_net = get_cycling_network(osm) 62 | nrow(cycle_net) 63 | table(cycle_net$cycleway_right) 64 | drive_net = get_driving_network(osm) 65 | cycle_net = distance_to_road(cycle_net, drive_net) 66 | cycle_net = classify_cycle_infrastructure(cycle_net) 67 | m = plot_osm_tmap(cycle_net) 68 | tmap::tmap_save(m, "leith_walk_full.html") 69 | browseURL("leith_walk_full.html") 70 | ``` 71 | 72 | ```{r} 73 | leith_walk_narrow = cycle_net |> 74 | filter(osm_id == 1209807764) 75 | get_cycling_network(leith_walk_narrow) 76 | leith_walk_narrow_minimal = leith_walk_narrow |> 77 | select_if(~ !all(is.na(.))) |> 78 | sf::st_drop_geometry() 79 | leith_walk_narrow_minimal 80 | ``` 81 | 82 | ```{r, echo=FALSE} 83 | # osm_id highway oneway cycleway_chars lit 84 | # 1 1209807764 cycleway yes NA|NA|NA|NA|NA|NA|NA|NA|NA|NA|NA|NA|NA|NA yes 85 | # width surface smoothness z_order distance_to_road detailed_segregation 86 | # 1 1 asphalt good 0 9.1 Level track 87 | # cycle_pedestrian_separation width_clean cycle_segregation 88 | # 1 Unknown 1 Segregated Track (narrow) 89 | ``` 90 | 91 | Another part of the same corridor, way 1214008544, is classified as Shared Footway: 92 | 93 | ```{r} 94 | leith_walk_footway = cycle_net |> 95 | filter(osm_id == 1214008544) 96 | leith_walk_footway_minimal = leith_walk_footway |> 97 | select_if(~ !all(is.na(.))) |> 98 | sf::st_drop_geometry() 99 | leith_walk_footway_minimal 100 | ``` 101 | 102 | ```{r, echo=FALSE} 103 | # osm_id name highway maxspeed oneway cycleway_left cycleway_right lit 104 | # 1 578114866 Leith Walk primary 20 mph yes separate no yes 105 | # surface z_order 106 | # 1 asphalt 7 107 | # other_tags 108 | # 1 "name:left"=>"Crighton Place","ref"=>"A900","sidewalk:left"=>"separate","sidewalk:right"=>"no" 109 | ``` 110 | 111 | Let's compare them: 112 | 113 | ```{r} 114 | waldo::compare(leith_walk_narrow, leith_walk_footway) 115 | ``` 116 | 117 | Highway=path should be 118 | 119 | ```{r} 120 | leith_walk = osm |> 121 | filter(name == "Leith Walk") 122 | cycle_net = get_cycling_network(leith_walk) 123 | drive_net = get_driving_network(osm) 124 | cycle_net = distance_to_road(cycle_net, drive_net) 125 | cycle_net = classify_cycle_infrastructure(cycle_net) 126 | cycle_net |> 127 | pull(cycle_segregation) 128 | m = plot_osm_tmap(cycle_net) 129 | m 130 | ``` 131 | 132 | ```{r} 133 | tmap::tmap_save(m, "leith_walk.html") 134 | browseURL("leith_walk.html") 135 | ``` 136 | 137 | # 757849580 138 | 139 | ```{r} 140 | cloch_lighthouse = zonebuilder::zb_zone("Cloch Lighthouse", n_circles = 3) 141 | cloch_lighthouse_union = sf::st_union(cloch_lighthouse) 142 | osm = get_travel_network("Scotland", boundary = cloch_lighthouse_union, boundary_type = "clipsrc") 143 | osm_cloch_road = osm |> 144 | filter(osm_id == 757849580) 145 | osm_cloch_road_1km = osm_cloch_road |> 146 | sf::st_buffer(1000) 147 | osm_cloch_road_1km = osm |> 148 | sf::st_intersection(osm_cloch_road_1km) 149 | plot(osm_cloch_road_1km$geometry) 150 | mapview::mapview(osm_cloch_road_1km) 151 | cycle_net = get_cycling_network(osm_cloch_road_1km) 152 | drive_net = get_driving_network(osm_cloch_road_1km) 153 | cycle_net = distance_to_road(cycle_net, drive_net) 154 | cycle_net = classify_cycle_infrastructure(cycle_net) 155 | # # # Make changes to osmactive code then reload with 156 | devtools::load_all() 157 | # osm_classified_coch = osm_classified |> 158 | # filter(osm_id == 757849580) 159 | # # osm_classified_coch$cycle_pedestrian_separation 160 | # # # [1] Shared Footway (not segregated) 161 | # # # 3 Levels: Shared Footway (segregated) < ... < Unknown 162 | # # osm_classified_coch$cycle_segregation 163 | # osm_classified = osm_classified_coch 164 | cycle_net_cloch_road = cycle_net |> 165 | filter(osm_id == 757849580) 166 | cycle_net_cloch_road$cycle_segregation # Should be shared footway 167 | m = plot_osm_tmap(cycle_net) 168 | tmap::tmap_mode("view") 169 | m 170 | tmap::tmap_save(m, "cloch_road.html") 171 | # Save the cycle_net 172 | sf::write_sf(cycle_net, "cycle_net_cloch_road.geojson") 173 | system("gh release list") 174 | system("gh release upload v0.1 cycle_net_cloch_road.geojson cloch_road.html --clobber") 175 | # See result online at https://github.com/nptscot/osmactive/releases/tag/v0.1/cycle_net_cloch_road.geojson 176 | ``` -------------------------------------------------------------------------------- /R/test-code/portugal.R: -------------------------------------------------------------------------------- 1 | # classify osm tags in 4 types: 2 | # 1. Cycle track or lane: Light or separated tracks exclusive for cycling 3 | # 2. Advisory lane: Marked (e.g. sharrow or advisory) cycle lanes, but shared with motor vehicles 4 | # 3. Protected Active: Shared with pedestrians but not with motor vehicles 5 | # 4. Mixed traffic: Shared with motor vehicles 6 | 7 | 8 | 9 | library(dplyr) 10 | library(sf) 11 | library(osmactive) 12 | library(tmap) 13 | 14 | et_active = function() { 15 | c( 16 | "maxspeed", 17 | "oneway", 18 | "bicycle", 19 | "cycleway", 20 | "cycleway:left", 21 | "cycleway:right", 22 | "cycleway:both", 23 | "lanes", 24 | "lanes:both_ways", 25 | "lanes:forward", 26 | "lanes:backward", 27 | "lanes:bus", 28 | "lanes:bus:conditional", 29 | "oneway", 30 | "width", # useful to ensure width of cycleways is at least 1.5m 31 | "segregated", # classifies whether cycles and pedestrians are segregated on shared paths 32 | "sidewalk", # useful to ensure width of cycleways is at least 1.5m 33 | "footway", 34 | # "highway", # included by default 35 | # "name", # included by default 36 | "service", 37 | "surface", 38 | "tracktype", 39 | "surface", 40 | "smoothness", 41 | "access", 42 | "foot" # add this to filter the protected to active modes 43 | ) 44 | } 45 | 46 | get_travel_network = function( 47 | place, 48 | extra_tags = et_active(), 49 | columns_to_remove = c("waterway", "aerialway", "barrier", "manmade"), 50 | ... 51 | ) { 52 | osm_highways = osmextract::oe_get( 53 | place = place, 54 | extra_tags = extra_tags, 55 | ... 56 | ) 57 | osm_highways |> 58 | dplyr::filter(!is.na(highway)) |> 59 | # Remove all service tags based on https://wiki.openstreetmap.org/wiki/Key:service 60 | dplyr::filter(is.na(service)) |> 61 | dplyr::select(-dplyr::matches(columns_to_remove)) 62 | } 63 | 64 | 65 | u = "https://ushift.tecnico.ulisboa.pt/content/data/lisbon_limit.geojson" 66 | f = basename(u) 67 | if (!file.exists(f)) download.file(u, f) 68 | lisbon = sf::read_sf(f) 69 | lisbon = lisbon |> 70 | sf::st_cast("POLYGON") 71 | osm = get_travel_network("Portugal", boundary = lisbon, boundary_type = "clipsrc", force_vectortranslate = TRUE) 72 | cycle_net = get_cycling_network(osm) 73 | drive_net = get_driving_network_major(osm) 74 | cycle_net = distance_to_road(cycle_net, drive_net) 75 | 76 | 77 | classify_cycle_infrastructure_portugal = function(osm) { 78 | 79 | CYCLE_TRACK = "Cycle track or lane" # PT: Ciclovia segregada 80 | ADVISORY = "Advisory lane" # PT: Via partilhada com veículos motorizados (ex. zonas 30) 81 | PROTECTED_ACTIVE = "Protected Active" # PT: Via partilhada com peões 82 | MIXED_TRAFFIC = "Mixed traffic" # PT: Via banalizada 83 | 84 | osm |> 85 | # 1. Preliminary classification 86 | # If highway == cycleway|pedestrian|path, detailed_segregation can be defined in most cases... 87 | dplyr::mutate(detailed_segregation = dplyr::case_when( 88 | 89 | # Dedicated cycle lanes 90 | highway == "cycleway" ~ CYCLE_TRACK, 91 | highway == "path" & bicycle == "designated" ~ CYCLE_TRACK, 92 | 93 | # Sidewalks shared with bicycles 94 | highway == "footway" & bicycle == "yes" ~ PROTECTED_ACTIVE, 95 | highway == "pedestrian" & bicycle == "designated" ~ PROTECTED_ACTIVE, 96 | 97 | # When `segregated` tag set, it is not shared with traffic (https://wiki.openstreetmap.org/wiki/Key:segregated) 98 | segregated == "yes" ~ CYCLE_TRACK, 99 | segregated == "no" ~ CYCLE_TRACK, 100 | 101 | # If none verify, lets consider them lanes shared with cars 102 | TRUE ~ MIXED_TRAFFIC 103 | )) |> 104 | 105 | # 2. Let's analyse the `cycleway` tags... (https://wiki.openstreetmap.org/wiki/Tag:highway%3Dcycleway) 106 | tidyr::unite("cycleway_chars", dplyr::starts_with("cycleway"), sep = "|", remove = FALSE) |> 107 | dplyr::mutate(detailed_segregation2 = dplyr::case_when( 108 | stringr::str_detect(cycleway_chars, "separate") & detailed_segregation == "Mixed traffic" ~ CYCLE_TRACK, 109 | stringr::str_detect(cycleway_chars, "buffered_lane") & detailed_segregation == "Mixed traffic" ~ CYCLE_TRACK, 110 | stringr::str_detect(cycleway_chars, "segregated") & detailed_segregation == "Mixed traffic" ~ CYCLE_TRACK, 111 | TRUE ~ detailed_segregation 112 | )) |> 113 | 114 | dplyr::mutate(detailed_segregation2 = dplyr::case_when( 115 | stringr::str_detect(cycleway_chars, "shared_lane") ~ ADVISORY, 116 | stringr::str_detect(cycleway_chars, "lane") & detailed_segregation == "Mixed traffic" ~ CYCLE_TRACK, 117 | stringr::str_detect(cycleway_chars, "track") & detailed_segregation == "Mixed traffic" ~ CYCLE_TRACK, 118 | TRUE ~ detailed_segregation 119 | )) |> 120 | 121 | # 3. Let's clarify that previously classified cycle lanes are not shared with pedestrians 122 | dplyr::mutate(detailed_segregation4 = dplyr::case_when( 123 | detailed_segregation2 == CYCLE_TRACK & highway %in% c("cycleway", "path") & foot %in% c("designated", "permissive", "private", "use_sidepath", "yes") & (is.na(sidewalk) | sidewalk=="no") & (is.na(segregated) | segregated=="no") ~ PROTECTED_ACTIVE, 124 | detailed_segregation2 == CYCLE_TRACK & highway == "footway" & bicycle %in% c("yes" , "designated") ~ PROTECTED_ACTIVE, 125 | detailed_segregation2 == CYCLE_TRACK & highway == "pedestrian" & bicycle %in% c("yes" , "designated") ~ PROTECTED_ACTIVE, 126 | TRUE ~ detailed_segregation2 127 | )) |> 128 | 129 | dplyr::mutate(cycle_segregation = factor( 130 | detailed_segregation4, 131 | levels = c(CYCLE_TRACK, ADVISORY, PROTECTED_ACTIVE, MIXED_TRAFFIC), 132 | ordered = TRUE 133 | )) 134 | } 135 | 136 | 137 | # "Cycle track or lane": Light or separated tracks exclusive for cycling 138 | # "Mixed traffic": Marked (e.g. sharrow or advisory) cycle lanes 139 | # "Proctected Active":Shared with pedestrians 140 | 141 | cycle_net_pt = classify_cycle_infrastructure_portugal(cycle_net) 142 | 143 | # table(stringr::str_detect(cycle_net_pt$cycleway_chars, "lane") & cycle_net_pt$detailed_segregation == "Mixed traffic") 144 | 145 | 146 | table(cycle_net_pt$detailed_segregation) 147 | table(cycle_net_pt$detailed_segregation2) 148 | table(cycle_net_pt$detailed_segregation4) 149 | table(cycle_net_pt$cycle_segregation) 150 | 151 | m = plot_osm_tmap(cycle_net_pt) 152 | m 153 | 154 | mapview::mapview(cycle_net_pt |> filter(cycle_segregation != "Mixed traffic"), zcol="cycle_segregation") 155 | 156 | # there are still a lot of them what have the osm tag foot="yes" and shouldn't have it. 157 | # Edit in OSM. Examples 158 | # https://www.openstreetmap.org/way/976381232 159 | # https://www.openstreetmap.org/way/686372908 160 | # https://www.openstreetmap.org/way/498545079 161 | 162 | 163 | -------------------------------------------------------------------------------- /vignettes/references.bib: -------------------------------------------------------------------------------- 1 | 2 | @article{lovelace2020, 3 | title = {Methods to Prioritise Pop-up Active Transport Infrastructure}, 4 | author = {Lovelace, Robin and Talbot, Joseph and Morgan, Malcolm and Lucas-Smith, Martin}, 5 | year = {2020}, 6 | month = {07}, 7 | date = {2020-07-08}, 8 | journal = {Transport Findings}, 9 | pages = {13421}, 10 | doi = {10.32866/001c.13421}, 11 | url = {https://transportfindings.org/article/13421-methods-to-prioritise-pop-up-active-transport-infrastructure} 12 | } 13 | 14 | @article{ito2022, 15 | title = {Where to invest in cycle parking: A portfolio management approach to spatial transport planning}, 16 | author = {Ito, Yuhei and Morgan, Malcolm and Lovelace, Robin}, 17 | year = {2022}, 18 | month = {11}, 19 | date = {2022-11-10}, 20 | journal = {Environment and Planning B: Urban Analytics and City Science}, 21 | pages = {23998083221138575}, 22 | doi = {10.1177/23998083221138575}, 23 | url = {https://doi.org/10.1177/23998083221138575} 24 | } 25 | 26 | @article{lovelace2017, 27 | title = {The Propensity to Cycle Tool: An open source online system for sustainable transport planning}, 28 | author = {Lovelace, Robin and Goodman, Anna and Aldred, Rachel and Berkoff, Nikolai and Abbas, Ali and Woodcock, James}, 29 | year = {2017}, 30 | month = {01}, 31 | date = {2017-01-01}, 32 | journal = {Journal of Transport and Land Use}, 33 | volume = {10}, 34 | number = {1}, 35 | doi = {10.5198/jtlu.2016.862} 36 | } 37 | 38 | @article{goodman2019, 39 | title = {Scenarios of cycling to school in England, and associated health and carbon impacts: Application of the {\textquoteleft}Propensity to Cycle Tool{\textquoteright}}, 40 | author = {Goodman, Anna and Rojas, Ilan Fridman and Woodcock, James and Aldred, Rachel and Berkoff, Nikolai and Morgan, Malcolm and Abbas, Ali and Lovelace, Robin}, 41 | year = {2019}, 42 | month = {03}, 43 | date = {2019-03-01}, 44 | journal = {Journal of Transport and Health}, 45 | pages = {263--278}, 46 | volume = {12}, 47 | doi = {10.1016/j.jth.2019.01.008}, 48 | url = {http://www.sciencedirect.com/science/article/pii/S2214140518301257} 49 | } 50 | 51 | @article{lovelace2024, 52 | title = {Cycle route uptake and scenario estimation (CRUSE): an approach for developing strategic cycle network planning tools}, 53 | author = {Lovelace, Robin and Talbot, Joey and Vidal-Tortosa, Eugeni and Mahfouz, Hussein and Brick, Elaine and Wright, Peter and {O{\textquoteright}Toole}, Gary and Brennan, Dan and Meade, Suzanne}, 54 | year = {2024}, 55 | month = {09}, 56 | date = {2024-09-30}, 57 | journal = {European Transport Research Review}, 58 | pages = {55}, 59 | volume = {16}, 60 | number = {1}, 61 | doi = {10.1186/s12544-024-00668-8}, 62 | url = {https://doi.org/10.1186/s12544-024-00668-8} 63 | } 64 | 65 | @article{félix2025, 66 | title = {Reproducible methods for modeling combined public transport and cycling trips and associated benefits: Evidence from the biclaR tool}, 67 | author = {{Félix}, Rosa and Moura, Filipe and Lovelace, Robin}, 68 | year = {2025}, 69 | month = {04}, 70 | date = {2025-04-01}, 71 | journal = {Computers, Environment and Urban Systems}, 72 | pages = {102230}, 73 | volume = {117}, 74 | doi = {10.1016/j.compenvurbsys.2024.102230}, 75 | url = {https://www.sciencedirect.com/science/article/pii/S0198971524001595}, 76 | } 77 | 78 | @article{vybornova2024, 79 | title = {BikeNodePlanner: a data-driven decision support tool for bicycle node network planning}, 80 | author = {Vybornova, Anastassia and {Vierø}, Ane Rahbek and Hansen, Kirsten Krogh and Szell, Michael}, 81 | year = {2024}, 82 | month = {12}, 83 | date = {2024-12-28}, 84 | doi = {10.48550/arXiv.2412.20270}, 85 | url = {http://arxiv.org/abs/2412.20270}, 86 | note = {arXiv:2412.20270 [cs]} 87 | } 88 | 89 | @article{vierø2024, 90 | title = {BikeDNA: A tool for bicycle infrastructure data and network assessment}, 91 | author = {{Vierø}, Ane Rahbek and Vybornova, Anastassia and Szell, Michael}, 92 | year = {2024}, 93 | month = {02}, 94 | date = {2024-02-01}, 95 | journal = {Environment and Planning B: Urban Analytics and City Science}, 96 | pages = {512--528}, 97 | volume = {51}, 98 | number = {2}, 99 | doi = {10.1177/23998083231184471}, 100 | url = {https://doi.org/10.1177/23998083231184471}, 101 | } 102 | 103 | @article{lovelace2020a, 104 | title = {Open access transport models: A leverage point in sustainable transport planning}, 105 | author = {Lovelace, Robin and Parkin, John and Cohen, Tom}, 106 | year = {2020}, 107 | month = {10}, 108 | date = {2020-10-01}, 109 | journal = {Transport Policy}, 110 | pages = {47--54}, 111 | volume = {97}, 112 | doi = {10.1016/j.tranpol.2020.06.015}, 113 | url = {http://www.sciencedirect.com/science/article/pii/S0967070X19302781}, 114 | } 115 | 116 | @phdthesis{gonçalves2023, 117 | title = {Map Services Management}, 118 | author = {{Gonçalves}, {André Pinhal}}, 119 | year = {2023}, 120 | month = {11}, 121 | date = {2023-11-08}, 122 | url = {https://recipp.ipp.pt/handle/10400.22/24086}, 123 | note = {Accepted: 2023-12-06T09:44:39Z}, 124 | langid = {eng} 125 | } 126 | 127 | @inproceedings{lovelace_reproducible_2024, 128 | title = {Reproducible Methods for Network Simplification for Data Visualisation and Transport Planning}, 129 | booktitle = {32nd {{GISRUK Conference}} 2024}, 130 | author = {Lovelace, Robin and Wang, Zhao and Deakin, Will and Parry, Josiah}, 131 | year = {2024}, 132 | month = apr, 133 | publisher = {Zenodo}, 134 | address = {Leeds}, 135 | doi = {10.5281/zenodo.11077553}, 136 | urldate = {2024-05-02}, 137 | abstract = {Route network datasets, crucial to transport models, have grown complex, leading to visualization issues and potential misinterpretations. We address this by presenting two methods for simplifying these datasets: image skeletonization and Voronoi diagram-centreline identification. We have developed two packages, the `parenx' Python package (available on pip) and the `rnetmatch' R package (available on GitHub) to implement these methods. The approach has applications in transportation, demonstrated by their use in the publicly available Network Planning Tool funded by Transport for Scotland.} 138 | } 139 | 140 | 141 | @techreport{departmentfortransport2020, 142 | title = {Cycle infrastructure design (LTN 1/20)}, 143 | author = {{Department for Transport}}, 144 | year = {2020}, 145 | date = {2020}, 146 | pages = {188}, 147 | url = {https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/904088/cycle-infrastructure-design-ltn-1-20.pdf}, 148 | address = {London}, 149 | langid = {en} 150 | } 151 | 152 | @article{szell2021, 153 | title = {Growing Urban Bicycle Networks}, 154 | author = {Szell, Michael and Mimar, Sayat and Perlman, Tyler and Ghoshal, Gourab and Sinatra, Roberta}, 155 | year = {2021}, 156 | month = {07}, 157 | date = {2021-07-05}, 158 | journal = {arXiv:2107.02185 [physics]}, 159 | url = {http://arxiv.org/abs/2107.02185}, 160 | note = {arXiv: 2107.02185 161 | Citation Key: szell{\_}growing{\_}2021} 162 | } 163 | -------------------------------------------------------------------------------- /code/traffic-volumes.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(sf) 3 | library(rsgeo) 4 | 5 | cycle_net_joined = readRDS("data-raw/cycle-net-joined.Rds") 6 | traffic_volumes_scotland = read_sf("data-raw/final_estimates_Scotland.gpkg") 7 | 8 | # Edinburgh traffic volumes 9 | edinburgh = zonebuilder::zb_zone("Edinburgh") 10 | edinburgh_3km = edinburgh |> 11 | # Change number in next line to change zone size: 12 | dplyr::filter(circle_id <= 2) |> 13 | sf::st_union() 14 | traffic_volumes_edinburgh = traffic_volumes_scotland[edinburgh_3km, ] 15 | 16 | # tm_shape(traffic_volumes_edinburgh) + tm_lines("pred_flows") 17 | 18 | # Join traffic volumes with cycle_net 19 | # See tutorial: https://github.com/acteng/network-join-demos 20 | cycle_net_traffic_polygons = stplanr::rnet_join( 21 | max_angle_diff = 30, 22 | rnet_x = cycle_net_joined, 23 | rnet_y = traffic_volumes_edinburgh %>% 24 | transmute( 25 | name_1, 26 | road_classification, 27 | pred_flows 28 | ) %>% 29 | sf::st_cast(to = "LINESTRING"), 30 | dist = 15, 31 | segment_length = 10 32 | ) 33 | 34 | # # Check results: 35 | # cycle_net_traffic_polygons %>% 36 | # select(pred_flows) %>% 37 | # plot() 38 | 39 | # group by + summarise stage 40 | cycleways_with_traffic_df = cycle_net_traffic_polygons %>% 41 | st_drop_geometry() %>% 42 | group_by(osm_id) %>% 43 | summarise( 44 | pred_flows = median(pred_flows), 45 | road_classification = most_common_value(road_classification), 46 | name_1 = most_common_value(name_1) 47 | ) 48 | 49 | # join back onto cycle_net 50 | cycle_net_traffic = left_join(cycle_net_joined, cycleways_with_traffic_df) 51 | 52 | # Check results 53 | # tm_shape(cycle_net_traffic) + tm_lines("pred_flows", lwd = 2, breaks = c(0, 1000, 2000, 4000, 6000, 50000)) 54 | # summary(cycle_net_traffic$pred_flows) # many NAs 55 | 56 | tm_shape(cycle_net_traffic) + tm_lines("road_classification", lwd = 2) 57 | tm_shape(cycle_net_traffic) + tm_lines("highway", lwd = 2) 58 | 59 | # cycle_net_traffic$road_classification = gsub("A Road", "primary", cycle_net_traffic$road_classification) 60 | # cycle_net_traffic$road_classification = gsub("B Road", "secondary", cycle_net_traffic$road_classification) 61 | # cycle_net_traffic$road_classification = gsub("Classified Unnumbered", "tertiary", cycle_net_traffic$road_classification) 62 | 63 | # To investigate potential mapping errors (check to make sure this doesn't include genuine ratruns) 64 | high_flow = cycle_net_traffic %>% 65 | filter( 66 | highway %in% 67 | c("residential", "service") & 68 | road_classification %in% c("A Road", "B Road", "Classified Unnumbered") & 69 | pred_flows >= 4000 70 | ) 71 | tm_shape(high_flow) + tm_lines("pred_flows", lwd = 2) 72 | 73 | # Use original traffic estimates in some cases 74 | # e.g. where residential/service roads have been misclassified as A/B/C roads 75 | cycle_net_traffic = cycle_net_traffic %>% 76 | mutate( 77 | final_traffic = case_when( 78 | detailed_segregation == "Cycle track" ~ 0, 79 | highway %in% 80 | c("residential", "service") & 81 | road_classification %in% 82 | c("A Road", "B Road", "Classified Unnumbered") & 83 | pred_flows >= 4000 ~ 84 | final_volume, 85 | !is.na(pred_flows) ~ pred_flows, 86 | TRUE ~ final_volume 87 | ) 88 | ) 89 | 90 | # Check results 91 | tm_shape(cycle_net_traffic) + 92 | tm_lines( 93 | "final_traffic", 94 | lwd = 2, 95 | breaks = c(0, 1000, 2000, 4000, 6000, 50000) 96 | ) 97 | # tm_shape(cycle_net_traffic) + tm_lines("detailed_segregation") 98 | 99 | cycle_net_traffic = cycle_net_traffic %>% 100 | mutate( 101 | `Level of Service` = case_when( 102 | detailed_segregation == "Cycle track" ~ "High", 103 | detailed_segregation == "Level track" & final_speed <= 30 ~ "High", 104 | detailed_segregation == "Stepped or footway" & final_speed <= 20 ~ "High", 105 | detailed_segregation == "Stepped or footway" & 106 | final_speed == 30 & 107 | final_traffic < 4000 ~ 108 | "High", 109 | detailed_segregation == "Light segregation" & final_speed <= 20 ~ "High", 110 | detailed_segregation == "Light segregation" & 111 | final_speed == 30 & 112 | final_traffic < 4000 ~ 113 | "High", 114 | detailed_segregation == "Cycle lane" & 115 | final_speed <= 20 & 116 | final_traffic < 4000 ~ 117 | "High", 118 | detailed_segregation == "Cycle lane" & 119 | final_speed == 30 & 120 | final_traffic < 1000 ~ 121 | "High", 122 | detailed_segregation == "Mixed traffic" & 123 | final_speed <= 20 & 124 | final_traffic < 2000 ~ 125 | "High", 126 | detailed_segregation == "Mixed traffic" & 127 | final_speed == 30 & 128 | final_traffic < 1000 ~ 129 | "High", 130 | 131 | detailed_segregation == "Level track" & final_speed == 40 ~ "Medium", 132 | detailed_segregation == "Level track" & 133 | final_speed == 50 & 134 | final_traffic < 1000 ~ 135 | "Medium", 136 | detailed_segregation == "Stepped or footway" & final_speed <= 40 ~ 137 | "Medium", 138 | detailed_segregation == "Stepped or footway" & 139 | final_speed == 50 & 140 | final_traffic < 1000 ~ 141 | "Medium", 142 | detailed_segregation == "Light segregation" & final_speed == 30 ~ 143 | "Medium", 144 | detailed_segregation == "Light segregation" & 145 | final_speed == 40 & 146 | final_traffic < 2000 ~ 147 | "Medium", 148 | detailed_segregation == "Light segregation" & 149 | final_speed == 50 & 150 | final_traffic < 1000 ~ 151 | "Medium", 152 | detailed_segregation == "Cycle lane" & final_speed <= 20 ~ "Medium", 153 | detailed_segregation == "Cycle lane" & 154 | final_speed == 30 & 155 | final_traffic < 4000 ~ 156 | "Medium", 157 | detailed_segregation == "Cycle lane" & 158 | final_speed == 40 & 159 | final_traffic < 1000 ~ 160 | "Medium", 161 | detailed_segregation == "Mixed traffic" & 162 | final_speed <= 20 & 163 | final_traffic < 4000 ~ 164 | "Medium", 165 | detailed_segregation == "Mixed traffic" & 166 | final_speed == 30 & 167 | final_traffic < 2000 ~ 168 | "Medium", 169 | detailed_segregation == "Mixed traffic" & 170 | final_speed == 40 & 171 | final_traffic < 1000 ~ 172 | "Medium", 173 | 174 | detailed_segregation == "Level track" ~ "Low", 175 | detailed_segregation == "Stepped or footway" ~ "Low", 176 | detailed_segregation == "Light segregation" & final_speed <= 50 ~ "Low", 177 | detailed_segregation == "Light segregation" & 178 | final_speed == 60 & 179 | final_traffic < 1000 ~ 180 | "Low", 181 | detailed_segregation == "Cycle lane" & final_speed <= 50 ~ "Low", 182 | detailed_segregation == "Cycle lane" & 183 | final_speed == 60 & 184 | final_traffic < 1000 ~ 185 | "Low", 186 | detailed_segregation == "Mixed traffic" & final_speed <= 30 ~ "Low", 187 | detailed_segregation == "Mixed traffic" & 188 | final_speed == 40 & 189 | final_traffic < 2000 ~ 190 | "Low", 191 | detailed_segregation == "Mixed traffic" & 192 | final_speed == 60 & 193 | final_traffic < 1000 ~ 194 | "Low", 195 | 196 | detailed_segregation == "Light segregation" ~ "Should not be used", 197 | detailed_segregation == "Cycle lane" ~ "Should not be used", 198 | detailed_segregation == "Mixed traffic" ~ "Should not be used", 199 | TRUE ~ "Unknown" 200 | ) 201 | ) %>% 202 | dplyr::mutate( 203 | `Level of Service` = factor( 204 | `Level of Service`, 205 | levels = c("High", "Medium", "Low", "Should not be used"), 206 | ordered = TRUE 207 | ) 208 | ) 209 | 210 | 211 | tm_shape(cycle_net_traffic) + 212 | tm_lines("Level of Service", lwd = 2, palette = "viridis") 213 | -------------------------------------------------------------------------------- /code/classify-roads.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(tmap) 3 | library(sf) 4 | tmap_mode("view") 5 | 6 | edinburgh = zonebuilder::zb_zone("Edinburgh") 7 | edinburgh_3km = edinburgh |> 8 | # Change number in next line to change zone size: 9 | dplyr::filter(circle_id <= 2) |> 10 | sf::st_union() 11 | osm = get_travel_network( 12 | "Scotland", 13 | boundary = edinburgh_3km, 14 | boundary_type = "clipsrc" 15 | ) 16 | cycle_net = get_cycling_network(osm) 17 | drive_net = get_driving_network_major(osm) 18 | cycle_net = distance_to_road(cycle_net, drive_net) 19 | cycle_net = classify_cycle_infrastructure(cycle_net) 20 | m = plot_osm_tmap(cycle_net) 21 | m 22 | 23 | tm_shape(drive_net) + tm_lines("highway", lwd = 2) 24 | 25 | # Clean speeds in drive_net 26 | drive_net = clean_speeds(drive_net) 27 | 28 | drive_net = estimate_traffic(drive_net) 29 | 30 | # table(drive_net$maxspeed_clean, useNA = "always") 31 | # # 20 30 40 32 | # # 860 152 9 0 33 | 34 | # Check and clean cycle_net 35 | # table(cycle_net$cycle_segregation, useNA = "always") 36 | # # Cycle track Roadside cycle track Mixed traffic 37 | # # 267 257 5394 38 | # 39 | # # Check with roads are missing speed limits 40 | # nospeed = cycle_net %>% 41 | # filter(is.na(maxspeed)) 42 | # table(nospeed$highway, useNA = "always") 43 | # # cycleway path pedestrian residential service tertiary_link unclassified 44 | # # 306 69 68 27 1775 2 6 0 45 | 46 | # First clean cycle_net speed limits 47 | # Functions here derived from https://github.com/udsleeds/openinfra/blob/main/R/oi_clean_maxspeed_uk.R 48 | cycle_net = clean_speeds(cycle_net) 49 | 50 | # table(cycle_net$maxspeed_clean, useNA = "always") 51 | # # 5 10 20 30 40 52 | # # 20 71 5137 205 9 443 53 | 54 | # Add assumed traffic volumes 55 | # Use Juan's estimates instead where possible 56 | cycle_net = estimate_traffic(cycle_net) 57 | 58 | # table(cycle_net$assumed_volume, useNA = "always") 59 | # # 500 1000 3000 5000 6000 60 | # # 2085 2311 576 79 365 466 61 | 62 | # Join cycle_net and drive_net 63 | # See tutorial: https://github.com/acteng/network-join-demos 64 | cycle_net_joined_polygons = stplanr::rnet_join( 65 | rnet_x = cycle_net, 66 | rnet_y = drive_net %>% 67 | transmute( 68 | maxspeed_road = maxspeed_clean, 69 | highway_join = highway, 70 | volume_join = assumed_volume 71 | ) %>% 72 | sf::st_cast(to = "LINESTRING"), 73 | dist = 20, 74 | segment_length = 10 75 | ) 76 | 77 | # # Check results: 78 | # cycle_net_joined_polygons %>% 79 | # select(maxspeed_road) %>% 80 | # plot() 81 | 82 | # group by + summarise stage 83 | cycleways_with_road_speeds_df = cycle_net_joined_polygons %>% 84 | st_drop_geometry() %>% 85 | group_by(osm_id) %>% 86 | summarise( 87 | maxspeed_road = most_common_value(maxspeed_road), 88 | highway_join = most_common_value(highway_join), 89 | volume_join = most_common_value(volume_join) 90 | ) %>% 91 | mutate( 92 | maxspeed_road = as.numeric(maxspeed_road), 93 | volume_join = as.numeric(volume_join) 94 | ) 95 | 96 | # join back onto cycle_net 97 | 98 | cycle_net_joined = left_join(cycle_net, cycleways_with_road_speeds_df) 99 | 100 | # table(cycle_net_joined$maxspeed_road, useNA = "always") 101 | # # 20 mph 30 mph 40 mph 102 | # # 1683 334 18 3847 103 | 104 | cycle_net_joined = cycle_net_joined %>% 105 | mutate( 106 | final_speed = case_when( 107 | !is.na(maxspeed_clean) ~ maxspeed_clean, 108 | TRUE ~ maxspeed_road 109 | ), 110 | final_volume = case_when( 111 | !is.na(assumed_volume) ~ assumed_volume, 112 | TRUE ~ volume_join 113 | ) 114 | ) 115 | 116 | table(cycle_net_joined$final_speed, useNA = "always") 117 | # 5 10 20 30 40 118 | # 20 71 5271 227 10 286 119 | 120 | roadside = cycle_net_joined %>% 121 | filter(cycle_segregation == "Roadside cycle track") 122 | 123 | # There are some roadside cycle tracks that still aren't linked to roads 124 | table(roadside$final_speed, useNA = "always") 125 | # 20 mph 30 mph 126 | # 200 38 19 127 | table(roadside$highway, useNA = "always") 128 | # cycleway path pedestrian primary residential secondary tertiary unclassified 129 | # 98 14 6 94 7 8 23 7 0 130 | 131 | table(roadside$final_volume, useNA = "always") 132 | # 1000 3000 5000 6000 133 | # 14 60 18 145 20 134 | 135 | # Now go to script traffic-volumes.R 136 | 137 | # # Classify by final speed ------------------------------------------------- 138 | # 139 | # cycle_net_joined = cycle_net_joined %>% 140 | # mutate(`Level of Service` = case_when( 141 | # detailed_segregation == "Cycle track" ~ "High", 142 | # detailed_segregation == "Level track" & final_speed <= 30 ~ "High", 143 | # detailed_segregation == "Stepped or footway" & final_speed <= 20 ~ "High", 144 | # detailed_segregation == "Stepped or footway" & final_speed == 30 & final_volume < 4000 ~ "High", 145 | # detailed_segregation == "Light segregation" & final_speed <= 20 ~ "High", 146 | # detailed_segregation == "Light segregation" & final_speed == 30 & final_volume < 4000 ~ "High", 147 | # detailed_segregation == "Cycle lane" & final_speed <= 20 & final_volume < 4000 ~ "High", 148 | # detailed_segregation == "Cycle lane" & final_speed == 30 & final_volume < 1000 ~ "High", 149 | # detailed_segregation == "Mixed traffic" & final_speed <= 20 & final_volume < 2000 ~ "High", 150 | # detailed_segregation == "Mixed traffic" & final_speed == 30 & final_volume < 1000 ~ "High", 151 | # 152 | # detailed_segregation == "Level track" & final_speed == 40 ~ "Medium", 153 | # detailed_segregation == "Level track" & final_speed == 50 & final_volume < 1000 ~ "Medium", 154 | # detailed_segregation == "Stepped or footway" & final_speed <= 40 ~ "Medium", 155 | # detailed_segregation == "Stepped or footway" & final_speed == 50 & final_volume < 1000 ~ "Medium", 156 | # detailed_segregation == "Light segregation" & final_speed == 30 ~ "Medium", 157 | # detailed_segregation == "Light segregation" & final_speed == 40 & final_volume < 2000 ~ "Medium", 158 | # detailed_segregation == "Light segregation" & final_speed == 50 & final_volume < 1000 ~ "Medium", 159 | # detailed_segregation == "Cycle lane" & final_speed <= 20 ~ "Medium", 160 | # detailed_segregation == "Cycle lane" & final_speed == 30 & final_volume < 4000 ~ "Medium", 161 | # detailed_segregation == "Cycle lane" & final_speed == 40 & final_volume < 1000 ~ "Medium", 162 | # detailed_segregation == "Mixed traffic" & final_speed <= 20 & final_volume < 4000 ~ "Medium", 163 | # detailed_segregation == "Mixed traffic" & final_speed == 30 & final_volume < 2000 ~ "Medium", 164 | # detailed_segregation == "Mixed traffic" & final_speed == 40 & final_volume < 1000 ~ "Medium", 165 | # 166 | # 167 | # detailed_segregation == "Level track" ~ "Low", 168 | # detailed_segregation == "Stepped or footway" ~ "Low", 169 | # detailed_segregation == "Light segregation" & final_speed <= 50 ~ "Low", 170 | # detailed_segregation == "Light segregation" & final_speed == 60 & final_volume < 1000 ~ "Low", 171 | # detailed_segregation == "Cycle lane" & final_speed <= 50 ~ "Low", 172 | # detailed_segregation == "Cycle lane" & final_speed == 60 & final_volume < 1000 ~ "Low", 173 | # detailed_segregation == "Mixed traffic" & final_speed <= 30 ~ "Low", 174 | # detailed_segregation == "Mixed traffic" & final_speed == 40 & final_volume < 2000 ~ "Low", 175 | # detailed_segregation == "Mixed traffic" & final_speed == 60 & final_volume < 1000 ~ "Low", 176 | # 177 | # detailed_segregation == "Light segregation" ~ "Should not be used", 178 | # detailed_segregation == "Cycle lane" ~ "Should not be used", 179 | # detailed_segregation == "Mixed traffic" ~ "Should not be used", 180 | # TRUE ~ "Unknown" 181 | # )) %>% 182 | # dplyr::mutate(`Level of Service` = factor( 183 | # `Level of Service`, 184 | # levels = c("High", "Medium", "Low", "Should not be used"), 185 | # ordered = TRUE 186 | # )) 187 | # 188 | # tm_shape(cycle_net_joined) + tm_lines("Level of Service", lwd = 2, palette = "viridis") 189 | # 190 | # # Checks 191 | # snbu = cycle_net_joined %>% filter(level_of_service == "Should not be used") 192 | # View(snbu) 193 | # tm_shape(snbu) + tm_lines("highway", lwd = 2) 194 | # 195 | # paths = cycle_net_joined %>% filter( 196 | # highway == "path" | highway == "pedestrian", 197 | # bicycle == "permissive" | bicycle == "yes" 198 | # ) 199 | # tm_shape(paths) + tm_lines("highway") 200 | 201 | saveRDS(cycle_net, "data-raw/cycle-net.Rds") 202 | saveRDS(cycle_net_joined, "data-raw/cycle-net-joined.Rds") 203 | -------------------------------------------------------------------------------- /inst/extdata/los_table_complete.csv: -------------------------------------------------------------------------------- 1 | Speed (85th kph),Speed Limit (mph),Speed Limit (kph),AADT,infrastructure,level_of_service 2 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,Mixed Traffic Street,3 3 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,Off Road Path,3 4 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,Segregated Track (wide),3 5 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,Segregated Track (narrow),3 6 | 0 to 30 kph,<20 mph,<32 kph,0 to 999,Painted Cycle Lane,3 7 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,Mixed Traffic Street,3 8 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,Off Road Path,3 9 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,Segregated Track (wide),3 10 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,Segregated Track (narrow),3 11 | 0 to 30 kph,<20 mph,<32 kph,1000 to 1999,Painted Cycle Lane,3 12 | 0 to 30 kph,<20 mph,<32 kph,2000 to 3999,Mixed Traffic Street,2 13 | 0 to 30 kph,<20 mph,<32 kph,2000 to 3999,Off Road Path,3 14 | 0 to 30 kph,<20 mph,<32 kph,2000 to 3999,Segregated Track (wide),3 15 | 0 to 30 kph,<20 mph,<32 kph,2000 to 3999,Segregated Track (narrow),3 16 | 0 to 30 kph,<20 mph,<32 kph,2000 to 3999,Painted Cycle Lane,3 17 | 0 to 30 kph,<20 mph,<32 kph,4000+,Mixed Traffic Street,1 18 | 0 to 30 kph,<20 mph,<32 kph,4000+,Off Road Path,3 19 | 0 to 30 kph,<20 mph,<32 kph,4000+,Segregated Track (wide),3 20 | 0 to 30 kph,<20 mph,<32 kph,4000+,Segregated Track (narrow),3 21 | 0 to 30 kph,<20 mph,<32 kph,4000+,Painted Cycle Lane,2 22 | 0 to 30 kph,20 mph,32 kph,0 to 999,Mixed Traffic Street,3 23 | 0 to 30 kph,20 mph,32 kph,0 to 999,Off Road Path,3 24 | 0 to 30 kph,20 mph,32 kph,0 to 999,Segregated Track (wide),3 25 | 0 to 30 kph,20 mph,32 kph,0 to 999,Segregated Track (narrow),3 26 | 0 to 30 kph,20 mph,32 kph,0 to 999,Painted Cycle Lane,3 27 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,Mixed Traffic Street,3 28 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,Off Road Path,3 29 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,Segregated Track (wide),3 30 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,Segregated Track (narrow),3 31 | 0 to 30 kph,20 mph,32 kph,1000 to 1999,Painted Cycle Lane,3 32 | 0 to 30 kph,20 mph,32 kph,2000 to 3999,Mixed Traffic Street,2 33 | 0 to 30 kph,20 mph,32 kph,2000 to 3999,Off Road Path,3 34 | 0 to 30 kph,20 mph,32 kph,2000 to 3999,Segregated Track (wide),3 35 | 0 to 30 kph,20 mph,32 kph,2000 to 3999,Segregated Track (narrow),3 36 | 0 to 30 kph,20 mph,32 kph,2000 to 3999,Painted Cycle Lane,3 37 | 0 to 30 kph,20 mph,32 kph,4000+,Mixed Traffic Street,1 38 | 0 to 30 kph,20 mph,32 kph,4000+,Off Road Path,3 39 | 0 to 30 kph,20 mph,32 kph,4000+,Segregated Track (wide),3 40 | 0 to 30 kph,20 mph,32 kph,4000+,Segregated Track (narrow),3 41 | 0 to 30 kph,20 mph,32 kph,4000+,Painted Cycle Lane,2 42 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,Mixed Traffic Street,3 43 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,Off Road Path,3 44 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,Segregated Track (wide),3 45 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,Segregated Track (narrow),3 46 | 30 kph to 50 kph,30 mph,48 kph,0 to 999,Painted Cycle Lane,3 47 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,Mixed Traffic Street,2 48 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,Off Road Path,3 49 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,Segregated Track (wide),3 50 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,Segregated Track (narrow),3 51 | 30 kph to 50 kph,30 mph,48 kph,1000 to 1999,Painted Cycle Lane,2 52 | 30 kph to 50 kph,30 mph,48 kph,2000 to 3999,Mixed Traffic Street,1 53 | 30 kph to 50 kph,30 mph,48 kph,2000 to 3999,Off Road Path,3 54 | 30 kph to 50 kph,30 mph,48 kph,2000 to 3999,Segregated Track (wide),3 55 | 30 kph to 50 kph,30 mph,48 kph,2000 to 3999,Segregated Track (narrow),3 56 | 30 kph to 50 kph,30 mph,48 kph,2000 to 3999,Painted Cycle Lane,2 57 | 30 kph to 50 kph,30 mph,48 kph,4000+,Mixed Traffic Street,1 58 | 30 kph to 50 kph,30 mph,48 kph,4000+,Off Road Path,3 59 | 30 kph to 50 kph,30 mph,48 kph,4000+,Segregated Track (wide),3 60 | 30 kph to 50 kph,30 mph,48 kph,4000+,Segregated Track (narrow),2 61 | 30 kph to 50 kph,30 mph,48 kph,4000+,Painted Cycle Lane,1 62 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,Mixed Traffic Street,2 63 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,Off Road Path,3 64 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,Segregated Track (wide),2 65 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,Segregated Track (narrow),2 66 | 50 kph to 65 kph,40 mph,64 kph,0 to 999,Painted Cycle Lane,2 67 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,Mixed Traffic Street,1 68 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,Off Road Path,3 69 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,Segregated Track (wide),2 70 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,Segregated Track (narrow),2 71 | 50 kph to 65 kph,40 mph,64 kph,1000 to 1999,Painted Cycle Lane,1 72 | 50 kph to 65 kph,40 mph,64 kph,2000 to 3999,Mixed Traffic Street,0 73 | 50 kph to 65 kph,40 mph,64 kph,2000 to 3999,Off Road Path,3 74 | 50 kph to 65 kph,40 mph,64 kph,2000 to 3999,Segregated Track (wide),2 75 | 50 kph to 65 kph,40 mph,64 kph,2000 to 3999,Segregated Track (narrow),1 76 | 50 kph to 65 kph,40 mph,64 kph,2000 to 3999,Painted Cycle Lane,1 77 | 50 kph to 65 kph,40 mph,64 kph,4000+,Mixed Traffic Street,0 78 | 50 kph to 65 kph,40 mph,64 kph,4000+,Off Road Path,3 79 | 50 kph to 65 kph,40 mph,64 kph,4000+,Segregated Track (wide),2 80 | 50 kph to 65 kph,40 mph,64 kph,4000+,Segregated Track (narrow),1 81 | 50 kph to 65 kph,40 mph,64 kph,4000+,Painted Cycle Lane,1 82 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,Mixed Traffic Street,1 83 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,Off Road Path,3 84 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,Segregated Track (wide),2 85 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,Segregated Track (narrow),2 86 | 65 kph to 80 kph,50 mph,80 kph,0 to 999,Painted Cycle Lane,1 87 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,Mixed Traffic Street,0 88 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,Off Road Path,3 89 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,Segregated Track (wide),1 90 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,Segregated Track (narrow),1 91 | 65 kph to 80 kph,50 mph,80 kph,1000 to 1999,Painted Cycle Lane,1 92 | 65 kph to 80 kph,50 mph,80 kph,2000 to 3999,Mixed Traffic Street,0 93 | 65 kph to 80 kph,50 mph,80 kph,2000 to 3999,Off Road Path,3 94 | 65 kph to 80 kph,50 mph,80 kph,2000 to 3999,Segregated Track (wide),1 95 | 65 kph to 80 kph,50 mph,80 kph,2000 to 3999,Segregated Track (narrow),1 96 | 65 kph to 80 kph,50 mph,80 kph,2000 to 3999,Painted Cycle Lane,1 97 | 65 kph to 80 kph,50 mph,80 kph,4000+,Mixed Traffic Street,0 98 | 65 kph to 80 kph,50 mph,80 kph,4000+,Off Road Path,3 99 | 65 kph to 80 kph,50 mph,80 kph,4000+,Segregated Track (wide),1 100 | 65 kph to 80 kph,50 mph,80 kph,4000+,Segregated Track (narrow),1 101 | 65 kph to 80 kph,50 mph,80 kph,4000+,Painted Cycle Lane,1 102 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,Mixed Traffic Street,1 103 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,Off Road Path,3 104 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,Segregated Track (wide),1 105 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,Segregated Track (narrow),1 106 | 80 kph to 95 kph,60 mph,97 kph,0 to 999,Painted Cycle Lane,1 107 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,Mixed Traffic Street,0 108 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,Off Road Path,3 109 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,Segregated Track (wide),1 110 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,Segregated Track (narrow),0 111 | 80 kph to 95 kph,60 mph,97 kph,1000 to 1999,Painted Cycle Lane,0 112 | 80 kph to 95 kph,60 mph,97 kph,2000 to 3999,Mixed Traffic Street,0 113 | 80 kph to 95 kph,60 mph,97 kph,2000 to 3999,Off Road Path,3 114 | 80 kph to 95 kph,60 mph,97 kph,2000 to 3999,Segregated Track (wide),1 115 | 80 kph to 95 kph,60 mph,97 kph,2000 to 3999,Segregated Track (narrow),0 116 | 80 kph to 95 kph,60 mph,97 kph,2000 to 3999,Painted Cycle Lane,0 117 | 80 kph to 95 kph,60 mph,97 kph,4000+,Mixed Traffic Street,0 118 | 80 kph to 95 kph,60 mph,97 kph,4000+,Off Road Path,3 119 | 80 kph to 95 kph,60 mph,97 kph,4000+,Segregated Track (wide),1 120 | 80 kph to 95 kph,60 mph,97 kph,4000+,Segregated Track (narrow),0 121 | 80 kph to 95 kph,60 mph,97 kph,4000+,Painted Cycle Lane,0 122 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,Mixed Traffic Street,0 123 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,Off Road Path,3 124 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,Segregated Track (wide),1 125 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,Segregated Track (narrow),0 126 | 95 kph to 110 kph,60+ mph,97+ kph,0 to 999,Painted Cycle Lane,0 127 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,Mixed Traffic Street,0 128 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,Off Road Path,3 129 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,Segregated Track (wide),1 130 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,Segregated Track (narrow),0 131 | 95 kph to 110 kph,60+ mph,97+ kph,1000 to 1999,Painted Cycle Lane,0 132 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 3999,Mixed Traffic Street,0 133 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 3999,Off Road Path,3 134 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 3999,Segregated Track (wide),1 135 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 3999,Segregated Track (narrow),0 136 | 95 kph to 110 kph,60+ mph,97+ kph,2000 to 3999,Painted Cycle Lane,0 137 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,Mixed Traffic Street,0 138 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,Off Road Path,3 139 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,Segregated Track (wide),1 140 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,Segregated Track (narrow),0 141 | 95 kph to 110 kph,60+ mph,97+ kph,4000+,Painted Cycle Lane,0 142 | -------------------------------------------------------------------------------- /R/get_parallel_values.R: -------------------------------------------------------------------------------- 1 | #' Get values from parallel features 2 | #' 3 | #' This function finds features in a 'target' network (`target_net`) that are parallel 4 | #' and close to features in a 'source' network (`source_net`) and imputes missing 5 | #' values in a specified column of the `target_net` based on the median value 6 | #' from the nearby parallel features in the `source_net`. 7 | #' 8 | #' It's a generalisation of the `find_nearby_speeds` function previously used 9 | #' to impute missing `maxspeed` values in a cycle network based on nearby roads. 10 | #' 11 | #' @param target_net An sf object representing the network where values need imputation (e.g., cycle network). Must contain a unique `osm_id` column. 12 | #' @param source_net An sf object representing the network to source values from (e.g., drive network). Must contain a unique `osm_id` column. 13 | #' @param column The name of the column (as a string) in both networks to check for NAs 14 | #' in `target_net` and source values from `source_net`. Defaults to "maxspeed". 15 | #' This column should ideally be numeric or coercible to numeric after removing units. 16 | #' @param buffer_dist The buffer distance (in the units of the CRS) around `target_net` 17 | #' features to search for nearby `source_net` features. Defaults to 10. 18 | #' @param angle_threshold The maximum allowed absolute difference in bearing (degrees) 19 | #' between features in `target_net` and `source_net` for them to be considered parallel. 20 | #' Defaults to 20. 21 | #' @param value_pattern Optional regex pattern to remove from the `column` values in `source_net` 22 | #' before converting to numeric. Defaults to " mph". Set to NULL to skip removal. 23 | #' @param value_replacement Replacement string for `value_pattern`. Defaults to "". 24 | #' @param add_suffix Optional suffix to add back to the imputed numeric value before 25 | #' assigning it back to the `column`. Defaults to " mph". Set to NULL to skip adding suffix. 26 | #' 27 | #' @return An sf object, the `target_net` with the specified `column` potentially 28 | #' updated with imputed values from `source_net`. 29 | #' @export 30 | #' @importFrom sf st_buffer st_point_on_surface st_join st_drop_geometry st_sf st_geometry 31 | #' @importFrom dplyr filter select mutate group_by summarise ungroup left_join case_when if_else row_number inner_join rename all_of 32 | #' @examples 33 | #' # Assuming cycle_net_f and drive_net_f are loaded sf objects from the package 34 | #' 35 | #' # Impute missing 'maxspeed' in cycle_net_f using drive_net_f 36 | #' cycle_net_updated_speed = get_parallel_values( 37 | #' target_net = cycle_net_f, 38 | #' source_net = drive_net_f, 39 | #' column = "maxspeed", 40 | #' buffer_dist = 10, 41 | #' angle_threshold = 20 42 | #' ) 43 | #' 44 | #' # Example imputing a hypothetical 'width' column (assuming it exists and needs imputation) 45 | #' # cycle_net_f$width = as.character(cycle_net_f$width) # Ensure character if adding suffix 46 | #' # cycle_net_f$width[sample(1:nrow(cycle_net_f), 5)] = NA # Add some NAs for example 47 | #' # cycle_net_updated_width = get_parallel_values( 48 | #' # target_net = cycle_net_f, 49 | #' # source_net = drive_net_f, # Using drive_net just for example structure 50 | #' # column = "width", 51 | #' # buffer_dist = 5, 52 | #' # angle_threshold = 15, 53 | #' # value_pattern = NULL, # Assuming width is already numeric or has no suffix 54 | #' # add_suffix = NULL 55 | #' # ) 56 | #' 57 | #' print(paste("NA maxspeed before:", sum(is.na(cycle_net_f$maxspeed)))) 58 | #' print(paste("NA maxspeed after:", sum(is.na(cycle_net_updated_speed$maxspeed)))) 59 | get_parallel_values = function(target_net, source_net, column = "maxspeed", 60 | buffer_dist = 10, angle_threshold = 20, 61 | value_pattern = " mph", value_replacement = "", 62 | add_suffix = " mph") { 63 | 64 | # --- Input Checks --- 65 | if (!requireNamespace("stplanr", quietly = TRUE)) { 66 | stop("Package 'stplanr' needed for this function to work. Please install it.", call. = FALSE) 67 | } 68 | if (!inherits(target_net, "sf") || !inherits(source_net, "sf")) { 69 | stop("Both target_net and source_net must be sf objects.") 70 | } 71 | if (!column %in% names(target_net)) { 72 | stop("Column '", column, "' not found in target_net.") 73 | } 74 | if (!column %in% names(source_net)) { 75 | stop("Column '", column, "' not found in source_net.") 76 | } 77 | if (!"osm_id" %in% names(target_net) || !"osm_id" %in% names(source_net)) { 78 | stop("Both target_net and source_net must contain a unique 'osm_id' column.") 79 | } 80 | # Check if CRS are the same, warn if not? Or transform? Assume user provides compatible CRS for now. 81 | 82 | # --- Symbol Setup --- 83 | col_sym = rlang::sym(column) 84 | col_new_sym = rlang::sym(paste0(column, "_new")) 85 | osm_id_target_sym = rlang::sym("osm_id_target") # For clarity in joins/grouping 86 | 87 | # --- 1. Prepare Target Network (Features needing imputation) --- 88 | target_missing = target_net |> 89 | dplyr::filter(is.na(!!col_sym)) 90 | 91 | if (nrow(target_missing) == 0) { 92 | message("No missing values found in column '", column, "' of target_net. Returning original data.") 93 | return(target_net) 94 | } 95 | 96 | # Calculate bearing and select necessary columns: 97 | target_missing$azimuth_target = stplanr::line_bearing(target_missing, bidirectional = TRUE) 98 | target_missing = target_missing |> 99 | dplyr::select(osm_id, azimuth_target) 100 | # Buffer the target features needing imputation 101 | target_missing_buffer = sf::st_buffer(target_missing, dist = buffer_dist) 102 | 103 | 104 | target_missing_buffer = sf::st_buffer(target_missing, dist = buffer_dist) 105 | 106 | source_with_values = source_net |> 107 | dplyr::filter(!is.na(!!col_sym) & !(!!col_sym %in% c("", " "))) 108 | 109 | if (nrow(source_with_values) == 0) { 110 | warning("No non-missing values found in column '", column, "' of source_net. Cannot impute.") 111 | return(target_net) 112 | } 113 | 114 | # Calculate bearing and select necessary columns: 115 | source_with_values$azimuth_source = stplanr::line_bearing(source_with_values, bidirectional = TRUE) 116 | source_with_values = source_with_values |> 117 | dplyr::select(azimuth_source, !!col_sym) |> 118 | # Rename col_sym to _source: 119 | dplyr::rename(new_values = !!col_sym) 120 | 121 | # Use points on surface for potentially faster spatial join 122 | source_with_values_points = sf::st_point_on_surface(source_with_values) 123 | 124 | source_with_values_points_near = 125 | source_with_values_points[target_missing_buffer, ] 126 | 127 | # Check if any points were found within the buffer 128 | if (nrow(source_with_values_points_near) == 0) { 129 | warning("No nearby source features found within the buffer distance. Cannot impute.") 130 | return(target_net) 131 | } 132 | 133 | # --- 3. Spatial Join: Find nearby source points within target buffers --- 134 | # This join links each target buffer to the source points it contains 135 | joined_data_sf = sf::st_join( 136 | target_missing_buffer |> 137 | dplyr::select(osm_id, azimuth_target), # Keep only relevant columns 138 | source_with_values_points 139 | ) 140 | 141 | # --- 4. Filter by Angle and Calculate New Value --- 142 | joined_data_clean = joined_data_sf |> 143 | sf::st_drop_geometry() |> # Now work with the attribute table 144 | # angle_diff = abs(azimuth_cycle - azimuth_road), 145 | # maxspeed_numeric = gsub(maxspeed, pattern = " mph", replacement = "") |> 146 | # as.numeric() 147 | # ) |> 148 | # filter(angle_diff < angle_threshold) |> 149 | # group_by(osm_id) |> 150 | # dplyr::summarise( 151 | # maxspeed_new = median(maxspeed_numeric, na.rm = TRUE) |> 152 | # paste0(" mph") 153 | # ) |> 154 | # ungroup() 155 | dplyr::mutate( 156 | angle_diff = abs(azimuth_target - azimuth_source), 157 | new_value_numeric = gsub(new_values, pattern = value_pattern, replacement = value_replacement) |> 158 | as.numeric() 159 | ) |> 160 | dplyr::filter(angle_diff < angle_threshold) |> 161 | # Group by the target feature ID 162 | dplyr::group_by(osm_id) |> 163 | # Calculate the median of the numeric values from parallel source features 164 | dplyr::summarise( 165 | # Use dynamic name for the summarized column 166 | new_value := stats::median(new_value_numeric, na.rm = TRUE), 167 | .groups = 'drop' # Drop grouping 168 | ) |> 169 | # Add suffix if specified 170 | dplyr::mutate( 171 | new_value = if (!is.null(add_suffix)) { 172 | paste0(new_value, add_suffix) 173 | } else { 174 | as.character(new_value) # Ensure it's character if no suffix 175 | } 176 | ) 177 | 178 | target_net_joined = dplyr::left_join( 179 | target_net, 180 | joined_data_clean |> 181 | dplyr::select(osm_id, new_value), # Select only relevant columns for join 182 | by = "osm_id" # Join by the unique ID 183 | ) |> 184 | # Update the original column where it was NA using the new value 185 | dplyr::mutate( 186 | # Coerce new value to match type of original column if necessary? 187 | # For now, assume character assignment works or original column is character. 188 | # This might fail if original column is strictly numeric and new value has suffix. 189 | # Consider adding type check and coercion. 190 | !!col_sym := dplyr::case_when( 191 | is.na(!!col_sym) & !is.na(new_value) ~ new_value, # Impute only if original is NA and new is not NA 192 | TRUE ~ !!col_sym 193 | ) 194 | ) |> 195 | # Remove the temporary new value column 196 | dplyr::select(-new_value) 197 | return(target_net_joined) 198 | } 199 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | fig.path = "man/figures/README-", 10 | collapse = TRUE, 11 | comment = "#>", 12 | message = FALSE, 13 | warning = FALSE, 14 | cache = FALSE 15 | ) 16 | ``` 17 | 18 | # osmactive 19 | 20 | 21 | [![R-CMD-check](https://github.com/nptscot/osmactive/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/nptscot/osmactive/actions/workflows/R-CMD-check.yaml) 22 | 23 | 24 | 25 | The goal of osmactive is to provide functions, example datasets and documentation for extracting active travel infrastructure from OpenStreetMap data. 26 | 27 | ```{r, eval=FALSE, echo=FALSE} 28 | # Setup instructions 29 | # Create the README.Rmd with usethis: 30 | usethis::use_readme_rmd() 31 | # Add description: 32 | usethis::use_description() 33 | # Add package dependencies 34 | usethis::use_package("osmextract") 35 | # Create the R directory 36 | usethis::use_r("osmactive") 37 | usethis::use_package("geos") 38 | usethis::use_package("sf") 39 | usethis::use_package("dplyr") 40 | usethis::use_package("stringr") 41 | # Ignore README.Rmd in build ignore 42 | usethis::use_build_ignore("README.Rmd") 43 | usethis::use_mit_license("Robin Lovelace") 44 | 45 | # Add pkgdown site 46 | usethis::use_pkgdown() 47 | # pkgdown pages 48 | usethis::use_pkgdown_github_pages() 49 | # Add actions 50 | usethis::use_github_action("pkgdown") 51 | usethis::use_github_action("check-standard") 52 | # Add vignette with paper on cycle infrastructure classification 53 | usethis::use_vignette("classifying-cycle-infrastructure") 54 | # Add example datasets 55 | usethis::use_data_raw("osm_edinburgh") 56 | usethis::use_vignette("classify-cbd", "Scottish Cycling by Design classification") 57 | # Style the package 58 | remotes::install_github("Robinlovelace/styler.equals") 59 | styler.equals::style_pkg() 60 | # And the README: 61 | styler.equals::style_file("README.Rmd") 62 | # build readme: 63 | devtools::build_readme() 64 | ``` 65 | 66 | ```{r, eval=FALSE, echo=FALSE} 67 | # For testing: 68 | devtools::document() 69 | devtools::check() 70 | ``` 71 | 72 | Install the package with: 73 | 74 | ```{r, eval=FALSE} 75 | remotes::install_github("nptscot/osmactive") 76 | ``` 77 | 78 | ```{r} 79 | library(osmactive) 80 | library(tmap) # for mapping 81 | library(dplyr) # for data manipulation 82 | library(sf) # for spatial data 83 | ``` 84 | 85 | Alternatively, you can load the package with the following for local development: 86 | 87 | ```{r, eval=TRUE, results='hide'} 88 | devtools::load_all() 89 | ``` 90 | 91 | ## Minimal example 92 | 93 | The package comes with example data for testing functions. 94 | You can test the functions as follows: 95 | 96 | ```{r minimal} 97 | osm = osm_edinburgh 98 | cycle_net = get_cycling_network(osm) 99 | drive_net = get_driving_network(osm) 100 | drive_net_major = get_driving_network(osm) 101 | cycle_net = distance_to_road(cycle_net, drive_net) 102 | cycle_net = classify_cycle_infrastructure(cycle_net) 103 | table(cycle_net$detailed_segregation) 104 | table(cycle_net$cycle_segregation) 105 | ``` 106 | 107 | You can also create plots with the packaged `plot_osm_tmap()` function: 108 | 109 | ```{r minimal_plot_osm} 110 | m = plot_osm_tmap(cycle_net) 111 | m 112 | ``` 113 | 114 | Estimate the 'level of service' of cycle infrastructure: 115 | 116 | ```{r} 117 | #| label: level_of_service 118 | cycle_net_los = level_of_service(cycle_net) 119 | table(cycle_net_los$`Level of Service`) 120 | plot(cycle_net_los["Level of Service"]) 121 | ``` 122 | 123 | Use any plotting code you like: 124 | 125 | ```{r minimal_plot_osm_tmap, eval=FALSE, echo=FALSE} 126 | tm_shape(osm) + 127 | tm_lines(col = "grey") + 128 | tm_shape(cycle_net) + 129 | tm_lines(col = "green") + 130 | tm_shape(drive_net) + 131 | tm_lines(col = "darkgrey") + 132 | tm_shape(drive_net_major) + 133 | tm_lines(col = "black") 134 | ``` 135 | 136 | # Running as a shiny app 137 | 138 | ```{r} 139 | #| eval: false 140 | shiny::runApp("code/app.R") 141 | ``` 142 | 143 | 144 | ## Leeds example 145 | 146 | ```{r leeds} 147 | osm = get_travel_network("Leeds") 148 | cycle_net = get_cycling_network(osm) 149 | drive_net = get_driving_network(osm) 150 | cycle_net_d = distance_to_road(cycle_net, drive_net) 151 | cycle_net_c = classify_cycle_infrastructure(cycle_net_d) 152 | m = plot_osm_tmap(cycle_net_c) 153 | m 154 | ``` 155 | 156 | ```{r} 157 | #| include: false 158 | tmap_save(m, "classify_cycle_infrastructure_leeds.html") 159 | browseURL("classify_cycle_infrastructure_leeds.html") 160 | 161 | system("gh release upload v0.1 classify_cycle_infrastructure_leeds.html --clobber") 162 | 163 | # Available: 164 | # https://github.com/nptscot/osmactive/releases/download/v0.1/classify_cycle_infrastructure_leeds.html 165 | # cycle_net_c = classify_cycle_infrastructure(cycle_net_d, include_mixed_traffic = TRUE) 166 | cycle_net_c_all = classify_cycle_infrastructure(cycle_net_d) 167 | cycle_net_los = level_of_service(cycle_net_c_all) 168 | names(cycle_net_los) 169 | cycle_net_los = cycle_net_los |> 170 | select(name, cycle_segregation, detailed_segregation, `Level of Service`, AADT, surface, `Speed Limit (mph)`) 171 | cycle_net_wetherby_road = cycle_net_los |> 172 | filter(stringr::str_detect(name, "Wetherby Road")) 173 | summary(cycle_net_los$`Level of Service`) 174 | table(cycle_net_los$`Level of Service`) 175 | plot(cycle_net_los["Level of Service"]) 176 | m = tm_shape(cycle_net_los) + 177 | tm_lines(col = "Level of Service") 178 | tmap::tmap_save(m, "classify_cycle_infrastructure_leeds_los.html") 179 | # browseURL("classify_cycle_infrastructure_leeds_los.html") 180 | ``` 181 | 182 | ## Edinburgh example 183 | 184 | ```{r edinburgh} 185 | osm = get_travel_network("Edinburgh") 186 | cycle_net = get_cycling_network(osm) 187 | drive_net = get_driving_network(osm) 188 | cycle_net = distance_to_road(cycle_net, drive_net) 189 | cycle_net = classify_cycle_infrastructure(cycle_net) 190 | m = plot_osm_tmap(cycle_net) 191 | m 192 | ``` 193 | 194 | 195 | 196 | ```{r} 197 | #| include: false 198 | tmap_save(m, "cycle_net_edinburgh.html") 199 | browseURL("cycle_net_edinburgh.html") 200 | system("gh release upload v0.1 cycle_net_edinburgh.html --clobber") 201 | # Available: 202 | # https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_edinburgh.html 203 | ``` 204 | 205 | ## Dublin example 206 | 207 | ```{r dublin} 208 | dublin_zones = zonebuilder::zb_zone("Dublin") 209 | dublin_6km = dublin_zones |> 210 | filter(circle_id <= 3) |> 211 | sf::st_union() 212 | osm = get_travel_network("Republic of Ireland", boundary = dublin_6km, boundary_type = "clipsrc") 213 | cycle_net = get_cycling_network(osm) 214 | drive_net = get_driving_network(osm) 215 | cycle_net = distance_to_road(cycle_net, drive_net) 216 | cycle_net = classify_cycle_infrastructure(cycle_net) 217 | m = plot_osm_tmap(cycle_net) 218 | m 219 | ``` 220 | 221 | ```{r} 222 | #| include: false 223 | tmap_save(m, "classify_cycle_infrastructure_dublin.html") 224 | browseURL("classify_cycle_infrastructure_dublin.html") 225 | # upload: 226 | system("gh release upload v0.1 classify_cycle_infrastructure_dublin.html --clobber") 227 | ``` 228 | 229 | ## Lisbon example 230 | 231 | ```{r lisbon} 232 | u = "https://ushift.tecnico.ulisboa.pt/content/data/lisbon_limit.geojson" 233 | f = basename(u) 234 | if (!file.exists(f)) download.file(u, f) 235 | lisbon = sf::read_sf(f) 236 | lisbon = lisbon |> 237 | sf::st_cast("POLYGON") 238 | osm = get_travel_network("Portugal", boundary = lisbon, boundary_type = "clipsrc", force_vectortranslate = TRUE) 239 | cycle_net = get_cycling_network(osm) 240 | drive_net = get_driving_network(osm) 241 | cycle_net = distance_to_road(cycle_net, drive_net) 242 | cycle_net = classify_cycle_infrastructure(cycle_net) 243 | m = plot_osm_tmap(cycle_net) 244 | m 245 | ``` 246 | 247 | ```{r} 248 | #| include: false 249 | tmap_save(m, "classify_cycle_infrastructure_lisbon.html") 250 | browseURL("classify_cycle_infrastructure_lisbon.html") 251 | # upload: 252 | system("gh release upload v0.1 classify_cycle_infrastructure_lisbon.html --clobber") 253 | 254 | # Test for surfaces: 255 | table(cycle_net$surface) 256 | ``` 257 | 258 | ## London 259 | 260 | ```{r london} 261 | london = zonebuilder::zb_zone("Southwark Station", n_circles = 1) 262 | london = sf::st_union(london) |> 263 | sf::st_make_valid() 264 | osm = get_travel_network(london, boundary = london, boundary_type = "clipsrc") 265 | cycle_net = get_cycling_network(osm) 266 | drive_net = get_driving_network(osm) 267 | cycle_net = distance_to_road(cycle_net, drive_net) 268 | cycle_net = classify_cycle_infrastructure(cycle_net) 269 | m = plot_osm_tmap(cycle_net) 270 | m 271 | ``` 272 | 273 | ```{r} 274 | #| include: false 275 | tmap_save(m, "classify_cycle_infrastructure_london.html") 276 | browseURL("classify_cycle_infrastructure_london.html") 277 | # upload: 278 | system("gh release upload v0.1 classify_cycle_infrastructure_london.html --clobber") 279 | ``` 280 | 281 | ## Cambridge 282 | 283 | ```{r cambridge} 284 | cambridge = zonebuilder::zb_zone("Cambridge") 285 | cambridge = sf::st_union(cambridge) |> 286 | sf::st_make_valid() 287 | osm = get_travel_network("Cambridge", boundary = cambridge, boundary_type = "clipsrc") 288 | cycle_net = get_cycling_network(osm) 289 | drive_net = get_driving_network(osm) 290 | cycle_net = distance_to_road(cycle_net, drive_net) 291 | cycle_net = classify_cycle_infrastructure(cycle_net) 292 | m = plot_osm_tmap(cycle_net) 293 | m 294 | ``` 295 | 296 | ```{r} 297 | #| include: false 298 | tmap_save(m, "classify_cycle_infrastructure_cambridge.html") 299 | browseURL("classify_cycle_infrastructure_cambridge.html") 300 | # upload: 301 | system("gh release upload v0.1 classify_cycle_infrastructure_cambridge.html --clobber") 302 | ``` 303 | 304 | ## Bristol example 305 | 306 | ```{r bristol} 307 | osm = get_travel_network("Bristol") 308 | cycle_net = get_cycling_network(osm) 309 | drive_net = get_driving_network(osm) 310 | cycle_net = distance_to_road(cycle_net, drive_net) 311 | cycle_net = classify_cycle_infrastructure(cycle_net) 312 | m = plot_osm_tmap(cycle_net) 313 | m 314 | ``` 315 | 316 | 317 | 318 | ```{r} 319 | #| include: false 320 | tmap_save(m, "cycle_net_bristol.html") 321 | browseURL("cycle_net_bristol.html") 322 | system("gh release upload v0.1 cycle_net_bristol.html --clobber") 323 | # Available: 324 | # https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_bristol.html 325 | ``` 326 | 327 | ## Christchurch 328 | 329 | ```{r christchurch} 330 | library(sf) 331 | zones = zonebuilder::zb_zone("Christchurch") 332 | zones_6km = zones |> 333 | dplyr::filter(circle_id <= 3) |> 334 | sf::st_union() 335 | # mapview::mapview(zones_6km) 336 | osm = get_travel_network("New Zealand", boundary = zones_6km, boundary_type = "clipsrc", force_vectortranslate = TRUE, force_download = TRUE) 337 | # osm = get_travel_network("New Zealand") 338 | # osm = osm[zones_6km, , op = sf::st_within] 339 | dim(osm) 340 | cycle_net = get_cycling_network(osm) 341 | drive_net = get_driving_network(osm) 342 | cycle_net = distance_to_road(cycle_net, drive_net) 343 | cycle_net = classify_cycle_infrastructure(cycle_net) 344 | summary(cycle_net$cycle_segregation) 345 | plot(cycle_net) 346 | m = plot_osm_tmap(cycle_net) 347 | m 348 | ``` 349 | 350 | ```{r} 351 | #| include: false 352 | # Save an interactive version of the map to check the resu lts as follows: 353 | tmap_save(m, "cycle_net_christchurch.html") 354 | browseURL("cycle_net_christchurch.html") 355 | system("gh release upload v0.1 cycle_net_christchurch.html --clobber") 356 | # Available: 357 | browseURL("https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_christchurch.html") 358 | ``` -------------------------------------------------------------------------------- /vignettes/classify-cbd.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Scottish Cycling by Design classification" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Scottish Cycling by Design classification} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | ```{r setup} 19 | options(timeout = 3000) 20 | library(osmactive) 21 | library(tidyverse) 22 | library(sf) 23 | library(dplyr) 24 | library(mapview) 25 | tmap::tmap_mode("view") 26 | ``` 27 | 28 | The default classification is the Cycling by Design guidance from the Scottish Government. The following code shows how we implemented it. 29 | 30 | # Transport Scotland's Cycling by Design classification 31 | 32 | The level of service associated with each link can be calculated based on the Level of Service table in the Cycling by Design guidance: 33 | 34 | ![](https://nptscot.github.io/images/clos_facilities.png) 35 | 36 | 37 | 38 | ```{r} 39 | #| eval: false 40 | #| echo: false 41 | #| label: update-los-table 42 | # NOTE: run this locally 43 | devtools::load_all() 44 | # table(cycle_net$cycle_segregation) 45 | # Segregated Track (wide) Off Road Path Segregated Track (narrow) 46 | # 214 973 859 47 | # Shared Footway Painted Cycle Lane 48 | # 3325 522 49 | los_table = read_csv("https://github.com/nptscot/osmactive/raw/refs/heads/main/inst/extdata/level-of-service-table.csv") 50 | 51 | # names(los_table) 52 | # [1] "Motor Traffic Speed (85th percentile)" 53 | # [2] "Speed Limit (mph)" 54 | # [3] "Speed Limit (kph)" 55 | # [4] "Two-way traffic flow (pcu per day)" 56 | # [5] "Two-way traffic flow (pcu per hour)" 57 | # [6] "Mixed Traffic Street" 58 | # [7] "Detached or Remote Cycle Track" 59 | # [8] "Cycle Track at Carriageway Level" 60 | # [9] "Stepped or Footway Level Cycle Track" 61 | # [10] "Light Segregation" 62 | # [11] "Cycle Lane" 63 | 64 | 65 | los_table_npt = los_table |> 66 | transmute( 67 | `Speed (85th kph)` = `Motor Traffic Speed (85th percentile)`, 68 | `Speed Limit (mph)` = `Speed Limit (mph)`, 69 | `Speed Limit (kph)` = `Speed Limit (kph)`, 70 | `AADT` = `Two-way traffic flow (pcu per day)`, 71 | `Mixed Traffic Street`, 72 | `Off Road Path` = `Detached or Remote Cycle Track`, 73 | `Segregated Track (wide)` = `Cycle Track at Carriageway Level`, 74 | `Segregated Track (narrow)` = `Light Segregation`, 75 | `Painted Cycle Lane` = `Cycle Lane` 76 | ) |> 77 | # Convert the categories to "0 to 1999", "2000 to 3999", "4000+" 78 | mutate( 79 | AADT = npt_to_cbd_aadt(AADT) 80 | ) 81 | 82 | los_table_complete = los_table_npt |> 83 | pivot_longer( 84 | cols = c("Mixed Traffic Street", 85 | "Off Road Path", 86 | "Segregated Track (wide)", 87 | "Segregated Track (narrow)", 88 | "Painted Cycle Lane"), 89 | names_to = "infrastructure", 90 | values_to = "level_of_service" 91 | ) 92 | 93 | write_csv(los_table_complete, "inst/extdata/los_table_complete.csv") 94 | usethis::use_data(los_table_complete, overwrite = TRUE) 95 | ``` 96 | 97 | # Testing the classification 98 | 99 | ```{r} 100 | osm_net = osmactive::get_travel_network("Edinburgh") |> st_transform(crs = 27700) 101 | zone = zonebuilder::zb_zone("edinburgh", n_circles = 3) |> st_transform(crs = 27700) 102 | osm_net = osm_net[zone, ] 103 | osm = osmactive::get_cycling_network(osm_net) 104 | cycle_net = get_cycling_network(osm) 105 | drive_net = get_driving_network(osm) 106 | drive_net_major = get_driving_network(osm) 107 | cycle_net = distance_to_road(cycle_net, drive_net) 108 | cycle_net = classify_cycle_infrastructure(cycle_net) 109 | table(cycle_net$detailed_segregation) 110 | table(cycle_net$cycle_segregation) 111 | ``` 112 | 113 | 114 | We can then create the `Speed Limit (mph)` and `AADT` columns in the `cycle_net` object: 115 | 116 | ```{r} 117 | print("Original maxspeed for residential:") 118 | unique((cycle_net |> filter(highway == "residential"))$maxspeed) 119 | print("Original maxspeed for service:") 120 | unique((cycle_net |> filter(highway == "service"))$maxspeed) 121 | 122 | cycle_net_speeds = clean_speeds(cycle_net) 123 | unique((cycle_net_speeds |> filter(highway == "residential"))$maxspeed_clean) 124 | unique((cycle_net_speeds |> filter(highway == "service"))$maxspeed_clean) 125 | 126 | cycle_net_volumes = estimate_traffic(cycle_net_speeds) 127 | cycle_net$`Speed Limit (mph)` = classify_speeds(cycle_net_speeds$maxspeed_clean) 128 | cycle_net$AADT = npt_to_cbd_aadt_numeric(cycle_net_volumes$assumed_volume) 129 | table(cycle_net$`Speed Limit (mph)`) 130 | table(cycle_net$AADT) 131 | ``` 132 | 133 | 134 | ```{r} 135 | #| eval: false 136 | #| echo: false 137 | # table(los_table_long$`Speed Limit (mph)`) 138 | # <20 mph 20 mph 30 mph 40 mph 50 mph 60+ mph 139 | # 12 16 12 8 8 4 140 | cycle_net_speeds = clean_speeds(cycle_net) 141 | # table(cycle_net_speeds$maxspeed_clean) 142 | # 15 20 30 40 50 60 143 | # 1 314 239 92 2 22 144 | classify_speeds = function(speed_mph) { 145 | dplyr::case_when( 146 | speed_mph < 20 ~ "<20 mph", 147 | speed_mph < 30 ~ "20 mph", 148 | speed_mph < 40 ~ "30 mph", 149 | speed_mph < 50 ~ "40 mph", 150 | speed_mph < 60 ~ "50 mph", 151 | speed_mph >= 60 ~ "60+ mph" 152 | ) 153 | } 154 | classify_speeds(30) 155 | classify_speeds(10) 156 | classify_speeds(NA) 157 | ``` 158 | 159 | ```{r minimal_plot_osm} 160 | m = plot_osm_tmap(cycle_net) 161 | m 162 | ``` 163 | 164 | Let's try running the code on a larger example, covering Edinburgh: 165 | 166 | but let's solve speed NA problem in cycle_net first: 167 | 168 | ```{r} 169 | devtools::load_all() # load minimal exmaple cycle_net_f and drive_net_f 170 | cycle_net = cycle_net_f 171 | drive_net = drive_net_f 172 | mapview::mapview(cycle_net, color = "blue") + 173 | mapview::mapview(drive_net, color = "red") 174 | 175 | cycle_net$maxspeed 176 | # Stplanr function: 177 | stplanr::line_bearing(cycle_net) 178 | # All values between -90 and 90 degrees 179 | stplanr::line_bearing(cycle_net, bidirectional = TRUE) 180 | 181 | # TODO: check implementations 182 | find_nearby_speeds = function(cycle_net, drive_net, buffer_dist = 10, angle_threshold = 20) { 183 | infra_no_maxspeed = cycle_net |> 184 | filter(is.na(maxspeed)) 185 | 186 | infra_no_maxspeed$azimuth_cycle = 187 | stplanr::line_bearing(infra_no_maxspeed, bidirectional = TRUE) 188 | 189 | # This should be a small datasets so OK to buffer: 190 | infra_no_maxspeed_buffer = sf::st_buffer(infra_no_maxspeed, dist = buffer_dist) 191 | 192 | roads_with_maxspeed = drive_net |> 193 | filter(!is.na(maxspeed)) 194 | 195 | roads_with_maxspeed$azimuth_road = 196 | stplanr::line_bearing(roads_with_maxspeed, bidirectional = TRUE) 197 | 198 | roads_with_maxspeed_points = sf::st_point_on_surface(roads_with_maxspeed) 199 | infra_no_maxspeed_nearby = infra_no_maxspeed_buffer[roads_with_maxspeed_points, ] 200 | 201 | joined_data = sf::st_join( 202 | infra_no_maxspeed_nearby |> 203 | dplyr::select(osm_id, azimuth_cycle), 204 | roads_with_maxspeed_points |> 205 | dplyr::select(maxspeed, azimuth_road), 206 | join = sf::st_intersects 207 | ) 208 | 209 | joined_data_clean = joined_data |> 210 | sf::st_drop_geometry() |> 211 | mutate( 212 | angle_diff = abs(azimuth_cycle - azimuth_road), 213 | maxspeed_numeric = gsub(maxspeed, pattern = " mph", replacement = "") |> 214 | as.numeric() 215 | ) |> 216 | filter(angle_diff < angle_threshold) |> 217 | group_by(osm_id) |> 218 | dplyr::summarise( 219 | maxspeed_new = median(maxspeed_numeric, na.rm = TRUE) |> 220 | paste0(" mph") 221 | ) |> 222 | ungroup() 223 | 224 | cycle_net_joined = left_join( 225 | cycle_net, 226 | joined_data_clean |> 227 | dplyr::select(osm_id, maxspeed_new), 228 | ) |> 229 | mutate( 230 | maxspeed = case_when( 231 | is.na(maxspeed) ~ maxspeed_new, 232 | TRUE ~ maxspeed 233 | ) 234 | ) |> 235 | select(-maxspeed_new) 236 | 237 | 238 | return(cycle_net_joined) 239 | } 240 | 241 | cycle_net_f_updated = find_nearby_speeds(cycle_net_f, drive_net_f) 242 | 243 | mapview(cycle_net_f_updated, zcol = "maxspeed") 244 | 245 | 246 | # Number of rows with NA maxspeed, before: 247 | sum(is.na(cycle_net_f$maxspeed)) 248 | sum(is.na(cycle_net_f_updated$maxspeed)) 249 | # We've reduced the number of NAs from 28 to 20 in the test dataset 250 | ``` 251 | 252 | ```{r} 253 | # testing new get_parallel_values() function 254 | devtools::load_all() 255 | target_net = cycle_net_f 256 | source_net = drive_net_f 257 | cycle_net_f_updated2 = get_parallel_values( 258 | cycle_net, 259 | drive_net, 260 | buffer_dist = 10, 261 | angle_threshold = 20 262 | ) 263 | 264 | # Check the number of NAs after imputation 265 | sum(is.na(cycle_net_f_updated2$maxspeed)) 266 | # Check the number of NAs before imputation 267 | sum(is.na(cycle_net_f$maxspeed)) 268 | ``` 269 | 270 | 271 | ```{r edinburgh} 272 | osm = get_travel_network("Edinburgh") 273 | cycle_net = get_cycling_network(osm) 274 | drive_net = get_driving_network(osm) 275 | 276 | joined_data = find_nearby_speeds(cycle_net, drive_net) 277 | cycle_net = update_missing_speeds(cycle_net, joined_data) 278 | 279 | mapview(cycle_net[central_edinburgh, ], zcol = "maxspeed") 280 | 281 | cycle_net = distance_to_road(cycle_net, drive_net) 282 | cycle_net = classify_cycle_infrastructure(cycle_net) 283 | mapview::mapview(cycle_net |>select(geometry, cycle_segregation), zcol = "cycle_segregation") 284 | central_edinburgh = osm_edinburgh |> 285 | sf::st_union() |> 286 | sf::st_centroid() |> 287 | sf::st_buffer(3000) 288 | m = plot_osm_tmap(cycle_net[central_edinburgh, ]) 289 | m 290 | ``` 291 | 292 | We can pick out specific bits of infrastructure, e.g. those mentioned in issue [#67](https://github.com/nptscot/osmactive/issues/67): 293 | 294 | > https://www.openstreetmap.org/way/1137759929 shows up as this right now, but this looks like a narrow segregated / stepped track to me. https://www.openstreetmap.org/way/1209807768 is another case that doesn't seem like off-road. 295 | 296 | > My idea of off-road cycleway were things like https://www.openstreetmap.org/way/7973833 and https://www.openstreetmap.org/way/265320503 -- things with no parallel road nearby. 297 | 298 | Let's pull out those bits of infrastructure: 299 | 300 | ```{r} 301 | cycle_net_included = cycle_net |> 302 | subset(osm_id %in% c(1137759929, 1209807768, 7973833, 265320503)) 303 | names(cycle_net_included) 304 | cycle_net_included[c("osm_id", "distance_to_road", "cycle_segregation")] |> 305 | sf::st_drop_geometry() |> 306 | knitr::kable() 307 | ``` 308 | 309 | We can also plot these as follows: 310 | 311 | ```{r} 312 | m = plot_osm_tmap(cycle_net_included) 313 | m 314 | ``` 315 | 316 | ## Tests for Leith Walk 317 | 318 | ```{r} 319 | drive_net_speeds = clean_speeds(drive_net) 320 | # Check for Leith Walk 321 | drive_net_speeds |> 322 | filter(name == "Leith Walk") |> 323 | select(matches("speed")) 324 | ``` 325 | 326 | ```{r} 327 | # devtools::load_all() 328 | cycle_net_los = level_of_service(cycle_net) 329 | # |> 330 | # select(osm_id, `Level of Service`, `Speed Limit (mph)`, AADT, cycle_segregation) 331 | table(cycle_net_los$`Level of Service`) 332 | # Count NA values: 333 | cycle_net_los |> 334 | sf::st_drop_geometry() |> 335 | group_by(`Speed Limit (mph)`, AADT, cycle_segregation) |> 336 | summarise(na = sum(is.na(`Level of Service`))) |> 337 | filter(na > 0) |> 338 | arrange(desc(na)) 339 | cycle_net_los_plot = cycle_net_los |> 340 | select(osm_id, `Speed Limit (mph)`, AADT, cycle_segregation, `Level of Service`) 341 | mapview::mapview(cycle_net_los, zcol = "Level of Service") 342 | ``` 343 | 344 | ```{r} 345 | #| eval: false 346 | #| echo: false 347 | # Try for Leeds 348 | devtools::load_all() 349 | osm = get_travel_network("Leeds") 350 | cycle_net = get_cycling_network(osm) 351 | drive_net = get_driving_network(osm) 352 | cycle_net = distance_to_road(cycle_net, drive_net) 353 | cycle_net = classify_cycle_infrastructure(cycle_net, include_mixed_traffic = TRUE) 354 | # Add aadt and speed limit 355 | cycle_net_speeds = clean_speeds(cycle_net) 356 | cycle_net = estimate_traffic(cycle_net_speeds) 357 | # Classify volumes: 358 | cycle_net$AADT = npt_to_cbd_aadt_numeric(cycle_net$assumed_volume) 359 | cycle_net$`Speed Limit (mph)` = classify_speeds(cycle_net_speeds$maxspeed_clean) 360 | names(cycle_net_speeds) 361 | cycle_net_los = level_of_service(cycle_net) 362 | # mapview::mapview(cycle_net_los, zcol = "Level of Service") 363 | table(cycle_net_los$`Level of Service`) 364 | 365 | chapeltown_area = zonebuilder::zb_zone("Chapeltown, Leeds", n_circles = 3) 366 | cycle_net_los_ctown = cycle_net_los |> 367 | sf::st_filter(chapeltown_area) |> 368 | select(osm_id, highway, `Speed Limit (mph)`, AADT, cycle_segregation, `Level of Service`) 369 | nrow(cycle_net_los_ctown) 370 | mapview::mapview(cycle_net_los_ctown, zcol = "Level of Service") 371 | # Check non-compliant ones 372 | cycle_net_los_ctown |> 373 | filter(`Level of Service` == "Should not be used (non-compliant intervention)") |> 374 | mapview::mapview(zcol = "Level of Service") 375 | ``` 376 | 377 | ```{r} 378 | #| eval: false 379 | #| echo: false 380 | # check the clean_speeds function 381 | library(dplyr) 382 | library(sf) 383 | 384 | cycle_net_df = st_drop_geometry(cycle_net) 385 | 386 | # Summarize the original data (rows with missing maxspeed) 387 | original_na_summary = cycle_net_df |> 388 | filter(is.na(maxspeed)) |> 389 | group_by(highway) |> 390 | summarise(count_before = n()) 391 | 392 | # Filter rows with missing maxspeed and apply the cleaning function 393 | cycle_net_na = cycle_net_df |> filter(is.na(maxspeed)) 394 | unique(cycle_net_speeds$highway) 395 | # [1] "residential" "tertiary" "service" "secondary" 396 | # [5] "unclassified" "footway" "primary" "trunk" 397 | # [9] "cycleway" "pedestrian" "path" "living_street" 398 | # [13] "trunk_link" "secondary_link" "primary_link" "tertiary_link" 399 | # [17] "busway" "razed" 400 | cycle_net_speeds = clean_speeds(cycle_net_na) 401 | 402 | # Drop geometry from the cleaned data as well 403 | cycle_net_speeds_df = st_drop_geometry(cycle_net_speeds) 404 | 405 | # Summarize the cleaned data, using maxspeed_clean (the updated speed) 406 | cleaned_summary = cycle_net_speeds_df |> 407 | group_by(highway) |> 408 | summarise( 409 | updated_speed = first(maxspeed_clean), 410 | count_after = n() 411 | ) 412 | 413 | # Join the before and after summaries to compare 414 | comparison = left_join(original_na_summary, cleaned_summary, by = "highway") 415 | 416 | print(comparison) 417 | ``` 418 | 419 | -------------------------------------------------------------------------------- /vignettes/gisruk2025.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | # conference: GISRUK 2025 3 | title: "Mapping, classifying, and integrating diverse street network datasets: new methods and open source tools for active travel planning" 4 | author: 5 | - name: Robin Lovelace 6 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 7 | orcid: 0000-0001-5679-6536 8 | - name: Zhao Wang 9 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 10 | orcid: 0000-0002-4054-0533 11 | - name: Hussein Mahfouz 12 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 13 | orcid: 0000-0002-6043-8616 14 | - name: Juan Pablo Fonseca Zamora 15 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 16 | orcid: 0009-0006-7042-3828 17 | - name: Angus Calder 18 | affiliation: Sustrans Scotland 19 | - name: Martin Lucas-Smith 20 | affiliation: CycleStreets Ltd, Cambridge, UK 21 | - name: Dustin Carlino 22 | affiliation: Alan Turing Institute, London, UK 23 | orcid: 0000-0002-5565-0989 24 | - name: Josiah Parry 25 | affiliation: Environmental Systems Research Institute (Esri), Redlands, CA, USA 26 | orcid: 0000-0001-9910-865X 27 | - name: Rosa Félix 28 | affiliation: University of Lisbon 29 | orcid: 0000-0002-5642-6006 30 | # abstract: | 31 | # Evidence on street networks and their potential changes under future scenarios is crucial for active travel planning. However, most active travel models rely on oversimplified street representations, using single variables like 'quietness', neglecting key factors such as footway widths. This paper introduces new methods for classifying street networks for active travel, integrating diverse datasets from OpenStreetMap and official sources. Implemented in open source software packages `osmactive` and `anime`, these methods are scalable and reproducible. The results are showcased in a web application hosted at www.npt.scot, demonstrating how geographic data science can drive high-impact research. 32 | # # implemented in open source software packages including `osmactive` R package and the `anime` Rust crate 33 | # keywords: [network analysis, transport planning, OpenStreetMap, active travel, reproducible research] 34 | # execute: 35 | # echo: false 36 | # message: false 37 | # warning: false 38 | # eval: false 39 | # # format: gfm 40 | # # quarto add sdesabbata/quarto-gisruk 41 | # format: 42 | # gisruk-pdf: 43 | # keep-tex: true 44 | # bibliography: references.bib 45 | output: rmarkdown::html_vignette 46 | vignette: > 47 | %\VignetteIndexEntry{Mapping, classifying, and integrating diverse street network datasets: new methods and open source tools for active travel planning} 48 | %\VignetteEngine{knitr::rmarkdown} 49 | %\VignetteEncoding{UTF-8} 50 | --- 51 | 52 | 53 | 54 | 55 | 56 | ```{r} 57 | #| echo: false 58 | knitr::opts_chunk$set(eval = FALSE) 59 | ``` 60 | 61 | ```{r} 62 | #| include: false 63 | # install pak if not installed: 64 | if (!requireNamespace("pak", quietly = TRUE)) { 65 | install.packages("pak") 66 | } 67 | pak::pkg_install("tidyverse", ask = FALSE) 68 | 69 | github_pkgs = c( 70 | "nptscot/osmactive", 71 | "josiahparry/anime/r" 72 | ) 73 | pak::pkg_install(github_pkgs, ask = FALSE) 74 | library(tidyverse) 75 | library(osmactive) 76 | library(tmap) 77 | ``` 78 | 79 | 80 | 81 | 82 | 83 | ```{r} 84 | #| eval: false 85 | #| echo: false 86 | quarto::quarto_render("os_blog_post.qmd", output_format = "docx") 87 | d = list.dirs("~/..") 88 | d_onedrive = d[grep("OneDrive", d)] 89 | list.files("~/OneDrive/projects-all/atumscotland/phase3") 90 | # file.copy("os_blog_post.docx", "~/OneDrive/projects-all/atumscotland/phase3", overwrite = TRUE) 91 | list.files( "C:\\Users\\georl_admin\\OneDrive - University of Leeds\\projects-all\\atumscotland\\phase3") 92 | file.copy("os_blog_post.docx", "C:\\Users\\georl_admin\\OneDrive - University of Leeds\\projects-all\\atumscotland\\phase3", overwrite = TRUE) 93 | # Upload to gh 94 | quarto::quarto_render("gisruk2025.qmd") 95 | system("gh release upload v0.1 gisruk2025.pdf --clobber") 96 | ``` 97 | 98 | # Published versions 99 | 100 | See the [pdf version](https://github.com/nptscot/osmactive/releases/download/v0.1/gisruk2025.pdf) and [html version](https://nptscot.github.io/osmactive/articles/gisruk2025.html) at [nptscot.github.io/osmactive](https://nptscot.github.io/osmactive/articles/gisruk2025.html). 101 | 102 | 103 | # Introduction 104 | 105 | Active travel is an accessible and cost-effective way to replace short car trips and make transport systems more sustainable. 106 | The Network Planning Tool (NPT) for Scotland is a new web-based strategic network planning tool that estimates cycling potential down to the street level. 107 | The NPT builds on the functionality of the Propensity to Cycle Tool (PCT) and related tools [@lovelace2017; @goodman2019; @lovelace2024; @félix2025], offering a detailed nationwide cycling potential analysis for Scotland. 108 | The NPT is funded by Transport Scotland and developed by the University of Leeds in collaboration with Sustrans Scotland. 109 | The tool is open-source and hosted at [github.come/nptscot](https://github.com/nptscot), enabling others to learn from and contribute to the underlying methods to help make transport planning more open and participatory [@lovelace2020a]. 110 | 111 | A unique feature of the NPT is its integration of multiple layers into a single tool, overcoming limitations of previous strategic network planning tools, which generally focus either on behaviour data (such as the PCT) or physical infrastructure [@vybornova2024; @vierø2024]. 112 | The NPT brings together more than a dozen datasets, including Ordnance Survey Mastermap and OpenStreetmap data products to provide comprehensive data on the potential for mode shift, infrastructure, modelled motor traffic levels, and street space at the network level. 113 | This paper presents new geographic methods we developed to support this work, with reference to reproducible code that use the new `osmactive` R package for classifying national-scale OpenStreetMap (OSM) datasets based on attributes and geographic relationships, and the `anime` Rust crate for astonishingly fast and accurate route network data integration. 114 | 115 | # Datasets and methods 116 | 117 | The NPT uses datasets from diverse sources including from the DfT's network of traffic sensors, motor traffic counts based a survey evaluating Edinburgh's roll-out of 20 mph speed limits, the National Travel Survey, and the Scottish Household Travel Survey. 118 | We used four key datasets representing the road network for this project: 119 | 120 | - Ordnance Survey OpenRoads, an open access and simplified representation of the road network that is ideal for visualisation 121 | - OS MasterMap Highways, a more detailed dataset that includes information on road widths and other features 122 | - OS Mastermap Topography, the most detailed vector dataset that includes detailed information on the geometry of many features of the man-made environment, including curb lines. In the OS Topo layer roads and other pieces of transport infrastructure such as footways (pavements) are represented not as corridor or lane centerlines, but as polygons. 123 | - OpenStreetMap, a crowdsourced dataset that is continuously updated by community volunteers. The dataset is rich, with tags for width and smoothness of infrastructure, but is not as consistent as the OS datasets. 124 | 125 | Ordnance Survey's MasterMap Highways and Topography datasets provide unparalleled accuracy and detail, but are a struggle to import using consumer-grade hardware and standard tools such as QGIS. 126 | To overcome this issue we developed [the `mastermapr` package](https://github.com/acteng/mastermapr) in collaboration with the government agency Active Travel England (ATE), to efficiently and flexibly imports MasterMap datasets (50 GB compressed). 127 | After importing the four route network datasets outlined above, we integrated them using the following steps: 128 | 129 | - Pavement widths were calculated from the OS MasterMap Topography dataset using the function `get_pavement_widths()` from the `osmactive` package. This function calculates the width of manmade roadside features associated with each road segment by dividing the area of matching polygons associated with each road segment by the length of the road segment. 130 | - The `get_bus_routes()` function from the `osmactive` package was used to determine the number of bus lanes on each road segment. 131 | - We used the `anime` Rust crate to efficiently join the networks, based on alignment and proximity. The use of spatial indexes makes the implementation highly efficient compared with the [`rnet_join()`](https://docs.ropensci.org/stplanr/reference/rnet_join.html) function in the R package `stplanr` that we were using previously. See the [josiahparry/ANIME codebase on GitHub](https://github.com/JosiahParry/anime) for details of the new algorithm and its implementation as an R (and planned Python) interface to the new [`anime` Rust crate](https://crates.io/crates/anime). 132 | 133 | ## Classifying OpenStreetNetwork ways 134 | 135 | Functions including `get_travel_network()` and `classify_cycle_infrastructure()` from the `osmactive` package were used to classify cycle infrastructure types based on the presence of cycle lanes, tracks, and other features (see the code snippet which generates @fig-bristol below). 136 | 137 | ```{r} 138 | #| label: bristol 139 | #| eval: false 140 | #| echo: true 141 | osm = get_travel_network("bristol") 142 | cycle_net = get_cycling_network(osm) 143 | drive_net = get_driving_network(osm) 144 | cycle_net = distance_to_road(cycle_net, drive_net) 145 | cycle_net = classify_cycle_infrastructure(cycle_net) 146 | plot_osm_tmap(cycle_net) 147 | ``` 148 | 149 | 150 | 151 | ```{r} 152 | #| include: false 153 | #| eval: false 154 | m = plot_osm_tmap(cycle_net) 155 | m + 156 | tm_layout(legend.position = c("right", "top")) 157 | m 158 | tmap_save(m, "cycle_net_bristol.html") 159 | browseURL("cycle_net_bristol.html") 160 | system("gh release upload v0.1 cycle_net_bristol.html --clobber") 161 | # Available: 162 | # https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_bristol.html 163 | ``` 164 | 165 | ![Cycle infrastructre and painted cycle lanes in Bristol, generated with 5 lines of code using the osmactive package. See interactive version online at [github.com/nptscot/osmactive/releases](https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_bristol.html).](images/paste-1.png){#fig-bristol} 166 | 167 | ## Road width measurements 168 | 169 | Two key measurements are needed to assess whether existing roads can accommodate cycle infrastructure: carriageway width and corridor width, as defined below. 170 | 171 | Accurate and available carriageway width measurements are important because they determine if proposed infrastructure can fit solely within the carriageway, without the need for moving curbs or other roadside features. 172 | The Scottish Cycling by Design (CbD) guidance document outlines three types of cycle infrastructure that run along the carriageway, building on Department for Transport guidance [@departmentfortransport2020]: 173 | 174 | - Cycle track at carriageway level 175 | - Stepped cycle track 176 | - Cycle track at footway level 177 | 178 | The first option, cycle track that is at carriage level, has additional advantages over stepped cycleway, being often cheaper to construct [with no new tarmac required in many cases](https://www.transport.gov.scot/media/50323/cycling-by-design-update-2019-final-document-15-september-2021-1.pdf#page=76) but requires sufficient width on the existing carriageway to accommodate the cycle infrastructure. 179 | 180 | Corridor width captures the carriageway plus any built roadside features, such as footways. 181 | The following [`dplyr` query](https://dplyr.tidyverse.org/) was used to extract the width of manmade roadside features: `filter(descriptive_group == "Roadside", make == "Manmade")`. 182 | From that point, we calculated the width of pavements associated with each road segment by dividing the area of matching polygons associated with each road segment by the length of the road segment, as implemented in the `get_pavement_widths()` function in the `osmactive` package. 183 | 184 | ## Minimum cycle track and buffer widths 185 | 186 | The corridor width is important because it determines whether, in cases where there is insufficient space on the carriageway, part of the footway or other manmade roadside features may be reallocated for cycle infrastructure, while still maintaining recommended minimum widths for pedestrians. 187 | CbD guidance on widths is summarised in table @tbl-cbd. 188 | 189 | | Cycle infrastructure type | Absolute minimum width | Desirable minimum width | 190 | |--------------------------|-----------------------|------------------------| 191 | | Unidirectional cycle track | 1.5 m | 2.0 m | 192 | | Bidirectional cycle track | 2.0 m | 3.0 m | 193 | 194 | : Table illustrating the minimum width requirements for cycle infrastructure according to the Cycling by Design (CbD) guidance. {#tbl-cbd} 195 | 196 | CbD also specifies buffers that must be accounted for when calculating the effective available width for cycle infrastructure (@tbl-buffers). 197 | 198 | | Road type / Speed limit | Buffer width | 199 | |-------------------------|--------------| 200 | | 30 mph | 0.5 m | 201 | | 40 mph | 1.0 m | 202 | | 50 mph | 2.0 m | 203 | | 60 mph | 2.5 m | 204 | | 70 mph | 3.5 m | 205 | 206 | : Table illustrating the buffer widths for cycle infrastructure based on road type and speed limit according to the Cycling by Design (CbD) guidance. {#tbl-buffers} 207 | 208 | Speed limit data taken from OSM was used to determine the buffer width for each road segment. 209 | 210 | ## Bus routes and road traffic assumptions 211 | 212 | The minimum space requirements for motor traffic depend on the uses of the road, including whether it is a bus route and whether there are dedicated bus lanes. 213 | Based on the Active Travel England cross section check tool, part of [Active Travel England's open access Excel-based design tools](https://www.gov.uk/government/publications/active-travel-england-design-assistance-tools) (see [acteng.github.io/inspectorate_tools/](https://acteng.github.io/inspectorate_tools/) for web-based versions), we assumed the following widths for motor traffic: 214 | 215 | - Non-bus routes: 2 × 2.75 m 216 | - Bus routes without dedicated bus lanes: 2 × 3.2 m 217 | - Bus routes with dedicated bus lanes: 2 × 3.2 m plus an additional space of `n_bus_lanes` × 3.2 m for the dedicated bus lanes. 218 | 219 | 220 | 221 | The number of bus lanes was determined from the OSM data with the function [`get_bus_routes()`](https://nptscot.github.io/osmactive/reference/get_bus_routes.html) in the R package `osmactive` that we developed for this project. 222 | The results are illustrated in @fig-edinburgh-bus-lanes, which can also be reproduced with the following [Overpass Turbo query](https://overpass-turbo.eu/s/1Xaf): 223 | 224 | ``` 225 | relation["route"="bus"]({{bbox}}); 226 | ``` 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | # Results 237 | 238 | The result of the data engineering, data integration, modelling and visualisation steps described above is datasets that are visualised interactively in the NPT web application, using `pmtiles` for efficient rendering of large datasets [@gonçalves2023]. 239 | The key components of the NPT are: 240 | 241 | - The "Route network" layer, presenting data on cycling potential on OSM-based and simplified [@lovelace_reproducible_2024] network layers. 242 | - The "Infrastructure and traffic" layer, which includes data on motor traffic levels and cycle infrastructure. 243 | - The "Street space" layer, which categorises roads in accordance with the Cycling by Design guidance, specifying the methodology for classifying road spaces and cycle infrastructure, presented in @fig-street-space. 244 | - The "Core network" layer, representing a cohesive network that could be a priority when planning new infrastructure, building on previous work [@szell2021]. 245 | - The [Network Planning Workspace (NPW)](https://nptscot.github.io/npw/) web application, which allows users to explore the data and create custom scenarios of infrastructure change. 246 | 247 | 248 | 249 | 250 | 251 | # Conclusion 252 | 253 | The Network Planning Tool for Scotland is a cutting-edge web application designed for strategic cycle network planning. 254 | A unique feature of the NPT is its integration of multiple layers into a single tool, overcoming limitations of previous strategic network planning tools, which generally focus either on behaviour data or physical infrastructure. 255 | The new street space layer represents a step change in access to combined carriageway and corridor widths, the first time this data has been made available at a national level, to the best of our knowledge. 256 | 257 | In future work we plan to improve the NPT by incorporating new datasets from a variety of sources, including [Scotland's Spatial Hub](https://data.spatialhub.scot). 258 | We would like to develop context-specific classifications in the `osmactive` package to better address the diverse needs of different urban environments and support the roll-out the methods in new places. 259 | 260 | # References 261 | 262 | ::: {#refs} 263 | ::: 264 | 265 | # Acknowledgements {.appendix .unnumbered} 266 | 267 | 268 | 269 | Thanks to Transport Scotland for funding the development of the Network Planning Tool for Scotland, and to the many users who have provided feedback on the tool. 270 | Thanks to Sustrans Scotland, and Matt Davis and Angus Calder in particular, for collaborating on the project. 271 | 272 | Thanks to the OpenStreetMap community for creating and maintaining the data that underpins the NPT. 273 | Thanks to Ordnance Survey for providing the MasterMap data that underpins the NPT (contains OS data © Crown copyright and database rights 2025 OS licence number 100046668). 274 | 275 | # Biographies {.appendix .unnumbered} 276 | 277 | 278 | 279 | Robin Lovelace is Professor of Transport Data Science at the Leeds Institute for Transport Studies (ITS) and specialises in data science and geocomputation, with a focus on modeling transport systems, active travel, and decarbonisation. 280 | 281 | Zhao Wang is a researcher at the Leeds Institute for Transport Studies (ITS). 282 | Zhao specializes in machine learning, data science and geocomputation for transport planning and engineering. 283 | 284 | Hussein Mahfouz is a PhD student at the Leeds Institute for Transport Studies (ITS) and specialises in data science and geocomputation for transport planning and engineering, with a focus on Demand Responsive Transport (DRT). 285 | 286 | Juan Pablo Fonseca Zamora is a PhD student at the Leeds Institute for Transport Studies (ITS) and specialises in new methods for traffic modelling. 287 | 288 | Angus Calder is a Senior Mobility Planner at Sustrans Scotland and specialises in active travel planning and infrastructure design. 289 | 290 | Martin Lucas-Smith is a Director at CycleStreets Ltd and specialises in open data and open source software for cycling. 291 | 292 | Dustin Carlino is an independent researcher and software developer, specialising in open source software development and geographic data science. 293 | 294 | Josiah Parry is a Senior Product Engineer at Environmental Systems Research Institute, Inc (Esri) and an open source software developer. 295 | 296 | Rosa Félix is a researcher at the University of Lisbon and specialises geographic methods for transport planning, with a focus on active travel and decarbonisation. 297 | 298 | ```{=html} 299 | 308 | ``` 309 | -------------------------------------------------------------------------------- /data-raw/gisruk2025.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | conference: GISRUK 2025 3 | title: "Mapping, classifying, and integrating diverse street network datasets: new methods and open source tools for active travel planning" 4 | author: 5 | - name: Robin Lovelace 6 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 7 | orcid: 0000-0001-5679-6536 8 | - name: Zhao Wang 9 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 10 | orcid: 0000-0002-4054-0533 11 | - name: Hussein Mahfouz 12 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 13 | orcid: 0000-0002-6043-8616 14 | - name: Juan Pablo Fonseca Zamora 15 | affiliation: Leeds Institute for Transport Studies, University of Leeds, UK 16 | orcid: 0009-0006-7042-3828 17 | - name: Angus Calder 18 | affiliation: Sustrans Scotland 19 | - name: Martin Lucas-Smith 20 | affiliation: CycleStreets Ltd, Cambridge, UK 21 | - name: Dustin Carlino 22 | affiliation: Alan Turing Institute, London, UK 23 | orcid: 0000-0002-5565-0989 24 | - name: Josiah Parry 25 | affiliation: Environmental Systems Research Institute (Esri), Redlands, CA, USA 26 | orcid: 0000-0001-9910-865X 27 | - name: Rosa Félix 28 | affiliation: University of Lisbon 29 | orcid: 0000-0002-5642-6006 30 | abstract: | 31 | Evidence on street networks and their potential changes under future scenarios is crucial for active travel planning. However, most active travel models rely on oversimplified street representations, using single variables like 'quietness', neglecting key factors such as footway widths. This paper introduces new methods for classifying street networks for active travel, integrating diverse datasets from OpenStreetMap and official sources. Implemented in open source software packages `osmactive` and `anime`, these methods are scalable and reproducible. The results are showcased in a web application hosted at www.npt.scot, demonstrating how geographic data science can drive high-impact research. 32 | # implemented in open source software packages including `osmactive` R package and the `anime` Rust crate 33 | keywords: [network analysis, transport planning, OpenStreetMap, active travel, reproducible research] 34 | execute: 35 | echo: false 36 | message: false 37 | warning: false 38 | # eval: false 39 | # format: gfm 40 | # quarto add sdesabbata/quarto-gisruk 41 | format: 42 | gisruk-pdf: 43 | keep-tex: true 44 | bibliography: references.bib 45 | --- 46 | 47 | ```{=html} 48 | 51 | ``` 52 | 53 | 54 | 55 | 56 | 57 | ```{r} 58 | #| include: false 59 | # install pak if not installed: 60 | if (!requireNamespace("pak", quietly = TRUE)) { 61 | install.packages("pak") 62 | } 63 | pak::pkg_install("tidyverse", ask = FALSE) 64 | 65 | github_pkgs = c( 66 | "nptscot/osmactive", 67 | "josiahparry/anime/r" 68 | ) 69 | pak::pkg_install(github_pkgs, ask = FALSE) 70 | library(tidyverse) 71 | library(osmactive) 72 | library(tmap) 73 | ``` 74 | 75 | 76 | 77 | 78 | 79 | ```{r} 80 | #| eval: false 81 | #| echo: false 82 | quarto::quarto_render("os_blog_post.qmd", output_format = "docx") 83 | d = list.dirs("~/..") 84 | d_onedrive = d[grep("OneDrive", d)] 85 | list.files("~/OneDrive/projects-all/atumscotland/phase3") 86 | # file.copy("os_blog_post.docx", "~/OneDrive/projects-all/atumscotland/phase3", overwrite = TRUE) 87 | list.files( "C:\\Users\\georl_admin\\OneDrive - University of Leeds\\projects-all\\atumscotland\\phase3") 88 | file.copy("os_blog_post.docx", "C:\\Users\\georl_admin\\OneDrive - University of Leeds\\projects-all\\atumscotland\\phase3", overwrite = TRUE) 89 | # Upload to gh 90 | quarto::quarto_render("gisruk2025.qmd") 91 | system("gh release upload v0.1 gisruk2025.pdf --clobber") 92 | ``` 93 | 94 | 95 | 96 | ```{=html} 97 | 129 | ``` 130 | 131 | ```{=html} 132 | 147 | ``` 148 | 149 | ```{=html} 150 | 218 | ``` 219 | 220 | {{< pagebreak >}} 221 | 222 | # Introduction 223 | 224 | Active travel is an accessible and cost-effective way to replace short car trips and make transport systems more sustainable. 225 | The Network Planning Tool (NPT) for Scotland is a new web-based strategic network planning tool that estimates cycling potential down to the street level. 226 | The NPT builds on the functionality of the Propensity to Cycle Tool (PCT) and related tools [@lovelace2017; @goodman2019; @lovelace2024; @félix2025], offering a detailed nationwide cycling potential analysis for Scotland. 227 | The NPT is funded by Transport Scotland and developed by the University of Leeds in collaboration with Sustrans Scotland. 228 | The tool is open-source and hosted at [github.come/nptscot](https://github.com/nptscot), enabling others to learn from and contribute to the underlying methods to help make transport planning more open and participatory [@lovelace2020a]. 229 | 230 | A unique feature of the NPT is its integration of multiple layers into a single tool, overcoming limitations of previous strategic network planning tools, which generally focus either on behaviour data (such as the PCT) or physical infrastructure [@vybornova2024; @vierø2024]. 231 | The NPT brings together more than a dozen datasets, including Ordnance Survey Mastermap and OpenStreetmap data products to provide comprehensive data on the potential for mode shift, infrastructure, modelled motor traffic levels, and street space at the network level. 232 | This paper presents new geographic methods we developed to support this work, with reference to reproducible code that use the new `osmactive` R package for classifying national-scale OpenStreetMap (OSM) datasets based on attributes and geographic relationships, and the `anime` Rust crate for astonishingly fast and accurate route network data integration. 233 | 234 | # Datasets and methods 235 | 236 | The NPT uses datasets from diverse sources including from the DfT's network of traffic sensors, motor traffic counts based a survey evaluating Edinburgh's roll-out of 20 mph speed limits, the National Travel Survey, and the Scottish Household Travel Survey. 237 | We used four key datasets representing the road network for this project: 238 | 239 | - Ordnance Survey OpenRoads, an open access and simplified representation of the road network that is ideal for visualisation 240 | - OS MasterMap Highways, a more detailed dataset that includes information on road widths and other features 241 | - OS Mastermap Topography, the most detailed vector dataset that includes detailed information on the geometry of many features of the man-made environment, including curb lines. In the OS Topo layer roads and other pieces of transport infrastructure such as footways (pavements) are represented not as corridor or lane centerlines, but as polygons. 242 | - OpenStreetMap, a crowdsourced dataset that is continuously updated by community volunteers. The dataset is rich, with tags for width and smoothness of infrastructure, but is not as consistent as the OS datasets. 243 | 244 | Ordnance Survey’s MasterMap Highways and Topography datasets provide unparalleled accuracy and detail, but are a struggle to import using consumer-grade hardware and standard tools such as QGIS. 245 | To overcome this issue we developed [the `mastermapr` package](https://github.com/acteng/mastermapr) in collaboration with the government agency Active Travel England (ATE), to efficiently and flexibly imports MasterMap datasets (50 GB compressed). 246 | After importing the four route network datasets outlined above, we integrated them using the following steps: 247 | 248 | - Pavement widths were calculated from the OS MasterMap Topography dataset using the function `get_pavement_widths()` from the `osmactive` package. This function calculates the width of manmade roadside features associated with each road segment by dividing the area of matching polygons associated with each road segment by the length of the road segment. 249 | - The `get_bus_routes()` function from the `osmactive` package was used to determine the number of bus lanes on each road segment. 250 | - We used the `anime` Rust crate to efficiently join the networks, based on alignment and proximity. The use of spatial indexes makes the implementation highly efficient compared with the [`rnet_join()`](https://docs.ropensci.org/stplanr/reference/rnet_join.html) function in the R package `stplanr` that we were using previously. See the [josiahparry/ANIME codebase on GitHub](https://github.com/JosiahParry/anime) for details of the new algorithm and its implementation as an R (and planned Python) interface to the new [`anime` Rust crate](https://crates.io/crates/anime). 251 | 252 | ## Classifying OpenStreetNetwork ways 253 | 254 | Functions including `get_travel_network()` and `classify_cycle_infrastructure()` from the `osmactive` package were used to classify cycle infrastructure types based on the presence of cycle lanes, tracks, and other features (see the code snippet which generates @fig-bristol below). 255 | 256 | ```{r} 257 | #| label: bristol 258 | #| eval: false 259 | #| echo: true 260 | osm = get_travel_network("bristol") 261 | cycle_net = get_cycling_network(osm) 262 | drive_net = get_driving_network(osm) 263 | cycle_net = distance_to_road(cycle_net, drive_net) 264 | cycle_net = classify_cycle_infrastructure(cycle_net) 265 | plot_osm_tmap(cycle_net) 266 | ``` 267 | 268 | 269 | 270 | ```{r} 271 | #| include: false 272 | #| eval: false 273 | m = plot_osm_tmap(cycle_net) 274 | m + 275 | tm_layout(legend.position = c("right", "top")) 276 | m 277 | tmap_save(m, "cycle_net_bristol.html") 278 | browseURL("cycle_net_bristol.html") 279 | system("gh release upload v0.1 cycle_net_bristol.html --clobber") 280 | # Available: 281 | # https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_bristol.html 282 | ``` 283 | 284 | ![Cycle infrastructre and painted cycle lanes in Bristol, generated with 5 lines of code using the osmactive package. See interactive version online at [github.com/nptscot/osmactive/releases](https://github.com/nptscot/osmactive/releases/download/v0.1/cycle_net_bristol.html).](images/paste-1.png){#fig-bristol} 285 | 286 | ## Road width measurements 287 | 288 | Two key measurements are needed to assess whether existing roads can accommodate cycle infrastructure: carriageway width and corridor width, as defined below. 289 | 290 | Accurate and available carriageway width measurements are important because they determine if proposed infrastructure can fit solely within the carriageway, without the need for moving curbs or other roadside features. 291 | The Scottish Cycling by Design (CbD) guidance document outlines three types of cycle infrastructure that run along the carriageway, building on Department for Transport guidance [@departmentfortransport2020]: 292 | 293 | - Cycle track at carriageway level 294 | - Stepped cycle track 295 | - Cycle track at footway level 296 | 297 | The first option, cycle track that is at carriage level, has additional advantages over stepped cycleway, being often cheaper to construct [with no new tarmac required in many cases](https://www.transport.gov.scot/media/50323/cycling-by-design-update-2019-final-document-15-september-2021-1.pdf#page=76) but requires sufficient width on the existing carriageway to accommodate the cycle infrastructure. 298 | 299 | Corridor width captures the carriageway plus any built roadside features, such as footways. 300 | The following [`dplyr` query](https://dplyr.tidyverse.org/) was used to extract the width of manmade roadside features: `filter(descriptive_group == "Roadside", make == "Manmade")`. 301 | From that point, we calculated the width of pavements associated with each road segment by dividing the area of matching polygons associated with each road segment by the length of the road segment, as implemented in the `get_pavement_widths()` function in the `osmactive` package. 302 | 303 | ## Minimum cycle track and buffer widths 304 | 305 | The corridor width is important because it determines whether, in cases where there is insufficient space on the carriageway, part of the footway or other manmade roadside features may be reallocated for cycle infrastructure, while still maintaining recommended minimum widths for pedestrians. 306 | CbD guidance on widths is summarised in table @tbl-cbd. 307 | 308 | | Cycle infrastructure type | Absolute minimum width | Desirable minimum width | 309 | |--------------------------|-----------------------|------------------------| 310 | | Unidirectional cycle track | 1.5 m | 2.0 m | 311 | | Bidirectional cycle track | 2.0 m | 3.0 m | 312 | 313 | : Table illustrating the minimum width requirements for cycle infrastructure according to the Cycling by Design (CbD) guidance. {#tbl-cbd} 314 | 315 | CbD also specifies buffers that must be accounted for when calculating the effective available width for cycle infrastructure (@tbl-buffers). 316 | 317 | | Road type / Speed limit | Buffer width | 318 | |-------------------------|--------------| 319 | | 30 mph | 0.5 m | 320 | | 40 mph | 1.0 m | 321 | | 50 mph | 2.0 m | 322 | | 60 mph | 2.5 m | 323 | | 70 mph | 3.5 m | 324 | 325 | : Table illustrating the buffer widths for cycle infrastructure based on road type and speed limit according to the Cycling by Design (CbD) guidance. {#tbl-buffers} 326 | 327 | Speed limit data taken from OSM was used to determine the buffer width for each road segment. 328 | 329 | ## Bus routes and road traffic assumptions 330 | 331 | The minimum space requirements for motor traffic depend on the uses of the road, including whether it is a bus route and whether there are dedicated bus lanes. 332 | Based on the Active Travel England cross section check tool, part of [Active Travel England's open access Excel-based design tools](https://www.gov.uk/government/publications/active-travel-england-design-assistance-tools) (see [acteng.github.io/inspectorate_tools/](https://acteng.github.io/inspectorate_tools/) for web-based versions), we assumed the following widths for motor traffic: 333 | 334 | - Non-bus routes: 2 × 2.75 m 335 | - Bus routes without dedicated bus lanes: 2 × 3.2 m 336 | - Bus routes with dedicated bus lanes: 2 × 3.2 m plus an additional space of `n_bus_lanes` × 3.2 m for the dedicated bus lanes. 337 | 338 | 339 | 340 | The number of bus lanes was determined from the OSM data with the function [`get_bus_routes()`](https://nptscot.github.io/osmactive/reference/get_bus_routes.html) in the R package `osmactive` that we developed for this project. 341 | The results are illustrated in @fig-edinburgh-bus-lanes, which can also be reproduced with the following [Overpass Turbo query](https://overpass-turbo.eu/s/1Xaf): 342 | 343 | ``` 344 | relation["route"="bus"]({{bbox}}); 345 | ``` 346 | 347 | 348 | 349 | 350 | 351 | ![Bus routes in Edinburgh, extracted from OpenStreetMap](edinburgh_bus_lanes.png){#fig-edinburgh-bus-lanes} 352 | 353 | 354 | 355 | # Results 356 | 357 | The result of the data engineering, data integration, modelling and visualisation steps described above is datasets that are visualised interactively in the NPT web application, using `pmtiles` for efficient rendering of large datasets [@gonçalves2023]. 358 | The key components of the NPT are: 359 | 360 | - The "Route network" layer, presenting data on cycling potential on OSM-based and simplified [@lovelace_reproducible_2024] network layers. 361 | - The "Infrastructure and traffic" layer, which includes data on motor traffic levels and cycle infrastructure. 362 | - The "Street space" layer, which categorises roads in accordance with the Cycling by Design guidance, specifying the methodology for classifying road spaces and cycle infrastructure, presented in @fig-street-space. 363 | - The "Core network" layer, representing a cohesive network that could be a priority when planning new infrastructure, building on previous work [@szell2021]. 364 | - The [Network Planning Workspace (NPW)](https://nptscot.github.io/npw/) web application, which allows users to explore the data and create custom scenarios of infrastructure change. 365 | 366 | ![Street Space layer in Network Planning Tool for Scotland.](street_space.png){#fig-street-space} 367 | 368 | 369 | 370 | # Conclusion 371 | 372 | The Network Planning Tool for Scotland is a cutting-edge web application designed for strategic cycle network planning. 373 | A unique feature of the NPT is its integration of multiple layers into a single tool, overcoming limitations of previous strategic network planning tools, which generally focus either on behaviour data or physical infrastructure. 374 | The new street space layer represents a step change in access to combined carriageway and corridor widths, the first time this data has been made available at a national level, to the best of our knowledge. 375 | 376 | In future work we plan to improve the NPT by incorporating new datasets from a variety of sources, including [Scotland's Spatial Hub](https://data.spatialhub.scot). 377 | We would like to develop context-specific classifications in the `osmactive` package to better address the diverse needs of different urban environments and support the roll-out the methods in new places. 378 | 379 | # References 380 | 381 | ::: {#refs} 382 | ::: 383 | 384 | # Acknowledgements {.appendix .unnumbered} 385 | 386 | 387 | 388 | Thanks to Transport Scotland for funding the development of the Network Planning Tool for Scotland, and to the many users who have provided feedback on the tool. 389 | Thanks to Sustrans Scotland, and Matt Davis and Angus Calder in particular, for collaborating on the project. 390 | 391 | Thanks to the OpenStreetMap community for creating and maintaining the data that underpins the NPT. 392 | Thanks to Ordnance Survey for providing the MasterMap data that underpins the NPT (contains OS data © Crown copyright and database rights 2025 OS licence number 100046668). 393 | 394 | # Biographies {.appendix .unnumbered} 395 | 396 | 397 | 398 | Robin Lovelace is Professor of Transport Data Science at the Leeds Institute for Transport Studies (ITS) and specialises in data science and geocomputation, with a focus on modeling transport systems, active travel, and decarbonisation. 399 | 400 | Zhao Wang is a researcher at the Leeds Institute for Transport Studies (ITS). 401 | Zhao specializes in machine learning, data science and geocomputation for transport planning and engineering. 402 | 403 | Hussein Mahfouz is a PhD student at the Leeds Institute for Transport Studies (ITS) and specialises in data science and geocomputation for transport planning and engineering, with a focus on Demand Responsive Transport (DRT). 404 | 405 | Juan Pablo Fonseca Zamora is a PhD student at the Leeds Institute for Transport Studies (ITS) and specialises in new methods for traffic modelling. 406 | 407 | Angus Calder is a Senior Mobility Planner at Sustrans Scotland and specialises in active travel planning and infrastructure design. 408 | 409 | Martin Lucas-Smith is a Director at CycleStreets Ltd and specialises in open data and open source software for cycling. 410 | 411 | Dustin Carlino is an independent researcher and software developer, specialising in open source software development and geographic data science. 412 | 413 | Josiah Parry is a Senior Product Engineer at Environmental Systems Research Institute, Inc (Esri) and an open source software developer. 414 | 415 | Rosa Félix is a researcher at the University of Lisbon and specialises geographic methods for transport planning, with a focus on active travel and decarbonisation. 416 | 417 | ```{=html} 418 | 427 | ``` --------------------------------------------------------------------------------