|
|
|
|
|
|
|
|
|
|
|
generate_station_label <- function(name, id, country, start_date, end_date, detailed_summary, resolution_info, val = NULL, label_name = "Observation", unit = "", missing_label = NULL, missing_color = "#8A8F98") { |
|
|
tryCatch( |
|
|
{ |
|
|
val_html <- "" |
|
|
if (!is.null(val) && !is.na(val)) { |
|
|
|
|
|
color <- if (grepl("Temperature", label_name, ignore.case = TRUE)) { |
|
|
if (exists("temp_to_color")) temp_to_color(val) else "#0056b3" |
|
|
} else if (grepl("Precipitation", label_name, ignore.case = TRUE)) { |
|
|
if (exists("precip_to_color")) precip_to_color(val) else "#0056b3" |
|
|
} else if (grepl("Pressure", label_name, ignore.case = TRUE)) { |
|
|
if (exists("pressure_to_color")) pressure_to_color(val) else "#0056b3" |
|
|
} else if (grepl("Wind Speed", label_name, ignore.case = TRUE)) { |
|
|
if (exists("wind_speed_to_color")) wind_speed_to_color(val) else "#0056b3" |
|
|
} else { |
|
|
"#0056b3" |
|
|
} |
|
|
|
|
|
val_html <- paste0( |
|
|
"<div style='margin: 12px 0; padding: 10px; background: ", color, "10; border: 1px solid ", color, "30; border-radius: 8px; text-align: center; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);'>", |
|
|
"<div style='font-size: 0.75em; color: #666; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; margin-bottom: 4px;'>", label_name, "</div>", |
|
|
"<div style='font-size: 2em; font-weight: 900; color: ", color, "; line-height: 1; display: flex; align-items: baseline; justify-content: center;'>", |
|
|
round(val, 1), |
|
|
"<span style='font-size: 0.5em; margin-left: 3px; font-weight: 600;'>", unit, "</span>", |
|
|
"</div>", |
|
|
"</div>" |
|
|
) |
|
|
} else if (!is.null(missing_label) && !is.na(missing_label)) { |
|
|
color <- missing_color |
|
|
val_html <- paste0( |
|
|
"<div style='margin: 12px 0; padding: 10px; background: ", color, "10; border: 1px solid ", color, "30; border-radius: 8px; text-align: center; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);'>", |
|
|
"<div style='font-size: 0.75em; color: #666; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; margin-bottom: 4px;'>", label_name, "</div>", |
|
|
"<div style='font-size: 1.4em; font-weight: 800; color: ", color, "; line-height: 1; display: flex; align-items: baseline; justify-content: center;'>", |
|
|
htmltools::htmlEscape(missing_label), |
|
|
"</div>", |
|
|
"</div>" |
|
|
) |
|
|
} |
|
|
|
|
|
paste0( |
|
|
"<div style='font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif; min-width: 250px; max-width: 300px; padding: 4px;'>", |
|
|
|
|
|
"<div style='border-bottom: 2px solid #f0f0f0; padding-bottom: 10px; margin-bottom: 10px;'>", |
|
|
"<div style='font-size: 1.15em; font-weight: 800; color: #1a1a1a; line-height: 1.3; margin-bottom: 6px;'>", htmltools::htmlEscape(name), "</div>", |
|
|
"<div style='display: flex; justify-content: space-between; align-items: center;'>", |
|
|
"<span style='font-size: 0.8em; color: #0056b3; font-weight: 700; background: #eef4ff; padding: 3px 8px; border-radius: 20px; border: 1px solid #d0e1ff;'>", htmltools::htmlEscape(country), "</span>", |
|
|
"<span style='font-size: 0.75em; color: #888; font-family: monospace; background: #f5f5f5; padding: 2px 5px; border-radius: 4px;'>", id, "</span>", |
|
|
"</div>", |
|
|
"</div>", |
|
|
|
|
|
|
|
|
val_html, |
|
|
|
|
|
|
|
|
"<div style='background: #fafafa; border-radius: 8px; padding: 10px; border: 1px solid #f0f0f0;'>", |
|
|
"<div style='font-size: 0.75em; color: #777; font-weight: 700; margin-bottom: 6px; text-transform: uppercase; display: flex; align-items: center; gap: 4px;'>", |
|
|
"<span style='width: 8px; height: 8px; background: #28a745; border-radius: 50%; display: inline-block;'></span> Status & Capabilities", |
|
|
"</div>", |
|
|
"<div style='font-size: 0.85em; color: #333; line-height: 1.5;'>", |
|
|
resolution_info, |
|
|
"</div>", |
|
|
"</div>", |
|
|
"</div>", |
|
|
"</div>" |
|
|
) |
|
|
}, |
|
|
error = function(e) { |
|
|
paste0("<div style='padding:10px;'><b>", htmltools::htmlEscape(name), "</b><br><small color='#888'>(", id, ")</small></div>") |
|
|
} |
|
|
) |
|
|
} |
|
|
|
|
|
server <- function(input, output, session) { |
|
|
|
|
|
current_station_id <- reactiveVal(NULL) |
|
|
station_data <- reactiveVal(NULL) |
|
|
loading_status <- reactiveVal(FALSE) |
|
|
loading_diagnostics <- reactiveVal("") |
|
|
map_fetch_token <- reactiveVal(0) |
|
|
map_fetch_active <- reactiveVal(FALSE) |
|
|
map_render_pending <- reactiveVal(FALSE) |
|
|
map_render_token <- reactiveVal(NULL) |
|
|
previous_station_choices <- reactiveVal(NULL) |
|
|
url_initialized <- reactiveVal(FALSE) |
|
|
current_resolution <- reactiveVal("Hourly") |
|
|
|
|
|
|
|
|
style_change_trigger <- reactiveVal(0) |
|
|
map_initialized <- reactiveVal(FALSE) |
|
|
stations_before_id <- reactiveVal(NULL) |
|
|
current_raster_layers <- reactiveVal(character(0)) |
|
|
basemap_debounced <- shiny::debounce(reactive(input$basemap), 200) |
|
|
|
|
|
|
|
|
last_dept <- reactiveVal("All") |
|
|
|
|
|
|
|
|
map_bounds <- list( |
|
|
lat_min = 30.0, |
|
|
lat_max = 75.0, |
|
|
lng_min = -25.0, |
|
|
lng_max = 55.0 |
|
|
) |
|
|
|
|
|
|
|
|
station_internal_cols <- c( |
|
|
"station_id", "station_name", "standard_name", "value", "param_name", |
|
|
"level", "method", "period", "period_seconds", "method_rank", "level_rank", |
|
|
"temp", "rh", "wind_dir", "pressure", "pressure_sea", "pressure_station", "precip", "solar_global", "dew_point" |
|
|
) |
|
|
|
|
|
get_station_display_cols <- function(df) { |
|
|
all_cols <- names(df) |
|
|
display_cols <- all_cols[!(all_cols %in% station_internal_cols)] |
|
|
|
|
|
alias_map <- attr(df, "preferred_alias_map") |
|
|
if (!is.null(alias_map) && length(alias_map) > 0) { |
|
|
alias_names <- names(alias_map) |
|
|
for (alias in alias_names) { |
|
|
if (is.null(alias) || is.na(alias) || !nzchar(alias)) next |
|
|
if (!(alias %in% display_cols)) next |
|
|
|
|
|
variant_col <- alias_map[[alias]] |
|
|
if (is.null(variant_col) || is.na(variant_col) || !nzchar(variant_col)) next |
|
|
if (variant_col %in% display_cols && variant_col != alias) { |
|
|
display_cols <- setdiff(display_cols, alias) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
display_cols <- display_cols[order(nchar(display_cols))] |
|
|
|
|
|
kept_cols <- character(0) |
|
|
for (col in display_cols) { |
|
|
if (col == "datetime") { |
|
|
kept_cols <- c(kept_cols, col) |
|
|
next |
|
|
} |
|
|
|
|
|
is_duplicate <- FALSE |
|
|
for (prev_col in kept_cols) { |
|
|
if (prev_col == "datetime") next |
|
|
|
|
|
v1 <- df[[col]] |
|
|
v2 <- df[[prev_col]] |
|
|
if (length(v1) != length(v2)) next |
|
|
|
|
|
same <- if (is.numeric(v1) && is.numeric(v2)) { |
|
|
(is.na(v1) & is.na(v2)) | (!is.na(v1) & !is.na(v2) & abs(v1 - v2) <= 1e-9) |
|
|
} else { |
|
|
(is.na(v1) & is.na(v2)) | (!is.na(v1) & !is.na(v2) & as.character(v1) == as.character(v2)) |
|
|
} |
|
|
|
|
|
if (all(same)) { |
|
|
is_duplicate <- TRUE |
|
|
break |
|
|
} |
|
|
} |
|
|
|
|
|
if (!is_duplicate) { |
|
|
kept_cols <- c(kept_cols, col) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
final_cols <- all_cols[all_cols %in% kept_cols] |
|
|
display_cols <- final_cols[final_cols %in% display_cols] |
|
|
|
|
|
|
|
|
display_cols <- sort_weather_columns(display_cols) |
|
|
|
|
|
display_cols |
|
|
} |
|
|
|
|
|
format_wind_speed_decimals <- function(df, digits = 1) { |
|
|
if (is.null(df) || nrow(df) == 0) { |
|
|
return(df) |
|
|
} |
|
|
|
|
|
wind_cols <- names(df)[grepl("^(wind_speed|wind_gust|wind_speed_of_gust)", names(df))] |
|
|
if (length(wind_cols) == 0) { |
|
|
return(df) |
|
|
} |
|
|
|
|
|
for (col in wind_cols) { |
|
|
if (is.numeric(df[[col]])) { |
|
|
df[[col]] <- round(df[[col]], digits) |
|
|
} |
|
|
} |
|
|
|
|
|
df |
|
|
} |
|
|
|
|
|
queue_map_render <- function(message = "Plotting stations...") { |
|
|
if (!isTRUE(isolate(map_render_pending()))) { |
|
|
return() |
|
|
} |
|
|
|
|
|
token <- isolate(map_render_token()) |
|
|
if (is.null(token)) { |
|
|
map_render_pending(FALSE) |
|
|
return() |
|
|
} |
|
|
|
|
|
session$sendCustomMessage("awaitMapRender", list( |
|
|
mapId = "map", |
|
|
token = token, |
|
|
text = message |
|
|
)) |
|
|
} |
|
|
|
|
|
label_layer_ids <- c( |
|
|
|
|
|
"waterway_line_label", "water_name_point_label", "water_name_line_label", |
|
|
"highway-name-path", "highway-name-minor", "highway-name-major", |
|
|
"highway-shield-non-us", "highway-shield-us-interstate", "road_shield_us", |
|
|
"airport", "label_other", "label_village", "label_town", "label_state", |
|
|
"label_city", "label_city_capital", "label_country_3", "label_country_2", "label_country_1", |
|
|
|
|
|
"road_oneway", "road_oneway_opposite", "poi_r20", "poi_r7", "poi_r1", "poi_transit", |
|
|
|
|
|
"waterway-line-label", "water-name-point-label", "water-name-line-label", |
|
|
"highway-shield-non-us", "highway-shield-us-interstate", "road-shield-us", |
|
|
"label-other", "label-village", "label-town", "label-state", |
|
|
"label-city", "label-city-capital", "label-country-3", "label-country-2", "label-country-1", |
|
|
|
|
|
"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" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
non_label_layer_ids <- c( |
|
|
"background", "park", "water", "landcover_ice_shelf", "landcover_glacier", |
|
|
"landuse_residential", "landcover_wood", "waterway", "building", |
|
|
"tunnel_motorway_casing", "tunnel_motorway_inner", "aeroway-taxiway", |
|
|
"aeroway-runway-casing", "aeroway-area", "aeroway-runway", |
|
|
"road_area_pier", "road_pier", "highway_path", "highway_minor", |
|
|
"highway_major_casing", "highway_major_inner", "highway_major_subtle", |
|
|
"highway_motorway_casing", "highway_motorway_inner", "highway_motorway_subtle", |
|
|
"railway_transit", "railway_transit_dashline", "railway_service", |
|
|
"railway_service_dashline", "railway", "railway_dashline", |
|
|
"highway_motorway_bridge_casing", "highway_motorway_bridge_inner", |
|
|
"boundary_3", "boundary_2", "boundary_disputed" |
|
|
) |
|
|
|
|
|
apply_label_visibility <- function(proxy, show_labels) { |
|
|
visibility <- if (isTRUE(show_labels)) "visible" else "none" |
|
|
for (layer_id in label_layer_ids) { |
|
|
tryCatch( |
|
|
{ |
|
|
proxy %>% set_layout_property(layer_id, "visibility", visibility) |
|
|
}, |
|
|
error = function(e) { |
|
|
|
|
|
} |
|
|
) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
parse_url_params <- function(query_string) { |
|
|
if (is.null(query_string) || query_string == "" || query_string == "?") { |
|
|
return(list()) |
|
|
} |
|
|
qs <- sub("^\\?", "", query_string) |
|
|
if (qs == "") { |
|
|
return(list()) |
|
|
} |
|
|
|
|
|
pairs <- strsplit(qs, "&")[[1]] |
|
|
params <- list() |
|
|
for (pair in pairs) { |
|
|
kv <- strsplit(pair, "=")[[1]] |
|
|
if (length(kv) == 2) { |
|
|
params[[kv[1]]] <- URLdecode(kv[2]) |
|
|
} |
|
|
} |
|
|
params |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
} |
|
|
|
|
|
station_id <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$id[1]) else NULL |
|
|
station_name <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$name[1]) else NULL |
|
|
s_country <- if (!is.null(st_meta) && nrow(st_meta) > 0) { |
|
|
as.character(st_meta$country[1]) |
|
|
} else if (!is.null(input$country_selector) && length(input$country_selector) > 0 && !("All Countries" %in% input$country_selector)) { |
|
|
paste(input$country_selector, collapse = ", ") |
|
|
} else { |
|
|
NULL |
|
|
} |
|
|
|
|
|
|
|
|
resolution <- current_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" |
|
|
} |
|
|
|
|
|
session$sendCustomMessage("updateParentURL", list( |
|
|
station = station_name, |
|
|
stationName = station_name, |
|
|
country = s_country, |
|
|
resolution = resolution, |
|
|
view = view, |
|
|
view = view, |
|
|
parameter = input$map_parameter, |
|
|
hours_ago = if (!is.null(input$time_slider)) input$time_slider else 0, |
|
|
time_label = if (!is.null(input$time_slider)) { |
|
|
target_time <- Sys.time() - (input$time_slider * 3600) |
|
|
format(target_time, "%Y-%m-%d %H:00 UTC", tz = "UTC") |
|
|
} else { |
|
|
format(Sys.time(), "%Y-%m-%d %H:00 UTC", tz = "UTC") |
|
|
} |
|
|
)) |
|
|
|
|
|
|
|
|
hours_val <- if (!is.null(input$time_slider)) input$time_slider else 0 |
|
|
query_parts <- c() |
|
|
if (!is.null(station_name)) query_parts <- c(query_parts, paste0("station=", URLencode(station_name))) |
|
|
if (!is.null(s_country)) query_parts <- c(query_parts, paste0("country=", URLencode(s_country))) |
|
|
query_parts <- c( |
|
|
query_parts, |
|
|
paste0("resolution=", URLencode(resolution)), |
|
|
paste0("view=", URLencode(view)), |
|
|
paste0("parameter=", URLencode(if (is.null(input$map_parameter)) "air_temperature" else input$map_parameter)) |
|
|
) |
|
|
query_string <- paste0("?", paste(query_parts, collapse = "&")) |
|
|
updateQueryString(query_string, mode = "push") |
|
|
} |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(!url_initialized()) |
|
|
|
|
|
query <- session$clientData$url_search |
|
|
params <- parse_url_params(query) |
|
|
|
|
|
if (length(params) == 0) { |
|
|
url_initialized(TRUE) |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$resolution) && params$resolution %in% c("Hourly", "Daily", "6-min", "Monthly")) { |
|
|
current_resolution(params$resolution) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$parameter)) { |
|
|
updateSelectInput(session, "map_parameter", selected = params$parameter) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$hours_ago)) { |
|
|
val <- as.numeric(params$hours_ago) |
|
|
if (!is.na(val) && val >= 0 && val <= 23) { |
|
|
updateSliderInput(session, "time_slider", value = val) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!is.null(params$country)) { |
|
|
shinyjs::delay(300, { |
|
|
selected_country <- strsplit(params$country, ",")[[1]][1] |
|
|
updateSelectizeInput(session, "country_selector", selected = trimws(selected_country)) |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(params$station)) { |
|
|
station_ref <- params$station |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shinyjs::delay(500, { |
|
|
st <- isolate(all_stations()) |
|
|
if (!is.null(st)) { |
|
|
|
|
|
match <- st %>% filter(id == station_ref | name == station_ref) |
|
|
if (nrow(match) > 0) { |
|
|
target_id <- match$id[1] |
|
|
current_station_id(target_id) |
|
|
updateSelectizeInput(session, "station_selector", selected = target_id) |
|
|
|
|
|
|
|
|
fetch_trigger(fetch_trigger() + 1) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
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") { |
|
|
nav_select("main_nav", "Stations Info") |
|
|
} else if (view %in% c("dashboard-plots", "dashboard-data")) { |
|
|
nav_select("main_nav", "Dashboard") |
|
|
if (view == "dashboard-data") { |
|
|
shinyjs::delay(200, { |
|
|
|
|
|
shinyjs::runjs("$('#dashboard_subtabs a[data-value=\"Data\"]').tab('show');") |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
url_initialized(TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
input$station_selector |
|
|
input$main_nav |
|
|
input$dashboard_subtabs |
|
|
current_station_id() |
|
|
input$map_parameter |
|
|
input$time_slider |
|
|
input$country_selector |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
req(url_initialized()) |
|
|
|
|
|
|
|
|
broadcast_state() |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
current_index <- reactive({ |
|
|
NULL |
|
|
}) |
|
|
|
|
|
all_stations <- reactive({ |
|
|
|
|
|
get_meteogate_stations() |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
is_loading <- loading_status() |
|
|
|
|
|
|
|
|
inputs_to_toggle <- c( |
|
|
"station_selector", |
|
|
"country_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) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observe({ |
|
|
st <- all_stations() |
|
|
req(st) |
|
|
|
|
|
|
|
|
obs <- viewport_obs_data() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
country_list <- if (!is.null(obs) && nrow(obs) > 0) { |
|
|
|
|
|
ids_with_data <- unique(obs$station_id) |
|
|
visible_stations <- st %>% filter(id %in% ids_with_data) |
|
|
sort(unique(visible_stations$country)) |
|
|
} else { |
|
|
|
|
|
sort(unique(st$country)) |
|
|
} |
|
|
|
|
|
|
|
|
countries <- c("All Countries", country_list) |
|
|
|
|
|
|
|
|
|
|
|
current_sel <- isolate(input$country_selector) |
|
|
updateSelectizeInput(session, "country_selector", choices = countries, selected = current_sel, server = TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$country_selector, |
|
|
{ |
|
|
req(all_stations()) |
|
|
|
|
|
|
|
|
df <- all_stations() |
|
|
|
|
|
if (!is.null(input$country_selector) && length(input$country_selector) > 0 && !("All Countries" %in% input$country_selector)) { |
|
|
df_subset <- df %>% filter(country %in% input$country_selector) |
|
|
|
|
|
if (nrow(df_subset) > 0) { |
|
|
lons <- range(df_subset$longitude, na.rm = TRUE) |
|
|
lats <- range(df_subset$latitude, na.rm = TRUE) |
|
|
|
|
|
if (nrow(df_subset) == 1) { |
|
|
maplibre_proxy("map") %>% |
|
|
fly_to(center = c(df_subset$longitude[1], df_subset$latitude[1]), zoom = 12) |
|
|
} else { |
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds(c(lons[1], lats[1], lons[2], lats[2]), animate = TRUE) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds( |
|
|
c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max), |
|
|
animate = TRUE |
|
|
) |
|
|
} |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
filtered_stations <- reactive({ |
|
|
req(all_stations(), current_resolution()) |
|
|
df <- all_stations() |
|
|
res <- tolower(current_resolution()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
df <- df %>% mutate( |
|
|
st_params = if ("available_params" %in% names(.)) available_params else NA_character_, |
|
|
resolution_info = purrr::map2_chr(end_date, st_params, function(ed, p) { |
|
|
p_text <- get_resolution_params(res, p) |
|
|
paste0( |
|
|
ifelse(as.Date(ed) >= Sys.Date() - 7, "Active Now", "Historical Data Only"), |
|
|
"<br>Available:<br>", p_text |
|
|
) |
|
|
}) |
|
|
) |
|
|
|
|
|
|
|
|
as.data.frame(df) |
|
|
}) |
|
|
|
|
|
filtered_stations_sf <- reactive({ |
|
|
df <- filtered_stations() |
|
|
req(df) |
|
|
if (nrow(df) == 0) { |
|
|
return(sf::st_as_sf(df, coords = c("longitude", "latitude"), crs = 4326, remove = FALSE)) |
|
|
} |
|
|
|
|
|
sf::st_as_sf(df, coords = c("longitude", "latitude"), crs = 4326, remove = FALSE) |
|
|
}) |
|
|
|
|
|
|
|
|
output$time_slider_ui <- renderUI({ |
|
|
|
|
|
initial_minute <- as.numeric(format(Sys.time(), "%M")) |
|
|
initial_val <- if (initial_minute < 30) 1 else 0 |
|
|
|
|
|
sliderInput( |
|
|
"time_slider", |
|
|
label = NULL, |
|
|
min = 0, |
|
|
max = 23, |
|
|
value = initial_val, |
|
|
step = 1, |
|
|
ticks = TRUE, |
|
|
width = "100%" |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$station_selector, { |
|
|
req(input$station_selector) |
|
|
id_val <- input$station_selector |
|
|
|
|
|
|
|
|
|
|
|
s_meta <- filtered_stations() %>% filter(id == id_val) |
|
|
|
|
|
if (nrow(s_meta) == 0) { |
|
|
|
|
|
s_meta <- all_stations() %>% filter(id == id_val) |
|
|
|
|
|
if (!("resolution_info" %in% names(s_meta))) { |
|
|
s_meta$resolution_info <- "Data status unavailable for current filter" |
|
|
} |
|
|
} |
|
|
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) |
|
|
|
|
|
|
|
|
obs <- viewport_obs_data() |
|
|
match <- if (!is.null(obs)) obs %>% filter(station_id == id_val) else NULL |
|
|
current_temp <- if (!is.null(match) && nrow(match) > 0) match$temp[1] else NULL |
|
|
current_param_name <- if (!is.null(match) && nrow(match) > 0 && "param_name" %in% names(match)) { |
|
|
match$param_name[1] |
|
|
} else { |
|
|
NA_character_ |
|
|
} |
|
|
|
|
|
|
|
|
current_param <- isolate(input$map_parameter) |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
param_label <- if (current_param == "precipitation_amount") "Precipitation" else "Air Temperature" |
|
|
param_unit <- if (current_param == "precipitation_amount") "mm" else "°C" |
|
|
missing_label <- if (current_param == "precipitation_amount") "No precip available at this hour" else NULL |
|
|
|
|
|
res_info <- if ("resolution_info" %in% names(s_meta)) s_meta$resolution_info[1] else "Data status unavailable" |
|
|
if (is.null(res_info) || is.na(res_info)) res_info <- "Data status unavailable for current filter" |
|
|
|
|
|
label_name <- if (current_param == "precipitation_amount") { |
|
|
format_precip_label(current_param_name, param_label) |
|
|
} else { |
|
|
param_label |
|
|
} |
|
|
|
|
|
lbl <- generate_station_label( |
|
|
s_meta$name[1], s_meta$id[1], s_meta$country[1], |
|
|
s_meta$start_date[1], s_meta$end_date[1], s_meta$detailed_summary[1], |
|
|
res_info, |
|
|
val = current_temp, |
|
|
label_name = label_name, |
|
|
unit = param_unit, |
|
|
missing_label = missing_label |
|
|
) |
|
|
|
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), lng_val, lat_val, lbl) |
|
|
|
|
|
|
|
|
}) |
|
|
|
|
|
start_map_fetch <- function(message) { |
|
|
if (isTRUE(isolate(loading_status()))) { |
|
|
return(NULL) |
|
|
} |
|
|
|
|
|
token <- isolate(map_fetch_token()) + 1 |
|
|
map_fetch_token(token) |
|
|
map_fetch_active(TRUE) |
|
|
map_render_pending(TRUE) |
|
|
map_render_token(token) |
|
|
|
|
|
session$sendCustomMessage("freezeUI", list(text = message, allowCancel = FALSE)) |
|
|
|
|
|
token |
|
|
} |
|
|
|
|
|
end_map_fetch <- function(token) { |
|
|
if (is.null(token)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
if (isolate(map_fetch_token()) != token) { |
|
|
return() |
|
|
} |
|
|
|
|
|
map_fetch_active(FALSE) |
|
|
if (!isTRUE(isolate(loading_status())) && !isTRUE(isolate(map_render_pending()))) { |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
} |
|
|
} |
|
|
|
|
|
unfreeze_ui_if_idle <- function() { |
|
|
if (!isTRUE(isolate(loading_status())) && !isTRUE(isolate(map_fetch_active())) && !isTRUE(isolate(map_render_pending()))) { |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
} |
|
|
} |
|
|
|
|
|
observeEvent(input$map_rendered, |
|
|
{ |
|
|
req(input$map_rendered) |
|
|
token <- suppressWarnings(as.numeric(input$map_rendered$token)) |
|
|
current_token <- isolate(map_render_token()) |
|
|
if (is.na(token) || is.null(current_token) || token != current_token) { |
|
|
return() |
|
|
} |
|
|
|
|
|
map_render_pending(FALSE) |
|
|
if (!isTRUE(isolate(loading_status())) && !isTRUE(isolate(map_fetch_active()))) { |
|
|
session$sendCustomMessage("unfreezeUI", list()) |
|
|
} |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
viewport_obs_data <- reactiveVal(NULL) |
|
|
|
|
|
observe({ |
|
|
df <- filtered_stations() |
|
|
req(df, current_resolution(), input$map_parameter, !is.null(input$time_slider)) |
|
|
|
|
|
hours_ago <- input$time_slider |
|
|
|
|
|
param_slug <- input$map_parameter |
|
|
selected_param_ui <- case_when( |
|
|
param_slug == "air_temperature" ~ "Air Temperature", |
|
|
param_slug == "precipitation_amount" ~ "Precipitation", |
|
|
param_slug == "air_pressure_at_mean_sea_level" ~ "Sea Level Pressure", |
|
|
param_slug == "wind_speed" ~ "Wind Speed", |
|
|
TRUE ~ "Data" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if (nrow(df) > 0) { |
|
|
lats <- range(df$latitude, na.rm = TRUE) |
|
|
lons <- range(df$longitude, na.rm = TRUE) |
|
|
|
|
|
|
|
|
bounds <- list( |
|
|
lat_min = lats[1] - 0.1, |
|
|
lat_max = lats[2] + 0.1, |
|
|
lng_min = lons[1] - 0.1, |
|
|
lng_max = lons[2] + 0.1 |
|
|
) |
|
|
} else { |
|
|
if (!isTRUE(map_initialized())) { |
|
|
viewport_obs_data(NULL) |
|
|
return() |
|
|
} |
|
|
bounds <- map_bounds |
|
|
} |
|
|
|
|
|
time_label <- if (hours_ago == 0) "current hour" else paste0(hours_ago, " hour(s) ago") |
|
|
map_fetch_message <- paste0("Fetching map ", selected_param_ui, " (", time_label, ")...") |
|
|
fetch_token <- start_map_fetch(map_fetch_message) |
|
|
if (is.null(fetch_token)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
current_session <- shiny::getDefaultReactiveDomain() |
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(current_session, { |
|
|
if (isolate(map_fetch_token()) != fetch_token) { |
|
|
return() |
|
|
} |
|
|
|
|
|
result <- tryCatch( |
|
|
get_observations_for_viewport( |
|
|
zoom = 10, |
|
|
bounds = bounds, |
|
|
hours_ago = hours_ago, |
|
|
stations = df, |
|
|
param_name = param_slug |
|
|
), |
|
|
error = function(e) NULL |
|
|
) |
|
|
|
|
|
if (isolate(map_fetch_token()) != fetch_token) { |
|
|
return() |
|
|
} |
|
|
|
|
|
viewport_obs_data(result) |
|
|
end_map_fetch(fetch_token) |
|
|
}) |
|
|
}, delay = 0.05) |
|
|
}) |
|
|
|
|
|
|
|
|
output$station_count_filtered <- renderText({ |
|
|
n <- nrow(filtered_stations()) |
|
|
paste(n, "stations found") |
|
|
}) |
|
|
|
|
|
|
|
|
output$temp_data_info <- renderUI({ |
|
|
hours_ago <- if (!is.null(input$time_slider)) input$time_slider else return(NULL) |
|
|
obs_data <- viewport_obs_data() |
|
|
|
|
|
if (is.null(obs_data) || nrow(obs_data) == 0) { |
|
|
return(tags$small(style = "color: #888;", "No temperature data available in view")) |
|
|
} |
|
|
|
|
|
|
|
|
avg_time <- mean(obs_data$datetime, na.rm = TRUE) |
|
|
time_str <- format(avg_time, "%Y-%m-%d %H:%M UTC") |
|
|
|
|
|
div( |
|
|
style = "font-size: 0.85em; color: #555; margin-top: 5px;", |
|
|
tags$span( |
|
|
style = "display: flex; align-items: center; gap: 5px;", |
|
|
bsicons::bs_icon("thermometer-half", size = "1em"), |
|
|
paste0("Showing: ", nrow(obs_data), " stations") |
|
|
), |
|
|
div(style = "margin-left: 20px; font-weight: 500;", time_str), |
|
|
tags$small( |
|
|
style = "color: #888;", |
|
|
if (hours_ago == 0) "Current observations" else paste0(hours_ago, " hour(s) ago") |
|
|
) |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
output$selected_time_label <- renderText({ |
|
|
hours_ago <- if (!is.null(input$time_slider)) input$time_slider else return("") |
|
|
target_time <- Sys.time() - (hours_ago * 3600) |
|
|
format(target_time, "%Y-%m-%d %H:00 UTC", tz = "UTC") |
|
|
}) |
|
|
|
|
|
|
|
|
output$map <- renderMaplibre({ |
|
|
maplibre( |
|
|
style = "https://tiles.openfreemap.org/styles/positron", |
|
|
center = c(10, 50), |
|
|
zoom = 4 |
|
|
) %>% |
|
|
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) |
|
|
|
|
|
|
|
|
lon_span <- lons[2] - lons[1] |
|
|
lat_span <- lats[2] - lats[1] |
|
|
|
|
|
|
|
|
if (nrow(df) == 1) { |
|
|
maplibre_proxy("map") %>% |
|
|
fly_to(center = c(df$longitude[1], df$latitude[1]), zoom = 12) |
|
|
} else if (lon_span > 60 || lat_span > 40) { |
|
|
|
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
fly_to(center = c(2.2137, 20), zoom = 2) |
|
|
} else { |
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds(c(lons[1], lats[1], lons[2], lats[2]), animate = 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(basemap_debounced(), { |
|
|
basemap <- basemap_debounced() |
|
|
proxy <- maplibre_proxy("map") |
|
|
|
|
|
if (basemap %in% c("ofm_positron", "ofm_bright")) { |
|
|
|
|
|
style_url <- switch(basemap, |
|
|
"ofm_positron" = "https://tiles.openfreemap.org/styles/positron", |
|
|
"ofm_bright" = "https://tiles.openfreemap.org/styles/bright" |
|
|
) |
|
|
|
|
|
proxy %>% |
|
|
set_style(style_url, preserve_layers = FALSE) |
|
|
|
|
|
|
|
|
stations_before_id("waterway_line_label") |
|
|
|
|
|
current_session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- basemap |
|
|
|
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(current_session, { |
|
|
|
|
|
current_basemap <- isolate(input$basemap) |
|
|
if (current_basemap != selected_basemap) { |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
apply_label_visibility(maplibre_proxy("map"), isolate(input$show_labels)) |
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.35) |
|
|
} else if (basemap == "sentinel") { |
|
|
|
|
|
|
|
|
proxy %>% |
|
|
set_style(ofm_positron_style, preserve_layers = FALSE) |
|
|
|
|
|
current_session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- 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("sentinel_source_", unique_suffix) |
|
|
layer_id <- paste0("sentinel_layer_", unique_suffix) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
add_raster_source(id = source_id, tiles = c(sentinel_url), tileSize = 256, attribution = sentinel_attribution) %>% |
|
|
add_layer( |
|
|
id = layer_id, |
|
|
type = "raster", |
|
|
source = source_id, |
|
|
paint = list("raster-opacity" = 1), |
|
|
before_id = "background" |
|
|
) |
|
|
|
|
|
|
|
|
for (layer_id_kill in non_label_layer_ids) { |
|
|
tryCatch( |
|
|
{ |
|
|
maplibre_proxy("map") %>% set_layout_property(layer_id_kill, "visibility", "none") |
|
|
}, |
|
|
error = function(e) { |
|
|
|
|
|
} |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
apply_label_visibility(maplibre_proxy("map"), isolate(input$show_labels)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stations_before_id("waterway_line_label") |
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.5) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$show_labels, |
|
|
{ |
|
|
apply_label_visibility(maplibre_proxy("map"), input$show_labels) |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
observeEvent(current_resolution(), |
|
|
{ |
|
|
station_data(NULL) |
|
|
parsed_data_list(list()) |
|
|
|
|
|
|
|
|
if (!is.null(current_station_id())) { |
|
|
loading_status(TRUE) |
|
|
} |
|
|
}, |
|
|
priority = 1000, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
observe({ |
|
|
df <- filtered_stations_sf() |
|
|
req(df, current_resolution()) |
|
|
|
|
|
|
|
|
req(map_initialized()) |
|
|
|
|
|
|
|
|
style_change_trigger() |
|
|
|
|
|
|
|
|
current_param <- isolate(input$map_parameter) |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
is_precip <- current_param == "precipitation_amount" |
|
|
is_pressure <- current_param == "air_pressure_at_mean_sea_level" |
|
|
is_wind <- current_param == "wind_speed" |
|
|
is_wind <- current_param == "wind_speed" |
|
|
|
|
|
param_label <- case_when( |
|
|
is_precip ~ "Precipitation", |
|
|
is_pressure ~ "Sea Level Pressure", |
|
|
is_wind ~ "Wind Speed", |
|
|
TRUE ~ "Air Temperature" |
|
|
) |
|
|
param_unit <- case_when( |
|
|
is_precip ~ "mm", |
|
|
is_pressure ~ "hPa", |
|
|
is_wind ~ "m/s", |
|
|
TRUE ~ "°C" |
|
|
) |
|
|
missing_label <- case_when( |
|
|
is_precip ~ "No precip available at this hour", |
|
|
is_pressure ~ "No pressure available", |
|
|
is_wind ~ "No wind available", |
|
|
TRUE ~ NA_character_ |
|
|
) |
|
|
|
|
|
|
|
|
obs_data <- viewport_obs_data() |
|
|
|
|
|
if (!is.null(obs_data) && nrow(obs_data) > 0) { |
|
|
|
|
|
df <- df %>% |
|
|
left_join(obs_data, by = c("id" = "station_id")) |
|
|
if (!"param_name" %in% names(df)) { |
|
|
df$param_name <- NA_character_ |
|
|
} |
|
|
} else { |
|
|
df$temp <- NA_real_ |
|
|
df$param_name <- NA_character_ |
|
|
if (!is_precip) { |
|
|
|
|
|
maplibre_proxy("map") %>% clear_layer("stations") |
|
|
queue_map_render() |
|
|
return() |
|
|
} |
|
|
} |
|
|
|
|
|
if (!is_precip) { |
|
|
|
|
|
df <- df %>% filter(!is.na(temp)) |
|
|
} |
|
|
|
|
|
if (nrow(df) == 0) { |
|
|
maplibre_proxy("map") %>% clear_layer("stations") |
|
|
queue_map_render() |
|
|
return() |
|
|
} |
|
|
|
|
|
if (is_precip) { |
|
|
df <- df %>% mutate(label_name = format_precip_label(param_name, param_label)) |
|
|
} else { |
|
|
df$label_name <- param_label |
|
|
} |
|
|
|
|
|
use_detailed_tooltips <- isTRUE(input$detailed_tooltips) |
|
|
if (use_detailed_tooltips) { |
|
|
|
|
|
df <- df %>% mutate( |
|
|
tooltip_content = purrr::pmap_chr( |
|
|
list(name, id, country, start_date, end_date, detailed_summary, resolution_info, temp, label_name), |
|
|
function(name, id, country, start_date, end_date, detailed_summary, resolution_info, val, label_name) { |
|
|
generate_station_label(name, id, country, start_date, end_date, detailed_summary, resolution_info, |
|
|
val = val, label_name = label_name, unit = param_unit, missing_label = missing_label |
|
|
) |
|
|
} |
|
|
) |
|
|
) |
|
|
} else { |
|
|
|
|
|
label_name_vec <- df$label_name |
|
|
value_text <- if (is_precip) { |
|
|
fallback_text <- if (!is.null(missing_label) && !is.na(missing_label)) missing_label else "No data" |
|
|
ifelse(!is.na(df$temp), paste0(round(df$temp, 1), " ", param_unit), fallback_text) |
|
|
} else { |
|
|
paste0(round(df$temp, 1), " ", param_unit) |
|
|
} |
|
|
|
|
|
df$tooltip_content <- paste0( |
|
|
"<div style='min-width: 180px; font-size: 13px;'>", |
|
|
"<div style='font-weight: 700; font-size: 1.1em;'>", htmltools::htmlEscape(df$name), "</div>", |
|
|
"<div style='font-size: 0.95em; color: #666;'>", htmltools::htmlEscape(df$country), "</div>", |
|
|
"<div style='font-size: 0.95em; color: #666;'>", |
|
|
htmltools::htmlEscape(label_name_vec), ": ", htmltools::htmlEscape(value_text), |
|
|
"</div>", |
|
|
"</div>" |
|
|
) |
|
|
} |
|
|
|
|
|
if (nrow(df) > 0) { |
|
|
|
|
|
df <- df %>% arrange(!is.na(temp), temp) |
|
|
|
|
|
|
|
|
df$temp[is.na(df$temp)] <- -999 |
|
|
|
|
|
|
|
|
color_expr <- get_maplibre_color_expr(current_param, "temp") |
|
|
|
|
|
|
|
|
map_data <- df %>% |
|
|
select(id, name, country, temp, tooltip_content, geometry) |
|
|
|
|
|
before_layer_id <- isolate(stations_before_id()) |
|
|
|
|
|
|
|
|
stroke_width <- if (current_param == "precipitation_amount") 0.5 else 0 |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
clear_layer("stations") %>% |
|
|
add_circle_layer( |
|
|
id = "stations", |
|
|
source = map_data, |
|
|
circle_color = color_expr, |
|
|
circle_radius = 7, |
|
|
circle_stroke_color = "#333333", |
|
|
circle_stroke_width = stroke_width, |
|
|
circle_opacity = 0.8, |
|
|
tooltip = get_column("tooltip_content"), |
|
|
before_id = before_layer_id |
|
|
) |
|
|
|
|
|
|
|
|
sid <- isolate(current_station_id()) |
|
|
if (!is.null(sid)) { |
|
|
sel_row <- df %>% filter(id == sid) |
|
|
if (nrow(sel_row) > 0) { |
|
|
|
|
|
sel_temp <- if (sel_row$temp[1] <= -999) NA_real_ else sel_row$temp[1] |
|
|
sel_label <- generate_station_label( |
|
|
sel_row$name[1], sel_row$id[1], sel_row$country[1], |
|
|
sel_row$start_date[1], sel_row$end_date[1], sel_row$detailed_summary[1], |
|
|
sel_row$resolution_info[1], |
|
|
val = sel_temp, |
|
|
label_name = sel_row$label_name[1], |
|
|
unit = param_unit, |
|
|
missing_label = missing_label |
|
|
) |
|
|
highlight_selected_station(maplibre_proxy("map"), sel_row$longitude[1], sel_row$latitude[1], sel_label, move_map = FALSE) |
|
|
} else { |
|
|
maplibre_proxy("map") %>% clear_layer("selected-highlight") |
|
|
} |
|
|
} |
|
|
|
|
|
queue_map_render() |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
id_val <- current_station_id() |
|
|
if (is.null(id_val)) { |
|
|
maplibre_proxy("map") %>% clear_layer("selected-highlight") |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
s_meta <- filtered_stations() %>% filter(id == id_val) |
|
|
if (nrow(s_meta) == 0) { |
|
|
s_meta <- all_stations() %>% filter(id == id_val) |
|
|
} |
|
|
|
|
|
|
|
|
req(nrow(s_meta) > 0) |
|
|
|
|
|
|
|
|
obs <- viewport_obs_data() |
|
|
match <- if (!is.null(obs)) obs %>% filter(station_id == id_val) else NULL |
|
|
current_temp <- if (!is.null(match) && nrow(match) > 0) match$temp[1] else NULL |
|
|
current_param_name <- if (!is.null(match) && nrow(match) > 0 && "param_name" %in% names(match)) { |
|
|
match$param_name[1] |
|
|
} else { |
|
|
NA_character_ |
|
|
} |
|
|
|
|
|
|
|
|
current_param <- isolate(input$map_parameter) |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
is_precip <- current_param == "precipitation_amount" |
|
|
is_pressure <- current_param == "air_pressure_at_mean_sea_level" |
|
|
is_wind <- current_param == "wind_speed" |
|
|
is_wind <- current_param == "wind_speed" |
|
|
|
|
|
param_label <- case_when( |
|
|
is_precip ~ "Precipitation", |
|
|
is_pressure ~ "Sea Level Pressure", |
|
|
is_wind ~ "Wind Speed", |
|
|
TRUE ~ "Air Temperature" |
|
|
) |
|
|
param_unit <- case_when( |
|
|
is_precip ~ "mm", |
|
|
is_pressure ~ "hPa", |
|
|
is_wind ~ "m/s", |
|
|
TRUE ~ "°C" |
|
|
) |
|
|
missing_label <- case_when( |
|
|
is_precip ~ "No precip available at this hour", |
|
|
is_pressure ~ "No pressure available", |
|
|
is_wind ~ "No wind available", |
|
|
TRUE ~ NA_character_ |
|
|
) |
|
|
|
|
|
res_info <- if ("resolution_info" %in% names(s_meta)) s_meta$resolution_info[1] else "Data status unavailable" |
|
|
if (is.null(res_info) || is.na(res_info)) res_info <- "Data status unavailable for current filter" |
|
|
|
|
|
label_name <- if (current_param == "precipitation_amount") { |
|
|
format_precip_label(current_param_name, param_label) |
|
|
} else { |
|
|
param_label |
|
|
} |
|
|
|
|
|
lbl <- generate_station_label( |
|
|
s_meta$name[1], s_meta$id[1], s_meta$country[1], |
|
|
s_meta$start_date[1], s_meta$end_date[1], s_meta$detailed_summary[1], |
|
|
res_info, |
|
|
val = current_temp, |
|
|
label_name = label_name, |
|
|
unit = param_unit, |
|
|
missing_label = missing_label |
|
|
) |
|
|
|
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), s_meta$longitude[1], s_meta$latitude[1], lbl, move_map = FALSE) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
current_station_id(id_val) |
|
|
|
|
|
|
|
|
s_meta <- filtered_stations() %>% filter(id == id_val) |
|
|
if (nrow(s_meta) == 0) { |
|
|
s_meta <- all_stations() %>% filter(id == id_val) |
|
|
} |
|
|
|
|
|
if (nrow(s_meta) > 0) { |
|
|
|
|
|
current_temp <- clicked_data$properties$temp |
|
|
current_param_name <- clicked_data$properties$param_name |
|
|
if (is.null(current_temp) || is.null(current_param_name) || is.na(current_param_name)) { |
|
|
obs <- viewport_obs_data() |
|
|
match <- if (!is.null(obs)) obs %>% filter(station_id == id_val) else NULL |
|
|
if (is.null(current_temp) && !is.null(match) && nrow(match) > 0) { |
|
|
current_temp <- match$temp[1] |
|
|
} |
|
|
if ((is.null(current_param_name) || is.na(current_param_name)) && !is.null(match) && nrow(match) > 0 && "param_name" %in% names(match)) { |
|
|
current_param_name <- match$param_name[1] |
|
|
} |
|
|
} |
|
|
if (is.null(current_param_name)) { |
|
|
current_param_name <- NA_character_ |
|
|
} |
|
|
|
|
|
res_info <- if ("resolution_info" %in% names(s_meta)) s_meta$resolution_info[1] else "Data status unavailable" |
|
|
if (is.null(res_info) || is.na(res_info)) res_info <- "Data status unavailable for current filter" |
|
|
|
|
|
current_param <- isolate(input$map_parameter) |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
param_label <- if (current_param == "precipitation_amount") "Precipitation" else "Air Temperature" |
|
|
param_unit <- if (current_param == "precipitation_amount") "mm" else "°C" |
|
|
missing_label <- if (current_param == "precipitation_amount") "No precip available at this hour" else NULL |
|
|
|
|
|
label_name <- if (current_param == "precipitation_amount") { |
|
|
format_precip_label(current_param_name, param_label) |
|
|
} else { |
|
|
param_label |
|
|
} |
|
|
|
|
|
lbl <- generate_station_label( |
|
|
s_meta$name[1], s_meta$id[1], s_meta$country[1], |
|
|
s_meta$start_date[1], s_meta$end_date[1], s_meta$detailed_summary[1], |
|
|
res_info, |
|
|
val = current_temp, |
|
|
label_name = label_name, |
|
|
unit = param_unit, |
|
|
missing_label = missing_label |
|
|
) |
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), s_meta$longitude[1], s_meta$latitude[1], lbl, move_map = TRUE) |
|
|
} |
|
|
|
|
|
|
|
|
fetch_trigger(fetch_trigger() + 1) |
|
|
|
|
|
|
|
|
updateSelectizeInput(session, "station_selector", selected = id_val) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
observeEvent(current_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 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
fetch_trigger <- reactiveVal(0) |
|
|
fetch_last_processed_request <- reactiveVal(NULL) |
|
|
|
|
|
|
|
|
fetch_debounce_ms <- 150 |
|
|
fetch_ui_flush_delay <- 0.05 |
|
|
|
|
|
|
|
|
station_cache_ttl <- 600 |
|
|
station_cache_max <- 20 |
|
|
station_cache <- reactiveVal(list()) |
|
|
|
|
|
cache_key_for <- function(station_id, resolution) { |
|
|
paste0(station_id, "|", resolution) |
|
|
} |
|
|
|
|
|
get_cached_station <- function(key) { |
|
|
cache <- isolate(station_cache()) |
|
|
entry <- cache[[key]] |
|
|
if (is.null(entry) || is.null(entry$ts) || is.null(entry$df)) { |
|
|
return(NULL) |
|
|
} |
|
|
age <- as.numeric(difftime(Sys.time(), entry$ts, units = "secs")) |
|
|
if (!is.finite(age) || age > station_cache_ttl) { |
|
|
cache[[key]] <- NULL |
|
|
station_cache(cache) |
|
|
return(NULL) |
|
|
} |
|
|
entry$df |
|
|
} |
|
|
|
|
|
set_cached_station <- function(key, df) { |
|
|
cache <- isolate(station_cache()) |
|
|
cache[[key]] <- list(ts = Sys.time(), df = df) |
|
|
if (length(cache) > station_cache_max) { |
|
|
times <- vapply(cache, function(x) as.numeric(x$ts), numeric(1)) |
|
|
drop_n <- length(cache) - station_cache_max |
|
|
drop_keys <- names(sort(times))[seq_len(drop_n)] |
|
|
if (length(drop_keys) > 0) { |
|
|
cache[drop_keys] <- NULL |
|
|
} |
|
|
} |
|
|
station_cache(cache) |
|
|
} |
|
|
|
|
|
|
|
|
window_reactive <- reactive({ |
|
|
|
|
|
ft <- fetch_trigger() |
|
|
|
|
|
|
|
|
|
|
|
end_t <- lubridate::floor_date(Sys.time(), "10 minutes") |
|
|
start_t <- end_t - (24 * 3600) |
|
|
|
|
|
list(id = current_station_id(), start = start_t, end = end_t, trigger = ft) |
|
|
}) |
|
|
window_debounced <- window_reactive %>% debounce(fetch_debounce_ms) |
|
|
|
|
|
|
|
|
get_station_name <- function() { |
|
|
sid <- current_station_id() |
|
|
if (is.null(sid)) { |
|
|
return("") |
|
|
} |
|
|
st <- all_stations() %>% filter(id == sid) |
|
|
if (nrow(st) > 0) st$name[1] else sid |
|
|
} |
|
|
|
|
|
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()) |
|
|
|
|
|
unfreeze_ui_if_idle() |
|
|
} |
|
|
|
|
|
|
|
|
observeEvent(input$cancel_loading, { |
|
|
reset_fetch("Cancelled by user") |
|
|
showNotification("Loading cancelled by user.", type = "warning") |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
req(window_debounced()$start, window_debounced()$end) |
|
|
|
|
|
current_request <- window_debounced() |
|
|
|
|
|
|
|
|
if (is.null(current_request$id)) { |
|
|
unfreeze_ui_if_idle() |
|
|
return() |
|
|
} |
|
|
|
|
|
last_request <- fetch_last_processed_request() |
|
|
|
|
|
|
|
|
if (!is.null(last_request) && identical(current_request, last_request)) { |
|
|
return() |
|
|
} |
|
|
|
|
|
|
|
|
fetch_last_processed_request(current_request) |
|
|
|
|
|
|
|
|
station_id <- isolate(window_debounced()$id) |
|
|
|
|
|
st_meta <- isolate(all_stations()) %>% filter(id == station_id) |
|
|
if (nrow(st_meta) == 0) { |
|
|
reset_fetch("Station not found.") |
|
|
return() |
|
|
} |
|
|
|
|
|
res_key <- tolower(isolate(current_resolution())) |
|
|
cache_key <- cache_key_for(station_id, res_key) |
|
|
cached_df <- get_cached_station(cache_key) |
|
|
if (!is.null(cached_df)) { |
|
|
station_data(cached_df) |
|
|
loading_diagnostics(paste0("Loaded ", nrow(cached_df), " records from cache.")) |
|
|
updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
loading_status(FALSE) |
|
|
unfreeze_ui_if_idle() |
|
|
return() |
|
|
} |
|
|
|
|
|
loading_status(TRUE) |
|
|
|
|
|
s_name <- st_meta$name[1] |
|
|
s_country_fetch <- st_meta$country[1] |
|
|
msg <- paste0("Fetching data for ", s_name, " (", s_country_fetch, ")...") |
|
|
loading_diagnostics(msg) |
|
|
fetch_message(msg) |
|
|
session$sendCustomMessage("freezeUI", list(text = msg)) |
|
|
|
|
|
|
|
|
later::later(function() { |
|
|
tryCatch( |
|
|
{ |
|
|
s_date <- current_request$start |
|
|
e_date <- current_request$end |
|
|
|
|
|
|
|
|
df <- read_meteogate_data(station_id, s_date, e_date) |
|
|
|
|
|
if (is.null(df) || nrow(df) == 0) { |
|
|
loading_diagnostics("No data returned from API.") |
|
|
station_data(NULL) |
|
|
} else { |
|
|
set_cached_station(cache_key, df) |
|
|
station_data(df) |
|
|
loading_diagnostics(paste0("Loaded ", nrow(df), " records.")) |
|
|
|
|
|
updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
} |
|
|
}, |
|
|
error = function(e) { |
|
|
loading_diagnostics(paste("Error:", e$message)) |
|
|
station_data(NULL) |
|
|
} |
|
|
) |
|
|
|
|
|
loading_status(FALSE) |
|
|
|
|
|
unfreeze_ui_if_idle() |
|
|
}, fetch_ui_flush_delay) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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$panel_station_name <- renderText({ |
|
|
req(current_station_id()) |
|
|
s <- all_stations() %>% filter(id == current_station_id()) |
|
|
if (nrow(s) > 0) paste0(s$name[1], " (", s$id[1], ")") else current_station_id() |
|
|
}) |
|
|
|
|
|
output$panel_station_meta <- renderText({ |
|
|
req(current_station_id()) |
|
|
s <- all_stations() %>% filter(id == current_station_id()) |
|
|
if (nrow(s) > 0) { |
|
|
paste0( |
|
|
s$state[1], " | ", |
|
|
"Lat: ", s$latitude[1], " | Lon: ", s$longitude[1], " | Elev: ", s$elevation[1], "m" |
|
|
) |
|
|
} else { |
|
|
"" |
|
|
} |
|
|
}) |
|
|
|
|
|
observe({ |
|
|
df <- station_data() |
|
|
req(df) |
|
|
|
|
|
|
|
|
if (!is.null(input$modal_date_start) && !is.null(input$modal_date_end)) { |
|
|
df <- df %>% filter( |
|
|
datetime >= as.POSIXct(input$modal_date_start), |
|
|
datetime <= as.POSIXct(input$modal_date_end) + days(1) |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
output$temp_single_plot <- renderPlotly({ |
|
|
create_temperature_single_plot(df) |
|
|
}) |
|
|
output$humidity_plot <- renderPlotly({ |
|
|
create_humidity_single_plot(df) |
|
|
}) |
|
|
|
|
|
|
|
|
output$solar_radiation_plot <- renderPlotly({ |
|
|
create_solar_radiation_plot(df) |
|
|
}) |
|
|
output$sunshine_duration_plot <- renderPlotly({ |
|
|
create_sunshine_plot(df) |
|
|
}) |
|
|
output$etp_plot <- renderPlotly({ |
|
|
create_etp_plot(df) |
|
|
}) |
|
|
output$precipitation_only_plot <- renderPlotly({ |
|
|
create_precipitation_only_plot(df, resolution = tolower(current_resolution())) |
|
|
}) |
|
|
output$snow_plot <- renderPlotly({ |
|
|
create_snow_plot(df) |
|
|
}) |
|
|
output$ground_temp_plot <- renderPlotly({ |
|
|
create_ground_temp_plot(df) |
|
|
}) |
|
|
output$soil_temp_plot <- renderPlotly({ |
|
|
create_soil_temp_plot(df) |
|
|
}) |
|
|
output$wind_2m_gust_plot <- renderPlotly({ |
|
|
create_wind_2m_gust_plot(df) |
|
|
}) |
|
|
|
|
|
|
|
|
output$pressure_plot <- renderPlotly({ |
|
|
create_pressure_plot(df) |
|
|
}) |
|
|
output$wind_time_series <- renderPlotly({ |
|
|
create_wind_time_series_plot(df) |
|
|
}) |
|
|
output$cloud_cover_plot <- renderPlotly({ |
|
|
create_cloud_cover_plot(df) |
|
|
}) |
|
|
output$visibility_plot <- renderPlotly({ |
|
|
create_visibility_plot(df) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
output$wind_rose <- renderPlotly({ |
|
|
create_wind_rose_plot(df) |
|
|
}) |
|
|
|
|
|
|
|
|
output$weathergami_plot <- renderPlotly({ |
|
|
create_weathergami_plot(df) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
output$hourly_data_table <- DT::renderDataTable({ |
|
|
req(station_data()) |
|
|
res <- tolower(current_resolution()) |
|
|
|
|
|
df_display <- station_data() |
|
|
df_display <- format_wind_speed_decimals(df_display, digits = 1) |
|
|
display_cols <- get_station_display_cols(df_display) |
|
|
units <- attr(df_display, "units") |
|
|
|
|
|
|
|
|
df_display <- df_display %>% mutate(datetime = format(datetime, "%Y-%m-%d %H:%M")) |
|
|
|
|
|
|
|
|
if ("datetime" %in% display_cols) { |
|
|
display_cols <- c("datetime", setdiff(display_cols, "datetime")) |
|
|
} |
|
|
|
|
|
|
|
|
pretty_labels <- sapply(seq_along(display_cols), function(i) { |
|
|
col <- display_cols[i] |
|
|
|
|
|
label <- col %>% |
|
|
gsub("_", " ", .) %>% |
|
|
stringr::str_to_title() |
|
|
|
|
|
|
|
|
if (col == "datetime") { |
|
|
return(if (res == "daily") "Date" else "Date/Time") |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(units) && !is.null(units[[col]])) { |
|
|
label <- paste0(label, " [", units[[col]], "]") |
|
|
} |
|
|
return(label) |
|
|
}, USE.NAMES = FALSE) |
|
|
|
|
|
|
|
|
datatable(df_display %>% select(all_of(display_cols)), |
|
|
colnames = pretty_labels, |
|
|
options = list(pageLength = 15, scrollX = TRUE) |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
output$param_definitions_ui <- renderUI({ |
|
|
req(current_resolution(), station_data(), current_station_id()) |
|
|
res <- tolower(current_resolution()) |
|
|
df <- station_data() |
|
|
sid <- current_station_id() |
|
|
|
|
|
|
|
|
|
|
|
st_meta <- all_stations() %>% filter(id == sid) |
|
|
|
|
|
expected_str <- NA_character_ |
|
|
if (res == "hourly" && "params_hourly" %in% names(st_meta)) { |
|
|
expected_str <- st_meta$params_hourly[1] |
|
|
} else if (res == "daily" && "params_daily" %in% names(st_meta)) { |
|
|
expected_str <- st_meta$params_daily[1] |
|
|
} else if (res == "10-min" && "params_10min" %in% names(st_meta)) { |
|
|
expected_str <- st_meta$params_10min[1] |
|
|
} else if (res == "monthly" && "params_monthly" %in% names(st_meta)) expected_str <- st_meta$params_monthly[1] |
|
|
|
|
|
|
|
|
display_cols <- get_station_display_cols(df) |
|
|
|
|
|
|
|
|
has_data <- sapply(display_cols, function(col) { |
|
|
any(!is.na(df[[col]])) |
|
|
}) |
|
|
|
|
|
available_cols <- display_cols[has_data] |
|
|
|
|
|
available_labels <- available_cols %>% |
|
|
gsub("_", " ", .) %>% |
|
|
stringr::str_to_title() |
|
|
|
|
|
|
|
|
if (length(available_labels) > 0 && available_labels[1] == "Datetime") { |
|
|
available_labels[1] <- if (res == "daily") "Date" else "Date/Time" |
|
|
} |
|
|
|
|
|
ui_elements <- list() |
|
|
|
|
|
if (length(available_labels) > 0) { |
|
|
ui_elements[[length(ui_elements) + 1]] <- div( |
|
|
style = "background-color: #d4edda; border: 1px solid #28a745; border-radius: 5px; padding: 10px; margin-bottom: 5px; font-size: 0.85rem;", |
|
|
tags$strong(bsicons::bs_icon("check-circle"), " Available: "), |
|
|
paste(available_labels, collapse = " | ") |
|
|
) |
|
|
} |
|
|
|
|
|
if (!is.null(expected_str) && !is.na(expected_str) && nzchar(expected_str)) { |
|
|
ui_elements[[length(ui_elements) + 1]] <- div( |
|
|
style = "background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; padding: 10px; margin-bottom: 5px; font-size: 0.85rem;", |
|
|
tags$strong(bsicons::bs_icon("info-circle"), " Metadata Expected Parameters: "), |
|
|
expected_str |
|
|
) |
|
|
} |
|
|
|
|
|
if (length(ui_elements) == 0) { |
|
|
return(div( |
|
|
style = "background-color: #f8d7da; border: 1px solid #dc3545; border-radius: 5px; padding: 10px; margin-bottom: 15px; font-size: 0.85rem;", |
|
|
bsicons::bs_icon("x-circle"), " No data found for this station and resolution." |
|
|
)) |
|
|
} |
|
|
|
|
|
tagList(ui_elements) |
|
|
}) |
|
|
|
|
|
|
|
|
output$download_hourly <- downloadHandler( |
|
|
filename = function() { |
|
|
id <- current_station_id() |
|
|
res <- if (!is.null(current_resolution())) tolower(current_resolution()) else "data" |
|
|
if (is.null(id)) { |
|
|
paste0("meteo_data_", res, ".xlsx") |
|
|
} else { |
|
|
paste0("meteo_station_", id, "_", res, ".xlsx") |
|
|
} |
|
|
}, |
|
|
content = function(file) { |
|
|
req(station_data()) |
|
|
|
|
|
out_df <- station_data() %>% format_wind_speed_decimals(digits = 1) |
|
|
write_xlsx(out_df, path = file) |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
output$station_info_header <- renderUI({ |
|
|
id <- current_station_id() |
|
|
if (is.null(id)) { |
|
|
return(NULL) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
stations_base <- get_meteogate_stations() |
|
|
meta <- stations_base %>% dplyr::filter(id == !!id) |
|
|
if (nrow(meta) == 0) { |
|
|
|
|
|
meta <- all_stations() %>% dplyr::filter(id == !!id) |
|
|
if (nrow(meta) == 0) { |
|
|
return(NULL) |
|
|
} |
|
|
} |
|
|
|
|
|
s_name <- meta$name[1] |
|
|
s_state <- meta$country[1] |
|
|
s_elev <- meta$elevation[1] |
|
|
|
|
|
|
|
|
res_label <- current_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(paste0(s_name, " (", s_state, ")"), 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({ |
|
|
|
|
|
obs_data <- viewport_obs_data() |
|
|
current_param <- input$map_parameter |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
|
|
|
|
|
|
param_label <- switch(current_param, |
|
|
"air_temperature" = "Temperature (°C)", |
|
|
"precipitation_amount" = "Precipitation (mm)", |
|
|
"air_pressure_at_mean_sea_level" = "Pressure (hPa)", |
|
|
"wind_speed" = "Wind Speed (m/s)", |
|
|
"Value" |
|
|
) |
|
|
|
|
|
|
|
|
base_stations <- filtered_stations() %>% |
|
|
mutate( |
|
|
display_params = sapply(available_params, function(p) get_resolution_params(tolower(current_resolution()), p)) |
|
|
) |
|
|
|
|
|
|
|
|
if (!is.null(obs_data) && nrow(obs_data) > 0) { |
|
|
obs_filtered <- obs_data %>% |
|
|
filter(!is.na(station_id), !is.na(temp)) %>% |
|
|
group_by(station_id) %>% |
|
|
arrange(desc(datetime)) %>% |
|
|
slice(1) %>% |
|
|
ungroup() %>% |
|
|
select(station_id, temp) |
|
|
} else { |
|
|
obs_filtered <- data.frame( |
|
|
station_id = character(0), |
|
|
temp = numeric(0), |
|
|
stringsAsFactors = FALSE |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
df_stations <- base_stations %>% |
|
|
inner_join(obs_filtered, by = c("id" = "station_id")) %>% |
|
|
rename(!!param_label := temp) |
|
|
|
|
|
|
|
|
|
|
|
df_stations <- df_stations %>% |
|
|
select( |
|
|
"Station ID" = id, |
|
|
"Name" = name, |
|
|
"Country" = country, |
|
|
"Region" = region, |
|
|
"Elevation" = elevation, |
|
|
!!param_label, |
|
|
"Parameters" = display_params |
|
|
) |
|
|
|
|
|
datatable(df_stations, |
|
|
selection = "none", |
|
|
rownames = FALSE, |
|
|
options = list(pageLength = 20), |
|
|
callback = JS(" |
|
|
table.on('dblclick', 'tr', function() { |
|
|
var rowData = table.row(this).data(); |
|
|
if (rowData !== undefined && rowData !== null) { |
|
|
// First column is Station ID (id_clim) |
|
|
var stationId = rowData[0]; |
|
|
Shiny.setInputValue('table_station_dblclick', stationId, {priority: 'event'}); |
|
|
} |
|
|
}); |
|
|
") |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$table_station_dblclick, { |
|
|
raw_id <- as.character(input$table_station_dblclick) |
|
|
req(raw_id) |
|
|
station_id <- trimws(raw_id) |
|
|
|
|
|
|
|
|
updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
|
|
|
|
|
|
current_station_id(station_id) |
|
|
|
|
|
fetch_trigger(fetch_trigger() + 1) |
|
|
|
|
|
|
|
|
updateSelectizeInput(session, "station_selector", selected = station_id) |
|
|
|
|
|
|
|
|
s <- NULL |
|
|
df <- filtered_stations() |
|
|
if (!is.null(df)) { |
|
|
s <- df %>% filter(as.character(id) == station_id) |
|
|
} |
|
|
|
|
|
if (is.null(s) || nrow(s) == 0) { |
|
|
|
|
|
s <- all_stations() %>% filter(as.character(id) == station_id) |
|
|
} |
|
|
|
|
|
|
|
|
if (!is.null(s) && nrow(s) > 0) { |
|
|
lat_val <- s$latitude[1] |
|
|
lng_val <- s$longitude[1] |
|
|
|
|
|
|
|
|
res_info <- if ("resolution_info" %in% names(s)) s$resolution_info[1] else "Data status unavailable" |
|
|
if (is.null(res_info) || is.na(res_info)) res_info <- "Data status unavailable for current filter" |
|
|
|
|
|
|
|
|
obs <- viewport_obs_data() |
|
|
match <- if (!is.null(obs)) obs %>% filter(station_id == station_id) else NULL |
|
|
current_temp <- if (!is.null(match) && nrow(match) > 0) match$temp[1] else NULL |
|
|
current_param_name <- if (!is.null(match) && nrow(match) > 0 && "param_name" %in% names(match)) { |
|
|
match$param_name[1] |
|
|
} else { |
|
|
NA_character_ |
|
|
} |
|
|
|
|
|
current_param <- isolate(input$map_parameter) |
|
|
if (is.null(current_param)) current_param <- "air_temperature" |
|
|
param_label <- if (current_param == "precipitation_amount") "Precipitation" else "Air Temperature" |
|
|
param_unit <- if (current_param == "precipitation_amount") "mm" else "°C" |
|
|
missing_label <- if (current_param == "precipitation_amount") "No precip available at this hour" else NULL |
|
|
|
|
|
label_name <- if (current_param == "precipitation_amount") { |
|
|
format_precip_label(current_param_name, param_label) |
|
|
} else { |
|
|
param_label |
|
|
} |
|
|
|
|
|
lbl <- generate_station_label( |
|
|
s$name[1], s$id[1], s$country[1], |
|
|
s$start_date[1], s$end_date[1], s$detailed_summary[1], |
|
|
res_info, |
|
|
val = current_temp, |
|
|
label_name = label_name, |
|
|
unit = param_unit, |
|
|
missing_label = missing_label |
|
|
) |
|
|
|
|
|
highlight_selected_station(maplibre_proxy("map"), lng_val, lat_val, lbl) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
output$details_tabs <- renderUI({ |
|
|
req(current_resolution()) |
|
|
res <- tolower(current_resolution()) |
|
|
df <- station_data() |
|
|
req(df) |
|
|
|
|
|
|
|
|
df <- clean_weather_data(df) |
|
|
|
|
|
|
|
|
has_temp <- ("temp" %in% names(df) && any(!is.na(df$temp))) || |
|
|
("temp_min" %in% names(df) && any(!is.na(df$temp_min))) || |
|
|
("temp_max" %in% names(df) && any(!is.na(df$temp_max))) |
|
|
|
|
|
has_rh <- ("rh" %in% names(df) && any(!is.na(df$rh))) |
|
|
|
|
|
|
|
|
has_humidity_data <- has_rh || (has_temp && has_rh) |
|
|
|
|
|
has_wind <- ("wind_speed" %in% names(df) && any(!is.na(df$wind_speed))) || |
|
|
("wind_gust" %in% names(df) && any(!is.na(df$wind_gust))) |
|
|
|
|
|
has_pressure <- ("pressure" %in% names(df) && any(!is.na(df$pressure))) |
|
|
|
|
|
has_cloud <- ("cloud_cover" %in% names(df) && any(!is.na(df$cloud_cover))) |
|
|
|
|
|
has_precip <- ("precip" %in% names(df) && any(!is.na(df$precip))) |
|
|
|
|
|
has_snow <- ("snow_fresh" %in% names(df) && any(df$snow_fresh > 0, na.rm = TRUE)) || |
|
|
("snow_depth" %in% names(df) && any(df$snow_depth > 0, na.rm = TRUE)) |
|
|
|
|
|
has_ground_temp <- ("temp_min_ground" %in% names(df) && any(!is.na(df$temp_min_ground))) || |
|
|
("temp_min_50cm" %in% names(df) && any(!is.na(df$temp_min_50cm))) |
|
|
|
|
|
has_soil_temp <- ("soil_temp_10cm" %in% names(df) && any(!is.na(df$soil_temp_10cm))) || |
|
|
("soil_temp_20cm" %in% names(df) && any(!is.na(df$soil_temp_20cm))) || |
|
|
("soil_temp_50cm" %in% names(df) && any(!is.na(df$soil_temp_50cm))) |
|
|
|
|
|
has_wind_2m_gust <- ("wind_speed_2m" %in% names(df) && any(!is.na(df$wind_speed_2m))) || |
|
|
("wind_gust_inst" %in% names(df) && any(!is.na(df$wind_gust_inst))) |
|
|
|
|
|
has_solar <- ("solar_global" %in% names(df) && any(!is.na(df$solar_global))) |
|
|
|
|
|
has_sunshine <- ("sunshine_duration" %in% names(df) && any(!is.na(df$sunshine_duration))) |
|
|
|
|
|
has_etp <- ("etp" %in% names(df) && any(!is.na(df$etp))) |
|
|
|
|
|
has_wind_dir <- ("wind_dir" %in% names(df) && any(!is.na(df$wind_dir))) && has_wind |
|
|
|
|
|
has_pressure_data <- ("pressure" %in% names(df) && any(!is.na(df$pressure))) || |
|
|
("pressure_sea" %in% names(df) && any(!is.na(df$pressure_sea))) || |
|
|
("pressure_station" %in% names(df) && any(!is.na(df$pressure_station))) |
|
|
|
|
|
has_vis <- ("visibility" %in% names(df) && any(!is.na(df$visibility))) |
|
|
|
|
|
has_daily_temp <- ("temp_min" %in% names(df) && any(!is.na(df$temp_min))) && |
|
|
("temp_max" %in% names(df) && any(!is.na(df$temp_max))) |
|
|
|
|
|
|
|
|
plot_list <- tagList() |
|
|
|
|
|
|
|
|
if (has_temp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("temp_single_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_humidity_data) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("humidity_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_precip) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("precipitation_only_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_ground_temp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("ground_temp_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"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_pressure_data) { |
|
|
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_cover_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_wind) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("wind_time_series", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_wind_2m_gust) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("wind_2m_gust_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_vis) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("visibility_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_duration_plot", height = "320px"))) |
|
|
} |
|
|
|
|
|
|
|
|
if (has_etp) { |
|
|
plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("etp_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"))) |
|
|
} |
|
|
|
|
|
|
|
|
div( |
|
|
class = "row g-3", |
|
|
style = "padding: 10px;", |
|
|
plot_list |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
output$map_legend <- renderUI({ |
|
|
obs <- viewport_obs_data() |
|
|
param_type <- input$map_parameter |
|
|
|
|
|
if (is.null(param_type) || param_type == "air_temperature") { |
|
|
|
|
|
|
|
|
ecmwf_breaks <- seq(-50, 58, by = 2) |
|
|
|
|
|
|
|
|
t_max <- 40 |
|
|
t_min <- -20 |
|
|
|
|
|
if (!is.null(obs) && nrow(obs) > 0) { |
|
|
current_range <- range(obs$temp, na.rm = TRUE) |
|
|
|
|
|
t_max <- ceiling(current_range[2] / 2) * 2 |
|
|
t_min <- floor(current_range[1] / 2) * 2 |
|
|
t_max <- min(58, t_max) |
|
|
t_min <- max(-50, t_min) |
|
|
} |
|
|
|
|
|
|
|
|
temps <- ecmwf_breaks[ecmwf_breaks >= t_min & ecmwf_breaks <= t_max] |
|
|
temps <- rev(temps) |
|
|
|
|
|
tagList( |
|
|
div(class = "legend-title", "[°C]"), |
|
|
lapply(temps, function(t) { |
|
|
color <- temp_to_color(t) |
|
|
|
|
|
show_label <- t %% 8 == 0 | t == 0 |
|
|
div( |
|
|
class = "legend-item", |
|
|
div(class = "legend-color", style = paste0("background-color: ", color, "; opacity: 0.8;")), |
|
|
if (show_label) div(class = "legend-label", t) |
|
|
) |
|
|
}) |
|
|
) |
|
|
} else if (param_type == "air_pressure_at_mean_sea_level") { |
|
|
|
|
|
p_stops <- seq(960, 1040, by = 5) |
|
|
|
|
|
|
|
|
display_min <- 960 |
|
|
display_max <- 1040 |
|
|
|
|
|
if (!is.null(obs) && nrow(obs) > 0) { |
|
|
|
|
|
rng <- range(obs$temp, na.rm = TRUE) |
|
|
|
|
|
d_min_snap <- floor(rng[1] / 5) * 5 |
|
|
d_max_snap <- ceiling(rng[2] / 5) * 5 |
|
|
|
|
|
|
|
|
display_min <- max(960, d_min_snap) |
|
|
display_max <- min(1040, d_max_snap) |
|
|
|
|
|
|
|
|
if (display_max < display_min) display_max <- display_min |
|
|
} |
|
|
|
|
|
|
|
|
display_stops <- p_stops[p_stops >= display_min & p_stops <= display_max] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
display_stops <- rev(display_stops) |
|
|
|
|
|
tagList( |
|
|
div(class = "legend-title", "[hPa]"), |
|
|
lapply(display_stops, function(p) { |
|
|
|
|
|
|
|
|
color <- pressure_to_color(p) |
|
|
|
|
|
|
|
|
div( |
|
|
class = "legend-item", |
|
|
div(class = "legend-color", style = paste0("background-color: ", color, "; opacity: 0.9;")), |
|
|
div(class = "legend-label", p) |
|
|
) |
|
|
}) |
|
|
) |
|
|
} else if (param_type == "wind_speed") { |
|
|
|
|
|
|
|
|
w_stops <- c(0.5, 2, 4, 6, 10, 15, 20, 25, 30, 40, 50) |
|
|
|
|
|
|
|
|
display_max <- 30 |
|
|
if (!is.null(obs) && nrow(obs) > 0) { |
|
|
rng <- range(obs$temp, na.rm = TRUE) |
|
|
w_max_data <- rng[2] |
|
|
|
|
|
display_max <- w_stops[findInterval(w_max_data, w_stops) + 1] |
|
|
if (is.na(display_max)) display_max <- 50 |
|
|
display_max <- max(15, display_max) |
|
|
} |
|
|
|
|
|
|
|
|
display_stops <- w_stops[w_stops <= display_max] |
|
|
|
|
|
|
|
|
display_stops <- rev(display_stops) |
|
|
|
|
|
tagList( |
|
|
div(class = "legend-title", "[m/s]"), |
|
|
lapply(display_stops, function(w) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
color <- wind_speed_to_color(w) |
|
|
|
|
|
div( |
|
|
class = "legend-item", |
|
|
div(class = "legend-color", style = paste0("background-color: ", color, "; opacity: 0.9;")), |
|
|
div(class = "legend-label", w) |
|
|
) |
|
|
}) |
|
|
) |
|
|
} else { |
|
|
|
|
|
p_max <- 50 |
|
|
p_min <- 0 |
|
|
|
|
|
if (!is.null(obs) && nrow(obs) > 0) { |
|
|
current_range <- range(obs$temp, na.rm = TRUE) |
|
|
p_max <- ceiling(current_range[2] / 5) * 5 |
|
|
p_min <- 0 |
|
|
p_max <- min(50, max(5, p_max)) |
|
|
} |
|
|
|
|
|
precip_stops <- c(0, 0.1, 1, 5, 10, 25, 50) |
|
|
precip_stops <- precip_stops[precip_stops <= p_max] |
|
|
precip_stops <- rev(precip_stops) |
|
|
|
|
|
tagList( |
|
|
div(class = "legend-title", "[mm]"), |
|
|
lapply(precip_stops, function(p) { |
|
|
color <- precip_to_color(p) |
|
|
div( |
|
|
class = "legend-item", |
|
|
div(class = "legend-color", style = paste0("background-color: ", color, "; opacity: 0.8;")), |
|
|
div(class = "legend-label", p) |
|
|
) |
|
|
}) |
|
|
) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
output$download_data <- downloadHandler( |
|
|
filename = function() { |
|
|
paste0("meteo_station_", current_station_id(), ".csv") |
|
|
}, |
|
|
content = function(file) { |
|
|
out_df <- station_data() %>% format_wind_speed_decimals(digits = 1) |
|
|
write_csv(out_df, file) |
|
|
} |
|
|
) |
|
|
} |
|
|
|