|
|
|
|
|
|
|
|
|
|
|
generate_station_label <- function(name, id, state, start_date, end_date, detailed_summary, |
|
|
overall_start = NULL, overall_end = NULL, resolution = NULL) { |
|
|
tryCatch( |
|
|
{ |
|
|
format_range <- function(start_val, end_val) { |
|
|
format_val <- function(value, is_end = FALSE) { |
|
|
if (is.null(value) || is.na(value) || value == "") { |
|
|
return("Unknown") |
|
|
} |
|
|
|
|
|
if (inherits(value, "Date")) { |
|
|
return(format(value, "%Y-%m-%d")) |
|
|
} |
|
|
|
|
|
value <- as.character(value) |
|
|
if (value == "99999999") { |
|
|
return(if (is_end) "Present" else "Unknown") |
|
|
} |
|
|
if (value == "00000000") { |
|
|
return("Unknown") |
|
|
} |
|
|
|
|
|
if (grepl("^\\d{4}-\\d{2}-\\d{2}$", value)) { |
|
|
d <- as.Date(value) |
|
|
} else { |
|
|
d <- as.Date(value, format = "%Y%m%d") |
|
|
} |
|
|
if (is.na(d)) { |
|
|
return("Unknown") |
|
|
} |
|
|
format(d, "%Y-%m-%d") |
|
|
} |
|
|
|
|
|
paste0(format_val(start_val, FALSE), " to ", format_val(end_val, TRUE)) |
|
|
} |
|
|
|
|
|
res_label <- if (is.null(resolution) || is.na(resolution) || resolution == "") { |
|
|
"Data" |
|
|
} else { |
|
|
paste0(toupper(substr(resolution, 1, 1)), substr(resolution, 2, nchar(resolution))) |
|
|
} |
|
|
|
|
|
station_range <- format_range(start_date, end_date) |
|
|
coverage_start <- if (is.null(overall_start)) start_date else overall_start |
|
|
coverage_end <- if (is.null(overall_end)) end_date else overall_end |
|
|
coverage_range <- format_range(coverage_start, coverage_end) |
|
|
summary_text <- if (is.null(detailed_summary) || is.na(detailed_summary) || detailed_summary == "") { |
|
|
"No Data" |
|
|
} else { |
|
|
detailed_summary |
|
|
} |
|
|
|
|
|
paste0( |
|
|
"<div style='font-size:18px; max-width: 400px;'>", |
|
|
"<b>", htmltools::htmlEscape(name), "</b> (", id, ")<br>", |
|
|
"<span style='font-size:80%;'>State: ", htmltools::htmlEscape(state), "</span><br>", |
|
|
"<span style='font-size:80%; color:#555;'>", |
|
|
"Station Active: ", station_range, |
|
|
"</span><br>", |
|
|
"<span style='font-size:80%; color:#555;'>", |
|
|
res_label, " Coverage: ", coverage_range, |
|
|
"</span><br>", |
|
|
"<div style='font-size:80%; color:#333; font-weight:bold; margin-top:5px;'>", |
|
|
"Data Availability (", res_label, "):</div>", |
|
|
"<div style='font-size:75%; color:#333; line-height: 1.1;'>• ", |
|
|
paste0( |
|
|
gsub(", ", "<br>• ", htmltools::htmlEscape(summary_text)), |
|
|
"<br><small><i>Note: 'Present' indicates ongoing monitoring;<br>", |
|
|
"some parameters may have<br>", |
|
|
"upload delays.</i></small>" |
|
|
), |
|
|
"</div>", |
|
|
"</div>" |
|
|
) |
|
|
}, |
|
|
error = function(e) { |
|
|
paste0("<b>", htmltools::htmlEscape(name), "</b> (", id, ")") |
|
|
} |
|
|
) |
|
|
} |
|
|
|
|
|
server <- function(input, output, session) { |
|
|
|
|
|
current_station_id <- reactiveVal(NULL) |
|
|
station_data <- reactiveVal(NULL) |
|
|
loading_status <- reactiveVal(FALSE) |
|
|
loading_diagnostics <- reactiveVal("") |
|
|
previous_station_choices <- reactiveVal(NULL) |
|
|
previous_date_range <- reactiveVal(NULL) |
|
|
url_initialized <- reactiveVal(FALSE) |
|
|
|
|
|
|
|
|
style_change_trigger <- reactiveVal(0) |
|
|
map_initialized <- reactiveVal(FALSE) |
|
|
stations_before_id <- reactiveVal(NULL) |
|
|
current_raster_layers <- reactiveVal(character(0)) |
|
|
|
|
|
|
|
|
map_bounds <- list( |
|
|
lng_min = 5.5, |
|
|
lat_min = 47.0, |
|
|
lng_max = 15.5, |
|
|
lat_max = 55.5 |
|
|
) |
|
|
|
|
|
|
|
|
parse_url_params <- function(query) { |
|
|
params <- list() |
|
|
if (length(query) == 0) { |
|
|
return(params) |
|
|
} |
|
|
|
|
|
|
|
|
decode <- function(x) URLdecode(x) |
|
|
|
|
|
|
|
|
pairs <- strsplit(query, "&")[[1]] |
|
|
for (pair in pairs) { |
|
|
parts <- strsplit(pair, "=")[[1]] |
|
|
if (length(parts) == 2) { |
|
|
key <- parts[1] |
|
|
val <- decode(parts[2]) |
|
|
params[[key]] <- val |
|
|
} |
|
|
} |
|
|
params |
|
|
} |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(!url_initialized()) |
|
|
query <- session$clientData$url_search |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
q_str <- sub("^\\?", "", query) |
|
|
params <- parse_url_params(q_str) |
|
|
|
|
|
|
|
|
if (length(params) > 0) { |
|
|
|
|
|
if (!is.null(params$resolution)) { |
|
|
updateRadioButtons(session, "data_resolution", selected = params$resolution) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$start) && !is.null(params$end)) { |
|
|
updateDateRangeInput(session, "date_range", start = params$start, end = params$end) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$landname)) { |
|
|
updateSelectizeInput(session, "state_selector", selected = params$landname) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$station)) { |
|
|
station_ref <- params$station |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shinyjs::delay(500, { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_st <- isolate(all_stations()) |
|
|
if (!is.null(current_st)) { |
|
|
|
|
|
match <- current_st %>% filter(id == station_ref) |
|
|
if (nrow(match) == 0) { |
|
|
|
|
|
match <- current_st %>% filter(name == station_ref) |
|
|
} |
|
|
|
|
|
if (nrow(match) > 0) { |
|
|
target_id <- match$id[1] |
|
|
updateSelectizeInput(session, "station_selector", selected = target_id) |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$view)) { |
|
|
view <- params$view |
|
|
shinyjs::delay(800, { |
|
|
if (view == "map") { |
|
|
nav_select("main_nav", "Map View") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else if (view == "station-info") { |
|
|
updateNavbarPage(session, "main_nav", selected = "Stations Info") |
|
|
} else if (grepl("dashboard", view)) { |
|
|
updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
|
|
|
if (view == "dashboard-data") { |
|
|
shinyjs::delay(200, { |
|
|
nav_select("dashboard_subtabs", "Data") |
|
|
|
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
url_initialized(TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$data_resolution, |
|
|
{ |
|
|
session$sendCustomMessage("freezeUI", list(text = "Switching resolution...")) |
|
|
|
|
|
|
|
|
res <- tolower(input$data_resolution) |
|
|
|
|
|
tryCatch( |
|
|
{ |
|
|
new_end <- Sys.Date() |
|
|
if (res %in% c("monthly", "daily")) { |
|
|
|
|
|
new_start <- new_end - (365 * 6) |
|
|
} else { |
|
|
|
|
|
new_start <- new_end - 366 |
|
|
} |
|
|
updateDateRangeInput(session, "date_range", start = new_start, end = new_end) |
|
|
}, |
|
|
error = function(e) { |
|
|
message("Note: Could not update date range: ", e$message) |
|
|
} |
|
|
) |
|
|
}, |
|
|
priority = 1000, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
current_index <- reactive({ |
|
|
req(input$data_resolution) |
|
|
|
|
|
res <- tolower(input$data_resolution) |
|
|
|
|
|
load_dwd_index(res) |
|
|
}) |
|
|
|
|
|
all_stations <- reactive({ |
|
|
req(input$data_resolution) |
|
|
res <- tolower(input$data_resolution) |
|
|
|
|
|
load_enriched_stations(res) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
st <- all_stations() |
|
|
req(st) |
|
|
|
|
|
states <- sort(unique(st$state)) |
|
|
|
|
|
|
|
|
|
|
|
updateSelectizeInput(session, "state_selector", choices = states, server = TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
is_loading <- loading_status() |
|
|
|
|
|
|
|
|
inputs_to_toggle <- c( |
|
|
"state_selector", |
|
|
"station_selector", |
|
|
"date_range", |
|
|
"zoom_home", |
|
|
"main_nav", |
|
|
"download_hourly" |
|
|
) |
|
|
|
|
|
if (is_loading) { |
|
|
for (inp in inputs_to_toggle) { |
|
|
shinyjs::disable(inp) |
|
|
} |
|
|
} else { |
|
|
for (inp in inputs_to_toggle) { |
|
|
shinyjs::enable(inp) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
filtered_stations <- reactive({ |
|
|
req(all_stations(), input$date_range) |
|
|
df <- all_stations() |
|
|
|
|
|
|
|
|
|
|
|
range_start <- as.numeric(format(input$date_range[1], "%Y%m%d")) |
|
|
range_end <- as.numeric(format(input$date_range[2], "%Y%m%d")) |
|
|
|
|
|
|
|
|
if (!is.null(input$state_selector) && length(input$state_selector) > 0) { |
|
|
df <- df %>% filter(state %in% input$state_selector) |
|
|
} |
|
|
|
|
|
df <- df %>% filter( |
|
|
as.numeric(station_overall_start) <= range_end & |
|
|
as.numeric(station_overall_end) >= range_start |
|
|
) |
|
|
|
|
|
|
|
|
as.data.frame(df) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
df <- filtered_stations() |
|
|
req(df) |
|
|
|
|
|
|
|
|
|
|
|
ids <- as.character(df$id) |
|
|
names <- paste0(as.character(df$name), " (", ids, ")") |
|
|
|
|
|
if (length(ids) > 0) { |
|
|
new_choices <- setNames(ids, names) |
|
|
} else { |
|
|
new_choices <- character(0) |
|
|
} |
|
|
|
|
|
|
|
|
prev_choices <- previous_station_choices() |
|
|
new_ids <- sort(unname(new_choices)) |
|
|
prev_ids <- if (!is.null(prev_choices)) sort(unname(prev_choices)) else NULL |
|
|
|
|
|
if (is.null(prev_ids) || !identical(new_ids, prev_ids)) { |
|
|
|
|
|
current_sel <- input$station_selector |
|
|
|
|
|
updateSelectizeInput(session, "station_selector", |
|
|
choices = new_choices, |
|
|
selected = current_sel, |
|
|
server = TRUE |
|
|
) |
|
|
previous_station_choices(new_choices) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
broadcast_state <- function(view_override = NULL) { |
|
|
|
|
|
sid <- current_station_id() |
|
|
st_meta <- NULL |
|
|
if (!is.null(sid)) { |
|
|
all <- isolate(all_stations()) |
|
|
|
|
|
if (!is.null(all)) { |
|
|
st_meta <- all %>% |
|
|
filter(id == sid) %>% |
|
|
head(1) |
|
|
} |
|
|
} |
|
|
|
|
|
station_id <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$id) else NULL |
|
|
station_name <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$name) else NULL |
|
|
landname <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$state) else NULL |
|
|
|
|
|
|
|
|
resolution <- input$data_resolution |
|
|
|
|
|
|
|
|
main_tab <- input$main_nav |
|
|
view <- if (!is.null(view_override)) { |
|
|
view_override |
|
|
} else if (!is.null(main_tab)) { |
|
|
if (main_tab == "Map View") { |
|
|
"map" |
|
|
} else if (main_tab == "Stations Info") { |
|
|
"station-info" |
|
|
} else if (main_tab == "Dashboard") { |
|
|
subtab <- input$dashboard_subtabs |
|
|
|
|
|
if (!is.null(subtab) && subtab == "Data") { |
|
|
"dashboard-data" |
|
|
} else { |
|
|
"dashboard-plots" |
|
|
} |
|
|
} else { |
|
|
"map" |
|
|
} |
|
|
} else { |
|
|
"map" |
|
|
} |
|
|
|
|
|
start_date <- if (!is.null(input$date_range)) as.character(input$date_range[1]) else NULL |
|
|
end_date <- if (!is.null(input$date_range)) as.character(input$date_range[2]) else NULL |
|
|
|
|
|
session$sendCustomMessage("updateParentURL", list( |
|
|
station = station_id, |
|
|
stationName = station_name, |
|
|
landname = landname, |
|
|
resolution = resolution, |
|
|
view = view, |
|
|
start = start_date, |
|
|
end = end_date |
|
|
)) |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
observeEvent(input$station_selector, { |
|
|
req(input$station_selector) |
|
|
id_val <- input$station_selector |
|
|
|
|
|
|
|
|
s_meta <- all_stations() %>% filter(id == id_val) |
|
|
req(nrow(s_meta) > 0) |
|
|
|
|
|
lat_val <- s_meta$latitude[1] |
|
|
lng_val <- s_meta$longitude[1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
prev_id <- current_station_id() |
|
|
if (!is.null(prev_id) && prev_id == id_val) { |
|
|
return() |
|
|
} |
|
|
|
|
|
current_station_id(id_val) |
|
|
|
|
|
|
|
|
|
|
|
res <- if (is.null(input$data_resolution)) NULL else tolower(input$data_resolution) |
|
|
overall_start <- if ("station_overall_start" %in% names(s_meta)) s_meta$station_overall_start[1] else s_meta$start_date[1] |
|
|
overall_end <- if ("station_overall_end" %in% names(s_meta)) s_meta$station_overall_end[1] else s_meta$end_date[1] |
|
|
lbl <- generate_station_label( |
|
|
s_meta$name[1], s_meta$id[1], s_meta$state[1], |
|
|
s_meta$start_date[1], s_meta$end_date[1], s_meta$detailed_summary[1], |
|
|
overall_start, overall_end, res |
|
|
) |
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), lng_val, lat_val, lbl) |
|
|
|
|
|
|
|
|
output$show_details_panel <- reactive(TRUE) |
|
|
outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE) |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
output$station_count_filtered <- renderText({ |
|
|
n <- nrow(filtered_stations()) |
|
|
paste(n, "stations found") |
|
|
}) |
|
|
|
|
|
|
|
|
output$map <- renderMaplibre({ |
|
|
maplibre( |
|
|
style = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", |
|
|
center = c(10.45, 51.16), |
|
|
zoom = 6 |
|
|
) %>% |
|
|
add_navigation_control(show_compass = FALSE, visualize_pitch = FALSE, position = "top-left") |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(!map_initialized()) |
|
|
req(input$map_zoom) |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds( |
|
|
c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max) |
|
|
) |
|
|
|
|
|
map_initialized(TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$zoom_home, { |
|
|
df <- filtered_stations() |
|
|
req(df) |
|
|
if (nrow(df) > 0) { |
|
|
|
|
|
lons <- range(df$longitude, na.rm = TRUE) |
|
|
lats <- range(df$latitude, na.rm = TRUE) |
|
|
|
|
|
|
|
|
if (nrow(df) == 1) { |
|
|
maplibre_proxy("map") %>% |
|
|
fly_to(center = c(df$longitude[1], df$latitude[1]), zoom = 12) |
|
|
} else { |
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds(c(lons[1], lats[1], lons[2], lats[2]), animate = TRUE) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$state_selector, |
|
|
{ |
|
|
df <- filtered_stations() |
|
|
req(df) |
|
|
if (nrow(df) > 0) { |
|
|
|
|
|
lons <- range(df$longitude, na.rm = TRUE) |
|
|
lats <- range(df$latitude, na.rm = TRUE) |
|
|
|
|
|
if (nrow(df) == 1) { |
|
|
maplibre_proxy("map") %>% |
|
|
fly_to(center = c(df$longitude[1], df$latitude[1]), zoom = 12) |
|
|
} else { |
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds(c(lons[1], lats[1], lons[2], lats[2]), animate = TRUE) |
|
|
} |
|
|
} |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observeEvent(input$main_nav, { |
|
|
if (input$main_nav == "Map View") { |
|
|
|
|
|
shinyjs::runjs(" |
|
|
setTimeout(function() { |
|
|
var map = document.getElementById('map'); |
|
|
if (map && map.__mapgl) { |
|
|
map.__mapgl.resize(); |
|
|
} |
|
|
}, 200); |
|
|
") |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$basemap, { |
|
|
proxy <- maplibre_proxy("map") |
|
|
|
|
|
|
|
|
old_layers <- isolate(current_raster_layers()) |
|
|
if (length(old_layers) > 0) { |
|
|
for (layer_id in old_layers) { |
|
|
proxy %>% clear_layer(layer_id) |
|
|
} |
|
|
current_raster_layers(character(0)) |
|
|
} |
|
|
|
|
|
if (input$basemap %in% c("carto_positron", "carto_voyager", "esri_imagery")) { |
|
|
|
|
|
style_url <- switch(input$basemap, |
|
|
"carto_positron" = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", |
|
|
"carto_voyager" = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json", |
|
|
"esri_imagery" = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json" |
|
|
) |
|
|
|
|
|
proxy %>% |
|
|
set_style(style_url) |
|
|
|
|
|
|
|
|
stations_before_id("watername_ocean") |
|
|
|
|
|
|
|
|
if (input$basemap == "esri_imagery") { |
|
|
current_session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- input$basemap |
|
|
|
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(current_session, { |
|
|
|
|
|
current_basemap <- isolate(input$basemap) |
|
|
if (current_basemap != selected_basemap) { |
|
|
return() |
|
|
} |
|
|
|
|
|
unique_suffix <- as.numeric(Sys.time()) * 1000 |
|
|
source_id <- paste0("esri_imagery_source_", unique_suffix) |
|
|
layer_id <- paste0("esri_imagery_layer_", unique_suffix) |
|
|
|
|
|
esri_url <- "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
add_raster_source(id = source_id, tiles = c(esri_url), tileSize = 256) %>% |
|
|
add_layer( |
|
|
id = layer_id, |
|
|
type = "raster", |
|
|
source = source_id, |
|
|
paint = list("raster-opacity" = 1), |
|
|
before_id = "watername_ocean" |
|
|
) |
|
|
|
|
|
current_raster_layers(c(layer_id)) |
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.5) |
|
|
} else { |
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
} |
|
|
} else { |
|
|
|
|
|
tile_url <- if (input$basemap %in% c("osm", "osm_gray")) { |
|
|
"https://tile.openstreetmap.org/{z}/{x}/{y}.png" |
|
|
} else { |
|
|
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}" |
|
|
} |
|
|
|
|
|
attribution_text <- if (input$basemap %in% c("osm", "osm_gray")) { |
|
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
|
|
} else { |
|
|
"Tiles © Esri" |
|
|
} |
|
|
|
|
|
paint_props <- list("raster-opacity" = 1) |
|
|
if (input$basemap == "osm_gray") { |
|
|
paint_props[["raster-saturation"]] <- -0.9 |
|
|
paint_props[["raster-contrast"]] <- 0.3 |
|
|
} |
|
|
|
|
|
|
|
|
blank_style <- list( |
|
|
version = 8, |
|
|
sources = list(), |
|
|
layers = list(), |
|
|
metadata = list(timestamp = as.numeric(Sys.time())) |
|
|
) |
|
|
json_blank <- jsonlite::toJSON(blank_style, auto_unbox = TRUE) |
|
|
blank_uri <- paste0("data:application/json,", URLencode(as.character(json_blank), reserved = TRUE)) |
|
|
|
|
|
proxy %>% |
|
|
set_style(blank_uri) |
|
|
|
|
|
current_session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- input$basemap |
|
|
|
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(current_session, { |
|
|
current_basemap <- isolate(input$basemap) |
|
|
if (current_basemap != selected_basemap) { |
|
|
return() |
|
|
} |
|
|
|
|
|
unique_suffix <- as.numeric(Sys.time()) * 1000 |
|
|
source_id <- paste0("raster_source_", unique_suffix) |
|
|
layer_id <- paste0("raster_layer_", unique_suffix) |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
add_raster_source(id = source_id, tiles = c(tile_url), tileSize = 256, attribution = attribution_text) %>% |
|
|
add_layer( |
|
|
id = layer_id, |
|
|
type = "raster", |
|
|
source = source_id, |
|
|
paint = paint_props |
|
|
) |
|
|
|
|
|
stations_before_id(NULL) |
|
|
current_raster_layers(c(layer_id)) |
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.5) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$show_labels, |
|
|
{ |
|
|
visibility <- if (input$show_labels) "visible" else "none" |
|
|
|
|
|
label_layers <- c( |
|
|
"place_villages", "place_town", "place_country_2", "place_country_1", |
|
|
"place_state", "place_continent", |
|
|
"place_city_r6", "place_city_r5", "place_city_dot_r7", "place_city_dot_r4", |
|
|
"place_city_dot_r2", "place_city_dot_z7", |
|
|
"place_capital_dot_z7", "place_capital", |
|
|
"roadname_minor", "roadname_sec", "roadname_pri", "roadname_major", |
|
|
"motorway_name", |
|
|
"watername_ocean", "watername_sea", "watername_lake", "watername_lake_line", |
|
|
"poi_stadium", "poi_park", "poi_zoo", |
|
|
"airport_label", |
|
|
"country-label", "state-label", "settlement-major-label", "settlement-minor-label", |
|
|
"settlement-subdivision-label", "road-label", "waterway-label", "natural-point-label", |
|
|
"poi-label", "airport-label" |
|
|
) |
|
|
|
|
|
proxy <- maplibre_proxy("map") |
|
|
|
|
|
for (layer_id in label_layers) { |
|
|
tryCatch( |
|
|
{ |
|
|
proxy <- proxy %>% set_layout_property(layer_id, "visibility", visibility) |
|
|
}, |
|
|
error = function(e) { |
|
|
|
|
|
} |
|
|
) |
|
|
} |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
df <- filtered_stations() |
|
|
req(df, input$data_resolution) |
|
|
|
|
|
|
|
|
req(map_initialized()) |
|
|
|
|
|
|
|
|
style_change_trigger() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
res <- tolower(input$data_resolution) |
|
|
n_rows <- nrow(df) |
|
|
coverage_start <- if ("station_overall_start" %in% names(df)) df$station_overall_start else df$start_date |
|
|
coverage_end <- if ("station_overall_end" %in% names(df)) df$station_overall_end else df$end_date |
|
|
|
|
|
|
|
|
df$popup_content <- purrr::pmap_chr( |
|
|
list( |
|
|
df$name, df$id, df$state, df$start_date, df$end_date, |
|
|
df$detailed_summary, coverage_start, coverage_end, rep(res, n_rows) |
|
|
), |
|
|
generate_station_label |
|
|
) |
|
|
|
|
|
if (nrow(df) > 0) { |
|
|
|
|
|
df <- df %>% |
|
|
mutate( |
|
|
circle_color = "navy", |
|
|
circle_radius = 6, |
|
|
circle_stroke_color = "#00000000", |
|
|
circle_stroke_width = 0 |
|
|
) |
|
|
|
|
|
|
|
|
map_data <- st_as_sf(df, coords = c("longitude", "latitude"), crs = 4326) |
|
|
|
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
clear_layer("stations") %>% |
|
|
add_circle_layer( |
|
|
id = "stations", |
|
|
source = map_data, |
|
|
circle_color = get_column("circle_color"), |
|
|
circle_radius = get_column("circle_radius"), |
|
|
circle_stroke_color = get_column("circle_stroke_color"), |
|
|
circle_stroke_width = get_column("circle_stroke_width"), |
|
|
circle_opacity = 0.7, |
|
|
tooltip = get_column("popup_content"), |
|
|
before_id = stations_before_id() |
|
|
) |
|
|
|
|
|
|
|
|
sid <- isolate(current_station_id()) |
|
|
if (!is.null(sid)) { |
|
|
sel_row <- df %>% filter(id == sid) |
|
|
if (nrow(sel_row) > 0) { |
|
|
highlight_selected_station(maplibre_proxy("map"), sel_row$longitude[1], sel_row$latitude[1], sel_row$popup_content[1], move_map = FALSE) |
|
|
} else { |
|
|
|
|
|
maplibre_proxy("map") %>% clear_layer("selected-highlight") |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$map_feature_click, { |
|
|
clicked_data <- input$map_feature_click |
|
|
|
|
|
|
|
|
if (!is.null(clicked_data) && (isTRUE(clicked_data$layer_id == "stations") || isTRUE(clicked_data$layer == "stations"))) { |
|
|
|
|
|
id_val <- clicked_data$properties$id |
|
|
|
|
|
if (!is.null(id_val)) { |
|
|
|
|
|
current_station_id(id_val) |
|
|
|
|
|
|
|
|
|
|
|
s_meta <- all_stations() %>% filter(id == id_val) |
|
|
|
|
|
if (nrow(s_meta) > 0) { |
|
|
lng <- s_meta$longitude[1] |
|
|
lat <- s_meta$latitude[1] |
|
|
|
|
|
res <- if (is.null(input$data_resolution)) NULL else tolower(input$data_resolution) |
|
|
overall_start <- if ("station_overall_start" %in% names(s_meta)) s_meta$station_overall_start[1] else s_meta$start_date[1] |
|
|
overall_end <- if ("station_overall_end" %in% names(s_meta)) s_meta$station_overall_end[1] else s_meta$end_date[1] |
|
|
lbl <- generate_station_label( |
|
|
s_meta$name[1], s_meta$id[1], s_meta$state[1], |
|
|
s_meta$start_date[1], s_meta$end_date[1], s_meta$detailed_summary[1], |
|
|
overall_start, overall_end, res |
|
|
) |
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), lng, lat, lbl) |
|
|
} |
|
|
|
|
|
|
|
|
updateSelectizeInput(session, "station_selector", selected = id_val) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observeEvent(input$date_range, { |
|
|
req(input$date_range) |
|
|
d_start <- input$date_range[1] |
|
|
d_end <- input$date_range[2] |
|
|
|
|
|
|
|
|
if (any(is.na(c(d_start, d_end)))) { |
|
|
return() |
|
|
} |
|
|
req(input$data_resolution) |
|
|
|
|
|
|
|
|
prev <- previous_date_range() |
|
|
|
|
|
|
|
|
if (is.null(prev)) { |
|
|
previous_date_range(c(d_start, d_end)) |
|
|
return() |
|
|
} |
|
|
|
|
|
prev_start <- prev[1] |
|
|
prev_end <- prev[2] |
|
|
|
|
|
|
|
|
diff <- as.numeric(difftime(d_end, d_start, units = "days")) |
|
|
|
|
|
|
|
|
if (is.na(diff)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
res <- tolower(input$data_resolution) |
|
|
is_extended <- res %in% c("monthly", "daily") |
|
|
max_days <- if (is_extended) 3652 else 366 |
|
|
min_days <- 28 |
|
|
|
|
|
if (diff > max_days || diff < min_days) { |
|
|
|
|
|
start_changed <- !isTRUE(all.equal(d_start, prev_start)) |
|
|
end_changed <- !isTRUE(all.equal(d_end, prev_end)) |
|
|
|
|
|
if (end_changed && !start_changed) { |
|
|
|
|
|
target_diff <- if (diff > max_days) max_days else min_days |
|
|
new_start <- d_end - target_diff |
|
|
|
|
|
freezeReactiveValue(input, "date_range") |
|
|
updateDateRangeInput(session, "date_range", start = new_start, end = d_end) |
|
|
|
|
|
|
|
|
previous_date_range(c(new_start, d_end)) |
|
|
} else { |
|
|
|
|
|
target_diff <- if (diff > max_days) max_days else min_days |
|
|
new_end <- d_start + target_diff |
|
|
|
|
|
freezeReactiveValue(input, "date_range") |
|
|
updateDateRangeInput(session, "date_range", start = d_start, end = new_end) |
|
|
|
|
|
|
|
|
previous_date_range(c(d_start, new_end)) |
|
|
} |
|
|
} else { |
|
|
|
|
|
previous_date_range(c(d_start, d_end)) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fetch_stage <- reactiveVal(0) |
|
|
fetch_message <- reactiveVal("Idle") |
|
|
fetch_queue <- reactiveVal(list()) |
|
|
fetch_queue_idx <- reactiveVal(0) |
|
|
parsed_data_list <- reactiveVal(list()) |
|
|
|
|
|
|
|
|
fetch_total_size <- reactiveVal(0) |
|
|
fetch_current_pos <- reactiveVal(0) |
|
|
fetch_current_token <- reactiveVal(NULL) |
|
|
fetch_temp_file <- reactiveVal(NULL) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observeEvent(input$data_resolution, |
|
|
{ |
|
|
station_data(NULL) |
|
|
parsed_data_list(list()) |
|
|
|
|
|
|
|
|
if (!is.null(current_station_id())) { |
|
|
loading_status(TRUE) |
|
|
} |
|
|
}, |
|
|
priority = 1000, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
observeEvent(current_station_id(), |
|
|
{ |
|
|
station_data(NULL) |
|
|
parsed_data_list(list()) |
|
|
|
|
|
|
|
|
if (!is.null(current_station_id())) { |
|
|
loading_status(TRUE) |
|
|
} |
|
|
}, |
|
|
priority = 1000, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
window_reactive <- reactive({ |
|
|
req(input$date_range) |
|
|
list(id = current_station_id(), start = input$date_range[1], end = input$date_range[2]) |
|
|
}) |
|
|
window_debounced <- window_reactive %>% debounce(500) |
|
|
|
|
|
reset_fetch <- function(msg = NULL) { |
|
|
fetch_stage(0) |
|
|
|
|
|
fetch_current_token(as.numeric(Sys.time())) |
|
|
|
|
|
loading_status(FALSE) |
|
|
if (!is.null(msg)) loading_diagnostics(msg) |
|
|
tmp <- fetch_temp_file() |
|
|
if (!is.null(tmp) && file.exists(tmp)) unlink(tmp) |
|
|
fetch_temp_file(NULL) |
|
|
parsed_data_list(list()) |
|
|
|
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
} |
|
|
|
|
|
|
|
|
observeEvent(input$cancel_loading, { |
|
|
reset_fetch("Cancelled by user") |
|
|
showNotification("Loading cancelled by user.", type = "warning") |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(window_debounced()$id, window_debounced()$start, window_debounced()$end) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
station_id <- window_debounced()$id |
|
|
loading_status(TRUE) |
|
|
|
|
|
|
|
|
station_meta <- all_stations() %>% filter(id == station_id) |
|
|
station_name <- if (nrow(station_meta) > 0) station_meta$name[1] else station_id |
|
|
|
|
|
msg <- "Initializing data fetch..." |
|
|
loading_diagnostics(msg) |
|
|
fetch_message(msg) |
|
|
|
|
|
session$sendCustomMessage("freezeUI", list(text = msg, station = station_name)) |
|
|
|
|
|
|
|
|
req_start_date <- as.Date(window_debounced()$start) |
|
|
req_end_date <- as.Date(window_debounced()$end) |
|
|
|
|
|
|
|
|
parse_dwd_date <- function(x) { |
|
|
as.Date(x, format = "%Y%m%d") |
|
|
} |
|
|
|
|
|
targets <- current_index() %>% |
|
|
filter(id == station_id) %>% |
|
|
filter( |
|
|
|
|
|
|
|
|
type == "recent" | |
|
|
type == "solar" | |
|
|
( |
|
|
!is.na(start_date) & !is.na(end_date) & |
|
|
parse_dwd_date(start_date) <= req_end_date & |
|
|
parse_dwd_date(end_date) >= req_start_date |
|
|
) |
|
|
) |
|
|
|
|
|
if (nrow(targets) == 0) { |
|
|
|
|
|
raw_targets <- current_index() %>% filter(id == station_id) |
|
|
msg <- "No data found in index for this station." |
|
|
|
|
|
if (nrow(raw_targets) > 0) { |
|
|
|
|
|
msg <- "No data found for the selected date range." |
|
|
} |
|
|
|
|
|
reset_fetch(msg) |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
q <- split(targets, seq(nrow(targets))) |
|
|
fetch_queue(q) |
|
|
fetch_queue_idx(1) |
|
|
parsed_data_list(list()) |
|
|
|
|
|
|
|
|
token <- as.numeric(Sys.time()) |
|
|
fetch_current_token(token) |
|
|
|
|
|
fetch_stage(2) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(fetch_stage() == 2) |
|
|
idx <- fetch_queue_idx() |
|
|
q <- fetch_queue() |
|
|
|
|
|
if (idx > length(q)) { |
|
|
|
|
|
fetch_stage(6) |
|
|
} else { |
|
|
|
|
|
target <- q[[idx]] |
|
|
msg <- paste0("Downloading file ", idx, "/", length(q), "...") |
|
|
loading_diagnostics(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
fetch_message(msg) |
|
|
|
|
|
|
|
|
fetch_total_size(0) |
|
|
fetch_current_pos(0) |
|
|
|
|
|
|
|
|
tmp <- tempfile(fileext = ".zip") |
|
|
fetch_temp_file(tmp) |
|
|
file.create(tmp) |
|
|
|
|
|
fetch_stage(4) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
req(fetch_stage() == 4) |
|
|
|
|
|
|
|
|
chunk_size <- 50 * 1024 * 1024 |
|
|
total <- fetch_total_size() |
|
|
current <- fetch_current_pos() |
|
|
tmp <- fetch_temp_file() |
|
|
idx <- fetch_queue_idx() |
|
|
q <- fetch_queue() |
|
|
target <- q[[idx]] |
|
|
|
|
|
|
|
|
if (total > 0) { |
|
|
end <- min(current + chunk_size - 1, total - 1) |
|
|
} else { |
|
|
end <- current + chunk_size - 1 |
|
|
} |
|
|
|
|
|
token <- fetch_current_token() |
|
|
message(paste0("[Debug] Stage 4: Starting chunk download. Range: ", current, "-", end, " Total: ", total)) |
|
|
|
|
|
later::later(function() { |
|
|
if (!identical(isolate(fetch_current_token()), token)) { |
|
|
message("[Debug] Stage 4: Token mismatch in later callback. Aborting.") |
|
|
return() |
|
|
} |
|
|
|
|
|
tryCatch( |
|
|
{ |
|
|
isolate({ |
|
|
h <- curl::new_handle() |
|
|
curl::handle_setopt( |
|
|
h, |
|
|
range = paste0(current, "-", end), |
|
|
|
|
|
connecttimeout = 10, |
|
|
low_speed_time = 30, |
|
|
low_speed_limit = 100 |
|
|
) |
|
|
|
|
|
message(paste0("[Debug] Stage 4: Executing curl for ", target$url)) |
|
|
|
|
|
tryCatch( |
|
|
{ |
|
|
resp <- curl::curl_fetch_memory(target$url, handle = h) |
|
|
message(paste0("[Debug] Stage 4: Curl finished. Status: ", resp$status_code)) |
|
|
|
|
|
if (resp$status_code == 200) { |
|
|
|
|
|
con <- file(tmp, open = "wb") |
|
|
writeBin(resp$content, con) |
|
|
close(con) |
|
|
|
|
|
new_pos <- length(resp$content) |
|
|
fetch_current_pos(new_pos) |
|
|
fetch_total_size(new_pos) |
|
|
|
|
|
msg <- paste0("Downloaded file ", idx, "/", length(q), ": ", format_bytes(new_pos), " (Finished)") |
|
|
loading_diagnostics(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
fetch_message(msg) |
|
|
|
|
|
fetch_stage(5) |
|
|
} else if (resp$status_code == 206) { |
|
|
|
|
|
con <- file(tmp, open = "ab") |
|
|
writeBin(resp$content, con) |
|
|
close(con) |
|
|
|
|
|
bytes_received <- length(resp$content) |
|
|
new_pos <- current + bytes_received |
|
|
fetch_current_pos(new_pos) |
|
|
|
|
|
|
|
|
if (total == 0) { |
|
|
headers <- curl::parse_headers_list(resp$headers) |
|
|
if (!is.null(headers[["content-range"]])) { |
|
|
cr <- headers[["content-range"]] |
|
|
parts <- strsplit(cr, "/")[[1]] |
|
|
if (length(parts) == 2 && parts[2] != "*") { |
|
|
t_val <- as.numeric(parts[2]) |
|
|
if (!is.na(t_val)) fetch_total_size(t_val) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
total <- fetch_total_size() |
|
|
|
|
|
done <- FALSE |
|
|
percent_str <- "" |
|
|
|
|
|
if (total > 0) { |
|
|
if (new_pos >= total) done <- TRUE |
|
|
pct <- round(new_pos / total * 100) |
|
|
percent_str <- paste0(" / ", format_bytes(total), " (", pct, "%)") |
|
|
} else { |
|
|
if (bytes_received < chunk_size) done <- TRUE |
|
|
percent_str <- " (Unknown total)" |
|
|
} |
|
|
|
|
|
msg <- paste0( |
|
|
"Downloading file ", idx, "/", length(q), ": ", |
|
|
format_bytes(new_pos), percent_str |
|
|
) |
|
|
loading_diagnostics(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
fetch_message(msg) |
|
|
|
|
|
if (done) { |
|
|
if (total == 0) fetch_total_size(new_pos) |
|
|
fetch_stage(5) |
|
|
} else { |
|
|
fetch_stage(4) |
|
|
} |
|
|
} else if (resp$status_code == 416) { |
|
|
|
|
|
if (total == 0 && current > 0) { |
|
|
fetch_stage(5) |
|
|
} else { |
|
|
loading_diagnostics(paste("Download Error HTTP", resp$status_code)) |
|
|
fetch_stage(0) |
|
|
} |
|
|
} else { |
|
|
loading_diagnostics(paste("Download Error HTTP", resp$status_code)) |
|
|
fetch_stage(0) |
|
|
} |
|
|
}, |
|
|
error = function(e) { |
|
|
|
|
|
message(paste0("[Debug] Download Failed: ", e$message)) |
|
|
loading_diagnostics(paste("Download Failed:", e$message)) |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
showNotification( |
|
|
paste("Download stopped: Connection timed out or stuck. Please try again later. (File ", idx, ")"), |
|
|
type = "error", |
|
|
duration = NULL |
|
|
) |
|
|
fetch_stage(0) |
|
|
} |
|
|
) |
|
|
}) |
|
|
}, |
|
|
error = function(e) { |
|
|
message(paste0("[Debug] Critical Async Error: ", e$message)) |
|
|
msg <- paste("Critical Async Error:", e$message) |
|
|
|
|
|
loading_diagnostics(msg) |
|
|
loading_status(FALSE) |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
fetch_stage(0) |
|
|
} |
|
|
) |
|
|
}, 0.05) |
|
|
|
|
|
fetch_stage(-1) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(fetch_stage() == 5) |
|
|
token <- fetch_current_token() |
|
|
|
|
|
idx <- fetch_queue_idx() |
|
|
len <- length(fetch_queue()) |
|
|
msg <- paste0("Parsing file ", idx, "/", len, "...") |
|
|
loading_diagnostics(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
fetch_message(msg) |
|
|
|
|
|
later::later(function() { |
|
|
if (!identical(isolate(fetch_current_token()), token)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
isolate({ |
|
|
tmp <- fetch_temp_file() |
|
|
|
|
|
|
|
|
|
|
|
s_date <- window_debounced()$start |
|
|
e_date <- window_debounced()$end |
|
|
|
|
|
|
|
|
parsed <- tryCatch(read_dwd_data(tmp, s_date, e_date), error = function(e) NULL) |
|
|
|
|
|
if (!is.null(parsed)) { |
|
|
|
|
|
plist <- parsed_data_list() |
|
|
plist[[length(plist) + 1]] <- parsed |
|
|
parsed_data_list(plist) |
|
|
} |
|
|
|
|
|
unlink(tmp) |
|
|
fetch_temp_file(NULL) |
|
|
|
|
|
|
|
|
fetch_queue_idx(idx + 1) |
|
|
fetch_stage(2) |
|
|
}) |
|
|
}, 0.1) |
|
|
|
|
|
fetch_stage(-1) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(fetch_stage() == 6) |
|
|
|
|
|
token <- fetch_current_token() |
|
|
msg <- "Merging and finalizing data..." |
|
|
loading_diagnostics(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
fetch_message(msg) |
|
|
|
|
|
later::later(function() { |
|
|
if (!identical(isolate(fetch_current_token()), token)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
isolate({ |
|
|
plist <- parsed_data_list() |
|
|
|
|
|
if (length(plist) == 0) { |
|
|
loading_diagnostics("No valid data found.") |
|
|
loading_status(FALSE) |
|
|
station_data(NULL) |
|
|
fetch_stage(0) |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
final_df <- purrr::reduce(plist, full_join, by = "datetime") |
|
|
|
|
|
|
|
|
weather_vars <- c( |
|
|
"temp", "temp_min", "temp_max", "temp_min_avg", "temp_max_avg", "rh", "dew_point", |
|
|
"abs_humidity", "vapor_pressure", "wet_bulb_temp", |
|
|
"precip", "wind_speed", "wind_dir", "pressure", "station_pressure", "cloud_cover", "cloud_cover_indicator", |
|
|
"wind_gust_max", "solar_global", "sunshine_duration", |
|
|
"soil_temp_2cm", "soil_temp_5cm", "soil_temp_10cm", "soil_temp_20cm", "soil_temp_50cm", "soil_temp_100cm", |
|
|
"soil_temp_min_5cm", |
|
|
"snow_depth", "snow_water_equiv", "snow_fresh_sum", "snow_depth_sum", |
|
|
"thunderstorm", "glaze", "graupel", "hail", "fog", "frost", "storm_6", "storm_8", "dew", |
|
|
"precip_net_thunderstorm", "precip_net_graupel", "precip_net_hail", "precip_net_fog", |
|
|
"visibility", "visibility_indicator", "weather_code", "weather_text", |
|
|
"cloud_layer1_code", "cloud_layer1_abbrev", "cloud_layer1_height", "cloud_layer1_amount", |
|
|
"cloud_layer2_code", "cloud_layer2_abbrev", "cloud_layer2_height", "cloud_layer2_amount", |
|
|
"cloud_layer3_code", "cloud_layer3_abbrev", "cloud_layer3_height", "cloud_layer3_amount", |
|
|
"cloud_layer4_code", "cloud_layer4_abbrev", "cloud_layer4_height", "cloud_layer4_amount" |
|
|
) |
|
|
available_cols <- names(final_df) |
|
|
clean_df <- final_df %>% select(datetime) |
|
|
|
|
|
for (v in weather_vars) { |
|
|
v_cols <- available_cols[grepl(paste0("^", v, "(\\.|$)"), available_cols)] |
|
|
if (length(v_cols) > 0) { |
|
|
clean_df[[v]] <- do.call(coalesce, final_df[v_cols]) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
clean_df <- clean_df %>% |
|
|
distinct(datetime, .keep_all = TRUE) %>% |
|
|
arrange(datetime) |
|
|
|
|
|
|
|
|
s_date <- window_debounced()$start |
|
|
e_date <- window_debounced()$end |
|
|
|
|
|
if (!is.null(s_date) && !is.null(e_date)) { |
|
|
req_start <- as.POSIXct(s_date) |
|
|
req_end <- as.POSIXct(e_date) + days(1) |
|
|
|
|
|
filtered_df <- clean_df %>% |
|
|
filter(datetime >= req_start, datetime <= req_end) |
|
|
|
|
|
|
|
|
if (nrow(filtered_df) == 0 && nrow(clean_df) > 0) { |
|
|
data_max <- max(clean_df$datetime, na.rm = TRUE) |
|
|
data_min <- min(clean_df$datetime, na.rm = TRUE) |
|
|
|
|
|
|
|
|
if (req_start > data_max || req_end < data_min) { |
|
|
msg <- paste0("Warning: No data in selected range. Available data ends: ", format(data_max, "%Y-%m-%d"), ". Displaying available data.") |
|
|
loading_diagnostics(msg) |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
clean_df <- filtered_df |
|
|
} |
|
|
|
|
|
|
|
|
if (nrow(clean_df) == 0 && nrow(final_df) > 0) { |
|
|
msg <- "No data found for the selected date range." |
|
|
loading_diagnostics(msg) |
|
|
} |
|
|
|
|
|
|
|
|
station_data(clean_df) |
|
|
loading_diagnostics(paste0("Success: ", nrow(clean_df), " rows loaded.")) |
|
|
loading_status(FALSE) |
|
|
fetch_stage(0) |
|
|
|
|
|
|
|
|
if (input$main_nav != "Dashboard") { |
|
|
updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
} |
|
|
|
|
|
|
|
|
session$sendCustomMessage("freezeUI", list(text = "Rendering plots...")) |
|
|
later::later(function() { |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
}, 2.0) |
|
|
}) |
|
|
}, 0.1) |
|
|
|
|
|
fetch_stage(-1) |
|
|
}) |
|
|
|
|
|
|
|
|
output$is_loading <- reactive({ |
|
|
loading_status() |
|
|
}) |
|
|
outputOptions(output, "is_loading", suspendWhenHidden = FALSE) |
|
|
|
|
|
output$station_ready <- reactive({ |
|
|
!is.null(station_data()) |
|
|
}) |
|
|
outputOptions(output, "station_ready", suspendWhenHidden = FALSE) |
|
|
|
|
|
output$data_diagnostics <- renderUI({ |
|
|
HTML(loading_diagnostics()) |
|
|
}) |
|
|
|
|
|
output$has_diag <- reactive({ |
|
|
nzchar(loading_diagnostics()) |
|
|
}) |
|
|
outputOptions(output, "has_diag", suspendWhenHidden = FALSE) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output$temp_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_temperature_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$humidity_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_humidity_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$wind_overview_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_wind_overview_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$pressure_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_pressure_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$cloud_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_cloud_cover_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$precip_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_precipitation_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$wind_rose <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_wind_rose_plot(df) |
|
|
}) |
|
|
|
|
|
output$solar_radiation_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_solar_radiation_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$sunshine_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_sunshine_duration_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$snow_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_snow_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$soil_temp_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_soil_temp_plot(df, resolution = isolate(tolower(input$data_resolution))) |
|
|
}) |
|
|
|
|
|
output$diurnal_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_diurnal_plot(df) |
|
|
}) |
|
|
|
|
|
output$weathergami_plot <- renderPlotly({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
create_weathergami_plot(df) |
|
|
}) |
|
|
|
|
|
|
|
|
output$hourly_data_table <- DT::renderDataTable({ |
|
|
req(!loading_status()) |
|
|
df <- station_data() |
|
|
req(df) |
|
|
|
|
|
|
|
|
df <- df %>% mutate(datetime = format(datetime, "%Y-%m-%d %H:%M")) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renaming_vec <- setNames(names(dwd_column_labels), dwd_column_labels) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
display_labels <- setNames(names(dwd_column_labels), dwd_column_labels) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cols_to_rename <- dwd_column_labels[names(dwd_column_labels) %in% names(df)] |
|
|
|
|
|
|
|
|
rename_map <- setNames(names(cols_to_rename), cols_to_rename) |
|
|
|
|
|
|
|
|
req(df) |
|
|
|
|
|
|
|
|
df_display <- df |
|
|
for (col in names(dwd_column_labels)) { |
|
|
if (col %in% names(df_display)) { |
|
|
names(df_display)[names(df_display) == col] <- dwd_column_labels[[col]] |
|
|
} |
|
|
} |
|
|
|
|
|
datatable(df_display, |
|
|
options = list(pageLength = 15, scrollX = TRUE) |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
output$download_hourly <- downloadHandler( |
|
|
filename = function() { |
|
|
id <- current_station_id() |
|
|
res <- if (!is.null(input$data_resolution)) tolower(input$data_resolution) else "data" |
|
|
if (is.null(id)) { |
|
|
paste0("dwd_data_", res, ".xlsx") |
|
|
} else { |
|
|
paste0("dwd_station_", id, "_", res, ".xlsx") |
|
|
} |
|
|
}, |
|
|
content = function(file) { |
|
|
req(station_data()) |
|
|
|
|
|
|
|
|
out_df <- station_data() |
|
|
write_xlsx(out_df, path = file) |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
output$station_info_header <- renderUI({ |
|
|
id <- current_station_id() |
|
|
if (is.null(id)) { |
|
|
return(NULL) |
|
|
} |
|
|
|
|
|
|
|
|
meta <- all_stations() %>% dplyr::filter(id == !!id) |
|
|
if (nrow(meta) == 0) { |
|
|
return(NULL) |
|
|
} |
|
|
|
|
|
s_name <- meta$name[1] |
|
|
s_state <- meta$state[1] |
|
|
s_elev <- meta$elevation[1] |
|
|
|
|
|
|
|
|
res_label <- input$data_resolution |
|
|
res_class <- if (tolower(res_label) == "hourly") "bg-primary" else "bg-success" |
|
|
|
|
|
|
|
|
df <- station_data() |
|
|
if (is.null(df) || nrow(df) == 0) { |
|
|
dates_text <- "No data loaded" |
|
|
} else { |
|
|
date_range <- range(as.Date(df$datetime), na.rm = TRUE) |
|
|
dates_text <- paste(date_range[1], "to", date_range[2]) |
|
|
} |
|
|
|
|
|
|
|
|
card( |
|
|
style = "margin-bottom: 20px; border-left: 5px solid #007bff;", |
|
|
card_body( |
|
|
padding = 15, |
|
|
layout_columns( |
|
|
fill = FALSE, |
|
|
|
|
|
div( |
|
|
strong("Station"), br(), |
|
|
span(s_name, style = "font-size: 1.1rem;"), br(), |
|
|
tags$small(class = "text-muted", paste("ID:", id)) |
|
|
), |
|
|
|
|
|
div( |
|
|
strong("Location"), br(), |
|
|
span(s_state), br(), |
|
|
tags$small(class = "text-muted", paste0(meta$latitude[1], "°N, ", meta$longitude[1], "°E")) |
|
|
), |
|
|
|
|
|
div( |
|
|
strong("Technical"), br(), |
|
|
span(paste0(s_elev, " m")), br(), |
|
|
span(class = paste("badge", res_class), res_label) |
|
|
), |
|
|
|
|
|
div( |
|
|
strong("Data Selection"), br(), |
|
|
span(dates_text) |
|
|
), |
|
|
|
|
|
div( |
|
|
class = "d-flex align-items-center justify-content-end", |
|
|
downloadButton( |
|
|
"download_hourly", |
|
|
label = "Export Excel", |
|
|
class = "btn-sm btn-primary", |
|
|
icon = icon("file-excel") |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
}) |
|
|
|
|
|
output$table <- DT::renderDataTable({ |
|
|
|
|
|
|
|
|
df_display <- filtered_stations() %>% |
|
|
select( |
|
|
"ID" = id, |
|
|
"Name" = name, |
|
|
"State" = state, |
|
|
"Elev." = elevation, |
|
|
"Status" = detailed_summary |
|
|
) |
|
|
|
|
|
datatable(df_display, |
|
|
selection = "none", |
|
|
rownames = FALSE, |
|
|
callback = JS(" |
|
|
table.on('dblclick', 'tr', function() { |
|
|
var rowData = table.row(this).data(); |
|
|
if (rowData !== undefined && rowData !== null) { |
|
|
var stationId = rowData[0]; |
|
|
Shiny.setInputValue('table_station_dblclick', stationId, {priority: 'event'}); |
|
|
} |
|
|
}); |
|
|
") |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$table_station_dblclick, { |
|
|
id_val <- input$table_station_dblclick |
|
|
req(id_val) |
|
|
|
|
|
|
|
|
|
|
|
s <- filtered_stations() %>% filter(id == id_val) |
|
|
|
|
|
|
|
|
if (nrow(s) == 0) { |
|
|
s <- all_stations() %>% filter(id == id_val) |
|
|
} |
|
|
|
|
|
req(nrow(s) > 0) |
|
|
|
|
|
current_station_id(id_val) |
|
|
|
|
|
|
|
|
updateSelectizeInput(session, "station_selector", selected = id_val) |
|
|
|
|
|
|
|
|
lat_val <- s$latitude[1] |
|
|
lng_val <- s$longitude[1] |
|
|
|
|
|
|
|
|
res <- if (is.null(input$data_resolution)) NULL else tolower(input$data_resolution) |
|
|
overall_start <- if ("station_overall_start" %in% names(s)) s$station_overall_start[1] else s$start_date[1] |
|
|
overall_end <- if ("station_overall_end" %in% names(s)) s$station_overall_end[1] else s$end_date[1] |
|
|
lbl <- generate_station_label( |
|
|
s$name[1], s$id[1], s$state[1], |
|
|
s$start_date[1], s$end_date[1], s$detailed_summary[1], |
|
|
overall_start, overall_end, res |
|
|
) |
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), lng_val, lat_val, lbl) |
|
|
|
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
output$details_tabs <- renderUI({ |
|
|
req(input$data_resolution) |
|
|
|
|
|
|
|
|
if (isTRUE(loading_status())) { |
|
|
return(NULL) |
|
|
} |
|
|
|
|
|
res <- isolate(tolower(input$data_resolution)) |
|
|
df <- station_data() |
|
|
req(df) |
|
|
|
|
|
|
|
|
df_clean <- clean_dwd_data(df) |
|
|
|
|
|
|
|
|
has_temp <- ("temp" %in% names(df_clean) && any(!is.na(df_clean$temp))) || |
|
|
("temp_min" %in% names(df_clean) && any(!is.na(df_clean$temp_min))) || |
|
|
("temp_max" %in% names(df_clean) && any(!is.na(df_clean$temp_max))) |
|
|
|
|
|
has_humidity <- ("rh" %in% names(df_clean) && any(!is.na(df_clean$rh))) || |
|
|
("dew_point" %in% names(df_clean) && any(!is.na(df_clean$dew_point))) |
|
|
|
|
|
has_wind <- ("wind_speed" %in% names(df_clean) && any(!is.na(df_clean$wind_speed))) || |
|
|
("wind_gust_max" %in% names(df_clean) && any(!is.na(df_clean$wind_gust_max))) |
|
|
|
|
|
has_pressure <- ("pressure" %in% names(df_clean) && any(!is.na(df_clean$pressure))) || |
|
|
("station_pressure" %in% names(df_clean) && any(!is.na(df_clean$station_pressure))) |
|
|
|
|
|
has_cloud <- ("cloud_cover" %in% names(df_clean) && any(!is.na(df_clean$cloud_cover))) |
|
|
|
|
|
has_precip <- ("precip" %in% names(df_clean) && any(!is.na(df_clean$precip))) |
|
|
|
|
|
has_solar <- ("solar_global" %in% names(df_clean) && any(!is.na(df_clean$solar_global))) |
|
|
|
|
|
has_sunshine <- ("sunshine_duration" %in% names(df_clean) && any(!is.na(df_clean$sunshine_duration))) |
|
|
|
|
|
has_wind_dir <- ("wind_dir" %in% names(df_clean) && any(!is.na(df_clean$wind_dir))) && has_wind |
|
|
|
|
|
has_daily_temp <- ("temp_min" %in% names(df_clean) && any(!is.na(df_clean$temp_min))) && |
|
|
("temp_max" %in% names(df_clean) && any(!is.na(df_clean$temp_max))) |
|
|
|
|
|
has_snow <- ("snow_depth" %in% names(df_clean) && any(!is.na(df_clean$snow_depth))) || |
|
|
("snow_water_equiv" %in% names(df_clean) && any(!is.na(df_clean$snow_water_equiv))) || |
|
|
("snow_fresh_sum" %in% names(df_clean) && any(!is.na(df_clean$snow_fresh_sum))) || |
|
|
("snow_depth_sum" %in% names(df_clean) && any(!is.na(df_clean$snow_depth_sum))) |
|
|
|
|
|
has_soil_temp <- ("soil_temp_2cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_2cm))) || |
|
|
("soil_temp_5cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_5cm))) || |
|
|
("soil_temp_10cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_10cm))) || |
|
|
("soil_temp_20cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_20cm))) || |
|
|
("soil_temp_50cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_50cm))) || |
|
|
("soil_temp_100cm" %in% names(df_clean) && any(!is.na(df_clean$soil_temp_100cm))) |
|
|
|
|
|
|
|
|
plot_list <- tagList() |
|
|
|
|
|
if (has_temp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("temp_plot", height = "320px"))) |
|
|
} |
|
|
if (has_humidity) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("humidity_plot", height = "320px"))) |
|
|
} |
|
|
if (has_wind) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("wind_overview_plot", height = "320px"))) |
|
|
} |
|
|
if (has_pressure) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("pressure_plot", height = "320px"))) |
|
|
} |
|
|
if (has_cloud) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("cloud_plot", height = "320px"))) |
|
|
} |
|
|
if (has_precip) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("precip_plot", height = "320px"))) |
|
|
} |
|
|
if (has_solar) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("solar_radiation_plot", height = "320px"))) |
|
|
} |
|
|
if (has_sunshine) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("sunshine_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (res == "hourly" && has_wind_dir) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("wind_rose", height = "320px"))) |
|
|
} |
|
|
if (res == "daily" && has_daily_temp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("weathergami_plot", height = "320px"))) |
|
|
} |
|
|
if (has_snow) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("snow_plot", height = "320px"))) |
|
|
} |
|
|
if (has_soil_temp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("soil_temp_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
div( |
|
|
class = "row g-3", |
|
|
style = "padding: 10px;", |
|
|
plot_list |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observeEvent(input$main_nav, |
|
|
{ |
|
|
broadcast_state() |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observeEvent(current_station_id(), |
|
|
{ |
|
|
broadcast_state() |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observeEvent(input$dashboard_subtabs, |
|
|
{ |
|
|
broadcast_state() |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observeEvent(input$data_resolution, |
|
|
{ |
|
|
broadcast_state() |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observeEvent(input$date_range, |
|
|
{ |
|
|
broadcast_state() |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
} |
|
|
|