3v324v23 commited on
Commit
9372abc
·
1 Parent(s): f8d7f02

Now species specific query, full download

Browse files
Files changed (2) hide show
  1. .ipynb_checkpoints/app-checkpoint.R +672 -0
  2. app.R +197 -115
.ipynb_checkpoints/app-checkpoint.R ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##################################################################
2
+ # Single R Script: Mortality Analysis + Shiny - Two-Click BBox
3
+ ##################################################################
4
+
5
+ ### 1) Install/Load Required Packages ####
6
+ required_packages <- c(
7
+ "httr", "jsonlite", "tidyverse", "glue", "lubridate",
8
+ "wesanderson", "viridis", "shinycssloaders",
9
+ "DT", "maps", "mapdata", "leaflet", "leaflet.extras",
10
+ "shinythemes","shiny"
11
+ )
12
+
13
+ installed_packages <- rownames(installed.packages())
14
+ for (pkg in required_packages) {
15
+ if (!pkg %in% installed_packages) {
16
+ install.packages(pkg, dependencies = TRUE)
17
+ }
18
+ }
19
+
20
+ library(httr)
21
+ library(jsonlite)
22
+ library(tidyverse)
23
+ library(glue)
24
+ library(lubridate)
25
+ library(wesanderson)
26
+ library(viridis)
27
+ library(shinycssloaders)
28
+ library(DT)
29
+ library(maps)
30
+ library(mapdata)
31
+ library(leaflet)
32
+ library(leaflet.extras)
33
+ library(shinythemes)
34
+ library(shiny)
35
+
36
+
37
+ ##################################################################
38
+ # 2) Mortality-Analysis Functions
39
+ ##################################################################
40
+
41
+ fetch_dead_data_once <- function(
42
+ place_id = NULL,
43
+ swlat = NULL,
44
+ swlng = NULL,
45
+ nelat = NULL,
46
+ nelng = NULL,
47
+ start_date,
48
+ end_date,
49
+ iconic_taxa = "Aves",
50
+ per_page = 1000,
51
+ max_pages = 50
52
+ ) {
53
+ base_url <- "https://api.inaturalist.org/v1/observations"
54
+
55
+ query_params <- glue(
56
+ "iconic_taxa={iconic_taxa}&",
57
+ "term_id=17&",
58
+ "term_value_id=19&",
59
+ "verifiable=true&",
60
+ "d1={start_date}&d2={end_date}&",
61
+ "order=desc&order_by=created_at&",
62
+ "per_page={per_page}"
63
+ )
64
+
65
+ loc_part <- ""
66
+ if (!is.null(place_id)) {
67
+ loc_part <- glue("&place_id={place_id}")
68
+ } else if (!is.null(swlat) && !is.null(swlng) &&
69
+ !is.null(nelat) && !is.null(nelng)) {
70
+ loc_part <- glue("&nelat={nelat}&nelng={nelng}&swlat={swlat}&swlng={swlng}")
71
+ } else {
72
+ stop("Must provide either 'place_id' OR bounding box (swlat, swlng, nelat, nelng).")
73
+ }
74
+
75
+ observations_list <- list()
76
+ current_page <- 1
77
+
78
+ while (current_page <= max_pages) {
79
+ query_url <- paste0(base_url, "?", query_params,
80
+ "&page=", current_page, loc_part)
81
+
82
+ message("Fetching page ", current_page,
83
+ " [", start_date, " to ", end_date, "]:\n", query_url)
84
+
85
+ resp <- GET(query_url)
86
+ if (http_error(resp)) {
87
+ warning("HTTP error on page ", current_page, ": ", status_code(resp))
88
+ break
89
+ }
90
+
91
+ parsed <- content(resp, as = "text", encoding = "UTF-8") %>%
92
+ fromJSON(flatten = TRUE)
93
+
94
+ if (length(parsed$results) == 0) {
95
+ message("No more results at page ", current_page)
96
+ break
97
+ }
98
+
99
+ obs_page_df <- as_tibble(parsed$results)
100
+ observations_list[[current_page]] <- obs_page_df
101
+
102
+ if (nrow(obs_page_df) < per_page) {
103
+ message("Reached last page of results at page ", current_page)
104
+ break
105
+ }
106
+
107
+ current_page <- current_page + 1
108
+ Sys.sleep(1) # polite pause for the API
109
+ }
110
+
111
+ observations_all <- bind_rows(observations_list)
112
+ return(observations_all)
113
+ }
114
+
115
+
116
+ fetch_dead_data_monthly <- function(
117
+ year,
118
+ place_id = NULL,
119
+ swlat = NULL,
120
+ swlng = NULL,
121
+ nelat = NULL,
122
+ nelng = NULL,
123
+ iconic_taxa = "Aves"
124
+ ) {
125
+ monthly_list <- list()
126
+
127
+ for (month_i in 1:12) {
128
+ start_date <- as.Date(glue("{year}-{sprintf('%02d', month_i)}-01"))
129
+ end_date <- start_date %m+% months(1) %m-% days(1)
130
+
131
+ if (year(start_date) != year) break
132
+
133
+ message("\n--- Querying ", year, ", month ", month_i, " ---")
134
+ df_month <- fetch_dead_data_once(
135
+ place_id = place_id,
136
+ swlat = swlat,
137
+ swlng = swlng,
138
+ nelat = nelat,
139
+ nelng = nelng,
140
+ start_date = start_date,
141
+ end_date = end_date,
142
+ iconic_taxa= iconic_taxa
143
+ )
144
+ monthly_list[[month_i]] <- df_month
145
+ }
146
+
147
+ year_df <- bind_rows(monthly_list)
148
+ return(year_df)
149
+ }
150
+
151
+
152
+ getDeadVertebrates_monthlyLoop <- function(
153
+ years = c(2022, 2023),
154
+ place_id = NULL,
155
+ swlat = NULL,
156
+ swlng = NULL,
157
+ nelat = NULL,
158
+ nelng = NULL,
159
+ iconic_taxa = "Aves",
160
+ per_page = 1000,
161
+ max_pages = 50,
162
+ outdir = NULL
163
+ ) {
164
+ all_years_list <- list()
165
+ for (yr in years) {
166
+ message("\n========= YEAR: ", yr, " ==========\n")
167
+ yr_df <- fetch_dead_data_monthly(
168
+ year = yr,
169
+ place_id = place_id,
170
+ swlat = swlat,
171
+ swlng = swlng,
172
+ nelat = nelat,
173
+ nelng = nelng,
174
+ iconic_taxa= iconic_taxa
175
+ ) %>%
176
+ mutate(Window = as.character(yr))
177
+
178
+ all_years_list[[as.character(yr)]] <- yr_df
179
+ }
180
+
181
+ merged_df_all <- bind_rows(all_years_list)
182
+
183
+ if (!"created_at_details.date" %in% names(merged_df_all) ||
184
+ nrow(merged_df_all) == 0) {
185
+ daily_plot <- ggplot() +
186
+ labs(title = "No 'Dead' Observations Found", x = NULL, y = NULL) +
187
+ theme_void()
188
+
189
+ top_species_plot <- ggplot() +
190
+ labs(title = "No species data", x = NULL, y = NULL) +
191
+ theme_void()
192
+
193
+ map_hotspots_gg <- ggplot() +
194
+ labs(title = "No data for hotspots map") +
195
+ theme_void()
196
+
197
+ return(list(
198
+ merged_df = merged_df_all,
199
+ daily_plot = daily_plot,
200
+ top_species_plot = top_species_plot,
201
+ map_hotspots_gg = map_hotspots_gg
202
+ ))
203
+ }
204
+
205
+ if (!is.null(outdir)) {
206
+ if (!dir.exists(outdir)) {
207
+ dir.create(outdir, recursive = TRUE)
208
+ }
209
+ readr::write_csv(merged_df_all, file.path(outdir, "merged_df_top_all_data.csv"))
210
+ }
211
+
212
+ counts_by_day <- merged_df_all %>%
213
+ mutate(obs_date = as.Date(`created_at_details.date`)) %>%
214
+ group_by(Window, obs_date) %>%
215
+ summarise(n = n(), .groups = "drop")
216
+
217
+ n_windows <- length(unique(counts_by_day$Window))
218
+ wes_colors <- wes_palette("Zissou1", n_windows, type = "discrete")
219
+
220
+ daily_plot <- ggplot(counts_by_day, aes(x = obs_date, y = n, color = Window)) +
221
+ geom_line(size = 1.2) +
222
+ geom_point(size = 2) +
223
+ scale_color_manual(values = wes_colors) +
224
+ scale_x_date(date_labels = "%b", date_breaks = "1 month") +
225
+ labs(
226
+ title = glue("Daily 'Dead' Observations (Years {paste(years, collapse=', ')})"),
227
+ x = "Month",
228
+ y = "Number of Observations",
229
+ color = "Year"
230
+ ) +
231
+ theme_minimal(base_size = 14) +
232
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
233
+
234
+ if ("taxon.name" %in% names(merged_df_all)) {
235
+ species_counts <- merged_df_all %>%
236
+ filter(!is.na(taxon.name)) %>%
237
+ group_by(Window, taxon.name) %>%
238
+ summarise(dead_count = n(), .groups = "drop")
239
+
240
+ top_species_overall <- species_counts %>%
241
+ group_by(taxon.name) %>%
242
+ summarise(total_dead = sum(dead_count)) %>%
243
+ arrange(desc(total_dead)) %>%
244
+ slice_head(n = 20)
245
+
246
+ species_top20 <- species_counts %>%
247
+ filter(taxon.name %in% top_species_overall$taxon.name)
248
+
249
+ top_species_plot <- ggplot(species_top20, aes(
250
+ x = reorder(taxon.name, -dead_count),
251
+ y = dead_count,
252
+ fill= Window
253
+ )) +
254
+ geom_col(position = position_dodge(width = 0.7)) +
255
+ coord_flip() +
256
+ scale_fill_manual(values = wes_colors) +
257
+ labs(
258
+ title = "Top 20 Species with 'Dead' Observations",
259
+ x = "Species",
260
+ y = "Number of Dead Observations",
261
+ fill = "Year"
262
+ ) +
263
+ theme_minimal(base_size = 14)
264
+ } else {
265
+ top_species_plot <- ggplot() +
266
+ labs(title = "No 'taxon.name' column found", x = NULL, y = NULL) +
267
+ theme_void()
268
+ }
269
+
270
+ daily_quantile <- quantile(counts_by_day$n, probs = 0.90, na.rm = TRUE)
271
+ high_mortality_days <- counts_by_day %>%
272
+ filter(n >= daily_quantile) %>%
273
+ pull(obs_date)
274
+
275
+ merged_high <- merged_df_all %>%
276
+ mutate(obs_date = as.Date(`created_at_details.date`)) %>%
277
+ filter(obs_date %in% high_mortality_days)
278
+
279
+ if ("location" %in% names(merged_high)) {
280
+ location_df <- merged_high %>%
281
+ filter(!is.na(location) & location != "") %>%
282
+ separate(location, into = c("lat_str", "lon_str"), sep = ",", remove = FALSE) %>%
283
+ mutate(
284
+ latitude = as.numeric(lat_str),
285
+ longitude = as.numeric(lon_str)
286
+ )
287
+
288
+ if (nrow(location_df) == 0) {
289
+ map_hotspots_gg <- ggplot() +
290
+ labs(title = "No data in top 90th percentile days with valid location") +
291
+ theme_void()
292
+ } else {
293
+ min_lon <- min(location_df$longitude, na.rm = TRUE)
294
+ max_lon <- max(location_df$longitude, na.rm = TRUE)
295
+ min_lat <- min(location_df$latitude, na.rm = TRUE)
296
+ max_lat <- max(location_df$latitude, na.rm = TRUE)
297
+
298
+ map_hotspots_gg <- ggplot(location_df, aes(x = longitude, y = latitude, color = Window)) +
299
+ borders("world", fill = "gray80", colour = "white") +
300
+ geom_point(alpha = 0.6, size = 2) +
301
+ scale_color_manual(values = wes_colors) +
302
+ coord_quickmap(xlim = c(min_lon, max_lon),
303
+ ylim = c(min_lat, max_lat),
304
+ expand = TRUE) +
305
+ labs(
306
+ title = glue("Top 90th percentile mortality days ({paste(years, collapse=', ')})"),
307
+ x = "Longitude",
308
+ y = "Latitude",
309
+ color = "Year"
310
+ ) +
311
+ theme_minimal(base_size = 14)
312
+ }
313
+ } else {
314
+ map_hotspots_gg <- ggplot() +
315
+ labs(title = "No 'location' column for top 90% days map") +
316
+ theme_void()
317
+ }
318
+
319
+ if (!is.null(outdir)) {
320
+ if (!dir.exists(outdir)) {
321
+ dir.create(outdir, recursive = TRUE)
322
+ }
323
+
324
+ readr::write_csv(merged_high, file.path(outdir, "merged_df_top90.csv"))
325
+ ggsave(file.path(outdir, "daily_plot.png"),
326
+ daily_plot, width = 8, height = 5, dpi = 300)
327
+ ggsave(file.path(outdir, "top_species_plot.png"),
328
+ top_species_plot, width = 7, height = 7, dpi = 300)
329
+ ggsave(file.path(outdir, "map_hotspots.png"),
330
+ map_hotspots_gg, width = 8, height = 5, dpi = 300)
331
+ }
332
+
333
+ return(list(
334
+ merged_df = merged_high,
335
+ daily_plot = daily_plot,
336
+ top_species_plot = top_species_plot,
337
+ map_hotspots_gg = map_hotspots_gg,
338
+ daily_90th_quant = daily_quantile
339
+ ))
340
+ }
341
+
342
+
343
+ ##################################################################
344
+ # 3) Shiny App: UI + Server
345
+ ##################################################################
346
+
347
+ ui <- fluidPage(
348
+ theme = shinytheme("flatly"),
349
+
350
+ # -- Logo and Title at the top --
351
+ fluidRow(
352
+ column(
353
+ width = 2,
354
+ # .all_logos.png in www folder
355
+ tags$img(src = "www/all_logos.png", height = "400px")
356
+ ),
357
+ column(
358
+ width = 10,
359
+ titlePanel("Dead Wildlife Observations from iNaturalist")
360
+ )
361
+ ),
362
+ hr(),
363
+
364
+ # Layout with a sidebar containing TABS: Query, About, Participatory, How to Use
365
+ sidebarLayout(
366
+ sidebarPanel(
367
+ tabsetPanel(
368
+ id = "sidebar_tabs",
369
+
370
+ # == Query Panel ==
371
+ tabPanel(
372
+ title = "Query",
373
+ br(),
374
+ radioButtons("query_mode", "Choose Query Mode:",
375
+ choices = c("Use place_id" = "place",
376
+ "Two clicks to define bounding box" = "twoclick")),
377
+
378
+ # If place ID chosen, show numeric input
379
+ conditionalPanel(
380
+ condition = "input.query_mode == 'place'",
381
+ numericInput("place_id", "place_id (e.g. 1 for USA, 6712 for Canada)",
382
+ value = 1, min = 1, max = 999999, step = 1)
383
+ ),
384
+
385
+ # If two-click BBox chosen, show Leaflet & bounding box info
386
+ conditionalPanel(
387
+ condition = "input.query_mode == 'twoclick'",
388
+ helpText("Left-click once for the first corner, and once more for the opposite corner.
389
+ The SW and NE corners will be automatically computed."),
390
+ leafletOutput("map_two_click", height = "300px"),
391
+ br(),
392
+ actionButton("clear_bbox", "Clear bounding box"),
393
+ br(), br(),
394
+ verbatimTextOutput("bbox_coords")
395
+ ),
396
+
397
+ checkboxGroupInput("years", "Select Year(s):",
398
+ choices = 2021:2025,
399
+ selected = c(2021, 2022, 2023)),
400
+
401
+ selectInput("iconic_taxon", "Select Taxonomic Group:",
402
+ choices = c("Aves", "Mammalia", "Reptilia", "Amphibia", "Actinopterygii", "Mollusca", "Animalia"),
403
+ selected = "Aves"),
404
+
405
+ actionButton("run_query", "Run Query", icon = icon("play")),
406
+ hr(),
407
+ downloadButton("downloadData", "Download Top-90% CSV", icon = icon("download"))
408
+ ),
409
+
410
+ # == About Panel ==
411
+ tabPanel(
412
+ title = "About",
413
+ br(),
414
+ p("This Shiny application was created by Diego Ellis Soto (UC Berkeley).
415
+ It queries iNaturalist for observations that have been annotated as 'Dead' wildlife (term_id=17, term_value_id=19).
416
+ The data is fetched via the iNaturalist API and summarized here for scientific or conservation purposes.")
417
+ ),
418
+
419
+ # == Participatory Science Panel ==
420
+ tabPanel(
421
+ title = "Participatory Science",
422
+ br(),
423
+ p("Citizen science platforms like iNaturalist allow everyday people to collect and share data about local biodiversity.
424
+ Recording observations of dead wildlife can help track mortality events, disease spread, and other factors affecting animal populations."),
425
+ p("We encourage everyone to contribute their sightings responsibly, ensuring that any data on roadkill or other mortalities can help conservation efforts and
426
+ raise public awareness.")
427
+ ),
428
+
429
+ # == How To Use Panel ==
430
+ tabPanel(
431
+ title = "How to Use",
432
+ br(),
433
+ p("This application lets you retrieve data about dead wildlife observations from iNaturalist.
434
+ You can either specify a place_id (e.g., country or region) or define a custom bounding box with two clicks on the map.
435
+ After choosing which years and taxonomic group to query, press 'Run Query.'"),
436
+ p("These data are critical for understanding patterns of wildlife mortality, identifying hotspots of roadkill or disease, and informing conservation actions.
437
+ By systematically collecting and analyzing these records, conservation biologists and policymakers can make evidence-based decisions to protect wildlife populations.")
438
+ )
439
+ )
440
+ ),
441
+
442
+ # Main panel with tabbed outputs
443
+ mainPanel(
444
+ tabsetPanel(
445
+ tabPanel("Daily Time Series", withSpinner(plotOutput("dailyPlot"), type = 6)),
446
+ tabPanel("Top Species", withSpinner(plotOutput("speciesPlot"), type = 6)),
447
+ tabPanel("Hotspots Map (90th%)", withSpinner(plotOutput("hotspotMap"), type = 6)),
448
+ tabPanel("Data Table (Top-90%)", withSpinner(DT::dataTableOutput("dataTable"), type = 6))
449
+ )
450
+ )
451
+ )
452
+ )
453
+
454
+ server <- function(input, output, session) {
455
+
456
+ # Reactive values to store bounding box corners from two clicks
457
+ rv <- reactiveValues(
458
+ corner1 = NULL, # first click: (lat, lng)
459
+ corner2 = NULL, # second click: (lat, lng)
460
+ bbox = NULL # computed bounding box: c(swlat, swlng, nelat, nelng)
461
+ )
462
+
463
+ # Render a simple Leaflet map (no draw toolbar).
464
+ output$map_two_click <- renderLeaflet({
465
+ leaflet() %>%
466
+ addTiles() %>%
467
+ setView(lng = -100, lat = 40, zoom = 4)
468
+ })
469
+
470
+ # Observe map clicks when "Two clicks to define bounding box" is selected
471
+ observeEvent(input$map_two_click_click, {
472
+ req(input$query_mode == "twoclick")
473
+
474
+ click <- input$map_two_click_click
475
+ if (is.null(click)) return()
476
+
477
+ lat_clicked <- click$lat
478
+ lng_clicked <- click$lng
479
+
480
+ # If corner1 is NULL, store the first corner
481
+ if (is.null(rv$corner1)) {
482
+ rv$corner1 <- c(lat_clicked, lng_clicked)
483
+ showNotification("First corner set. Now click for the opposite corner.")
484
+
485
+ # Add marker for the first corner
486
+ leafletProxy("map_two_click") %>%
487
+ clearMarkers() %>% # remove any old markers
488
+ addMarkers(lng = lng_clicked, lat = lat_clicked,
489
+ popup = "Corner 1")
490
+
491
+ rv$corner2 <- NULL
492
+ rv$bbox <- NULL
493
+
494
+ } else {
495
+ # This is the second corner
496
+ rv$corner2 <- c(lat_clicked, lng_clicked)
497
+
498
+ # We'll compute bounding box from the two corners
499
+ lat_min <- min(rv$corner1[1], rv$corner2[1])
500
+ lat_max <- max(rv$corner1[1], rv$corner2[1])
501
+ lng_min <- min(rv$corner1[2], rv$corner2[2])
502
+ lng_max <- max(rv$corner1[2], rv$corner2[2])
503
+
504
+ rv$bbox <- c(lat_min, lng_min, lat_max, lng_max)
505
+
506
+ showNotification("Second corner set. Bounding box defined!", duration = 2)
507
+
508
+ # Add marker for the second corner & a rectangle to visualize the bounding box
509
+ leafletProxy("map_two_click") %>%
510
+ clearMarkers() %>%
511
+ addMarkers(lng = rv$corner1[2], lat = rv$corner1[1],
512
+ popup = "Corner 1") %>%
513
+ addMarkers(lng = rv$corner2[2], lat = rv$corner2[1],
514
+ popup = "Corner 2") %>%
515
+ # draw rectangle
516
+ clearShapes() %>%
517
+ addRectangles(
518
+ lng1 = lng_min, lat1 = lat_min,
519
+ lng2 = lng_max, lat2 = lat_max,
520
+ fillColor = "red", fillOpacity = 0.2,
521
+ color = "red"
522
+ )
523
+ }
524
+ })
525
+
526
+ # Button to clear bounding box and reset corners
527
+ observeEvent(input$clear_bbox, {
528
+ rv$corner1 <- NULL
529
+ rv$corner2 <- NULL
530
+ rv$bbox <- NULL
531
+
532
+ leafletProxy("map_two_click") %>%
533
+ clearMarkers() %>%
534
+ clearShapes()
535
+ })
536
+
537
+ # Show the user the bounding box
538
+ output$bbox_coords <- renderText({
539
+ req(input$query_mode == "twoclick")
540
+
541
+ if (is.null(rv$bbox)) {
542
+ "No bounding box defined yet."
543
+ } else {
544
+ paste0(
545
+ "Bounding box:\n",
546
+ "SW corner: (", rv$bbox[1], ", ", rv$bbox[2], ")\n",
547
+ "NE corner: (", rv$bbox[3], ", ", rv$bbox[4], ")"
548
+ )
549
+ }
550
+ })
551
+
552
+ # Reactive value to store final query results
553
+ result_data <- reactiveVal(NULL)
554
+
555
+ # "Run Query" button
556
+ observeEvent(input$run_query, {
557
+ req(input$years)
558
+ shiny::validate(shiny::need(length(input$years) > 0, "Please select at least one year"))
559
+
560
+ yrs <- as.numeric(input$years)
561
+
562
+ withProgress(message = 'Fetching data from iNaturalist...', value = 0, {
563
+ incProgress(0.3)
564
+
565
+ if (input$query_mode == "place") {
566
+ # place_id mode
567
+ place_id_val <- input$place_id
568
+ swlat_val <- NULL
569
+ swlng_val <- NULL
570
+ nelat_val <- NULL
571
+ nelng_val <- NULL
572
+
573
+ } else {
574
+ # two-click bounding box
575
+ shiny::validate(shiny::need(!is.null(rv$bbox), "Please click twice on the map to define bounding box."))
576
+
577
+ place_id_val <- NULL
578
+ swlat_val <- rv$bbox[1]
579
+ swlng_val <- rv$bbox[2]
580
+ nelat_val <- rv$bbox[3]
581
+ nelng_val <- rv$bbox[4]
582
+ }
583
+
584
+ query_res <- getDeadVertebrates_monthlyLoop(
585
+ years = yrs,
586
+ place_id = place_id_val,
587
+ swlat = swlat_val,
588
+ swlng = swlng_val,
589
+ nelat = nelat_val,
590
+ nelng = nelng_val,
591
+ iconic_taxa = input$iconic_taxon,
592
+ outdir = NULL
593
+ )
594
+
595
+ result_data(query_res)
596
+ incProgress(1)
597
+ })
598
+ })
599
+
600
+ # (a) Daily Plot
601
+ output$dailyPlot <- renderPlot({
602
+ req(result_data())
603
+ result_data()$daily_plot
604
+ })
605
+
606
+ # (b) Top Species Plot
607
+ output$speciesPlot <- renderPlot({
608
+ req(result_data())
609
+ result_data()$top_species_plot
610
+ })
611
+
612
+ # (c) Hotspots Map
613
+ output$hotspotMap <- renderPlot({
614
+ req(result_data())
615
+ result_data()$map_hotspots_gg
616
+ })
617
+
618
+ # (d) Data Table (top-90% days)
619
+ output$dataTable <- DT::renderDataTable({
620
+ req(result_data())
621
+ df <- result_data()$merged_df
622
+
623
+ if (nrow(df) == 0) {
624
+ return(DT::datatable(data.frame(Message = "No records found"), options = list(pageLength = 5)))
625
+ }
626
+
627
+ df <- df %>%
628
+ mutate(
629
+ inat_link = paste0(
630
+ "<a href='https://www.inaturalist.org/observations/",
631
+ id, "' target='_blank'>", id, "</a>"
632
+ )
633
+ )
634
+
635
+ # If photos exist
636
+ photo_col <- "taxon.default_photo.square_url"
637
+ if (photo_col %in% names(df)) {
638
+ df$image_thumb <- ifelse(
639
+ !is.na(df[[photo_col]]) & df[[photo_col]] != "",
640
+ paste0("<img src='", df[[photo_col]], "' width='50'/>"),
641
+ "No Img"
642
+ )
643
+ } else {
644
+ df$image_thumb <- "No Img"
645
+ }
646
+
647
+ show_cols <- c(
648
+ "inat_link", "image_thumb", "taxon.name", "created_at_details.date",
649
+ setdiff(names(df), c("inat_link", "image_thumb", "taxon.name", "created_at_details.date"))
650
+ )
651
+
652
+ DT::datatable(
653
+ df[ , show_cols, drop = FALSE],
654
+ escape = FALSE,
655
+ options = list(pageLength = 10, autoWidth = TRUE)
656
+ )
657
+ })
658
+
659
+ # Download CSV handler
660
+ output$downloadData <- downloadHandler(
661
+ filename = function() {
662
+ paste0("inat_dead_top90_", Sys.Date(), ".csv")
663
+ },
664
+ content = function(file) {
665
+ req(result_data())
666
+ readr::write_csv(result_data()$merged_df, file)
667
+ }
668
+ )
669
+ }
670
+
671
+ # Run the Shiny app
672
+ shinyApp(ui = ui, server = server)
app.R CHANGED
@@ -7,7 +7,7 @@ required_packages <- c(
7
  "httr", "jsonlite", "tidyverse", "glue", "lubridate",
8
  "wesanderson", "viridis", "shinycssloaders",
9
  "DT", "maps", "mapdata", "leaflet", "leaflet.extras",
10
- "shinythemes","shiny"
11
  )
12
 
13
  installed_packages <- rownames(installed.packages())
@@ -21,6 +21,7 @@ library(httr)
21
  library(jsonlite)
22
  library(tidyverse)
23
  library(glue)
 
24
  library(lubridate)
25
  library(wesanderson)
26
  library(viridis)
@@ -33,7 +34,6 @@ library(leaflet.extras)
33
  library(shinythemes)
34
  library(shiny)
35
 
36
-
37
  ##################################################################
38
  # 2) Mortality-Analysis Functions
39
  ##################################################################
@@ -46,22 +46,45 @@ fetch_dead_data_once <- function(
46
  nelng = NULL,
47
  start_date,
48
  end_date,
49
- iconic_taxa = "Aves",
 
 
 
50
  per_page = 1000,
51
  max_pages = 50
52
  ) {
53
  base_url <- "https://api.inaturalist.org/v1/observations"
54
 
55
- query_params <- glue(
56
- "iconic_taxa={iconic_taxa}&",
57
- "term_id=17&",
58
- "term_value_id=19&",
59
- "verifiable=true&",
60
- "d1={start_date}&d2={end_date}&",
61
- "order=desc&order_by=created_at&",
62
- "per_page={per_page}"
 
63
  )
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  loc_part <- ""
66
  if (!is.null(place_id)) {
67
  loc_part <- glue("&place_id={place_id}")
@@ -105,7 +128,7 @@ fetch_dead_data_once <- function(
105
  }
106
 
107
  current_page <- current_page + 1
108
- Sys.sleep(1) # polite pause for the API
109
  }
110
 
111
  observations_all <- bind_rows(observations_list)
@@ -120,7 +143,8 @@ fetch_dead_data_monthly <- function(
120
  swlng = NULL,
121
  nelat = NULL,
122
  nelng = NULL,
123
- iconic_taxa = "Aves"
 
124
  ) {
125
  monthly_list <- list()
126
 
@@ -128,7 +152,7 @@ fetch_dead_data_monthly <- function(
128
  start_date <- as.Date(glue("{year}-{sprintf('%02d', month_i)}-01"))
129
  end_date <- start_date %m+% months(1) %m-% days(1)
130
 
131
- if (year(start_date) != year) break
132
 
133
  message("\n--- Querying ", year, ", month ", month_i, " ---")
134
  df_month <- fetch_dead_data_once(
@@ -139,7 +163,8 @@ fetch_dead_data_monthly <- function(
139
  nelng = nelng,
140
  start_date = start_date,
141
  end_date = end_date,
142
- iconic_taxa= iconic_taxa
 
143
  )
144
  monthly_list[[month_i]] <- df_month
145
  }
@@ -150,16 +175,19 @@ fetch_dead_data_monthly <- function(
150
 
151
 
152
  getDeadVertebrates_monthlyLoop <- function(
153
- years = c(2022, 2023),
154
- place_id = NULL,
155
- swlat = NULL,
156
- swlng = NULL,
157
- nelat = NULL,
158
- nelng = NULL,
159
- iconic_taxa = "Aves",
160
- per_page = 1000,
161
- max_pages = 50,
162
- outdir = NULL
 
 
 
163
  ) {
164
  all_years_list <- list()
165
  for (yr in years) {
@@ -171,7 +199,8 @@ getDeadVertebrates_monthlyLoop <- function(
171
  swlng = swlng,
172
  nelat = nelat,
173
  nelng = nelng,
174
- iconic_taxa= iconic_taxa
 
175
  ) %>%
176
  mutate(Window = as.character(yr))
177
 
@@ -195,10 +224,12 @@ getDeadVertebrates_monthlyLoop <- function(
195
  theme_void()
196
 
197
  return(list(
 
198
  merged_df = merged_df_all,
199
  daily_plot = daily_plot,
200
  top_species_plot = top_species_plot,
201
- map_hotspots_gg = map_hotspots_gg
 
202
  ))
203
  }
204
 
@@ -206,7 +237,7 @@ getDeadVertebrates_monthlyLoop <- function(
206
  if (!dir.exists(outdir)) {
207
  dir.create(outdir, recursive = TRUE)
208
  }
209
- readr::write_csv(merged_df_all, file.path(outdir, "merged_df_top_all_data.csv"))
210
  }
211
 
212
  counts_by_day <- merged_df_all %>%
@@ -220,7 +251,7 @@ getDeadVertebrates_monthlyLoop <- function(
220
  daily_plot <- ggplot(counts_by_day, aes(x = obs_date, y = n, color = Window)) +
221
  geom_line(size = 1.2) +
222
  geom_point(size = 2) +
223
- scale_color_manual(values = wes_colors) +
224
  scale_x_date(date_labels = "%b", date_breaks = "1 month") +
225
  labs(
226
  title = glue("Daily 'Dead' Observations (Years {paste(years, collapse=', ')})"),
@@ -298,7 +329,7 @@ getDeadVertebrates_monthlyLoop <- function(
298
  map_hotspots_gg <- ggplot(location_df, aes(x = longitude, y = latitude, color = Window)) +
299
  borders("world", fill = "gray80", colour = "white") +
300
  geom_point(alpha = 0.6, size = 2) +
301
- scale_color_manual(values = wes_colors) +
302
  coord_quickmap(xlim = c(min_lon, max_lon),
303
  ylim = c(min_lat, max_lat),
304
  expand = TRUE) +
@@ -317,10 +348,6 @@ getDeadVertebrates_monthlyLoop <- function(
317
  }
318
 
319
  if (!is.null(outdir)) {
320
- if (!dir.exists(outdir)) {
321
- dir.create(outdir, recursive = TRUE)
322
- }
323
-
324
  readr::write_csv(merged_high, file.path(outdir, "merged_df_top90.csv"))
325
  ggsave(file.path(outdir, "daily_plot.png"),
326
  daily_plot, width = 8, height = 5, dpi = 300)
@@ -331,6 +358,7 @@ getDeadVertebrates_monthlyLoop <- function(
331
  }
332
 
333
  return(list(
 
334
  merged_df = merged_high,
335
  daily_plot = daily_plot,
336
  top_species_plot = top_species_plot,
@@ -339,11 +367,41 @@ getDeadVertebrates_monthlyLoop <- function(
339
  ))
340
  }
341
 
342
-
343
  ##################################################################
344
  # 3) Shiny App: UI + Server
345
  ##################################################################
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  ui <- fluidPage(
348
  theme = shinytheme("flatly"),
349
 
@@ -351,7 +409,6 @@ ui <- fluidPage(
351
  fluidRow(
352
  column(
353
  width = 2,
354
- # .all_logos.png in www folder
355
  tags$img(src = "www/all_logos.png", height = "400px")
356
  ),
357
  column(
@@ -361,7 +418,6 @@ ui <- fluidPage(
361
  ),
362
  hr(),
363
 
364
- # Layout with a sidebar containing TABS: Query, About, Participatory, How to Use
365
  sidebarLayout(
366
  sidebarPanel(
367
  tabsetPanel(
@@ -371,22 +427,31 @@ ui <- fluidPage(
371
  tabPanel(
372
  title = "Query",
373
  br(),
374
- radioButtons("query_mode", "Choose Query Mode:",
375
- choices = c("Use place_id" = "place",
376
- "Two clicks to define bounding box" = "twoclick")),
 
 
 
 
 
 
 
 
 
 
377
 
378
- # If place ID chosen, show numeric input
379
  conditionalPanel(
380
- condition = "input.query_mode == 'place'",
381
- numericInput("place_id", "place_id (e.g. 1 for USA, 6712 for Canada)",
382
  value = 1, min = 1, max = 999999, step = 1)
383
  ),
384
 
385
- # If two-click BBox chosen, show Leaflet & bounding box info
386
  conditionalPanel(
387
- condition = "input.query_mode == 'twoclick'",
388
- helpText("Left-click once for the first corner, and once more for the opposite corner.
389
- The SW and NE corners will be automatically computed."),
390
  leafletOutput("map_two_click", height = "300px"),
391
  br(),
392
  actionButton("clear_bbox", "Clear bounding box"),
@@ -394,17 +459,37 @@ ui <- fluidPage(
394
  verbatimTextOutput("bbox_coords")
395
  ),
396
 
 
397
  checkboxGroupInput("years", "Select Year(s):",
398
- choices = 2021:2025,
399
- selected = c(2021, 2022, 2023)),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- selectInput("iconic_taxon", "Select Taxonomic Group:",
402
- choices = c("Aves", "Mammalia", "Reptilia", "Amphibia", "Actinopterygii", "Mollusca", "Animalia"),
403
- selected = "Aves"),
404
 
405
  actionButton("run_query", "Run Query", icon = icon("play")),
406
  hr(),
407
- downloadButton("downloadData", "Download Top-90% CSV", icon = icon("download"))
 
 
 
408
  ),
409
 
410
  # == About Panel ==
@@ -431,10 +516,9 @@ ui <- fluidPage(
431
  title = "How to Use",
432
  br(),
433
  p("This application lets you retrieve data about dead wildlife observations from iNaturalist.
434
- You can either specify a place_id (e.g., country or region) or define a custom bounding box with two clicks on the map.
435
- After choosing which years and taxonomic group to query, press 'Run Query.'"),
436
- p("These data are critical for understanding patterns of wildlife mortality, identifying hotspots of roadkill or disease, and informing conservation actions.
437
- By systematically collecting and analyzing these records, conservation biologists and policymakers can make evidence-based decisions to protect wildlife populations.")
438
  )
439
  )
440
  ),
@@ -453,23 +537,20 @@ ui <- fluidPage(
453
 
454
  server <- function(input, output, session) {
455
 
456
- # Reactive values to store bounding box corners from two clicks
457
  rv <- reactiveValues(
458
- corner1 = NULL, # first click: (lat, lng)
459
- corner2 = NULL, # second click: (lat, lng)
460
- bbox = NULL # computed bounding box: c(swlat, swlng, nelat, nelng)
461
  )
462
 
463
- # Render a simple Leaflet map (no draw toolbar).
464
  output$map_two_click <- renderLeaflet({
465
  leaflet() %>%
466
  addTiles() %>%
467
  setView(lng = -100, lat = 40, zoom = 4)
468
  })
469
 
470
- # Observe map clicks when "Two clicks to define bounding box" is selected
471
  observeEvent(input$map_two_click_click, {
472
- req(input$query_mode == "twoclick")
473
 
474
  click <- input$map_two_click_click
475
  if (is.null(click)) return()
@@ -477,25 +558,20 @@ server <- function(input, output, session) {
477
  lat_clicked <- click$lat
478
  lng_clicked <- click$lng
479
 
480
- # If corner1 is NULL, store the first corner
481
  if (is.null(rv$corner1)) {
482
  rv$corner1 <- c(lat_clicked, lng_clicked)
483
  showNotification("First corner set. Now click for the opposite corner.")
484
 
485
- # Add marker for the first corner
486
  leafletProxy("map_two_click") %>%
487
- clearMarkers() %>% # remove any old markers
488
- addMarkers(lng = lng_clicked, lat = lat_clicked,
489
- popup = "Corner 1")
490
 
491
  rv$corner2 <- NULL
492
- rv$bbox <- NULL
493
 
494
  } else {
495
- # This is the second corner
496
  rv$corner2 <- c(lat_clicked, lng_clicked)
497
 
498
- # We'll compute bounding box from the two corners
499
  lat_min <- min(rv$corner1[1], rv$corner2[1])
500
  lat_max <- max(rv$corner1[1], rv$corner2[1])
501
  lng_min <- min(rv$corner1[2], rv$corner2[2])
@@ -505,14 +581,10 @@ server <- function(input, output, session) {
505
 
506
  showNotification("Second corner set. Bounding box defined!", duration = 2)
507
 
508
- # Add marker for the second corner & a rectangle to visualize the bounding box
509
  leafletProxy("map_two_click") %>%
510
  clearMarkers() %>%
511
- addMarkers(lng = rv$corner1[2], lat = rv$corner1[1],
512
- popup = "Corner 1") %>%
513
- addMarkers(lng = rv$corner2[2], lat = rv$corner2[1],
514
- popup = "Corner 2") %>%
515
- # draw rectangle
516
  clearShapes() %>%
517
  addRectangles(
518
  lng1 = lng_min, lat1 = lat_min,
@@ -523,7 +595,6 @@ server <- function(input, output, session) {
523
  }
524
  })
525
 
526
- # Button to clear bounding box and reset corners
527
  observeEvent(input$clear_bbox, {
528
  rv$corner1 <- NULL
529
  rv$corner2 <- NULL
@@ -534,9 +605,8 @@ server <- function(input, output, session) {
534
  clearShapes()
535
  })
536
 
537
- # Show the user the bounding box
538
  output$bbox_coords <- renderText({
539
- req(input$query_mode == "twoclick")
540
 
541
  if (is.null(rv$bbox)) {
542
  "No bounding box defined yet."
@@ -549,38 +619,45 @@ server <- function(input, output, session) {
549
  }
550
  })
551
 
552
- # Reactive value to store final query results
553
  result_data <- reactiveVal(NULL)
554
 
555
- # "Run Query" button
556
  observeEvent(input$run_query, {
557
  req(input$years)
558
- shiny::validate(shiny::need(length(input$years) > 0, "Please select at least one year"))
559
 
560
  yrs <- as.numeric(input$years)
561
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
  withProgress(message = 'Fetching data from iNaturalist...', value = 0, {
563
  incProgress(0.3)
564
 
565
- if (input$query_mode == "place") {
566
- # place_id mode
567
- place_id_val <- input$place_id
568
- swlat_val <- NULL
569
- swlng_val <- NULL
570
- nelat_val <- NULL
571
- nelng_val <- NULL
572
-
573
- } else {
574
- # two-click bounding box
575
- shiny::validate(shiny::need(!is.null(rv$bbox), "Please click twice on the map to define bounding box."))
576
-
577
- place_id_val <- NULL
578
- swlat_val <- rv$bbox[1]
579
- swlng_val <- rv$bbox[2]
580
- nelat_val <- rv$bbox[3]
581
- nelng_val <- rv$bbox[4]
582
- }
583
-
584
  query_res <- getDeadVertebrates_monthlyLoop(
585
  years = yrs,
586
  place_id = place_id_val,
@@ -588,8 +665,9 @@ server <- function(input, output, session) {
588
  swlng = swlng_val,
589
  nelat = nelat_val,
590
  nelng = nelng_val,
591
- iconic_taxa = input$iconic_taxon,
592
- outdir = NULL
 
593
  )
594
 
595
  result_data(query_res)
@@ -597,31 +675,28 @@ server <- function(input, output, session) {
597
  })
598
  })
599
 
600
- # (a) Daily Plot
601
  output$dailyPlot <- renderPlot({
602
  req(result_data())
603
  result_data()$daily_plot
604
  })
605
 
606
- # (b) Top Species Plot
607
  output$speciesPlot <- renderPlot({
608
  req(result_data())
609
  result_data()$top_species_plot
610
  })
611
 
612
- # (c) Hotspots Map
613
  output$hotspotMap <- renderPlot({
614
  req(result_data())
615
  result_data()$map_hotspots_gg
616
  })
617
 
618
- # (d) Data Table (top-90% days)
619
  output$dataTable <- DT::renderDataTable({
620
  req(result_data())
621
- df <- result_data()$merged_df
622
 
623
  if (nrow(df) == 0) {
624
- return(DT::datatable(data.frame(Message = "No records found"), options = list(pageLength = 5)))
 
625
  }
626
 
627
  df <- df %>%
@@ -632,7 +707,6 @@ server <- function(input, output, session) {
632
  )
633
  )
634
 
635
- # If photos exist
636
  photo_col <- "taxon.default_photo.square_url"
637
  if (photo_col %in% names(df)) {
638
  df$image_thumb <- ifelse(
@@ -656,8 +730,7 @@ server <- function(input, output, session) {
656
  )
657
  })
658
 
659
- # Download CSV handler
660
- output$downloadData <- downloadHandler(
661
  filename = function() {
662
  paste0("inat_dead_top90_", Sys.Date(), ".csv")
663
  },
@@ -666,7 +739,16 @@ server <- function(input, output, session) {
666
  readr::write_csv(result_data()$merged_df, file)
667
  }
668
  )
 
 
 
 
 
 
 
 
 
 
669
  }
670
 
671
- # Run the Shiny app
672
  shinyApp(ui = ui, server = server)
 
7
  "httr", "jsonlite", "tidyverse", "glue", "lubridate",
8
  "wesanderson", "viridis", "shinycssloaders",
9
  "DT", "maps", "mapdata", "leaflet", "leaflet.extras",
10
+ "shinythemes", "shiny"
11
  )
12
 
13
  installed_packages <- rownames(installed.packages())
 
21
  library(jsonlite)
22
  library(tidyverse)
23
  library(glue)
24
+ require(viridis)
25
  library(lubridate)
26
  library(wesanderson)
27
  library(viridis)
 
34
  library(shinythemes)
35
  library(shiny)
36
 
 
37
  ##################################################################
38
  # 2) Mortality-Analysis Functions
39
  ##################################################################
 
46
  nelng = NULL,
47
  start_date,
48
  end_date,
49
+ iconic_taxa = NULL,
50
+ taxon_name = NULL,
51
+ # Removed conservation_status from UI, but function can still handle it if needed
52
+ conservation_status = NULL,
53
  per_page = 1000,
54
  max_pages = 50
55
  ) {
56
  base_url <- "https://api.inaturalist.org/v1/observations"
57
 
58
+ q_parts <- list(
59
+ "term_id=17",
60
+ "term_value_id=19",
61
+ "verifiable=true",
62
+ glue("d1={start_date}"),
63
+ glue("d2={end_date}"),
64
+ "order=desc",
65
+ "order_by=created_at",
66
+ glue("per_page={per_page}")
67
  )
68
 
69
+ if (!is.null(iconic_taxa) && iconic_taxa != "") {
70
+ q_parts <- c(q_parts, glue("iconic_taxa={iconic_taxa}"))
71
+ }
72
+
73
+ if (!is.null(taxon_name) && taxon_name != "") {
74
+ q_parts <- c(q_parts, glue("taxon_name={URLencode(taxon_name)}"))
75
+ }
76
+
77
+ if (!is.null(conservation_status) && conservation_status != "") {
78
+ # The function remains flexible if you ever decide to pass a status again
79
+ if (!grepl("=", conservation_status, fixed = TRUE)) {
80
+ q_parts <- c(q_parts, glue("cs={URLencode(conservation_status)}"))
81
+ } else {
82
+ q_parts <- c(q_parts, conservation_status)
83
+ }
84
+ }
85
+
86
+ query_params <- paste(q_parts, collapse = "&")
87
+
88
  loc_part <- ""
89
  if (!is.null(place_id)) {
90
  loc_part <- glue("&place_id={place_id}")
 
128
  }
129
 
130
  current_page <- current_page + 1
131
+ Sys.sleep(1) # polite pause
132
  }
133
 
134
  observations_all <- bind_rows(observations_list)
 
143
  swlng = NULL,
144
  nelat = NULL,
145
  nelng = NULL,
146
+ iconic_taxa = NULL,
147
+ taxon_name = NULL
148
  ) {
149
  monthly_list <- list()
150
 
 
152
  start_date <- as.Date(glue("{year}-{sprintf('%02d', month_i)}-01"))
153
  end_date <- start_date %m+% months(1) %m-% days(1)
154
 
155
+ if (lubridate::year(start_date) != year) break
156
 
157
  message("\n--- Querying ", year, ", month ", month_i, " ---")
158
  df_month <- fetch_dead_data_once(
 
163
  nelng = nelng,
164
  start_date = start_date,
165
  end_date = end_date,
166
+ iconic_taxa= iconic_taxa,
167
+ taxon_name = taxon_name
168
  )
169
  monthly_list[[month_i]] <- df_month
170
  }
 
175
 
176
 
177
  getDeadVertebrates_monthlyLoop <- function(
178
+ years = c(2022, 2023),
179
+ place_id = NULL,
180
+ swlat = NULL,
181
+ swlng = NULL,
182
+ nelat = NULL,
183
+ nelng = NULL,
184
+ iconic_taxa = NULL,
185
+ taxon_name = NULL,
186
+ # Removed from UI, but function remains flexible
187
+ conservation_status = NULL,
188
+ per_page = 1000,
189
+ max_pages = 50,
190
+ outdir = NULL
191
  ) {
192
  all_years_list <- list()
193
  for (yr in years) {
 
199
  swlng = swlng,
200
  nelat = nelat,
201
  nelng = nelng,
202
+ iconic_taxa= iconic_taxa,
203
+ taxon_name = taxon_name
204
  ) %>%
205
  mutate(Window = as.character(yr))
206
 
 
224
  theme_void()
225
 
226
  return(list(
227
+ merged_df_all = merged_df_all,
228
  merged_df = merged_df_all,
229
  daily_plot = daily_plot,
230
  top_species_plot = top_species_plot,
231
+ map_hotspots_gg = map_hotspots_gg,
232
+ daily_90th_quant = NA
233
  ))
234
  }
235
 
 
237
  if (!dir.exists(outdir)) {
238
  dir.create(outdir, recursive = TRUE)
239
  }
240
+ readr::write_csv(merged_df_all, file.path(outdir, "merged_df_ALL_data.csv"))
241
  }
242
 
243
  counts_by_day <- merged_df_all %>%
 
251
  daily_plot <- ggplot(counts_by_day, aes(x = obs_date, y = n, color = Window)) +
252
  geom_line(size = 1.2) +
253
  geom_point(size = 2) +
254
+ scale_color_viridis_d() +
255
  scale_x_date(date_labels = "%b", date_breaks = "1 month") +
256
  labs(
257
  title = glue("Daily 'Dead' Observations (Years {paste(years, collapse=', ')})"),
 
329
  map_hotspots_gg <- ggplot(location_df, aes(x = longitude, y = latitude, color = Window)) +
330
  borders("world", fill = "gray80", colour = "white") +
331
  geom_point(alpha = 0.6, size = 2) +
332
+ scale_color_viridis_d() +
333
  coord_quickmap(xlim = c(min_lon, max_lon),
334
  ylim = c(min_lat, max_lat),
335
  expand = TRUE) +
 
348
  }
349
 
350
  if (!is.null(outdir)) {
 
 
 
 
351
  readr::write_csv(merged_high, file.path(outdir, "merged_df_top90.csv"))
352
  ggsave(file.path(outdir, "daily_plot.png"),
353
  daily_plot, width = 8, height = 5, dpi = 300)
 
358
  }
359
 
360
  return(list(
361
+ merged_df_all = merged_df_all,
362
  merged_df = merged_high,
363
  daily_plot = daily_plot,
364
  top_species_plot = top_species_plot,
 
367
  ))
368
  }
369
 
 
370
  ##################################################################
371
  # 3) Shiny App: UI + Server
372
  ##################################################################
373
 
374
+ # Example named vector of countries with known iNaturalist place_ids.
375
+ # This is truncated. For a truly complete list, you may load from an external file
376
+ # or build your own vector with place_ids from iNaturalist.
377
+ country_choices <- c(
378
+ "Afghanistan" = 4337,
379
+ "Albania" = 8563,
380
+ "Algeria" = 9755,
381
+ "Andorra" = 12244,
382
+ "Angola" = 12242,
383
+ "Argentina" = 28,
384
+ "Armenia" = 8537,
385
+ "Australia" = 6744,
386
+ "Austria" = 8168,
387
+ "Bahamas" = 7691,
388
+ "Bahrain" = 11873,
389
+ "Bangladesh" = 11884,
390
+ "Belarus" = 8517,
391
+ "Belgium" = 8034,
392
+ "Belize" = 6881,
393
+ "Benin" = 12345,
394
+ "Bhutan" = 11905,
395
+ "Bolivia" = 40,
396
+ "Bosnia and Herzegovina" = 8565,
397
+ "Botswana" = 12271,
398
+ # ...
399
+ "United States" = 1,
400
+ "Canada" = 6712,
401
+ "Mexico" = 6883
402
+ # (Add more as needed)
403
+ )
404
+
405
  ui <- fluidPage(
406
  theme = shinytheme("flatly"),
407
 
 
409
  fluidRow(
410
  column(
411
  width = 2,
 
412
  tags$img(src = "www/all_logos.png", height = "400px")
413
  ),
414
  column(
 
418
  ),
419
  hr(),
420
 
 
421
  sidebarLayout(
422
  sidebarPanel(
423
  tabsetPanel(
 
427
  tabPanel(
428
  title = "Query",
429
  br(),
430
+ # 1) Radio to pick how to define region
431
+ radioButtons("region_mode", "Region Input Mode:",
432
+ choices = c("Select Country by Name" = "country",
433
+ "Enter Numeric place_id" = "place",
434
+ "Two-Click Bounding Box" = "bbox")),
435
+
436
+ # All countries from a large list
437
+ conditionalPanel(
438
+ condition = "input.region_mode == 'country'",
439
+ selectInput("country_selection", "Select Country:",
440
+ choices = country_choices,
441
+ selected = 6712) # e.g., Canada by default
442
+ ),
443
 
444
+ # Manually typed place_id
445
  conditionalPanel(
446
+ condition = "input.region_mode == 'place'",
447
+ numericInput("place_id", "Numeric place_id (e.g. 1 = USA, 6712 = Canada)",
448
  value = 1, min = 1, max = 999999, step = 1)
449
  ),
450
 
451
+ # Two-click bounding box
452
  conditionalPanel(
453
+ condition = "input.region_mode == 'bbox'",
454
+ helpText("Left-click once for the SW corner, once more for the NE corner."),
 
455
  leafletOutput("map_two_click", height = "300px"),
456
  br(),
457
  actionButton("clear_bbox", "Clear bounding box"),
 
459
  verbatimTextOutput("bbox_coords")
460
  ),
461
 
462
+ # Years
463
  checkboxGroupInput("years", "Select Year(s):",
464
+ choices = 2018:2025,
465
+ selected = c(2022, 2023)),
466
+
467
+ # 2) Radio: either 'Taxon Class' or exact species name
468
+ radioButtons("query_type", "Query By:",
469
+ choices = c("Taxon Class" = "iconic",
470
+ "Exact Species Name" = "species")),
471
+ # If 'Taxon Class' is chosen:
472
+ conditionalPanel(
473
+ condition = "input.query_type == 'iconic'",
474
+ selectInput("iconic_taxon", "Select Taxon Class:",
475
+ choices = c("Aves", "Mammalia", "Reptilia", "Amphibia",
476
+ "Actinopterygii", "Mollusca", "Animalia"),
477
+ selected = "Aves")
478
+ ),
479
+ # If species name is chosen:
480
+ conditionalPanel(
481
+ condition = "input.query_type == 'species'",
482
+ textInput("species_name", "Enter exact species name (e.g. Puma concolor)", "")
483
+ ),
484
 
485
+ # Removed the Conservation Status input
 
 
486
 
487
  actionButton("run_query", "Run Query", icon = icon("play")),
488
  hr(),
489
+ # Two Download buttons: top 90% vs. ALL data
490
+ downloadButton("downloadTop90", "Download Top-90% CSV", icon = icon("download")),
491
+ br(), br(),
492
+ downloadButton("downloadAll", "Download ALL Data CSV", icon = icon("download"))
493
  ),
494
 
495
  # == About Panel ==
 
516
  title = "How to Use",
517
  br(),
518
  p("This application lets you retrieve data about dead wildlife observations from iNaturalist.
519
+ You can choose a country name, manually provide a numeric place_id, or define a custom bounding box by clicking twice on the map."),
520
+ p("You can also decide whether to query by taxon class (e.g., Aves) or by exact species name (e.g. Puma concolor)."),
521
+ p("After selecting your inputs, press 'Run Query.' Two separate CSV downloads are provided: (1) for all data retrieved, and (2) for only the top-90% mortality days (for hotspot analysis).")
 
522
  )
523
  )
524
  ),
 
537
 
538
  server <- function(input, output, session) {
539
 
 
540
  rv <- reactiveValues(
541
+ corner1 = NULL,
542
+ corner2 = NULL,
543
+ bbox = NULL
544
  )
545
 
 
546
  output$map_two_click <- renderLeaflet({
547
  leaflet() %>%
548
  addTiles() %>%
549
  setView(lng = -100, lat = 40, zoom = 4)
550
  })
551
 
 
552
  observeEvent(input$map_two_click_click, {
553
+ req(input$region_mode == "bbox")
554
 
555
  click <- input$map_two_click_click
556
  if (is.null(click)) return()
 
558
  lat_clicked <- click$lat
559
  lng_clicked <- click$lng
560
 
 
561
  if (is.null(rv$corner1)) {
562
  rv$corner1 <- c(lat_clicked, lng_clicked)
563
  showNotification("First corner set. Now click for the opposite corner.")
564
 
 
565
  leafletProxy("map_two_click") %>%
566
+ clearMarkers() %>%
567
+ addMarkers(lng = lng_clicked, lat = lat_clicked, popup = "Corner 1")
 
568
 
569
  rv$corner2 <- NULL
570
+ rv$bbox <- NULL
571
 
572
  } else {
 
573
  rv$corner2 <- c(lat_clicked, lng_clicked)
574
 
 
575
  lat_min <- min(rv$corner1[1], rv$corner2[1])
576
  lat_max <- max(rv$corner1[1], rv$corner2[1])
577
  lng_min <- min(rv$corner1[2], rv$corner2[2])
 
581
 
582
  showNotification("Second corner set. Bounding box defined!", duration = 2)
583
 
 
584
  leafletProxy("map_two_click") %>%
585
  clearMarkers() %>%
586
+ addMarkers(lng = rv$corner1[2], lat = rv$corner1[1], popup = "Corner 1") %>%
587
+ addMarkers(lng = rv$corner2[2], lat = rv$corner2[1], popup = "Corner 2") %>%
 
 
 
588
  clearShapes() %>%
589
  addRectangles(
590
  lng1 = lng_min, lat1 = lat_min,
 
595
  }
596
  })
597
 
 
598
  observeEvent(input$clear_bbox, {
599
  rv$corner1 <- NULL
600
  rv$corner2 <- NULL
 
605
  clearShapes()
606
  })
607
 
 
608
  output$bbox_coords <- renderText({
609
+ req(input$region_mode == "bbox")
610
 
611
  if (is.null(rv$bbox)) {
612
  "No bounding box defined yet."
 
619
  }
620
  })
621
 
 
622
  result_data <- reactiveVal(NULL)
623
 
 
624
  observeEvent(input$run_query, {
625
  req(input$years)
626
+ shiny::validate(shiny::need(length(input$years) > 0, "Please select at least one year."))
627
 
628
  yrs <- as.numeric(input$years)
629
 
630
+ place_id_val <- NULL
631
+ swlat_val <- NULL
632
+ swlng_val <- NULL
633
+ nelat_val <- NULL
634
+ nelng_val <- NULL
635
+
636
+ if (input$region_mode == "country") {
637
+ place_id_val <- as.numeric(input$country_selection)
638
+
639
+ } else if (input$region_mode == "place") {
640
+ place_id_val <- input$place_id
641
+
642
+ } else {
643
+ shiny::validate(shiny::need(!is.null(rv$bbox), "Please click twice on the map to define bounding box."))
644
+ swlat_val <- rv$bbox[1]
645
+ swlng_val <- rv$bbox[2]
646
+ nelat_val <- rv$bbox[3]
647
+ nelng_val <- rv$bbox[4]
648
+ }
649
+
650
+ iconic_val <- NULL
651
+ species_val <- NULL
652
+ if (input$query_type == "iconic") {
653
+ iconic_val <- input$iconic_taxon
654
+ } else {
655
+ species_val <- input$species_name
656
+ }
657
+
658
  withProgress(message = 'Fetching data from iNaturalist...', value = 0, {
659
  incProgress(0.3)
660
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  query_res <- getDeadVertebrates_monthlyLoop(
662
  years = yrs,
663
  place_id = place_id_val,
 
665
  swlng = swlng_val,
666
  nelat = nelat_val,
667
  nelng = nelng_val,
668
+ iconic_taxa = iconic_val,
669
+ taxon_name = species_val
670
+ # Removed conservation_status from the call
671
  )
672
 
673
  result_data(query_res)
 
675
  })
676
  })
677
 
 
678
  output$dailyPlot <- renderPlot({
679
  req(result_data())
680
  result_data()$daily_plot
681
  })
682
 
 
683
  output$speciesPlot <- renderPlot({
684
  req(result_data())
685
  result_data()$top_species_plot
686
  })
687
 
 
688
  output$hotspotMap <- renderPlot({
689
  req(result_data())
690
  result_data()$map_hotspots_gg
691
  })
692
 
 
693
  output$dataTable <- DT::renderDataTable({
694
  req(result_data())
695
+ df <- result_data()$merged_df # top 90% subset
696
 
697
  if (nrow(df) == 0) {
698
+ return(DT::datatable(data.frame(Message = "No records found"),
699
+ options = list(pageLength = 5)))
700
  }
701
 
702
  df <- df %>%
 
707
  )
708
  )
709
 
 
710
  photo_col <- "taxon.default_photo.square_url"
711
  if (photo_col %in% names(df)) {
712
  df$image_thumb <- ifelse(
 
730
  )
731
  })
732
 
733
+ output$downloadTop90 <- downloadHandler(
 
734
  filename = function() {
735
  paste0("inat_dead_top90_", Sys.Date(), ".csv")
736
  },
 
739
  readr::write_csv(result_data()$merged_df, file)
740
  }
741
  )
742
+
743
+ output$downloadAll <- downloadHandler(
744
+ filename = function() {
745
+ paste0("inat_dead_ALL_", Sys.Date(), ".csv")
746
+ },
747
+ content = function(file) {
748
+ req(result_data())
749
+ readr::write_csv(result_data()$merged_df_all, file)
750
+ }
751
+ )
752
  }
753
 
 
754
  shinyApp(ui = ui, server = server)