Now species specific query, full download
Browse files- .ipynb_checkpoints/app-checkpoint.R +672 -0
- 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 =
|
|
|
|
|
|
|
50 |
per_page = 1000,
|
51 |
max_pages = 50
|
52 |
) {
|
53 |
base_url <- "https://api.inaturalist.org/v1/observations"
|
54 |
|
55 |
-
|
56 |
-
"
|
57 |
-
"
|
58 |
-
"
|
59 |
-
"
|
60 |
-
"
|
61 |
-
"order=desc
|
62 |
-
"
|
|
|
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
|
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 =
|
|
|
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
|
154 |
-
place_id
|
155 |
-
swlat
|
156 |
-
swlng
|
157 |
-
nelat
|
158 |
-
nelng
|
159 |
-
iconic_taxa
|
160 |
-
|
161 |
-
|
162 |
-
|
|
|
|
|
|
|
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, "
|
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 |
-
|
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 |
-
|
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 |
-
|
375 |
-
|
376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
377 |
|
378 |
-
#
|
379 |
conditionalPanel(
|
380 |
-
condition = "input.
|
381 |
-
numericInput("place_id", "place_id (e.g. 1
|
382 |
value = 1, min = 1, max = 999999, step = 1)
|
383 |
),
|
384 |
|
385 |
-
#
|
386 |
conditionalPanel(
|
387 |
-
condition = "input.
|
388 |
-
helpText("Left-click once for the
|
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 =
|
399 |
-
selected = c(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
|
401 |
-
|
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 |
-
|
|
|
|
|
|
|
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
|
435 |
-
|
436 |
-
p("
|
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,
|
459 |
-
corner2 = NULL,
|
460 |
-
bbox = NULL
|
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$
|
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() %>%
|
488 |
-
addMarkers(lng = lng_clicked, lat = lat_clicked,
|
489 |
-
popup = "Corner 1")
|
490 |
|
491 |
rv$corner2 <- NULL
|
492 |
-
rv$bbox
|
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 |
-
|
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$
|
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 =
|
592 |
-
|
|
|
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"),
|
|
|
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 |
-
|
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)
|