3v324v23 commited on
Commit
82e6525
·
1 Parent(s): 2c6df64
.gitattributes CHANGED
@@ -32,3 +32,4 @@ saved_model/**/* 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
 
 
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
.gitignore CHANGED
@@ -1 +1,3 @@
1
  .DS_Store
 
 
 
1
  .DS_Store
2
+ .Rproj.user
3
+ rsconnect
Dockerfile CHANGED
@@ -1,14 +1,13 @@
1
- FROM rocker/r-base:latest
2
 
3
- WORKDIR /code
4
 
5
- RUN install2.r --error \
6
- shiny \
7
- dplyr \
8
- ggplot2 \
9
- readr \
10
- ggExtra
11
-
12
  COPY . .
 
13
 
14
  CMD ["R", "--quiet", "-e", "shiny::runApp(host='0.0.0.0', port=7860)"]
 
1
+ FROM quay.io/jupyter/minimal-notebook:ubuntu-24.04
2
 
3
+ USER root
4
 
5
+ # R & RStudio
6
+ RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_r.sh | bash
7
+ RUN curl -s https://raw.githubusercontent.com/boettiger-lab/repo2docker-r/refs/heads/main/install_rstudio.sh | bash
8
+
9
+ WORKDIR /code
 
 
10
  COPY . .
11
+ RUN Rscript install.r
12
 
13
  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("[email protected]"))
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("[email protected]")),
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("[email protected]"))
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,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setup
2
+ require(shinyjs)
3
+ library(shiny)
4
+ library(shinydashboard)
5
+ library(leaflet)
6
+ library(mapboxapi)
7
+ library(tidyverse)
8
+ library(tidycensus)
9
+ library(sf)
10
+ library(DT)
11
+ library(RColorBrewer)
12
+ library(terra)
13
+ library(data.table)
14
+ library(mapview)
15
+ library(sjPlot)
16
+ library(sjlabelled)
17
+ library(bslib)
18
+ library(shinycssloaders)
19
+
20
+ # ------------------------------------------------
21
+ # 1) API Keys
22
+ # ------------------------------------------------
23
+ mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
24
+ # mb_access_token(mapbox_token, install = FALSE)
25
+
26
+ # ------------------------------------------------
27
+ # 2) Load Data
28
+ # ------------------------------------------------
29
+ # -- Greenspace
30
+ getwd()
31
+ osm_greenspace <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/greenspaces_osm_nad83.shp", quiet = TRUE) %>%
32
+
33
+ st_transform(4326)
34
+ if (!"name" %in% names(osm_greenspace)) {
35
+ osm_greenspace$name <- "Unnamed Greenspace"
36
+ }
37
+
38
+ # -- NDVI Raster
39
+ ndvi <- terra::rast("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/SF_EastBay_NDVI_Sentinel_10.tif")
40
+
41
+
42
+ # -- GBIF data
43
+ # Load what is basically inter_gbif !!!!!
44
+ # load("data/sf_gbif.Rdata") # => sf_gbif
45
+
46
+ download.file('https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/gbif_census_ndvi_anno.Rdata', '/tmp/gbif_census_ndvi_anno.Rdata')
47
+ load('/tmp/gbif_census_ndvi_anno.Rdata')
48
+ vect_gbif <- vect(sf_gbif)
49
+ # -- Precomputed CBG data
50
+ download.file('https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/cbg_vect_sf.Rdata', '/tmp/cbg_vect_sf.Rdata')
51
+ load('/tmp/cbg_vect_sf.Rdata')
52
+
53
+ if (!"unique_species" %in% names(cbg_vect_sf)) {
54
+ cbg_vect_sf$unique_species <- cbg_vect_sf$n_species
55
+ }
56
+ if (!"n_observations" %in% names(cbg_vect_sf)) {
57
+ cbg_vect_sf$n_observations <- cbg_vect_sf$n
58
+ }
59
+ if (!"median_inc" %in% names(cbg_vect_sf)) {
60
+ cbg_vect_sf$median_inc <- cbg_vect_sf$medincE
61
+ }
62
+ if (!"ndvi_mean" %in% names(cbg_vect_sf)) {
63
+ cbg_vect_sf$ndvi_mean <- cbg_vect_sf$ndvi_sentinel
64
+ }
65
+
66
+ # -- Hotspots/Coldspots
67
+ biodiv_hotspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/hotspots.shp", quiet = TRUE) %>% st_transform(4326)
68
+ biodiv_coldspots <- st_read("/vsicurl/https://huggingface.co/datasets/boettiger-lab/sf_biodiv_access/resolve/main/coldspots.shp", quiet = TRUE) %>% st_transform(4326)
69
+
70
+
71
+
72
+ #
73
+ # # Community Organizations shapefile
74
+ # # For now simulate
75
+ #
76
+ # # Define San Francisco bounding box coordinates
77
+ # sf_bbox <- st_bbox(c(
78
+ # xmin = -122.5247, # Western longitude
79
+ # ymin = 37.7045, # Southern latitude
80
+ # xmax = -122.3569, # Eastern longitude
81
+ # ymax = 37.8334 # Northern latitude
82
+ # ), crs = st_crs(4326)) # WGS84 CRS
83
+ #
84
+ # # Convert bounding box to polygon
85
+ # sf_boundary <- st_as_sfc(sf_bbox) %>% st_make_valid()
86
+ #
87
+ # # Transform boundary to projected CRS for accurate buffering (EPSG:3310)
88
+ # sf_boundary_proj <- st_transform(sf_boundary, 3310)
89
+ #
90
+ # # Set seed for reproducibility
91
+ # set.seed(123)
92
+ #
93
+ # # Simulate 20 random points within San Francisco boundary
94
+ # community_points <- st_sample(sf_boundary_proj, size = 20, type = "random")
95
+ #
96
+ # # Convert to sf object with POINT geometry and assign unique names
97
+ # community_points_sf <- st_sf(
98
+ # NAME = paste("Community Org", 1:20),
99
+ # geometry = community_points
100
+ # )
101
+ # # Select first 3 points to buffer
102
+ # buffered_points_sf <- community_points_sf[1:3, ] %>%
103
+ # st_buffer(dist = 100) # Buffer distance in meters
104
+ #
105
+ # # Update the NAME column to indicate buffered areas
106
+ # buffered_points_sf$NAME <- paste(buffered_points_sf$NAME, "Area")
107
+ # community_points_sf <- st_transform(community_points_sf, 4326)
108
+ # buffered_points_sf <- st_transform(buffered_points_sf, 4326)
109
+ #
110
+ # # Combine points and polygons into one sf object
111
+ # community_orgs <- bind_rows(
112
+ # community_points_sf,
113
+ # buffered_points_sf
114
+ # )
115
+ #
116
+ # # View the combined dataset
117
+ # print(community_orgs)
118
+ #
119
+ # community_points_only <- community_orgs %>% filter(st_geometry_type(geometry) == "POINT")
120
+ # community_polygons_only <- community_orgs %>% filter(st_geometry_type(geometry) == "POLYGON")
121
+ #
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Reimagining SF Shiny App
3
  emoji: 📚
4
  colorFrom: blue
5
  colorTo: yellow
@@ -7,4 +7,72 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Sf Biodiv Access Shiny
3
  emoji: 📚
4
  colorFrom: blue
