diego-ellis-soto commited on
Commit Β·
d90cea5
1
Parent(s): 5ac159a
Fixed muni and default map
Browse files
app.R
CHANGED
|
@@ -231,8 +231,8 @@ pretty_mode <- function(x) {
|
|
| 231 |
x == "walking" ~ "Walking",
|
| 232 |
x == "cycling" ~ "Cycling",
|
| 233 |
x == "driving-traffic" ~ "Driving-Traffic",
|
| 234 |
-
x == "transit" ~ "Transit",
|
| 235 |
-
x == "walk_transit" ~ "Walk
|
| 236 |
TRUE ~ tools::toTitleCase(x)
|
| 237 |
)
|
| 238 |
}
|
|
@@ -677,6 +677,101 @@ gtfs_isochrone_compat <- function(gtfs, from, start_time, end_time,
|
|
| 677 |
out[!duplicated(out$stop_id), , drop = FALSE]
|
| 678 |
}
|
| 679 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
build_walk_transit_isochrone <- function(
|
| 681 |
location_sf, total_time_min, dep_secs,
|
| 682 |
walk_to_stop_min, walk_from_stop_min,
|
|
@@ -834,37 +929,45 @@ build_walk_transit_isochrone <- function(
|
|
| 834 |
# call for the pure Transit mode. It is loaded here with that one call rewritten
|
| 835 |
# to the shim above, so this file stays the only thing that changes on disk.
|
| 836 |
# Equivalent to source("code/iso_metrics_AUDITED.R", local = TRUE) otherwise.
|
| 837 |
-
.
|
| 838 |
-
|
| 839 |
-
# (a) the
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
.
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
#
|
| 852 |
-
#
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
|
| 864 |
# Evaluated at top level, so the definitions land in this app environment -
|
| 865 |
# exactly where source(..., local = TRUE) would have put them.
|
| 866 |
-
eval(parse(text = .
|
| 867 |
-
rm(.
|
| 868 |
|
| 869 |
# The audited helper is sourced into this app environment. Replace only its two
|
| 870 |
# lazy lookup loaders so they keep using the absolute project-root paths above
|
|
@@ -1882,8 +1985,8 @@ transport_mode_choices <- list(
|
|
| 1882 |
"Walking" = "walking",
|
| 1883 |
"Cycling" = "cycling",
|
| 1884 |
"Driving with Traffic" = "driving-traffic",
|
| 1885 |
-
"Transit (
|
| 1886 |
-
"Walk + Transit
|
| 1887 |
)
|
| 1888 |
|
| 1889 |
# =============================================================================
|
|
@@ -2297,8 +2400,8 @@ ui <- dashboardPage(
|
|
| 2297 |
"Walking" = "walking",
|
| 2298 |
"Cycling" = "cycling",
|
| 2299 |
"Driving with Traffic" = "driving-traffic",
|
| 2300 |
-
"Transit (
|
| 2301 |
-
"Walk + Transit
|
| 2302 |
),
|
| 2303 |
selected = c("driving", "walking")
|
| 2304 |
),
|
|
@@ -2315,7 +2418,36 @@ ui <- dashboardPage(
|
|
| 2315 |
"Transit departure flexibility window (minutes):",
|
| 2316 |
min = 0, max = 20, value = 10, step = 5
|
| 2317 |
),
|
| 2318 |
-
helpText("Several departures after the selected time can be evaluated for walk + transit.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2319 |
),
|
| 2320 |
|
| 2321 |
conditionalPanel(
|
|
@@ -3229,9 +3361,37 @@ ui <- dashboardPage(
|
|
| 3229 |
status = "primary", solidHeader = TRUE, width = 12,
|
| 3230 |
p(
|
| 3231 |
style = "font-size: 13px; color: #555;",
|
| 3232 |
-
"Six modes are supported across two routing engines.
|
| 3233 |
-
"
|
| 3234 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3235 |
),
|
| 3236 |
tags$table(
|
| 3237 |
class = "table table-hover table-sm",
|
|
@@ -4125,7 +4285,7 @@ server <- function(input, output, session) {
|
|
| 4125 |
"Species Richness", "Data Availability",
|
| 4126 |
"CalEnviroScreen (CI Score)", "SF EJ Communities",
|
| 4127 |
"Transit Routes", "Transit Stops",
|
| 4128 |
-
"Isochrones", "Transit Isochrones", "NDVI Raster"
|
| 4129 |
),
|
| 4130 |
options = layersControlOptions(collapsed = TRUE)
|
| 4131 |
) |>
|
|
@@ -5487,6 +5647,7 @@ server <- function(input, output, session) {
|
|
| 5487 |
clearGroup("selected_point") |>
|
| 5488 |
clearGroup("Isochrones") |>
|
| 5489 |
clearGroup("Transit Isochrones") |>
|
|
|
|
| 5490 |
clearGroup("NDVI Raster")
|
| 5491 |
})
|
| 5492 |
|
|
@@ -5514,7 +5675,15 @@ server <- function(input, output, session) {
|
|
| 5514 |
})
|
| 5515 |
outputOptions(output, "proposal_ready", suspendWhenHidden = FALSE)
|
| 5516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5517 |
observeEvent(input$generate_iso, {
|
|
|
|
| 5518 |
pt <- chosen_point()
|
| 5519 |
if (is.null(pt)) return()
|
| 5520 |
if (length(input$transport_modes) == 0) return()
|
|
@@ -5524,6 +5693,7 @@ server <- function(input, output, session) {
|
|
| 5524 |
removeControl("ndvi_legend") |>
|
| 5525 |
clearGroup("Isochrones") |>
|
| 5526 |
clearGroup("Transit Isochrones") |>
|
|
|
|
| 5527 |
clearGroup("NDVI Raster")
|
| 5528 |
|
| 5529 |
# All isochrone construction lives in build_isochrones() (code/iso_metrics_AUDITED.R)
|
|
@@ -5550,6 +5720,7 @@ server <- function(input, output, session) {
|
|
| 5550 |
removeControl("ndvi_legend") |>
|
| 5551 |
clearGroup("Isochrones") |>
|
| 5552 |
clearGroup("Transit Isochrones") |>
|
|
|
|
| 5553 |
clearGroup("NDVI Raster")
|
| 5554 |
|
| 5555 |
standard_like_modes <- c("driving", "walking", "cycling", "driving-traffic", "walk_transit")
|
|
@@ -5603,7 +5774,11 @@ server <- function(input, output, session) {
|
|
| 5603 |
poly_i <- transit_iso[i, ]
|
| 5604 |
time_i <- as.numeric(poly_i$time[[1]])
|
| 5605 |
|
| 5606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5607 |
area_km2 <- round(as.numeric(st_area(st_transform(poly_i, 3857))) / 1e6, 2)
|
| 5608 |
t_score <- if (area_km2 > 0) round(n_stops_in / area_km2, 2) else NA_real_
|
| 5609 |
|
|
@@ -5637,6 +5812,24 @@ server <- function(input, output, session) {
|
|
| 5637 |
label = paste0("Transit ", time_i, " min"),
|
| 5638 |
popup = popup_html
|
| 5639 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5640 |
}
|
| 5641 |
}
|
| 5642 |
|
|
|
|
| 231 |
x == "walking" ~ "Walking",
|
| 232 |
x == "cycling" ~ "Cycling",
|
| 233 |
x == "driving-traffic" ~ "Driving-Traffic",
|
| 234 |
+
x == "transit" ~ "Transit (optimistic)",
|
| 235 |
+
x == "walk_transit" ~ "Walk + Transit (conservative)",
|
| 236 |
TRUE ~ tools::toTitleCase(x)
|
| 237 |
)
|
| 238 |
}
|
|
|
|
| 677 |
out[!duplicated(out$stop_id), , drop = FALSE]
|
| 678 |
}
|
| 679 |
|
| 680 |
+
# ---------------------------------------------------------------------------
|
| 681 |
+
# REALISTIC TRANSIT ISOCHRONE
|
| 682 |
+
# ---------------------------------------------------------------------------
|
| 683 |
+
# The original Transit mode drew the CONVEX HULL of the stops reachable from the
|
| 684 |
+
# single nearest stop. A hull measures how far apart the outermost stops are,
|
| 685 |
+
# not what you can reach: a few far-flung stops on fast radial corridors fill in
|
| 686 |
+
# a huge triangle of land no route serves, while dense inner-city service with
|
| 687 |
+
# fifty nearby stops draws a small one. Measured from two origins at 15 min,
|
| 688 |
+
# Telegraph Hill reached MORE of the network (52 stops vs 43) yet got a hull less
|
| 689 |
+
# than half the size (1.41 vs 3.03 km2).
|
| 690 |
+
#
|
| 691 |
+
# This is the standard construction instead - walk to nearby stops, ride, then
|
| 692 |
+
# walk out from each destination stop with whatever time remains, and union the
|
| 693 |
+
# pieces, giving the characteristic "beads along corridors" shape:
|
| 694 |
+
#
|
| 695 |
+
# reachable = walkshed(origin, T)
|
| 696 |
+
# U { walkshed(stop s, T - access(s0) - ride(s0 -> s)) }
|
| 697 |
+
#
|
| 698 |
+
# Walking speed is 60 m/min, calibrated rather than assumed: the median Mapbox
|
| 699 |
+
# walking-network isochrone at 5/10/15 min has an equal-area radius implying
|
| 700 |
+
# 62.3 / 60.1 / 59.4 m/min, so a straight-line 80 m/min would overstate the
|
| 701 |
+
# reachable area by about (80/60)^2 = 1.8x. Buffers are computed in UTM
|
| 702 |
+
# (ISO_METRIC_CRS), not Web Mercator, which at this latitude would inflate every
|
| 703 |
+
# distance by roughly 1/cos(37.8) = 1.27x.
|
| 704 |
+
build_transit_isochrone_union <- function(
|
| 705 |
+
location_sf, total_time_min, dep_secs, gtfs_stops_sf, gtfs_router,
|
| 706 |
+
walk_m_per_min = 60, max_access_min = 10, max_board_stops = 8,
|
| 707 |
+
charge_wait = FALSE
|
| 708 |
+
) {
|
| 709 |
+
if (is.null(gtfs_router) || is.null(gtfs_stops_sf)) return(NULL)
|
| 710 |
+
if (!is.finite(total_time_min) || total_time_min <= 0) return(NULL)
|
| 711 |
+
|
| 712 |
+
loc_m <- st_transform(location_sf, ISO_METRIC_CRS)
|
| 713 |
+
stops_m <- st_transform(gtfs_stops_sf, ISO_METRIC_CRS)
|
| 714 |
+
|
| 715 |
+
# you can always just walk, so the origin's own walkshed is always included
|
| 716 |
+
parts <- list(st_geometry(st_buffer(loc_m, total_time_min * walk_m_per_min)))
|
| 717 |
+
|
| 718 |
+
access_min <- as.numeric(st_distance(loc_m, stops_m)) / walk_m_per_min
|
| 719 |
+
|
| 720 |
+
# Expected wait for the first vehicle. A rider who turns up without consulting
|
| 721 |
+
# a timetable waits, on average, half the headway. The median Muni headway is
|
| 722 |
+
# 15 min, so this charges about 7.5 min of a 15 min budget - which is why the
|
| 723 |
+
# paper reports it as a separate variant rather than folding it in silently.
|
| 724 |
+
# Waiting at any TRANSFER is already inside gtfs_traveltimes()'s duration, so
|
| 725 |
+
# only the boarding wait is added here.
|
| 726 |
+
wait_min <- rep(0, nrow(stops_m))
|
| 727 |
+
if (isTRUE(charge_wait)) {
|
| 728 |
+
hw <- suppressWarnings(as.numeric(gtfs_stops_sf$mean_headway_min))
|
| 729 |
+
if (all(is.na(hw))) {
|
| 730 |
+
warning("no headway data on gtfs_stops_sf; expected wait not charged")
|
| 731 |
+
} else {
|
| 732 |
+
hw[is.na(hw)] <- stats::median(hw, na.rm = TRUE) # stops with no data
|
| 733 |
+
wait_min <- hw / 2
|
| 734 |
+
}
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
board <- which(access_min + wait_min <= total_time_min &
|
| 738 |
+
access_min <= min(max_access_min, total_time_min))
|
| 739 |
+
|
| 740 |
+
if (length(board) > 0) {
|
| 741 |
+
# nearest few boarding stops only: routing every stop within walking range
|
| 742 |
+
# would make one map click take tens of seconds and barely move the shape.
|
| 743 |
+
board <- board[order(access_min[board])][seq_len(min(length(board), max_board_stops))]
|
| 744 |
+
|
| 745 |
+
sid_all <- as.character(gtfs_stops_sf$stop_id)
|
| 746 |
+
best <- rep(Inf, length(sid_all)) # best total time to reach each stop
|
| 747 |
+
for (bi in board) {
|
| 748 |
+
remaining <- total_time_min - access_min[bi] - wait_min[bi]
|
| 749 |
+
if (remaining <= 0) next
|
| 750 |
+
rr <- gtfs_isochrone_compat(
|
| 751 |
+
gtfs = gtfs_router, from = sid_all[bi], start_time = dep_secs,
|
| 752 |
+
end_time = dep_secs + remaining * 60, from_is_id = TRUE
|
| 753 |
+
)
|
| 754 |
+
if (is.null(rr) || !nrow(rr)) next
|
| 755 |
+
idx <- match(as.character(rr$stop_id), sid_all)
|
| 756 |
+
ok <- !is.na(idx)
|
| 757 |
+
if (!any(ok)) next
|
| 758 |
+
cost <- access_min[bi] + wait_min[bi] + rr$duration[ok] / 60
|
| 759 |
+
best[idx[ok]] <- pmin(best[idx[ok]], cost)
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
egress_min <- total_time_min - best
|
| 763 |
+
keep <- which(is.finite(egress_min) & egress_min > 0)
|
| 764 |
+
if (length(keep) > 0) {
|
| 765 |
+
parts <- append(parts, list(st_geometry(
|
| 766 |
+
st_buffer(stops_m[keep, ], egress_min[keep] * walk_m_per_min))))
|
| 767 |
+
}
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
out <- st_union(do.call(c, parts))
|
| 771 |
+
if (length(out) == 0) return(NULL)
|
| 772 |
+
st_transform(out, 4326)
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
build_walk_transit_isochrone <- function(
|
| 776 |
location_sf, total_time_min, dep_secs,
|
| 777 |
walk_to_stop_min, walk_from_stop_min,
|
|
|
|
| 929 |
# call for the pure Transit mode. It is loaded here with that one call rewritten
|
| 930 |
# to the shim above, so this file stays the only thing that changes on disk.
|
| 931 |
# Equivalent to source("code/iso_metrics_AUDITED.R", local = TRUE) otherwise.
|
| 932 |
+
.iso_lines <- readLines("code/iso_metrics_AUDITED.R", warn = FALSE)
|
| 933 |
+
|
| 934 |
+
# (a) Replace the whole Transit block with a call to the realistic builder
|
| 935 |
+
# above. Line-based splice rather than a text substitution, because the block
|
| 936 |
+
# spans nested braces that no single regex matches safely.
|
| 937 |
+
.b0 <- grep("^ stop_dists <- st_distance\\(location_sf, gtfs_stops_sf\\)$", .iso_lines)
|
| 938 |
+
.b1 <- grep("^ if \\(!is.null\\(iso_poly\\)\\) \\{$", .iso_lines)
|
| 939 |
+
if (length(.b0) == 1 && length(.b1) >= 1 && any(.b1 > .b0)) {
|
| 940 |
+
.b1 <- .b1[.b1 > .b0][1]
|
| 941 |
+
.iso_lines <- c(
|
| 942 |
+
.iso_lines[seq_len(.b0 - 1L)],
|
| 943 |
+
" dep_secs <- as.numeric(transit_hour) * 3600",
|
| 944 |
+
"",
|
| 945 |
+
" for (t in times) {",
|
| 946 |
+
" # transit_iso_cache is bypassed on purpose: it stores the old convex",
|
| 947 |
+
" # hulls keyed on the nearest stop, which are the wrong geometry.",
|
| 948 |
+
" iso_poly <- tryCatch(",
|
| 949 |
+
" build_transit_isochrone_union(",
|
| 950 |
+
" location_sf = location_sf, total_time_min = t, dep_secs = dep_secs,",
|
| 951 |
+
" gtfs_stops_sf = gtfs_stops_sf, gtfs_router = gtfs_router,",
|
| 952 |
+
" charge_wait = isTRUE(getOption(\"bas.transit_charge_wait\", FALSE))),",
|
| 953 |
+
" error = function(e) {",
|
| 954 |
+
" warning(\"transit isochrone failed: \", conditionMessage(e)); NULL })",
|
| 955 |
+
"",
|
| 956 |
+
.iso_lines[seq(.b1, length(.iso_lines))]
|
| 957 |
+
)
|
| 958 |
+
} else {
|
| 959 |
+
message("[app] could not locate the Transit block in code/iso_metrics_AUDITED.R - left unchanged.")
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
# (b) safety net: any remaining call to the function that does not exist in
|
| 963 |
+
# gtfsrouter 0.1.4 still routes through the shim.
|
| 964 |
+
.iso_lines <- gsub("gtfsrouter::gtfs_isochrone(", "gtfs_isochrone_compat(",
|
| 965 |
+
.iso_lines, fixed = TRUE)
|
| 966 |
|
| 967 |
# Evaluated at top level, so the definitions land in this app environment -
|
| 968 |
# exactly where source(..., local = TRUE) would have put them.
|
| 969 |
+
eval(parse(text = paste(.iso_lines, collapse = "\n")))
|
| 970 |
+
rm(.iso_lines, .b0, .b1)
|
| 971 |
|
| 972 |
# The audited helper is sourced into this app environment. Replace only its two
|
| 973 |
# lazy lookup loaders so they keep using the absolute project-root paths above
|
|
|
|
| 1985 |
"Walking" = "walking",
|
| 1986 |
"Cycling" = "cycling",
|
| 1987 |
"Driving with Traffic" = "driving-traffic",
|
| 1988 |
+
"Transit - optimistic (no wait)" = "transit",
|
| 1989 |
+
"Walk + Transit - conservative" = "walk_transit"
|
| 1990 |
)
|
| 1991 |
|
| 1992 |
# =============================================================================
|
|
|
|
| 2400 |
"Walking" = "walking",
|
| 2401 |
"Cycling" = "cycling",
|
| 2402 |
"Driving with Traffic" = "driving-traffic",
|
| 2403 |
+
"Transit - optimistic (no wait)" = "transit",
|
| 2404 |
+
"Walk + Transit - conservative" = "walk_transit"
|
| 2405 |
),
|
| 2406 |
selected = c("driving", "walking")
|
| 2407 |
),
|
|
|
|
| 2418 |
"Transit departure flexibility window (minutes):",
|
| 2419 |
min = 0, max = 20, value = 10, step = 5
|
| 2420 |
),
|
| 2421 |
+
helpText("Several departures after the selected time can be evaluated for walk + transit."),
|
| 2422 |
+
checkboxInput(
|
| 2423 |
+
"transit_charge_wait",
|
| 2424 |
+
"Charge the expected wait (half the headway at the boarding stop)",
|
| 2425 |
+
value = FALSE
|
| 2426 |
+
),
|
| 2427 |
+
helpText(
|
| 2428 |
+
"Off by default, matching the paper's turn-up-and-go upper bound. Switching it on ",
|
| 2429 |
+
"reproduces the paper's scheduled variant: the median Muni headway is 15 minutes, so ",
|
| 2430 |
+
"this costs about 7.5 minutes of a 15-minute budget."
|
| 2431 |
+
),
|
| 2432 |
+
tags$div(
|
| 2433 |
+
style = paste("background:#eef4fb; border-left:4px solid #2166ac;",
|
| 2434 |
+
"padding:9px 11px; margin-top:9px; font-size:12px; line-height:1.5;"),
|
| 2435 |
+
tags$b("The two transit modes bracket the answer - they are not duplicates."),
|
| 2436 |
+
tags$ul(
|
| 2437 |
+
style = "margin:6px 0 0 0; padding-left:18px;",
|
| 2438 |
+
tags$li(tags$b("Transit - optimistic. "),
|
| 2439 |
+
"Walk up to 10 min to a stop, ride, then walk out with ALL remaining time. ",
|
| 2440 |
+
"No waiting is charged. Straight-line walking at a calibrated 60 m/min. ",
|
| 2441 |
+
"Fast, no API cost. ", tags$i("This is the method used in the paper.")),
|
| 2442 |
+
tags$li(tags$b("Walk + Transit - conservative. "),
|
| 2443 |
+
"First and last mile each capped by your sliders above (default 5 min), ",
|
| 2444 |
+
"only the 12 best last-mile stops, best departure searched across the window, ",
|
| 2445 |
+
"and real street-network walksheds from Mapbox. Slower and uses API calls.")
|
| 2446 |
+
),
|
| 2447 |
+
tags$div(style = "margin-top:6px; color:#44515c;",
|
| 2448 |
+
"Measured at 15 min, the optimistic mode returns roughly 2-4x the area. ",
|
| 2449 |
+
"The truth sits between them: treat them as an upper and a lower bound.")
|
| 2450 |
+
)
|
| 2451 |
),
|
| 2452 |
|
| 2453 |
conditionalPanel(
|
|
|
|
| 3361 |
status = "primary", solidHeader = TRUE, width = 12,
|
| 3362 |
p(
|
| 3363 |
style = "font-size: 13px; color: #555;",
|
| 3364 |
+
"Six modes are supported across two routing engines. Both transit modes now walk at BOTH ends ",
|
| 3365 |
+
"β you cannot reach nature by riding past it, only by getting off and walking β so they differ ",
|
| 3366 |
+
"in how generous they are, not in whether walking is included."
|
| 3367 |
+
),
|
| 3368 |
+
tags$div(
|
| 3369 |
+
style = paste("background:#eef4fb; border-left:4px solid #2166ac; padding:11px 13px;",
|
| 3370 |
+
"margin:4px 0 12px 0; font-size:13px; line-height:1.55;"),
|
| 3371 |
+
tags$b("Why there are two transit modes"),
|
| 3372 |
+
tags$p(style = "margin:6px 0 0 0;",
|
| 3373 |
+
"They bracket the answer rather than duplicating it. Measured at a 15-minute budget the ",
|
| 3374 |
+
"optimistic mode returns roughly two to four times the area of the conservative one; the ",
|
| 3375 |
+
"honest reading is that reality sits between them."),
|
| 3376 |
+
tags$ul(
|
| 3377 |
+
style = "margin:8px 0 0 0; padding-left:18px;",
|
| 3378 |
+
tags$li(tags$b("Transit β optimistic (no wait). "),
|
| 3379 |
+
"Access walk capped at 10 minutes, then all remaining time is given to the walk out ",
|
| 3380 |
+
"from each reachable stop. No waiting for the vehicle is charged. Walking is a ",
|
| 3381 |
+
"straight-line buffer at 60 m/min β calibrated, not assumed: the median Mapbox ",
|
| 3382 |
+
"walking-network isochrone at 5/10/15 minutes implies 62.3 / 60.1 / 59.4 m/min, so a ",
|
| 3383 |
+
"nominal 80 m/min would overstate reachable area by about 1.8x. No API calls, under ",
|
| 3384 |
+
"a second per query. ", tags$b("This is the construction used in the manuscript.")),
|
| 3385 |
+
tags$li(tags$b("Walk + Transit β conservative. "),
|
| 3386 |
+
"First and last mile each capped by the sliders (5 minutes by default), only the 12 ",
|
| 3387 |
+
"best last-mile stops are buffered, the best departure within the flexibility window ",
|
| 3388 |
+
"is searched, and the walksheds are real Mapbox street-network isochrones rather than ",
|
| 3389 |
+
"circles. Slower, and it consumes Mapbox API calls.")
|
| 3390 |
+
),
|
| 3391 |
+
tags$p(style = "margin:8px 0 0 0; color:#44515c;",
|
| 3392 |
+
"Neither charges a half-headway wait. Since the median Muni headway is 15 minutes, an ",
|
| 3393 |
+
"expected 7.5-minute wait would consume half of a 15-minute budget, so both modes remain ",
|
| 3394 |
+
"optimistic about waiting even though they differ in how far they let you walk.")
|
| 3395 |
),
|
| 3396 |
tags$table(
|
| 3397 |
class = "table table-hover table-sm",
|
|
|
|
| 4285 |
"Species Richness", "Data Availability",
|
| 4286 |
"CalEnviroScreen (CI Score)", "SF EJ Communities",
|
| 4287 |
"Transit Routes", "Transit Stops",
|
| 4288 |
+
"Isochrones", "Transit Isochrones", "Muni Stops Reached", "NDVI Raster"
|
| 4289 |
),
|
| 4290 |
options = layersControlOptions(collapsed = TRUE)
|
| 4291 |
) |>
|
|
|
|
| 5647 |
clearGroup("selected_point") |>
|
| 5648 |
clearGroup("Isochrones") |>
|
| 5649 |
clearGroup("Transit Isochrones") |>
|
| 5650 |
+
clearGroup("Muni Stops Reached") |>
|
| 5651 |
clearGroup("NDVI Raster")
|
| 5652 |
})
|
| 5653 |
|
|
|
|
| 5675 |
})
|
| 5676 |
outputOptions(output, "proposal_ready", suspendWhenHidden = FALSE)
|
| 5677 |
|
| 5678 |
+
# build_isochrones() lives in the sourced helper and its signature is shared
|
| 5679 |
+
# with the comparer tab, so the wait toggle is passed as an option rather than
|
| 5680 |
+
# threaded through as another argument.
|
| 5681 |
+
observeEvent(input$transit_charge_wait, {
|
| 5682 |
+
options(bas.transit_charge_wait = isTRUE(input$transit_charge_wait))
|
| 5683 |
+
}, ignoreNULL = FALSE)
|
| 5684 |
+
|
| 5685 |
observeEvent(input$generate_iso, {
|
| 5686 |
+
options(bas.transit_charge_wait = isTRUE(input$transit_charge_wait))
|
| 5687 |
pt <- chosen_point()
|
| 5688 |
if (is.null(pt)) return()
|
| 5689 |
if (length(input$transport_modes) == 0) return()
|
|
|
|
| 5693 |
removeControl("ndvi_legend") |>
|
| 5694 |
clearGroup("Isochrones") |>
|
| 5695 |
clearGroup("Transit Isochrones") |>
|
| 5696 |
+
clearGroup("Muni Stops Reached") |>
|
| 5697 |
clearGroup("NDVI Raster")
|
| 5698 |
|
| 5699 |
# All isochrone construction lives in build_isochrones() (code/iso_metrics_AUDITED.R)
|
|
|
|
| 5720 |
removeControl("ndvi_legend") |>
|
| 5721 |
clearGroup("Isochrones") |>
|
| 5722 |
clearGroup("Transit Isochrones") |>
|
| 5723 |
+
clearGroup("Muni Stops Reached") |>
|
| 5724 |
clearGroup("NDVI Raster")
|
| 5725 |
|
| 5726 |
standard_like_modes <- c("driving", "walking", "cycling", "driving-traffic", "walk_transit")
|
|
|
|
| 5774 |
poly_i <- transit_iso[i, ]
|
| 5775 |
time_i <- as.numeric(poly_i$time[[1]])
|
| 5776 |
|
| 5777 |
+
# keep the stops themselves, not just the count: they are drawn below so
|
| 5778 |
+
# the user can see WHERE the isochrone lets them get off and walk out.
|
| 5779 |
+
stops_in <- tryCatch(suppressWarnings(st_intersection(gtfs_stops_sf, poly_i)),
|
| 5780 |
+
error = function(e) NULL)
|
| 5781 |
+
n_stops_in <- if (is.null(stops_in)) 0 else nrow(stops_in)
|
| 5782 |
area_km2 <- round(as.numeric(st_area(st_transform(poly_i, 3857))) / 1e6, 2)
|
| 5783 |
t_score <- if (area_km2 > 0) round(n_stops_in / area_km2, 2) else NA_real_
|
| 5784 |
|
|
|
|
| 5812 |
label = paste0("Transit ", time_i, " min"),
|
| 5813 |
popup = popup_html
|
| 5814 |
)
|
| 5815 |
+
|
| 5816 |
+
# Draw the reachable stops only for the LARGEST budget, which is a
|
| 5817 |
+
# superset of the smaller ones - drawing them per budget would stack
|
| 5818 |
+
# several markers on the same stop.
|
| 5819 |
+
if (!is.null(stops_in) && nrow(stops_in) > 0 &&
|
| 5820 |
+
isTRUE(time_i == max(as.numeric(transit_iso$time), na.rm = TRUE))) {
|
| 5821 |
+
leafletProxy("isoMap") |>
|
| 5822 |
+
addCircleMarkers(
|
| 5823 |
+
data = stops_in,
|
| 5824 |
+
group = "Muni Stops Reached",
|
| 5825 |
+
radius = 3.5, weight = 1,
|
| 5826 |
+
color = "#20456e", fillColor = "#4a90d9", fillOpacity = 0.9,
|
| 5827 |
+
label = ~paste0(stop_name, " (", stop_id, ")"),
|
| 5828 |
+
popup = ~paste0("<strong>", stop_name, "</strong><br>Stop ID: ", stop_id,
|
| 5829 |
+
"<br><small>Reachable within ", time_i,
|
| 5830 |
+
" min, including the walk at each end.</small>")
|
| 5831 |
+
)
|
| 5832 |
+
}
|
| 5833 |
}
|
| 5834 |
}
|
| 5835 |
|