diego-ellis-soto commited on
Commit
ea5f4e5
Β·
1 Parent(s): aedb415

Ready for huggingface!

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