Spaces:
Sleeping
Sleeping
major update
#1
by avephill - opened
- .Rhistory +0 -45
- .gitattributes +34 -0
- .github/workflows/deploy.yml +0 -20
- .gitignore +1 -6
- Dockerfile +3 -7
- R/old_poc/app_20250110.R +1110 -0
- R/old_poc/app_old.R +986 -0
- R/old_poc/app_works_no_shinydashboard.R +1022 -0
- R/old_poc/make_RSF_hexbin.R +9 -0
- R/setup.R +83 -0
- README.md +36 -167
- Rscripts/iso_metrics.R +0 -709
- Rscripts/prep/build_cbg_greenspace_coverage.R +0 -44
- Rscripts/prep/build_equity_layers.R +0 -34
- Rscripts/prep/create_annotated_gbif_parquet.R +0 -79
- Rscripts/prep/implement_optimizations.R +0 -53
- Rscripts/prep/making-greenspace-raster.R +0 -186
- Rscripts/prep/making-rsfprogram-raster.R +0 -164
- Rscripts/prep/profile_startup.R +0 -444
- Rscripts/prep/run_all_prep.R +0 -23
- Rscripts/prep/upload_to_huggingface.R +0 -48
- Rscripts/setup_unified.R +0 -275
- SF_biodiv_access_shiny.Rproj +0 -13
- app.R +0 -0
- install.r +18 -28
- penguins.csv +345 -0
- rsconnect/shinyapps.io/diego-ellis-soto/RSF_Biodiversity_Access.dcf +0 -12
- rsconnect/shinyapps.io/diego-ellis-soto/SF_biodiv_access.dcf +0 -13
- www/app_pastel.css +0 -89
.Rhistory
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
library(shiny); runApp('app_v2.R')
|
| 2 |
-
GETWD()
|
| 3 |
-
getwd()
|
| 4 |
-
list.files()
|
| 5 |
-
list.files('data/')
|
| 6 |
-
list.files('data/cached/')
|
| 7 |
-
runApp('app_v2.R')
|
| 8 |
-
library(tidycensus)
|
| 9 |
-
library(sf)
|
| 10 |
-
library(dplyr)
|
| 11 |
-
# requires a Census API key (install once via census_api_key())
|
| 12 |
-
sf <- get_acs(
|
| 13 |
-
geography = "county",
|
| 14 |
-
state = "CA",
|
| 15 |
-
variables = "B01003_001", # total population (dummy variable just to trigger geometry)
|
| 16 |
-
year = 2016,
|
| 17 |
-
geometry = TRUE
|
| 18 |
-
) %>%
|
| 19 |
-
filter(NAME == "San Francisco County, California") %>%
|
| 20 |
-
st_transform(3310) %>%
|
| 21 |
-
st_cast("POLYGON") %>%
|
| 22 |
-
mutate(area = st_area(geometry)) %>%
|
| 23 |
-
slice_max(area, n = 1) %>%
|
| 24 |
-
select(GEOID, NAME, geometry)
|
| 25 |
-
runApp('app_v2.R')
|
| 26 |
-
library(sf)
|
| 27 |
-
library(dplyr)
|
| 28 |
-
# Remote zipped shapefile bundle from TPL
|
| 29 |
-
zip_url <- "/vsizip//vsicurl/https://parkserve.tpl.org/downloads/Parkserve_Shapefiles_05212025.zip"
|
| 30 |
-
# Inspect available layers in the remote zip
|
| 31 |
-
st_layers(zip_url)
|
| 32 |
-
getwd()
|
| 33 |
-
runApp('app_v2.R')
|
| 34 |
-
x <- st_read('/Users/diegoellis/Desktop/RSF_next_steps/RSF_Program_Projects_polygons_Diego (1)/RSF_Program_Projects_polygons_Diego.shp')
|
| 35 |
-
st_write(x, "data/source/RSF_Program_Projects_polygons.gpkg")
|
| 36 |
-
#
|
| 37 |
-
rsf_projects <- st_read("data/source/RSF_Program_Projects_polygons.gpkg", quiet = TRUE) |>
|
| 38 |
-
st_transform(4326)
|
| 39 |
-
runApp('app_v2.R')
|
| 40 |
-
runApp('app_v2.R')
|
| 41 |
-
runApp('app_v2.R')
|
| 42 |
-
runApp('app_v2.R')
|
| 43 |
-
runApp('~/Desktop/Projects/Postdoc/OLD_SF_BIODIV_ACCESS/SF_biodiv_access/backup-shiny/with_spider_plot_w_muni_v3.R')
|
| 44 |
-
runApp('~/Desktop/Projects/Postdoc/OLD_SF_BIODIV_ACCESS/SF_biodiv_access/backup-shiny/with_spider_plot_w_muni_v2.R')
|
| 45 |
-
runApp('~/Desktop/Projects/Postdoc/OLD_SF_BIODIV_ACCESS/SF_biodiv_access/backup-shiny/with_spider_plot_w_muni.R')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitattributes
CHANGED
|
@@ -1 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*.png filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/deploy.yml
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
name: Sync to Hugging Face hub
|
| 2 |
-
on:
|
| 3 |
-
push:
|
| 4 |
-
branches: [main]
|
| 5 |
-
|
| 6 |
-
# to run this workflow manually from the Actions tab
|
| 7 |
-
workflow_dispatch:
|
| 8 |
-
|
| 9 |
-
jobs:
|
| 10 |
-
sync-to-hub:
|
| 11 |
-
runs-on: ubuntu-latest
|
| 12 |
-
steps:
|
| 13 |
-
- uses: actions/checkout@v3
|
| 14 |
-
with:
|
| 15 |
-
fetch-depth: 0
|
| 16 |
-
lfs: true
|
| 17 |
-
- name: Push to hub
|
| 18 |
-
env:
|
| 19 |
-
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 20 |
-
run: git push -f https://cboettig:$HF_TOKEN@huggingface.co/spaces/boettiger-lab/sf_biodiv_access_shiny/ main
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -1,8 +1,3 @@
|
|
| 1 |
-
# macOS system files
|
| 2 |
.DS_Store
|
| 3 |
-
data
|
| 4 |
.Rproj.user
|
| 5 |
-
|
| 6 |
-
old
|
| 7 |
-
startup_benchmarks.csv
|
| 8 |
-
startup_benchmarks.png
|
|
|
|
|
|
|
| 1 |
.DS_Store
|
|
|
|
| 2 |
.Rproj.user
|
| 3 |
+
rsconnect
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -2,16 +2,12 @@ FROM quay.io/jupyter/minimal-notebook:ubuntu-24.04
|
|
| 2 |
|
| 3 |
USER root
|
| 4 |
|
| 5 |
-
# R & RStudio
|
| 6 |
RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_r.sh | bash
|
| 7 |
RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_rstudio.sh | bash
|
| 8 |
|
| 9 |
-
WORKDIR /code
|
| 10 |
COPY . .
|
| 11 |
RUN Rscript install.r
|
| 12 |
|
| 13 |
-
|
| 14 |
-
EXPOSE 7860
|
| 15 |
-
|
| 16 |
-
# Entry app
|
| 17 |
-
CMD ["R", "--quiet", "-e", "shiny::runApp('app.R', host='0.0.0.0', port=7860)"]
|
|
|
|
| 2 |
|
| 3 |
USER root
|
| 4 |
|
| 5 |
+
# R & RStudio
|
| 6 |
RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_r.sh | bash
|
| 7 |
RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_rstudio.sh | bash
|
| 8 |
|
| 9 |
+
WORKDIR /code
|
| 10 |
COPY . .
|
| 11 |
RUN Rscript install.r
|
| 12 |
|
| 13 |
+
CMD ["R", "--quiet", "-e", "shiny::runApp(host='0.0.0.0', port=7860)"]
|
|
|
|
|
|
|
|
|
|
|
|
R/old_poc/app_20250110.R
ADDED
|
@@ -0,0 +1,1110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# truncate the name
|
| 2 |
+
# Geocoder shiny all -> Adapt !!!
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
# Get working directory, perhaps shiny apps is not receiving the data and the www?
|
| 7 |
+
# rsconnect::setAccountInfo(name='diego-ellis-soto', token='A47BE3C9E4B9EBCDFEC889AF31F64154', secret='g2Q2rxeYCiwlH81EkPXcCGsiHMgdyhTznJRmHtea')
|
| 8 |
+
# deployApp()
|
| 9 |
+
# Add that you can hover over the greespace and get its name
|
| 10 |
+
# Improve the titles of the ggplots of the model coefficient estimates and of ggplot using the gbif summary table on data avialability vs species richness. Also log transform these values for better data visualization
|
| 11 |
+
# Also the ggplot of data avialability vs species richness. should also update if the user decides to subset by class or family. Until then, its okay to retain the general plot using all the data from gbif_sf
|
| 12 |
+
|
| 13 |
+
# Optimize some calculations? Shorten
|
| 14 |
+
|
| 15 |
+
# Look at code human facets or relate social vulnerabiltiy income
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
###############################################################################
|
| 20 |
+
# Shiny App: San Francisco Biodiversity Access Decision Support Tool
|
| 21 |
+
# Author: Diego Ellis Soto, et al.
|
| 22 |
+
# University of California Berkeley, ESPM
|
| 23 |
+
# California Academy of Sciences
|
| 24 |
+
###############################################################################
|
| 25 |
+
require(shinyjs)
|
| 26 |
+
library(shiny)
|
| 27 |
+
library(leaflet)
|
| 28 |
+
library(mapboxapi)
|
| 29 |
+
library(tidyverse)
|
| 30 |
+
library(tidycensus)
|
| 31 |
+
library(sf)
|
| 32 |
+
library(DT)
|
| 33 |
+
library(RColorBrewer)
|
| 34 |
+
library(terra)
|
| 35 |
+
library(data.table) # for fread
|
| 36 |
+
library(mapview) # for mapview objects
|
| 37 |
+
library(sjPlot) # for plotting lm model coefficients
|
| 38 |
+
library(sjlabelled) # optional if needed for sjPlot
|
| 39 |
+
require(bslib)
|
| 40 |
+
require(shinycssloaders)
|
| 41 |
+
source('R/setup.R')
|
| 42 |
+
# Global theme definition
|
| 43 |
+
theme <- bs_theme(
|
| 44 |
+
bootswatch = "flatly",
|
| 45 |
+
base_font = font_google("Roboto"),
|
| 46 |
+
heading_font = font_google("Roboto Slab"),
|
| 47 |
+
bg = "#f8f9fa",
|
| 48 |
+
fg = "#212529"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# ------------------------------------------------
|
| 52 |
+
# 3) UI
|
| 53 |
+
# ------------------------------------------------
|
| 54 |
+
ui <- fluidPage(
|
| 55 |
+
theme = theme, # Introduce a theme from bslib
|
| 56 |
+
|
| 57 |
+
# For dynamically show and hide a 'Calculating' message
|
| 58 |
+
useShinyjs(), # Initialize shinyjs
|
| 59 |
+
div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
|
| 60 |
+
titlePanel("San Francisco Biodiversity Access Decision Support Tool"),
|
| 61 |
+
p('Explore your local biodiversity and your access to it!'),
|
| 62 |
+
fluidRow(
|
| 63 |
+
column(
|
| 64 |
+
width = 12, align = "center",
|
| 65 |
+
tags$img(src = "UC Berkeley_logo.png",
|
| 66 |
+
height = "120px", style = "margin:10px;"),
|
| 67 |
+
tags$img(src = "California_academy_logo.png",
|
| 68 |
+
height = "120px", style = "margin:10px;"),
|
| 69 |
+
tags$img(src = "Reimagining_San_Francisco.png",
|
| 70 |
+
height = "120px", style = "margin:10px;")
|
| 71 |
+
),
|
| 72 |
+
theme=bs_theme(bootswatch='yeti')
|
| 73 |
+
),
|
| 74 |
+
|
| 75 |
+
fluidRow(
|
| 76 |
+
column(
|
| 77 |
+
width = 12,
|
| 78 |
+
br(),
|
| 79 |
+
p("This application demonstrates an approach for exploring biodiversity access in San Francisco..."),
|
| 80 |
+
# (Your summary text can go here)
|
| 81 |
+
)
|
| 82 |
+
),
|
| 83 |
+
br(),
|
| 84 |
+
fluidRow(
|
| 85 |
+
column(
|
| 86 |
+
width = 12,
|
| 87 |
+
br(),
|
| 88 |
+
tags$b("App Summary (Fill out with RSF data working group):"),
|
| 89 |
+
# Increasingly, we ask ourselves about what increasing access to biodiversity really means.
|
| 90 |
+
# Importantly, accessibility differs from human mobility in urban planning studies for equitable transportation systems.
|
| 91 |
+
p("
|
| 92 |
+
This application allows users to either click on a map or geocode an address (in progress)
|
| 93 |
+
to generate travel-time isochrones across multiple transportation modes (e.g., pedestrian, cycling, driving, driving during traffic).
|
| 94 |
+
It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
|
| 95 |
+
and summarizes biodiversity records from GBIF. We explore what biodiversity access means
|
| 96 |
+
Users can explore information that we often relate to biodiversity in urban environments including greenspace coverage, population estimates, and species diversity within each isochrone."),
|
| 97 |
+
|
| 98 |
+
tags$b("Created by:"),
|
| 99 |
+
p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
|
| 100 |
+
|
| 101 |
+
p("Contact Information",
|
| 102 |
+
strong("diego.ellissoto@berkeley.edu"))
|
| 103 |
+
|
| 104 |
+
)
|
| 105 |
+
),
|
| 106 |
+
br(),
|
| 107 |
+
# fluidRow(
|
| 108 |
+
# column(
|
| 109 |
+
# width = 6 , # quitar
|
| 110 |
+
tabsetPanel(
|
| 111 |
+
|
| 112 |
+
# 1) Isochrone Explorer
|
| 113 |
+
tabPanel("Isochrone Explorer",
|
| 114 |
+
sidebarLayout(
|
| 115 |
+
sidebarPanel(
|
| 116 |
+
radioButtons(
|
| 117 |
+
"location_choice",
|
| 118 |
+
"Select how to choose your location:",
|
| 119 |
+
choices = c("Address (Geocode)" = "address",
|
| 120 |
+
"Click on Map" = "map_click"),
|
| 121 |
+
selected = "map_click"
|
| 122 |
+
),
|
| 123 |
+
|
| 124 |
+
conditionalPanel(
|
| 125 |
+
condition = "input.location_choice == 'address'",
|
| 126 |
+
textInput(
|
| 127 |
+
"user_address",
|
| 128 |
+
"Enter Address:",
|
| 129 |
+
value = "",
|
| 130 |
+
placeholder = "e.g., 1600 Amphitheatre Parkway, Mountain View, CA"
|
| 131 |
+
)
|
| 132 |
+
),
|
| 133 |
+
|
| 134 |
+
checkboxGroupInput(
|
| 135 |
+
"transport_modes",
|
| 136 |
+
"Select Transportation Modes:",
|
| 137 |
+
choices = list("Driving" = "driving",
|
| 138 |
+
"Walking" = "walking",
|
| 139 |
+
"Cycling" = "cycling",
|
| 140 |
+
"Driving with Traffic"= "driving-traffic"),
|
| 141 |
+
selected = c("driving", "walking")
|
| 142 |
+
),
|
| 143 |
+
|
| 144 |
+
checkboxGroupInput(
|
| 145 |
+
"iso_times",
|
| 146 |
+
"Select Isochrone Times (minutes):",
|
| 147 |
+
choices = list("5" = 5, "10" = 10, "15" = 15),
|
| 148 |
+
selected = c(5, 10)
|
| 149 |
+
),
|
| 150 |
+
|
| 151 |
+
actionButton("generate_iso", "Generate Isochrones"),
|
| 152 |
+
actionButton("clear_map", "Clear")
|
| 153 |
+
|
| 154 |
+
),
|
| 155 |
+
|
| 156 |
+
mainPanel(
|
| 157 |
+
leafletOutput("isoMap", height = 600),
|
| 158 |
+
|
| 159 |
+
fluidRow(
|
| 160 |
+
column(12,
|
| 161 |
+
br(),
|
| 162 |
+
uiOutput("bioScoreBox"),
|
| 163 |
+
br(),
|
| 164 |
+
uiOutput("closestGreenspaceUI")
|
| 165 |
+
)
|
| 166 |
+
),
|
| 167 |
+
|
| 168 |
+
br(),
|
| 169 |
+
DTOutput("dataTable") %>% withSpinner(type = 8, color = "#337ab7"),
|
| 170 |
+
|
| 171 |
+
br(),
|
| 172 |
+
br(),
|
| 173 |
+
fluidRow(
|
| 174 |
+
column(12,
|
| 175 |
+
plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#337ab7")
|
| 176 |
+
)
|
| 177 |
+
),
|
| 178 |
+
|
| 179 |
+
br(),
|
| 180 |
+
br(),
|
| 181 |
+
br(),
|
| 182 |
+
fluidRow(
|
| 183 |
+
column(12,
|
| 184 |
+
plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#f39c12")
|
| 185 |
+
)
|
| 186 |
+
)
|
| 187 |
+
)
|
| 188 |
+
)
|
| 189 |
+
),
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ), # end of column wifth
|
| 193 |
+
#br.?
|
| 194 |
+
# column(
|
| 195 |
+
# width=6,
|
| 196 |
+
tabPanel(
|
| 197 |
+
"GBIF Summaries",
|
| 198 |
+
sidebarLayout(
|
| 199 |
+
sidebarPanel(
|
| 200 |
+
selectInput(
|
| 201 |
+
"class_filter",
|
| 202 |
+
"Select a GBIF Class to Summarize:",
|
| 203 |
+
choices = c("All", sort(unique(sf_gbif$class))),
|
| 204 |
+
selected = "All"
|
| 205 |
+
),
|
| 206 |
+
selectInput(
|
| 207 |
+
"family_filter",
|
| 208 |
+
"Filter by Family (optional):",
|
| 209 |
+
choices = c("All", sort(unique(sf_gbif$family))),
|
| 210 |
+
selected = "All"
|
| 211 |
+
)
|
| 212 |
+
),
|
| 213 |
+
mainPanel(
|
| 214 |
+
DTOutput("classTable"),
|
| 215 |
+
br(),
|
| 216 |
+
h3("Observations vs. Species Richness"),
|
| 217 |
+
plotOutput("obsVsSpeciesPlot", height = "300px"),
|
| 218 |
+
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
| 219 |
+
)
|
| 220 |
+
)
|
| 221 |
+
) %>% withSpinner(type = 8, color = "#337ab7")
|
| 222 |
+
),
|
| 223 |
+
# )
|
| 224 |
+
|
| 225 |
+
# ),
|
| 226 |
+
|
| 227 |
+
fluidRow(
|
| 228 |
+
column(
|
| 229 |
+
width = 12,
|
| 230 |
+
tags$b("Reimagining San Francisco (Fill out with CAS):"),
|
| 231 |
+
p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
|
| 232 |
+
and technological dimensions to shape a sustainable future for the Bay Area.
|
| 233 |
+
This collaboration unites diverse stakeholders to explore innovations in urban planning,
|
| 234 |
+
conservation, and community engagement. The Reimagining San Francisco Data Working Group has been tasked with identifying and integrating multiple sources of socio-ecological biodiversity information in a co-development framework."),
|
| 235 |
+
|
| 236 |
+
tags$b("Why Biodiversity Access Matters (Polish this):"),
|
| 237 |
+
p("Ensuring equitable access to biodiversity is essential for human well-being,
|
| 238 |
+
ecological resilience, and global policy decisions related to conservation.
|
| 239 |
+
Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
|
| 240 |
+
and provide cultural, recreational, and health benefits to local communities.
|
| 241 |
+
Recognizing that cities are particularly complex socio-ecological systems facing both legacies of sociocultural practices as well as current ongoing dynamic human activities and pressures.
|
| 242 |
+
Incorporating multiple facets of biodiversity metrics alongside variables employed by city planners, human geographers, and decision-makers into urban planning will allow a more integrative lens in creating a sustainable future for cities and their residents."),
|
| 243 |
+
|
| 244 |
+
tags$b("How We Calculate Biodiversity Access Percentile:"),
|
| 245 |
+
p("Total unique species found within the user-generated isochrone.
|
| 246 |
+
We then compare that value to the distribution of unique species counts across all census block groups,
|
| 247 |
+
converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
|
| 248 |
+
A higher percentile indicates greater biodiversity within the chosen area,
|
| 249 |
+
relative to other parts of the city or region.")
|
| 250 |
+
),
|
| 251 |
+
|
| 252 |
+
tags$b("Next Steps:"),
|
| 253 |
+
tags$ul(
|
| 254 |
+
tags$li("Add impervious surface"),
|
| 255 |
+
tags$li("National walkability score"),
|
| 256 |
+
tags$li("Social vulnerability score"),
|
| 257 |
+
tags$li("NatureServe biodiversity maps"),
|
| 258 |
+
tags$li("Calculate cold-hotspots within ggregation of H6 bins instead of by census block group: Ask Carl"),
|
| 259 |
+
tags$li("Species range maps"),
|
| 260 |
+
tags$li("Add common name GBIF"),
|
| 261 |
+
tags$li("Partner orgs"),
|
| 262 |
+
tags$li("Optimize speed -> store variables -> H-ify the world?"),
|
| 263 |
+
tags$li("Brainstorm and co-develop the biodiversity access score"),
|
| 264 |
+
tags$li("For the GBIF summaries, add an annotated GBIF_sf with environmental variables so we can see landcover type association across the biodiversity within the isochrone.")
|
| 265 |
+
)
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# )
|
| 271 |
+
|
| 272 |
+
# Separate section for the plot outside of the "GBIF Summaries" tab
|
| 273 |
+
|
| 274 |
+
# tabsetPanel(
|
| 275 |
+
|
| 276 |
+
# # 1) Isochrone Explorer
|
| 277 |
+
# tabPanel(
|
| 278 |
+
# mainPanel(
|
| 279 |
+
# DTOutput("classTable"),
|
| 280 |
+
# br(),
|
| 281 |
+
# fluidRow(
|
| 282 |
+
# column(
|
| 283 |
+
# 6,
|
| 284 |
+
# # A simple scatter or line plot for n_observations vs n_species
|
| 285 |
+
# plotOutput("obsVsSpeciesPlot", height = "300px")
|
| 286 |
+
# )
|
| 287 |
+
# # ,
|
| 288 |
+
# # column(
|
| 289 |
+
# # 6,
|
| 290 |
+
# # # A regression model plot using sjPlot
|
| 291 |
+
# # plotOutput("lmCoefficientsPlot", height = "300px")
|
| 292 |
+
# # )
|
| 293 |
+
# )
|
| 294 |
+
# )
|
| 295 |
+
# )
|
| 296 |
+
# ),
|
| 297 |
+
#
|
| 298 |
+
# br()
|
| 299 |
+
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
# fluidRow(
|
| 304 |
+
# column(
|
| 305 |
+
# 12,
|
| 306 |
+
# tags$h3("Species Richness vs Data Availability"),
|
| 307 |
+
# fluidRow(
|
| 308 |
+
# column(6, uiOutput("mapNUI")),
|
| 309 |
+
# column(6, uiOutput("mapSpeciesUI"))
|
| 310 |
+
# )
|
| 311 |
+
# )
|
| 312 |
+
# )
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# ------------------------------------------------
|
| 316 |
+
# 4) Server
|
| 317 |
+
# ------------------------------------------------
|
| 318 |
+
server <- function(input, output, session) {
|
| 319 |
+
|
| 320 |
+
chosen_point <- reactiveVal(NULL)
|
| 321 |
+
|
| 322 |
+
# ------------------------------------------------
|
| 323 |
+
# Leaflet Base + Hide Overlays
|
| 324 |
+
# ------------------------------------------------
|
| 325 |
+
output$isoMap <- renderLeaflet({
|
| 326 |
+
pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
|
| 327 |
+
|
| 328 |
+
pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
|
| 329 |
+
# 2) Color palette for data availability
|
| 330 |
+
pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
leaflet() %>%
|
| 334 |
+
addTiles(group = "Street Map (Default)") %>%
|
| 335 |
+
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
|
| 336 |
+
addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
|
| 337 |
+
|
| 338 |
+
addPolygons(
|
| 339 |
+
data = cbg_vect_sf,
|
| 340 |
+
group = "Income",
|
| 341 |
+
# fillColor = ~pal_cbg(unique_species),
|
| 342 |
+
fillColor = ~pal_cbg(medincE),
|
| 343 |
+
fillOpacity = 0.6,
|
| 344 |
+
color = "white",
|
| 345 |
+
weight = 1,
|
| 346 |
+
# label = "Income",
|
| 347 |
+
label=~GEOID,
|
| 348 |
+
highlightOptions = highlightOptions(
|
| 349 |
+
weight = 5,
|
| 350 |
+
color = "blue",
|
| 351 |
+
fillOpacity = 0.5,
|
| 352 |
+
bringToFront = TRUE
|
| 353 |
+
),
|
| 354 |
+
labelOptions = labelOptions(
|
| 355 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 356 |
+
textsize = "12px",
|
| 357 |
+
direction = "auto"
|
| 358 |
+
)
|
| 359 |
+
) %>%
|
| 360 |
+
|
| 361 |
+
addPolygons(
|
| 362 |
+
data = osm_greenspace,
|
| 363 |
+
group = "Greenspace",
|
| 364 |
+
fillColor = "darkgreen",
|
| 365 |
+
fillOpacity = 0.3,
|
| 366 |
+
color = "green",
|
| 367 |
+
weight = 1,
|
| 368 |
+
label = ~name,
|
| 369 |
+
highlightOptions = highlightOptions(
|
| 370 |
+
weight = 5,
|
| 371 |
+
color = "blue",
|
| 372 |
+
fillOpacity = 0.5,
|
| 373 |
+
bringToFront = TRUE
|
| 374 |
+
),
|
| 375 |
+
labelOptions = labelOptions(
|
| 376 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 377 |
+
textsize = "12px",
|
| 378 |
+
direction = "auto"
|
| 379 |
+
)
|
| 380 |
+
) %>%
|
| 381 |
+
|
| 382 |
+
addPolygons(
|
| 383 |
+
data = biodiv_hotspots,
|
| 384 |
+
group = "Hotspots (KnowBR)",
|
| 385 |
+
fillColor = "firebrick",
|
| 386 |
+
fillOpacity = 0.2,
|
| 387 |
+
color = "firebrick",
|
| 388 |
+
weight = 2,
|
| 389 |
+
label = ~GEOID,
|
| 390 |
+
highlightOptions = highlightOptions(
|
| 391 |
+
weight = 5,
|
| 392 |
+
color = "blue",
|
| 393 |
+
fillOpacity = 0.5,
|
| 394 |
+
bringToFront = TRUE
|
| 395 |
+
),
|
| 396 |
+
labelOptions = labelOptions(
|
| 397 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 398 |
+
textsize = "12px",
|
| 399 |
+
direction = "auto"
|
| 400 |
+
)
|
| 401 |
+
) %>%
|
| 402 |
+
|
| 403 |
+
addPolygons(
|
| 404 |
+
data = biodiv_coldspots,
|
| 405 |
+
group = "Coldspots (KnowBR)",
|
| 406 |
+
fillColor = "navyblue",
|
| 407 |
+
fillOpacity = 0.2,
|
| 408 |
+
color = "navyblue",
|
| 409 |
+
weight = 2,
|
| 410 |
+
label = ~GEOID,
|
| 411 |
+
highlightOptions = highlightOptions(
|
| 412 |
+
weight = 5,
|
| 413 |
+
color = "blue",
|
| 414 |
+
fillOpacity = 0.5,
|
| 415 |
+
bringToFront = TRUE
|
| 416 |
+
),
|
| 417 |
+
labelOptions = labelOptions(
|
| 418 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 419 |
+
textsize = "12px",
|
| 420 |
+
direction = "auto"
|
| 421 |
+
)
|
| 422 |
+
) %>%
|
| 423 |
+
|
| 424 |
+
# Add richness and nobs
|
| 425 |
+
# -- Richness layer
|
| 426 |
+
addPolygons(
|
| 427 |
+
data = cbg_vect_sf,
|
| 428 |
+
group = "Species Richness",
|
| 429 |
+
fillColor = ~pal_rich(unique_species),
|
| 430 |
+
fillOpacity = 0.6,
|
| 431 |
+
color = "white",
|
| 432 |
+
weight = 1,
|
| 433 |
+
label =~unique_species,
|
| 434 |
+
popup = ~paste0(
|
| 435 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 436 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 437 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 438 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 439 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 440 |
+
)
|
| 441 |
+
) %>%
|
| 442 |
+
|
| 443 |
+
# -- Data Availability layer
|
| 444 |
+
addPolygons(
|
| 445 |
+
data = cbg_vect_sf,
|
| 446 |
+
group = "Data Availability",
|
| 447 |
+
fillColor = ~pal_data(n_observations),
|
| 448 |
+
fillOpacity = 0.6,
|
| 449 |
+
color = "white",
|
| 450 |
+
weight = 1,
|
| 451 |
+
label =~n_observations,
|
| 452 |
+
popup = ~paste0(
|
| 453 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 454 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 455 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 456 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 457 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 458 |
+
)
|
| 459 |
+
) %>%
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
|
| 463 |
+
addLayersControl(
|
| 464 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 465 |
+
overlayGroups = c("Income", "Greenspace","Species Richness", "Data Availability",
|
| 466 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)"),
|
| 467 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 468 |
+
) %>%
|
| 469 |
+
hideGroup("Income") %>%
|
| 470 |
+
hideGroup("Greenspace") %>%
|
| 471 |
+
hideGroup("Hotspots (KnowBR)") %>%
|
| 472 |
+
hideGroup("Coldspots (KnowBR)") %>%
|
| 473 |
+
hideGroup("Species Richness") %>%
|
| 474 |
+
hideGroup("Data Availability")
|
| 475 |
+
})
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
# ------------------------------------------------
|
| 479 |
+
# Observe map clicks (location_choice = 'map_click')
|
| 480 |
+
# ------------------------------------------------
|
| 481 |
+
observeEvent(input$isoMap_click, {
|
| 482 |
+
req(input$location_choice == "map_click")
|
| 483 |
+
click <- input$isoMap_click
|
| 484 |
+
if (!is.null(click)) {
|
| 485 |
+
chosen_point(c(lon = click$lng, lat = click$lat))
|
| 486 |
+
leafletProxy("isoMap") %>%
|
| 487 |
+
clearMarkers() %>%
|
| 488 |
+
addCircleMarkers(
|
| 489 |
+
lng = click$lng, lat = click$lat,
|
| 490 |
+
radius = 6, color = "firebrick",
|
| 491 |
+
label = "Map Click Location"
|
| 492 |
+
)
|
| 493 |
+
}
|
| 494 |
+
})
|
| 495 |
+
|
| 496 |
+
# ------------------------------------------------
|
| 497 |
+
# Observe clearinf of map
|
| 498 |
+
# ------------------------------------------------
|
| 499 |
+
observeEvent(input$clear_map, {
|
| 500 |
+
# Reset the chosen point
|
| 501 |
+
chosen_point(NULL)
|
| 502 |
+
|
| 503 |
+
# Clear all markers and isochrones from the map
|
| 504 |
+
leafletProxy("isoMap") %>%
|
| 505 |
+
clearMarkers() %>%
|
| 506 |
+
clearShapes() %>%
|
| 507 |
+
clearGroup("Isochrones") %>%
|
| 508 |
+
clearGroup("NDVI Raster")
|
| 509 |
+
|
| 510 |
+
# Optional: Reset any other reactive values if needed
|
| 511 |
+
showNotification("Map cleared. You can select a new location.")
|
| 512 |
+
})
|
| 513 |
+
|
| 514 |
+
# ------------------------------------------------
|
| 515 |
+
# Generate Isochrones
|
| 516 |
+
# ------------------------------------------------
|
| 517 |
+
isochrones_data <- eventReactive(input$generate_iso, {
|
| 518 |
+
|
| 519 |
+
leafletProxy("isoMap") %>%
|
| 520 |
+
clearGroup("Isochrones") %>%
|
| 521 |
+
clearGroup("NDVI Raster")
|
| 522 |
+
|
| 523 |
+
# If user selected address:
|
| 524 |
+
if (input$location_choice == "address") {
|
| 525 |
+
if (nchar(input$user_address) < 5) {
|
| 526 |
+
showNotification("Please enter a more complete address.", type = "error")
|
| 527 |
+
return(NULL)
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
loc_df <- tryCatch({
|
| 531 |
+
mb_geocode(input$user_address, access_token = mapbox_token)
|
| 532 |
+
}, error = function(e) {
|
| 533 |
+
showNotification(paste("Geocoding failed:", e$message), type = "error")
|
| 534 |
+
NULL
|
| 535 |
+
})
|
| 536 |
+
|
| 537 |
+
# Check for valid lat/lon
|
| 538 |
+
if (is.null(loc_df) || nrow(loc_df) == 0 || is.na(loc_df$lon[1]) || is.na(loc_df$lat[1])) {
|
| 539 |
+
showNotification("No valid geocoding results found.", type = "warning")
|
| 540 |
+
return(NULL)
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
chosen_point(c(lon = loc_df$lon[1], lat = loc_df$lat[1]))
|
| 544 |
+
|
| 545 |
+
leafletProxy("isoMap") %>%
|
| 546 |
+
clearMarkers() %>%
|
| 547 |
+
addCircleMarkers(
|
| 548 |
+
lng = loc_df$lon[1], lat = loc_df$lat[1],
|
| 549 |
+
radius = 6, color = "navyblue",
|
| 550 |
+
label = "Geocoded Address"
|
| 551 |
+
) %>%
|
| 552 |
+
setView(lng = loc_df$lon[1], lat = loc_df$lat[1], zoom = 13)
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
pt <- chosen_point()
|
| 556 |
+
if (is.null(pt)) {
|
| 557 |
+
showNotification("No location selected! Provide an address or click the map.", type = "error")
|
| 558 |
+
return(NULL)
|
| 559 |
+
}
|
| 560 |
+
if (length(input$transport_modes) == 0) {
|
| 561 |
+
showNotification("Select at least one transportation mode.", type = "error")
|
| 562 |
+
return(NULL)
|
| 563 |
+
}
|
| 564 |
+
if (length(input$iso_times) == 0) {
|
| 565 |
+
showNotification("Select at least one isochrone time.", type = "error")
|
| 566 |
+
return(NULL)
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
location_sf <- st_as_sf(
|
| 570 |
+
data.frame(lon = pt["lon"], lat = pt["lat"]),
|
| 571 |
+
coords = c("lon","lat"), crs = 4326
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
iso_list <- list()
|
| 575 |
+
for (mode in input$transport_modes) {
|
| 576 |
+
for (t in input$iso_times) {
|
| 577 |
+
iso <- tryCatch({
|
| 578 |
+
mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
|
| 579 |
+
access_token = mapbox_token)
|
| 580 |
+
}, error = function(e) {
|
| 581 |
+
showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
|
| 582 |
+
NULL
|
| 583 |
+
})
|
| 584 |
+
if (!is.null(iso)) {
|
| 585 |
+
iso$mode <- mode
|
| 586 |
+
iso$time <- t
|
| 587 |
+
iso_list <- append(iso_list, list(iso))
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
if (length(iso_list) == 0) {
|
| 592 |
+
showNotification("No isochrones generated.", type = "warning")
|
| 593 |
+
return(NULL)
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
|
| 597 |
+
all_iso
|
| 598 |
+
})
|
| 599 |
+
|
| 600 |
+
# ------------------------------------------------
|
| 601 |
+
# Plot Isochrones + NDVI
|
| 602 |
+
# ------------------------------------------------
|
| 603 |
+
observeEvent(isochrones_data(), {
|
| 604 |
+
iso_data <- isochrones_data()
|
| 605 |
+
req(iso_data)
|
| 606 |
+
|
| 607 |
+
iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
|
| 608 |
+
pal <- colorRampPalette(brewer.pal(8, "Set2"))
|
| 609 |
+
cols <- pal(nrow(iso_data))
|
| 610 |
+
|
| 611 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 612 |
+
poly_i <- iso_data[i, ]
|
| 613 |
+
leafletProxy("isoMap") %>%
|
| 614 |
+
addPolygons(
|
| 615 |
+
data = poly_i,
|
| 616 |
+
group = "Isochrones",
|
| 617 |
+
color = cols[i],
|
| 618 |
+
weight = 2,
|
| 619 |
+
fillOpacity = 0.4,
|
| 620 |
+
label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
|
| 621 |
+
)
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
iso_union <- st_union(iso_data)
|
| 625 |
+
iso_union_vect <- vect(iso_union)
|
| 626 |
+
ndvi_crop <- crop(ndvi, iso_union_vect)
|
| 627 |
+
ndvi_mask <- mask(ndvi_crop, iso_union_vect)
|
| 628 |
+
ndvi_vals <- values(ndvi_mask)
|
| 629 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 630 |
+
|
| 631 |
+
# Could be removed ####
|
| 632 |
+
if (length(ndvi_vals) > 0) {
|
| 633 |
+
ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
|
| 634 |
+
|
| 635 |
+
leafletProxy("isoMap") %>%
|
| 636 |
+
addRasterImage(
|
| 637 |
+
x = ndvi_mask,
|
| 638 |
+
colors = ndvi_pal,
|
| 639 |
+
opacity = 0.7,
|
| 640 |
+
project = TRUE,
|
| 641 |
+
group = "NDVI Raster"
|
| 642 |
+
) %>%
|
| 643 |
+
addLegend(
|
| 644 |
+
position = "bottomright",
|
| 645 |
+
pal = ndvi_pal,
|
| 646 |
+
values = ndvi_vals,
|
| 647 |
+
title = "NDVI"
|
| 648 |
+
)
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
leafletProxy("isoMap") %>%
|
| 652 |
+
addLayersControl(
|
| 653 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 654 |
+
overlayGroups = c("Income", "Greenspace",
|
| 655 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
| 656 |
+
"Isochrones", "NDVI Raster"),
|
| 657 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 658 |
+
)
|
| 659 |
+
})
|
| 660 |
+
|
| 661 |
+
# ------------------------------------------------
|
| 662 |
+
# socio_data Reactive + Summaries
|
| 663 |
+
# ------------------------------------------------
|
| 664 |
+
socio_data <- reactive({
|
| 665 |
+
iso_data <- isochrones_data()
|
| 666 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 667 |
+
return(data.frame())
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
acs_wide <- cbg_vect_sf %>%
|
| 671 |
+
mutate(
|
| 672 |
+
population = popE,
|
| 673 |
+
med_income = medincE
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
hotspot_union <- st_union(biodiv_hotspots)
|
| 677 |
+
coldspot_union <- st_union(biodiv_coldspots)
|
| 678 |
+
|
| 679 |
+
results <- data.frame()
|
| 680 |
+
|
| 681 |
+
# Calculate distance to coldspot and hotspots
|
| 682 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 683 |
+
poly_i <- iso_data[i, ]
|
| 684 |
+
|
| 685 |
+
dist_hot <- st_distance(poly_i, hotspot_union)
|
| 686 |
+
dist_cold <- st_distance(poly_i, coldspot_union)
|
| 687 |
+
dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
|
| 688 |
+
dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
|
| 689 |
+
|
| 690 |
+
inter_acs <- st_intersection(acs_wide, poly_i)
|
| 691 |
+
#
|
| 692 |
+
vect_acs_wide <- vect(acs_wide)
|
| 693 |
+
vect_poly_i <- vect(poly_i)
|
| 694 |
+
inter_acs <- intersect(vect_acs_wide, vect_poly_i)
|
| 695 |
+
inter_acs = st_as_sf(inter_acs)
|
| 696 |
+
#
|
| 697 |
+
|
| 698 |
+
pop_total <- 0
|
| 699 |
+
inc_str <- "N/A"
|
| 700 |
+
if (nrow(inter_acs) > 0) {
|
| 701 |
+
inter_acs$area <- st_area(inter_acs)
|
| 702 |
+
inter_acs$area_num <- as.numeric(inter_acs$area)
|
| 703 |
+
inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
|
| 704 |
+
inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
|
| 705 |
+
|
| 706 |
+
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
| 707 |
+
|
| 708 |
+
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
| 709 |
+
sum(inter_acs$area_num, na.rm = TRUE)
|
| 710 |
+
if (!is.na(w_income) && w_income > 0) {
|
| 711 |
+
inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
# inter_gs <- st_intersection(osm_greenspace, poly_i)
|
| 716 |
+
|
| 717 |
+
vec_osm_greenspace = vect(osm_greenspace)
|
| 718 |
+
vect_poly_i <- vect(poly_i)
|
| 719 |
+
inter_gs <- intersect(vec_osm_greenspace, vect_poly_i)
|
| 720 |
+
inter_gs = st_as_sf(inter_gs)
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
|
| 724 |
+
gs_area_m2 <- 0
|
| 725 |
+
if (nrow(inter_gs) > 0) {
|
| 726 |
+
gs_area_m2 <- sum(st_area(inter_gs))
|
| 727 |
+
}
|
| 728 |
+
iso_area_m2 <- as.numeric(st_area(poly_i))
|
| 729 |
+
gs_area_m2 <- as.numeric(gs_area_m2)
|
| 730 |
+
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
| 731 |
+
|
| 732 |
+
poly_vect <- vect(poly_i)
|
| 733 |
+
ndvi_crop <- crop(ndvi, poly_vect)
|
| 734 |
+
ndvi_mask <- mask(ndvi_crop, poly_vect)
|
| 735 |
+
ndvi_vals <- values(ndvi_mask)
|
| 736 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 737 |
+
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
|
| 738 |
+
|
| 739 |
+
# inter_gbif <- st_intersection(sf_gbif, poly_i)
|
| 740 |
+
|
| 741 |
+
vect_poly_i = vect(poly_i)
|
| 742 |
+
|
| 743 |
+
inter_gbif = intersect(vect_gbif,vect_poly_i)
|
| 744 |
+
inter_gbif = st_as_sf(inter_gbif)
|
| 745 |
+
# inter_gbif <- st_intersection(sf_gbif, poly_i)
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
inter_gbif_acs = sf_gbif |> dplyr::mutate(income = medincE,
|
| 749 |
+
ndvi = ndvi_sentinel)
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
n_records <- nrow(inter_gbif)
|
| 753 |
+
n_species <- length(unique(inter_gbif$species))
|
| 754 |
+
|
| 755 |
+
n_birds <- length(unique(inter_gbif$species[ inter_gbif$class == "Aves" ]))
|
| 756 |
+
n_mammals <- length(unique(inter_gbif$species[ inter_gbif$class == "Mammalia" ]))
|
| 757 |
+
n_plants <- length(unique(inter_gbif$species[ inter_gbif$class %in%
|
| 758 |
+
c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
|
| 759 |
+
"Equisetopsida","Bryopsida","Marchantiopsida") ]))
|
| 760 |
+
|
| 761 |
+
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
| 762 |
+
# iso_area_sqm <- round(iso_area_m2, 2)
|
| 763 |
+
|
| 764 |
+
row_i <- data.frame(
|
| 765 |
+
Mode = tools::toTitleCase(poly_i$mode),
|
| 766 |
+
Time = poly_i$time,
|
| 767 |
+
# IsochroneArea_m2 = iso_area_sqm,
|
| 768 |
+
IsochroneArea_km2 = iso_area_km2,
|
| 769 |
+
DistToHotspot_km = dist_hot_km,
|
| 770 |
+
DistToColdspot_km = dist_cold_km,
|
| 771 |
+
EstimatedPopulation = pop_total,
|
| 772 |
+
MedianIncome = inc_str,
|
| 773 |
+
MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
|
| 774 |
+
GBIF_Records = n_records,
|
| 775 |
+
GBIF_Species = n_species,
|
| 776 |
+
Bird_Species = n_birds,
|
| 777 |
+
Mammal_Species = n_mammals,
|
| 778 |
+
Plant_Species = n_plants,
|
| 779 |
+
Greenspace_m2 = round(gs_area_m2, 2),
|
| 780 |
+
Greenspace_percent = round(gs_percent, 2),
|
| 781 |
+
stringsAsFactors = FALSE
|
| 782 |
+
)
|
| 783 |
+
results <- rbind(results, row_i)
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
iso_union <- st_union(iso_data)
|
| 787 |
+
|
| 788 |
+
# inter_all_gbif <- st_intersection(sf_gbif, iso_union)
|
| 789 |
+
|
| 790 |
+
# vect_gbif <- vect(sf_gbif)
|
| 791 |
+
vect_iso <- vect(iso_union)
|
| 792 |
+
inter_all_gbif <- intersect(vect_gbif, vect_iso)
|
| 793 |
+
inter_all_gbif = st_as_sf(inter_all_gbif)
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
union_n_species <- length(unique(inter_all_gbif$species))
|
| 797 |
+
rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
| 798 |
+
attr(results, "bio_percentile") <- rank_percentile
|
| 799 |
+
|
| 800 |
+
# Closest Greenspace from ANY part of the isochrone
|
| 801 |
+
dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
|
| 802 |
+
if (length(dist_mat) > 0) {
|
| 803 |
+
min_dist <- min(dist_mat)
|
| 804 |
+
min_idx <- which.min(dist_mat)
|
| 805 |
+
gs_name <- osm_greenspace$name[min_idx]
|
| 806 |
+
attr(results, "closest_greenspace") <- gs_name
|
| 807 |
+
} else {
|
| 808 |
+
attr(results, "closest_greenspace") <- "None"
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
results
|
| 812 |
+
})
|
| 813 |
+
|
| 814 |
+
# ------------------------------------------------
|
| 815 |
+
# Render main summary table
|
| 816 |
+
# ------------------------------------------------
|
| 817 |
+
output$dataTable <- renderDT({
|
| 818 |
+
df <- socio_data()
|
| 819 |
+
if (nrow(df) == 0) {
|
| 820 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 821 |
+
}
|
| 822 |
+
DT::datatable(
|
| 823 |
+
df,
|
| 824 |
+
colnames = c(
|
| 825 |
+
"Mode" = "Mode",
|
| 826 |
+
"Time (min)" = "Time",
|
| 827 |
+
# "Area (m²)" = "IsochroneArea_m2",
|
| 828 |
+
"Area (km²)" = "IsochroneArea_km2",
|
| 829 |
+
"Dist. Hotspot (km)" = "DistToHotspot_km",
|
| 830 |
+
"Dist. Coldspot (km)" = "DistToColdspot_km",
|
| 831 |
+
"Population" = "EstimatedPopulation",
|
| 832 |
+
"Median Income" = "MedianIncome",
|
| 833 |
+
"Mean NDVI" = "MeanNDVI",
|
| 834 |
+
"GBIF Records" = "GBIF_Records",
|
| 835 |
+
"Unique Species" = "GBIF_Species",
|
| 836 |
+
"Bird Species" = "Bird_Species",
|
| 837 |
+
"Mammal Species" = "Mammal_Species",
|
| 838 |
+
"Plant Species" = "Plant_Species",
|
| 839 |
+
"Greenspace (m²)" = "Greenspace_m2",
|
| 840 |
+
"Greenspace (%)" = "Greenspace_percent"
|
| 841 |
+
),
|
| 842 |
+
options = list(pageLength = 10, autoWidth = TRUE),
|
| 843 |
+
rownames = FALSE
|
| 844 |
+
)
|
| 845 |
+
})
|
| 846 |
+
|
| 847 |
+
# ------------------------------------------------
|
| 848 |
+
# Biodiversity Access Score + Closest Greenspace
|
| 849 |
+
# ------------------------------------------------
|
| 850 |
+
output$bioScoreBox <- renderUI({
|
| 851 |
+
df <- socio_data()
|
| 852 |
+
if (nrow(df) == 0) return(NULL)
|
| 853 |
+
|
| 854 |
+
percentile <- attr(df, "bio_percentile")
|
| 855 |
+
if (is.null(percentile)) percentile <- "N/A"
|
| 856 |
+
else percentile <- paste0(percentile, "th Percentile")
|
| 857 |
+
|
| 858 |
+
wellPanel(
|
| 859 |
+
HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
|
| 860 |
+
)
|
| 861 |
+
})
|
| 862 |
+
|
| 863 |
+
output$closestGreenspaceUI <- renderUI({
|
| 864 |
+
df <- socio_data()
|
| 865 |
+
if (nrow(df) == 0) return(NULL)
|
| 866 |
+
gs_name <- attr(df, "closest_greenspace")
|
| 867 |
+
if (is.null(gs_name)) gs_name <- "None"
|
| 868 |
+
|
| 869 |
+
tagList(
|
| 870 |
+
strong("Closest Greenspace (from any part of the Isochrone):"),
|
| 871 |
+
p(gs_name)
|
| 872 |
+
)
|
| 873 |
+
})
|
| 874 |
+
|
| 875 |
+
# ------------------------------------------------
|
| 876 |
+
# Secondary table: user-selected CLASS & FAMILY
|
| 877 |
+
# ------------------------------------------------
|
| 878 |
+
output$classTable <- renderDT({
|
| 879 |
+
iso_data <- isochrones_data()
|
| 880 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 881 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
iso_union <- st_union(iso_data)
|
| 885 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
vect_iso <- vect(iso_union)
|
| 889 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
| 890 |
+
inter_gbif = st_as_sf(inter_gbif)
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
# Add a quick ACS intersection for mean income & NDVI if needed
|
| 895 |
+
acs_wide <- cbg_vect_sf %>% mutate(
|
| 896 |
+
income = median_inc,
|
| 897 |
+
ndvi = ndvi_mean
|
| 898 |
+
)
|
| 899 |
+
# this can be skipped !
|
| 900 |
+
# inter_gbif_acs <- st_intersection(inter_gbif, acs_wide)
|
| 901 |
+
|
| 902 |
+
inter_gbif_acs = sf_gbif |> dplyr::mutate(income = medincE,
|
| 903 |
+
ndvi = ndvi_sentinel)#We can do this because we preannotated ndvi and us census information
|
| 904 |
+
|
| 905 |
+
if (input$class_filter != "All") {
|
| 906 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
|
| 907 |
+
}
|
| 908 |
+
if (input$family_filter != "All") {
|
| 909 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
if (nrow(inter_gbif_acs) == 0) {
|
| 913 |
+
return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
species_counts <- inter_gbif_acs %>%
|
| 917 |
+
st_drop_geometry() %>%
|
| 918 |
+
group_by(species) %>%
|
| 919 |
+
summarize(
|
| 920 |
+
n_records = n(),
|
| 921 |
+
mean_income = round(mean(income, na.rm=TRUE), 2),
|
| 922 |
+
mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
|
| 923 |
+
.groups = "drop"
|
| 924 |
+
) %>%
|
| 925 |
+
arrange(desc(n_records))
|
| 926 |
+
|
| 927 |
+
DT::datatable(
|
| 928 |
+
species_counts,
|
| 929 |
+
colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
|
| 930 |
+
options = list(pageLength = 10),
|
| 931 |
+
rownames = FALSE
|
| 932 |
+
)
|
| 933 |
+
})
|
| 934 |
+
|
| 935 |
+
# ------------------------------------------------
|
| 936 |
+
# Ggplot: Biodiversity & Socioeconomic Summary
|
| 937 |
+
# ------------------------------------------------
|
| 938 |
+
output$bioSocPlot <- renderPlot({
|
| 939 |
+
df <- socio_data()
|
| 940 |
+
if (nrow(df) == 0) return(NULL)
|
| 941 |
+
|
| 942 |
+
df_plot <- df %>%
|
| 943 |
+
mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
|
| 944 |
+
|
| 945 |
+
ggplot(df_plot, aes(x = IsoLabel)) +
|
| 946 |
+
geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
|
| 947 |
+
geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
|
| 948 |
+
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
| 949 |
+
labs(
|
| 950 |
+
x = "Isochrone (Mode-Time)",
|
| 951 |
+
y = "Unique Species (Blue) \n | Population (Red) (thousands)",
|
| 952 |
+
title = "Biodiversity & Socioeconomic Summary"
|
| 953 |
+
) +
|
| 954 |
+
theme_minimal(base_size = 14) +
|
| 955 |
+
theme(
|
| 956 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 957 |
+
axis.text.y = element_text(size = 12),
|
| 958 |
+
axis.title.x = element_text(size = 14),
|
| 959 |
+
axis.title.y = element_text(size = 14)
|
| 960 |
+
)
|
| 961 |
+
})
|
| 962 |
+
|
| 963 |
+
# ------------------------------------------------
|
| 964 |
+
# Bar plot: GBIF records by institutionCode
|
| 965 |
+
# ------------------------------------------------
|
| 966 |
+
output$collectionPlot <- renderPlot({
|
| 967 |
+
iso_data <- isochrones_data()
|
| 968 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 969 |
+
plot.new()
|
| 970 |
+
title("No GBIF records found in this isochrone.")
|
| 971 |
+
return(NULL)
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
iso_union <- st_union(iso_data)
|
| 975 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 976 |
+
|
| 977 |
+
vect_iso <- vect(iso_union)
|
| 978 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
| 979 |
+
inter_gbif = st_as_sf(inter_gbif)
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
if (nrow(inter_gbif) == 0) {
|
| 984 |
+
plot.new()
|
| 985 |
+
title("No GBIF records found in this isochrone.")
|
| 986 |
+
return(NULL)
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
df_code <- inter_gbif %>%
|
| 990 |
+
st_drop_geometry() %>%
|
| 991 |
+
group_by(institutionCode) %>%
|
| 992 |
+
summarize(count = n(), .groups = "drop") %>%
|
| 993 |
+
arrange(desc(count)) %>%
|
| 994 |
+
mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
|
| 995 |
+
|
| 996 |
+
ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) + # replaced institutionCode with trunacedCode
|
| 997 |
+
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
| 998 |
+
labs(
|
| 999 |
+
x = "Institution Code (Truncoded)",
|
| 1000 |
+
y = "Number of Records",
|
| 1001 |
+
title = "GBIF Records by Institution Code (Isochrone Union)"
|
| 1002 |
+
) +
|
| 1003 |
+
theme_minimal(base_size = 14) +
|
| 1004 |
+
theme(
|
| 1005 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 1006 |
+
axis.text.y = element_text(size = 12),
|
| 1007 |
+
axis.title.x = element_text(size = 14),
|
| 1008 |
+
axis.title.y = element_text(size = 14)
|
| 1009 |
+
)
|
| 1010 |
+
})
|
| 1011 |
+
|
| 1012 |
+
# ------------------------------------------------
|
| 1013 |
+
# Additional Section: mapview for species richness vs. data availability
|
| 1014 |
+
# ------------------------------------------------
|
| 1015 |
+
output$mapNUI <- renderUI({
|
| 1016 |
+
map_n <- mapview(cbg_vect_sf, zcol = "n", layer.name="Data Availability (n)")
|
| 1017 |
+
map_n@map
|
| 1018 |
+
})
|
| 1019 |
+
|
| 1020 |
+
output$mapSpeciesUI <- renderUI({
|
| 1021 |
+
map_s <- mapview(cbg_vect_sf, zcol = "n_species", layer.name="Species Richness (n_species)")
|
| 1022 |
+
map_s@map
|
| 1023 |
+
})
|
| 1024 |
+
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
|
| 1030 |
+
|
| 1031 |
+
|
| 1032 |
+
|
| 1033 |
+
# ------------------------------------------------
|
| 1034 |
+
# Additional Plot: n_observations vs n_species
|
| 1035 |
+
# ------------------------------------------------
|
| 1036 |
+
|
| 1037 |
+
# Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
|
| 1038 |
+
|
| 1039 |
+
filtered_data <- reactive({
|
| 1040 |
+
data <- cbg_vect_sf
|
| 1041 |
+
if (input$class_filter != "All") {
|
| 1042 |
+
data <- data[data$class == input$class_filter, ]
|
| 1043 |
+
}
|
| 1044 |
+
if (input$family_filter != "All") {
|
| 1045 |
+
data <- data[data$family == input$family_filter, ]
|
| 1046 |
+
}
|
| 1047 |
+
data
|
| 1048 |
+
})
|
| 1049 |
+
|
| 1050 |
+
output$obsVsSpeciesPlot <- renderPlot({
|
| 1051 |
+
data <- filtered_data()
|
| 1052 |
+
ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
|
| 1053 |
+
geom_point(color = "blue", alpha = 0.6) +
|
| 1054 |
+
labs(
|
| 1055 |
+
x = "Log(Number of Observations)",
|
| 1056 |
+
y = "Log(Species Richness)",
|
| 1057 |
+
title = "Filtered Data Availability vs. Species Richness"
|
| 1058 |
+
) +
|
| 1059 |
+
theme_minimal(base_size = 14)
|
| 1060 |
+
})
|
| 1061 |
+
|
| 1062 |
+
# output$obsVsSpeciesPlot <- renderPlot({
|
| 1063 |
+
# # A simple scatter plot of n_observations vs. n_species from cbg_vect_sf
|
| 1064 |
+
# ggplot(cbg_vect_sf, aes(x = log(n_observations+1), y = log(unique_species+1)) ) +
|
| 1065 |
+
# geom_point(color = "blue", alpha = 0.6) +
|
| 1066 |
+
# labs(
|
| 1067 |
+
# x = "Number of Observations (n_observations)",
|
| 1068 |
+
# y = "Number of Species (n_species)",
|
| 1069 |
+
# title = "Data Availability vs. Species Richness"
|
| 1070 |
+
# ) +
|
| 1071 |
+
# theme_minimal(base_size = 14)
|
| 1072 |
+
# })
|
| 1073 |
+
|
| 1074 |
+
# ------------------------------------------------
|
| 1075 |
+
# Additional Plot: Linear model of n_species ~ n_observations + median_inc + ndvi_mean
|
| 1076 |
+
# ------------------------------------------------
|
| 1077 |
+
# output$lmCoefficientsPlot <- renderPlot({
|
| 1078 |
+
# # Build a linear model with cbg_vect_sf
|
| 1079 |
+
# # Must ensure there are no NAs
|
| 1080 |
+
# df_lm <- cbg_vect_sf %>%
|
| 1081 |
+
# filter(!is.na(n_observations),
|
| 1082 |
+
# !is.na(unique_species),
|
| 1083 |
+
# !is.na(median_inc),
|
| 1084 |
+
# !is.na(ndvi_mean))
|
| 1085 |
+
#
|
| 1086 |
+
# if (nrow(df_lm) < 5) {
|
| 1087 |
+
# # not enough data
|
| 1088 |
+
# plot.new()
|
| 1089 |
+
# title("Not enough data for linear model.")
|
| 1090 |
+
# return(NULL)
|
| 1091 |
+
# }
|
| 1092 |
+
#
|
| 1093 |
+
# # Model
|
| 1094 |
+
# fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
|
| 1095 |
+
#
|
| 1096 |
+
# # Using sjPlot to visualize coefficients
|
| 1097 |
+
# # We store in an object and then print it
|
| 1098 |
+
# p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
|
| 1099 |
+
# print(p)
|
| 1100 |
+
# })
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
shinyApp(ui, server)
|
| 1104 |
+
# run_with_themer(shinyApp(ui, server))
|
| 1105 |
+
# library(profvis)
|
| 1106 |
+
#
|
| 1107 |
+
# profvis({
|
| 1108 |
+
# shinyApp(ui, server)
|
| 1109 |
+
# })
|
| 1110 |
+
|
R/old_poc/app_old.R
ADDED
|
@@ -0,0 +1,986 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sharing the app https://shiny.posit.co/r/getstarted/shiny-basics/lesson7/
|
| 2 |
+
# rsconnect::setAccountInfo(name='diego-ellis-soto', token='A47BE3C9E4B9EBCDFEC889AF31F64154', secret='g2Q2rxeYCiwlH81EkPXcCGsiHMgdyhTznJRmHtea')
|
| 3 |
+
# deployApp()
|
| 4 |
+
# Add that you can hover over the greespace and get its name
|
| 5 |
+
# Improve the titles of the ggplots of the model coefficient estimates and of ggplot using the gbif summary table on data avialability vs species richness. Also log transform these values for better data visualization
|
| 6 |
+
# Also the ggplot of data avialability vs species richness. should also update if the user decides to subset by class or family. Until then, its okay to retain the general plot using all the data from gbif_sf
|
| 7 |
+
|
| 8 |
+
# Optimize some calculations? Shorten
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
###############################################################################
|
| 15 |
+
# Shiny App: San Francisco Biodiversity Access Decision Support Tool
|
| 16 |
+
# Author: Diego Ellis Soto, et al.
|
| 17 |
+
# University of California Berkeley, ESPM
|
| 18 |
+
# California Academy of Sciences
|
| 19 |
+
###############################################################################
|
| 20 |
+
|
| 21 |
+
library(shiny)
|
| 22 |
+
library(leaflet)
|
| 23 |
+
library(mapboxapi)
|
| 24 |
+
library(tidyverse)
|
| 25 |
+
library(tidycensus)
|
| 26 |
+
library(sf)
|
| 27 |
+
library(DT)
|
| 28 |
+
library(RColorBrewer)
|
| 29 |
+
library(terra)
|
| 30 |
+
library(data.table) # for fread
|
| 31 |
+
library(mapview) # for mapview objects
|
| 32 |
+
library(sjPlot) # for plotting lm model coefficients
|
| 33 |
+
library(sjlabelled) # optional if needed for sjPlot
|
| 34 |
+
|
| 35 |
+
# ------------------------------------------------
|
| 36 |
+
# 1) API Keys
|
| 37 |
+
# ------------------------------------------------
|
| 38 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
| 39 |
+
mb_access_token(mapbox_token, install = FALSE)
|
| 40 |
+
|
| 41 |
+
# ------------------------------------------------
|
| 42 |
+
# 2) Load Data
|
| 43 |
+
# ------------------------------------------------
|
| 44 |
+
# -- Greenspace
|
| 45 |
+
osm_greenspace <- st_read("data/greenspaces_osm_nad83.shp", quiet = TRUE) %>%
|
| 46 |
+
st_transform(4326)
|
| 47 |
+
if (!"name" %in% names(osm_greenspace)) {
|
| 48 |
+
osm_greenspace$name <- "Unnamed Greenspace"
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
# -- NDVI Raster
|
| 52 |
+
ndvi <- rast("data/SF_EastBay_NDVI_Sentinel_10.tif")
|
| 53 |
+
|
| 54 |
+
# -- GBIF data
|
| 55 |
+
load("data/sf_gbif.Rdata") # => sf_gbif
|
| 56 |
+
|
| 57 |
+
# -- Precomputed CBG data
|
| 58 |
+
load('data/cbg_vect_sf.Rdata')
|
| 59 |
+
if (!"unique_species" %in% names(cbg_vect_sf)) {
|
| 60 |
+
cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
| 61 |
+
}
|
| 62 |
+
if (!"n_observations" %in% names(cbg_vect_sf)) {
|
| 63 |
+
cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
| 64 |
+
}
|
| 65 |
+
if (!"median_inc" %in% names(cbg_vect_sf)) {
|
| 66 |
+
cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
| 67 |
+
}
|
| 68 |
+
if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
|
| 69 |
+
cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
# -- Hotspots/Coldspots
|
| 73 |
+
biodiv_hotspots <- st_read("data/hotspots.shp", quiet = TRUE) %>% st_transform(4326)
|
| 74 |
+
biodiv_coldspots <- st_read("data/coldspots.shp", quiet = TRUE) %>% st_transform(4326)
|
| 75 |
+
|
| 76 |
+
# ------------------------------------------------
|
| 77 |
+
# 3) UI
|
| 78 |
+
# ------------------------------------------------
|
| 79 |
+
ui <- fluidPage(
|
| 80 |
+
titlePanel("San Francisco Biodiversity Access Decision Support Tool"),
|
| 81 |
+
|
| 82 |
+
fluidRow(
|
| 83 |
+
column(
|
| 84 |
+
width = 12, align = "center",
|
| 85 |
+
tags$img(src = "UC Berkeley_logo.png",
|
| 86 |
+
height = "120px", style = "margin:10px;"),
|
| 87 |
+
tags$img(src = "California_academy_logo.png",
|
| 88 |
+
height = "120px", style = "margin:10px;"),
|
| 89 |
+
tags$img(src = "Reimagining_San_Francisco.png",
|
| 90 |
+
height = "120px", style = "margin:10px;")
|
| 91 |
+
)
|
| 92 |
+
),
|
| 93 |
+
|
| 94 |
+
fluidRow(
|
| 95 |
+
column(
|
| 96 |
+
width = 12,
|
| 97 |
+
br(),
|
| 98 |
+
p("This application demonstrates an approach for exploring biodiversity access in San Francisco..."),
|
| 99 |
+
# (Your summary text can go here)
|
| 100 |
+
)
|
| 101 |
+
),
|
| 102 |
+
br(),
|
| 103 |
+
fluidRow(
|
| 104 |
+
column(
|
| 105 |
+
width = 12,
|
| 106 |
+
br(),
|
| 107 |
+
tags$b("App Summary (Fill out with RSF data working group):"),
|
| 108 |
+
# Increasingly, we ask ourselves about what increasing access to biodiversity really means.
|
| 109 |
+
# Importantly, accessibility differs from human mobility in urban planning studies for equitable transportation systems.
|
| 110 |
+
p("
|
| 111 |
+
This application allows users to either click on a map or geocode an address (in progress)
|
| 112 |
+
to generate travel-time isochrones across multiple transportation modes (e.g., pedestrian, cycling, driving, driving during traffic).
|
| 113 |
+
It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
|
| 114 |
+
and summarizes biodiversity records from GBIF. We explore what biodiversity access means
|
| 115 |
+
Users can explore information that we often relate to biodiversity in urban environments including greenspace coverage, population estimates, and species diversity within each isochrone."),
|
| 116 |
+
|
| 117 |
+
tags$b("Reimagining San Francisco (Fill out with CAS):"),
|
| 118 |
+
p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
|
| 119 |
+
and technological dimensions to shape a sustainable future for the Bay Area.
|
| 120 |
+
This collaboration unites diverse stakeholders to explore innovations in urban planning,
|
| 121 |
+
conservation, and community engagement. The Reimagining San Francisco Data Working Group has been tasked with identifying and integrating multiple sources of socio-ecological biodiversity information in a co-development framework."),
|
| 122 |
+
|
| 123 |
+
tags$b("Why Biodiversity Access Matters (Polish this):"),
|
| 124 |
+
p("
|
| 125 |
+
# Ensuring equitable access to biodiversity is essential for human well-being,
|
| 126 |
+
# ecological resilience, and global policy decisions related to conservation.
|
| 127 |
+
# Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
|
| 128 |
+
# and provide cultural, recreational, and health benefits to local communities.
|
| 129 |
+
Recognizing that cities are particularly complex socio-ecological systems facing both legacies of sociocultural practices as well as current ongoing dynamic human activities and pressures.
|
| 130 |
+
Incorporating multiple facets of biodiversity metrics alongside variables employed by city planners, human geographers, and decision-makers into urban planning will allow a more integrative lens in creating a sustainable future for cities and their residents."),
|
| 131 |
+
|
| 132 |
+
tags$b("How We Calculate Biodiversity Access Percentile:"),
|
| 133 |
+
p("Total unique species found within the user-generated isochrone.
|
| 134 |
+
We then compare that value to the distribution of unique species counts across all census block groups,
|
| 135 |
+
converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
|
| 136 |
+
A higher percentile indicates greater biodiversity within the chosen area,
|
| 137 |
+
relative to other parts of the city or region."),
|
| 138 |
+
|
| 139 |
+
tags$b("Created by:"),
|
| 140 |
+
p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
|
| 141 |
+
|
| 142 |
+
p("Contact Information",
|
| 143 |
+
strong("diego.ellissoto@berkeley.edu")),
|
| 144 |
+
|
| 145 |
+
tags$b("Next Steps:"),
|
| 146 |
+
tags$ul(
|
| 147 |
+
tags$li("Add impervious surface"),
|
| 148 |
+
tags$li("National walkability score"),
|
| 149 |
+
tags$li("Social vulnerability score"),
|
| 150 |
+
tags$li("NatureServe biodiversity maps"),
|
| 151 |
+
tags$li("Calculate cold-hotspots within ggregation of H6 bins instead of by census block group: Ask Carl"),
|
| 152 |
+
tags$li("Species range maps"),
|
| 153 |
+
tags$li("Add common name GBIF"),
|
| 154 |
+
tags$li("Partner orgs"),
|
| 155 |
+
tags$li("Optimize speed -> store variables -> H-ify the world?"),
|
| 156 |
+
tags$li("Brainstorm and co-develop the biodiversity access score"),
|
| 157 |
+
tags$li("For the GBIF summaries, add an annotated GBIF_sf with environmental variables so we can see landcover type association across the biodiversity within the isochrone.")
|
| 158 |
+
)
|
| 159 |
+
)
|
| 160 |
+
),
|
| 161 |
+
br(),
|
| 162 |
+
|
| 163 |
+
tabsetPanel(
|
| 164 |
+
|
| 165 |
+
# 1) Isochrone Explorer
|
| 166 |
+
tabPanel("Isochrone Explorer",
|
| 167 |
+
sidebarLayout(
|
| 168 |
+
sidebarPanel(
|
| 169 |
+
radioButtons(
|
| 170 |
+
"location_choice",
|
| 171 |
+
"Select how to choose your location:",
|
| 172 |
+
choices = c("Address (Geocode)" = "address",
|
| 173 |
+
"Click on Map" = "map_click"),
|
| 174 |
+
selected = "map_click"
|
| 175 |
+
),
|
| 176 |
+
|
| 177 |
+
conditionalPanel(
|
| 178 |
+
condition = "input.location_choice == 'address'",
|
| 179 |
+
textInput(
|
| 180 |
+
"user_address",
|
| 181 |
+
"Enter Address:",
|
| 182 |
+
value = "",
|
| 183 |
+
placeholder = "e.g., 1600 Amphitheatre Parkway, Mountain View, CA"
|
| 184 |
+
)
|
| 185 |
+
),
|
| 186 |
+
|
| 187 |
+
checkboxGroupInput(
|
| 188 |
+
"transport_modes",
|
| 189 |
+
"Select Transportation Modes:",
|
| 190 |
+
choices = list("Driving" = "driving",
|
| 191 |
+
"Walking" = "walking",
|
| 192 |
+
"Cycling" = "cycling",
|
| 193 |
+
"Driving with Traffic"= "driving-traffic"),
|
| 194 |
+
selected = c("driving", "walking")
|
| 195 |
+
),
|
| 196 |
+
|
| 197 |
+
checkboxGroupInput(
|
| 198 |
+
"iso_times",
|
| 199 |
+
"Select Isochrone Times (minutes):",
|
| 200 |
+
choices = list("5" = 5, "10" = 10, "15" = 15),
|
| 201 |
+
selected = c(5, 10)
|
| 202 |
+
),
|
| 203 |
+
|
| 204 |
+
actionButton("generate_iso", "Generate Isochrones"),
|
| 205 |
+
actionButton("clear_map", "Clear")
|
| 206 |
+
|
| 207 |
+
),
|
| 208 |
+
|
| 209 |
+
mainPanel(
|
| 210 |
+
leafletOutput("isoMap", height = 600),
|
| 211 |
+
|
| 212 |
+
fluidRow(
|
| 213 |
+
column(12,
|
| 214 |
+
br(),
|
| 215 |
+
uiOutput("bioScoreBox"),
|
| 216 |
+
uiOutput("closestGreenspaceUI")
|
| 217 |
+
)
|
| 218 |
+
),
|
| 219 |
+
|
| 220 |
+
br(),
|
| 221 |
+
DTOutput("dataTable"),
|
| 222 |
+
|
| 223 |
+
br(),
|
| 224 |
+
fluidRow(
|
| 225 |
+
column(12,
|
| 226 |
+
plotOutput("bioSocPlot", height = "400px")
|
| 227 |
+
)
|
| 228 |
+
),
|
| 229 |
+
|
| 230 |
+
br(),
|
| 231 |
+
fluidRow(
|
| 232 |
+
column(12,
|
| 233 |
+
plotOutput("collectionPlot", height = "300px")
|
| 234 |
+
)
|
| 235 |
+
)
|
| 236 |
+
)
|
| 237 |
+
)
|
| 238 |
+
),
|
| 239 |
+
|
| 240 |
+
#br.?
|
| 241 |
+
tabPanel(
|
| 242 |
+
"GBIF Summaries",
|
| 243 |
+
sidebarLayout(
|
| 244 |
+
sidebarPanel(
|
| 245 |
+
selectInput(
|
| 246 |
+
"class_filter",
|
| 247 |
+
"Select a GBIF Class to Summarize:",
|
| 248 |
+
choices = c("All", sort(unique(sf_gbif$class))),
|
| 249 |
+
selected = "All"
|
| 250 |
+
),
|
| 251 |
+
selectInput(
|
| 252 |
+
"family_filter",
|
| 253 |
+
"Filter by Family (optional):",
|
| 254 |
+
choices = c("All", sort(unique(sf_gbif$family))),
|
| 255 |
+
selected = "All"
|
| 256 |
+
)
|
| 257 |
+
),
|
| 258 |
+
mainPanel(
|
| 259 |
+
DTOutput("classTable"),
|
| 260 |
+
br(),
|
| 261 |
+
h3("Observations vs. Species Richness"),
|
| 262 |
+
plotOutput("obsVsSpeciesPlot", height = "400px"),
|
| 263 |
+
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
| 264 |
+
)
|
| 265 |
+
)
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# )
|
| 270 |
+
|
| 271 |
+
# Separate section for the plot outside of the "GBIF Summaries" tab
|
| 272 |
+
|
| 273 |
+
# tabsetPanel(
|
| 274 |
+
|
| 275 |
+
# # 1) Isochrone Explorer
|
| 276 |
+
# tabPanel(
|
| 277 |
+
# mainPanel(
|
| 278 |
+
# DTOutput("classTable"),
|
| 279 |
+
# br(),
|
| 280 |
+
# fluidRow(
|
| 281 |
+
# column(
|
| 282 |
+
# 6,
|
| 283 |
+
# # A simple scatter or line plot for n_observations vs n_species
|
| 284 |
+
# plotOutput("obsVsSpeciesPlot", height = "300px")
|
| 285 |
+
# )
|
| 286 |
+
# # ,
|
| 287 |
+
# # column(
|
| 288 |
+
# # 6,
|
| 289 |
+
# # # A regression model plot using sjPlot
|
| 290 |
+
# # plotOutput("lmCoefficientsPlot", height = "300px")
|
| 291 |
+
# # )
|
| 292 |
+
# )
|
| 293 |
+
# )
|
| 294 |
+
# )
|
| 295 |
+
# ),
|
| 296 |
+
#
|
| 297 |
+
# br()
|
| 298 |
+
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# fluidRow(
|
| 303 |
+
# column(
|
| 304 |
+
# 12,
|
| 305 |
+
# tags$h3("Species Richness vs Data Availability"),
|
| 306 |
+
# fluidRow(
|
| 307 |
+
# column(6, uiOutput("mapNUI")),
|
| 308 |
+
# column(6, uiOutput("mapSpeciesUI"))
|
| 309 |
+
# )
|
| 310 |
+
# )
|
| 311 |
+
# )
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# ------------------------------------------------
|
| 315 |
+
# 4) Server
|
| 316 |
+
# ------------------------------------------------
|
| 317 |
+
server <- function(input, output, session) {
|
| 318 |
+
|
| 319 |
+
chosen_point <- reactiveVal(NULL)
|
| 320 |
+
|
| 321 |
+
# ------------------------------------------------
|
| 322 |
+
# Leaflet Base + Hide Overlays
|
| 323 |
+
# ------------------------------------------------
|
| 324 |
+
output$isoMap <- renderLeaflet({
|
| 325 |
+
pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
|
| 326 |
+
|
| 327 |
+
pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
|
| 328 |
+
# 2) Color palette for data availability
|
| 329 |
+
pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
leaflet() %>%
|
| 333 |
+
addTiles(group = "Street Map (Default)") %>%
|
| 334 |
+
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
|
| 335 |
+
addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
|
| 336 |
+
|
| 337 |
+
addPolygons(
|
| 338 |
+
data = cbg_vect_sf,
|
| 339 |
+
group = "Income",
|
| 340 |
+
# fillColor = ~pal_cbg(unique_species),
|
| 341 |
+
fillColor = ~pal_cbg(medincE),
|
| 342 |
+
fillOpacity = 0.6,
|
| 343 |
+
color = "white",
|
| 344 |
+
weight = 1,
|
| 345 |
+
label = "Income"
|
| 346 |
+
) %>%
|
| 347 |
+
|
| 348 |
+
addPolygons(
|
| 349 |
+
data = osm_greenspace,
|
| 350 |
+
group = "Greenspace",
|
| 351 |
+
fillColor = "darkgreen",
|
| 352 |
+
fillOpacity = 0.3,
|
| 353 |
+
color = "green",
|
| 354 |
+
weight = 1,
|
| 355 |
+
label = ~name,
|
| 356 |
+
highlightOptions = highlightOptions(
|
| 357 |
+
weight = 5,
|
| 358 |
+
color = "blue",
|
| 359 |
+
fillOpacity = 0.5,
|
| 360 |
+
bringToFront = TRUE
|
| 361 |
+
),
|
| 362 |
+
labelOptions = labelOptions(
|
| 363 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 364 |
+
textsize = "12px",
|
| 365 |
+
direction = "auto"
|
| 366 |
+
)
|
| 367 |
+
) %>%
|
| 368 |
+
|
| 369 |
+
addPolygons(
|
| 370 |
+
data = biodiv_hotspots,
|
| 371 |
+
group = "Hotspots (KnowBR)",
|
| 372 |
+
fillColor = "firebrick",
|
| 373 |
+
fillOpacity = 0.2,
|
| 374 |
+
color = "firebrick",
|
| 375 |
+
weight = 2,
|
| 376 |
+
label = "Biodiversity Hotspot"
|
| 377 |
+
) %>%
|
| 378 |
+
|
| 379 |
+
addPolygons(
|
| 380 |
+
data = biodiv_coldspots,
|
| 381 |
+
group = "Coldspots (KnowBR)",
|
| 382 |
+
fillColor = "navyblue",
|
| 383 |
+
fillOpacity = 0.2,
|
| 384 |
+
color = "navyblue",
|
| 385 |
+
weight = 2,
|
| 386 |
+
label = "Biodiversity Coldspot"
|
| 387 |
+
) %>%
|
| 388 |
+
|
| 389 |
+
# Add richness and nobs
|
| 390 |
+
# -- Richness layer
|
| 391 |
+
addPolygons(
|
| 392 |
+
data = cbg_vect_sf,
|
| 393 |
+
group = "Species Richness",
|
| 394 |
+
fillColor = ~pal_rich(unique_species),
|
| 395 |
+
fillOpacity = 0.6,
|
| 396 |
+
color = "white",
|
| 397 |
+
weight = 1,
|
| 398 |
+
popup = ~paste0(
|
| 399 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 400 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 401 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 402 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 403 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 404 |
+
)
|
| 405 |
+
) %>%
|
| 406 |
+
|
| 407 |
+
# -- Data Availability layer
|
| 408 |
+
addPolygons(
|
| 409 |
+
data = cbg_vect_sf,
|
| 410 |
+
group = "Data Availability",
|
| 411 |
+
fillColor = ~pal_data(n_observations),
|
| 412 |
+
fillOpacity = 0.6,
|
| 413 |
+
color = "white",
|
| 414 |
+
weight = 1,
|
| 415 |
+
popup = ~paste0(
|
| 416 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 417 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 418 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 419 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 420 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 421 |
+
)
|
| 422 |
+
) %>%
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
|
| 426 |
+
addLayersControl(
|
| 427 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 428 |
+
overlayGroups = c("Income", "Greenspace","Species Richness", "Data Availability",
|
| 429 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)"),
|
| 430 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 431 |
+
) %>%
|
| 432 |
+
hideGroup("Income") %>%
|
| 433 |
+
hideGroup("Greenspace") %>%
|
| 434 |
+
hideGroup("Hotspots (KnowBR)") %>%
|
| 435 |
+
hideGroup("Coldspots (KnowBR)") %>%
|
| 436 |
+
hideGroup("Species Richness") %>%
|
| 437 |
+
hideGroup("Data Availability")
|
| 438 |
+
})
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
# ------------------------------------------------
|
| 442 |
+
# Observe map clicks (location_choice = 'map_click')
|
| 443 |
+
# ------------------------------------------------
|
| 444 |
+
observeEvent(input$isoMap_click, {
|
| 445 |
+
req(input$location_choice == "map_click")
|
| 446 |
+
click <- input$isoMap_click
|
| 447 |
+
if (!is.null(click)) {
|
| 448 |
+
chosen_point(c(lon = click$lng, lat = click$lat))
|
| 449 |
+
leafletProxy("isoMap") %>%
|
| 450 |
+
clearMarkers() %>%
|
| 451 |
+
addCircleMarkers(
|
| 452 |
+
lng = click$lng, lat = click$lat,
|
| 453 |
+
radius = 6, color = "firebrick",
|
| 454 |
+
label = "Map Click Location"
|
| 455 |
+
)
|
| 456 |
+
}
|
| 457 |
+
})
|
| 458 |
+
|
| 459 |
+
# ------------------------------------------------
|
| 460 |
+
# Observe clearinf of map
|
| 461 |
+
# ------------------------------------------------
|
| 462 |
+
observeEvent(input$clear_map, {
|
| 463 |
+
# Reset the chosen point
|
| 464 |
+
chosen_point(NULL)
|
| 465 |
+
|
| 466 |
+
# Clear all markers and isochrones from the map
|
| 467 |
+
leafletProxy("isoMap") %>%
|
| 468 |
+
clearMarkers() %>%
|
| 469 |
+
clearShapes() %>%
|
| 470 |
+
clearGroup("Isochrones") %>%
|
| 471 |
+
clearGroup("NDVI Raster")
|
| 472 |
+
|
| 473 |
+
# Optional: Reset any other reactive values if needed
|
| 474 |
+
showNotification("Map cleared. You can select a new location.")
|
| 475 |
+
})
|
| 476 |
+
|
| 477 |
+
# ------------------------------------------------
|
| 478 |
+
# Generate Isochrones
|
| 479 |
+
# ------------------------------------------------
|
| 480 |
+
isochrones_data <- eventReactive(input$generate_iso, {
|
| 481 |
+
|
| 482 |
+
leafletProxy("isoMap") %>%
|
| 483 |
+
clearGroup("Isochrones") %>%
|
| 484 |
+
clearGroup("NDVI Raster")
|
| 485 |
+
|
| 486 |
+
# If user selected address:
|
| 487 |
+
if (input$location_choice == "address") {
|
| 488 |
+
if (nchar(input$user_address) < 5) {
|
| 489 |
+
showNotification("Please enter a more complete address.", type = "error")
|
| 490 |
+
return(NULL)
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
loc_df <- tryCatch({
|
| 494 |
+
mb_geocode(input$user_address, access_token = mapbox_token)
|
| 495 |
+
}, error = function(e) {
|
| 496 |
+
showNotification(paste("Geocoding failed:", e$message), type = "error")
|
| 497 |
+
NULL
|
| 498 |
+
})
|
| 499 |
+
|
| 500 |
+
# Check for valid lat/lon
|
| 501 |
+
if (is.null(loc_df) || nrow(loc_df) == 0 || is.na(loc_df$lon[1]) || is.na(loc_df$lat[1])) {
|
| 502 |
+
showNotification("No valid geocoding results found.", type = "warning")
|
| 503 |
+
return(NULL)
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
chosen_point(c(lon = loc_df$lon[1], lat = loc_df$lat[1]))
|
| 507 |
+
|
| 508 |
+
leafletProxy("isoMap") %>%
|
| 509 |
+
clearMarkers() %>%
|
| 510 |
+
addCircleMarkers(
|
| 511 |
+
lng = loc_df$lon[1], lat = loc_df$lat[1],
|
| 512 |
+
radius = 6, color = "navyblue",
|
| 513 |
+
label = "Geocoded Address"
|
| 514 |
+
) %>%
|
| 515 |
+
setView(lng = loc_df$lon[1], lat = loc_df$lat[1], zoom = 13)
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
pt <- chosen_point()
|
| 519 |
+
if (is.null(pt)) {
|
| 520 |
+
showNotification("No location selected! Provide an address or click the map.", type = "error")
|
| 521 |
+
return(NULL)
|
| 522 |
+
}
|
| 523 |
+
if (length(input$transport_modes) == 0) {
|
| 524 |
+
showNotification("Select at least one transportation mode.", type = "error")
|
| 525 |
+
return(NULL)
|
| 526 |
+
}
|
| 527 |
+
if (length(input$iso_times) == 0) {
|
| 528 |
+
showNotification("Select at least one isochrone time.", type = "error")
|
| 529 |
+
return(NULL)
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
location_sf <- st_as_sf(
|
| 533 |
+
data.frame(lon = pt["lon"], lat = pt["lat"]),
|
| 534 |
+
coords = c("lon","lat"), crs = 4326
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
iso_list <- list()
|
| 538 |
+
for (mode in input$transport_modes) {
|
| 539 |
+
for (t in input$iso_times) {
|
| 540 |
+
iso <- tryCatch({
|
| 541 |
+
mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
|
| 542 |
+
access_token = mapbox_token)
|
| 543 |
+
}, error = function(e) {
|
| 544 |
+
showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
|
| 545 |
+
NULL
|
| 546 |
+
})
|
| 547 |
+
if (!is.null(iso)) {
|
| 548 |
+
iso$mode <- mode
|
| 549 |
+
iso$time <- t
|
| 550 |
+
iso_list <- append(iso_list, list(iso))
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
if (length(iso_list) == 0) {
|
| 555 |
+
showNotification("No isochrones generated.", type = "warning")
|
| 556 |
+
return(NULL)
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
|
| 560 |
+
all_iso
|
| 561 |
+
})
|
| 562 |
+
|
| 563 |
+
# ------------------------------------------------
|
| 564 |
+
# Plot Isochrones + NDVI
|
| 565 |
+
# ------------------------------------------------
|
| 566 |
+
observeEvent(isochrones_data(), {
|
| 567 |
+
iso_data <- isochrones_data()
|
| 568 |
+
req(iso_data)
|
| 569 |
+
|
| 570 |
+
iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
|
| 571 |
+
pal <- colorRampPalette(brewer.pal(8, "Set2"))
|
| 572 |
+
cols <- pal(nrow(iso_data))
|
| 573 |
+
|
| 574 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 575 |
+
poly_i <- iso_data[i, ]
|
| 576 |
+
leafletProxy("isoMap") %>%
|
| 577 |
+
addPolygons(
|
| 578 |
+
data = poly_i,
|
| 579 |
+
group = "Isochrones",
|
| 580 |
+
color = cols[i],
|
| 581 |
+
weight = 2,
|
| 582 |
+
fillOpacity = 0.4,
|
| 583 |
+
label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
|
| 584 |
+
)
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
iso_union <- st_union(iso_data)
|
| 588 |
+
iso_union_vect <- vect(iso_union)
|
| 589 |
+
ndvi_crop <- crop(ndvi, iso_union_vect)
|
| 590 |
+
ndvi_mask <- mask(ndvi_crop, iso_union_vect)
|
| 591 |
+
ndvi_vals <- values(ndvi_mask)
|
| 592 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 593 |
+
|
| 594 |
+
if (length(ndvi_vals) > 0) {
|
| 595 |
+
ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
|
| 596 |
+
|
| 597 |
+
leafletProxy("isoMap") %>%
|
| 598 |
+
addRasterImage(
|
| 599 |
+
x = ndvi_mask,
|
| 600 |
+
colors = ndvi_pal,
|
| 601 |
+
opacity = 0.7,
|
| 602 |
+
project = TRUE,
|
| 603 |
+
group = "NDVI Raster"
|
| 604 |
+
) %>%
|
| 605 |
+
addLegend(
|
| 606 |
+
position = "bottomright",
|
| 607 |
+
pal = ndvi_pal,
|
| 608 |
+
values = ndvi_vals,
|
| 609 |
+
title = "NDVI"
|
| 610 |
+
)
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
leafletProxy("isoMap") %>%
|
| 614 |
+
addLayersControl(
|
| 615 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 616 |
+
overlayGroups = c("Income", "Greenspace",
|
| 617 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
| 618 |
+
"Isochrones", "NDVI Raster"),
|
| 619 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 620 |
+
)
|
| 621 |
+
})
|
| 622 |
+
|
| 623 |
+
# ------------------------------------------------
|
| 624 |
+
# socio_data Reactive + Summaries
|
| 625 |
+
# ------------------------------------------------
|
| 626 |
+
socio_data <- reactive({
|
| 627 |
+
iso_data <- isochrones_data()
|
| 628 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 629 |
+
return(data.frame())
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
acs_wide <- cbg_vect_sf %>%
|
| 633 |
+
mutate(
|
| 634 |
+
population = popE,
|
| 635 |
+
med_income = medincE
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
hotspot_union <- st_union(biodiv_hotspots)
|
| 639 |
+
coldspot_union <- st_union(biodiv_coldspots)
|
| 640 |
+
|
| 641 |
+
results <- data.frame()
|
| 642 |
+
|
| 643 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 644 |
+
poly_i <- iso_data[i, ]
|
| 645 |
+
|
| 646 |
+
dist_hot <- st_distance(poly_i, hotspot_union)
|
| 647 |
+
dist_cold <- st_distance(poly_i, coldspot_union)
|
| 648 |
+
dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
|
| 649 |
+
dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
|
| 650 |
+
|
| 651 |
+
inter_acs <- st_intersection(acs_wide, poly_i)
|
| 652 |
+
|
| 653 |
+
pop_total <- 0
|
| 654 |
+
inc_str <- "N/A"
|
| 655 |
+
if (nrow(inter_acs) > 0) {
|
| 656 |
+
inter_acs$area <- st_area(inter_acs)
|
| 657 |
+
inter_acs$area_num <- as.numeric(inter_acs$area)
|
| 658 |
+
inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
|
| 659 |
+
inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
|
| 660 |
+
|
| 661 |
+
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
| 662 |
+
|
| 663 |
+
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
| 664 |
+
sum(inter_acs$area_num, na.rm = TRUE)
|
| 665 |
+
if (!is.na(w_income) && w_income > 0) {
|
| 666 |
+
inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
inter_gs <- st_intersection(osm_greenspace, poly_i)
|
| 671 |
+
gs_area_m2 <- 0
|
| 672 |
+
if (nrow(inter_gs) > 0) {
|
| 673 |
+
gs_area_m2 <- sum(st_area(inter_gs))
|
| 674 |
+
}
|
| 675 |
+
iso_area_m2 <- as.numeric(st_area(poly_i))
|
| 676 |
+
gs_area_m2 <- as.numeric(gs_area_m2)
|
| 677 |
+
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
| 678 |
+
|
| 679 |
+
poly_vect <- vect(poly_i)
|
| 680 |
+
ndvi_crop <- crop(ndvi, poly_vect)
|
| 681 |
+
ndvi_mask <- mask(ndvi_crop, poly_vect)
|
| 682 |
+
ndvi_vals <- values(ndvi_mask)
|
| 683 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 684 |
+
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
|
| 685 |
+
|
| 686 |
+
inter_gbif <- st_intersection(sf_gbif, poly_i)
|
| 687 |
+
n_records <- nrow(inter_gbif)
|
| 688 |
+
n_species <- length(unique(inter_gbif$species))
|
| 689 |
+
|
| 690 |
+
n_birds <- length(unique(inter_gbif$species[ inter_gbif$class == "Aves" ]))
|
| 691 |
+
n_mammals <- length(unique(inter_gbif$species[ inter_gbif$class == "Mammalia" ]))
|
| 692 |
+
n_plants <- length(unique(inter_gbif$species[ inter_gbif$class %in%
|
| 693 |
+
c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
|
| 694 |
+
"Equisetopsida","Bryopsida","Marchantiopsida") ]))
|
| 695 |
+
|
| 696 |
+
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
| 697 |
+
iso_area_sqm <- round(iso_area_m2, 2)
|
| 698 |
+
|
| 699 |
+
row_i <- data.frame(
|
| 700 |
+
Mode = tools::toTitleCase(poly_i$mode),
|
| 701 |
+
Time = poly_i$time,
|
| 702 |
+
IsochroneArea_m2 = iso_area_sqm,
|
| 703 |
+
IsochroneArea_km2 = iso_area_km2,
|
| 704 |
+
DistToHotspot_km = dist_hot_km,
|
| 705 |
+
DistToColdspot_km = dist_cold_km,
|
| 706 |
+
EstimatedPopulation = pop_total,
|
| 707 |
+
MedianIncome = inc_str,
|
| 708 |
+
MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
|
| 709 |
+
GBIF_Records = n_records,
|
| 710 |
+
GBIF_Species = n_species,
|
| 711 |
+
Bird_Species = n_birds,
|
| 712 |
+
Mammal_Species = n_mammals,
|
| 713 |
+
Plant_Species = n_plants,
|
| 714 |
+
Greenspace_m2 = round(gs_area_m2, 2),
|
| 715 |
+
Greenspace_percent = round(gs_percent, 2),
|
| 716 |
+
stringsAsFactors = FALSE
|
| 717 |
+
)
|
| 718 |
+
results <- rbind(results, row_i)
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
iso_union <- st_union(iso_data)
|
| 722 |
+
inter_all_gbif <- st_intersection(sf_gbif, iso_union)
|
| 723 |
+
union_n_species <- length(unique(inter_all_gbif$species))
|
| 724 |
+
rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
| 725 |
+
attr(results, "bio_percentile") <- rank_percentile
|
| 726 |
+
|
| 727 |
+
# Closest Greenspace from ANY part of the isochrone
|
| 728 |
+
dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
|
| 729 |
+
if (length(dist_mat) > 0) {
|
| 730 |
+
min_dist <- min(dist_mat)
|
| 731 |
+
min_idx <- which.min(dist_mat)
|
| 732 |
+
gs_name <- osm_greenspace$name[min_idx]
|
| 733 |
+
attr(results, "closest_greenspace") <- gs_name
|
| 734 |
+
} else {
|
| 735 |
+
attr(results, "closest_greenspace") <- "None"
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
results
|
| 739 |
+
})
|
| 740 |
+
|
| 741 |
+
# ------------------------------------------------
|
| 742 |
+
# Render main summary table
|
| 743 |
+
# ------------------------------------------------
|
| 744 |
+
output$dataTable <- renderDT({
|
| 745 |
+
df <- socio_data()
|
| 746 |
+
if (nrow(df) == 0) {
|
| 747 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 748 |
+
}
|
| 749 |
+
DT::datatable(
|
| 750 |
+
df,
|
| 751 |
+
colnames = c(
|
| 752 |
+
"Mode" = "Mode",
|
| 753 |
+
"Time (min)" = "Time",
|
| 754 |
+
"Area (m²)" = "IsochroneArea_m2",
|
| 755 |
+
"Area (km²)" = "IsochroneArea_km2",
|
| 756 |
+
"Dist. Hotspot (km)" = "DistToHotspot_km",
|
| 757 |
+
"Dist. Coldspot (km)" = "DistToColdspot_km",
|
| 758 |
+
"Population" = "EstimatedPopulation",
|
| 759 |
+
"Median Income" = "MedianIncome",
|
| 760 |
+
"Mean NDVI" = "MeanNDVI",
|
| 761 |
+
"GBIF Records" = "GBIF_Records",
|
| 762 |
+
"Unique Species" = "GBIF_Species",
|
| 763 |
+
"Bird Species" = "Bird_Species",
|
| 764 |
+
"Mammal Species" = "Mammal_Species",
|
| 765 |
+
"Plant Species" = "Plant_Species",
|
| 766 |
+
"Greenspace (m²)" = "Greenspace_m2",
|
| 767 |
+
"Greenspace (%)" = "Greenspace_percent"
|
| 768 |
+
),
|
| 769 |
+
options = list(pageLength = 10, autoWidth = TRUE),
|
| 770 |
+
rownames = FALSE
|
| 771 |
+
)
|
| 772 |
+
})
|
| 773 |
+
|
| 774 |
+
# ------------------------------------------------
|
| 775 |
+
# Biodiversity Access Score + Closest Greenspace
|
| 776 |
+
# ------------------------------------------------
|
| 777 |
+
output$bioScoreBox <- renderUI({
|
| 778 |
+
df <- socio_data()
|
| 779 |
+
if (nrow(df) == 0) return(NULL)
|
| 780 |
+
|
| 781 |
+
percentile <- attr(df, "bio_percentile")
|
| 782 |
+
if (is.null(percentile)) percentile <- "N/A"
|
| 783 |
+
else percentile <- paste0(percentile, "th Percentile")
|
| 784 |
+
|
| 785 |
+
wellPanel(
|
| 786 |
+
HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
|
| 787 |
+
)
|
| 788 |
+
})
|
| 789 |
+
|
| 790 |
+
output$closestGreenspaceUI <- renderUI({
|
| 791 |
+
df <- socio_data()
|
| 792 |
+
if (nrow(df) == 0) return(NULL)
|
| 793 |
+
gs_name <- attr(df, "closest_greenspace")
|
| 794 |
+
if (is.null(gs_name)) gs_name <- "None"
|
| 795 |
+
|
| 796 |
+
tagList(
|
| 797 |
+
strong("Closest Greenspace (from any part of the Isochrone):"),
|
| 798 |
+
p(gs_name)
|
| 799 |
+
)
|
| 800 |
+
})
|
| 801 |
+
|
| 802 |
+
# ------------------------------------------------
|
| 803 |
+
# Secondary table: user-selected CLASS & FAMILY
|
| 804 |
+
# ------------------------------------------------
|
| 805 |
+
output$classTable <- renderDT({
|
| 806 |
+
iso_data <- isochrones_data()
|
| 807 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 808 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
iso_union <- st_union(iso_data)
|
| 812 |
+
inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 813 |
+
|
| 814 |
+
# Add a quick ACS intersection for mean income & NDVI if needed
|
| 815 |
+
acs_wide <- cbg_vect_sf %>% mutate(
|
| 816 |
+
income = median_inc,
|
| 817 |
+
ndvi = ndvi_mean
|
| 818 |
+
)
|
| 819 |
+
|
| 820 |
+
inter_gbif_acs <- st_intersection(inter_gbif, acs_wide)
|
| 821 |
+
|
| 822 |
+
if (input$class_filter != "All") {
|
| 823 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
|
| 824 |
+
}
|
| 825 |
+
if (input$family_filter != "All") {
|
| 826 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
if (nrow(inter_gbif_acs) == 0) {
|
| 830 |
+
return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
species_counts <- inter_gbif_acs %>%
|
| 834 |
+
st_drop_geometry() %>%
|
| 835 |
+
group_by(species) %>%
|
| 836 |
+
summarize(
|
| 837 |
+
n_records = n(),
|
| 838 |
+
mean_income = round(mean(income, na.rm=TRUE), 2),
|
| 839 |
+
mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
|
| 840 |
+
.groups = "drop"
|
| 841 |
+
) %>%
|
| 842 |
+
arrange(desc(n_records))
|
| 843 |
+
|
| 844 |
+
DT::datatable(
|
| 845 |
+
species_counts,
|
| 846 |
+
colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
|
| 847 |
+
options = list(pageLength = 10),
|
| 848 |
+
rownames = FALSE
|
| 849 |
+
)
|
| 850 |
+
})
|
| 851 |
+
|
| 852 |
+
# ------------------------------------------------
|
| 853 |
+
# Ggplot: Biodiversity & Socioeconomic Summary
|
| 854 |
+
# ------------------------------------------------
|
| 855 |
+
output$bioSocPlot <- renderPlot({
|
| 856 |
+
df <- socio_data()
|
| 857 |
+
if (nrow(df) == 0) return(NULL)
|
| 858 |
+
|
| 859 |
+
df_plot <- df %>%
|
| 860 |
+
mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
|
| 861 |
+
|
| 862 |
+
ggplot(df_plot, aes(x = IsoLabel)) +
|
| 863 |
+
geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
|
| 864 |
+
geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
|
| 865 |
+
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
| 866 |
+
labs(
|
| 867 |
+
x = "Isochrone (Mode-Time)",
|
| 868 |
+
y = "Blue bars: Unique Species \n | Red line: Population (thousands)",
|
| 869 |
+
title = "Biodiversity & Socioeconomic Summary"
|
| 870 |
+
) +
|
| 871 |
+
theme_minimal(base_size = 14) +
|
| 872 |
+
theme(
|
| 873 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 874 |
+
axis.text.y = element_text(size = 12),
|
| 875 |
+
axis.title.x = element_text(size = 14),
|
| 876 |
+
axis.title.y = element_text(size = 14)
|
| 877 |
+
)
|
| 878 |
+
})
|
| 879 |
+
|
| 880 |
+
# ------------------------------------------------
|
| 881 |
+
# Bar plot: GBIF records by institutionCode
|
| 882 |
+
# ------------------------------------------------
|
| 883 |
+
output$collectionPlot <- renderPlot({
|
| 884 |
+
iso_data <- isochrones_data()
|
| 885 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 886 |
+
plot.new()
|
| 887 |
+
title("No GBIF records found in this isochrone.")
|
| 888 |
+
return(NULL)
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
iso_union <- st_union(iso_data)
|
| 892 |
+
inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 893 |
+
if (nrow(inter_gbif) == 0) {
|
| 894 |
+
plot.new()
|
| 895 |
+
title("No GBIF records found in this isochrone.")
|
| 896 |
+
return(NULL)
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
df_code <- inter_gbif %>%
|
| 900 |
+
st_drop_geometry() %>%
|
| 901 |
+
group_by(institutionCode) %>%
|
| 902 |
+
summarize(count = n(), .groups = "drop") %>%
|
| 903 |
+
arrange(desc(count))
|
| 904 |
+
|
| 905 |
+
ggplot(df_code, aes(x = reorder(institutionCode, -count), y = count)) +
|
| 906 |
+
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
| 907 |
+
labs(
|
| 908 |
+
x = "Institution Code",
|
| 909 |
+
y = "Number of Records",
|
| 910 |
+
title = "GBIF Records by Institution Code (Isochrone Union)"
|
| 911 |
+
) +
|
| 912 |
+
theme_minimal(base_size = 14) +
|
| 913 |
+
theme(
|
| 914 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 915 |
+
axis.text.y = element_text(size = 12),
|
| 916 |
+
axis.title.x = element_text(size = 14),
|
| 917 |
+
axis.title.y = element_text(size = 14)
|
| 918 |
+
)
|
| 919 |
+
})
|
| 920 |
+
|
| 921 |
+
# ------------------------------------------------
|
| 922 |
+
# Additional Section: mapview for species richness vs. data availability
|
| 923 |
+
# ------------------------------------------------
|
| 924 |
+
output$mapNUI <- renderUI({
|
| 925 |
+
map_n <- mapview(cbg_vect_sf, zcol = "n", layer.name="Data Availability (n)")
|
| 926 |
+
map_n@map
|
| 927 |
+
})
|
| 928 |
+
|
| 929 |
+
output$mapSpeciesUI <- renderUI({
|
| 930 |
+
map_s <- mapview(cbg_vect_sf, zcol = "n_species", layer.name="Species Richness (n_species)")
|
| 931 |
+
map_s@map
|
| 932 |
+
})
|
| 933 |
+
|
| 934 |
+
# ------------------------------------------------
|
| 935 |
+
# Additional Plot: n_observations vs n_species
|
| 936 |
+
# ------------------------------------------------
|
| 937 |
+
output$obsVsSpeciesPlot <- renderPlot({
|
| 938 |
+
# A simple scatter plot of n_observations vs. n_species from cbg_vect_sf
|
| 939 |
+
ggplot(cbg_vect_sf, aes(x = log(n_observations+1), y = log(unique_species+1)) ) +
|
| 940 |
+
geom_point(color = "blue", alpha = 0.6) +
|
| 941 |
+
labs(
|
| 942 |
+
x = "Number of Observations (n_observations)",
|
| 943 |
+
y = "Number of Species (n_species)",
|
| 944 |
+
title = "Data Availability vs. Species Richness"
|
| 945 |
+
) +
|
| 946 |
+
theme_minimal(base_size = 14)
|
| 947 |
+
})
|
| 948 |
+
|
| 949 |
+
# ------------------------------------------------
|
| 950 |
+
# Additional Plot: Linear model of n_species ~ n_observations + median_inc + ndvi_mean
|
| 951 |
+
# ------------------------------------------------
|
| 952 |
+
# output$lmCoefficientsPlot <- renderPlot({
|
| 953 |
+
# # Build a linear model with cbg_vect_sf
|
| 954 |
+
# # Must ensure there are no NAs
|
| 955 |
+
# df_lm <- cbg_vect_sf %>%
|
| 956 |
+
# filter(!is.na(n_observations),
|
| 957 |
+
# !is.na(unique_species),
|
| 958 |
+
# !is.na(median_inc),
|
| 959 |
+
# !is.na(ndvi_mean))
|
| 960 |
+
#
|
| 961 |
+
# if (nrow(df_lm) < 5) {
|
| 962 |
+
# # not enough data
|
| 963 |
+
# plot.new()
|
| 964 |
+
# title("Not enough data for linear model.")
|
| 965 |
+
# return(NULL)
|
| 966 |
+
# }
|
| 967 |
+
#
|
| 968 |
+
# # Model
|
| 969 |
+
# fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
|
| 970 |
+
#
|
| 971 |
+
# # Using sjPlot to visualize coefficients
|
| 972 |
+
# # We store in an object and then print it
|
| 973 |
+
# p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
|
| 974 |
+
# print(p)
|
| 975 |
+
# })
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
shinyApp(ui, server)
|
| 979 |
+
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
# library(profvis)
|
| 983 |
+
#
|
| 984 |
+
# profvis({
|
| 985 |
+
# shinyApp(ui, server)
|
| 986 |
+
# })
|
R/old_poc/app_works_no_shinydashboard.R
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
###############################################################################
|
| 2 |
+
# Shiny App: San Francisco Biodiversity Access Decision Support Tool
|
| 3 |
+
# Author: Diego Ellis Soto, et al.
|
| 4 |
+
# University of California Berkeley, ESPM
|
| 5 |
+
# California Academy of Sciences
|
| 6 |
+
###############################################################################
|
| 7 |
+
require(shinyjs)
|
| 8 |
+
library(shiny)
|
| 9 |
+
library(leaflet)
|
| 10 |
+
library(mapboxapi)
|
| 11 |
+
library(tidyverse)
|
| 12 |
+
library(tidycensus)
|
| 13 |
+
library(sf)
|
| 14 |
+
library(DT)
|
| 15 |
+
library(RColorBrewer)
|
| 16 |
+
library(terra)
|
| 17 |
+
library(data.table) # for fread
|
| 18 |
+
library(mapview) # for mapview objects
|
| 19 |
+
library(sjPlot) # for plotting lm model coefficients
|
| 20 |
+
library(sjlabelled) # optional if needed for sjPlot
|
| 21 |
+
require(bslib)
|
| 22 |
+
require(shinycssloaders)
|
| 23 |
+
|
| 24 |
+
source('R/setup.R') # Ensure this script loads necessary data objects
|
| 25 |
+
|
| 26 |
+
# Define your Mapbox token securely
|
| 27 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
| 28 |
+
|
| 29 |
+
# Global theme definition
|
| 30 |
+
theme <- bs_theme(
|
| 31 |
+
bootswatch = "flatly",
|
| 32 |
+
base_font = font_google("Roboto"),
|
| 33 |
+
heading_font = font_google("Roboto Slab"),
|
| 34 |
+
bg = "#f8f9fa",
|
| 35 |
+
fg = "#212529"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# ------------------------------------------------
|
| 39 |
+
# 3) UI
|
| 40 |
+
# ------------------------------------------------
|
| 41 |
+
ui <- fluidPage(
|
| 42 |
+
theme = theme, # Introduce a theme from bslib
|
| 43 |
+
|
| 44 |
+
# For dynamically show and hide a 'Calculating' message
|
| 45 |
+
useShinyjs(), # Initialize shinyjs
|
| 46 |
+
div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
|
| 47 |
+
|
| 48 |
+
titlePanel("San Francisco Biodiversity Access Decision Support Tool"),
|
| 49 |
+
p('Explore your local biodiversity and your access to it!'),
|
| 50 |
+
|
| 51 |
+
fluidRow(
|
| 52 |
+
column(
|
| 53 |
+
width = 12, align = "center",
|
| 54 |
+
tags$img(src = "www/UC Berkeley_logo.png",
|
| 55 |
+
height = "120px", style = "margin:10px;"),
|
| 56 |
+
tags$img(src = "www/California_academy_logo.png",
|
| 57 |
+
height = "120px", style = "margin:10px;"),
|
| 58 |
+
tags$img(src = "www/Reimagining_San_Francisco.png",
|
| 59 |
+
height = "120px", style = "margin:10px;")
|
| 60 |
+
),
|
| 61 |
+
theme=bs_theme(bootswatch='yeti')
|
| 62 |
+
),
|
| 63 |
+
|
| 64 |
+
fluidRow(
|
| 65 |
+
column(
|
| 66 |
+
width = 12,
|
| 67 |
+
br(),
|
| 68 |
+
tags$b("App Summary (Fill out with RSF data working group):"),
|
| 69 |
+
p("
|
| 70 |
+
This application allows users to either click on a map or geocode an address
|
| 71 |
+
to generate travel-time isochrones across multiple transportation modes
|
| 72 |
+
(e.g., pedestrian, cycling, driving, driving during traffic).
|
| 73 |
+
It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
|
| 74 |
+
and summarizes biodiversity records from GBIF. Users can explore information
|
| 75 |
+
related to biodiversity in urban environments, including greenspace coverage,
|
| 76 |
+
population estimates, and species diversity within each isochrone.
|
| 77 |
+
"),
|
| 78 |
+
|
| 79 |
+
tags$b("Created by:"),
|
| 80 |
+
p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
|
| 81 |
+
|
| 82 |
+
p("Contact Information: ", strong("diego.ellissoto@berkeley.edu"))
|
| 83 |
+
)
|
| 84 |
+
),
|
| 85 |
+
|
| 86 |
+
br(),
|
| 87 |
+
|
| 88 |
+
# Tabbed Interface
|
| 89 |
+
tabsetPanel(
|
| 90 |
+
# 1) Isochrone Explorer Tab
|
| 91 |
+
tabPanel("Isochrone Explorer",
|
| 92 |
+
sidebarLayout(
|
| 93 |
+
sidebarPanel(
|
| 94 |
+
radioButtons(
|
| 95 |
+
"location_choice",
|
| 96 |
+
"Select how to choose your location:",
|
| 97 |
+
choices = c("Address (Geocode)" = "address",
|
| 98 |
+
"Click on Map" = "map_click"),
|
| 99 |
+
selected = "map_click"
|
| 100 |
+
),
|
| 101 |
+
|
| 102 |
+
conditionalPanel(
|
| 103 |
+
condition = "input.location_choice == 'address'",
|
| 104 |
+
mapboxGeocoderInput(
|
| 105 |
+
inputId = "geocoder",
|
| 106 |
+
placeholder = "Search for an address",
|
| 107 |
+
access_token = mapbox_token
|
| 108 |
+
)
|
| 109 |
+
),
|
| 110 |
+
|
| 111 |
+
checkboxGroupInput(
|
| 112 |
+
"transport_modes",
|
| 113 |
+
"Select Transportation Modes:",
|
| 114 |
+
choices = list("Driving" = "driving",
|
| 115 |
+
"Walking" = "walking",
|
| 116 |
+
"Cycling" = "cycling",
|
| 117 |
+
"Driving with Traffic"= "driving-traffic"),
|
| 118 |
+
selected = c("driving", "walking")
|
| 119 |
+
),
|
| 120 |
+
|
| 121 |
+
checkboxGroupInput(
|
| 122 |
+
"iso_times",
|
| 123 |
+
"Select Isochrone Times (minutes):",
|
| 124 |
+
choices = list("5" = 5, "10" = 10, "15" = 15),
|
| 125 |
+
selected = c(5, 10)
|
| 126 |
+
),
|
| 127 |
+
|
| 128 |
+
actionButton("generate_iso", "Generate Isochrones"),
|
| 129 |
+
actionButton("clear_map", "Clear")
|
| 130 |
+
),
|
| 131 |
+
|
| 132 |
+
mainPanel(
|
| 133 |
+
leafletOutput("isoMap", height = 600),
|
| 134 |
+
|
| 135 |
+
fluidRow(
|
| 136 |
+
column(12,
|
| 137 |
+
br(),
|
| 138 |
+
uiOutput("bioScoreBox"),
|
| 139 |
+
br(),
|
| 140 |
+
uiOutput("closestGreenspaceUI")
|
| 141 |
+
)
|
| 142 |
+
),
|
| 143 |
+
|
| 144 |
+
br(),
|
| 145 |
+
DTOutput("dataTable") %>% withSpinner(type = 8, color = "#337ab7"),
|
| 146 |
+
|
| 147 |
+
br(),
|
| 148 |
+
br(),
|
| 149 |
+
fluidRow(
|
| 150 |
+
column(12,
|
| 151 |
+
plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#337ab7")
|
| 152 |
+
)
|
| 153 |
+
),
|
| 154 |
+
|
| 155 |
+
br(),
|
| 156 |
+
br(),
|
| 157 |
+
br(),
|
| 158 |
+
fluidRow(
|
| 159 |
+
column(12,
|
| 160 |
+
plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#f39c12")
|
| 161 |
+
)
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
)
|
| 165 |
+
),
|
| 166 |
+
|
| 167 |
+
# 2) GBIF Summaries Tab
|
| 168 |
+
tabPanel(
|
| 169 |
+
"GBIF Summaries",
|
| 170 |
+
sidebarLayout(
|
| 171 |
+
sidebarPanel(
|
| 172 |
+
selectInput(
|
| 173 |
+
"class_filter",
|
| 174 |
+
"Select a GBIF Class to Summarize:",
|
| 175 |
+
choices = c("All", sort(unique(sf_gbif$class))),
|
| 176 |
+
selected = "All"
|
| 177 |
+
),
|
| 178 |
+
selectInput(
|
| 179 |
+
"family_filter",
|
| 180 |
+
"Filter by Family (optional):",
|
| 181 |
+
choices = c("All", sort(unique(sf_gbif$family))),
|
| 182 |
+
selected = "All"
|
| 183 |
+
)
|
| 184 |
+
),
|
| 185 |
+
mainPanel(
|
| 186 |
+
DTOutput("classTable"),
|
| 187 |
+
br(),
|
| 188 |
+
h3("Observations vs. Species Richness"),
|
| 189 |
+
plotOutput("obsVsSpeciesPlot", height = "300px"),
|
| 190 |
+
p("This plot displays the relationship between the number of observations and the species richness. Use this visualization to understand data coverage and biodiversity trends.")
|
| 191 |
+
)
|
| 192 |
+
)
|
| 193 |
+
) %>% withSpinner(type = 8, color = "#337ab7")
|
| 194 |
+
),
|
| 195 |
+
|
| 196 |
+
# Additional Information and Next Steps
|
| 197 |
+
fluidRow(
|
| 198 |
+
column(
|
| 199 |
+
width = 12,
|
| 200 |
+
tags$b("Reimagining San Francisco (Fill out with CAS):"),
|
| 201 |
+
p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
|
| 202 |
+
and technological dimensions to shape a sustainable future for the Bay Area.
|
| 203 |
+
This collaboration unites diverse stakeholders to explore innovations in urban planning,
|
| 204 |
+
conservation, and community engagement. The Reimagining San Francisco Data Working Group has been tasked with identifying and integrating multiple sources of socio-ecological biodiversity information in a co-development framework."),
|
| 205 |
+
|
| 206 |
+
tags$b("Why Biodiversity Access Matters (Polish this):"),
|
| 207 |
+
p("Ensuring equitable access to biodiversity is essential for human well-being,
|
| 208 |
+
ecological resilience, and global policy decisions related to conservation.
|
| 209 |
+
Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
|
| 210 |
+
and provide cultural, recreational, and health benefits to local communities.
|
| 211 |
+
Recognizing that cities are particularly complex socio-ecological systems facing both legacies of sociocultural practices as well as current ongoing dynamic human activities and pressures.
|
| 212 |
+
Incorporating multiple facets of biodiversity metrics alongside variables employed by city planners, human geographers, and decision-makers into urban planning will allow a more integrative lens in creating a sustainable future for cities and their residents."),
|
| 213 |
+
|
| 214 |
+
tags$b("How We Calculate Biodiversity Access Percentile:"),
|
| 215 |
+
p("Total unique species found within the user-generated isochrone.
|
| 216 |
+
We then compare that value to the distribution of unique species counts across all census block groups,
|
| 217 |
+
converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
|
| 218 |
+
A higher percentile indicates greater biodiversity within the chosen area,
|
| 219 |
+
relative to other parts of the city or region.")
|
| 220 |
+
),
|
| 221 |
+
|
| 222 |
+
tags$b("Next Steps:"),
|
| 223 |
+
tags$ul(
|
| 224 |
+
tags$li("Add impervious surface"),
|
| 225 |
+
tags$li("National walkability score"),
|
| 226 |
+
tags$li("Social vulnerability score"),
|
| 227 |
+
tags$li("NatureServe biodiversity maps"),
|
| 228 |
+
tags$li("Calculate cold-hotspots within aggregation of H6 bins instead of by census block group: Ask Carl"),
|
| 229 |
+
tags$li("Species range maps"),
|
| 230 |
+
tags$li("Add common name GBIF"),
|
| 231 |
+
tags$li("Partner orgs"),
|
| 232 |
+
tags$li("Optimize speed -> store variables -> H-ify the world?"),
|
| 233 |
+
tags$li("Brainstorm and co-develop the biodiversity access score"),
|
| 234 |
+
tags$li("For the GBIF summaries, add an annotated GBIF_sf with environmental variables so we can see landcover type association across the biodiversity within the isochrone.")
|
| 235 |
+
)
|
| 236 |
+
)
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# ------------------------------------------------
|
| 240 |
+
# 4) Server
|
| 241 |
+
# ------------------------------------------------
|
| 242 |
+
server <- function(input, output, session) {
|
| 243 |
+
|
| 244 |
+
chosen_point <- reactiveVal(NULL)
|
| 245 |
+
|
| 246 |
+
# ------------------------------------------------
|
| 247 |
+
# Leaflet Base + Hide Overlays
|
| 248 |
+
# ------------------------------------------------
|
| 249 |
+
output$isoMap <- renderLeaflet({
|
| 250 |
+
pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
|
| 251 |
+
|
| 252 |
+
pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
|
| 253 |
+
# 2) Color palette for data availability
|
| 254 |
+
pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
|
| 255 |
+
|
| 256 |
+
leaflet() %>%
|
| 257 |
+
addTiles(group = "Street Map (Default)") %>%
|
| 258 |
+
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
|
| 259 |
+
addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
|
| 260 |
+
|
| 261 |
+
addPolygons(
|
| 262 |
+
data = cbg_vect_sf,
|
| 263 |
+
group = "Income",
|
| 264 |
+
fillColor = ~pal_cbg(medincE),
|
| 265 |
+
fillOpacity = 0.6,
|
| 266 |
+
color = "white",
|
| 267 |
+
weight = 1,
|
| 268 |
+
label=~medincE,
|
| 269 |
+
highlightOptions = highlightOptions(
|
| 270 |
+
weight = 5,
|
| 271 |
+
color = "blue",
|
| 272 |
+
fillOpacity = 0.5,
|
| 273 |
+
bringToFront = TRUE
|
| 274 |
+
),
|
| 275 |
+
labelOptions = labelOptions(
|
| 276 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 277 |
+
textsize = "12px",
|
| 278 |
+
direction = "auto"
|
| 279 |
+
)
|
| 280 |
+
) %>%
|
| 281 |
+
|
| 282 |
+
addPolygons(
|
| 283 |
+
data = osm_greenspace,
|
| 284 |
+
group = "Greenspace",
|
| 285 |
+
fillColor = "darkgreen",
|
| 286 |
+
fillOpacity = 0.3,
|
| 287 |
+
color = "green",
|
| 288 |
+
weight = 1,
|
| 289 |
+
label = ~name,
|
| 290 |
+
highlightOptions = highlightOptions(
|
| 291 |
+
weight = 5,
|
| 292 |
+
color = "blue",
|
| 293 |
+
fillOpacity = 0.5,
|
| 294 |
+
bringToFront = TRUE
|
| 295 |
+
),
|
| 296 |
+
labelOptions = labelOptions(
|
| 297 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 298 |
+
textsize = "12px",
|
| 299 |
+
direction = "auto",
|
| 300 |
+
noHide = FALSE # Labels appear on hover
|
| 301 |
+
)
|
| 302 |
+
) %>%
|
| 303 |
+
|
| 304 |
+
addPolygons(
|
| 305 |
+
data = biodiv_hotspots,
|
| 306 |
+
group = "Hotspots (KnowBR)",
|
| 307 |
+
fillColor = "firebrick",
|
| 308 |
+
fillOpacity = 0.2,
|
| 309 |
+
color = "firebrick",
|
| 310 |
+
weight = 2,
|
| 311 |
+
label = ~GEOID,
|
| 312 |
+
highlightOptions = highlightOptions(
|
| 313 |
+
weight = 5,
|
| 314 |
+
color = "blue",
|
| 315 |
+
fillOpacity = 0.5,
|
| 316 |
+
bringToFront = TRUE
|
| 317 |
+
),
|
| 318 |
+
labelOptions = labelOptions(
|
| 319 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 320 |
+
textsize = "12px",
|
| 321 |
+
direction = "auto"
|
| 322 |
+
)
|
| 323 |
+
) %>%
|
| 324 |
+
|
| 325 |
+
addPolygons(
|
| 326 |
+
data = biodiv_coldspots,
|
| 327 |
+
group = "Coldspots (KnowBR)",
|
| 328 |
+
fillColor = "navyblue",
|
| 329 |
+
fillOpacity = 0.2,
|
| 330 |
+
color = "navyblue",
|
| 331 |
+
weight = 2,
|
| 332 |
+
label = ~GEOID,
|
| 333 |
+
highlightOptions = highlightOptions(
|
| 334 |
+
weight = 5,
|
| 335 |
+
color = "blue",
|
| 336 |
+
fillOpacity = 0.5,
|
| 337 |
+
bringToFront = TRUE
|
| 338 |
+
),
|
| 339 |
+
labelOptions = labelOptions(
|
| 340 |
+
style = list("font-weight" = "bold", "color" = "blue"),
|
| 341 |
+
textsize = "12px",
|
| 342 |
+
direction = "auto"
|
| 343 |
+
)
|
| 344 |
+
) %>%
|
| 345 |
+
|
| 346 |
+
# Add richness and nobs
|
| 347 |
+
# -- Richness layer
|
| 348 |
+
addPolygons(
|
| 349 |
+
data = cbg_vect_sf,
|
| 350 |
+
group = "Species Richness",
|
| 351 |
+
fillColor = ~pal_rich(unique_species),
|
| 352 |
+
fillOpacity = 0.6,
|
| 353 |
+
color = "white",
|
| 354 |
+
weight = 1,
|
| 355 |
+
label =~unique_species,
|
| 356 |
+
popup = ~paste0(
|
| 357 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 358 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 359 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 360 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 361 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 362 |
+
)
|
| 363 |
+
) %>%
|
| 364 |
+
|
| 365 |
+
# -- Data Availability layer
|
| 366 |
+
addPolygons(
|
| 367 |
+
data = cbg_vect_sf,
|
| 368 |
+
group = "Data Availability",
|
| 369 |
+
fillColor = ~pal_data(n_observations),
|
| 370 |
+
fillOpacity = 0.6,
|
| 371 |
+
color = "white",
|
| 372 |
+
weight = 1,
|
| 373 |
+
label =~n_observations,
|
| 374 |
+
popup = ~paste0(
|
| 375 |
+
"<strong>GEOID: </strong>", GEOID,
|
| 376 |
+
"<br><strong>Observations: </strong>", n_observations,
|
| 377 |
+
"<br><strong>Species Richness: </strong>", unique_species,
|
| 378 |
+
"<br><strong>Median Income: </strong>", median_inc,
|
| 379 |
+
"<br><strong>Mean NDVI: </strong>", ndvi_mean
|
| 380 |
+
)
|
| 381 |
+
) %>%
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
|
| 385 |
+
addLayersControl(
|
| 386 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 387 |
+
overlayGroups = c("Income", "Greenspace","Species Richness", "Data Availability",
|
| 388 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)"),
|
| 389 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 390 |
+
) %>%
|
| 391 |
+
hideGroup("Income") %>%
|
| 392 |
+
hideGroup("Greenspace") %>%
|
| 393 |
+
hideGroup("Hotspots (KnowBR)") %>%
|
| 394 |
+
hideGroup("Coldspots (KnowBR)") %>%
|
| 395 |
+
hideGroup("Species Richness") %>%
|
| 396 |
+
hideGroup("Data Availability")
|
| 397 |
+
})
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
# ------------------------------------------------
|
| 401 |
+
# Observe map clicks (location_choice = 'map_click')
|
| 402 |
+
# ------------------------------------------------
|
| 403 |
+
observeEvent(input$isoMap_click, {
|
| 404 |
+
req(input$location_choice == "map_click")
|
| 405 |
+
click <- input$isoMap_click
|
| 406 |
+
if (!is.null(click)) {
|
| 407 |
+
chosen_point(c(lon = click$lng, lat = click$lat))
|
| 408 |
+
|
| 409 |
+
# Provide feedback with coordinates
|
| 410 |
+
showNotification(
|
| 411 |
+
paste0("Map clicked at Longitude: ", round(click$lng, 5),
|
| 412 |
+
", Latitude: ", round(click$lat, 5)),
|
| 413 |
+
type = "message"
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# Update the map with a marker
|
| 417 |
+
leafletProxy("isoMap") %>%
|
| 418 |
+
clearMarkers() %>%
|
| 419 |
+
addCircleMarkers(
|
| 420 |
+
lng = click$lng, lat = click$lat,
|
| 421 |
+
radius = 6, color = "firebrick",
|
| 422 |
+
label = "Map Click Location"
|
| 423 |
+
)
|
| 424 |
+
}
|
| 425 |
+
})
|
| 426 |
+
|
| 427 |
+
# ------------------------------------------------
|
| 428 |
+
# Observe geocoder input
|
| 429 |
+
# ------------------------------------------------
|
| 430 |
+
observeEvent(input$geocoder, {
|
| 431 |
+
req(input$location_choice == "address")
|
| 432 |
+
geocode_result <- input$geocoder
|
| 433 |
+
if (!is.null(geocode_result)) {
|
| 434 |
+
# Extract coordinates
|
| 435 |
+
xy <- geocoder_as_xy(geocode_result)
|
| 436 |
+
|
| 437 |
+
# Update the chosen_point reactive value
|
| 438 |
+
chosen_point(c(lon = xy[1], lat = xy[2]))
|
| 439 |
+
|
| 440 |
+
# Provide feedback with the geocoded address and coordinates
|
| 441 |
+
showNotification(
|
| 442 |
+
paste0("Address geocoded to Longitude: ", round(xy[1], 5),
|
| 443 |
+
", Latitude: ", round(xy[2], 5)),
|
| 444 |
+
type = "message"
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
# Update the map with a marker
|
| 448 |
+
leafletProxy("isoMap") %>%
|
| 449 |
+
clearMarkers() %>%
|
| 450 |
+
addCircleMarkers(
|
| 451 |
+
lng = xy[1], lat = xy[2],
|
| 452 |
+
radius = 6, color = "navyblue",
|
| 453 |
+
label = "Geocoded Address"
|
| 454 |
+
) %>%
|
| 455 |
+
flyTo(lng = xy[1], lat = xy[2], zoom = 13)
|
| 456 |
+
}
|
| 457 |
+
})
|
| 458 |
+
|
| 459 |
+
# ------------------------------------------------
|
| 460 |
+
# Observe clearing of map
|
| 461 |
+
# ------------------------------------------------
|
| 462 |
+
observeEvent(input$clear_map, {
|
| 463 |
+
# Reset the chosen point
|
| 464 |
+
chosen_point(NULL)
|
| 465 |
+
|
| 466 |
+
# Clear all markers and isochrones from the map
|
| 467 |
+
leafletProxy("isoMap") %>%
|
| 468 |
+
clearMarkers() %>%
|
| 469 |
+
# clearShapes() %>%
|
| 470 |
+
clearGroup("Isochrones") %>%
|
| 471 |
+
clearGroup("NDVI Raster")
|
| 472 |
+
|
| 473 |
+
# Optional: Reset any other reactive values if needed
|
| 474 |
+
showNotification("Map cleared. You can select a new location.")
|
| 475 |
+
})
|
| 476 |
+
|
| 477 |
+
# ------------------------------------------------
|
| 478 |
+
# Generate Isochrones
|
| 479 |
+
# ------------------------------------------------
|
| 480 |
+
isochrones_data <- eventReactive(input$generate_iso, {
|
| 481 |
+
|
| 482 |
+
leafletProxy("isoMap") %>%
|
| 483 |
+
clearGroup("Isochrones") %>%
|
| 484 |
+
clearGroup("NDVI Raster")
|
| 485 |
+
|
| 486 |
+
# If user selected address:
|
| 487 |
+
if (input$location_choice == "address") {
|
| 488 |
+
if (is.null(input$geocoder)) {
|
| 489 |
+
showNotification("Please use the geocoder to select an address.", type = "error")
|
| 490 |
+
return(NULL)
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
# Coordinates are already set via the geocoder observer
|
| 494 |
+
# No need to geocode again
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
pt <- chosen_point()
|
| 498 |
+
if (is.null(pt)) {
|
| 499 |
+
showNotification("No location selected! Provide an address or click the map.", type = "error")
|
| 500 |
+
return(NULL)
|
| 501 |
+
}
|
| 502 |
+
if (length(input$transport_modes) == 0) {
|
| 503 |
+
showNotification("Select at least one transportation mode.", type = "error")
|
| 504 |
+
return(NULL)
|
| 505 |
+
}
|
| 506 |
+
if (length(input$iso_times) == 0) {
|
| 507 |
+
showNotification("Select at least one isochrone time.", type = "error")
|
| 508 |
+
return(NULL)
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
location_sf <- st_as_sf(
|
| 512 |
+
data.frame(lon = pt["lon"], lat = pt["lat"]),
|
| 513 |
+
coords = c("lon","lat"), crs = 4326
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
iso_list <- list()
|
| 517 |
+
for (mode in input$transport_modes) {
|
| 518 |
+
for (t in input$iso_times) {
|
| 519 |
+
iso <- tryCatch({
|
| 520 |
+
mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
|
| 521 |
+
access_token = mapbox_token)
|
| 522 |
+
}, error = function(e) {
|
| 523 |
+
showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
|
| 524 |
+
NULL
|
| 525 |
+
})
|
| 526 |
+
if (!is.null(iso)) {
|
| 527 |
+
iso$mode <- mode
|
| 528 |
+
iso$time <- t
|
| 529 |
+
iso_list <- append(iso_list, list(iso))
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
if (length(iso_list) == 0) {
|
| 534 |
+
showNotification("No isochrones generated.", type = "warning")
|
| 535 |
+
return(NULL)
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
|
| 539 |
+
all_iso
|
| 540 |
+
})
|
| 541 |
+
|
| 542 |
+
# ------------------------------------------------
|
| 543 |
+
# Plot Isochrones + NDVI
|
| 544 |
+
# ------------------------------------------------
|
| 545 |
+
observeEvent(isochrones_data(), {
|
| 546 |
+
iso_data <- isochrones_data()
|
| 547 |
+
req(iso_data)
|
| 548 |
+
|
| 549 |
+
iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
|
| 550 |
+
pal <- colorRampPalette(brewer.pal(8, "Set2"))
|
| 551 |
+
cols <- pal(nrow(iso_data))
|
| 552 |
+
|
| 553 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 554 |
+
poly_i <- iso_data[i, ]
|
| 555 |
+
leafletProxy("isoMap") %>%
|
| 556 |
+
addPolygons(
|
| 557 |
+
data = poly_i,
|
| 558 |
+
group = "Isochrones",
|
| 559 |
+
color = cols[i],
|
| 560 |
+
weight = 2,
|
| 561 |
+
fillOpacity = 0.4,
|
| 562 |
+
label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
|
| 563 |
+
)
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
iso_union <- st_union(iso_data)
|
| 567 |
+
iso_union_vect <- vect(iso_union)
|
| 568 |
+
ndvi_crop <- crop(ndvi, iso_union_vect)
|
| 569 |
+
ndvi_mask <- mask(ndvi_crop, iso_union_vect)
|
| 570 |
+
ndvi_vals <- values(ndvi_mask)
|
| 571 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 572 |
+
|
| 573 |
+
if (length(ndvi_vals) > 0) {
|
| 574 |
+
ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
|
| 575 |
+
|
| 576 |
+
leafletProxy("isoMap") %>%
|
| 577 |
+
addRasterImage(
|
| 578 |
+
x = ndvi_mask,
|
| 579 |
+
colors = ndvi_pal,
|
| 580 |
+
opacity = 0.7,
|
| 581 |
+
project = TRUE,
|
| 582 |
+
group = "NDVI Raster"
|
| 583 |
+
) %>%
|
| 584 |
+
addLegend(
|
| 585 |
+
position = "bottomright",
|
| 586 |
+
pal = ndvi_pal,
|
| 587 |
+
values = ndvi_vals,
|
| 588 |
+
title = "NDVI"
|
| 589 |
+
)
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
leafletProxy("isoMap") %>%
|
| 593 |
+
addLayersControl(
|
| 594 |
+
baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
|
| 595 |
+
overlayGroups = c("Income", "Greenspace",
|
| 596 |
+
"Hotspots (KnowBR)", "Coldspots (KnowBR)",
|
| 597 |
+
"Isochrones", "NDVI Raster"),
|
| 598 |
+
options = layersControlOptions(collapsed = FALSE)
|
| 599 |
+
)
|
| 600 |
+
})
|
| 601 |
+
|
| 602 |
+
# ------------------------------------------------
|
| 603 |
+
# socio_data Reactive + Summaries
|
| 604 |
+
# ------------------------------------------------
|
| 605 |
+
socio_data <- reactive({
|
| 606 |
+
iso_data <- isochrones_data()
|
| 607 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 608 |
+
return(data.frame())
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
acs_wide <- cbg_vect_sf %>%
|
| 612 |
+
mutate(
|
| 613 |
+
population = popE,
|
| 614 |
+
med_income = medincE
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
hotspot_union <- st_union(biodiv_hotspots)
|
| 618 |
+
coldspot_union <- st_union(biodiv_coldspots)
|
| 619 |
+
|
| 620 |
+
results <- data.frame()
|
| 621 |
+
|
| 622 |
+
# Calculate distance to coldspot and hotspots
|
| 623 |
+
for (i in seq_len(nrow(iso_data))) {
|
| 624 |
+
poly_i <- iso_data[i, ]
|
| 625 |
+
|
| 626 |
+
dist_hot <- st_distance(poly_i, hotspot_union)
|
| 627 |
+
dist_cold <- st_distance(poly_i, coldspot_union)
|
| 628 |
+
dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
|
| 629 |
+
dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
|
| 630 |
+
|
| 631 |
+
inter_acs <- st_intersection(acs_wide, poly_i)
|
| 632 |
+
|
| 633 |
+
vect_acs_wide <- vect(acs_wide)
|
| 634 |
+
vect_poly_i <- vect(poly_i)
|
| 635 |
+
inter_acs <- intersect(vect_acs_wide, vect_poly_i)
|
| 636 |
+
inter_acs = st_as_sf(inter_acs)
|
| 637 |
+
|
| 638 |
+
pop_total <- 0
|
| 639 |
+
inc_str <- "N/A"
|
| 640 |
+
if (nrow(inter_acs) > 0) {
|
| 641 |
+
inter_acs$area <- st_area(inter_acs)
|
| 642 |
+
inter_acs$area_num <- as.numeric(inter_acs$area)
|
| 643 |
+
inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
|
| 644 |
+
inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
|
| 645 |
+
|
| 646 |
+
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
| 647 |
+
|
| 648 |
+
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
| 649 |
+
sum(inter_acs$area_num, na.rm = TRUE)
|
| 650 |
+
if (!is.na(w_income) && w_income > 0) {
|
| 651 |
+
inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
|
| 652 |
+
}
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
# Intersection with greenspace
|
| 656 |
+
vec_osm_greenspace <- vect(osm_greenspace)
|
| 657 |
+
inter_gs <- intersect(vec_osm_greenspace, vect_poly_i)
|
| 658 |
+
inter_gs = st_as_sf(inter_gs)
|
| 659 |
+
|
| 660 |
+
gs_area_m2 <- 0
|
| 661 |
+
if (nrow(inter_gs) > 0) {
|
| 662 |
+
gs_area_m2 <- sum(st_area(inter_gs))
|
| 663 |
+
}
|
| 664 |
+
iso_area_m2 <- as.numeric(st_area(poly_i))
|
| 665 |
+
gs_area_m2 <- as.numeric(gs_area_m2)
|
| 666 |
+
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
| 667 |
+
|
| 668 |
+
# NDVI Calculation
|
| 669 |
+
poly_vect <- vect(poly_i)
|
| 670 |
+
ndvi_crop <- crop(ndvi, poly_vect)
|
| 671 |
+
ndvi_mask <- mask(ndvi_crop, poly_vect)
|
| 672 |
+
ndvi_vals <- values(ndvi_mask)
|
| 673 |
+
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 674 |
+
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
|
| 675 |
+
|
| 676 |
+
# Intersection with GBIF data
|
| 677 |
+
inter_gbif <- intersect(vect_gbif, vect_poly_i)
|
| 678 |
+
inter_gbif <- st_as_sf(inter_gbif)
|
| 679 |
+
|
| 680 |
+
inter_gbif_acs <- sf_gbif %>%
|
| 681 |
+
mutate(
|
| 682 |
+
income = medincE,
|
| 683 |
+
ndvi = ndvi_sentinel
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
if (nrow(inter_gbif) > 0) {
|
| 687 |
+
inter_gbif_acs <- inter_gbif_acs[inter_gbif_acs$GEOID %in% inter_gbif$GEOID, ]
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
n_records <- nrow(inter_gbif)
|
| 691 |
+
n_species <- length(unique(inter_gbif$species))
|
| 692 |
+
|
| 693 |
+
n_birds <- length(unique(inter_gbif$species[inter_gbif$class == "Aves"]))
|
| 694 |
+
n_mammals <- length(unique(inter_gbif$species[inter_gbif$class == "Mammalia"]))
|
| 695 |
+
n_plants <- length(unique(inter_gbif$species[inter_gbif$class %in%
|
| 696 |
+
c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
|
| 697 |
+
"Equisetopsida","Bryopsida","Marchantiopsida") ]))
|
| 698 |
+
|
| 699 |
+
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
| 700 |
+
|
| 701 |
+
row_i <- data.frame(
|
| 702 |
+
Mode = tools::toTitleCase(poly_i$mode),
|
| 703 |
+
Time = poly_i$time,
|
| 704 |
+
IsochroneArea_km2 = iso_area_km2,
|
| 705 |
+
DistToHotspot_km = dist_hot_km,
|
| 706 |
+
DistToColdspot_km = dist_cold_km,
|
| 707 |
+
EstimatedPopulation = pop_total,
|
| 708 |
+
MedianIncome = inc_str,
|
| 709 |
+
MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
|
| 710 |
+
GBIF_Records = n_records,
|
| 711 |
+
GBIF_Species = n_species,
|
| 712 |
+
Bird_Species = n_birds,
|
| 713 |
+
Mammal_Species = n_mammals,
|
| 714 |
+
Plant_Species = n_plants,
|
| 715 |
+
Greenspace_m2 = round(gs_area_m2, 2),
|
| 716 |
+
Greenspace_percent = round(gs_percent, 2),
|
| 717 |
+
stringsAsFactors = FALSE
|
| 718 |
+
)
|
| 719 |
+
results <- rbind(results, row_i)
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
iso_union <- st_union(iso_data)
|
| 723 |
+
vect_iso <- vect(iso_union)
|
| 724 |
+
inter_all_gbif <- intersect(vect_gbif, vect_iso)
|
| 725 |
+
inter_all_gbif <- st_as_sf(inter_all_gbif)
|
| 726 |
+
|
| 727 |
+
union_n_species <- length(unique(inter_all_gbif$species))
|
| 728 |
+
rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
| 729 |
+
attr(results, "bio_percentile") <- rank_percentile
|
| 730 |
+
|
| 731 |
+
# Closest Greenspace from ANY part of the isochrone
|
| 732 |
+
dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
|
| 733 |
+
if (length(dist_mat) > 0) {
|
| 734 |
+
min_dist <- min(dist_mat)
|
| 735 |
+
min_idx <- which.min(dist_mat)
|
| 736 |
+
gs_name <- osm_greenspace$name[min_idx]
|
| 737 |
+
attr(results, "closest_greenspace") <- gs_name
|
| 738 |
+
} else {
|
| 739 |
+
attr(results, "closest_greenspace") <- "None"
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
results
|
| 743 |
+
})
|
| 744 |
+
|
| 745 |
+
# ------------------------------------------------
|
| 746 |
+
# Render main summary table
|
| 747 |
+
# ------------------------------------------------
|
| 748 |
+
output$dataTable <- renderDT({
|
| 749 |
+
df <- socio_data()
|
| 750 |
+
if (nrow(df) == 0) {
|
| 751 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 752 |
+
}
|
| 753 |
+
DT::datatable(
|
| 754 |
+
df,
|
| 755 |
+
colnames = c(
|
| 756 |
+
"Mode" = "Mode",
|
| 757 |
+
"Time (min)" = "Time",
|
| 758 |
+
"Area (km²)" = "IsochroneArea_km2",
|
| 759 |
+
"Dist. Hotspot (km)" = "DistToHotspot_km",
|
| 760 |
+
"Dist. Coldspot (km)" = "DistToColdspot_km",
|
| 761 |
+
"Population" = "EstimatedPopulation",
|
| 762 |
+
"Median Income" = "MedianIncome",
|
| 763 |
+
"Mean NDVI" = "MeanNDVI",
|
| 764 |
+
"GBIF Records" = "GBIF_Records",
|
| 765 |
+
"Unique Species" = "GBIF_Species",
|
| 766 |
+
"Bird Species" = "Bird_Species",
|
| 767 |
+
"Mammal Species" = "Mammal_Species",
|
| 768 |
+
"Plant Species" = "Plant_Species",
|
| 769 |
+
"Greenspace (m²)" = "Greenspace_m2",
|
| 770 |
+
"Greenspace (%)" = "Greenspace_percent"
|
| 771 |
+
),
|
| 772 |
+
options = list(pageLength = 10, autoWidth = TRUE),
|
| 773 |
+
rownames = FALSE
|
| 774 |
+
)
|
| 775 |
+
})
|
| 776 |
+
|
| 777 |
+
# ------------------------------------------------
|
| 778 |
+
# Biodiversity Access Score + Closest Greenspace
|
| 779 |
+
# ------------------------------------------------
|
| 780 |
+
output$bioScoreBox <- renderUI({
|
| 781 |
+
df <- socio_data()
|
| 782 |
+
if (nrow(df) == 0) return(NULL)
|
| 783 |
+
|
| 784 |
+
percentile <- attr(df, "bio_percentile")
|
| 785 |
+
if (is.null(percentile)) percentile <- "N/A"
|
| 786 |
+
else percentile <- paste0(percentile, "th Percentile")
|
| 787 |
+
|
| 788 |
+
wellPanel(
|
| 789 |
+
HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
|
| 790 |
+
)
|
| 791 |
+
})
|
| 792 |
+
|
| 793 |
+
output$closestGreenspaceUI <- renderUI({
|
| 794 |
+
df <- socio_data()
|
| 795 |
+
if (nrow(df) == 0) return(NULL)
|
| 796 |
+
gs_name <- attr(df, "closest_greenspace")
|
| 797 |
+
if (is.null(gs_name)) gs_name <- "None"
|
| 798 |
+
|
| 799 |
+
tagList(
|
| 800 |
+
strong("Closest Greenspace (from any part of the Isochrone):"),
|
| 801 |
+
p(gs_name)
|
| 802 |
+
)
|
| 803 |
+
})
|
| 804 |
+
|
| 805 |
+
# ------------------------------------------------
|
| 806 |
+
# Secondary table: user-selected CLASS & FAMILY
|
| 807 |
+
# ------------------------------------------------
|
| 808 |
+
output$classTable <- renderDT({
|
| 809 |
+
iso_data <- isochrones_data()
|
| 810 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 811 |
+
return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
iso_union <- st_union(iso_data)
|
| 815 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 816 |
+
|
| 817 |
+
vect_iso <- vect(iso_union)
|
| 818 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
| 819 |
+
inter_gbif = st_as_sf(inter_gbif)
|
| 820 |
+
|
| 821 |
+
# Add a quick ACS intersection for mean income & NDVI if needed
|
| 822 |
+
acs_wide <- cbg_vect_sf %>% mutate(
|
| 823 |
+
income = median_inc,
|
| 824 |
+
ndvi = ndvi_mean
|
| 825 |
+
)
|
| 826 |
+
# this can be skipped !
|
| 827 |
+
# inter_gbif_acs <- st_intersection(inter_gbif, acs_wide)
|
| 828 |
+
|
| 829 |
+
inter_gbif_acs = sf_gbif |> dplyr::mutate(income = medincE,
|
| 830 |
+
ndvi = ndvi_sentinel)#We can do this because we preannotated ndvi and us census information
|
| 831 |
+
|
| 832 |
+
if (input$class_filter != "All") {
|
| 833 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
|
| 834 |
+
}
|
| 835 |
+
if (input$family_filter != "All") {
|
| 836 |
+
inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
if (nrow(inter_gbif_acs) == 0) {
|
| 840 |
+
return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
species_counts <- inter_gbif_acs %>%
|
| 844 |
+
st_drop_geometry() %>%
|
| 845 |
+
group_by(species) %>%
|
| 846 |
+
summarize(
|
| 847 |
+
n_records = n(),
|
| 848 |
+
mean_income = round(mean(income, na.rm=TRUE), 2),
|
| 849 |
+
mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
|
| 850 |
+
.groups = "drop"
|
| 851 |
+
) %>%
|
| 852 |
+
arrange(desc(n_records))
|
| 853 |
+
|
| 854 |
+
DT::datatable(
|
| 855 |
+
species_counts,
|
| 856 |
+
colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
|
| 857 |
+
options = list(pageLength = 10),
|
| 858 |
+
rownames = FALSE
|
| 859 |
+
)
|
| 860 |
+
})
|
| 861 |
+
|
| 862 |
+
# ------------------------------------------------
|
| 863 |
+
# Ggplot: Biodiversity & Socioeconomic Summary
|
| 864 |
+
# ------------------------------------------------
|
| 865 |
+
output$bioSocPlot <- renderPlot({
|
| 866 |
+
df <- socio_data()
|
| 867 |
+
if (nrow(df) == 0) return(NULL)
|
| 868 |
+
|
| 869 |
+
df_plot <- df %>%
|
| 870 |
+
mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
|
| 871 |
+
|
| 872 |
+
ggplot(df_plot, aes(x = IsoLabel)) +
|
| 873 |
+
geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
|
| 874 |
+
geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
|
| 875 |
+
geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
|
| 876 |
+
labs(
|
| 877 |
+
x = "Isochrone (Mode-Time)",
|
| 878 |
+
y = "Unique Species (Blue) | Population (Red) (Thousands)",
|
| 879 |
+
title = "Biodiversity & Socioeconomic Summary"
|
| 880 |
+
) +
|
| 881 |
+
theme_minimal(base_size = 14) +
|
| 882 |
+
theme(
|
| 883 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 884 |
+
axis.text.y = element_text(size = 12),
|
| 885 |
+
axis.title.x = element_text(size = 14),
|
| 886 |
+
axis.title.y = element_text(size = 14),
|
| 887 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
| 888 |
+
)
|
| 889 |
+
})
|
| 890 |
+
|
| 891 |
+
# ------------------------------------------------
|
| 892 |
+
# Bar plot: GBIF records by institutionCode
|
| 893 |
+
# ------------------------------------------------
|
| 894 |
+
output$collectionPlot <- renderPlot({
|
| 895 |
+
iso_data <- isochrones_data()
|
| 896 |
+
if (is.null(iso_data) || nrow(iso_data) == 0) {
|
| 897 |
+
plot.new()
|
| 898 |
+
title("No GBIF records found in this isochrone.")
|
| 899 |
+
return(NULL)
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
iso_union <- st_union(iso_data)
|
| 903 |
+
# inter_gbif <- st_intersection(sf_gbif, iso_union)
|
| 904 |
+
|
| 905 |
+
vect_iso <- vect(iso_union)
|
| 906 |
+
inter_gbif <- intersect(vect_gbif, vect_iso)
|
| 907 |
+
inter_gbif = st_as_sf(inter_gbif)
|
| 908 |
+
|
| 909 |
+
if (nrow(inter_gbif) == 0) {
|
| 910 |
+
plot.new()
|
| 911 |
+
title("No GBIF records found in this isochrone.")
|
| 912 |
+
return(NULL)
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
df_code <- inter_gbif %>%
|
| 916 |
+
st_drop_geometry() %>%
|
| 917 |
+
group_by(institutionCode) %>%
|
| 918 |
+
summarize(count = n(), .groups = "drop") %>%
|
| 919 |
+
arrange(desc(count)) %>%
|
| 920 |
+
mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
|
| 921 |
+
|
| 922 |
+
ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) + # replaced institutionCode with truncatedCode
|
| 923 |
+
geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
|
| 924 |
+
labs(
|
| 925 |
+
x = "Institution Code (Truncated)",
|
| 926 |
+
y = "Number of Records",
|
| 927 |
+
title = "GBIF Records by Institution Code (Isochrone Union)"
|
| 928 |
+
) +
|
| 929 |
+
theme_minimal(base_size = 14) +
|
| 930 |
+
theme(
|
| 931 |
+
axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
|
| 932 |
+
axis.text.y = element_text(size = 12),
|
| 933 |
+
axis.title.x = element_text(size = 14),
|
| 934 |
+
axis.title.y = element_text(size = 14),
|
| 935 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
| 936 |
+
)
|
| 937 |
+
})
|
| 938 |
+
|
| 939 |
+
# ------------------------------------------------
|
| 940 |
+
# Additional Section: mapview for species richness vs. data availability
|
| 941 |
+
# ------------------------------------------------
|
| 942 |
+
output$mapNUI <- renderUI({
|
| 943 |
+
map_n <- mapview(cbg_vect_sf, zcol = "n", layer.name="Data Availability (n)")
|
| 944 |
+
map_n@map
|
| 945 |
+
})
|
| 946 |
+
|
| 947 |
+
output$mapSpeciesUI <- renderUI({
|
| 948 |
+
map_s <- mapview(cbg_vect_sf, zcol = "n_species", layer.name="Species Richness (n_species)")
|
| 949 |
+
map_s@map
|
| 950 |
+
})
|
| 951 |
+
|
| 952 |
+
|
| 953 |
+
|
| 954 |
+
|
| 955 |
+
# ------------------------------------------------
|
| 956 |
+
# Additional Plot: n_observations vs n_species
|
| 957 |
+
# ------------------------------------------------
|
| 958 |
+
|
| 959 |
+
# Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
|
| 960 |
+
|
| 961 |
+
filtered_data <- reactive({
|
| 962 |
+
data <- cbg_vect_sf
|
| 963 |
+
if (input$class_filter != "All") {
|
| 964 |
+
data <- data[data$class == input$class_filter, ]
|
| 965 |
+
}
|
| 966 |
+
if (input$family_filter != "All") {
|
| 967 |
+
data <- data[data$family == input$family_filter, ]
|
| 968 |
+
}
|
| 969 |
+
data
|
| 970 |
+
})
|
| 971 |
+
|
| 972 |
+
output$obsVsSpeciesPlot <- renderPlot({
|
| 973 |
+
data <- filtered_data()
|
| 974 |
+
if (nrow(data) == 0) {
|
| 975 |
+
plot.new()
|
| 976 |
+
title("No data available for selected filters.")
|
| 977 |
+
return(NULL)
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
|
| 981 |
+
geom_point(color = "blue", alpha = 0.6) +
|
| 982 |
+
labs(
|
| 983 |
+
x = "Log(Number of Observations + 1)",
|
| 984 |
+
y = "Log(Species Richness + 1)",
|
| 985 |
+
title = "Data Availability vs. Species Richness"
|
| 986 |
+
) +
|
| 987 |
+
theme_minimal(base_size = 14) +
|
| 988 |
+
theme(
|
| 989 |
+
axis.text.x = element_text(size = 12),
|
| 990 |
+
axis.text.y = element_text(size = 12),
|
| 991 |
+
axis.title.x = element_text(size = 14),
|
| 992 |
+
axis.title.y = element_text(size = 14),
|
| 993 |
+
plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
|
| 994 |
+
)
|
| 995 |
+
})
|
| 996 |
+
|
| 997 |
+
# ------------------------------------------------
|
| 998 |
+
# [Optional: Linear Model Plot (Commented Out)]
|
| 999 |
+
# ------------------------------------------------
|
| 1000 |
+
# Uncomment and adjust if needed
|
| 1001 |
+
# output$lmCoefficientsPlot <- renderPlot({
|
| 1002 |
+
# df_lm <- cbg_vect_sf %>%
|
| 1003 |
+
# filter(!is.na(n_observations),
|
| 1004 |
+
# !is.na(unique_species),
|
| 1005 |
+
# !is.na(median_inc),
|
| 1006 |
+
# !is.na(ndvi_mean))
|
| 1007 |
+
#
|
| 1008 |
+
# if (nrow(df_lm) < 5) {
|
| 1009 |
+
# plot.new()
|
| 1010 |
+
# title("Not enough data for linear model.")
|
| 1011 |
+
# return(NULL)
|
| 1012 |
+
# }
|
| 1013 |
+
#
|
| 1014 |
+
# fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
|
| 1015 |
+
#
|
| 1016 |
+
# p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
|
| 1017 |
+
# print(p)
|
| 1018 |
+
# })
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
# Run the Shiny app
|
| 1022 |
+
shinyApp(ui, server)
|
R/old_poc/make_RSF_hexbin.R
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
require(hexSticker)
|
| 3 |
+
|
| 4 |
+
imgurl <- "www/Reimagining_San_Francisco.png"
|
| 5 |
+
|
| 6 |
+
sticker(imgurl, package="BioDivAccess", p_size=20, s_x=1, s_y=.75, s_width=.6,p_family = "Roboto",
|
| 7 |
+
filename="www/hexbin_RSF_logo.png")
|
| 8 |
+
|
| 9 |
+
|
R/setup.R
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# Setup: HuggingFace-optimized data loading
|
| 3 |
+
# ============================================================================
|
| 4 |
+
# This version uses GDAL virtual file system and temporary downloads
|
| 5 |
+
# for efficient loading in cloud/ephemeral environments like HuggingFace Spaces.
|
| 6 |
+
# For local development with persistent caching, use setup_local.R instead.
|
| 7 |
+
|
| 8 |
+
require(shinyjs)
|
| 9 |
+
library(shiny)
|
| 10 |
+
library(shinydashboard)
|
| 11 |
+
library(leaflet)
|
| 12 |
+
library(mapboxapi)
|
| 13 |
+
library(tidyverse)
|
| 14 |
+
library(tidycensus)
|
| 15 |
+
library(sf)
|
| 16 |
+
library(DT)
|
| 17 |
+
library(RColorBrewer)
|
| 18 |
+
library(terra)
|
| 19 |
+
library(data.table)
|
| 20 |
+
library(mapview)
|
| 21 |
+
library(sjPlot)
|
| 22 |
+
library(sjlabelled)
|
| 23 |
+
library(bslib)
|
| 24 |
+
library(shinycssloaders)
|
| 25 |
+
|
| 26 |
+
# ============================================================================
|
| 27 |
+
# API Keys
|
| 28 |
+
# ============================================================================
|
| 29 |
+
mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
|
| 30 |
+
|
| 31 |
+
# ============================================================================
|
| 32 |
+
# Load Data from HuggingFace
|
| 33 |
+
# ============================================================================
|
| 34 |
+
|
| 35 |
+
# -- Greenspace (read directly from URL via GDAL virtual file system)
|
| 36 |
+
osm_greenspace <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/greenspaces_osm_nad83.shp", quiet = TRUE) |>
|
| 37 |
+
st_transform(4326)
|
| 38 |
+
|
| 39 |
+
if (!"name" %in% names(osm_greenspace)) {
|
| 40 |
+
osm_greenspace$name <- "Unnamed Greenspace"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# -- Greenspace Distance Rasters (read directly from URL via GDAL virtual file system)
|
| 44 |
+
greenspace_dist_raster <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/nearest_greenspace_dist.tif")
|
| 45 |
+
greenspace_osmid_raster <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/nearest_greenspace_osmid.tif")
|
| 46 |
+
|
| 47 |
+
# -- NDVI Raster (read directly from URL via GDAL virtual file system)
|
| 48 |
+
ndvi <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/SF_EastBay_NDVI_Sentinel_10.tif")
|
| 49 |
+
|
| 50 |
+
# -- GBIF data (loaded via DuckDB parquet in app.R server function)
|
| 51 |
+
# DuckDB can read parquet files directly from URLs
|
| 52 |
+
gbif_parquet <- "https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/gbif_census_ndvi_anno.parquet"
|
| 53 |
+
|
| 54 |
+
# -- Precomputed CBG data (download to /tmp and load)
|
| 55 |
+
download.file(
|
| 56 |
+
'https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/cbg_vect_sf.Rdata',
|
| 57 |
+
'/tmp/cbg_vect_sf.Rdata'
|
| 58 |
+
)
|
| 59 |
+
load('/tmp/cbg_vect_sf.Rdata')
|
| 60 |
+
|
| 61 |
+
if (!"unique_species" %in% names(cbg_vect_sf)) {
|
| 62 |
+
cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
| 63 |
+
}
|
| 64 |
+
if (!"n_observations" %in% names(cbg_vect_sf)) {
|
| 65 |
+
cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
| 66 |
+
}
|
| 67 |
+
if (!"median_inc" %in% names(cbg_vect_sf)) {
|
| 68 |
+
cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
| 69 |
+
}
|
| 70 |
+
if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
|
| 71 |
+
cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# -- Hotspots/Coldspots (read directly from URL via GDAL virtual file system)
|
| 75 |
+
biodiv_hotspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/hotspots.shp", quiet = TRUE) |>
|
| 76 |
+
st_transform(4326)
|
| 77 |
+
|
| 78 |
+
biodiv_coldspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/coldspots.shp", quiet = TRUE) |>
|
| 79 |
+
st_transform(4326)
|
| 80 |
+
|
| 81 |
+
# -- RSF Program Projects (read directly from URL via GDAL virtual file system)
|
| 82 |
+
rsf_projects <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/RSF_Program_Projects_polygons.gpkg", quiet = TRUE) |>
|
| 83 |
+
st_transform(4326)
|
README.md
CHANGED
|
@@ -7,203 +7,72 @@ sdk: docker
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
# SF Biodiversity Access Decision Support Tool
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
## What the App Does
|
| 19 |
-
|
| 20 |
-
Users select a location anywhere in San Francisco — either by clicking the map or geocoding an address — and choose one or more transportation modes and travel-time thresholds (5, 10, or 15 minutes). The app then:
|
| 21 |
-
|
| 22 |
-
1. **Generates travel-time isochrones** — polygons representing the area reachable within the chosen time budget — using Mapbox (driving, walking, cycling, traffic-aware driving) and SF Muni GTFS data (transit, walk + transit).
|
| 23 |
-
2. **Summarizes biodiversity** within each isochrone: GBIF occurrence records, unique species richness, and breakdowns by taxonomic group (birds, mammals, plants).
|
| 24 |
-
3. **Summarizes greenspace access**: OSM greenspace coverage (%), distance to nearest greenspace via raster, and NDVI from Sentinel-2.
|
| 25 |
-
4. **Summarizes socioeconomic and environmental justice context**: area-weighted median income and population from ACS Census block groups, CalEnviroScreen Cumulative Impact scores, and SF Environmental Justice community burden scores.
|
| 26 |
-
5. **Computes the Biodiversity Access Index (BAI)** — a composite score benchmarked against citywide empirical distributions — and displays it as a spider/radar plot across seven dimensions: mobility access, route access, biodiversity potential, observation intensity, environmental quality, greenspace cover, and equity context.
|
| 27 |
-
6. **Displays partner RSF Program Projects** as a toggleable map layer.
|
| 28 |
-
|
| 29 |
-
---
|
| 30 |
-
|
| 31 |
-
## App Tabs
|
| 32 |
-
|
| 33 |
-
| Tab | Description |
|
| 34 |
-
|-----|-------------|
|
| 35 |
-
| **Isochrone Explorer** | Interactive map, isochrone generation, BAI spider plot, summary table, and metric plots |
|
| 36 |
-
| **Isochrone Comparer** | Pick two locations (one mode + travel time each) and compare them side by side: two BAI spider plots plus a difference table (transit score, biodiversity score, BAI, closest greenspace). Loads only when opened. |
|
| 37 |
-
| **GBIF Summaries** | Filter GBIF records by taxonomic class and family within the isochrone; species richness vs. sampling effort plot |
|
| 38 |
-
| **Community Science** | Map and table of partner community organizations |
|
| 39 |
-
| **About** | Full methodology, data sources, transport mode descriptions, and BAI explanation |
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
-
##
|
| 44 |
-
|
| 45 |
-
| Mode | Engine | Notes |
|
| 46 |
-
|------|--------|-------|
|
| 47 |
-
| Driving | Mapbox Navigation API | Free-flow road network |
|
| 48 |
-
| Walking | Mapbox Navigation API | Pedestrian paths and crossings |
|
| 49 |
-
| Cycling | Mapbox Navigation API | Dedicated cycle lanes where available |
|
| 50 |
-
| Driving with Traffic | Mapbox Navigation API | Real-time + historical congestion |
|
| 51 |
-
| Transit (GTFS) | gtfsrouter | SF Muni timetable-based stop-to-stop routing |
|
| 52 |
-
| Walk + Transit (Muni) | Mapbox + gtfsrouter | First-mile walk + Muni ride + last-mile walk, all within total time budget |
|
| 53 |
-
|
| 54 |
-
---
|
| 55 |
-
|
| 56 |
-
## Map Layers
|
| 57 |
-
|
| 58 |
-
All layers are toggleable in the map's layer control panel:
|
| 59 |
-
|
| 60 |
-
- **Income** — Median household income per census block group (ACS 5-yr)
|
| 61 |
-
- **Greenspace** — OSM parks and green areas
|
| 62 |
-
- **Greenspace Distance** — Raster showing distance (m) to nearest greenspace
|
| 63 |
-
- **RSF Program Distance** — Raster showing distance (m) to nearest RSF program polygon
|
| 64 |
-
- **RSF Program Projects** — Partner project areas from the RSF Initiative
|
| 65 |
-
- **Hotspots / Coldspots (KnowBR)** — Block groups with anomalously high/low species richness relative to sampling effort
|
| 66 |
-
- **Species Richness** — Unique GBIF species per census block group
|
| 67 |
-
- **Data Availability** — GBIF occurrence records per block group
|
| 68 |
-
- **CalEnviroScreen (CI Score)** — Cumulative environmental burden by census tract
|
| 69 |
-
- **SF EJ Communities** — SF Environmental Justice community burden scores
|
| 70 |
-
- **Transit Routes** — All SF Muni routes from GTFS shapes, colored by official SFMTA route color
|
| 71 |
-
- **Transit Stops** — All SF Muni stops with AM peak headway info
|
| 72 |
-
- **Isochrones** — Generated travel-time polygons
|
| 73 |
-
- **NDVI Raster** — Sentinel-2 NDVI cropped to the isochrone union
|
| 74 |
-
|
| 75 |
-
---
|
| 76 |
-
|
| 77 |
-
## Biodiversity Access Index (BAI)
|
| 78 |
-
|
| 79 |
-
The BAI is a composite indicator benchmarked against **citywide empirical distributions** (ECDFs across all SF census block groups), not just the current session. It comprises seven equally-weighted dimensions:
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
| Observation Intensity | GBIF records / km² | Higher = better |
|
| 87 |
-
| Environmental Quality | Mean NDVI | Higher = better |
|
| 88 |
-
| Greenspace Cover | % OSM greenspace area | Higher = better |
|
| 89 |
-
| Equity Context | SF EJ burden score (inverted) | Lower burden = better |
|
| 90 |
|
| 91 |
-
|
| 92 |
|
| 93 |
---
|
| 94 |
|
| 95 |
-
##
|
| 96 |
-
|
| 97 |
-
```
|
| 98 |
-
SF_biodiv_access_shiny/
|
| 99 |
-
├── app.R # Main app (sources Rscripts/setup_unified.R at startup)
|
| 100 |
-
├── Dockerfile # HF Spaces: install.r + shiny::runApp('app.R', …)
|
| 101 |
-
├── install.r # R package list for Docker
|
| 102 |
-
├── www/ # Static assets (e.g. app_pastel.css, logos)
|
| 103 |
-
├── Rscripts/
|
| 104 |
-
│ ├── setup_unified.R # Loads all app data: local data/cached + HuggingFace fallback
|
| 105 |
-
│ ├── iso_metrics.R # Shared point→isochrone→metrics→BAI→radar functions (used by both isochrone tabs)
|
| 106 |
-
│ └── prep/ # One-off builds → data/output/ (see run_all_prep.R)
|
| 107 |
-
└── data/
|
| 108 |
-
├── cached/ # Downloaded / runtime cache (often gitignored)
|
| 109 |
-
├── output/ # Prep outputs (often gitignored)
|
| 110 |
-
└── source/ # Raw inputs for prep (e.g. RSF polygons, GTFS extract)
|
| 111 |
-
```
|
| 112 |
-
|
| 113 |
-
> **Note:** The `data/` directory is gitignored. See setup instructions below.
|
| 114 |
-
|
| 115 |
-
---
|
| 116 |
|
| 117 |
-
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
| Census block groups + ACS | US Census / tidycensus | `.Rdata` | Downloaded from HuggingFace at startup |
|
| 123 |
-
| NDVI raster | Sentinel-2 (pre-processed) | GeoTIFF | Downloaded from HuggingFace at startup |
|
| 124 |
-
| OSM greenspace polygons | OpenStreetMap | Shapefile bundle | Downloaded from HuggingFace at startup |
|
| 125 |
-
| Greenspace distance rasters | Derived from OSM (see `making-greenspace-raster.R`) | GeoTIFF | Downloaded from HuggingFace at startup |
|
| 126 |
-
| RSF Program distance rasters | Derived from RSF polygons (see `making-rsfprogram-raster.R`) | GeoTIFF | Downloaded from HuggingFace at startup |
|
| 127 |
-
| Biodiversity hotspots/coldspots | KnowBR analysis on GBIF | Shapefile | Downloaded from HuggingFace at startup |
|
| 128 |
-
| SF Muni GTFS | SFMTA official GTFS feed | zip + rds + csv | Downloaded from HuggingFace at startup |
|
| 129 |
-
| CalEnviroScreen 4.0 | OEHHA | GeoPackage | Downloaded from HuggingFace at startup |
|
| 130 |
-
| SF EJ Communities | SF Environment | GeoPackage | Downloaded from HuggingFace at startup |
|
| 131 |
-
| RSF Program Projects | RSF Initiative | GeoPackage | Downloaded from HuggingFace at startup |
|
| 132 |
|
| 133 |
-
**
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
**GBIF queries** use [DuckDB](https://duckdb.org/) with the spatial extension, querying a local `.parquet` file directly via SQL `ST_Intersects` — avoiding loading the full dataset into memory.
|
| 137 |
|
| 138 |
---
|
| 139 |
|
| 140 |
-
## Local Development Setup
|
| 141 |
-
|
| 142 |
-
### Prerequisites
|
| 143 |
-
|
| 144 |
-
Install required R packages:
|
| 145 |
-
|
| 146 |
-
```r
|
| 147 |
-
install.packages(c(
|
| 148 |
-
"shiny", "shinydashboard", "leaflet", "mapboxapi", "tidyverse",
|
| 149 |
-
"tidycensus", "sf", "DT", "RColorBrewer", "terra", "data.table",
|
| 150 |
-
"mapview", "sjPlot", "sjlabelled", "bslib", "shinycssloaders",
|
| 151 |
-
"DBI", "duckdb", "dbplyr", "gtfsrouter", "tidytransit", "fmsb", "scales"
|
| 152 |
-
))
|
| 153 |
-
```
|
| 154 |
|
| 155 |
-
##
|
| 156 |
|
| 157 |
-
|
| 158 |
-
```r
|
| 159 |
-
source("Rscripts/prep/create_annotated_gbif_parquet.R")
|
| 160 |
-
# Creates: data/output/sf-gbif.parquet, data/output/gbif_census_ndvi_anno.parquet
|
| 161 |
-
```
|
| 162 |
|
| 163 |
-
**
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
```
|
| 167 |
-
|
| 168 |
-
**Step 3: Run the app**
|
| 169 |
-
```r
|
| 170 |
-
shiny::runApp("app.R")
|
| 171 |
-
```
|
| 172 |
-
|
| 173 |
-
### Startup Performance
|
| 174 |
-
|
| 175 |
-
Typical startup time: **~6–12 seconds** depending on whether caches exist.
|
| 176 |
|
| 177 |
---
|
| 178 |
|
| 179 |
-
##
|
| 180 |
-
|
| 181 |
-
The app is deployed via Docker on HuggingFace Spaces using `app.R` + `Rscripts/setup_unified.R`. Data is downloaded from the HuggingFace dataset repository into `data/cached/` at startup — so no large files need to be bundled in the image.
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
## Authors
|
| 188 |
|
| 189 |
-
|
| 190 |
-
University of California Berkeley (ESPM) · California Academy of Sciences
|
| 191 |
-
Contact: [diego.ellissoto@berkeley.edu](mailto:diego.ellissoto@berkeley.edu)
|
| 192 |
|
| 193 |
---
|
| 194 |
|
| 195 |
-
##
|
| 196 |
-
|
| 197 |
-
This tool is a **decision-support prototype** co-developed with the RSF Data Working Group. The BAI should be treated as an exploratory indicator; variable weights and reference distributions are subject to revision through stakeholder co-development.
|
| 198 |
|
| 199 |
-
**
|
| 200 |
-
-
|
| 201 |
-
-
|
| 202 |
-
- CDC Social Vulnerability Index
|
| 203 |
-
- NatureServe biodiversity / rarity maps
|
| 204 |
-
- Frequency-weighted multimodal transit accessibility
|
| 205 |
-
- Pre-cached transit isochrones for faster queries
|
| 206 |
|
| 207 |
---
|
| 208 |
|
| 209 |
-
<img src="www/hexbin_RSF_logo.png" width="
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
|
|
|
| 10 |
|
| 11 |
+
# SF Biodiversity Access Shiny App
|
| 12 |
|
| 13 |
+
This Shiny app provides decision support for the **Reimagining San Francisco Initiative**, aiming to explore the intersection of biodiversity, socio-economic variables, and greenspace accessibility.
|
| 14 |
|
| 15 |
+

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
---
|
| 18 |
|
| 19 |
+
## Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
- Users can either **click on the map** or **type an address** to generate isochrones for travel-time accessibility.
|
| 22 |
+
- Supports multiple transportation modes, including pedestrian, cycling, driving, and traffic-sensitive driving.
|
| 23 |
+
- Retrieves socio-economic data from **precomputed Census variables**.
|
| 24 |
+
- Calculates and overlays **NDVI** for vegetation analysis.
|
| 25 |
+
- Summarizes biodiversity records from **GBIF** and identifies species richness, greenspace, and socio-economic patterns.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+

|
| 28 |
|
| 29 |
---
|
| 30 |
|
| 31 |
+
## App Summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
This application allows users to:
|
| 34 |
|
| 35 |
+
- Generate travel-time isochrones across multiple transportation modes.
|
| 36 |
+
- Retrieve biodiversity and socio-economic data for a chosen area.
|
| 37 |
+
- Explore greenspace coverage, population estimates, and species diversity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
**Created by:**
|
| 40 |
+
Diego Ellis Soto. In collaboration with Carl Boettiger, Rebecca Johnson, Christopher J. Schell
|
| 41 |
+
Contact: diego.ellissoto@berkeley.edu
|
|
|
|
| 42 |
|
| 43 |
---
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
## Why Biodiversity Access Matters
|
| 47 |
|
| 48 |
+
Ensuring equitable access to biodiversity is essential for:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
- **Human well-being**: Promoting physical and mental health through exposure to nature.
|
| 51 |
+
- **Ecological resilience**: Supporting pollinators, moderating climate extremes, and enhancing ecosystem services.
|
| 52 |
+
- **Urban planning**: Incorporating biodiversity metrics into decision-making for sustainable urban futures.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
---
|
| 55 |
|
| 56 |
+
## Next Steps
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
1. Add impervious surface data, national walkability score, and social vulnerability index.
|
| 59 |
+
2. Integrate community organizations and NatureServe biodiversity maps.
|
| 60 |
+
3. Optimize speed by pre-storing variables and aggregating data.
|
| 61 |
+
4. Develop a comprehensive biodiversity access score in collaboration with stakeholders.
|
| 62 |
+
5. Annotate GBIF data with additional environmental variables for enhanced summaries.
|
| 63 |
|
| 64 |
+
## Public Transport Data
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
Future plans include integrating public transportation accessibility to further enhance decision-making capabilities.
|
|
|
|
|
|
|
| 67 |
|
| 68 |
---
|
| 69 |
|
| 70 |
+
## Repository Structure
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
- **App.R**: Main application file containing UI and server logic.
|
| 73 |
+
- **R/setup.R**: Script to load necessary datasets (e.g., annotated GBIF, NDVI).
|
| 74 |
+
- **www/**: Contains logos, screenshots, and other resources.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
---
|
| 77 |
|
| 78 |
+
<img src="www/hexbin_RSF_logo.png" width="100">
|
Rscripts/iso_metrics.R
DELETED
|
@@ -1,709 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Shared isochrone + metric functions (sourced once by app.R at startup)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# These four functions are the single source of truth for turning a point into
|
| 5 |
-
# an isochrone, scoring it, and drawing its BAI spider plot. Both the main
|
| 6 |
-
# "Isochrone Explorer" tab and the "Isochrone Comparer" tab call them, so the
|
| 7 |
-
# scoring logic lives in exactly one place.
|
| 8 |
-
#
|
| 9 |
-
# build_isochrones() point + modes/times -> isochrone sf (mode,time,geom)
|
| 10 |
-
# compute_iso_metrics() isochrone sf + point -> per-isochrone metric data.frame
|
| 11 |
-
# add_bai() metric df + benchmarks -> df + 7 BAI axes + composite BAI
|
| 12 |
-
# draw_radar() BAI df -> spider/radar plot (base graphics)
|
| 13 |
-
#
|
| 14 |
-
# These functions read the *static* objects loaded by Rscripts/setup_unified.R
|
| 15 |
-
# directly as globals (cbg_vect_sf, osm_greenspace, the distance/NDVI rasters,
|
| 16 |
-
# rsf_projects, cbg_greenspace_coverage, gtfs_stops_sf, gtfs_routes_sf,
|
| 17 |
-
# gtfs_router, transit_iso_cache, cenv_sf, sf_ej_sf) plus the helpers/config
|
| 18 |
-
# defined at the top of app.R (mapbox_token, mode_palette, pretty_mode, ecdf01,
|
| 19 |
-
# standardize_iso_sf, build_walk_transit_isochrone, safe_biodiv_hotspots/coldspots).
|
| 20 |
-
#
|
| 21 |
-
# Two objects are created per-session inside server() and so are passed in as
|
| 22 |
-
# explicit arguments rather than read as globals:
|
| 23 |
-
# gbif_tab -- the session's DuckDB handle on the GBIF parquet
|
| 24 |
-
# city_benchmarks -- the citywide ECDF reference distributions for the BAI
|
| 25 |
-
#
|
| 26 |
-
# compute_iso_metrics() reports progress via withProgress(), so it must be
|
| 27 |
-
# called from within a Shiny reactive context (which both tabs satisfy).
|
| 28 |
-
# ============================================================================
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# ----------------------------------------------------------------------------
|
| 32 |
-
# build_isochrones(): point + chosen modes/times -> combined isochrone sf
|
| 33 |
-
# ----------------------------------------------------------------------------
|
| 34 |
-
# point: named numeric c(lon = , lat = ) (the shape chosen_point() stores), or NULL.
|
| 35 |
-
# modes: character vector from c("driving","walking","cycling","driving-traffic",
|
| 36 |
-
# "transit","walk_transit"). times: numeric minutes. The transit_* / walk_*
|
| 37 |
-
# arguments only matter when a transit mode is selected; they default to the
|
| 38 |
-
# main tab's defaults so the comparer can pass a single mode/time and ignore them.
|
| 39 |
-
# Returns an sf with columns mode, time, geometry (EPSG:4326), or NULL if nothing
|
| 40 |
-
# could be built.
|
| 41 |
-
build_isochrones <- function(point, modes, times,
|
| 42 |
-
transit_hour = 9,
|
| 43 |
-
walk_to_stop_min = 5,
|
| 44 |
-
walk_from_stop_min = 5,
|
| 45 |
-
transit_departure_window_min = 10) {
|
| 46 |
-
if (is.null(point) || length(modes) == 0 || length(times) == 0) return(NULL)
|
| 47 |
-
|
| 48 |
-
location_sf <- st_as_sf(
|
| 49 |
-
data.frame(lon = point["lon"], lat = point["lat"]),
|
| 50 |
-
coords = c("lon", "lat"),
|
| 51 |
-
crs = 4326
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
iso_list <- list()
|
| 55 |
-
times <- as.numeric(times)
|
| 56 |
-
|
| 57 |
-
# --- Mapbox modes (driving / walking / cycling / driving-traffic) ----------
|
| 58 |
-
mapbox_modes <- intersect(modes, c("driving", "walking", "cycling", "driving-traffic"))
|
| 59 |
-
for (mode in mapbox_modes) {
|
| 60 |
-
for (t in times) {
|
| 61 |
-
iso <- tryCatch(
|
| 62 |
-
mb_isochrone(location_sf, time = t, profile = mode, access_token = mapbox_token),
|
| 63 |
-
# Surface the Mapbox error to the log instead of silently dropping it --
|
| 64 |
-
# otherwise a failed call just looks like "no isochrone" with no clue why.
|
| 65 |
-
error = function(e) { warning("mb_isochrone failed (", mode, " ", t, " min): ", conditionMessage(e)); NULL }
|
| 66 |
-
)
|
| 67 |
-
if (!is.null(iso)) {
|
| 68 |
-
iso_std <- standardize_iso_sf(iso, mode_name = mode, time_min = t)
|
| 69 |
-
if (!is.null(iso_std)) iso_list <- append(iso_list, list(iso_std))
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
# --- Transit (GTFS) --------------------------------------------------------
|
| 75 |
-
if ("transit" %in% modes && !is.null(gtfs_router) && !is.null(gtfs_stops_sf)) {
|
| 76 |
-
stop_dists <- st_distance(location_sf, gtfs_stops_sf)
|
| 77 |
-
nearest_idx <- which.min(stop_dists)
|
| 78 |
-
nearest_id <- as.character(gtfs_stops_sf$stop_id[nearest_idx])
|
| 79 |
-
dep_secs <- as.numeric(transit_hour) * 3600
|
| 80 |
-
|
| 81 |
-
for (t in times) {
|
| 82 |
-
iso_poly <- NULL
|
| 83 |
-
|
| 84 |
-
if (!is.null(transit_iso_cache) &&
|
| 85 |
-
!is.null(transit_iso_cache[[nearest_id]]) &&
|
| 86 |
-
!is.null(transit_iso_cache[[nearest_id]][[as.character(t)]])) {
|
| 87 |
-
iso_poly <- transit_iso_cache[[nearest_id]][[as.character(t)]]
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
if (is.null(iso_poly)) {
|
| 91 |
-
iso_result <- tryCatch(
|
| 92 |
-
gtfsrouter::gtfs_isochrone(
|
| 93 |
-
gtfs = gtfs_router,
|
| 94 |
-
from = nearest_id,
|
| 95 |
-
start_time = dep_secs,
|
| 96 |
-
end_time = dep_secs + t * 60,
|
| 97 |
-
from_is_id = TRUE
|
| 98 |
-
),
|
| 99 |
-
error = function(e) NULL
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
if (!is.null(iso_result) && nrow(iso_result) > 2) {
|
| 103 |
-
reachable_sf <- gtfs_stops_sf |>
|
| 104 |
-
filter(stop_id %in% as.character(iso_result$stop_id))
|
| 105 |
-
|
| 106 |
-
if (nrow(reachable_sf) > 2) {
|
| 107 |
-
iso_poly <- st_convex_hull(st_union(reachable_sf))
|
| 108 |
-
} else if (nrow(reachable_sf) > 0) {
|
| 109 |
-
iso_poly <- st_union(st_buffer(st_transform(reachable_sf, 3857), 100)) |>
|
| 110 |
-
st_transform(4326)
|
| 111 |
-
}
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
if (!is.null(iso_poly)) {
|
| 116 |
-
iso_sf <- st_sf(
|
| 117 |
-
mode = "transit",
|
| 118 |
-
time = as.numeric(t),
|
| 119 |
-
geometry = st_geometry(st_as_sf(iso_poly)),
|
| 120 |
-
crs = 4326
|
| 121 |
-
)
|
| 122 |
-
iso_sf <- standardize_iso_sf(iso_sf, mode_name = "transit", time_min = t)
|
| 123 |
-
iso_list <- append(iso_list, list(iso_sf))
|
| 124 |
-
}
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
# --- Walk + Transit (Muni) -------------------------------------------------
|
| 129 |
-
if ("walk_transit" %in% modes && !is.null(gtfs_router) && !is.null(gtfs_stops_sf)) {
|
| 130 |
-
dep_secs <- as.numeric(transit_hour) * 3600
|
| 131 |
-
valid_times <- times[times > walk_to_stop_min]
|
| 132 |
-
|
| 133 |
-
for (t in valid_times) {
|
| 134 |
-
wt_iso <- tryCatch(
|
| 135 |
-
build_walk_transit_isochrone(
|
| 136 |
-
location_sf = location_sf,
|
| 137 |
-
total_time_min = t,
|
| 138 |
-
dep_secs = dep_secs,
|
| 139 |
-
walk_to_stop_min = walk_to_stop_min,
|
| 140 |
-
walk_from_stop_min = walk_from_stop_min,
|
| 141 |
-
gtfs_stops_sf = gtfs_stops_sf,
|
| 142 |
-
gtfs_router = gtfs_router,
|
| 143 |
-
mapbox_token = mapbox_token,
|
| 144 |
-
departure_window_min = transit_departure_window_min,
|
| 145 |
-
departure_step_min = 5,
|
| 146 |
-
max_last_mile_stops = 12,
|
| 147 |
-
include_first_mile_polygon = TRUE
|
| 148 |
-
),
|
| 149 |
-
error = function(e) NULL
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
if (!is.null(wt_iso) && nrow(wt_iso) > 0) {
|
| 153 |
-
iso_list <- append(iso_list, list(wt_iso))
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
if (length(iso_list) == 0) return(NULL)
|
| 159 |
-
|
| 160 |
-
dplyr::bind_rows(iso_list) |>
|
| 161 |
-
st_as_sf() |>
|
| 162 |
-
st_make_valid() |>
|
| 163 |
-
st_transform(4326)
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
# ----------------------------------------------------------------------------
|
| 168 |
-
# compute_iso_metrics(): isochrone sf -> per-isochrone metric data.frame
|
| 169 |
-
# ----------------------------------------------------------------------------
|
| 170 |
-
# iso_data: the sf returned by build_isochrones(). point: the c(lon, lat) the
|
| 171 |
-
# isochrones were built from (used for the nearest-greenspace / nearest-RSF
|
| 172 |
-
# lookups), or NULL. gbif_tab: the session's DuckDB handle on the GBIF parquet.
|
| 173 |
-
# Returns a data.frame (one row per isochrone) carrying aggregate summaries as
|
| 174 |
-
# attributes: bio_percentile, city/mean transit scores, closest_greenspace(+dist),
|
| 175 |
-
# closest_rsf_program(+dist).
|
| 176 |
-
compute_iso_metrics <- function(iso_data, point, gbif_tab) {
|
| 177 |
-
if (is.null(iso_data) || nrow(iso_data) == 0) return(data.frame())
|
| 178 |
-
|
| 179 |
-
hotspot_union <- safe_biodiv_hotspots()
|
| 180 |
-
coldspot_union <- safe_biodiv_coldspots()
|
| 181 |
-
|
| 182 |
-
if (!is.null(hotspot_union)) hotspot_union <- st_union(hotspot_union)
|
| 183 |
-
if (!is.null(coldspot_union)) coldspot_union <- st_union(coldspot_union)
|
| 184 |
-
|
| 185 |
-
acs_wide <- cbg_vect_sf |>
|
| 186 |
-
mutate(population = popE, med_income = medincE)
|
| 187 |
-
|
| 188 |
-
results <- data.frame()
|
| 189 |
-
n_isos <- nrow(iso_data)
|
| 190 |
-
|
| 191 |
-
user_point_sf <- NULL
|
| 192 |
-
if (!is.null(point)) {
|
| 193 |
-
user_point_sf <- st_as_sf(
|
| 194 |
-
data.frame(lon = point["lon"], lat = point["lat"]),
|
| 195 |
-
coords = c("lon", "lat"),
|
| 196 |
-
crs = 4326
|
| 197 |
-
)
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
min_dist_val_global <- NA_real_
|
| 201 |
-
osm_greenspace_name_global <- NA_character_
|
| 202 |
-
if (!is.null(user_point_sf) && exists("greenspace_dist_raster") && exists("greenspace_osmid_raster")) {
|
| 203 |
-
try({
|
| 204 |
-
# Distance from the selected point to its nearest greenspace, read straight
|
| 205 |
-
# from the distance raster. terra::extract() returns data.frame(ID, value),
|
| 206 |
-
# so the distance is column 2 -- pulling column 1 (the point ID) was the bug
|
| 207 |
-
# that made this come back as "1" instead of the real distance in metres.
|
| 208 |
-
min_dist_val_global <- (greenspace_dist_raster |> extract(vect(user_point_sf)) |> pull(2))[1]
|
| 209 |
-
user_point_osm_id <- (greenspace_osmid_raster |> extract(vect(user_point_sf)) |> pull(2))[1]
|
| 210 |
-
osm_greenspace_name_global <- osm_greenspace |>
|
| 211 |
-
mutate(osm_id = as.numeric(osm_id)) |>
|
| 212 |
-
filter(osm_id == user_point_osm_id) |>
|
| 213 |
-
pull(name)
|
| 214 |
-
if (length(osm_greenspace_name_global) == 0 || is.na(osm_greenspace_name_global[1])) {
|
| 215 |
-
osm_greenspace_name_global <- "Unnamed Greenspace"
|
| 216 |
-
} else {
|
| 217 |
-
osm_greenspace_name_global <- osm_greenspace_name_global[1]
|
| 218 |
-
}
|
| 219 |
-
}, silent = TRUE)
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
min_rsf_dist_global <- NA_real_
|
| 223 |
-
rsf_program_name_global <- NA_character_
|
| 224 |
-
if (!is.null(user_point_sf) && exists("rsfprogram_dist_raster") && exists("rsfprogram_id_raster") && exists("rsf_projects")) {
|
| 225 |
-
try({
|
| 226 |
-
# Distance is column 2 (column 1 is the point ID) -- same fix as greenspace above.
|
| 227 |
-
min_rsf_dist_global <- (rsfprogram_dist_raster |> extract(vect(user_point_sf)) |> pull(2))[1]
|
| 228 |
-
user_point_rsf_pid <- (rsfprogram_id_raster |> extract(vect(user_point_sf)) |> pull(2))[1]
|
| 229 |
-
rsf_program_name_global <- rsf_projects |>
|
| 230 |
-
dplyr::filter(as.numeric(.data$polygon_id) == as.numeric(user_point_rsf_pid)) |>
|
| 231 |
-
dplyr::pull(.data$prj_name)
|
| 232 |
-
if (length(rsf_program_name_global) == 0 || is.na(rsf_program_name_global[1])) {
|
| 233 |
-
rsf_program_name_global <- "Unknown RSF program"
|
| 234 |
-
} else {
|
| 235 |
-
rsf_program_name_global <- rsf_program_name_global[1]
|
| 236 |
-
}
|
| 237 |
-
}, silent = TRUE)
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
withProgress(message = "Analyzing isochrones...", value = 0, {
|
| 241 |
-
|
| 242 |
-
for (i in seq_len(n_isos)) {
|
| 243 |
-
poly_i <- iso_data[i, ]
|
| 244 |
-
vect_poly_i <- vect(poly_i)
|
| 245 |
-
|
| 246 |
-
incProgress(
|
| 247 |
-
1 / n_isos,
|
| 248 |
-
detail = paste0(
|
| 249 |
-
pretty_mode(as.character(poly_i$mode[[1]])),
|
| 250 |
-
" – ", poly_i$time[[1]], " min"
|
| 251 |
-
)
|
| 252 |
-
)
|
| 253 |
-
|
| 254 |
-
dist_hot_km <- if (!is.null(hotspot_union)) {
|
| 255 |
-
round(as.numeric(min(st_distance(poly_i, hotspot_union))) / 1000, 3)
|
| 256 |
-
} else NA_real_
|
| 257 |
-
|
| 258 |
-
dist_cold_km <- if (!is.null(coldspot_union)) {
|
| 259 |
-
round(as.numeric(min(st_distance(poly_i, coldspot_union))) / 1000, 3)
|
| 260 |
-
} else NA_real_
|
| 261 |
-
|
| 262 |
-
inter_acs <- tryCatch(intersect(vect(acs_wide), vect_poly_i) |> st_as_sf(), error = function(e) NULL)
|
| 263 |
-
|
| 264 |
-
pop_total <- 0
|
| 265 |
-
w_income <- NA_real_
|
| 266 |
-
if (!is.null(inter_acs) && nrow(inter_acs) > 0) {
|
| 267 |
-
inter_acs <- inter_acs |>
|
| 268 |
-
mutate(
|
| 269 |
-
area_num = as.numeric(st_area(st_transform(geometry, 3857))),
|
| 270 |
-
weighted_pop = population * (area_num / sum(area_num, na.rm = TRUE))
|
| 271 |
-
)
|
| 272 |
-
pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
|
| 273 |
-
w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
|
| 274 |
-
sum(inter_acs$area_num, na.rm = TRUE)
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
iso_area_m2 <- as.numeric(st_area(st_transform(poly_i, 3857)))
|
| 278 |
-
iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
|
| 279 |
-
|
| 280 |
-
# Greenspace area within this isochrone: use pre-cached per-CBG coverage
|
| 281 |
-
# (replaces slow per-isochrone raster cellSize() which took ~40-55 s each).
|
| 282 |
-
# Method: intersect isochrone with CBGs, scale each CBG's greenspace_m2 by
|
| 283 |
-
# the fraction of that CBG falling inside the isochrone, then sum.
|
| 284 |
-
gs_area_m2 <- tryCatch({
|
| 285 |
-
if (exists("cbg_greenspace_coverage") && !is.null(cbg_greenspace_coverage)) {
|
| 286 |
-
poly_proj <- st_transform(poly_i, 3857)
|
| 287 |
-
cbg_proj_i <- st_transform(cbg_vect_sf[, "GEOID"], 3857) |>
|
| 288 |
-
mutate(cbg_area_m2 = as.numeric(st_area(geometry))) |>
|
| 289 |
-
st_make_valid()
|
| 290 |
-
inter_df <- st_intersection(cbg_proj_i, poly_proj) |>
|
| 291 |
-
mutate(inter_area_m2 = as.numeric(st_area(geometry))) |>
|
| 292 |
-
st_drop_geometry() |>
|
| 293 |
-
left_join(
|
| 294 |
-
cbg_greenspace_coverage[, c("GEOID", "greenspace_m2", "cbg_area_m2")],
|
| 295 |
-
by = "GEOID", suffix = c("_iso", "_cbg")
|
| 296 |
-
) |>
|
| 297 |
-
mutate(
|
| 298 |
-
greenspace_m2 = tidyr::replace_na(greenspace_m2, 0),
|
| 299 |
-
cbg_area_m2 = dplyr::coalesce(cbg_area_m2_cbg, cbg_area_m2_iso),
|
| 300 |
-
contrib = ifelse(cbg_area_m2 > 0,
|
| 301 |
-
inter_area_m2 / cbg_area_m2 * greenspace_m2,
|
| 302 |
-
0)
|
| 303 |
-
)
|
| 304 |
-
sum(inter_df$contrib, na.rm = TRUE)
|
| 305 |
-
} else if (exists("greenspace_dist_raster")) {
|
| 306 |
-
# Fallback to raster method if cache is unavailable
|
| 307 |
-
dist_crop <- terra::crop(greenspace_dist_raster, vect_poly_i)
|
| 308 |
-
dist_mask <- terra::mask(dist_crop, vect_poly_i)
|
| 309 |
-
is_greenspace <- dist_mask == 0
|
| 310 |
-
cell_areas <- terra::cellSize(is_greenspace, unit = "m")
|
| 311 |
-
as.numeric(terra::global(cell_areas * is_greenspace, "sum", na.rm = TRUE)[1, 1])
|
| 312 |
-
} else {
|
| 313 |
-
0
|
| 314 |
-
}
|
| 315 |
-
}, error = function(e) 0)
|
| 316 |
-
gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
|
| 317 |
-
|
| 318 |
-
mean_ndvi <- NA_real_
|
| 319 |
-
if (exists("ndvi")) {
|
| 320 |
-
ndvi_vals <- tryCatch(values(terra::mask(terra::crop(ndvi, vect_poly_i), vect_poly_i)), error = function(e) NA)
|
| 321 |
-
ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
|
| 322 |
-
mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm = TRUE), 3), NA_real_)
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
iso_wkt <- st_as_text(st_geometry(poly_i)[[1]])
|
| 326 |
-
gbif_summary <- tryCatch({
|
| 327 |
-
gbif_tab |>
|
| 328 |
-
filter(sql(glue("ST_Intersects(ST_GeomFromText(geom_wkt), ST_GeomFromText('{iso_wkt}'))"))) |>
|
| 329 |
-
summarise(
|
| 330 |
-
n_records = n(),
|
| 331 |
-
n_species = n_distinct(species),
|
| 332 |
-
n_birds = n_distinct(case_when(class == "Aves" ~ species, TRUE ~ NA_character_)),
|
| 333 |
-
n_mammals = n_distinct(case_when(class == "Mammalia" ~ species, TRUE ~ NA_character_)),
|
| 334 |
-
n_plants = n_distinct(case_when(
|
| 335 |
-
class %in% c("Magnoliopsida", "Liliopsida", "Pinopsida", "Polypodiopsida",
|
| 336 |
-
"Equisetopsida", "Bryopsida", "Marchantiopsida") ~ species,
|
| 337 |
-
TRUE ~ NA_character_
|
| 338 |
-
))
|
| 339 |
-
) |>
|
| 340 |
-
collect()
|
| 341 |
-
}, error = function(e) NULL)
|
| 342 |
-
n_records <- if (!is.null(gbif_summary) && nrow(gbif_summary) > 0) gbif_summary$n_records[[1]] else 0L
|
| 343 |
-
n_species <- if (!is.null(gbif_summary) && nrow(gbif_summary) > 0) gbif_summary$n_species[[1]] else 0L
|
| 344 |
-
n_birds <- if (!is.null(gbif_summary) && nrow(gbif_summary) > 0) gbif_summary$n_birds[[1]] else 0L
|
| 345 |
-
n_mammals <- if (!is.null(gbif_summary) && nrow(gbif_summary) > 0) gbif_summary$n_mammals[[1]] else 0L
|
| 346 |
-
n_plants <- if (!is.null(gbif_summary) && nrow(gbif_summary) > 0) gbif_summary$n_plants[[1]] else 0L
|
| 347 |
-
|
| 348 |
-
n_transit_stops <- NA_real_
|
| 349 |
-
transit_access_score <- NA_real_
|
| 350 |
-
freq_weighted_score <- NA_real_
|
| 351 |
-
mean_headway_iso <- NA_real_
|
| 352 |
-
nearest_stop_m <- NA_real_
|
| 353 |
-
nearest_stop_name <- NA_character_
|
| 354 |
-
|
| 355 |
-
if (!is.null(gtfs_stops_sf)) {
|
| 356 |
-
inter_transit <- tryCatch(st_intersection(gtfs_stops_sf, poly_i), error = function(e) NULL)
|
| 357 |
-
n_transit_stops <- if (!is.null(inter_transit)) nrow(inter_transit) else 0
|
| 358 |
-
|
| 359 |
-
dist_transit <- st_distance(poly_i, gtfs_stops_sf)
|
| 360 |
-
nearest_stop_m <- round(as.numeric(min(dist_transit)), 0)
|
| 361 |
-
nearest_stop_name <- gtfs_stops_sf$stop_name[which.min(dist_transit)]
|
| 362 |
-
|
| 363 |
-
transit_access_score <- ifelse(iso_area_km2 > 0, round(n_transit_stops / iso_area_km2, 2), NA_real_)
|
| 364 |
-
|
| 365 |
-
freq_weighted_score <- if (!is.null(inter_transit) &&
|
| 366 |
-
"mean_headway_min" %in% names(inter_transit) &&
|
| 367 |
-
nrow(inter_transit) > 0 &&
|
| 368 |
-
iso_area_km2 > 0) {
|
| 369 |
-
hw <- inter_transit$mean_headway_min
|
| 370 |
-
hw <- hw[!is.na(hw) & hw > 0]
|
| 371 |
-
if (length(hw) > 0) round(sum(60 / hw) / iso_area_km2, 2) else NA_real_
|
| 372 |
-
} else NA_real_
|
| 373 |
-
|
| 374 |
-
mean_headway_iso <- if (!is.null(inter_transit) &&
|
| 375 |
-
"mean_headway_min" %in% names(inter_transit) &&
|
| 376 |
-
nrow(inter_transit) > 0) {
|
| 377 |
-
round(mean(inter_transit$mean_headway_min, na.rm = TRUE), 1)
|
| 378 |
-
} else NA_real_
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
# Unique Muni route IDs whose shapes intersect this isochrone
|
| 382 |
-
n_unique_routes <- 0L
|
| 383 |
-
if (!is.null(gtfs_routes_sf)) {
|
| 384 |
-
routes_inter <- tryCatch(
|
| 385 |
-
st_intersection(gtfs_routes_sf[, "route_id"], poly_i),
|
| 386 |
-
error = function(e) NULL
|
| 387 |
-
)
|
| 388 |
-
n_unique_routes <- if (!is.null(routes_inter) && "route_id" %in% names(routes_inter)) {
|
| 389 |
-
length(unique(routes_inter$route_id))
|
| 390 |
-
} else 0L
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
sampling_density_km2 <- ifelse(iso_area_km2 > 0, round(n_records / iso_area_km2, 2), NA_real_)
|
| 394 |
-
|
| 395 |
-
mean_ciscore <- if (!is.null(cenv_sf)) {
|
| 396 |
-
tryCatch({
|
| 397 |
-
ce_inter <- st_intersection(cenv_sf, poly_i)
|
| 398 |
-
if (nrow(ce_inter) > 0) {
|
| 399 |
-
ce_inter$a <- as.numeric(st_area(st_transform(ce_inter, 3857)))
|
| 400 |
-
round(weighted.mean(ce_inter$CIscore, w = ce_inter$a, na.rm = TRUE), 1)
|
| 401 |
-
} else NA_real_
|
| 402 |
-
}, error = function(e) NA_real_)
|
| 403 |
-
} else NA_real_
|
| 404 |
-
|
| 405 |
-
mean_traffic_pctl <- if (!is.null(cenv_sf)) {
|
| 406 |
-
tryCatch({
|
| 407 |
-
ce_inter <- st_intersection(cenv_sf, poly_i)
|
| 408 |
-
if (nrow(ce_inter) > 0) {
|
| 409 |
-
ce_inter$a <- as.numeric(st_area(st_transform(ce_inter, 3857)))
|
| 410 |
-
round(weighted.mean(ce_inter$Traffic_Pctl, w = ce_inter$a, na.rm = TRUE), 1)
|
| 411 |
-
} else NA_real_
|
| 412 |
-
}, error = function(e) NA_real_)
|
| 413 |
-
} else NA_real_
|
| 414 |
-
|
| 415 |
-
mean_ej_score <- if (!is.null(sf_ej_sf)) {
|
| 416 |
-
tryCatch({
|
| 417 |
-
ej_inter <- st_intersection(sf_ej_sf, poly_i)
|
| 418 |
-
if (nrow(ej_inter) > 0) {
|
| 419 |
-
ej_inter$a <- as.numeric(st_area(st_transform(ej_inter, 3857)))
|
| 420 |
-
valid <- ej_inter[!is.na(ej_inter$score), ]
|
| 421 |
-
if (nrow(valid) > 0) {
|
| 422 |
-
round(weighted.mean(valid$score, w = valid$a, na.rm = TRUE), 1)
|
| 423 |
-
} else NA_real_
|
| 424 |
-
} else NA_real_
|
| 425 |
-
}, error = function(e) NA_real_)
|
| 426 |
-
} else NA_real_
|
| 427 |
-
|
| 428 |
-
row_i <- data.frame(
|
| 429 |
-
Mode = pretty_mode(as.character(poly_i$mode[[1]])),
|
| 430 |
-
Time = as.numeric(poly_i$time[[1]]),
|
| 431 |
-
IsochroneArea_km2 = iso_area_km2,
|
| 432 |
-
DistToHotspot_km = dist_hot_km,
|
| 433 |
-
DistToColdspot_km = dist_cold_km,
|
| 434 |
-
EstimatedPopulation = pop_total,
|
| 435 |
-
MedianIncome = round(w_income, 2),
|
| 436 |
-
MeanNDVI = mean_ndvi,
|
| 437 |
-
GBIF_Records = n_records,
|
| 438 |
-
GBIF_Species = n_species,
|
| 439 |
-
Bird_Species = n_birds,
|
| 440 |
-
Mammal_Species = n_mammals,
|
| 441 |
-
Plant_Species = n_plants,
|
| 442 |
-
SamplingDensity_km2 = sampling_density_km2,
|
| 443 |
-
Greenspace_percent = round(gs_percent, 2),
|
| 444 |
-
Transit_Stops = n_transit_stops,
|
| 445 |
-
Unique_Muni_Routes = n_unique_routes,
|
| 446 |
-
Transit_Access_Score = transit_access_score,
|
| 447 |
-
Freq_Weighted_Score = freq_weighted_score,
|
| 448 |
-
Mean_Headway_min = mean_headway_iso,
|
| 449 |
-
Nearest_Stop_m = nearest_stop_m,
|
| 450 |
-
Nearest_Stop_Name = nearest_stop_name,
|
| 451 |
-
CalEnviro_CIscore = mean_ciscore,
|
| 452 |
-
CalEnviro_Traffic_Pctl = mean_traffic_pctl,
|
| 453 |
-
SF_EJ_Score = mean_ej_score,
|
| 454 |
-
closest_greenspace = osm_greenspace_name_global,
|
| 455 |
-
closest_greenspace_dist_m = min_dist_val_global,
|
| 456 |
-
closest_rsf_program = rsf_program_name_global,
|
| 457 |
-
closest_rsf_program_dist_m = min_rsf_dist_global,
|
| 458 |
-
stringsAsFactors = FALSE
|
| 459 |
-
)
|
| 460 |
-
|
| 461 |
-
results <- rbind(results, row_i)
|
| 462 |
-
}
|
| 463 |
-
|
| 464 |
-
}) # end withProgress
|
| 465 |
-
|
| 466 |
-
union_wkt <- st_as_text(st_geometry(st_union(iso_data))[[1]])
|
| 467 |
-
union_n_species <- tryCatch({
|
| 468 |
-
gbif_tab |>
|
| 469 |
-
filter(sql(glue("ST_Intersects(ST_GeomFromText(geom_wkt), ST_GeomFromText('{union_wkt}'))"))) |>
|
| 470 |
-
summarise(n_species = n_distinct(species)) |>
|
| 471 |
-
collect() |>
|
| 472 |
-
pull(n_species)
|
| 473 |
-
}, error = function(e) 0L)
|
| 474 |
-
|
| 475 |
-
attr(results, "bio_percentile") <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
|
| 476 |
-
|
| 477 |
-
if (!is.null(gtfs_stops_sf)) {
|
| 478 |
-
sf_city_area_km2 <- 121.4
|
| 479 |
-
attr(results, "city_transit_score") <- round(nrow(gtfs_stops_sf) / sf_city_area_km2, 2)
|
| 480 |
-
attr(results, "mean_transit_score") <- round(mean(results$Transit_Access_Score, na.rm = TRUE), 2)
|
| 481 |
-
attr(results, "mean_transit_stops") <- round(mean(results$Transit_Stops, na.rm = TRUE), 1)
|
| 482 |
-
attr(results, "mean_muni_routes") <- round(mean(results$Unique_Muni_Routes, na.rm = TRUE), 1)
|
| 483 |
-
} else {
|
| 484 |
-
attr(results, "city_transit_score") <- NA_real_
|
| 485 |
-
attr(results, "mean_transit_score") <- NA_real_
|
| 486 |
-
attr(results, "mean_transit_stops") <- NA_real_
|
| 487 |
-
attr(results, "mean_muni_routes") <- NA_real_
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
if (nrow(results) > 0) {
|
| 491 |
-
closest_gs <- results |>
|
| 492 |
-
filter(!is.na(closest_greenspace_dist_m)) |>
|
| 493 |
-
slice_min(closest_greenspace_dist_m, n = 1)
|
| 494 |
-
if (nrow(closest_gs) > 0) {
|
| 495 |
-
attr(results, "closest_greenspace") <- closest_gs$closest_greenspace[1]
|
| 496 |
-
attr(results, "closest_greenspace_dist_m") <- closest_gs$closest_greenspace_dist_m[1]
|
| 497 |
-
} else {
|
| 498 |
-
attr(results, "closest_greenspace") <- "None"
|
| 499 |
-
attr(results, "closest_greenspace_dist_m") <- NA_real_
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
closest_rsf <- results |>
|
| 503 |
-
filter(!is.na(closest_rsf_program_dist_m)) |>
|
| 504 |
-
slice_min(closest_rsf_program_dist_m, n = 1)
|
| 505 |
-
if (nrow(closest_rsf) > 0) {
|
| 506 |
-
attr(results, "closest_rsf_program") <- closest_rsf$closest_rsf_program[1]
|
| 507 |
-
attr(results, "closest_rsf_program_dist_m") <- closest_rsf$closest_rsf_program_dist_m[1]
|
| 508 |
-
} else {
|
| 509 |
-
attr(results, "closest_rsf_program") <- "None"
|
| 510 |
-
attr(results, "closest_rsf_program_dist_m") <- NA_real_
|
| 511 |
-
}
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
results
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
# ----------------------------------------------------------------------------
|
| 519 |
-
# add_bai(): metric df + citywide benchmarks -> df + 7 BAI axes + composite BAI
|
| 520 |
-
# ----------------------------------------------------------------------------
|
| 521 |
-
# df: a data.frame from compute_iso_metrics(). city_benchmarks: the list of
|
| 522 |
-
# citywide reference distributions (the ECDFs each axis is scored against).
|
| 523 |
-
# Returns df with the seven *_std columns (each 0-1) plus BAI (their row mean),
|
| 524 |
-
# or NULL if df is empty.
|
| 525 |
-
add_bai <- function(df, city_benchmarks) {
|
| 526 |
-
if (is.null(df) || nrow(df) == 0) return(NULL)
|
| 527 |
-
|
| 528 |
-
ref <- city_benchmarks
|
| 529 |
-
|
| 530 |
-
eq_ref_inverted <- if (length(ref$ej) > 0) max(ref$ej, na.rm = TRUE) - ref$ej else numeric(0)
|
| 531 |
-
eq_obs_inverted <- ifelse(is.na(df$SF_EJ_Score), NA_real_, max(ref$ej, na.rm = TRUE) - df$SF_EJ_Score)
|
| 532 |
-
|
| 533 |
-
tmp <- df |>
|
| 534 |
-
mutate(
|
| 535 |
-
Mobility_Access_std = ecdf01(Transit_Access_Score, ref$transit_density),
|
| 536 |
-
Biodiversity_Potential_std = ecdf01(GBIF_Species, ref$biodiversity),
|
| 537 |
-
Observation_Intensity_std = ecdf01(SamplingDensity_km2, ref$sampling),
|
| 538 |
-
Environmental_Quality_std = ecdf01(MeanNDVI, ref$ndvi),
|
| 539 |
-
Greenspace_Cover_std = ecdf01(Greenspace_percent, ref$greenspace_cover),
|
| 540 |
-
Equity_Context_std = ecdf01(eq_obs_inverted, eq_ref_inverted),
|
| 541 |
-
Route_Access_std = ecdf01(Unique_Muni_Routes, ref$route_access)
|
| 542 |
-
)
|
| 543 |
-
|
| 544 |
-
tmp$BAI <- rowMeans(
|
| 545 |
-
tmp[, c(
|
| 546 |
-
"Mobility_Access_std",
|
| 547 |
-
"Biodiversity_Potential_std",
|
| 548 |
-
"Observation_Intensity_std",
|
| 549 |
-
"Environmental_Quality_std",
|
| 550 |
-
"Greenspace_Cover_std",
|
| 551 |
-
"Equity_Context_std",
|
| 552 |
-
"Route_Access_std"
|
| 553 |
-
)],
|
| 554 |
-
na.rm = TRUE
|
| 555 |
-
)
|
| 556 |
-
|
| 557 |
-
tmp
|
| 558 |
-
}
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
# Axis order shared by both radar functions, and the column each axis reads from.
|
| 562 |
-
# Keep these two vectors aligned -- the group labels below assume this clockwise
|
| 563 |
-
# order (1-2 Urban Access, 3-4 Biodiversity, 5-6 Environment, 7 EJ).
|
| 564 |
-
RADAR_AXIS_COLS <- c("Mobility_Access_std", "Route_Access_std", "Biodiversity_Potential_std",
|
| 565 |
-
"Observation_Intensity_std", "Environmental_Quality_std",
|
| 566 |
-
"Greenspace_Cover_std", "Equity_Context_std")
|
| 567 |
-
RADAR_AXIS_LABELS <- c("Stop\nDensity", "Route\nDiversity", "Species\nRichness",
|
| 568 |
-
"Obs.\nIntensity", "Vegetation\n(NDVI)", "Greenspace\nCover", "EJ\nContext")
|
| 569 |
-
|
| 570 |
-
# Radial group subheaders placed just beyond the axis labels. Shared by both
|
| 571 |
-
# radar functions so the grouping (Urban Access / Biodiversity / Environment /
|
| 572 |
-
# EJ) is drawn identically. Call after fmsb::radarchart() has drawn the chart.
|
| 573 |
-
draw_radar_group_labels <- function() {
|
| 574 |
-
n_ax <- 7
|
| 575 |
-
angs <- pi/2 - (0:(n_ax - 1)) * (2 * pi / n_ax)
|
| 576 |
-
cat_r <- 1.48 # radius just outside axis labels (~1.2-1.3)
|
| 577 |
-
|
| 578 |
-
text(cat_r * cos(mean(angs[1:2])), cat_r * sin(mean(angs[1:2])),
|
| 579 |
-
"Urban Access", cex = 0.72, col = "#2166ac", font = 2, xpd = TRUE)
|
| 580 |
-
text(cat_r * cos(mean(angs[3:4])), cat_r * sin(mean(angs[3:4])),
|
| 581 |
-
"Biodiversity", cex = 0.72, col = "#1b7837", font = 2, xpd = TRUE)
|
| 582 |
-
text(cat_r * cos(mean(angs[5:6])), cat_r * sin(mean(angs[5:6])),
|
| 583 |
-
"Environment", cex = 0.72, col = "#762a83", font = 2, xpd = TRUE)
|
| 584 |
-
text(cat_r * cos(angs[7]), cat_r * sin(angs[7]),
|
| 585 |
-
"Environmental\nJustice", cex = 0.72, col = "#b2182b", font = 2, xpd = TRUE)
|
| 586 |
-
}
|
| 587 |
-
|
| 588 |
-
# ----------------------------------------------------------------------------
|
| 589 |
-
# draw_radar(): BAI df -> spider/radar plot (base graphics, via fmsb)
|
| 590 |
-
# ----------------------------------------------------------------------------
|
| 591 |
-
# bai_df: the data.frame returned by add_bai() (one row per isochrone). title:
|
| 592 |
-
# the chart title. Draws directly to the current graphics device, so call it
|
| 593 |
-
# from inside a renderPlot({ }). Lines are coloured by transport mode.
|
| 594 |
-
draw_radar <- function(bai_df, title = "Biodiversity Access Index Profile") {
|
| 595 |
-
if (is.null(bai_df) || nrow(bai_df) == 0) return(NULL)
|
| 596 |
-
|
| 597 |
-
radar_df <- bai_df |>
|
| 598 |
-
mutate(
|
| 599 |
-
ModeTime = paste0(Mode, "_", Time, "m"),
|
| 600 |
-
`Stop\nDensity` = Mobility_Access_std,
|
| 601 |
-
`Route\nDiversity` = Route_Access_std,
|
| 602 |
-
`Species\nRichness` = Biodiversity_Potential_std,
|
| 603 |
-
`Obs.\nIntensity` = Observation_Intensity_std,
|
| 604 |
-
`Vegetation\n(NDVI)`= Environmental_Quality_std,
|
| 605 |
-
`Greenspace\nCover` = Greenspace_Cover_std,
|
| 606 |
-
`EJ\nContext` = Equity_Context_std
|
| 607 |
-
) |>
|
| 608 |
-
select(
|
| 609 |
-
ModeTime,
|
| 610 |
-
`Stop\nDensity`,
|
| 611 |
-
`Route\nDiversity`,
|
| 612 |
-
`Species\nRichness`,
|
| 613 |
-
`Obs.\nIntensity`,
|
| 614 |
-
`Vegetation\n(NDVI)`,
|
| 615 |
-
`Greenspace\nCover`,
|
| 616 |
-
`EJ\nContext`
|
| 617 |
-
)
|
| 618 |
-
|
| 619 |
-
radar_mat <- as.data.frame(radar_df[, -1])
|
| 620 |
-
rownames(radar_mat) <- radar_df$ModeTime
|
| 621 |
-
|
| 622 |
-
radar_mat <- rbind(
|
| 623 |
-
rep(1, ncol(radar_mat)),
|
| 624 |
-
rep(0, ncol(radar_mat)),
|
| 625 |
-
radar_mat
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
-
labels <- rownames(radar_mat)[-(1:2)]
|
| 629 |
-
line_cols <- mode_palette[gsub("_(.*)$", "", labels)]
|
| 630 |
-
line_cols[is.na(line_cols)] <- "#666666"
|
| 631 |
-
|
| 632 |
-
# Extra margin so two-line axis labels aren't clipped
|
| 633 |
-
par(mar = c(2, 2, 3, 2))
|
| 634 |
-
|
| 635 |
-
fmsb::radarchart(
|
| 636 |
-
radar_mat,
|
| 637 |
-
axistype = 1,
|
| 638 |
-
pcol = line_cols,
|
| 639 |
-
plwd = 2,
|
| 640 |
-
plty = 1,
|
| 641 |
-
cglcol = "grey80",
|
| 642 |
-
cglty = 1,
|
| 643 |
-
cglwd = 0.8,
|
| 644 |
-
axislabcol = "grey40",
|
| 645 |
-
vlcex = 0.88,
|
| 646 |
-
title = title
|
| 647 |
-
)
|
| 648 |
-
|
| 649 |
-
draw_radar_group_labels()
|
| 650 |
-
|
| 651 |
-
legend(
|
| 652 |
-
"topright",
|
| 653 |
-
legend = labels,
|
| 654 |
-
col = line_cols,
|
| 655 |
-
lty = 1,
|
| 656 |
-
lwd = 2,
|
| 657 |
-
cex = 0.75,
|
| 658 |
-
bty = "n"
|
| 659 |
-
)
|
| 660 |
-
}
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
# ----------------------------------------------------------------------------
|
| 664 |
-
# draw_compare_radar(): overlay several BAI rows on ONE radar (Comparer tab)
|
| 665 |
-
# ----------------------------------------------------------------------------
|
| 666 |
-
# bai_rows: a list of single-row add_bai() data.frames (one per point).
|
| 667 |
-
# labels: legend text, one per row (e.g. "Point A: Walking 5 min").
|
| 668 |
-
# colors: one line colour per row -- this is where the A-vs-B colour scheme
|
| 669 |
-
# lives, so keep it in sync with the point markers / isochrones.
|
| 670 |
-
# Unlike draw_radar() (coloured by mode), this colours by *point* so two
|
| 671 |
-
# isochrones of the same mode are still distinguishable. Draws to the current
|
| 672 |
-
# device; call from inside renderPlot({ }).
|
| 673 |
-
draw_compare_radar <- function(bai_rows, labels, colors,
|
| 674 |
-
title = "Biodiversity Access Index — Point A vs Point B") {
|
| 675 |
-
if (length(bai_rows) == 0) return(NULL)
|
| 676 |
-
|
| 677 |
-
mat <- as.data.frame(do.call(rbind, lapply(bai_rows, function(d) as.numeric(d[1, RADAR_AXIS_COLS]))))
|
| 678 |
-
names(mat) <- RADAR_AXIS_LABELS
|
| 679 |
-
# fmsb wants the axis max (1) and min (0) as the first two rows.
|
| 680 |
-
mat <- rbind(rep(1, ncol(mat)), rep(0, ncol(mat)), mat)
|
| 681 |
-
|
| 682 |
-
par(mar = c(2, 2, 3, 2))
|
| 683 |
-
fmsb::radarchart(
|
| 684 |
-
mat,
|
| 685 |
-
axistype = 1,
|
| 686 |
-
pcol = colors,
|
| 687 |
-
pfcol = grDevices::adjustcolor(colors, alpha.f = 0.2), # translucent fills so overlaps read
|
| 688 |
-
plwd = 3,
|
| 689 |
-
plty = 1,
|
| 690 |
-
cglcol = "grey80",
|
| 691 |
-
cglty = 1,
|
| 692 |
-
cglwd = 0.8,
|
| 693 |
-
axislabcol = "grey40",
|
| 694 |
-
vlcex = 0.9,
|
| 695 |
-
title = title
|
| 696 |
-
)
|
| 697 |
-
|
| 698 |
-
draw_radar_group_labels()
|
| 699 |
-
|
| 700 |
-
legend(
|
| 701 |
-
"topright",
|
| 702 |
-
legend = labels,
|
| 703 |
-
col = colors,
|
| 704 |
-
lty = 1,
|
| 705 |
-
lwd = 3,
|
| 706 |
-
cex = 0.85,
|
| 707 |
-
bty = "n"
|
| 708 |
-
)
|
| 709 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/build_cbg_greenspace_coverage.R
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: Per-CBG greenspace coverage -> data/output
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Inputs: cbg_vect_sf + osm_greenspace (loaded via Rscripts/setup_unified.R,
|
| 5 |
-
# which pulls them from data/cached/ or HuggingFace)
|
| 6 |
-
# Output: data/output/cbg_greenspace_coverage.csv (GEOID, greenspace_m2, cbg_area_m2)
|
| 7 |
-
#
|
| 8 |
-
# The CBG x greenspace st_union + st_intersection is the most expensive single
|
| 9 |
-
# step at app startup. The inputs are static, so we precompute it here once and
|
| 10 |
-
# let setup_unified.R just read this small CSV instead of recomputing every run.
|
| 11 |
-
# Re-run when cbg_vect_sf or the greenspace polygons change, then upload
|
| 12 |
-
# data/output/cbg_greenspace_coverage.csv to HuggingFace (see upload_to_huggingface.R).
|
| 13 |
-
#
|
| 14 |
-
# Working directory must be the project root (folder containing data/ and Rscripts/).
|
| 15 |
-
# ============================================================================
|
| 16 |
-
|
| 17 |
-
# Reuse the app's canonical loader so coverage is computed from the exact same
|
| 18 |
-
# cbg_vect_sf + osm_greenspace the app uses (also gives us sf/dplyr, etc.).
|
| 19 |
-
source("Rscripts/setup_unified.R")
|
| 20 |
-
|
| 21 |
-
out_dir <- "data/output"
|
| 22 |
-
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
|
| 23 |
-
|
| 24 |
-
cbg_proj <- st_transform(cbg_vect_sf[, "GEOID"], 3857) |>
|
| 25 |
-
mutate(cbg_area_m2 = as.numeric(st_area(geometry)))
|
| 26 |
-
gs_proj <- st_transform(osm_greenspace, 3857) |> st_make_valid()
|
| 27 |
-
gs_union <- st_union(gs_proj)
|
| 28 |
-
cbg_gs_inter <- st_intersection(cbg_proj, gs_union)
|
| 29 |
-
|
| 30 |
-
cbg_greenspace_coverage <- cbg_gs_inter |>
|
| 31 |
-
mutate(greenspace_m2 = as.numeric(st_area(geometry))) |>
|
| 32 |
-
st_drop_geometry() |>
|
| 33 |
-
group_by(GEOID) |>
|
| 34 |
-
summarise(greenspace_m2 = sum(greenspace_m2), .groups = "drop") |>
|
| 35 |
-
right_join(cbg_proj |> st_drop_geometry() |> dplyr::select(GEOID, cbg_area_m2), by = "GEOID") |>
|
| 36 |
-
mutate(
|
| 37 |
-
greenspace_m2 = tidyr::replace_na(greenspace_m2, 0),
|
| 38 |
-
GEOID = as.character(GEOID)
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
readr::write_csv(cbg_greenspace_coverage, file.path(out_dir, "cbg_greenspace_coverage.csv"))
|
| 42 |
-
message(glue::glue(
|
| 43 |
-
"[prep] wrote {nrow(cbg_greenspace_coverage)} CBG rows -> data/output/cbg_greenspace_coverage.csv"
|
| 44 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/build_equity_layers.R
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: CalEnviroScreen + SF EJ → GeoPackages in data/output
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Reads local inputs from data/source/, writes derived layers for HuggingFace upload.
|
| 5 |
-
|
| 6 |
-
library(sf)
|
| 7 |
-
library(dplyr)
|
| 8 |
-
|
| 9 |
-
out_dir <- "data/output"
|
| 10 |
-
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
|
| 11 |
-
|
| 12 |
-
# --- CalEnviroScreen (SF tracts only) -----------------------------------------
|
| 13 |
-
|
| 14 |
-
sf::st_read("data/source/calenviroscreen40gdb_F_2021.gdb", quiet = TRUE) |>
|
| 15 |
-
filter(grepl("san francisco", County, ignore.case = TRUE), !is.na(CIscore)) |>
|
| 16 |
-
select(
|
| 17 |
-
Tract, CIscore, CIscoreP,
|
| 18 |
-
PM2_5, PM2_5_Pctl, Traffic, Traffic_Pctl,
|
| 19 |
-
Poverty, Poverty_Pctl, HousBurd, HousBurd_Pctl,
|
| 20 |
-
County
|
| 21 |
-
) |>
|
| 22 |
-
sf::st_transform(4326) |>
|
| 23 |
-
sf::st_make_valid() |>
|
| 24 |
-
sf::st_write(file.path(out_dir, "calenviro_sf.gpkg"), delete_dsn = TRUE, quiet = TRUE)
|
| 25 |
-
|
| 26 |
-
# --- SF Environmental Justice Communities -------------------------------------
|
| 27 |
-
|
| 28 |
-
sf::st_read(
|
| 29 |
-
"data/source/San Francisco Environmental Justice Communities Map_20260401/geo_export_e303e420-19f7-4166-9736-c1dbeda5b82e.shp",
|
| 30 |
-
quiet = TRUE
|
| 31 |
-
) |>
|
| 32 |
-
sf::st_transform(4326) |>
|
| 33 |
-
sf::st_make_valid() |>
|
| 34 |
-
sf::st_write(file.path(out_dir, "sf_ej_communities_map.gpkg"), delete_dsn = TRUE, quiet = TRUE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/create_annotated_gbif_parquet.R
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: GBIF (DuckDB) → sf-gbif.parquet → CBG + NDVI → gbif_census_ndvi_anno.parquet
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# 1) COPY San Francisco iNaturalist occurrences from attached gbif.duckdb → parquet
|
| 5 |
-
# 2) Join census block groups, extract NDVI, write annotated parquet for the app
|
| 6 |
-
#
|
| 7 |
-
# Requires: local gbif.duckdb path below; data/source/cbg_vect_sf.Rdata,
|
| 8 |
-
# data/source/SF_EastBay_NDVI_Sentinel_10.tif
|
| 9 |
-
|
| 10 |
-
library(tidyverse)
|
| 11 |
-
library(duckdb)
|
| 12 |
-
library(sf)
|
| 13 |
-
library(terra)
|
| 14 |
-
library(arrow)
|
| 15 |
-
|
| 16 |
-
# ============================================================================
|
| 17 |
-
# 1) Export raw GBIF points
|
| 18 |
-
# ============================================================================
|
| 19 |
-
|
| 20 |
-
scon <- dbConnect(duckdb())
|
| 21 |
-
scon |> dbExecute("INSTALL spatial; LOAD spatial;")
|
| 22 |
-
scon |> dbExecute("ATTACH '~/Data/Occurrences/GBIF/gbif.duckdb' AS gbifmain;")
|
| 23 |
-
|
| 24 |
-
scon |> dbExecute(
|
| 25 |
-
"COPY
|
| 26 |
-
(SELECT DISTINCT year,kingdom,phylum,class,\"order\",family,genus,species,decimallatitude,decimallongitude,coordinateuncertaintyinmeters,institutioncode FROM gbifmain.gbif
|
| 27 |
-
NATURAL JOIN gbifmain.join_lookup
|
| 28 |
-
WHERE stateprovince='California'
|
| 29 |
-
AND county_name='San Francisco'
|
| 30 |
-
AND institutioncode='iNaturalist'
|
| 31 |
-
AND decimallatitude IS NOT NULL
|
| 32 |
-
AND decimallongitude IS NOT NULL)
|
| 33 |
-
TO
|
| 34 |
-
'data/output/sf-gbif.parquet'
|
| 35 |
-
(FORMAT parquet);
|
| 36 |
-
"
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
scon |> dbDisconnect(shutdown = TRUE)
|
| 40 |
-
|
| 41 |
-
# ============================================================================
|
| 42 |
-
# 2) Annotate: CBG + NDVI → gbif_census_ndvi_anno.parquet
|
| 43 |
-
# ============================================================================
|
| 44 |
-
|
| 45 |
-
gbif_in <- "data/output/sf-gbif.parquet"
|
| 46 |
-
cbg_rdata <- "data/source/cbg_vect_sf.Rdata"
|
| 47 |
-
ndvi_tif <- "data/source/SF_EastBay_NDVI_Sentinel_10.tif"
|
| 48 |
-
out_dir <- "data/output"
|
| 49 |
-
out_file <- file.path(out_dir, "gbif_census_ndvi_anno.parquet")
|
| 50 |
-
|
| 51 |
-
gbif <- arrow::read_parquet(gbif_in, as_data_frame = TRUE)
|
| 52 |
-
load(cbg_rdata)
|
| 53 |
-
ndvi <- terra::rast(ndvi_tif)
|
| 54 |
-
|
| 55 |
-
cbg_acs <- cbg_vect_sf |>
|
| 56 |
-
select(GEOID, medincE, popE, housingE)
|
| 57 |
-
|
| 58 |
-
pts <- gbif |>
|
| 59 |
-
filter(!is.na(decimallongitude), !is.na(decimallatitude)) |>
|
| 60 |
-
st_as_sf(coords = c("decimallongitude", "decimallatitude"), crs = 4326, remove = FALSE)
|
| 61 |
-
|
| 62 |
-
cbg_join <- st_join(pts, cbg_acs, join = st_within, left = TRUE)
|
| 63 |
-
|
| 64 |
-
cbg_join$ndvi_sentinel <- terra::extract(ndvi, cbg_join, ID = FALSE)$NDVI
|
| 65 |
-
|
| 66 |
-
out <- cbg_join |>
|
| 67 |
-
mutate(
|
| 68 |
-
geom_wkt = st_as_text(geometry),
|
| 69 |
-
longitude = decimallongitude,
|
| 70 |
-
latitude = decimallatitude
|
| 71 |
-
) |>
|
| 72 |
-
st_drop_geometry() |>
|
| 73 |
-
rename(
|
| 74 |
-
institutionCode = institutioncode,
|
| 75 |
-
coordinateUncertaintyInMeters = coordinateuncertaintyinmeters
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
|
| 79 |
-
arrow::write_parquet(out, out_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/implement_optimizations.R
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: GTFS timetable, stop headways, and feed zip → data/output
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Inputs: data/source/muni_gtfs-current/*.txt
|
| 5 |
-
# Outputs: data/output/gtfs_timetable_monday.rds
|
| 6 |
-
# data/output/gtfs_stop_headways.csv
|
| 7 |
-
# data/output/sf_muni_gtfs.zip
|
| 8 |
-
#
|
| 9 |
-
# Run after updating the GTFS extract. Then upload data/output/* to HuggingFace.
|
| 10 |
-
|
| 11 |
-
library(tidyverse)
|
| 12 |
-
library(gtfsrouter)
|
| 13 |
-
library(tidytransit)
|
| 14 |
-
library(glue)
|
| 15 |
-
|
| 16 |
-
out_dir <- "data/output"
|
| 17 |
-
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
|
| 18 |
-
out_dir_abs <- normalizePath(out_dir, mustWork = TRUE)
|
| 19 |
-
|
| 20 |
-
gtfs_dir <- "data/source/muni_gtfs-current"
|
| 21 |
-
if (!dir.exists(gtfs_dir)) {
|
| 22 |
-
stop(glue("GTFS folder not found: {gtfs_dir}"))
|
| 23 |
-
}
|
| 24 |
-
gtfs_txt <- list.files(gtfs_dir, pattern = "\\.txt$", full.names = FALSE)
|
| 25 |
-
if (length(gtfs_txt) == 0L) {
|
| 26 |
-
stop(glue("No .txt files under {gtfs_dir}"))
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
# --- Zip feed (same bytes consumers will download from HuggingFace) ----------
|
| 30 |
-
# Absolute zip path before setwd(); normalizePath(zip) is NA if the file does not exist yet
|
| 31 |
-
zip_abs <- file.path(out_dir_abs, "sf_muni_gtfs.zip")
|
| 32 |
-
old_wd <- getwd()
|
| 33 |
-
setwd(gtfs_dir)
|
| 34 |
-
utils::zip(zip_abs, files = gtfs_txt)
|
| 35 |
-
setwd(old_wd)
|
| 36 |
-
|
| 37 |
-
# --- gtfsrouter Monday timetable ------------------------------------------------
|
| 38 |
-
gr <- gtfsrouter::extract_gtfs(zip_abs, quiet = TRUE)
|
| 39 |
-
tt <- gtfsrouter::gtfs_timetable(gr, day = "Monday")
|
| 40 |
-
saveRDS(tt, file.path(out_dir_abs, "gtfs_timetable_monday.rds"), compress = "gzip")
|
| 41 |
-
|
| 42 |
-
# --- AM peak headways (inspectable CSV) ---------------------------------------
|
| 43 |
-
gt <- tidytransit::read_gtfs(zip_abs)
|
| 44 |
-
hw <- tidytransit::get_stop_frequency(gt, start_time = 7 * 3600, end_time = 9 * 3600) |>
|
| 45 |
-
group_by(stop_id) |>
|
| 46 |
-
summarise(
|
| 47 |
-
mean_headway_min = mean(mean_headway, na.rm = TRUE) / 60,
|
| 48 |
-
n_departures_peak = sum(n_departures, na.rm = TRUE),
|
| 49 |
-
.groups = "drop"
|
| 50 |
-
) |>
|
| 51 |
-
mutate(stop_id = as.character(stop_id))
|
| 52 |
-
|
| 53 |
-
readr::write_csv(hw, file.path(out_dir_abs, "gtfs_stop_headways.csv"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/making-greenspace-raster.R
DELETED
|
@@ -1,186 +0,0 @@
|
|
| 1 |
-
# Here we make 2 raster layers related to the nearest greenspaces in San Francisco
|
| 2 |
-
# at 30m resolution
|
| 3 |
-
# - 1) The distance to the nearest greenspace
|
| 4 |
-
# - 2) The osm_id of the nearest greenspace (for looking up the polygon)
|
| 5 |
-
|
| 6 |
-
# Load libraries
|
| 7 |
-
library(tidyverse)
|
| 8 |
-
library(terra)
|
| 9 |
-
library(tidyterra)
|
| 10 |
-
library(sf)
|
| 11 |
-
library(duckdb)
|
| 12 |
-
library(duckdbfs)
|
| 13 |
-
library(glue)
|
| 14 |
-
library(tictoc)
|
| 15 |
-
|
| 16 |
-
# Connect to DuckDB
|
| 17 |
-
tcon <- dbConnect(duckdb::duckdb())
|
| 18 |
-
|
| 19 |
-
# Load spatial extension
|
| 20 |
-
tcon |> dbExecute("
|
| 21 |
-
INSTALL spatial;
|
| 22 |
-
LOAD spatial;
|
| 23 |
-
SET memory_limit = '200GB';
|
| 24 |
-
SET preserve_insertion_order = false;
|
| 25 |
-
SET threads TO 8;
|
| 26 |
-
")
|
| 27 |
-
|
| 28 |
-
greenspace_shp <- "data/source/Greenspaces_osm_nad93/greenspaces_osm_nad83.shp"
|
| 29 |
-
|
| 30 |
-
greenspaces <- st_read(greenspace_shp, quiet = TRUE) |>
|
| 31 |
-
st_transform(3310)
|
| 32 |
-
|
| 33 |
-
# --- --- --- --- --- ---
|
| 34 |
-
# Commented out #
|
| 35 |
-
# --- --- --- --- --- ---
|
| 36 |
-
|
| 37 |
-
# # San Francisco county minus farallon islands
|
| 38 |
-
# sf <- st_read("data/source/CA_Counties/CA_Counties_TIGER2016.shp") %>%
|
| 39 |
-
# filter(NAME == "San Francisco") |>
|
| 40 |
-
# st_transform(3310) |>
|
| 41 |
-
# st_cast("POLYGON") %>%
|
| 42 |
-
# mutate(area = st_area(.)) |>
|
| 43 |
-
# slice_max(area)
|
| 44 |
-
#
|
| 45 |
-
library(tidycensus)
|
| 46 |
-
library(sf)
|
| 47 |
-
library(dplyr)
|
| 48 |
-
|
| 49 |
-
# requires a Census API key (install once via census_api_key())
|
| 50 |
-
sf <- get_acs(
|
| 51 |
-
geography = "county",
|
| 52 |
-
state = "CA",
|
| 53 |
-
variables = "B01003_001", # total population (dummy variable just to trigger geometry)
|
| 54 |
-
year = 2016,
|
| 55 |
-
geometry = TRUE
|
| 56 |
-
) %>%
|
| 57 |
-
filter(NAME == "San Francisco County, California") %>%
|
| 58 |
-
st_transform(3310) %>%
|
| 59 |
-
st_cast("POLYGON") %>%
|
| 60 |
-
mutate(area = st_area(geometry)) %>%
|
| 61 |
-
slice_max(area, n = 1) %>%
|
| 62 |
-
select(GEOID, NAME, geometry)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
empty.sr <- rast("data/source/slope.tif")
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
template.sr <- empty.sr %>%
|
| 69 |
-
# Give every cell an ID
|
| 70 |
-
mutate(cell_id = 1:ncell(.)) |>
|
| 71 |
-
# But filter to everything that isn't NA for slope values
|
| 72 |
-
filter(!is.na(prcnt_slope30)) |>
|
| 73 |
-
crop(sf |> st_transform(4326), mask = T) |>
|
| 74 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) |>
|
| 75 |
-
trim()
|
| 76 |
-
|
| 77 |
-
# Convert template raster to points (centroids) for spatial joins
|
| 78 |
-
template_pts <- template.sr |>
|
| 79 |
-
as.points(na.rm = TRUE) |>
|
| 80 |
-
st_as_sf() |>
|
| 81 |
-
st_transform(4326) # Transform back to WGS84 for consistency
|
| 82 |
-
|
| 83 |
-
# Create directories if they don't exist
|
| 84 |
-
dir.create("data/intermediate", showWarnings = FALSE)
|
| 85 |
-
dir.create("data/output", showWarnings = FALSE)
|
| 86 |
-
|
| 87 |
-
# Write template to file for DuckDB
|
| 88 |
-
template_pts %>%
|
| 89 |
-
write_sf("data/intermediate/template_pts.gpkg")
|
| 90 |
-
|
| 91 |
-
# Load data into DuckDB and set up geometries
|
| 92 |
-
tcon %>% dbExecute(glue("
|
| 93 |
-
CREATE OR REPLACE TABLE greenspace_geo
|
| 94 |
-
AS
|
| 95 |
-
SELECT * EXCLUDE geom,
|
| 96 |
-
ST_TRANSFORM(geom, 'EPSG:4269', 'EPSG:3310', always_xy := true) AS geom3310,
|
| 97 |
-
ST_Centroid(geom) AS centroid_geom3310,
|
| 98 |
-
ST_SimplifyPreserveTopology(ST_TRANSFORM(geom, 'EPSG:4269', 'EPSG:3310', always_xy := true), 10) AS simple_geom3310
|
| 99 |
-
FROM ST_READ('{greenspace_shp}');
|
| 100 |
-
|
| 101 |
-
CREATE INDEX idx_grn_simple_geom ON greenspace_geo USING RTREE (simple_geom3310);
|
| 102 |
-
"))
|
| 103 |
-
|
| 104 |
-
# Load template grid into DuckDB
|
| 105 |
-
tcon %>% dbExecute("
|
| 106 |
-
CREATE OR REPLACE TABLE template_grid_geo
|
| 107 |
-
AS
|
| 108 |
-
SELECT
|
| 109 |
-
cell_id,
|
| 110 |
-
geom,
|
| 111 |
-
ST_TRANSFORM(geom, 'EPSG:4326', 'EPSG:3310', always_xy := true) AS geom3310
|
| 112 |
-
FROM ST_Read('data/intermediate/template_pts.gpkg');
|
| 113 |
-
|
| 114 |
-
CREATE INDEX idx_template_geom ON template_grid_geo USING RTREE (geom3310);
|
| 115 |
-
CREATE INDEX idx_template_cell_id ON template_grid_geo (cell_id);
|
| 116 |
-
")
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
# Calculate nearest greenspace distances
|
| 120 |
-
nn_query <- glue("
|
| 121 |
-
CREATE OR REPLACE TABLE grn_distance_complete AS
|
| 122 |
-
WITH distances AS (
|
| 123 |
-
SELECT
|
| 124 |
-
template.cell_id,
|
| 125 |
-
template.geom AS template_geom,
|
| 126 |
-
ST_AsText(template.geom) AS geom_wkt,
|
| 127 |
-
osm_id,
|
| 128 |
-
ST_Distance(template.geom3310, green.simple_geom3310) AS distance_meters
|
| 129 |
-
FROM template_grid_geo AS template, greenspace_geo AS green
|
| 130 |
-
)
|
| 131 |
-
SELECT
|
| 132 |
-
cell_id,
|
| 133 |
-
template_geom,
|
| 134 |
-
geom_wkt,
|
| 135 |
-
MIN(distance_meters) AS distance_to_greenspace_meters,
|
| 136 |
-
arg_min(osm_id, distance_meters) AS nearest_greenspace_osmid
|
| 137 |
-
FROM distances
|
| 138 |
-
GROUP BY cell_id, template_geom, geom_wkt;
|
| 139 |
-
")
|
| 140 |
-
|
| 141 |
-
cat("Calculating nearest greenspace distances...\n")
|
| 142 |
-
tic()
|
| 143 |
-
tcon %>% dbExecute(nn_query)
|
| 144 |
-
toc()
|
| 145 |
-
# Takes 138 seconds
|
| 146 |
-
# 287 seconds on mac
|
| 147 |
-
|
| 148 |
-
# Get results from database
|
| 149 |
-
sf_dist_complete <- tcon %>%
|
| 150 |
-
tbl("grn_distance_complete") %>%
|
| 151 |
-
select(cell_id, distance_to_greenspace_meters, nearest_greenspace_osmid) %>%
|
| 152 |
-
collect()
|
| 153 |
-
|
| 154 |
-
# Create empty raster based on template
|
| 155 |
-
empty_raster <- empty.sr
|
| 156 |
-
values(empty_raster) <- NA
|
| 157 |
-
|
| 158 |
-
# Join results back to preserve exact cell alignment
|
| 159 |
-
aligned_results <- tibble(cells = 1:ncell(empty.sr)) %>%
|
| 160 |
-
left_join(sf_dist_complete %>% rename(cells = cell_id), by = "cells")
|
| 161 |
-
|
| 162 |
-
# Create distance raster
|
| 163 |
-
greenspace_nearest_dist <- empty_raster
|
| 164 |
-
values(greenspace_nearest_dist) <- aligned_results$distance_to_greenspace_meters
|
| 165 |
-
names(greenspace_nearest_dist) <- "greenspace_nearest_dist"
|
| 166 |
-
cropped_greenspace_nearest_dist <- greenspace_nearest_dist |>
|
| 167 |
-
crop(sf |> st_transform(4326), mask = T) |>
|
| 168 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) |>
|
| 169 |
-
trim()
|
| 170 |
-
|
| 171 |
-
# Create nearest greenspace ID raster
|
| 172 |
-
greenspace_nearest_osmid <- empty_raster
|
| 173 |
-
values(greenspace_nearest_osmid) <- as.numeric(aligned_results$nearest_greenspace_osmid)
|
| 174 |
-
names(greenspace_nearest_osmid) <- "greenspace_nearest_osmid"
|
| 175 |
-
cropped_greenspace_nearest_osmid <- greenspace_nearest_osmid |>
|
| 176 |
-
crop(sf |> st_transform(4326), mask = T) |>
|
| 177 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) |>
|
| 178 |
-
trim()
|
| 179 |
-
|
| 180 |
-
plot(cropped_greenspace_nearest_dist)
|
| 181 |
-
plot(cropped_greenspace_nearest_osmid)
|
| 182 |
-
|
| 183 |
-
# Write rasters
|
| 184 |
-
writeRaster(cropped_greenspace_nearest_dist, "data/output/nearest_greenspace_dist.tif", overwrite = TRUE)
|
| 185 |
-
# Use INT8S (64-bit signed integer) to store OSM IDs without precision loss
|
| 186 |
-
writeRaster(cropped_greenspace_nearest_osmid, "data/output/nearest_greenspace_osmid.tif", overwrite = TRUE, datatype = "INT8S")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/making-rsfprogram-raster.R
DELETED
|
@@ -1,164 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: Nearest RSF Program rasters (30 m, same grid as greenspace)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Produces:
|
| 5 |
-
# - nearest_rsfprogram_dist.tif — distance (m) to nearest RSF program polygon
|
| 6 |
-
# - nearest_rsfprogram_id.tif — polygon_id for lookup in RSF gpkg (prj_name)
|
| 7 |
-
#
|
| 8 |
-
# Source polygons: data/source/RSF_Program_Projects_polygons.gpkg
|
| 9 |
-
# Template grid: same slope.tif + SF county mask as making-greenspace-raster.R
|
| 10 |
-
# ============================================================================
|
| 11 |
-
|
| 12 |
-
library(tidyverse)
|
| 13 |
-
library(terra)
|
| 14 |
-
library(tidyterra)
|
| 15 |
-
library(sf)
|
| 16 |
-
library(duckdb)
|
| 17 |
-
library(glue)
|
| 18 |
-
library(tictoc)
|
| 19 |
-
library(tidycensus)
|
| 20 |
-
|
| 21 |
-
# ============================================================================
|
| 22 |
-
# DuckDB + spatial
|
| 23 |
-
# ============================================================================
|
| 24 |
-
tcon <- dbConnect(duckdb::duckdb())
|
| 25 |
-
|
| 26 |
-
tcon |> dbExecute("
|
| 27 |
-
INSTALL spatial;
|
| 28 |
-
LOAD spatial;
|
| 29 |
-
SET memory_limit = '200GB';
|
| 30 |
-
SET preserve_insertion_order = false;
|
| 31 |
-
SET threads TO 8;
|
| 32 |
-
")
|
| 33 |
-
|
| 34 |
-
rsf_gpkg <- "data/source/RSF_Program_Projects_polygons.gpkg"
|
| 35 |
-
|
| 36 |
-
# ============================================================================
|
| 37 |
-
# SF county boundary + template grid (match greenspace raster script)
|
| 38 |
-
# ============================================================================
|
| 39 |
-
sf <- get_acs(
|
| 40 |
-
geography = "county",
|
| 41 |
-
state = "CA",
|
| 42 |
-
variables = "B01003_001",
|
| 43 |
-
year = 2016,
|
| 44 |
-
geometry = TRUE
|
| 45 |
-
) |>
|
| 46 |
-
filter(NAME == "San Francisco County, California") |>
|
| 47 |
-
st_transform(3310) |>
|
| 48 |
-
st_cast("POLYGON") |>
|
| 49 |
-
mutate(area = st_area(geometry)) |>
|
| 50 |
-
slice_max(area, n = 1) |>
|
| 51 |
-
select(GEOID, NAME, geometry)
|
| 52 |
-
|
| 53 |
-
empty.sr <- rast("data/source/slope.tif")
|
| 54 |
-
|
| 55 |
-
template.sr <- empty.sr %>%
|
| 56 |
-
mutate(cell_id = 1:ncell(.)) %>%
|
| 57 |
-
filter(!is.na(prcnt_slope30)) %>%
|
| 58 |
-
crop(sf %>% st_transform(4326), mask = TRUE) %>%
|
| 59 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) %>%
|
| 60 |
-
trim()
|
| 61 |
-
|
| 62 |
-
template_pts <- template.sr %>%
|
| 63 |
-
as.points(na.rm = TRUE) %>%
|
| 64 |
-
st_as_sf() %>%
|
| 65 |
-
st_transform(4326)
|
| 66 |
-
|
| 67 |
-
dir.create("data/intermediate", showWarnings = FALSE)
|
| 68 |
-
dir.create("data/output", showWarnings = FALSE)
|
| 69 |
-
|
| 70 |
-
template_pts |>
|
| 71 |
-
write_sf("data/intermediate/template_pts.gpkg")
|
| 72 |
-
|
| 73 |
-
# Escape single quotes in path for SQL (Windows paths unlikely here)
|
| 74 |
-
rsf_gpkg_sql <- gsub("'", "''", normalizePath(rsf_gpkg, winslash = "/", mustWork = TRUE))
|
| 75 |
-
|
| 76 |
-
tcon |> dbExecute(glue("
|
| 77 |
-
CREATE OR REPLACE TABLE rsf_geo AS
|
| 78 |
-
SELECT
|
| 79 |
-
polygon_id,
|
| 80 |
-
prj_name,
|
| 81 |
-
ST_TRANSFORM(geom, 'EPSG:4326', 'EPSG:3310', always_xy := true) AS geom3310,
|
| 82 |
-
ST_SimplifyPreserveTopology(
|
| 83 |
-
ST_TRANSFORM(geom, 'EPSG:4326', 'EPSG:3310', always_xy := true),
|
| 84 |
-
10
|
| 85 |
-
) AS simple_geom3310
|
| 86 |
-
FROM ST_Read('{rsf_gpkg_sql}');
|
| 87 |
-
|
| 88 |
-
CREATE INDEX idx_rsf_simple_geom ON rsf_geo USING RTREE (simple_geom3310);
|
| 89 |
-
"))
|
| 90 |
-
|
| 91 |
-
tcon |> dbExecute("
|
| 92 |
-
CREATE OR REPLACE TABLE template_grid_geo AS
|
| 93 |
-
SELECT
|
| 94 |
-
cell_id,
|
| 95 |
-
geom,
|
| 96 |
-
ST_TRANSFORM(geom, 'EPSG:4326', 'EPSG:3310', always_xy := true) AS geom3310
|
| 97 |
-
FROM ST_Read('data/intermediate/template_pts.gpkg');
|
| 98 |
-
|
| 99 |
-
CREATE INDEX idx_template_geom ON template_grid_geo USING RTREE (geom3310);
|
| 100 |
-
CREATE INDEX idx_template_cell_id ON template_grid_geo (cell_id);
|
| 101 |
-
")
|
| 102 |
-
|
| 103 |
-
nn_query <- glue("
|
| 104 |
-
CREATE OR REPLACE TABLE rsf_distance_complete AS
|
| 105 |
-
WITH distances AS (
|
| 106 |
-
SELECT
|
| 107 |
-
template.cell_id,
|
| 108 |
-
template.geom AS template_geom,
|
| 109 |
-
ST_AsText(template.geom) AS geom_wkt,
|
| 110 |
-
rsf.polygon_id,
|
| 111 |
-
ST_Distance(template.geom3310, rsf.simple_geom3310) AS distance_meters
|
| 112 |
-
FROM template_grid_geo AS template, rsf_geo AS rsf
|
| 113 |
-
)
|
| 114 |
-
SELECT
|
| 115 |
-
cell_id,
|
| 116 |
-
template_geom,
|
| 117 |
-
geom_wkt,
|
| 118 |
-
MIN(distance_meters) AS distance_to_rsf_meters,
|
| 119 |
-
arg_min(polygon_id, distance_meters) AS nearest_rsf_polygon_id
|
| 120 |
-
FROM distances
|
| 121 |
-
GROUP BY cell_id, template_geom, geom_wkt;
|
| 122 |
-
")
|
| 123 |
-
|
| 124 |
-
cat("Calculating nearest RSF program distances...\n")
|
| 125 |
-
tic()
|
| 126 |
-
tcon |> dbExecute(nn_query)
|
| 127 |
-
toc()
|
| 128 |
-
|
| 129 |
-
rsf_dist_complete <- tcon |>
|
| 130 |
-
tbl("rsf_distance_complete") |>
|
| 131 |
-
select(cell_id, distance_to_rsf_meters, nearest_rsf_polygon_id) |>
|
| 132 |
-
collect()
|
| 133 |
-
|
| 134 |
-
empty_raster <- empty.sr
|
| 135 |
-
values(empty_raster) <- NA
|
| 136 |
-
|
| 137 |
-
aligned_results <- tibble(cells = 1:ncell(empty.sr)) |>
|
| 138 |
-
left_join(rsf_dist_complete |> rename(cells = cell_id), by = "cells")
|
| 139 |
-
|
| 140 |
-
rsf_nearest_dist <- empty_raster
|
| 141 |
-
values(rsf_nearest_dist) <- aligned_results$distance_to_rsf_meters
|
| 142 |
-
names(rsf_nearest_dist) <- "rsfprogram_nearest_dist"
|
| 143 |
-
|
| 144 |
-
cropped_rsf_dist <- rsf_nearest_dist %>%
|
| 145 |
-
crop(sf %>% st_transform(4326), mask = TRUE) %>%
|
| 146 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) %>%
|
| 147 |
-
trim()
|
| 148 |
-
|
| 149 |
-
rsf_nearest_id <- empty_raster
|
| 150 |
-
values(rsf_nearest_id) <- as.integer(round(as.numeric(aligned_results$nearest_rsf_polygon_id)))
|
| 151 |
-
names(rsf_nearest_id) <- "rsfprogram_nearest_id"
|
| 152 |
-
|
| 153 |
-
cropped_rsf_id <- rsf_nearest_id %>%
|
| 154 |
-
crop(sf %>% st_transform(4326), mask = TRUE) %>%
|
| 155 |
-
crop(ext(c(-123, -122, 37.65, 37.85))) %>%
|
| 156 |
-
trim()
|
| 157 |
-
|
| 158 |
-
plot(cropped_rsf_dist)
|
| 159 |
-
plot(cropped_rsf_id)
|
| 160 |
-
|
| 161 |
-
writeRaster(cropped_rsf_dist, "data/output/nearest_rsfprogram_dist.tif", overwrite = TRUE)
|
| 162 |
-
writeRaster(cropped_rsf_id, "data/output/nearest_rsfprogram_id.tif", overwrite = TRUE, datatype = "INT4S")
|
| 163 |
-
|
| 164 |
-
dbDisconnect(tcon, shutdown = TRUE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/profile_startup.R
DELETED
|
@@ -1,444 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Startup Performance Profiling
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# This script benchmarks each data loading step in setup.R to identify
|
| 5 |
-
# bottlenecks and guide optimization decisions (precomputation, caching, etc.)
|
| 6 |
-
|
| 7 |
-
library(tidyverse)
|
| 8 |
-
library(profvis)
|
| 9 |
-
|
| 10 |
-
# Create benchmark dataframe to track timings
|
| 11 |
-
benchmarks <- tibble(
|
| 12 |
-
step = character(),
|
| 13 |
-
description = character(),
|
| 14 |
-
time_sec = numeric(),
|
| 15 |
-
time_pct = numeric()
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
start_overall <- Sys.time()
|
| 19 |
-
|
| 20 |
-
# ============================================================================
|
| 21 |
-
# 1. Library Loading
|
| 22 |
-
# ============================================================================
|
| 23 |
-
t1 <- Sys.time()
|
| 24 |
-
|
| 25 |
-
library(shiny)
|
| 26 |
-
library(shinydashboard)
|
| 27 |
-
library(leaflet)
|
| 28 |
-
library(mapboxapi)
|
| 29 |
-
library(tidyverse)
|
| 30 |
-
library(tidycensus)
|
| 31 |
-
library(sf)
|
| 32 |
-
library(DT)
|
| 33 |
-
library(RColorBrewer)
|
| 34 |
-
library(terra)
|
| 35 |
-
library(data.table)
|
| 36 |
-
library(mapview)
|
| 37 |
-
library(sjPlot)
|
| 38 |
-
library(sjlabelled)
|
| 39 |
-
library(bslib)
|
| 40 |
-
library(shinycssloaders)
|
| 41 |
-
library(DBI)
|
| 42 |
-
library(duckdb)
|
| 43 |
-
library(dbplyr)
|
| 44 |
-
library(gtfsrouter)
|
| 45 |
-
library(tidytransit)
|
| 46 |
-
library(fmsb)
|
| 47 |
-
library(scales)
|
| 48 |
-
|
| 49 |
-
time_libraries <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 50 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 51 |
-
step = "00_libraries",
|
| 52 |
-
description = "Load all 23 packages",
|
| 53 |
-
time_sec = time_libraries,
|
| 54 |
-
time_pct = NA
|
| 55 |
-
))
|
| 56 |
-
|
| 57 |
-
# ============================================================================
|
| 58 |
-
# 2. Greenspace (OSM)
|
| 59 |
-
# ============================================================================
|
| 60 |
-
t1 <- Sys.time()
|
| 61 |
-
osm_greenspace <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/greenspaces_osm_nad83.shp", quiet = TRUE) |>
|
| 62 |
-
st_transform(4326)
|
| 63 |
-
if (!"name" %in% names(osm_greenspace)) {
|
| 64 |
-
osm_greenspace$name <- "Unnamed Greenspace"
|
| 65 |
-
}
|
| 66 |
-
time_greenspace <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 67 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 68 |
-
step = "01_greenspace",
|
| 69 |
-
description = "Load OSM greenspace polygons",
|
| 70 |
-
time_sec = time_greenspace,
|
| 71 |
-
time_pct = NA
|
| 72 |
-
))
|
| 73 |
-
|
| 74 |
-
# ============================================================================
|
| 75 |
-
# 3. Greenspace Distance Rasters
|
| 76 |
-
# ============================================================================
|
| 77 |
-
t1 <- Sys.time()
|
| 78 |
-
greenspace_dist_raster <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/nearest_greenspace_dist.tif")
|
| 79 |
-
greenspace_osmid_raster <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/nearest_greenspace_osmid.tif")
|
| 80 |
-
time_gs_rasters <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 81 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 82 |
-
step = "02_gs_rasters",
|
| 83 |
-
description = "Load greenspace distance rasters",
|
| 84 |
-
time_sec = time_gs_rasters,
|
| 85 |
-
time_pct = NA
|
| 86 |
-
))
|
| 87 |
-
|
| 88 |
-
# ============================================================================
|
| 89 |
-
# 4. NDVI Raster
|
| 90 |
-
# ============================================================================
|
| 91 |
-
t1 <- Sys.time()
|
| 92 |
-
ndvi <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/SF_EastBay_NDVI_Sentinel_10.tif")
|
| 93 |
-
time_ndvi <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 94 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 95 |
-
step = "03_ndvi",
|
| 96 |
-
description = "Load NDVI raster",
|
| 97 |
-
time_sec = time_ndvi,
|
| 98 |
-
time_pct = NA
|
| 99 |
-
))
|
| 100 |
-
|
| 101 |
-
# ============================================================================
|
| 102 |
-
# 5. CBG Vector Data
|
| 103 |
-
# ============================================================================
|
| 104 |
-
t1 <- Sys.time()
|
| 105 |
-
download.file(
|
| 106 |
-
'https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/cbg_vect_sf.Rdata',
|
| 107 |
-
'/tmp/cbg_vect_sf.Rdata',
|
| 108 |
-
mode = 'wb',
|
| 109 |
-
quiet = TRUE
|
| 110 |
-
)
|
| 111 |
-
load('/tmp/cbg_vect_sf.Rdata')
|
| 112 |
-
|
| 113 |
-
if (!"unique_species" %in% names(cbg_vect_sf)) {
|
| 114 |
-
cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
| 115 |
-
}
|
| 116 |
-
if (!"n_observations" %in% names(cbg_vect_sf)) {
|
| 117 |
-
cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
| 118 |
-
}
|
| 119 |
-
if (!"median_inc" %in% names(cbg_vect_sf)) {
|
| 120 |
-
cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
| 121 |
-
}
|
| 122 |
-
if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
|
| 123 |
-
cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
time_cbg <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 127 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 128 |
-
step = "04_cbg",
|
| 129 |
-
description = "Download + load census block groups",
|
| 130 |
-
time_sec = time_cbg,
|
| 131 |
-
time_pct = NA
|
| 132 |
-
))
|
| 133 |
-
|
| 134 |
-
# ============================================================================
|
| 135 |
-
# 6. Hotspots/Coldspots
|
| 136 |
-
# ============================================================================
|
| 137 |
-
t1 <- Sys.time()
|
| 138 |
-
biodiv_hotspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/hotspots.shp", quiet = TRUE) |>
|
| 139 |
-
st_transform(4326)
|
| 140 |
-
biodiv_coldspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/coldspots.shp", quiet = TRUE) |>
|
| 141 |
-
st_transform(4326)
|
| 142 |
-
time_hotcold <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 143 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 144 |
-
step = "05_hotcold",
|
| 145 |
-
description = "Load hotspots and coldspots",
|
| 146 |
-
time_sec = time_hotcold,
|
| 147 |
-
time_pct = NA
|
| 148 |
-
))
|
| 149 |
-
|
| 150 |
-
# ============================================================================
|
| 151 |
-
# 7. RSF Projects
|
| 152 |
-
# ============================================================================
|
| 153 |
-
t1 <- Sys.time()
|
| 154 |
-
rsf_projects <- st_read("data/source/RSF_Program_Projects_polygons.gpkg", quiet = TRUE) |>
|
| 155 |
-
st_transform(4326)
|
| 156 |
-
time_rsf <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 157 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 158 |
-
step = "06_rsf",
|
| 159 |
-
description = "Load RSF program projects",
|
| 160 |
-
time_sec = time_rsf,
|
| 161 |
-
time_pct = NA
|
| 162 |
-
))
|
| 163 |
-
|
| 164 |
-
# ============================================================================
|
| 165 |
-
# 8. GBIF Parquet Reference (just get path, don't load yet)
|
| 166 |
-
# ============================================================================
|
| 167 |
-
time_gbif_setup <- 0.001 # negligible
|
| 168 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 169 |
-
step = "07_gbif_setup",
|
| 170 |
-
description = "GBIF parquet path (lazy load in server)",
|
| 171 |
-
time_sec = time_gbif_setup,
|
| 172 |
-
time_pct = NA
|
| 173 |
-
))
|
| 174 |
-
|
| 175 |
-
# ============================================================================
|
| 176 |
-
# 9. GTFS Data
|
| 177 |
-
# ============================================================================
|
| 178 |
-
t1 <- Sys.time()
|
| 179 |
-
|
| 180 |
-
gtfs_path <- '/Users/diegoellis/Desktop/RSF_next_steps/GPFS_OSM_Transit/sf_muni_gtfs-current/'
|
| 181 |
-
|
| 182 |
-
# Stops
|
| 183 |
-
gtfs_stops_sf <- tryCatch({
|
| 184 |
-
read.csv(file.path(gtfs_path, 'stops.txt')) |>
|
| 185 |
-
st_as_sf(coords = c("stop_lon", "stop_lat"), crs = 4326)
|
| 186 |
-
}, error = function(e) {
|
| 187 |
-
warning("GTFS stops failed: ", e$message)
|
| 188 |
-
NULL
|
| 189 |
-
})
|
| 190 |
-
|
| 191 |
-
# Route shapes
|
| 192 |
-
gtfs_shapes_raw <- read.csv(file.path(gtfs_path, 'shapes.txt'))
|
| 193 |
-
gtfs_trips_raw <- read.csv(file.path(gtfs_path, 'trips.txt'))
|
| 194 |
-
gtfs_routes_raw <- read.csv(file.path(gtfs_path, 'routes.txt'))
|
| 195 |
-
|
| 196 |
-
shape_route_map <- gtfs_trips_raw |>
|
| 197 |
-
distinct(shape_id, route_id)
|
| 198 |
-
|
| 199 |
-
route_meta <- gtfs_routes_raw |>
|
| 200 |
-
select(route_id, route_short_name, route_long_name, route_color) |>
|
| 201 |
-
mutate(route_color_hex = paste0("#", trimws(route_color)))
|
| 202 |
-
|
| 203 |
-
shapes_split <- gtfs_shapes_raw |>
|
| 204 |
-
arrange(shape_id, shape_pt_sequence) |>
|
| 205 |
-
group_by(shape_id) |>
|
| 206 |
-
group_split()
|
| 207 |
-
|
| 208 |
-
shape_geoms <- lapply(shapes_split, function(s) {
|
| 209 |
-
st_linestring(cbind(s$shape_pt_lon, s$shape_pt_lat))
|
| 210 |
-
})
|
| 211 |
-
|
| 212 |
-
gtfs_routes_sf <- st_sf(
|
| 213 |
-
shape_id = sapply(shapes_split, function(s) s$shape_id[1]),
|
| 214 |
-
geometry = st_sfc(shape_geoms, crs = 4326)
|
| 215 |
-
) |>
|
| 216 |
-
left_join(shape_route_map, by = "shape_id") |>
|
| 217 |
-
left_join(route_meta, by = "route_id")
|
| 218 |
-
|
| 219 |
-
time_gtfs_basic <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 220 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 221 |
-
step = "08_gtfs_basic",
|
| 222 |
-
description = "Load GTFS stops and route shapes",
|
| 223 |
-
time_sec = time_gtfs_basic,
|
| 224 |
-
time_pct = NA
|
| 225 |
-
))
|
| 226 |
-
|
| 227 |
-
# ============================================================================
|
| 228 |
-
# 10. gtfsrouter Initialization
|
| 229 |
-
# ============================================================================
|
| 230 |
-
t1 <- Sys.time()
|
| 231 |
-
|
| 232 |
-
gtfs_router <- tryCatch({
|
| 233 |
-
gtfs_zip_path <- tempfile(fileext = ".zip")
|
| 234 |
-
old_wd <- getwd()
|
| 235 |
-
setwd(gtfs_path)
|
| 236 |
-
utils::zip(gtfs_zip_path, files = list.files('.', pattern = "\\.txt$"))
|
| 237 |
-
setwd(old_wd)
|
| 238 |
-
|
| 239 |
-
gr <- gtfsrouter::extract_gtfs(gtfs_zip_path)
|
| 240 |
-
gtfsrouter::gtfs_timetable(gr, day = "Monday")
|
| 241 |
-
}, error = function(e) {
|
| 242 |
-
warning("gtfsrouter failed: ", e$message)
|
| 243 |
-
NULL
|
| 244 |
-
})
|
| 245 |
-
|
| 246 |
-
time_gtfs_router <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 247 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 248 |
-
step = "09_gtfs_router",
|
| 249 |
-
description = "Initialize gtfsrouter + timetable",
|
| 250 |
-
time_sec = time_gtfs_router,
|
| 251 |
-
time_pct = NA
|
| 252 |
-
))
|
| 253 |
-
|
| 254 |
-
# ============================================================================
|
| 255 |
-
# 11. tidytransit Headways
|
| 256 |
-
# ============================================================================
|
| 257 |
-
t1 <- Sys.time()
|
| 258 |
-
|
| 259 |
-
gtfs_stop_headways <- tryCatch({
|
| 260 |
-
gt <- tidytransit::read_gtfs(gtfs_path)
|
| 261 |
-
tidytransit::get_stop_frequency(gt, start_time = 7 * 3600, end_time = 9 * 3600) |>
|
| 262 |
-
group_by(stop_id) |>
|
| 263 |
-
summarise(
|
| 264 |
-
mean_headway_min = mean(mean_headway, na.rm = TRUE) / 60,
|
| 265 |
-
n_departures_peak = sum(n_departures, na.rm = TRUE),
|
| 266 |
-
.groups = "drop"
|
| 267 |
-
) |>
|
| 268 |
-
mutate(stop_id = as.character(stop_id))
|
| 269 |
-
}, error = function(e) {
|
| 270 |
-
warning("tidytransit failed: ", e$message)
|
| 271 |
-
NULL
|
| 272 |
-
})
|
| 273 |
-
|
| 274 |
-
if (!is.null(gtfs_stop_headways) && !is.null(gtfs_stops_sf)) {
|
| 275 |
-
gtfs_stops_sf <- gtfs_stops_sf |>
|
| 276 |
-
mutate(stop_id = as.character(stop_id)) |>
|
| 277 |
-
left_join(gtfs_stop_headways, by = "stop_id")
|
| 278 |
-
}
|
| 279 |
-
|
| 280 |
-
time_gtfs_headways <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 281 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 282 |
-
step = "10_gtfs_headways",
|
| 283 |
-
description = "Compute transit stop headways (AM peak)",
|
| 284 |
-
time_sec = time_gtfs_headways,
|
| 285 |
-
time_pct = NA
|
| 286 |
-
))
|
| 287 |
-
|
| 288 |
-
# ============================================================================
|
| 289 |
-
# 12. CalEnviroScreen
|
| 290 |
-
# ============================================================================
|
| 291 |
-
t1 <- Sys.time()
|
| 292 |
-
|
| 293 |
-
calenviro_path <- '/Users/diegoellis/Downloads/calenviroscreen40gdb_F_2021.gdb'
|
| 294 |
-
if (!file.exists(calenviro_path)) {
|
| 295 |
-
calenviro_path <- '/Users/diegoellis/Desktop/Projects/Presentations/Data_Schell_Lab_Tutorial/calenviroscreen40gdb_F_2021.gdb'
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
cenv_sf <- tryCatch({
|
| 299 |
-
sf::st_read(calenviro_path, quiet = TRUE) |>
|
| 300 |
-
dplyr::filter(grepl("san francisco", County, ignore.case = TRUE), !is.na(CIscore)) |>
|
| 301 |
-
dplyr::select(
|
| 302 |
-
Tract, CIscore, CIscoreP,
|
| 303 |
-
PM2_5, PM2_5_Pctl, Traffic, Traffic_Pctl,
|
| 304 |
-
Poverty, Poverty_Pctl, HousBurd, HousBurd_Pctl,
|
| 305 |
-
County
|
| 306 |
-
) |>
|
| 307 |
-
sf::st_transform(4326) |>
|
| 308 |
-
sf::st_make_valid()
|
| 309 |
-
}, error = function(e) {
|
| 310 |
-
warning("CalEnviroScreen failed: ", e$message)
|
| 311 |
-
NULL
|
| 312 |
-
})
|
| 313 |
-
|
| 314 |
-
time_calenviro <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 315 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 316 |
-
step = "11_calenviro",
|
| 317 |
-
description = "Load CalEnviroScreen layer",
|
| 318 |
-
time_sec = time_calenviro,
|
| 319 |
-
time_pct = NA
|
| 320 |
-
))
|
| 321 |
-
|
| 322 |
-
# ============================================================================
|
| 323 |
-
# 13. SF EJ Communities
|
| 324 |
-
# ============================================================================
|
| 325 |
-
t1 <- Sys.time()
|
| 326 |
-
|
| 327 |
-
sf_ej_path <- '/Users/diegoellis/Downloads/San Francisco Environmental Justice Communities Map_20251217/geo_export_a21b0a0a-7306-46fd-8381-06581cdbe6e9.shp'
|
| 328 |
-
|
| 329 |
-
sf_ej_sf <- tryCatch({
|
| 330 |
-
sf::st_read(sf_ej_path, quiet = TRUE) |>
|
| 331 |
-
dplyr::mutate(
|
| 332 |
-
symbol_hex = stringr::str_split(symbol_rgb, ",\\s*") |>
|
| 333 |
-
lapply(function(x) sprintf("#%02X%02X%02X",
|
| 334 |
-
as.integer(x[1]), as.integer(x[2]), as.integer(x[3]))) |>
|
| 335 |
-
unlist(),
|
| 336 |
-
ej_label = dplyr::case_when(
|
| 337 |
-
is.na(score) ~ "Not EJ",
|
| 338 |
-
score >= 21 ~ "High EJ burden (21-30)",
|
| 339 |
-
score >= 11 ~ "Moderate EJ burden (11-20)",
|
| 340 |
-
score >= 1 ~ "Low EJ burden (1-10)",
|
| 341 |
-
score == 0 ~ "Score 0",
|
| 342 |
-
TRUE ~ "Unknown"
|
| 343 |
-
)
|
| 344 |
-
) |>
|
| 345 |
-
sf::st_transform(4326) |>
|
| 346 |
-
sf::st_make_valid()
|
| 347 |
-
}, error = function(e) {
|
| 348 |
-
warning("SF EJ layer failed: ", e$message)
|
| 349 |
-
NULL
|
| 350 |
-
})
|
| 351 |
-
|
| 352 |
-
time_ej <- as.numeric(difftime(Sys.time(), t1, units = "secs"))
|
| 353 |
-
benchmarks <- bind_rows(benchmarks, tibble(
|
| 354 |
-
step = "12_ej",
|
| 355 |
-
description = "Load SF EJ communities layer",
|
| 356 |
-
time_sec = time_ej,
|
| 357 |
-
time_pct = NA
|
| 358 |
-
))
|
| 359 |
-
|
| 360 |
-
# ============================================================================
|
| 361 |
-
# SUMMARY
|
| 362 |
-
# ============================================================================
|
| 363 |
-
time_overall <- as.numeric(difftime(Sys.time(), start_overall, units = "secs"))
|
| 364 |
-
|
| 365 |
-
benchmarks <- benchmarks |>
|
| 366 |
-
mutate(time_pct = round(100 * time_sec / time_overall, 1))
|
| 367 |
-
|
| 368 |
-
# Print results
|
| 369 |
-
cat("\n")
|
| 370 |
-
cat("================================================================================\n")
|
| 371 |
-
cat("STARTUP PERFORMANCE BENCHMARK\n")
|
| 372 |
-
cat("================================================================================\n\n")
|
| 373 |
-
|
| 374 |
-
print(benchmarks |> select(step, description, time_sec, time_pct))
|
| 375 |
-
|
| 376 |
-
cat("\n")
|
| 377 |
-
cat("TOTAL STARTUP TIME: ", round(time_overall, 2), " seconds\n")
|
| 378 |
-
cat("================================================================================\n\n")
|
| 379 |
-
|
| 380 |
-
# ============================================================================
|
| 381 |
-
# OPTIMIZATION RECOMMENDATIONS
|
| 382 |
-
# ============================================================================
|
| 383 |
-
cat("OPTIMIZATION RECOMMENDATIONS:\n")
|
| 384 |
-
cat("================================================================================\n\n")
|
| 385 |
-
|
| 386 |
-
# Flag slow steps (>5 seconds or >10% of total)
|
| 387 |
-
slow_steps <- benchmarks |>
|
| 388 |
-
filter(time_sec > 5 | time_pct > 10) |>
|
| 389 |
-
arrange(desc(time_sec))
|
| 390 |
-
|
| 391 |
-
if (nrow(slow_steps) > 0) {
|
| 392 |
-
cat("CRITICAL BOTTLENECKS (>5s or >10%):\n\n")
|
| 393 |
-
for (i in 1:nrow(slow_steps)) {
|
| 394 |
-
row <- slow_steps[i, ]
|
| 395 |
-
cat(" •", row$step, "(", row$time_sec, "s,", row$time_pct, "%)\n")
|
| 396 |
-
cat(" Description:", row$description, "\n")
|
| 397 |
-
|
| 398 |
-
if (grepl("libraries", row$step)) {
|
| 399 |
-
cat(" Recommendation: Load packages only in server() if possible. Use lazy loading.\n")
|
| 400 |
-
} else if (grepl("download", row$step)) {
|
| 401 |
-
cat(" Recommendation: Cache downloaded files locally or use precomputed versions.\n")
|
| 402 |
-
} else if (grepl("gtfsrouter|tidytransit", row$step)) {
|
| 403 |
-
cat(" Recommendation: Pre-compute and cache GTFS timetable. Consider lazy loading for session.\n")
|
| 404 |
-
} else if (grepl("calenviro|ej", row$step)) {
|
| 405 |
-
cat(" Recommendation: Pre-filter to SF boundary. Store as .gpkg or parquet locally.\n")
|
| 406 |
-
}
|
| 407 |
-
cat("\n")
|
| 408 |
-
}
|
| 409 |
-
} else {
|
| 410 |
-
cat("No critical bottlenecks detected (all steps < 5s).\n\n")
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
# Create visualization
|
| 414 |
-
p <- ggplot(benchmarks, aes(x = reorder(step, -time_sec), y = time_sec, fill = time_pct)) +
|
| 415 |
-
geom_col() +
|
| 416 |
-
geom_text(aes(label = paste0(round(time_sec, 2), "s")), vjust = -0.3, size = 3) +
|
| 417 |
-
scale_fill_gradient(low = "green", high = "red", name = "% of Total") +
|
| 418 |
-
labs(
|
| 419 |
-
title = "Shiny App Startup Performance Profile",
|
| 420 |
-
subtitle = paste0("Total time: ", round(time_overall, 2), " seconds"),
|
| 421 |
-
x = "Loading Step",
|
| 422 |
-
y = "Time (seconds)"
|
| 423 |
-
) +
|
| 424 |
-
theme_minimal() +
|
| 425 |
-
theme(
|
| 426 |
-
axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
|
| 427 |
-
plot.title = element_text(face = "bold", size = 14),
|
| 428 |
-
legend.position = "right"
|
| 429 |
-
)
|
| 430 |
-
|
| 431 |
-
print(p)
|
| 432 |
-
|
| 433 |
-
ggsave("/Users/diegoellis/Desktop/Projects/Postdoc/Biodiversity_Access_Indicator/SF_biodiv_access_shiny/startup_benchmark.png",
|
| 434 |
-
p, width = 12, height = 6, dpi = 150)
|
| 435 |
-
|
| 436 |
-
cat("\nBenchmark plot saved to: startup_benchmark.png\n")
|
| 437 |
-
|
| 438 |
-
# ============================================================================
|
| 439 |
-
# Export benchmark data
|
| 440 |
-
# ============================================================================
|
| 441 |
-
write_csv(benchmarks,
|
| 442 |
-
"/Users/diegoellis/Desktop/Projects/Postdoc/Biodiversity_Access_Indicator/SF_biodiv_access_shiny/startup_benchmarks.csv")
|
| 443 |
-
|
| 444 |
-
cat("Benchmark data saved to: startup_benchmarks.csv\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/run_all_prep.R
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: Run full data build → data/output
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Working directory must be the project root (folder containing data/ and Rscripts/).
|
| 5 |
-
#
|
| 6 |
-
# Order (fast steps first; heavy raster build last):
|
| 7 |
-
# 1. GBIF DuckDB → sf-gbif.parquet → gbif_census_ndvi_anno.parquet
|
| 8 |
-
# 2. CalEnviroScreen + SF EJ → GeoPackages
|
| 9 |
-
# 3. GTFS zip + timetable + stop headways (~20–30 s; cached for app startup)
|
| 10 |
-
# 4. Greenspace distance rasters (DuckDB; slow)
|
| 11 |
-
# 5. RSF Program nearest rasters (DuckDB; same grid as 4)
|
| 12 |
-
# 6. Per-CBG greenspace coverage CSV (precomputed so the app's first isochrone
|
| 13 |
-
# doesn't recompute the CBG x greenspace intersection)
|
| 14 |
-
#
|
| 15 |
-
# Then upload files from data/output/ to the HuggingFace dataset (manually or
|
| 16 |
-
# see comments in upload_to_huggingface.R).
|
| 17 |
-
|
| 18 |
-
source("Rscripts/prep/create_annotated_gbif_parquet.R")
|
| 19 |
-
source("Rscripts/prep/build_equity_layers.R")
|
| 20 |
-
source("Rscripts/prep/implement_optimizations.R")
|
| 21 |
-
source("Rscripts/prep/making-greenspace-raster.R")
|
| 22 |
-
source("Rscripts/prep/making-rsfprogram-raster.R")
|
| 23 |
-
source("Rscripts/prep/build_cbg_greenspace_coverage.R")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/prep/upload_to_huggingface.R
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Prep: Stage data/output → data/hf_upload (optional mirror before upload)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Canonical build artifacts live in data/output/ (from Rscripts/prep/* and
|
| 5 |
-
# run_all_prep.R). Copy them here, then upload data/hf_upload/ to HuggingFace:
|
| 6 |
-
#
|
| 7 |
-
# huggingface-cli upload boettiger-lab/sf_biodiv_access data/hf_upload/ . \
|
| 8 |
-
# --repo-type dataset
|
| 9 |
-
#
|
| 10 |
-
# Or upload files from data/output/ directly via the web UI.
|
| 11 |
-
#
|
| 12 |
-
# Already on HuggingFace (not staged from data/output): greenspaces_osm_nad83.{shp,...}
|
| 13 |
-
# Other manual assets: SF_EastBay_NDVI_Sentinel_10.tif, cbg_vect_sf.Rdata, hotspots/coldspots
|
| 14 |
-
#
|
| 15 |
-
# GTFS: sf_muni_gtfs.zip contains the feed; timetable .rds + headways .csv are
|
| 16 |
-
# precomputed (~20–30 s) so the app does not rebuild them every session.
|
| 17 |
-
# ============================================================================
|
| 18 |
-
|
| 19 |
-
library(glue)
|
| 20 |
-
|
| 21 |
-
out_dir <- "data/output"
|
| 22 |
-
upload_dir <- "data/hf_upload"
|
| 23 |
-
dir.create(upload_dir, recursive = TRUE, showWarnings = FALSE)
|
| 24 |
-
|
| 25 |
-
artifacts <- c(
|
| 26 |
-
"nearest_greenspace_dist.tif",
|
| 27 |
-
"nearest_greenspace_osmid.tif",
|
| 28 |
-
"nearest_rsfprogram_dist.tif",
|
| 29 |
-
"nearest_rsfprogram_id.tif",
|
| 30 |
-
"gbif_census_ndvi_anno.parquet",
|
| 31 |
-
"gtfs_timetable_monday.rds",
|
| 32 |
-
"gtfs_stop_headways.csv",
|
| 33 |
-
"sf_muni_gtfs.zip",
|
| 34 |
-
"calenviro_sf.gpkg",
|
| 35 |
-
"sf_ej_communities_map.gpkg",
|
| 36 |
-
"RSF_Program_Projects_polygons.gpkg",
|
| 37 |
-
"cbg_greenspace_coverage.csv"
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
for (f in artifacts) {
|
| 41 |
-
src <- file.path(out_dir, f)
|
| 42 |
-
dst <- file.path(upload_dir, f)
|
| 43 |
-
if (file.exists(src)) {
|
| 44 |
-
file.copy(src, dst, overwrite = TRUE)
|
| 45 |
-
} else {
|
| 46 |
-
warning(glue("Missing in data/output/: {f}"))
|
| 47 |
-
}
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rscripts/setup_unified.R
DELETED
|
@@ -1,275 +0,0 @@
|
|
| 1 |
-
# ============================================================================
|
| 2 |
-
# Setup: load all data for the app (sourced once at startup by app.R)
|
| 3 |
-
# ============================================================================
|
| 4 |
-
# Checks local files in data/cached/ first, falling back to HuggingFace
|
| 5 |
-
# downloads (see hf_or_local()). Loads everything the app needs in one pass.
|
| 6 |
-
#
|
| 7 |
-
# The two most expensive products -- the CBG x greenspace intersection and the
|
| 8 |
-
# transit-routing timetable -- are precomputed in Rscripts/prep/ and just read
|
| 9 |
-
# here, so startup stays fast without splitting the load across files.
|
| 10 |
-
# ============================================================================
|
| 11 |
-
|
| 12 |
-
# ----------------------------------------------------------------------------
|
| 13 |
-
# Libraries
|
| 14 |
-
# ----------------------------------------------------------------------------
|
| 15 |
-
# require(shinyjs) # commented: useShinyjs() enabled features the app never calls
|
| 16 |
-
library(shiny)
|
| 17 |
-
library(shinydashboard)
|
| 18 |
-
library(leaflet)
|
| 19 |
-
library(mapboxapi)
|
| 20 |
-
library(tidyverse)
|
| 21 |
-
# library(tidycensus) # commented: unused in app (CBG data is precomputed) -- speeds startup
|
| 22 |
-
library(sf)
|
| 23 |
-
library(DT)
|
| 24 |
-
# library(RColorBrewer) # commented: brewer.pal only appears in dead if(FALSE) blocks
|
| 25 |
-
library(terra)
|
| 26 |
-
# Commented out: no detectable use in the app, and loading them added ~1-2 s to
|
| 27 |
-
# startup (mapview/sjPlot/sjlabelled each pull large dependency trees). Re-enable
|
| 28 |
-
# the relevant line if you start using one.
|
| 29 |
-
# library(data.table)
|
| 30 |
-
# library(mapview)
|
| 31 |
-
# library(sjPlot)
|
| 32 |
-
# library(sjlabelled)
|
| 33 |
-
library(bslib)
|
| 34 |
-
library(shinycssloaders)
|
| 35 |
-
library(glue)
|
| 36 |
-
|
| 37 |
-
# ----------------------------------------------------------------------------
|
| 38 |
-
# HuggingFace base URL + cache dir + download helper
|
| 39 |
-
# ----------------------------------------------------------------------------
|
| 40 |
-
HF_BASE <- "https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main"
|
| 41 |
-
|
| 42 |
-
# Use data/cached/ when running locally (writable), otherwise fall back to
|
| 43 |
-
# /tmp/sf_biodiv_cache/ for read-only environments like HuggingFace Spaces.
|
| 44 |
-
cache_dir <- if (file.access(".", mode = 2) == 0) "data/cached" else "/tmp/sf_biodiv_cache"
|
| 45 |
-
dir.create(cache_dir, recursive = TRUE, showWarnings = FALSE)
|
| 46 |
-
|
| 47 |
-
# Helper: if the file already exists in cache_dir, return that path. Otherwise
|
| 48 |
-
# attempt to download from HuggingFace into cache_dir. Returns the destination
|
| 49 |
-
# path regardless -- caller must check file.exists() if the download may fail.
|
| 50 |
-
hf_or_local <- function(filename) {
|
| 51 |
-
dest <- file.path(cache_dir, filename)
|
| 52 |
-
if (!file.exists(dest)) {
|
| 53 |
-
tryCatch(
|
| 54 |
-
download.file(glue::glue("{HF_BASE}/{filename}"), dest, mode = "wb", quiet = TRUE),
|
| 55 |
-
error = function(e) warning(glue::glue("HuggingFace download failed for {filename}: {e$message}")),
|
| 56 |
-
warning = function(w) warning(glue::glue("HuggingFace download warning for {filename}: {w$message}"))
|
| 57 |
-
)
|
| 58 |
-
}
|
| 59 |
-
dest
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
message("[setup] loading greenspace, CBG, RSF, CalEnviroScreen, SF EJ…")
|
| 63 |
-
|
| 64 |
-
# ----------------------------------------------------------------------------
|
| 65 |
-
# Greenspace (OSM polygons) -- "Greenspace" map layer + coverage calc
|
| 66 |
-
# ----------------------------------------------------------------------------
|
| 67 |
-
greenspace_shp <- file.path(cache_dir, "greenspaces_osm_nad83.shp")
|
| 68 |
-
if (!file.exists(greenspace_shp)) {
|
| 69 |
-
for (ext in c("shp", "dbf", "prj", "shx")) {
|
| 70 |
-
hf_or_local(glue::glue("greenspaces_osm_nad83.{ext}"))
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
osm_greenspace <- st_read(greenspace_shp, quiet = TRUE) |> st_transform(4326)
|
| 74 |
-
if (!"name" %in% names(osm_greenspace)) osm_greenspace$name <- "Unnamed Greenspace"
|
| 75 |
-
|
| 76 |
-
# ----------------------------------------------------------------------------
|
| 77 |
-
# GBIF observations (parquet) -- path only; queried via DuckDB in app.R / server
|
| 78 |
-
# ----------------------------------------------------------------------------
|
| 79 |
-
gbif_parquet <- hf_or_local("gbif_census_ndvi_anno.parquet")
|
| 80 |
-
|
| 81 |
-
# ----------------------------------------------------------------------------
|
| 82 |
-
# Census block groups (CBG) -- Income / Richness / Data map layers
|
| 83 |
-
# ----------------------------------------------------------------------------
|
| 84 |
-
load(hf_or_local("cbg_vect_sf.Rdata"))
|
| 85 |
-
|
| 86 |
-
if (!"unique_species" %in% names(cbg_vect_sf)) cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
|
| 87 |
-
if (!"n_observations" %in% names(cbg_vect_sf)) cbg_vect_sf$n_observations <- cbg_vect_sf$n
|
| 88 |
-
if (!"median_inc" %in% names(cbg_vect_sf)) cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
|
| 89 |
-
if (!"ndvi_mean" %in% names(cbg_vect_sf)) cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
|
| 90 |
-
|
| 91 |
-
# ----------------------------------------------------------------------------
|
| 92 |
-
# RSF Program Projects -- "RSF Program Projects" map layer
|
| 93 |
-
# ----------------------------------------------------------------------------
|
| 94 |
-
rsf_projects <- st_read(hf_or_local("RSF_Program_Projects_polygons.gpkg"), quiet = TRUE) |>
|
| 95 |
-
st_transform(4326)
|
| 96 |
-
|
| 97 |
-
# ----------------------------------------------------------------------------
|
| 98 |
-
# CalEnviroScreen 4.0 (pre-filtered to SF) -- map overlay
|
| 99 |
-
# ----------------------------------------------------------------------------
|
| 100 |
-
cenv_sf <- tryCatch({
|
| 101 |
-
sf::st_read(hf_or_local("calenviro_sf.gpkg"), quiet = TRUE)
|
| 102 |
-
}, error = function(e) {
|
| 103 |
-
warning("CalEnviroScreen failed to load: ", e$message); NULL
|
| 104 |
-
})
|
| 105 |
-
|
| 106 |
-
# ----------------------------------------------------------------------------
|
| 107 |
-
# SF Environmental Justice Communities -- map overlay
|
| 108 |
-
# ----------------------------------------------------------------------------
|
| 109 |
-
sf_ej_sf <- tryCatch({
|
| 110 |
-
sf::st_read(hf_or_local("sf_ej_communities_map.gpkg"), quiet = TRUE) |>
|
| 111 |
-
dplyr::mutate(
|
| 112 |
-
symbol_hex = stringr::str_split(symbol_rgb, ",\\s*") |>
|
| 113 |
-
lapply(function(x) sprintf("#%02X%02X%02X",
|
| 114 |
-
as.integer(x[1]), as.integer(x[2]), as.integer(x[3]))) |>
|
| 115 |
-
unlist(),
|
| 116 |
-
ej_label = dplyr::case_when(
|
| 117 |
-
is.na(score) ~ "Not EJ",
|
| 118 |
-
score >= 21 ~ "High EJ burden (21-30)",
|
| 119 |
-
score >= 11 ~ "Moderate EJ burden (11-20)",
|
| 120 |
-
score >= 1 ~ "Low EJ burden (1-10)",
|
| 121 |
-
score == 0 ~ "Score 0",
|
| 122 |
-
TRUE ~ "Unknown"
|
| 123 |
-
)
|
| 124 |
-
)
|
| 125 |
-
}, error = function(e) {
|
| 126 |
-
warning("SF EJ layer failed to load: ", e$message); NULL
|
| 127 |
-
})
|
| 128 |
-
|
| 129 |
-
message("[setup] loading distance rasters + NDVI (terra lazy: headers only)…")
|
| 130 |
-
|
| 131 |
-
# ----------------------------------------------------------------------------
|
| 132 |
-
# Greenspace + RSF distance/id rasters + NDVI (terra is lazy: reads headers, not
|
| 133 |
-
# cells, so these open instantly; pixels are read during isochrone analysis).
|
| 134 |
-
# ----------------------------------------------------------------------------
|
| 135 |
-
greenspace_dist_raster <- terra::rast(hf_or_local("nearest_greenspace_dist.tif"))
|
| 136 |
-
greenspace_osmid_raster <- terra::rast(hf_or_local("nearest_greenspace_osmid.tif"))
|
| 137 |
-
rsfprogram_dist_raster <- terra::rast(hf_or_local("nearest_rsfprogram_dist.tif"))
|
| 138 |
-
rsfprogram_id_raster <- terra::rast(hf_or_local("nearest_rsfprogram_id.tif"))
|
| 139 |
-
ndvi <- terra::rast(hf_or_local("SF_EastBay_NDVI_Sentinel_10.tif"))
|
| 140 |
-
|
| 141 |
-
# ----------------------------------------------------------------------------
|
| 142 |
-
# Per-CBG greenspace overlap (used in the socio summary + BAI benchmark).
|
| 143 |
-
# Precomputed by Rscripts/prep/build_cbg_greenspace_coverage.R and cached as a
|
| 144 |
-
# small CSV (downloaded from HuggingFace like the other prep artifacts). If that
|
| 145 |
-
# file is unavailable, fall back to the (slow) st_union + st_intersection here.
|
| 146 |
-
# ----------------------------------------------------------------------------
|
| 147 |
-
cov_path <- hf_or_local("cbg_greenspace_coverage.csv")
|
| 148 |
-
cbg_greenspace_coverage <- if (file.exists(cov_path)) {
|
| 149 |
-
# GEOID forced to character so leading zeros survive (FIPS codes; join key).
|
| 150 |
-
readr::read_csv(cov_path, col_types = readr::cols(GEOID = readr::col_character()),
|
| 151 |
-
show_col_types = FALSE)
|
| 152 |
-
} else {
|
| 153 |
-
message("[setup] precomputed greenspace coverage not found; computing inline (slow)…")
|
| 154 |
-
cbg_proj <- st_transform(cbg_vect_sf[, "GEOID"], 3857) |>
|
| 155 |
-
mutate(cbg_area_m2 = as.numeric(st_area(geometry)))
|
| 156 |
-
gs_proj <- st_transform(osm_greenspace, 3857) |> st_make_valid()
|
| 157 |
-
gs_union <- st_union(gs_proj)
|
| 158 |
-
cbg_gs_inter <- st_intersection(cbg_proj, gs_union)
|
| 159 |
-
coverage <- cbg_gs_inter |>
|
| 160 |
-
mutate(greenspace_m2 = as.numeric(st_area(geometry))) |>
|
| 161 |
-
st_drop_geometry() |>
|
| 162 |
-
group_by(GEOID) |>
|
| 163 |
-
summarise(greenspace_m2 = sum(greenspace_m2), .groups = "drop") |>
|
| 164 |
-
right_join(cbg_proj |> st_drop_geometry() |> dplyr::select(GEOID, cbg_area_m2), by = "GEOID") |>
|
| 165 |
-
mutate(
|
| 166 |
-
greenspace_m2 = tidyr::replace_na(greenspace_m2, 0),
|
| 167 |
-
GEOID = as.character(GEOID)
|
| 168 |
-
)
|
| 169 |
-
readr::write_csv(coverage, file.path(cache_dir, "cbg_greenspace_coverage.csv"))
|
| 170 |
-
coverage
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
message("[setup] loading GTFS (stops, routes, headways, routing timetable)…")
|
| 174 |
-
|
| 175 |
-
# ----------------------------------------------------------------------------
|
| 176 |
-
# GTFS (SF Muni)
|
| 177 |
-
# ----------------------------------------------------------------------------
|
| 178 |
-
gtfs_zip_path <- hf_or_local("sf_muni_gtfs.zip")
|
| 179 |
-
|
| 180 |
-
# Unzip for read.csv(stops.txt, …); gtfsrouter/tidytransit read the .zip directly.
|
| 181 |
-
gtfs_unzip_dir <- file.path(cache_dir, "muni_gtfs")
|
| 182 |
-
dir.create(gtfs_unzip_dir, recursive = TRUE, showWarnings = FALSE)
|
| 183 |
-
if (!dir.exists(gtfs_unzip_dir) || length(list.files(gtfs_unzip_dir, pattern = "\\.txt$")) == 0L) {
|
| 184 |
-
unzip(gtfs_zip_path, exdir = gtfs_unzip_dir, overwrite = TRUE)
|
| 185 |
-
}
|
| 186 |
-
gtfs_path <- gtfs_unzip_dir
|
| 187 |
-
|
| 188 |
-
# --- Transit stops ("Transit Stops" overlay) --------------------------------
|
| 189 |
-
gtfs_stops_sf <- tryCatch({
|
| 190 |
-
read.csv(file.path(gtfs_path, "stops.txt")) |>
|
| 191 |
-
st_as_sf(coords = c("stop_lon", "stop_lat"), crs = 4326)
|
| 192 |
-
}, error = function(e) { warning("GTFS stops failed to load: ", e$message); NULL })
|
| 193 |
-
|
| 194 |
-
# --- Route shapes ("Transit Routes" overlay) --------------------------------
|
| 195 |
-
gtfs_routes_sf <- tryCatch({
|
| 196 |
-
gtfs_shapes_raw <- read.csv(file.path(gtfs_path, "shapes.txt"))
|
| 197 |
-
gtfs_trips_raw <- read.csv(file.path(gtfs_path, "trips.txt"))
|
| 198 |
-
gtfs_routes_raw <- read.csv(file.path(gtfs_path, "routes.txt"))
|
| 199 |
-
|
| 200 |
-
shape_route_map <- gtfs_trips_raw |> distinct(shape_id, route_id)
|
| 201 |
-
route_meta <- gtfs_routes_raw |>
|
| 202 |
-
select(route_id, route_short_name, route_long_name, route_color) |>
|
| 203 |
-
mutate(route_color_hex = paste0("#", trimws(route_color)))
|
| 204 |
-
|
| 205 |
-
shapes_split <- gtfs_shapes_raw |>
|
| 206 |
-
arrange(shape_id, shape_pt_sequence) |>
|
| 207 |
-
group_by(shape_id) |>
|
| 208 |
-
group_split()
|
| 209 |
-
|
| 210 |
-
shape_geoms <- lapply(shapes_split, function(s) {
|
| 211 |
-
st_linestring(cbind(s$shape_pt_lon, s$shape_pt_lat))
|
| 212 |
-
})
|
| 213 |
-
|
| 214 |
-
st_sf(
|
| 215 |
-
shape_id = sapply(shapes_split, function(s) s$shape_id[1]),
|
| 216 |
-
geometry = st_sfc(shape_geoms, crs = 4326)
|
| 217 |
-
) |>
|
| 218 |
-
left_join(shape_route_map, by = "shape_id") |>
|
| 219 |
-
left_join(route_meta, by = "route_id")
|
| 220 |
-
}, error = function(e) { warning("GTFS route shapes failed to load: ", e$message); NULL })
|
| 221 |
-
|
| 222 |
-
# --- Stop headways (AM peak 7-9am): cached as CSV (readable / diffable).
|
| 223 |
-
# Joined into gtfs_stops_sf below so they show in the stop popups on the map.
|
| 224 |
-
hw_csv <- file.path(cache_dir, "gtfs_stop_headways.csv")
|
| 225 |
-
hw_rds <- file.path(cache_dir, "gtfs_stop_headways.rds")
|
| 226 |
-
if (!file.exists(hw_csv) && file.exists(hw_rds)) {
|
| 227 |
-
readRDS(hw_rds) |> readr::write_csv(hw_csv)
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
gtfs_stop_headways <- tryCatch({
|
| 231 |
-
headways_path <- hf_or_local("gtfs_stop_headways.csv")
|
| 232 |
-
if (file.exists(headways_path)) {
|
| 233 |
-
readr::read_csv(headways_path, show_col_types = FALSE) |>
|
| 234 |
-
mutate(stop_id = as.character(stop_id))
|
| 235 |
-
} else {
|
| 236 |
-
gt <- tidytransit::read_gtfs(gtfs_zip_path)
|
| 237 |
-
hw <- tidytransit::get_stop_frequency(gt, start_time = 7 * 3600, end_time = 9 * 3600) |>
|
| 238 |
-
group_by(stop_id) |>
|
| 239 |
-
summarise(
|
| 240 |
-
mean_headway_min = mean(mean_headway, na.rm = TRUE) / 60,
|
| 241 |
-
n_departures_peak = sum(n_departures, na.rm = TRUE),
|
| 242 |
-
.groups = "drop"
|
| 243 |
-
) |>
|
| 244 |
-
mutate(stop_id = as.character(stop_id))
|
| 245 |
-
readr::write_csv(hw, hw_csv)
|
| 246 |
-
hw
|
| 247 |
-
}
|
| 248 |
-
}, error = function(e) { warning("tidytransit headway computation failed: ", e$message); NULL })
|
| 249 |
-
|
| 250 |
-
if (!is.null(gtfs_stop_headways) && !is.null(gtfs_stops_sf)) {
|
| 251 |
-
gtfs_stops_sf <- gtfs_stops_sf |>
|
| 252 |
-
mutate(stop_id = as.character(stop_id)) |>
|
| 253 |
-
left_join(gtfs_stop_headways, by = "stop_id")
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
# --- gtfsrouter timetable: routes transit isochrones (precomputed .rds) ------
|
| 257 |
-
gtfs_router <- tryCatch({
|
| 258 |
-
timetable_path <- hf_or_local("gtfs_timetable_monday.rds")
|
| 259 |
-
if (file.exists(timetable_path)) {
|
| 260 |
-
readRDS(timetable_path)
|
| 261 |
-
} else {
|
| 262 |
-
gr <- gtfsrouter::extract_gtfs(gtfs_zip_path)
|
| 263 |
-
result <- gtfsrouter::gtfs_timetable(gr, day = "Monday")
|
| 264 |
-
saveRDS(result, file.path(cache_dir, "gtfs_timetable_monday.rds"))
|
| 265 |
-
result
|
| 266 |
-
}
|
| 267 |
-
}, error = function(e) { warning("gtfsrouter failed to initialise: ", e$message); NULL })
|
| 268 |
-
|
| 269 |
-
# --- Pre-computed transit isochrone cache (keyed by nearest stop + time) -----
|
| 270 |
-
transit_iso_cache <- tryCatch({
|
| 271 |
-
p <- file.path(cache_dir, "transit_iso_cache.rds")
|
| 272 |
-
if (file.exists(p)) readRDS(p) else NULL
|
| 273 |
-
}, error = function(e) { NULL })
|
| 274 |
-
|
| 275 |
-
message("[setup] data load complete.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SF_biodiv_access_shiny.Rproj
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
Version: 1.0
|
| 2 |
-
|
| 3 |
-
RestoreWorkspace: Default
|
| 4 |
-
SaveWorkspace: Default
|
| 5 |
-
AlwaysSaveHistory: Default
|
| 6 |
-
|
| 7 |
-
EnableCodeIndexing: Yes
|
| 8 |
-
UseSpacesForTab: Yes
|
| 9 |
-
NumSpacesForTab: 2
|
| 10 |
-
Encoding: UTF-8
|
| 11 |
-
|
| 12 |
-
RnwWeave: Sweave
|
| 13 |
-
LaTeX: pdfLaTeX
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.R
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
install.r
CHANGED
|
@@ -1,29 +1,19 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
install.packages(c(
|
| 4 |
-
"bslib",
|
| 5 |
-
"data.table",
|
| 6 |
-
"DBI",
|
| 7 |
-
"dbplyr",
|
| 8 |
-
"DT",
|
| 9 |
-
"duckdb",
|
| 10 |
-
"fmsb",
|
| 11 |
-
"glue",
|
| 12 |
-
"gtfsrouter",
|
| 13 |
-
"leaflet",
|
| 14 |
-
"mapboxapi",
|
| 15 |
-
"mapview",
|
| 16 |
-
"RColorBrewer",
|
| 17 |
-
"scales",
|
| 18 |
-
"sf",
|
| 19 |
-
"shiny",
|
| 20 |
-
"shinycssloaders",
|
| 21 |
-
"shinydashboard",
|
| 22 |
-
"shinyjs",
|
| 23 |
-
"sjlabelled",
|
| 24 |
-
"sjPlot",
|
| 25 |
-
"terra",
|
| 26 |
-
"tidycensus",
|
| 27 |
-
"tidytransit",
|
| 28 |
-
"tidyverse"
|
| 29 |
-
))
|
|
|
|
| 1 |
+
install.packages(c("shinyjs",
|
| 2 |
+
"shiny",
|
| 3 |
+
"shinydashboard",
|
| 4 |
+
"leaflet",
|
| 5 |
+
"mapboxapi",
|
| 6 |
+
"tidyverse",
|
| 7 |
+
"tidycensus",
|
| 8 |
+
"sf",
|
| 9 |
+
"DT",
|
| 10 |
+
"RColorBrewer",
|
| 11 |
+
"terra",
|
| 12 |
+
"data.table",
|
| 13 |
+
"mapview",
|
| 14 |
+
"sjPlot",
|
| 15 |
+
"sjlabelled",
|
| 16 |
+
"bslib",
|
| 17 |
+
"shinycssloaders",
|
| 18 |
+
"duckdb"))
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
penguins.csv
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Species,Island,Bill Length (mm),Bill Depth (mm),Flipper Length (mm),Body Mass (g),Sex,Year
|
| 2 |
+
Adelie,Torgersen,39.1,18.7,181,3750,male,2007
|
| 3 |
+
Adelie,Torgersen,39.5,17.4,186,3800,female,2007
|
| 4 |
+
Adelie,Torgersen,40.3,18,195,3250,female,2007
|
| 5 |
+
Adelie,Torgersen,NA,NA,NA,NA,NA,2007
|
| 6 |
+
Adelie,Torgersen,36.7,19.3,193,3450,female,2007
|
| 7 |
+
Adelie,Torgersen,39.3,20.6,190,3650,male,2007
|
| 8 |
+
Adelie,Torgersen,38.9,17.8,181,3625,female,2007
|
| 9 |
+
Adelie,Torgersen,39.2,19.6,195,4675,male,2007
|
| 10 |
+
Adelie,Torgersen,34.1,18.1,193,3475,NA,2007
|
| 11 |
+
Adelie,Torgersen,42,20.2,190,4250,NA,2007
|
| 12 |
+
Adelie,Torgersen,37.8,17.1,186,3300,NA,2007
|
| 13 |
+
Adelie,Torgersen,37.8,17.3,180,3700,NA,2007
|
| 14 |
+
Adelie,Torgersen,41.1,17.6,182,3200,female,2007
|
| 15 |
+
Adelie,Torgersen,38.6,21.2,191,3800,male,2007
|
| 16 |
+
Adelie,Torgersen,34.6,21.1,198,4400,male,2007
|
| 17 |
+
Adelie,Torgersen,36.6,17.8,185,3700,female,2007
|
| 18 |
+
Adelie,Torgersen,38.7,19,195,3450,female,2007
|
| 19 |
+
Adelie,Torgersen,42.5,20.7,197,4500,male,2007
|
| 20 |
+
Adelie,Torgersen,34.4,18.4,184,3325,female,2007
|
| 21 |
+
Adelie,Torgersen,46,21.5,194,4200,male,2007
|
| 22 |
+
Adelie,Biscoe,37.8,18.3,174,3400,female,2007
|
| 23 |
+
Adelie,Biscoe,37.7,18.7,180,3600,male,2007
|
| 24 |
+
Adelie,Biscoe,35.9,19.2,189,3800,female,2007
|
| 25 |
+
Adelie,Biscoe,38.2,18.1,185,3950,male,2007
|
| 26 |
+
Adelie,Biscoe,38.8,17.2,180,3800,male,2007
|
| 27 |
+
Adelie,Biscoe,35.3,18.9,187,3800,female,2007
|
| 28 |
+
Adelie,Biscoe,40.6,18.6,183,3550,male,2007
|
| 29 |
+
Adelie,Biscoe,40.5,17.9,187,3200,female,2007
|
| 30 |
+
Adelie,Biscoe,37.9,18.6,172,3150,female,2007
|
| 31 |
+
Adelie,Biscoe,40.5,18.9,180,3950,male,2007
|
| 32 |
+
Adelie,Dream,39.5,16.7,178,3250,female,2007
|
| 33 |
+
Adelie,Dream,37.2,18.1,178,3900,male,2007
|
| 34 |
+
Adelie,Dream,39.5,17.8,188,3300,female,2007
|
| 35 |
+
Adelie,Dream,40.9,18.9,184,3900,male,2007
|
| 36 |
+
Adelie,Dream,36.4,17,195,3325,female,2007
|
| 37 |
+
Adelie,Dream,39.2,21.1,196,4150,male,2007
|
| 38 |
+
Adelie,Dream,38.8,20,190,3950,male,2007
|
| 39 |
+
Adelie,Dream,42.2,18.5,180,3550,female,2007
|
| 40 |
+
Adelie,Dream,37.6,19.3,181,3300,female,2007
|
| 41 |
+
Adelie,Dream,39.8,19.1,184,4650,male,2007
|
| 42 |
+
Adelie,Dream,36.5,18,182,3150,female,2007
|
| 43 |
+
Adelie,Dream,40.8,18.4,195,3900,male,2007
|
| 44 |
+
Adelie,Dream,36,18.5,186,3100,female,2007
|
| 45 |
+
Adelie,Dream,44.1,19.7,196,4400,male,2007
|
| 46 |
+
Adelie,Dream,37,16.9,185,3000,female,2007
|
| 47 |
+
Adelie,Dream,39.6,18.8,190,4600,male,2007
|
| 48 |
+
Adelie,Dream,41.1,19,182,3425,male,2007
|
| 49 |
+
Adelie,Dream,37.5,18.9,179,2975,NA,2007
|
| 50 |
+
Adelie,Dream,36,17.9,190,3450,female,2007
|
| 51 |
+
Adelie,Dream,42.3,21.2,191,4150,male,2007
|
| 52 |
+
Adelie,Biscoe,39.6,17.7,186,3500,female,2008
|
| 53 |
+
Adelie,Biscoe,40.1,18.9,188,4300,male,2008
|
| 54 |
+
Adelie,Biscoe,35,17.9,190,3450,female,2008
|
| 55 |
+
Adelie,Biscoe,42,19.5,200,4050,male,2008
|
| 56 |
+
Adelie,Biscoe,34.5,18.1,187,2900,female,2008
|
| 57 |
+
Adelie,Biscoe,41.4,18.6,191,3700,male,2008
|
| 58 |
+
Adelie,Biscoe,39,17.5,186,3550,female,2008
|
| 59 |
+
Adelie,Biscoe,40.6,18.8,193,3800,male,2008
|
| 60 |
+
Adelie,Biscoe,36.5,16.6,181,2850,female,2008
|
| 61 |
+
Adelie,Biscoe,37.6,19.1,194,3750,male,2008
|
| 62 |
+
Adelie,Biscoe,35.7,16.9,185,3150,female,2008
|
| 63 |
+
Adelie,Biscoe,41.3,21.1,195,4400,male,2008
|
| 64 |
+
Adelie,Biscoe,37.6,17,185,3600,female,2008
|
| 65 |
+
Adelie,Biscoe,41.1,18.2,192,4050,male,2008
|
| 66 |
+
Adelie,Biscoe,36.4,17.1,184,2850,female,2008
|
| 67 |
+
Adelie,Biscoe,41.6,18,192,3950,male,2008
|
| 68 |
+
Adelie,Biscoe,35.5,16.2,195,3350,female,2008
|
| 69 |
+
Adelie,Biscoe,41.1,19.1,188,4100,male,2008
|
| 70 |
+
Adelie,Torgersen,35.9,16.6,190,3050,female,2008
|
| 71 |
+
Adelie,Torgersen,41.8,19.4,198,4450,male,2008
|
| 72 |
+
Adelie,Torgersen,33.5,19,190,3600,female,2008
|
| 73 |
+
Adelie,Torgersen,39.7,18.4,190,3900,male,2008
|
| 74 |
+
Adelie,Torgersen,39.6,17.2,196,3550,female,2008
|
| 75 |
+
Adelie,Torgersen,45.8,18.9,197,4150,male,2008
|
| 76 |
+
Adelie,Torgersen,35.5,17.5,190,3700,female,2008
|
| 77 |
+
Adelie,Torgersen,42.8,18.5,195,4250,male,2008
|
| 78 |
+
Adelie,Torgersen,40.9,16.8,191,3700,female,2008
|
| 79 |
+
Adelie,Torgersen,37.2,19.4,184,3900,male,2008
|
| 80 |
+
Adelie,Torgersen,36.2,16.1,187,3550,female,2008
|
| 81 |
+
Adelie,Torgersen,42.1,19.1,195,4000,male,2008
|
| 82 |
+
Adelie,Torgersen,34.6,17.2,189,3200,female,2008
|
| 83 |
+
Adelie,Torgersen,42.9,17.6,196,4700,male,2008
|
| 84 |
+
Adelie,Torgersen,36.7,18.8,187,3800,female,2008
|
| 85 |
+
Adelie,Torgersen,35.1,19.4,193,4200,male,2008
|
| 86 |
+
Adelie,Dream,37.3,17.8,191,3350,female,2008
|
| 87 |
+
Adelie,Dream,41.3,20.3,194,3550,male,2008
|
| 88 |
+
Adelie,Dream,36.3,19.5,190,3800,male,2008
|
| 89 |
+
Adelie,Dream,36.9,18.6,189,3500,female,2008
|
| 90 |
+
Adelie,Dream,38.3,19.2,189,3950,male,2008
|
| 91 |
+
Adelie,Dream,38.9,18.8,190,3600,female,2008
|
| 92 |
+
Adelie,Dream,35.7,18,202,3550,female,2008
|
| 93 |
+
Adelie,Dream,41.1,18.1,205,4300,male,2008
|
| 94 |
+
Adelie,Dream,34,17.1,185,3400,female,2008
|
| 95 |
+
Adelie,Dream,39.6,18.1,186,4450,male,2008
|
| 96 |
+
Adelie,Dream,36.2,17.3,187,3300,female,2008
|
| 97 |
+
Adelie,Dream,40.8,18.9,208,4300,male,2008
|
| 98 |
+
Adelie,Dream,38.1,18.6,190,3700,female,2008
|
| 99 |
+
Adelie,Dream,40.3,18.5,196,4350,male,2008
|
| 100 |
+
Adelie,Dream,33.1,16.1,178,2900,female,2008
|
| 101 |
+
Adelie,Dream,43.2,18.5,192,4100,male,2008
|
| 102 |
+
Adelie,Biscoe,35,17.9,192,3725,female,2009
|
| 103 |
+
Adelie,Biscoe,41,20,203,4725,male,2009
|
| 104 |
+
Adelie,Biscoe,37.7,16,183,3075,female,2009
|
| 105 |
+
Adelie,Biscoe,37.8,20,190,4250,male,2009
|
| 106 |
+
Adelie,Biscoe,37.9,18.6,193,2925,female,2009
|
| 107 |
+
Adelie,Biscoe,39.7,18.9,184,3550,male,2009
|
| 108 |
+
Adelie,Biscoe,38.6,17.2,199,3750,female,2009
|
| 109 |
+
Adelie,Biscoe,38.2,20,190,3900,male,2009
|
| 110 |
+
Adelie,Biscoe,38.1,17,181,3175,female,2009
|
| 111 |
+
Adelie,Biscoe,43.2,19,197,4775,male,2009
|
| 112 |
+
Adelie,Biscoe,38.1,16.5,198,3825,female,2009
|
| 113 |
+
Adelie,Biscoe,45.6,20.3,191,4600,male,2009
|
| 114 |
+
Adelie,Biscoe,39.7,17.7,193,3200,female,2009
|
| 115 |
+
Adelie,Biscoe,42.2,19.5,197,4275,male,2009
|
| 116 |
+
Adelie,Biscoe,39.6,20.7,191,3900,female,2009
|
| 117 |
+
Adelie,Biscoe,42.7,18.3,196,4075,male,2009
|
| 118 |
+
Adelie,Torgersen,38.6,17,188,2900,female,2009
|
| 119 |
+
Adelie,Torgersen,37.3,20.5,199,3775,male,2009
|
| 120 |
+
Adelie,Torgersen,35.7,17,189,3350,female,2009
|
| 121 |
+
Adelie,Torgersen,41.1,18.6,189,3325,male,2009
|
| 122 |
+
Adelie,Torgersen,36.2,17.2,187,3150,female,2009
|
| 123 |
+
Adelie,Torgersen,37.7,19.8,198,3500,male,2009
|
| 124 |
+
Adelie,Torgersen,40.2,17,176,3450,female,2009
|
| 125 |
+
Adelie,Torgersen,41.4,18.5,202,3875,male,2009
|
| 126 |
+
Adelie,Torgersen,35.2,15.9,186,3050,female,2009
|
| 127 |
+
Adelie,Torgersen,40.6,19,199,4000,male,2009
|
| 128 |
+
Adelie,Torgersen,38.8,17.6,191,3275,female,2009
|
| 129 |
+
Adelie,Torgersen,41.5,18.3,195,4300,male,2009
|
| 130 |
+
Adelie,Torgersen,39,17.1,191,3050,female,2009
|
| 131 |
+
Adelie,Torgersen,44.1,18,210,4000,male,2009
|
| 132 |
+
Adelie,Torgersen,38.5,17.9,190,3325,female,2009
|
| 133 |
+
Adelie,Torgersen,43.1,19.2,197,3500,male,2009
|
| 134 |
+
Adelie,Dream,36.8,18.5,193,3500,female,2009
|
| 135 |
+
Adelie,Dream,37.5,18.5,199,4475,male,2009
|
| 136 |
+
Adelie,Dream,38.1,17.6,187,3425,female,2009
|
| 137 |
+
Adelie,Dream,41.1,17.5,190,3900,male,2009
|
| 138 |
+
Adelie,Dream,35.6,17.5,191,3175,female,2009
|
| 139 |
+
Adelie,Dream,40.2,20.1,200,3975,male,2009
|
| 140 |
+
Adelie,Dream,37,16.5,185,3400,female,2009
|
| 141 |
+
Adelie,Dream,39.7,17.9,193,4250,male,2009
|
| 142 |
+
Adelie,Dream,40.2,17.1,193,3400,female,2009
|
| 143 |
+
Adelie,Dream,40.6,17.2,187,3475,male,2009
|
| 144 |
+
Adelie,Dream,32.1,15.5,188,3050,female,2009
|
| 145 |
+
Adelie,Dream,40.7,17,190,3725,male,2009
|
| 146 |
+
Adelie,Dream,37.3,16.8,192,3000,female,2009
|
| 147 |
+
Adelie,Dream,39,18.7,185,3650,male,2009
|
| 148 |
+
Adelie,Dream,39.2,18.6,190,4250,male,2009
|
| 149 |
+
Adelie,Dream,36.6,18.4,184,3475,female,2009
|
| 150 |
+
Adelie,Dream,36,17.8,195,3450,female,2009
|
| 151 |
+
Adelie,Dream,37.8,18.1,193,3750,male,2009
|
| 152 |
+
Adelie,Dream,36,17.1,187,3700,female,2009
|
| 153 |
+
Adelie,Dream,41.5,18.5,201,4000,male,2009
|
| 154 |
+
Gentoo,Biscoe,46.1,13.2,211,4500,female,2007
|
| 155 |
+
Gentoo,Biscoe,50,16.3,230,5700,male,2007
|
| 156 |
+
Gentoo,Biscoe,48.7,14.1,210,4450,female,2007
|
| 157 |
+
Gentoo,Biscoe,50,15.2,218,5700,male,2007
|
| 158 |
+
Gentoo,Biscoe,47.6,14.5,215,5400,male,2007
|
| 159 |
+
Gentoo,Biscoe,46.5,13.5,210,4550,female,2007
|
| 160 |
+
Gentoo,Biscoe,45.4,14.6,211,4800,female,2007
|
| 161 |
+
Gentoo,Biscoe,46.7,15.3,219,5200,male,2007
|
| 162 |
+
Gentoo,Biscoe,43.3,13.4,209,4400,female,2007
|
| 163 |
+
Gentoo,Biscoe,46.8,15.4,215,5150,male,2007
|
| 164 |
+
Gentoo,Biscoe,40.9,13.7,214,4650,female,2007
|
| 165 |
+
Gentoo,Biscoe,49,16.1,216,5550,male,2007
|
| 166 |
+
Gentoo,Biscoe,45.5,13.7,214,4650,female,2007
|
| 167 |
+
Gentoo,Biscoe,48.4,14.6,213,5850,male,2007
|
| 168 |
+
Gentoo,Biscoe,45.8,14.6,210,4200,female,2007
|
| 169 |
+
Gentoo,Biscoe,49.3,15.7,217,5850,male,2007
|
| 170 |
+
Gentoo,Biscoe,42,13.5,210,4150,female,2007
|
| 171 |
+
Gentoo,Biscoe,49.2,15.2,221,6300,male,2007
|
| 172 |
+
Gentoo,Biscoe,46.2,14.5,209,4800,female,2007
|
| 173 |
+
Gentoo,Biscoe,48.7,15.1,222,5350,male,2007
|
| 174 |
+
Gentoo,Biscoe,50.2,14.3,218,5700,male,2007
|
| 175 |
+
Gentoo,Biscoe,45.1,14.5,215,5000,female,2007
|
| 176 |
+
Gentoo,Biscoe,46.5,14.5,213,4400,female,2007
|
| 177 |
+
Gentoo,Biscoe,46.3,15.8,215,5050,male,2007
|
| 178 |
+
Gentoo,Biscoe,42.9,13.1,215,5000,female,2007
|
| 179 |
+
Gentoo,Biscoe,46.1,15.1,215,5100,male,2007
|
| 180 |
+
Gentoo,Biscoe,44.5,14.3,216,4100,NA,2007
|
| 181 |
+
Gentoo,Biscoe,47.8,15,215,5650,male,2007
|
| 182 |
+
Gentoo,Biscoe,48.2,14.3,210,4600,female,2007
|
| 183 |
+
Gentoo,Biscoe,50,15.3,220,5550,male,2007
|
| 184 |
+
Gentoo,Biscoe,47.3,15.3,222,5250,male,2007
|
| 185 |
+
Gentoo,Biscoe,42.8,14.2,209,4700,female,2007
|
| 186 |
+
Gentoo,Biscoe,45.1,14.5,207,5050,female,2007
|
| 187 |
+
Gentoo,Biscoe,59.6,17,230,6050,male,2007
|
| 188 |
+
Gentoo,Biscoe,49.1,14.8,220,5150,female,2008
|
| 189 |
+
Gentoo,Biscoe,48.4,16.3,220,5400,male,2008
|
| 190 |
+
Gentoo,Biscoe,42.6,13.7,213,4950,female,2008
|
| 191 |
+
Gentoo,Biscoe,44.4,17.3,219,5250,male,2008
|
| 192 |
+
Gentoo,Biscoe,44,13.6,208,4350,female,2008
|
| 193 |
+
Gentoo,Biscoe,48.7,15.7,208,5350,male,2008
|
| 194 |
+
Gentoo,Biscoe,42.7,13.7,208,3950,female,2008
|
| 195 |
+
Gentoo,Biscoe,49.6,16,225,5700,male,2008
|
| 196 |
+
Gentoo,Biscoe,45.3,13.7,210,4300,female,2008
|
| 197 |
+
Gentoo,Biscoe,49.6,15,216,4750,male,2008
|
| 198 |
+
Gentoo,Biscoe,50.5,15.9,222,5550,male,2008
|
| 199 |
+
Gentoo,Biscoe,43.6,13.9,217,4900,female,2008
|
| 200 |
+
Gentoo,Biscoe,45.5,13.9,210,4200,female,2008
|
| 201 |
+
Gentoo,Biscoe,50.5,15.9,225,5400,male,2008
|
| 202 |
+
Gentoo,Biscoe,44.9,13.3,213,5100,female,2008
|
| 203 |
+
Gentoo,Biscoe,45.2,15.8,215,5300,male,2008
|
| 204 |
+
Gentoo,Biscoe,46.6,14.2,210,4850,female,2008
|
| 205 |
+
Gentoo,Biscoe,48.5,14.1,220,5300,male,2008
|
| 206 |
+
Gentoo,Biscoe,45.1,14.4,210,4400,female,2008
|
| 207 |
+
Gentoo,Biscoe,50.1,15,225,5000,male,2008
|
| 208 |
+
Gentoo,Biscoe,46.5,14.4,217,4900,female,2008
|
| 209 |
+
Gentoo,Biscoe,45,15.4,220,5050,male,2008
|
| 210 |
+
Gentoo,Biscoe,43.8,13.9,208,4300,female,2008
|
| 211 |
+
Gentoo,Biscoe,45.5,15,220,5000,male,2008
|
| 212 |
+
Gentoo,Biscoe,43.2,14.5,208,4450,female,2008
|
| 213 |
+
Gentoo,Biscoe,50.4,15.3,224,5550,male,2008
|
| 214 |
+
Gentoo,Biscoe,45.3,13.8,208,4200,female,2008
|
| 215 |
+
Gentoo,Biscoe,46.2,14.9,221,5300,male,2008
|
| 216 |
+
Gentoo,Biscoe,45.7,13.9,214,4400,female,2008
|
| 217 |
+
Gentoo,Biscoe,54.3,15.7,231,5650,male,2008
|
| 218 |
+
Gentoo,Biscoe,45.8,14.2,219,4700,female,2008
|
| 219 |
+
Gentoo,Biscoe,49.8,16.8,230,5700,male,2008
|
| 220 |
+
Gentoo,Biscoe,46.2,14.4,214,4650,NA,2008
|
| 221 |
+
Gentoo,Biscoe,49.5,16.2,229,5800,male,2008
|
| 222 |
+
Gentoo,Biscoe,43.5,14.2,220,4700,female,2008
|
| 223 |
+
Gentoo,Biscoe,50.7,15,223,5550,male,2008
|
| 224 |
+
Gentoo,Biscoe,47.7,15,216,4750,female,2008
|
| 225 |
+
Gentoo,Biscoe,46.4,15.6,221,5000,male,2008
|
| 226 |
+
Gentoo,Biscoe,48.2,15.6,221,5100,male,2008
|
| 227 |
+
Gentoo,Biscoe,46.5,14.8,217,5200,female,2008
|
| 228 |
+
Gentoo,Biscoe,46.4,15,216,4700,female,2008
|
| 229 |
+
Gentoo,Biscoe,48.6,16,230,5800,male,2008
|
| 230 |
+
Gentoo,Biscoe,47.5,14.2,209,4600,female,2008
|
| 231 |
+
Gentoo,Biscoe,51.1,16.3,220,6000,male,2008
|
| 232 |
+
Gentoo,Biscoe,45.2,13.8,215,4750,female,2008
|
| 233 |
+
Gentoo,Biscoe,45.2,16.4,223,5950,male,2008
|
| 234 |
+
Gentoo,Biscoe,49.1,14.5,212,4625,female,2009
|
| 235 |
+
Gentoo,Biscoe,52.5,15.6,221,5450,male,2009
|
| 236 |
+
Gentoo,Biscoe,47.4,14.6,212,4725,female,2009
|
| 237 |
+
Gentoo,Biscoe,50,15.9,224,5350,male,2009
|
| 238 |
+
Gentoo,Biscoe,44.9,13.8,212,4750,female,2009
|
| 239 |
+
Gentoo,Biscoe,50.8,17.3,228,5600,male,2009
|
| 240 |
+
Gentoo,Biscoe,43.4,14.4,218,4600,female,2009
|
| 241 |
+
Gentoo,Biscoe,51.3,14.2,218,5300,male,2009
|
| 242 |
+
Gentoo,Biscoe,47.5,14,212,4875,female,2009
|
| 243 |
+
Gentoo,Biscoe,52.1,17,230,5550,male,2009
|
| 244 |
+
Gentoo,Biscoe,47.5,15,218,4950,female,2009
|
| 245 |
+
Gentoo,Biscoe,52.2,17.1,228,5400,male,2009
|
| 246 |
+
Gentoo,Biscoe,45.5,14.5,212,4750,female,2009
|
| 247 |
+
Gentoo,Biscoe,49.5,16.1,224,5650,male,2009
|
| 248 |
+
Gentoo,Biscoe,44.5,14.7,214,4850,female,2009
|
| 249 |
+
Gentoo,Biscoe,50.8,15.7,226,5200,male,2009
|
| 250 |
+
Gentoo,Biscoe,49.4,15.8,216,4925,male,2009
|
| 251 |
+
Gentoo,Biscoe,46.9,14.6,222,4875,female,2009
|
| 252 |
+
Gentoo,Biscoe,48.4,14.4,203,4625,female,2009
|
| 253 |
+
Gentoo,Biscoe,51.1,16.5,225,5250,male,2009
|
| 254 |
+
Gentoo,Biscoe,48.5,15,219,4850,female,2009
|
| 255 |
+
Gentoo,Biscoe,55.9,17,228,5600,male,2009
|
| 256 |
+
Gentoo,Biscoe,47.2,15.5,215,4975,female,2009
|
| 257 |
+
Gentoo,Biscoe,49.1,15,228,5500,male,2009
|
| 258 |
+
Gentoo,Biscoe,47.3,13.8,216,4725,NA,2009
|
| 259 |
+
Gentoo,Biscoe,46.8,16.1,215,5500,male,2009
|
| 260 |
+
Gentoo,Biscoe,41.7,14.7,210,4700,female,2009
|
| 261 |
+
Gentoo,Biscoe,53.4,15.8,219,5500,male,2009
|
| 262 |
+
Gentoo,Biscoe,43.3,14,208,4575,female,2009
|
| 263 |
+
Gentoo,Biscoe,48.1,15.1,209,5500,male,2009
|
| 264 |
+
Gentoo,Biscoe,50.5,15.2,216,5000,female,2009
|
| 265 |
+
Gentoo,Biscoe,49.8,15.9,229,5950,male,2009
|
| 266 |
+
Gentoo,Biscoe,43.5,15.2,213,4650,female,2009
|
| 267 |
+
Gentoo,Biscoe,51.5,16.3,230,5500,male,2009
|
| 268 |
+
Gentoo,Biscoe,46.2,14.1,217,4375,female,2009
|
| 269 |
+
Gentoo,Biscoe,55.1,16,230,5850,male,2009
|
| 270 |
+
Gentoo,Biscoe,44.5,15.7,217,4875,NA,2009
|
| 271 |
+
Gentoo,Biscoe,48.8,16.2,222,6000,male,2009
|
| 272 |
+
Gentoo,Biscoe,47.2,13.7,214,4925,female,2009
|
| 273 |
+
Gentoo,Biscoe,NA,NA,NA,NA,NA,2009
|
| 274 |
+
Gentoo,Biscoe,46.8,14.3,215,4850,female,2009
|
| 275 |
+
Gentoo,Biscoe,50.4,15.7,222,5750,male,2009
|
| 276 |
+
Gentoo,Biscoe,45.2,14.8,212,5200,female,2009
|
| 277 |
+
Gentoo,Biscoe,49.9,16.1,213,5400,male,2009
|
| 278 |
+
Chinstrap,Dream,46.5,17.9,192,3500,female,2007
|
| 279 |
+
Chinstrap,Dream,50,19.5,196,3900,male,2007
|
| 280 |
+
Chinstrap,Dream,51.3,19.2,193,3650,male,2007
|
| 281 |
+
Chinstrap,Dream,45.4,18.7,188,3525,female,2007
|
| 282 |
+
Chinstrap,Dream,52.7,19.8,197,3725,male,2007
|
| 283 |
+
Chinstrap,Dream,45.2,17.8,198,3950,female,2007
|
| 284 |
+
Chinstrap,Dream,46.1,18.2,178,3250,female,2007
|
| 285 |
+
Chinstrap,Dream,51.3,18.2,197,3750,male,2007
|
| 286 |
+
Chinstrap,Dream,46,18.9,195,4150,female,2007
|
| 287 |
+
Chinstrap,Dream,51.3,19.9,198,3700,male,2007
|
| 288 |
+
Chinstrap,Dream,46.6,17.8,193,3800,female,2007
|
| 289 |
+
Chinstrap,Dream,51.7,20.3,194,3775,male,2007
|
| 290 |
+
Chinstrap,Dream,47,17.3,185,3700,female,2007
|
| 291 |
+
Chinstrap,Dream,52,18.1,201,4050,male,2007
|
| 292 |
+
Chinstrap,Dream,45.9,17.1,190,3575,female,2007
|
| 293 |
+
Chinstrap,Dream,50.5,19.6,201,4050,male,2007
|
| 294 |
+
Chinstrap,Dream,50.3,20,197,3300,male,2007
|
| 295 |
+
Chinstrap,Dream,58,17.8,181,3700,female,2007
|
| 296 |
+
Chinstrap,Dream,46.4,18.6,190,3450,female,2007
|
| 297 |
+
Chinstrap,Dream,49.2,18.2,195,4400,male,2007
|
| 298 |
+
Chinstrap,Dream,42.4,17.3,181,3600,female,2007
|
| 299 |
+
Chinstrap,Dream,48.5,17.5,191,3400,male,2007
|
| 300 |
+
Chinstrap,Dream,43.2,16.6,187,2900,female,2007
|
| 301 |
+
Chinstrap,Dream,50.6,19.4,193,3800,male,2007
|
| 302 |
+
Chinstrap,Dream,46.7,17.9,195,3300,female,2007
|
| 303 |
+
Chinstrap,Dream,52,19,197,4150,male,2007
|
| 304 |
+
Chinstrap,Dream,50.5,18.4,200,3400,female,2008
|
| 305 |
+
Chinstrap,Dream,49.5,19,200,3800,male,2008
|
| 306 |
+
Chinstrap,Dream,46.4,17.8,191,3700,female,2008
|
| 307 |
+
Chinstrap,Dream,52.8,20,205,4550,male,2008
|
| 308 |
+
Chinstrap,Dream,40.9,16.6,187,3200,female,2008
|
| 309 |
+
Chinstrap,Dream,54.2,20.8,201,4300,male,2008
|
| 310 |
+
Chinstrap,Dream,42.5,16.7,187,3350,female,2008
|
| 311 |
+
Chinstrap,Dream,51,18.8,203,4100,male,2008
|
| 312 |
+
Chinstrap,Dream,49.7,18.6,195,3600,male,2008
|
| 313 |
+
Chinstrap,Dream,47.5,16.8,199,3900,female,2008
|
| 314 |
+
Chinstrap,Dream,47.6,18.3,195,3850,female,2008
|
| 315 |
+
Chinstrap,Dream,52,20.7,210,4800,male,2008
|
| 316 |
+
Chinstrap,Dream,46.9,16.6,192,2700,female,2008
|
| 317 |
+
Chinstrap,Dream,53.5,19.9,205,4500,male,2008
|
| 318 |
+
Chinstrap,Dream,49,19.5,210,3950,male,2008
|
| 319 |
+
Chinstrap,Dream,46.2,17.5,187,3650,female,2008
|
| 320 |
+
Chinstrap,Dream,50.9,19.1,196,3550,male,2008
|
| 321 |
+
Chinstrap,Dream,45.5,17,196,3500,female,2008
|
| 322 |
+
Chinstrap,Dream,50.9,17.9,196,3675,female,2009
|
| 323 |
+
Chinstrap,Dream,50.8,18.5,201,4450,male,2009
|
| 324 |
+
Chinstrap,Dream,50.1,17.9,190,3400,female,2009
|
| 325 |
+
Chinstrap,Dream,49,19.6,212,4300,male,2009
|
| 326 |
+
Chinstrap,Dream,51.5,18.7,187,3250,male,2009
|
| 327 |
+
Chinstrap,Dream,49.8,17.3,198,3675,female,2009
|
| 328 |
+
Chinstrap,Dream,48.1,16.4,199,3325,female,2009
|
| 329 |
+
Chinstrap,Dream,51.4,19,201,3950,male,2009
|
| 330 |
+
Chinstrap,Dream,45.7,17.3,193,3600,female,2009
|
| 331 |
+
Chinstrap,Dream,50.7,19.7,203,4050,male,2009
|
| 332 |
+
Chinstrap,Dream,42.5,17.3,187,3350,female,2009
|
| 333 |
+
Chinstrap,Dream,52.2,18.8,197,3450,male,2009
|
| 334 |
+
Chinstrap,Dream,45.2,16.6,191,3250,female,2009
|
| 335 |
+
Chinstrap,Dream,49.3,19.9,203,4050,male,2009
|
| 336 |
+
Chinstrap,Dream,50.2,18.8,202,3800,male,2009
|
| 337 |
+
Chinstrap,Dream,45.6,19.4,194,3525,female,2009
|
| 338 |
+
Chinstrap,Dream,51.9,19.5,206,3950,male,2009
|
| 339 |
+
Chinstrap,Dream,46.8,16.5,189,3650,female,2009
|
| 340 |
+
Chinstrap,Dream,45.7,17,195,3650,female,2009
|
| 341 |
+
Chinstrap,Dream,55.8,19.8,207,4000,male,2009
|
| 342 |
+
Chinstrap,Dream,43.5,18.1,202,3400,female,2009
|
| 343 |
+
Chinstrap,Dream,49.6,18.2,193,3775,male,2009
|
| 344 |
+
Chinstrap,Dream,50.8,19,210,4100,male,2009
|
| 345 |
+
Chinstrap,Dream,50.2,18.7,198,3775,female,2009
|
rsconnect/shinyapps.io/diego-ellis-soto/RSF_Biodiversity_Access.dcf
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
name: RSF_Biodiversity_Access
|
| 2 |
-
title: SF_biodiv_access_shiny
|
| 3 |
-
username: diego-ellis-soto
|
| 4 |
-
account: diego-ellis-soto
|
| 5 |
-
server: shinyapps.io
|
| 6 |
-
hostUrl: https://api.shinyapps.io/v1
|
| 7 |
-
appId: 13693040
|
| 8 |
-
bundleId: 9634358
|
| 9 |
-
url: https://diego-ellis-soto.shinyapps.io/RSF_Biodiversity_Access/
|
| 10 |
-
version: 1
|
| 11 |
-
asMultiple: FALSE
|
| 12 |
-
asStatic: FALSE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rsconnect/shinyapps.io/diego-ellis-soto/SF_biodiv_access.dcf
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
name: SF_biodiv_access
|
| 2 |
-
title: SF_biodiv_access
|
| 3 |
-
username: diego-ellis-soto
|
| 4 |
-
account: diego-ellis-soto
|
| 5 |
-
server: shinyapps.io
|
| 6 |
-
hostUrl: https://api.shinyapps.io/v1
|
| 7 |
-
appId: 13693447
|
| 8 |
-
bundleId: 9631093
|
| 9 |
-
url: https://diego-ellis-soto.shinyapps.io/SF_biodiv_access/
|
| 10 |
-
version: 1
|
| 11 |
-
asMultiple: FALSE
|
| 12 |
-
asStatic: FALSE
|
| 13 |
-
ignoredFiles: app_old.R
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
www/app_pastel.css
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
/* SF Biodiversity Access — pastel chrome (shinydashboard overrides) */
|
| 2 |
-
|
| 3 |
-
.content-wrapper,
|
| 4 |
-
.right-side {
|
| 5 |
-
background-color: #f3f8f5 !important;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
.box {
|
| 9 |
-
border-radius: 12px;
|
| 10 |
-
overflow: hidden;
|
| 11 |
-
border: 1px solid rgba(61, 92, 74, 0.1);
|
| 12 |
-
box-shadow: 0 2px 14px rgba(45, 74, 58, 0.07);
|
| 13 |
-
margin-bottom: 20px;
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
.box .box-header {
|
| 17 |
-
font-weight: 600;
|
| 18 |
-
letter-spacing: 0.01em;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
.box.box-success > .box-header {
|
| 22 |
-
background: #c8e9d6 !important;
|
| 23 |
-
color: #1a4534 !important;
|
| 24 |
-
border-bottom: 1px solid rgba(26, 69, 52, 0.12);
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
.box.box-primary > .box-header {
|
| 28 |
-
background: #c9dcf5 !important;
|
| 29 |
-
color: #1f3d66 !important;
|
| 30 |
-
border-bottom: 1px solid rgba(31, 61, 102, 0.12);
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
.box.box-warning > .box-header {
|
| 34 |
-
background: #fce9c9 !important;
|
| 35 |
-
color: #5a4518 !important;
|
| 36 |
-
border-bottom: 1px solid rgba(90, 69, 24, 0.12);
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
.box.box-info > .box-header {
|
| 40 |
-
background: #c9ecf3 !important;
|
| 41 |
-
color: #1a4a55 !important;
|
| 42 |
-
border-bottom: 1px solid rgba(26, 74, 85, 0.12);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
.box.box-danger > .box-header {
|
| 46 |
-
background: #f5d4d4 !important;
|
| 47 |
-
color: #5c2222 !important;
|
| 48 |
-
border-bottom: 1px solid rgba(92, 34, 34, 0.12);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
.box-body {
|
| 52 |
-
background-color: #fcfdfc;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
.well {
|
| 56 |
-
background-color: #f7faf8 !important;
|
| 57 |
-
border: 1px solid #e3ece6 !important;
|
| 58 |
-
border-radius: 10px !important;
|
| 59 |
-
box-shadow: none !important;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/* Main title bar: strong contrast; title uses Roboto (matches body), not Roboto Slab */
|
| 63 |
-
.skin-green .main-header {
|
| 64 |
-
border-bottom: 2px solid rgba(25, 55, 42, 0.35);
|
| 65 |
-
box-shadow: 0 3px 14px rgba(25, 50, 38, 0.18);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.skin-green .main-header .logo {
|
| 69 |
-
background: linear-gradient(135deg, #5a8568 0%, #3d6b52 100%) !important;
|
| 70 |
-
color: #f7fffa !important;
|
| 71 |
-
border-right: 1px solid rgba(0, 0, 0, 0.12);
|
| 72 |
-
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
|
| 73 |
-
font-weight: 700 !important;
|
| 74 |
-
font-size: 1.5rem !important;
|
| 75 |
-
letter-spacing: 0.02em !important;
|
| 76 |
-
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
.skin-green .main-header .navbar {
|
| 80 |
-
background: linear-gradient(135deg, #5a8568 0%, #3d6b52 100%) !important;
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
.skin-green .main-header .navbar .sidebar-toggle {
|
| 84 |
-
color: #f7fffa !important;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
.skin-green .main-header .navbar .sidebar-toggle:hover {
|
| 88 |
-
background-color: rgba(255, 255, 255, 0.14) !important;
|
| 89 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|