.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
- .positai
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 (includes GDAL/PROJ/GEOS stack used by sf, terra)
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
- # Hugging Face Spaces expects port 7860
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
- <img src="www/Combined_logos.png" width="60%">
13
 
14
- An interactive Shiny app built for the **Reimagining San Francisco (RSF) Initiative** to explore how equitably San Francisco residents can access urban biodiversity across different transportation modes and socioeconomic contexts.
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
- ## Transportation Modes
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
- | Dimension | Variable | Direction |
82
- |-----------|----------|-----------|
83
- | Mobility Access | Transit stops / km² | Higher = better |
84
- | Route Access | Unique Muni routes crossing isochrone | Higher = better |
85
- | Biodiversity Potential | Unique GBIF species | Higher = better |
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
- All axes scaled 0–1. BAI = unweighted mean of all seven standardized components.
92
 
93
  ---
94
 
95
- ## Repository Structure
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
- ## Cyberinfrastructure & Data Sources
118
 
119
- | Dataset | Source | Format | How Loaded |
120
- |---------|--------|--------|-----------|
121
- | GBIF occurrences (SF) | Global Biodiversity Information Facility | Parquet | DuckDB spatial queries in server |
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
- **Remote data** is hosted on HuggingFace at
134
- [`boettiger-lab/sf_biodiv_access`](https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access) and cached locally in `data/cached/` by `setup_unified.R`. The two most expensive products the CBG × greenspace intersection and the transit-routing timetable — are precomputed in `Rscripts/prep/` and read at startup rather than recomputed, so the app starts quickly.
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
- ### First-Time Setup
156
 
157
- **Step 1: Build GBIF parquets** (one-time; needs local `gbif.duckdb` path in script)
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
- **Step 2: Pre-compute GTFS timetable and greenspace / RSF rasters** (one-time, slow)
164
- ```r
165
- source("Rscripts/prep/run_all_prep.R")
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
- ## Cloud Deployment (HuggingFace Spaces)
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
- See `Dockerfile` for the deployment configuration.
 
 
 
 
184
 
185
- ---
186
-
187
- ## Authors
188
 
189
- **Diego Ellis Soto**, Avery Hill, Christopher J. Schell, Carl Boettiger, Rebecca Johnson
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
- ## Status & Roadmap
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
- **Planned additions:**
200
- - Impervious surface coverage
201
- - National Walkability Index
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="80">
 
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
+ ![Screenshot of the App](www/app_screenshot_1.png)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ![Combined Logos](www/Combined_logos.png)
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
- options(repos = c(CRAN = "https://cloud.r-project.org"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- }