Skip to content

Commit d19690d

Browse files
Merge pull request #72 from U-Shift/development
Consolidation
2 parents 1d00cb7 + 75d83fb commit d19690d

28 files changed

Lines changed: 857 additions & 562 deletions

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export(filter_by_agency)
88
export(filter_by_modes)
99
export(filter_by_route_name)
1010
export(get_network_extension)
11+
export(get_prioritization_stats)
1112
export(get_route_frequency_hourly)
1213
export(get_stop_frequency_hourly)
1314
export(get_way_frequency_hourly)

R/get_network_extension.R

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
#'
1919
#' @examples
2020
#' \dontrun{
21-
#' gtfs = GTFShift::load_feed("gtfs.zip")
22-
#' route_extension = GTFShift::get_network_extension(gtfs)
21+
#' gtfs <- GTFShift::load_feed("gtfs.zip")
22+
#' route_extension <- GTFShift::get_network_extension(gtfs)
2323
#' }
2424
#'
2525
#' @seealso [GTFShift::get_route_frequency_hourly()]
@@ -29,38 +29,37 @@
2929
#'
3030
#' @export
3131
get_network_extension <- function(
32-
gtfs,
33-
route_identifier = "route_id",
34-
direction_wise = TRUE,
35-
unified = FALSE,
36-
date = GTFShift::calendar_nextBusinessWednesday(),
37-
use_osm_routes = NA
32+
gtfs,
33+
route_identifier = "route_id",
34+
direction_wise = TRUE,
35+
unified = FALSE,
36+
date = GTFShift::calendar_nextBusinessWednesday(),
37+
use_osm_routes = NA
3838
) {
39-
4039
# 0. Validations
4140
if (!(route_identifier %in% c("route_id", "route_short_name", "route_long_name"))) {
4241
stop("route_identifier should be one of: route_id, route_short_name or route_long_name")
4342
}
4443

4544
# Compute hourly frequencies for each route
46-
network = gtfs |> GTFShift::get_route_frequency_hourly(date = date, use_osm_routes = use_osm_routes, overline = FALSE)
45+
network <- gtfs |> GTFShift::get_route_frequency_hourly(date = date, use_osm_routes = use_osm_routes, overline = FALSE)
4746

4847
# Get unique shapes
49-
shapes_unique = network |>
48+
shapes_unique <- network |>
5049
st_drop_geometry() |>
5150
select(shape_id) |>
5251
distinct() |>
53-
left_join(network, by = "shape_id", multiple="first")
52+
left_join(network, by = "shape_id", multiple = "first")
5453

5554
# Compute daily frequencies per route shape
56-
network_redux = network |>
55+
network_redux <- network |>
5756
st_drop_geometry() |>
5857
group_by(.data[[route_identifier]], direction_id, shape_id) |>
5958
summarise(frequency_day = sum(frequency)) |>
6059
ungroup()
6160

6261
# Get shape with max frequencies per route
63-
network_redux_max = network_redux |>
62+
network_redux_max <- network_redux |>
6463
# Get max frequency shape per route (and direction, if direction_wise=TRUE)
6564
group_by(.data[[route_identifier]], shape_id, !!!if (direction_wise) rlang::syms("direction_id")) |>
6665
summarise(frequency_max = max(frequency_day)) |>
@@ -73,19 +72,21 @@ get_network_extension <- function(
7372
ungroup()
7473

7574
# Join with the original network to get the shapes and compute its distance
76-
network_redux_shapes = network_redux_max |>
75+
network_redux_shapes <- network_redux_max |>
7776
left_join(shapes_unique, by = "shape_id") |>
7877
st_as_sf() |>
79-
st_transform(crs = 3857) |> # For units in meters
80-
mutate(length = st_length(geometry))
78+
st_transform(crs = 3857) # For units in meters
79+
80+
geom_col <- st_geometry(network_redux_shapes)
81+
network_redux_shapes <- network_redux_shapes |> mutate(length = st_length(geom_col))
8182

8283
# Compute unified network extension
8384
if (unified) {
84-
network_union = network_redux_shapes |>
85+
network_union <- network_redux_shapes |>
8586
st_union() |>
8687
stplanr::line_cast() |>
8788
st_as_sf() |>
88-
mutate(length = st_length(x))
89+
mutate(length = st_length(geom_col))
8990
return(sum(network_union$length))
9091
}
9192

R/get_prioritization_stats.R

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#' Get prioritization stats
2+
#'
3+
#' Get statistics about lane prioritization
4+
#'
5+
#' @param lane_prioritization sf data.frame. Lane prioritization.
6+
#' @param weight Character. Weight to use for weighted mean. Accepted values: "length", "frequency".
7+
#'
8+
#' @returns List with statistics about lane prioritization, with the following attributes:
9+
#' \describe{
10+
#' \item{extension}{Total length of the prioritized network, in meters.}
11+
#' \item{extension_bus_lane}{Total length of the bus lane segments, in meters.}
12+
#' \item{speed_avg}{Average speed of the prioritized network, in km/h.}
13+
#' \item{speed_min}{Minimum speed of the prioritized network, in km/h.}
14+
#' \item{speed_max}{Maximum speed of the prioritized network, in km/h.}
15+
#' \item{n_lanes_avg}{Average number of lanes in the prioritized network.}
16+
#' \item{n_lanes_min}{Minimum number of lanes in the prioritized network.}
17+
#' \item{n_lanes_max}{Maximum number of lanes in the prioritized network.}
18+
#' }
19+
#'
20+
#' @examples
21+
#' \dontrun{
22+
#' lane_prioritization <- GTFShift::prioritize_lanes(gtfs, q)
23+
#' stats <- GTFShift::get_prioritization_stats(lane_prioritization)
24+
#' }
25+
#'
26+
#' @import dplyr
27+
#' @import sf
28+
#'
29+
#' @export
30+
get_prioritization_stats <- function(
31+
lane_prioritization,
32+
weight = c("length", "frequency")
33+
) {
34+
weight <- match.arg(weight)
35+
prioritization_internal <- lane_prioritization |>
36+
st_as_sf() |>
37+
st_transform(crs = 3857)
38+
39+
geom_col <- st_geometry(prioritization_internal)
40+
prioritization_internal <- prioritization_internal |>
41+
mutate(
42+
length = units::drop_units(st_length(geom_col))
43+
) |>
44+
st_drop_geometry()
45+
stats <- list()
46+
47+
# Compute bus lane extension
48+
stats$extension <- sum(prioritization_internal$length, na.rm = TRUE)
49+
stats$extension_bus_lane <- sum(
50+
prioritization_internal |> filter(is_bus_lane) |> pull(length),
51+
na.rm = TRUE
52+
)
53+
54+
# Compute average speed, weighted by chosen weight
55+
if ("speed_avg" %in% names(prioritization_internal)) {
56+
stats$speed_avg <- weighted.mean(prioritization_internal$speed_avg, prioritization_internal[[weight]], na.rm = TRUE)
57+
stats$speed_min <- min(prioritization_internal$speed_avg, na.rm = TRUE)
58+
stats$speed_max <- max(prioritization_internal$speed_avg, na.rm = TRUE)
59+
}
60+
61+
# Compute number of lanes, weighted by chosen weight
62+
stats$n_lanes_avg <- weighted.mean(prioritization_internal$n_lanes, prioritization_internal[[weight]], na.rm = TRUE)
63+
stats$n_lanes_min <- min(prioritization_internal$n_lanes, na.rm = TRUE)
64+
stats$n_lanes_max <- max(prioritization_internal$n_lanes, na.rm = TRUE)
65+
66+
return(stats)
67+
}

R/load_feed.R

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#' @param transfer_distance Integer (Default 300). Upper straight-line distance limit in meters for transfers.
77
#' @param transfer_time Integer (Default 120). Minimum time in seconds for transfers; all values below this will be replaced with this value, particularly all those defining in-place transfers where stop longitudes and latitudes remain identical.
88
#' @param transfer_street_routing Boolean (Default FALSE). If TRUE, transfer times are calculated by routing throughout the underlying street network (downloaded automatically).
9+
#' @param headers Named list or character vector (Optional). Custom HTTP headers for credentials when accessing the GTFS zip file URL.
910
#'
1011
#' @details
1112
#' In addition to loading the GTFS feed, this method validates its integrity and applies the proper corrections if it does not comply with the following validations:
@@ -32,7 +33,15 @@
3233
#' @import tidytransit
3334
#'
3435
#' @export
35-
load_feed <- function(path, store_path=NA, create_transfers=TRUE, transfer_distance=300, transfer_time=120, transfer_street_routing=FALSE) {
36+
load_feed <- function(path, store_path=NA, create_transfers=TRUE, transfer_distance=300, transfer_time=120, transfer_street_routing=FALSE, headers=NULL) {
37+
38+
# If path is a URL and headers are provided, download first
39+
if (grepl("^http", path) && !is.null(headers)) {
40+
temp_zip <- tempfile(fileext = ".zip")
41+
res <- httr::GET(path, httr::add_headers(.headers = headers), httr::write_disk(temp_zip, overwrite = TRUE))
42+
httr::stop_for_status(res)
43+
path <- temp_zip
44+
}
3645

3746
# LOAD GTFS
3847
gtfs <- tidytransit::read_gtfs(path)

R/osm_utils.R

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#' Filter OpenStreetMap ways by bus lane tags
2+
#'
3+
#' @param road_osm sf object. Road OSM data.
4+
#'
5+
#' @return sf object. Filtered road OSM data.
6+
#'
7+
#' @import dplyr
8+
#'
9+
#' @noRd
10+
filter_osm_bus_lanes <- function(road_osm) {
11+
cols_to_check_access <- grep("psv:lanes|bus:lanes", names(road_osm), value = TRUE)
12+
cols_to_check_count <- grep("lanes:psv|lanes:bus", names(road_osm), value = TRUE)
13+
14+
osm_lanes <- road_osm |> filter(
15+
# Based on https://wiki.openstreetmap.org/wiki/Bus_lanes
16+
psv == "designated" |
17+
highway == "busway" |
18+
(length(cols_to_check_access) & if_any(all_of(cols_to_check_access), ~ grepl("designated", .x))) |
19+
(length(cols_to_check_count) & if_any(all_of(cols_to_check_count), ~ is.numeric(.x) & .x >= 1))
20+
)
21+
22+
return(osm_lanes)
23+
}

R/prioritize_lanes.R

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,65 +37,67 @@
3737
#'
3838
#' @examples
3939
#' \dontrun{
40-
#' gtfs = GTFShift::load_feed("gtfs.zip")
41-
#' q = opq(bbox=sf::st_bbox(tidytransit::shapes_as_sf(gtfs$shapes))) |> add_osm_feature(key = "route", value = "bus")
42-
#' lanes_analysis = GTFShift::prioritize_lanes(gtfs, q)
40+
#' gtfs <- GTFShift::load_feed("gtfs.zip")
41+
#' q <- opq(bbox = sf::st_bbox(tidytransit::shapes_as_sf(gtfs$shapes))) |> add_osm_feature(key = "route", value = "bus")
42+
#' lanes_analysis <- GTFShift::prioritize_lanes(gtfs, q)
4343
#' }
4444
#'
4545
#' @import dplyr
4646
#' @import tidytransit
4747
#'
4848
#' @export
4949
prioritize_lanes <- function(
50-
gtfs,
51-
q,
52-
date = GTFShift::calendar_nextBusinessWednesday(),
53-
keep_osm_attributes = FALSE
50+
gtfs,
51+
q,
52+
date = GTFShift::calendar_nextBusinessWednesday(),
53+
keep_osm_attributes = FALSE
5454
) {
55-
5655
# Get way frequency hourly
57-
way_frequency = GTFShift::get_way_frequency_hourly(gtfs, q, date, TRUE)
56+
way_frequency <- GTFShift::get_way_frequency_hourly(gtfs, q, date, TRUE)
5857

5958
# Get bus lanes
60-
shapes_sf = tidytransit::shapes_as_sf(gtfs$shapes)
61-
shapes_bbox = sf::st_bbox(shapes_sf)
62-
bus_lanes = GTFShift::osm_bus_lanes(shapes_bbox)
59+
bus_lanes <- filter_osm_bus_lanes(way_frequency |> distinct(way_osm_id, .keep_all = TRUE))
6360

6461
# Aggregate data
6562
# > Add missing lanes columns, to prevent errors
66-
lane_cols = c("lanes", "lanes:forward", "lanes:backward", "lanes:both_ways", "oneway")
63+
lane_cols <- c("lanes", "lanes:forward", "lanes:backward", "lanes:both_ways", "oneway")
6764
for (col in lane_cols) {
6865
if (!(col %in% colnames(way_frequency))) {
69-
way_frequency[[col]] = NA_character_
66+
way_frequency[[col]] <- NA_character_
7067
}
7168
}
7269
parse_lanes <- function(x) {
7370
suppressWarnings(as.numeric(sub(";.*$", "", x)))
7471
}
7572

7673
# > Compute aggregation
77-
lanes = way_frequency |>
78-
left_join(bus_lanes |> sf::st_drop_geometry() |> select(osm_id) |> mutate(is_bus_lane = TRUE), by = c("way_osm_id" = "osm_id")) |>
74+
lanes <- way_frequency |>
75+
left_join(bus_lanes |> st_drop_geometry() |> select(way_osm_id) |> mutate(is_bus_lane = TRUE), by = "way_osm_id") |>
7976
mutate(
8077
is_bus_lane = ifelse(is.na(is_bus_lane), FALSE, is_bus_lane),
8178
n_lanes_parking = dplyr::case_when(
8279
# Any 'parking:both' or 'parking:lane:both' column present with value different from 'no'
83-
if_any(starts_with("parking:both"), ~ !is.na(.) & . != "no") ~ 2L,
84-
# Otherwise, count left and right sides separately based on specific tags
80+
if_any(matches("^parking(:lane)?:both"), ~ !is.na(.) & . != "no") ~ 2L,
81+
# Otherwise, count left and right sides separately based on specific tags (parking:lane:left/right or parking:left/right)
8582
TRUE ~ (
8683
as.integer(
87-
if_any(starts_with("parking:left"),~ !is.na(.) & . != "no")
84+
if_any(matches("^parking(:lane)?:left"), ~ !is.na(.) & . != "no")
8885
) +
89-
as.integer(
90-
if_any(starts_with("parking:right"), ~ !is.na(.) & . != "no")
91-
)
86+
as.integer(
87+
if_any(matches("^parking(:lane)?:right"), ~ !is.na(.) & . != "no")
88+
)
9289
)
9390
),
9491
n_lanes_circulation = coalesce(
9592
# Global count
9693
parse_lanes(lanes),
97-
# Directional count
98-
parse_lanes(`lanes:forward`) + parse_lanes(`lanes:backward`) + parse_lanes(`lanes:both_ways`),
94+
# Directional count (sum existing ones; returns NA if all are missing)
95+
na_if(
96+
coalesce(parse_lanes(`lanes:forward`), 0) +
97+
coalesce(parse_lanes(`lanes:backward`), 0) +
98+
coalesce(parse_lanes(`lanes:both_ways`), 0),
99+
0
100+
),
99101
# If oneway=="yes", then 1
100102
ifelse(oneway == "yes", 1, NA_integer_),
101103
# Else, assume 2 lanes, one per direction
@@ -121,7 +123,7 @@ prioritize_lanes <- function(
121123
)
122124

123125
if (!keep_osm_attributes) {
124-
lanes = lanes |>
126+
lanes <- lanes |>
125127
select(way_osm_id, hour, frequency, is_bus_lane, n_lanes_parking, n_lanes_circulation, n_lanes, n_directions, n_lanes_circulation_direction, n_lanes_direction, routes, shapes, geometry)
126128
}
127129

R/query_osm_bus_lanes.R

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#'
1212
#' @examples
1313
#' \dontrun{
14-
#' BBOX = sf::st_bbox(city_limit)
14+
#' BBOX <- sf::st_bbox(city_limit)
1515
#' bus_lanes <- GTFShift::osm_bus_lanes(BBOX)
1616
#' }
1717
#'
@@ -21,24 +21,14 @@
2121
#'
2222
#' @export
2323
osm_bus_lanes <- function(bbox) {
24-
25-
road_osm = road_osm = opq(bbox) |> # uses osmdata package, to extract only with BB
24+
road_osm <- opq(bbox) |> # uses osmdata package, to extract only with BB
2625
add_osm_feature(key = "highway") |>
2726
osmdata_sf() |>
2827
osm_poly2line() # makes roundabouts into lines
2928

30-
road_osm = road_osm$osm_lines
31-
32-
cols_to_check_access <- grep("psv:lanes|bus:lanes", names(road_osm), value = TRUE)
33-
cols_to_check_count <- grep("lanes:psv|lanes:bus", names(road_osm), value = TRUE)
29+
road_osm <- road_osm$osm_lines
3430

35-
osm_lanes = road_osm |> filter(
36-
# Based on https://wiki.openstreetmap.org/wiki/Bus_lanes
37-
psv == "designated"
38-
| highway == "busway"
39-
| (length(cols_to_check_access) & if_any(all_of(cols_to_check_access), ~ grepl("designated", .x)))
40-
| (length(cols_to_check_count) & if_any(all_of(cols_to_check_count), ~ is.numeric(.x) & .x >= 1))
41-
)
31+
osm_lanes <- filter_osm_bus_lanes(road_osm)
4232

4333
return(osm_lanes)
4434
}

0 commit comments

Comments
 (0)