5
  colorTo: yellow
 
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: [email protected]
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">
app.R CHANGED
@@ -1,58 +1,1157 @@
 
 
 
 
 
 
 
1
  library(shiny)
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  library(bslib)
3
- library(dplyr)
4
- library(ggplot2)
5
-
6
- df <- readr::read_csv("penguins.csv")
7
- # Find subset of columns that are suitable for scatter plot
8
- df_num <- df |> select(where(is.numeric), -Year)
9
-
10
- ui <- page_sidebar(
11
- theme = bs_theme(bootswatch = "minty"),
12
- title = "Penguins explorer",
13
- sidebar = sidebar(
14
- varSelectInput("xvar", "X variable", df_num, selected = "Bill Length (mm)"),
15
- varSelectInput("yvar", "Y variable", df_num, selected = "Bill Depth (mm)"),
16
- checkboxGroupInput("species", "Filter by species",
17
- choices = unique(df$Species), selected = unique(df$Species)
18
- ),
19
- hr(), # Add a horizontal rule
20
- checkboxInput("by_species", "Show species", TRUE),
21
- checkboxInput("show_margins", "Show marginal plots", TRUE),
22
- checkboxInput("smooth", "Add smoother"),
 
 
 
 
 
 
 
 
 
23
  ),
24
- plotOutput("scatter")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
 
 
 
 
27
  server <- function(input, output, session) {
28
- subsetted <- reactive({
29
- req(input$species)
30
- df |> filter(Species %in% input$species)
31
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- output$scatter <- renderPlot(
34
- {
35
- p <- ggplot(subsetted(), aes(!!input$xvar, !!input$yvar)) +
36
- theme_light() +
37
- list(
38
- theme(legend.position = "bottom"),
39
- if (input$by_species) aes(color = Species),
40
- geom_point(),
41
- if (input$smooth) geom_smooth()
42
- )
43
 
44
- if (input$show_margins) {
45
- margin_type <- if (input$by_species) "density" else "histogram"
46
- p <- p |> ggExtra::ggMarginal(
47
- type = margin_type, margins = "both",
48
- size = 8, groupColour = input$by_species, groupFill = input$by_species
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  )
 
 
 
50
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- p
53
- },
54
- res = 100
55
- )
56
  }
57
 
 
 
 
58
  shinyApp(ui, server)
 
 
 
 
 
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(shinydashboard)
10
+ library(leaflet)
11
+ library(mapboxapi)
12
+ library(tidyverse)
13
+ library(tidycensus)
14
+ library(sf)
15
+ library(DT)
16
+ library(RColorBrewer)
17
+ library(terra)
18
+ library(data.table)
19
+ library(mapview)
20
+ library(sjPlot)
21
+ library(sjlabelled)
22
  library(bslib)
23
+ library(shinycssloaders)
24
+
25
+ source('R/setup.R') # Load necessary data (annotated gbif, annotated cbg, ndvi)
26
+
27
+ # Define your Mapbox token securely
28
+ mapbox_token <- "pk.eyJ1Ijoia3dhbGtlcnRjdSIsImEiOiJjbHc3NmI0cDMxYzhyMmt0OXBiYnltMjVtIn0.Thtu6WqIhOfin6AykskM2g"
29
+
30
+ # Global theme definition using a green-themed bootswatch
31
+ theme <- bs_theme(
32
+ bootswatch = "minty", # 'minty' is a light green-themed bootswatch
33
+ base_font = font_google("Roboto"),
34
+ heading_font = font_google("Roboto Slab"),
35
+ bg = "#f0fff0", # Honeydew background
36
+ fg = "#2e8b57" # SeaGreen foreground
37
+ )
38
+
39
+ # UI
40
+ ui <- dashboardPage(
41
+ skin = "green", # shinydashboard skin color
42
+ dashboardHeader(title = "SF Biodiversity Access Tool"
43
+ ),
44
+
45
+ dashboardSidebar(
46
+ sidebarMenu(
47
+ menuItem("Isochrone Explorer", tabName = "isochrone", icon = icon("map-marker-alt")),
48
+ menuItem("GBIF Summaries", tabName = "gbif", icon = icon("table")),
49
+ menuItem("Community Science", tabName = "community_science", icon = icon("users")),
50
+ menuItem("About", tabName = "about", icon = icon("info-circle"))
51
+ )
52
  ),
53
+ dashboardBody(
54
+ theme = theme, # Apply the custom theme
55
+ useShinyjs(),
56
+ # Loading message
57
+ div(id = "loading", style = "display:none; font-size: 20px; color: red;", "Calculating..."),
58
+
59
+ # fluidRow(
60
+ # column(
61
+ # width = 2,
62
+ # imageOutput("Combined_logos")
63
+ # )
64
+ # ),
65
+
66
+ # fluidPage(
67
+ # box(
68
+ # tags$img(height = 100, width = 100,src = "Combined_logos.png"),
69
+ # imageOutput('Combined_logos')
70
+ # )
71
+ # ),
72
+
73
+
74
+ # fluidRow(
75
+ # column(
76
+ # width = 2,
77
+ # imageOutput("uc_berkeley_logo")
78
+ # ),
79
+ # column(
80
+ # width = 4,
81
+ # imageOutput("california_academy_logo")
82
+ # ),
83
+ # column(
84
+ # width = 6,
85
+ # imageOutput("reimagining_sf_logo")
86
+ # )
87
+ # ),
88
+ # fluidPage(
89
+ # # Application title
90
+ # # titlePanel("Test app"),
91
+ # # to render images in the www folder
92
+ # box(uiOutput("houz"), width = 3)
93
+ # ),
94
+
95
+ #
96
+ # fluidRow(
97
+ # column(
98
+ # width = 12, align = "center",
99
+ # tags$img(src = "UC_Berkeley_logo.png",
100
+ # height = "200px", style = "margin:10px;", alt = "UC Berkeley Logo"),
101
+ # tags$img(src = "California_academy_logo.png",
102
+ # height = "200px", style = "margin:10px;", alt = "California Academy Logo"),
103
+ # tags$img(src = "Reimagining_San_Francisco.png",
104
+ # height = "200px", style = "margin:10px;", alt = "Reimagining San Francisco Logo")
105
+ # )
106
+ # ),
107
+ # fluidPage(
108
+ # box(
109
+ # tags$img(height = 100, width = 100,src = "Rlogo.png"),
110
+ # imageOutput('image_logos')
111
+ # )
112
+ # ),
113
+
114
+ # Tab Items
115
+ tabItems(
116
+ # Isochrone Explorer Tab
117
+ tabItem(tabName = "isochrone",
118
+ fluidRow(
119
+ box(
120
+ title = "Controls", status = "success", solidHeader = TRUE, width = 4,
121
+ radioButtons(
122
+ "location_choice",
123
+ "Select Location Method:",
124
+ choices = c("Address (Geocode)" = "address",
125
+ "Click on Map" = "map_click"),
126
+ selected = "map_click"
127
+ ),
128
+
129
+ conditionalPanel(
130
+ condition = "input.location_choice == 'address'",
131
+ mapboxGeocoderInput(
132
+ inputId = "geocoder",
133
+ placeholder = "Search for an address",
134
+ access_token = mapbox_token
135
+ )
136
+ ),
137
+
138
+ checkboxGroupInput(
139
+ "transport_modes",
140
+ "Select Transportation Modes:",
141
+ choices = list("Driving" = "driving",
142
+ "Walking" = "walking",
143
+ "Cycling" = "cycling",
144
+ "Driving with Traffic"= "driving-traffic"),
145
+ selected = c("driving", "walking")
146
+ ),
147
+
148
+ checkboxGroupInput(
149
+ "iso_times",
150
+ "Select Isochrone Times (minutes):",
151
+ choices = list("5" = 5, "10" = 10, "15" = 15),
152
+ selected = c(5, 10)
153
+ ),
154
+
155
+ actionButton("generate_iso", "Generate Isochrones", icon = icon("play")),
156
+ actionButton("clear_map", "Clear", icon = icon("times"))
157
+ ),
158
+ box(
159
+ title = "Map", status = "success", solidHeader = TRUE, width = 8,
160
+ leafletOutput("isoMap", height = 600)
161
+ )
162
+ ),
163
+ fluidRow(
164
+ box(
165
+ title = "Biodiversity Access Score", status = "success", solidHeader = TRUE, width = 6,
166
+ uiOutput("bioScoreBox")
167
+ ),
168
+ box(
169
+ title = "Closest Greenspace", status = "success", solidHeader = TRUE, width = 6,
170
+ uiOutput("closestGreenspaceUI")
171
+ )
172
+ ),
173
+ fluidRow(
174
+ box(
175
+ title = "Summary Data", status = "success", solidHeader = TRUE, width = 12,
176
+ DTOutput("dataTable") %>% withSpinner(type = 8, color = "#28a745")
177
+ )
178
+ ),
179
+ fluidRow(
180
+ box(
181
+ title = "Biodiversity & Socioeconomic Summary", status = "success", solidHeader = TRUE, width = 12,
182
+ plotOutput("bioSocPlot", height = "400px") %>% withSpinner(type = 8, color = "#28a745")
183
+ )
184
+ ),
185
+ fluidRow(
186
+ box(
187
+ title = "GBIF Records by Institution", status = "success", solidHeader = TRUE, width = 12,
188
+ plotOutput("collectionPlot", height = "400px") %>% withSpinner(type = 8, color = "#28a745")
189
+ )
190
+ )
191
+ ),
192
+
193
+ # GBIF Summaries Tab
194
+ tabItem(tabName = "gbif",
195
+ fluidRow(
196
+ box(
197
+ title = "Filters", status = "success", solidHeader = TRUE, width = 4,
198
+ selectInput(
199
+ "class_filter",
200
+ "Select a GBIF Class to Summarize:",
201
+ choices = c("All", sort(unique(sf_gbif$class))),
202
+ selected = "All"
203
+ ),
204
+ selectInput(
205
+ "family_filter",
206
+ "Filter by Family (optional):",
207
+ choices = c("All", sort(unique(sf_gbif$family))),
208
+ selected = "All"
209
+ )
210
+ ),
211
+ box(
212
+ title = "Data Summary", status = "success", solidHeader = TRUE, width = 8,
213
+ DTOutput("classTable")
214
+ )
215
+ ),
216
+ fluidRow(
217
+ box(
218
+ title = "Observations vs. Species Richness", status = "success", solidHeader = TRUE, width = 12,
219
+ plotOutput("obsVsSpeciesPlot", height = "300px") %>% withSpinner(type = 8, color = "#28a745"),
220
+ 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.")
221
+ )
222
+ )
223
+ ),
224
+ # Community Science Tab
225
+ tabItem(tabName = "community_science",
226
+ fluidRow(
227
+ box(
228
+ title = "Partner Community Organizations", status = "success", solidHeader = TRUE, width = 12,
229
+ leafletOutput("communityMap", height = 600)
230
+ )
231
+ ),
232
+ fluidRow(
233
+ box(
234
+ title = "Community Organizations Data", status = "success", solidHeader = TRUE, width = 12,
235
+ DTOutput("communityTable") %>% withSpinner(type = 8, color = "#28a745")
236
+ )
237
+ )
238
+ ),
239
+
240
+ # About Tab
241
+ tabItem(tabName = "about",
242
+ fluidRow(
243
+ box(
244
+ title = "App Summary", status = "success", solidHeader = TRUE, width = 12,
245
+ tags$b("App Summary (Fill out with RSF data working group):"),
246
+ p("
247
+ This application allows users to either click on a map or geocode an address
248
+ to generate travel-time isochrones across multiple transportation modes
249
+ (e.g., pedestrian, cycling, driving, driving during traffic).
250
+ It retrieves socio-economic data from precomputed Census variables, calculates NDVI,
251
+ and summarizes biodiversity records from GBIF. Users can explore information
252
+ related to biodiversity in urban environments, including greenspace coverage,
253
+ population estimates, and species diversity within each isochrone.
254
+ "),
255
+
256
+ tags$b("Created by:"),
257
+ p(strong("Diego Ellis Soto", "Carl Boettiger, Rebecca Johnson, Christopher J. Schell")),
258
+
259
+ p("Contact Information: ", strong("[email protected]"))
260
+ )
261
+ ),
262
+ fluidRow(
263
+ box(
264
+ title = "Reimagining San Francisco", status = "success", solidHeader = TRUE, width = 12,
265
+ tags$b("Reimagining San Francisco (Fill out with CAS):"),
266
+ p("Reimagining San Francisco is an initiative aimed at integrating ecological, social,
267
+ and technological dimensions to shape a sustainable future for the Bay Area.
268
+ This collaboration unites diverse stakeholders to explore innovations in urban planning,
269
+ 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.")
270
+ )
271
+ ),
272
+ fluidRow(
273
+ box(
274
+ title = "Why Biodiversity Access Matters", status = "success", solidHeader = TRUE, width = 12,
275
+ p("Ensuring equitable access to biodiversity is essential for human well-being,
276
+ ecological resilience, and global policy decisions related to conservation.
277
+ Areas with higher biodiversity can support ecosystem services including pollinators, moderate climate extremes,
278
+ and provide cultural, recreational, and health benefits to local communities.
279
+ 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.
280
+ 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.")
281
+ )
282
+ ),
283
+ fluidRow(
284
+ box(
285
+ title = "How We Calculate Biodiversity Access Percentile", status = "success", solidHeader = TRUE, width = 12,
286
+ p("Total unique species found within the user-generated isochrone.
287
+ We then compare that value to the distribution of unique species counts across all census block groups,
288
+ converting that comparison into a percentile ranking (Polish this, look at the 15 Minute city).
289
+ A higher percentile indicates greater biodiversity within the chosen area,
290
+ relative to other parts of the city or region.")
291
+ )
292
+ ),
293
+ fluidRow(
294
+ box(
295
+ title = "Next Steps", status = "success", solidHeader = TRUE, width = 12,
296
+ tags$ul(
297
+ tags$li("Add impervious surface"),
298
+ tags$li("National walkability score"),
299
+ tags$li("Social vulnerability score"),
300
+ tags$li("NatureServe biodiversity maps"),
301
+ tags$li("Calculate cold-hotspots within aggregation of H6 bins instead of by census block group: Ask Carl"),
302
+ tags$li("Species range maps"),
303
+ tags$li("Add common name GBIF"),
304
+ tags$li("Partner orgs"),
305
+ tags$li("Optimize speed -> store variables -> H-ify the world?"),
306
+ tags$li("Brainstorm and co-develop the biodiversity access score"),
307
+ 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.")
308
+ )
309
+ )
310
+ )
311
+ )
312
+ )
313
+ )
314
  )
315
 
316
+ # ------------------------------------------------
317
+ # Server
318
+ # ------------------------------------------------
319
  server <- function(input, output, session) {
320
+
321
+ chosen_point <- reactiveVal(NULL)
322
+
323
+ # ------------------------------------------------
324
+ # Render logos
325
+ # ------------------------------------------------
326
+
327
+
328
+ output$combine_logo <- renderImage({
329
+ list(
330
+ src = file.path("www", "Combined_logos.png"),
331
+ width = "50%",
332
+ height = "45%",
333
+ alt = "Combined_logos"
334
+ )
335
+ }, deleteFile = FALSE)
336
+
337
+ # output$uc_berkeley_logo <- renderImage({
338
+ # list(
339
+ # src = file.path("www", "UC_Berkeley_logo.png"),
340
+ # width = "50%",
341
+ # height = "45%",
342
+ # alt = "UC Berkeley Logo"
343
+ # )
344
+ # }, deleteFile = FALSE)
345
+ #
346
+ # output$california_academy_logo <- renderImage({
347
+ # list(
348
+ # src = file.path("www", "California_academy_logo.png"),
349
+ # width = "50%",
350
+ # height = "45%",
351
+ # alt = "California Academy Logo"
352
+ # )
353
+ # }, deleteFile = FALSE)
354
+ #
355
+ # output$reimagining_sf_logo <- renderImage({
356
+ # list(
357
+ # src = file.path("www", "Reimagining_San_Francisco.png"),
358
+ # width = "50%",
359
+ # height = "45%",
360
+ # alt = "Reimagining San Francisco Logo"
361
+ # )
362
+ # }, deleteFile = FALSE)
363
 
 
 
 
 
 
 
 
 
 
 
364
 
365
+ # ------------------------------------------------
366
+ # Leaflet Base + Hide Overlays
367
+ # ------------------------------------------------
368
+ output$isoMap <- renderLeaflet({
369
+ pal_cbg <- colorNumeric("YlOrRd", cbg_vect_sf$medincE)
370
+
371
+ pal_rich <- colorNumeric("YlOrRd", domain = cbg_vect_sf$unique_species)
372
+ # Color palette for data availability
373
+ pal_data <- colorNumeric("Blues", domain = cbg_vect_sf$n_observations)
374
+
375
+ leaflet() %>%
376
+ addTiles(group = "Street Map (Default)") %>%
377
+ addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (ESRI)") %>%
378
+ addProviderTiles(providers$CartoDB.Positron, group = "CartoDB.Positron") %>%
379
+
380
+ addPolygons(
381
+ data = cbg_vect_sf,
382
+ group = "Income",
383
+ fillColor = ~pal_cbg(medincE),
384
+ fillOpacity = 0.6,
385
+ color = "white",
386
+ weight = 1,
387
+ label=~GEOID,
388
+ highlightOptions = highlightOptions(
389
+ weight = 5,
390
+ color = "blue",
391
+ fillOpacity = 0.5,
392
+ bringToFront = TRUE
393
+ ),
394
+ labelOptions = labelOptions(
395
+ style = list("font-weight" = "bold", "color" = "blue"),
396
+ textsize = "12px",
397
+ direction = "auto"
398
+ )
399
+ ) %>%
400
+
401
+ addPolygons(
402
+ data = osm_greenspace,
403
+ group = "Greenspace",
404
+ fillColor = "darkgreen",
405
+ fillOpacity = 0.3,
406
+ color = "green",
407
+ weight = 1,
408
+ label = ~name,
409
+ highlightOptions = highlightOptions(
410
+ weight = 5,
411
+ color = "blue",
412
+ fillOpacity = 0.5,
413
+ bringToFront = TRUE
414
+ ),
415
+ labelOptions = labelOptions(
416
+ style = list("font-weight" = "bold", "color" = "blue"),
417
+ textsize = "12px",
418
+ direction = "auto",
419
+ noHide = FALSE # Labels appear on hover
420
+ )
421
+ ) %>%
422
+
423
+ addPolygons(
424
+ data = biodiv_hotspots,
425
+ group = "Hotspots (KnowBR)",
426
+ fillColor = "firebrick",
427
+ fillOpacity = 0.2,
428
+ color = "firebrick",
429
+ weight = 2,
430
+ label = ~GEOID,
431
+ highlightOptions = highlightOptions(
432
+ weight = 5,
433
+ color = "blue",
434
+ fillOpacity = 0.5,
435
+ bringToFront = TRUE
436
+ ),
437
+ labelOptions = labelOptions(
438
+ style = list("font-weight" = "bold", "color" = "blue"),
439
+ textsize = "12px",
440
+ direction = "auto"
441
+ )
442
+ ) %>%
443
+
444
+ addPolygons(
445
+ data = biodiv_coldspots,
446
+ group = "Coldspots (KnowBR)",
447
+ fillColor = "navyblue",
448
+ fillOpacity = 0.2,
449
+ color = "navyblue",
450
+ weight = 2,
451
+ label = ~GEOID,
452
+ highlightOptions = highlightOptions(
453
+ weight = 5,
454
+ color = "blue",
455
+ fillOpacity = 0.5,
456
+ bringToFront = TRUE
457
+ ),
458
+ labelOptions = labelOptions(
459
+ style = list("font-weight" = "bold", "color" = "blue"),
460
+ textsize = "12px",
461
+ direction = "auto"
462
+ )
463
+ ) %>%
464
+
465
+ # Add Species Richness Layer
466
+ addPolygons(
467
+ data = cbg_vect_sf,
468
+ group = "Species Richness",
469
+ fillColor = ~pal_rich(unique_species),
470
+ fillOpacity = 0.6,
471
+ color = "white",
472
+ weight = 1,
473
+ label = ~unique_species,
474
+ popup = ~paste0(
475
+ "<strong>GEOID: </strong>", GEOID,
476
+ "<br><strong>Species Richness: </strong>", unique_species,
477
+ "<br><strong>Observations: </strong>", n_observations,
478
+ "<br><strong>Median Income: </strong>", median_inc,
479
+ "<br><strong>Mean NDVI: </strong>", ndvi_mean
480
+ )
481
+ ) %>%
482
+
483
+ # Add Data Availability Layer
484
+ addPolygons(
485
+ data = cbg_vect_sf,
486
+ group = "Data Availability",
487
+ fillColor = ~pal_data(n_observations),
488
+ fillOpacity = 0.6,
489
+ color = "white",
490
+ weight = 1,
491
+ label = ~n_observations,
492
+ popup = ~paste0(
493
+ "<strong>GEOID: </strong>", GEOID,
494
+ "<br><strong>Observations: </strong>", n_observations,
495
+ "<br><strong>Species Richness: </strong>", unique_species,
496
+ "<br><strong>Median Income: </strong>", median_inc,
497
+ "<br><strong>Mean NDVI: </strong>", ndvi_mean
498
+ )
499
+ ) %>%
500
+
501
+ setView(lng = -122.4194, lat = 37.7749, zoom = 12) %>%
502
+ addLayersControl(
503
+ baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
504
+ overlayGroups = c("Income", "Greenspace",
505
+ "Hotspots (KnowBR)", "Coldspots (KnowBR)",
506
+ "Species Richness", "Data Availability",
507
+ "Isochrones", "NDVI Raster"),
508
+ options = layersControlOptions(collapsed = FALSE)
509
+ ) %>%
510
+ hideGroup("Income") %>%
511
+ hideGroup("Greenspace") %>%
512
+ hideGroup("Hotspots (KnowBR)") %>%
513
+ hideGroup("Coldspots (KnowBR)") %>%
514
+ hideGroup("Species Richness") %>%
515
+ hideGroup("Data Availability")
516
+ })
517
+
518
+
519
+ # ------------------------------------------------
520
+ # Observe map clicks (location_choice = 'map_click')
521
+ # ------------------------------------------------
522
+ observeEvent(input$isoMap_click, {
523
+ req(input$location_choice == "map_click")
524
+ click <- input$isoMap_click
525
+ if (!is.null(click)) {
526
+ chosen_point(c(lon = click$lng, lat = click$lat))
527
+
528
+ # Provide feedback with coordinates
529
+ showNotification(
530
+ paste0("Map clicked at Longitude: ", round(click$lng, 5),
531
+ ", Latitude: ", round(click$lat, 5)),
532
+ type = "message"
533
+ )
534
+
535
+ # Update the map with a marker
536
+ leafletProxy("isoMap") %>%
537
+ clearMarkers() %>%
538
+ addCircleMarkers(
539
+ lng = click$lng, lat = click$lat,
540
+ radius = 6, color = "firebrick",
541
+ label = "Map Click Location"
542
+ )
543
+ }
544
+ })
545
+
546
+ # ------------------------------------------------
547
+ # Observe geocoder input
548
+ # ------------------------------------------------
549
+ observeEvent(input$geocoder, {
550
+ req(input$location_choice == "address")
551
+ geocode_result <- input$geocoder
552
+ if (!is.null(geocode_result)) {
553
+ # Extract coordinates
554
+ xy <- geocoder_as_xy(geocode_result)
555
+
556
+ # Update the chosen_point reactive value
557
+ chosen_point(c(lon = xy[1], lat = xy[2]))
558
+
559
+ # Provide feedback with the geocoded address and coordinates
560
+ showNotification(
561
+ paste0("Address geocoded to Longitude: ", round(xy[1], 5),
562
+ ", Latitude: ", round(xy[2], 5)),
563
+ type = "message"
564
+ )
565
+
566
+ # Update the map with a marker
567
+ leafletProxy("isoMap") %>%
568
+ clearMarkers() %>%
569
+ addCircleMarkers(
570
+ lng = xy[1], lat = xy[2],
571
+ radius = 6, color = "navyblue",
572
+ label = "Geocoded Address"
573
+ ) %>%
574
+ flyTo(lng = xy[1], lat = xy[2], zoom = 13)
575
+ }
576
+ })
577
+
578
+ # ------------------------------------------------
579
+ # Observe clearing of map
580
+ # ------------------------------------------------
581
+ observeEvent(input$clear_map, {
582
+ # Reset the chosen point
583
+ chosen_point(NULL)
584
+
585
+ # Clear all markers and isochrones from the map, but keep other layers
586
+ leafletProxy("isoMap") %>%
587
+ clearMarkers() %>%
588
+ clearGroup("Isochrones") %>%
589
+ clearGroup("NDVI Raster")
590
+
591
+ # Provide feedback to the user
592
+ showNotification("Map cleared. You can select a new location.", type = "message")
593
+ })
594
+
595
+ # ------------------------------------------------
596
+ # Generate Isochrones
597
+ # ------------------------------------------------
598
+ isochrones_data <- eventReactive(input$generate_iso, {
599
+
600
+ leafletProxy("isoMap") %>%
601
+ clearGroup("Isochrones") %>%
602
+ clearGroup("NDVI Raster")
603
+
604
+ # Validate inputs
605
+ pt <- chosen_point()
606
+ if (is.null(pt)) {
607
+ showNotification("No location selected! Provide an address or click the map.", type = "error")
608
+ return(NULL)
609
+ }
610
+ if (length(input$transport_modes) == 0) {
611
+ showNotification("Select at least one transportation mode.", type = "error")
612
+ return(NULL)
613
+ }
614
+ if (length(input$iso_times) == 0) {
615
+ showNotification("Select at least one isochrone time.", type = "error")
616
+ return(NULL)
617
+ }
618
+
619
+ location_sf <- st_as_sf(
620
+ data.frame(lon = pt["lon"], lat = pt["lat"]),
621
+ coords = c("lon","lat"), crs = 4326
622
+ )
623
+
624
+ iso_list <- list()
625
+ for (mode in input$transport_modes) {
626
+ for (t in input$iso_times) {
627
+ iso <- tryCatch({
628
+ mb_isochrone(location_sf, time = as.numeric(t), profile = mode,
629
+ access_token = mapbox_token)
630
+ }, error = function(e) {
631
+ showNotification(paste("Isochrone error:", mode, t, e$message), type = "error")
632
+ NULL
633
+ })
634
+ if (!is.null(iso)) {
635
+ iso$mode <- mode
636
+ iso$time <- t
637
+ iso_list <- append(iso_list, list(iso))
638
+ }
639
+ }
640
+ }
641
+ if (length(iso_list) == 0) {
642
+ showNotification("No isochrones generated.", type = "warning")
643
+ return(NULL)
644
+ }
645
+
646
+ all_iso <- do.call(rbind, iso_list) %>% st_transform(4326)
647
+ all_iso
648
+ })
649
+
650
+ # ------------------------------------------------
651
+ # Plot Isochrones + NDVI
652
+ # ------------------------------------------------
653
+ observeEvent(isochrones_data(), {
654
+ iso_data <- isochrones_data()
655
+ req(iso_data)
656
+
657
+ iso_data$iso_group <- paste(iso_data$mode, iso_data$time, sep = "_")
658
+ pal <- colorRampPalette(brewer.pal(8, "Set2"))
659
+ cols <- pal(nrow(iso_data))
660
+
661
+ for (i in seq_len(nrow(iso_data))) {
662
+ poly_i <- iso_data[i, ]
663
+ leafletProxy("isoMap") %>%
664
+ addPolygons(
665
+ data = poly_i,
666
+ group = "Isochrones",
667
+ color = cols[i],
668
+ weight = 2,
669
+ fillOpacity = 0.4,
670
+ label = paste0(poly_i$mode, " - ", poly_i$time, " mins")
671
+ )
672
+ }
673
+
674
+ iso_union <- st_union(iso_data)
675
+ iso_union_vect <- vect(iso_union)
676
+ ndvi_crop <- terra::crop(ndvi, iso_union_vect)
677
+ ndvi_mask <- terra::mask(ndvi_crop, iso_union_vect)
678
+ ndvi_vals <- values(ndvi_mask)
679
+ ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
680
+
681
+ if (length(ndvi_vals) > 0) {
682
+ ndvi_pal <- colorNumeric("YlGn", domain = range(ndvi_vals, na.rm = TRUE), na.color = "transparent")
683
+
684
+ leafletProxy("isoMap") %>%
685
+ addRasterImage(
686
+ x = ndvi_mask,
687
+ colors = ndvi_pal,
688
+ opacity = 0.7,
689
+ project = TRUE,
690
+ group = "NDVI Raster"
691
+ ) %>%
692
+ addLegend(
693
+ position = "bottomright",
694
+ pal = ndvi_pal,
695
+ values = ndvi_vals,
696
+ title = "NDVI"
697
+ )
698
+ }
699
+
700
+ # Ensure other layers remain
701
+ leafletProxy("isoMap") %>%
702
+ addLayersControl(
703
+ baseGroups = c("Street Map (Default)", "Satellite (ESRI)", "CartoDB.Positron"),
704
+ overlayGroups = c("Income", "Greenspace",
705
+ "Hotspots (KnowBR)", "Coldspots (KnowBR)",
706
+ "Species Richness", "Data Availability",
707
+ "Isochrones", "NDVI Raster"),
708
+ options = layersControlOptions(collapsed = FALSE)
709
+ )
710
+ })
711
+
712
+ # ------------------------------------------------
713
+ # socio_data Reactive + Summaries
714
+ # ------------------------------------------------
715
+ socio_data <- reactive({
716
+ iso_data <- isochrones_data()
717
+ if (is.null(iso_data) || nrow(iso_data) == 0) {
718
+ return(data.frame())
719
+ }
720
+
721
+ acs_wide <- cbg_vect_sf %>%
722
+ mutate(
723
+ population = popE,
724
+ med_income = medincE
725
+ )
726
+
727
+ hotspot_union <- st_union(biodiv_hotspots)
728
+ coldspot_union <- st_union(biodiv_coldspots)
729
+
730
+ results <- data.frame()
731
+
732
+ # Calculate distance to coldspot and hotspots
733
+ for (i in seq_len(nrow(iso_data))) {
734
+ poly_i <- iso_data[i, ]
735
+
736
+ dist_hot <- st_distance(poly_i, hotspot_union)
737
+ dist_cold <- st_distance(poly_i, coldspot_union)
738
+ dist_hot_km <- round(as.numeric(min(dist_hot)) / 1000, 3)
739
+ dist_cold_km <- round(as.numeric(min(dist_cold)) / 1000, 3)
740
+
741
+ inter_acs <- st_intersection(acs_wide, poly_i)
742
+
743
+ vect_acs_wide <- vect(acs_wide)
744
+ vect_poly_i <- vect(poly_i)
745
+ inter_acs <- intersect(vect_acs_wide, vect_poly_i)
746
+ inter_acs = st_as_sf(inter_acs)
747
+
748
+ pop_total <- 0
749
+ inc_str <- "N/A"
750
+ if (nrow(inter_acs) > 0) {
751
+ inter_acs$area <- st_area(inter_acs)
752
+ inter_acs$area_num <- as.numeric(inter_acs$area)
753
+ inter_acs$area_ratio <- inter_acs$area_num / as.numeric(st_area(inter_acs))
754
+ inter_acs$weighted_pop <- inter_acs$population * inter_acs$area_ratio
755
+
756
+ pop_total <- round(sum(inter_acs$weighted_pop, na.rm = TRUE))
757
+
758
+ w_income <- sum(inter_acs$med_income * inter_acs$area_num, na.rm = TRUE) /
759
+ sum(inter_acs$area_num, na.rm = TRUE)
760
+ if (!is.na(w_income) && w_income > 0) {
761
+ inc_str <- paste0("$", formatC(round(w_income, 2), format = "f", big.mark = ","))
762
+ }
763
+ }
764
+
765
+ # Intersection with greenspace
766
+ vec_osm_greenspace <- vect(osm_greenspace)
767
+ inter_gs <- intersect(vec_osm_greenspace, vect_poly_i)
768
+ inter_gs = st_as_sf(inter_gs)
769
+
770
+ gs_area_m2 <- 0
771
+ if (nrow(inter_gs) > 0) {
772
+ gs_area_m2 <- sum(st_area(inter_gs))
773
+ }
774
+ iso_area_m2 <- as.numeric(st_area(poly_i))
775
+ gs_area_m2 <- as.numeric(gs_area_m2)
776
+ gs_percent <- ifelse(iso_area_m2 > 0, 100 * gs_area_m2 / iso_area_m2, 0)
777
+
778
+ # NDVI Calculation
779
+ poly_vect <- vect(poly_i)
780
+ ndvi_crop <- terra::crop(ndvi, poly_vect)
781
+ ndvi_mask <- terra::mask(ndvi_crop, poly_vect)
782
+ ndvi_vals <- values(ndvi_mask)
783
+ ndvi_vals <- ndvi_vals[!is.na(ndvi_vals)]
784
+ mean_ndvi <- ifelse(length(ndvi_vals) > 0, round(mean(ndvi_vals, na.rm=TRUE), 3), NA)
785
+
786
+ # Intersection with GBIF data
787
+ inter_gbif <- intersect(vect_gbif, vect_poly_i)
788
+ inter_gbif <- st_as_sf(inter_gbif)
789
+
790
+ inter_gbif_acs <- sf_gbif %>%
791
+ mutate(
792
+ income = medincE,
793
+ ndvi = ndvi_sentinel
794
  )
795
+
796
+ if (nrow(inter_gbif) > 0) {
797
+ inter_gbif_acs <- inter_gbif_acs[inter_gbif_acs$GEOID %in% inter_gbif$GEOID, ]
798
  }
799
+
800
+ n_records <- nrow(inter_gbif)
801
+ n_species <- length(unique(inter_gbif$species))
802
+
803
+ n_birds <- length(unique(inter_gbif$species[inter_gbif$class == "Aves"]))
804
+ n_mammals <- length(unique(inter_gbif$species[inter_gbif$class == "Mammalia"]))
805
+ n_plants <- length(unique(inter_gbif$species[inter_gbif$class %in%
806
+ c("Magnoliopsida","Liliopsida","Pinopsida","Polypodiopsida",
807
+ "Equisetopsida","Bryopsida","Marchantiopsida") ]))
808
+
809
+ iso_area_km2 <- round(iso_area_m2 / 1e6, 3)
810
+
811
+ row_i <- data.frame(
812
+ Mode = tools::toTitleCase(poly_i$mode),
813
+ Time = poly_i$time,
814
+ IsochroneArea_km2 = iso_area_km2,
815
+ DistToHotspot_km = dist_hot_km,
816
+ DistToColdspot_km = dist_cold_km,
817
+ EstimatedPopulation = pop_total,
818
+ MedianIncome = inc_str,
819
+ MeanNDVI = ifelse(!is.na(mean_ndvi), mean_ndvi, "N/A"),
820
+ GBIF_Records = n_records,
821
+ GBIF_Species = n_species,
822
+ Bird_Species = n_birds,
823
+ Mammal_Species = n_mammals,
824
+ Plant_Species = n_plants,
825
+ Greenspace_m2 = round(gs_area_m2, 2),
826
+ Greenspace_percent = round(gs_percent, 2),
827
+ stringsAsFactors = FALSE
828
+ )
829
+ results <- rbind(results, row_i)
830
+ }
831
+
832
+ iso_union <- st_union(iso_data)
833
+ vect_iso <- vect(iso_union)
834
+ inter_all_gbif <- intersect(vect_gbif, vect_iso)
835
+ inter_all_gbif <- st_as_sf(inter_all_gbif)
836
+
837
+ union_n_species <- length(unique(inter_all_gbif$species))
838
+ rank_percentile <- round(100 * ecdf(cbg_vect_sf$unique_species)(union_n_species), 1)
839
+ attr(results, "bio_percentile") <- rank_percentile
840
+
841
+ # Closest Greenspace from ANY part of the isochrone
842
+ dist_mat <- st_distance(iso_union, osm_greenspace) # 1 x N matrix
843
+ if (length(dist_mat) > 0) {
844
+ min_dist <- min(dist_mat)
845
+ min_idx <- which.min(dist_mat)
846
+ gs_name <- osm_greenspace$name[min_idx]
847
+ attr(results, "closest_greenspace") <- gs_name
848
+ } else {
849
+ attr(results, "closest_greenspace") <- "None"
850
+ }
851
+
852
+ results
853
+ })
854
+
855
+ # ------------------------------------------------
856
+ # Render main summary table
857
+ # ------------------------------------------------
858
+ output$dataTable <- renderDT({
859
+ df <- socio_data()
860
+ if (nrow(df) == 0) {
861
+ return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
862
+ }
863
+ DT::datatable(
864
+ df,
865
+ colnames = c(
866
+ "Mode" = "Mode",
867
+ "Time (min)" = "Time",
868
+ "Area (km²)" = "IsochroneArea_km2",
869
+ "Dist. Hotspot (km)" = "DistToHotspot_km",
870
+ "Dist. Coldspot (km)" = "DistToColdspot_km",
871
+ "Population" = "EstimatedPopulation",
872
+ "Median Income" = "MedianIncome",
873
+ "Mean NDVI" = "MeanNDVI",
874
+ "GBIF Records" = "GBIF_Records",
875
+ "Unique Species" = "GBIF_Species",
876
+ "Bird Species" = "Bird_Species",
877
+ "Mammal Species" = "Mammal_Species",
878
+ "Plant Species" = "Plant_Species",
879
+ # "Greenspace (m²)" = "Greenspace_m2",
880
+ "Greenspace (%)" = "Greenspace_percent"
881
+ ),
882
+ options = list(pageLength = 10, autoWidth = TRUE),
883
+ rownames = FALSE
884
+ )
885
+ })
886
+
887
+ # ------------------------------------------------
888
+ # Biodiversity Access Score + Closest Greenspace
889
+ # ------------------------------------------------
890
+ output$bioScoreBox <- renderUI({
891
+ df <- socio_data()
892
+ if (nrow(df) == 0) return(NULL)
893
+
894
+ percentile <- attr(df, "bio_percentile")
895
+ if (is.null(percentile)) percentile <- "N/A"
896
+ else percentile <- paste0(percentile, "th Percentile")
897
+
898
+ wellPanel(
899
+ HTML(paste0("<h2>Biodiversity Access Score: ", percentile, "</h2>"))
900
+ )
901
+ })
902
+
903
+ output$closestGreenspaceUI <- renderUI({
904
+ df <- socio_data()
905
+ if (nrow(df) == 0) return(NULL)
906
+ gs_name <- attr(df, "closest_greenspace")
907
+ if (is.null(gs_name)) gs_name <- "None"
908
+
909
+ tagList(
910
+ strong("Closest Greenspace (from any part of the Isochrone):"),
911
+ p(gs_name)
912
+ )
913
+ })
914
+
915
+ # ------------------------------------------------
916
+ # Secondary table: user-selected CLASS & FAMILY
917
+ # ------------------------------------------------
918
+ output$classTable <- renderDT({
919
+ iso_data <- isochrones_data()
920
+ if (is.null(iso_data) || nrow(iso_data) == 0) {
921
+ return(DT::datatable(data.frame("Message" = "No isochrones generated yet.")))
922
+ }
923
+
924
+ iso_union <- st_union(iso_data)
925
+ vect_iso <- vect(iso_union)
926
+ inter_gbif <- intersect(vect_gbif, vect_iso)
927
+ inter_gbif = st_as_sf(inter_gbif)
928
+
929
+ inter_gbif_acs = sf_gbif %>%
930
+ mutate(
931
+ income = medincE,
932
+ ndvi = ndvi_sentinel
933
+ )
934
+
935
+ if (input$class_filter != "All") {
936
+ inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$class == input$class_filter, ]
937
+ }
938
+ if (input$family_filter != "All") {
939
+ inter_gbif_acs <- inter_gbif_acs[ inter_gbif_acs$family == input$family_filter, ]
940
+ }
941
+
942
+ if (nrow(inter_gbif_acs) == 0) {
943
+ return(DT::datatable(data.frame("Message" = "No records for that combination in the isochrone.")))
944
+ }
945
+
946
+ species_counts <- inter_gbif_acs %>%
947
+ st_drop_geometry() %>%
948
+ group_by(species) %>%
949
+ summarize(
950
+ n_records = n(),
951
+ mean_income = round(mean(income, na.rm=TRUE), 2),
952
+ mean_ndvi = round(mean(ndvi, na.rm=TRUE), 3),
953
+ .groups = "drop"
954
+ ) %>%
955
+ arrange(desc(n_records))
956
+
957
+ DT::datatable(
958
+ species_counts,
959
+ colnames = c("Species", "Number of Records", "Mean Income", "Mean NDVI"),
960
+ options = list(pageLength = 10),
961
+ rownames = FALSE
962
+ )
963
+ })
964
+
965
+ # ------------------------------------------------
966
+ # Ggplot: Biodiversity & Socioeconomic Summary
967
+ # ------------------------------------------------
968
+ output$bioSocPlot <- renderPlot({
969
+ df <- socio_data()
970
+ if (nrow(df) == 0) return(NULL)
971
+
972
+ df_plot <- df %>%
973
+ mutate(IsoLabel = paste0(Mode, "-", Time, "min"))
974
+
975
+ ggplot(df_plot, aes(x = IsoLabel)) +
976
+ geom_col(aes(y = GBIF_Species), fill = "steelblue", alpha = 0.7) +
977
+ geom_line(aes(y = EstimatedPopulation / 1000, group = 1), color = "red", size = 1) +
978
+ geom_point(aes(y = EstimatedPopulation / 1000), color = "red", size = 3) +
979
+ labs(
980
+ x = "Isochrone (Mode-Time)",
981
+ y = "Unique Species (Blue) | Population (Red) (Thousands)",
982
+ title = "Biodiversity & Socioeconomic Summary"
983
+ ) +
984
+ theme_minimal(base_size = 14) +
985
+ theme(
986
+ axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
987
+ axis.text.y = element_text(size = 12),
988
+ axis.title.x = element_text(size = 14),
989
+ axis.title.y = element_text(size = 14),
990
+ plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
991
+ )
992
+ })
993
+
994
+ # ------------------------------------------------
995
+ # Bar plot: GBIF records by institutionCode
996
+ # ------------------------------------------------
997
+ output$collectionPlot <- renderPlot({
998
+ iso_data <- isochrones_data()
999
+ if (is.null(iso_data) || nrow(iso_data) == 0) {
1000
+ plot.new()
1001
+ title("No GBIF records found in this isochrone.")
1002
+ return(NULL)
1003
+ }
1004
+
1005
+ iso_union <- st_union(iso_data)
1006
+ vect_iso <- vect(iso_union)
1007
+ inter_gbif <- intersect(vect_gbif, vect_iso)
1008
+ inter_gbif = st_as_sf(inter_gbif)
1009
+
1010
+ if (nrow(inter_gbif) == 0) {
1011
+ plot.new()
1012
+ title("No GBIF records found in this isochrone.")
1013
+ return(NULL)
1014
+ }
1015
+
1016
+ df_code <- inter_gbif %>%
1017
+ st_drop_geometry() %>%
1018
+ group_by(institutionCode) %>%
1019
+ summarize(count = n(), .groups = "drop") %>%
1020
+ arrange(desc(count)) %>%
1021
+ mutate(truncatedCode = substr(institutionCode, 1, 5)) # Shorter version of the names
1022
+
1023
+ ggplot(df_code, aes(x = reorder(truncatedCode, -count), y = count)) +
1024
+ geom_bar(stat = "identity", fill = "darkorange", alpha = 0.7) +
1025
+ labs(
1026
+ x = "Institution Code (Truncated)",
1027
+ y = "Number of Records",
1028
+ title = "GBIF Records by Institution Code (Isochrone Union)"
1029
+ ) +
1030
+ theme_minimal(base_size = 14) +
1031
+ theme(
1032
+ axis.text.x = element_text(angle = 45, hjust = 1, size = 12),
1033
+ axis.text.y = element_text(size = 12),
1034
+ axis.title.x = element_text(size = 14),
1035
+ axis.title.y = element_text(size = 14),
1036
+ plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
1037
+ )
1038
+ })
1039
+
1040
+ # ------------------------------------------------
1041
+ # Additional Plot: n_observations vs n_species
1042
+ # ------------------------------------------------
1043
+
1044
+ # Make it reactive: obsVsSpeciesPlot updates dynamically based on user-selected class_filter or family_filter.
1045
+
1046
+ filtered_data <- reactive({
1047
+ data <- cbg_vect_sf
1048
+ if (input$class_filter != "All") {
1049
+ data <- data[data$class == input$class_filter, ]
1050
+ }
1051
+ if (input$family_filter != "All") {
1052
+ data <- data[data$family == input$family_filter, ]
1053
+ }
1054
+ data
1055
+ })
1056
+
1057
+ output$obsVsSpeciesPlot <- renderPlot({
1058
+ data <- filtered_data()
1059
+ if (nrow(data) == 0) {
1060
+ plot.new()
1061
+ title("No data available for selected filters.")
1062
+ return(NULL)
1063
+ }
1064
+
1065
+ ggplot(data, aes(x = log(n_observations + 1), y = log(unique_species + 1))) +
1066
+ geom_point(color = "blue", alpha = 0.6) +
1067
+ labs(
1068
+ x = "Log(Number of Observations + 1)",
1069
+ y = "Log(Species Richness + 1)",
1070
+ title = "Data Availability vs. Species Richness"
1071
+ ) +
1072
+ theme_minimal(base_size = 14) +
1073
+ theme(
1074
+ axis.text.x = element_text(size = 12),
1075
+ axis.text.y = element_text(size = 12),
1076
+ axis.title.x = element_text(size = 14),
1077
+ axis.title.y = element_text(size = 14),
1078
+ plot.title = element_text(hjust = 0.5, size = 16, face = "bold")
1079
+ )
1080
+ })
1081
+
1082
+
1083
+
1084
+ # ------------------------------------------------
1085
+ # [Optional: Linear Model Plot (Commented Out)]
1086
+ # ------------------------------------------------
1087
+ # Uncomment and adjust if needed
1088
+ # output$lmCoefficientsPlot <- renderPlot({
1089
+ # df_lm <- cbg_vect_sf %>%
1090
+ # filter(!is.na(n_observations),
1091
+ # !is.na(unique_species),
1092
+ # !is.na(median_inc),
1093
+ # !is.na(ndvi_mean))
1094
+ #
1095
+ # if (nrow(df_lm) < 5) {
1096
+ # plot.new()
1097
+ # title("Not enough data for linear model.")
1098
+ # return(NULL)
1099
+ # }
1100
+ #
1101
+ # fit <- lm(unique_species ~ n_observations + median_inc + ndvi_mean, data = df_lm)
1102
+ #
1103
+ # p <- plot_model(fit, show.values = TRUE, value.offset = .3, title = "LM Coefficients: n_species ~ n_observations + median_inc + ndvi_mean")
1104
+ # print(p)
1105
+ # })
1106
+
1107
+
1108
+ #
1109
+ # # Add Images:
1110
+ # df_img = data.frame(id = c(1:3), img_path=c('California_academy_logo.png', 'Reimagining_San_Francisco.png', 'UC Berkeley_logo.png'))
1111
+ # n <- nrow(df_img)
1112
+ #
1113
+ # n <- nrow(df_img)
1114
+ #
1115
+ # observe({
1116
+ # for (i in 1:n)
1117
+ # {
1118
+ # print(i)
1119
+ # local({
1120
+ # my_i <- i
1121
+ # imagename = paste0("img", my_i)
1122
+ # print(imagename)
1123
+ # output[[imagename]] <-
1124
+ # renderImage({
1125
+ # list(src = file.path('www', df_img$img_path[my_i]),
1126
+ # width = "100%", height = "55%",
1127
+ # alt = "Image failed to render")
1128
+ # }, deleteFile = FALSE)
1129
+ # })
1130
+ # }
1131
+ # })
1132
+ #
1133
+ #
1134
+ # output$houz <- renderUI({
1135
+ #
1136
+ # image_output_list <-
1137
+ # lapply(1:n,
1138
+ # function(i)
1139
+ # {
1140
+ # imagename = paste0("img", i)
1141
+ # imageOutput(imagename)
1142
+ # })
1143
+ #
1144
+ # do.call(tagList, image_output_list)
1145
+ # })
1146
+
1147
 
 
 
 
 
1148
  }
1149
 
1150
+
1151
+
1152
+ # Run the Shiny app
1153
  shinyApp(ui, server)
1154
+
1155
+ #
1156
+
1157
+
install.r ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"))
www/California_academy_logo.png ADDED

Git LFS Details

  • SHA256: cd530e5a8f558fb4005b9e1a692b05ecbec14e81bffcdb1f98be19380cb347f1
  • Pointer size: 130 Bytes
  • Size of remote file: 18.5 kB
www/Combined_logos.png ADDED

Git LFS Details

  • SHA256: e44786edd4cce3836207e6243306e34c8fadc1a39a2484d2cbda35dfcf2b0dfe
  • Pointer size: 131 Bytes
  • Size of remote file: 201 kB
www/Reimagining_San_Francisco.png ADDED

Git LFS Details

  • SHA256: 71b5ea34c87176c52e7a9d8341d7482a9801dea7492bd638b17c5b1c0a79913b
  • Pointer size: 131 Bytes
  • Size of remote file: 760 kB
www/UC_Berkeley_logo.png ADDED

Git LFS Details

  • SHA256: a7b6fdf0ca38a7f973eeab5dd0f7ba7a9f2788f20dbe17ac34a285f6d41ffb7b
  • Pointer size: 130 Bytes
  • Size of remote file: 88.6 kB
www/hexbin_RSF_logo.png ADDED

Git LFS Details

  • SHA256: 53143f5ab2602e2fb05834bc9fb2d6e849cdaf8724f69ec49cabc663ddb2df0f
  • Pointer size: 130 Bytes
  • Size of remote file: 94.3 kB