Spaces:
Running
Running
| server <- function(input, output, session) { | |
| pending_country <- reactiveVal(NULL) | |
| pending_station <- reactiveVal(NULL) | |
| # URL parameter extraction | |
| observe({ | |
| req(stations_data()) | |
| query <- parseQueryString(session$clientData$url_search) | |
| resolved_country <- NULL | |
| if (!is.null(query$country)) { | |
| countries <- unique(stations_data()$Country) | |
| match_idx <- which(tolower(countries) == tolower(query$country)) | |
| if (length(match_idx) > 0) { | |
| resolved_country <- countries[match_idx[1]] | |
| } else { | |
| resolved_country <- query$country | |
| } | |
| pending_country(resolved_country) | |
| updateSelectizeInput(session, "country_obs", selected = resolved_country) | |
| } | |
| if (!is.null(query$station)) { | |
| resolved_station <- NULL | |
| stations_pool <- stations_data() | |
| if (!is.null(resolved_country)) { | |
| stations_pool <- stations_pool[stations_pool$Country == resolved_country, ] | |
| } | |
| match_idx <- which(tolower(stations_pool$StationName) == tolower(query$station)) | |
| if (length(match_idx) > 0) { | |
| resolved_station <- stations_pool$StationName[match_idx[1]] | |
| } else { | |
| resolved_station <- query$station | |
| } | |
| pending_station(resolved_station) | |
| updateSelectizeInput(session, "station_obs", selected = resolved_station) | |
| } | |
| }) | |
| # Broadcast state updates | |
| observe({ | |
| country_val <- input$country_obs | |
| station_val <- input$station_obs | |
| if (!is.null(pending_country())) { | |
| if (isTRUE(country_val == pending_country())) { | |
| pending_country(NULL) | |
| } else { | |
| country_val <- pending_country() | |
| } | |
| } | |
| if (!is.null(pending_station())) { | |
| if (isTRUE(station_val == pending_station())) { | |
| pending_station(NULL) | |
| } else { | |
| station_val <- pending_station() | |
| } | |
| } | |
| session$sendCustomMessage("updateParentURL", list( | |
| country = country_val, | |
| station = station_val | |
| )) | |
| }) | |
| # Output for ui.R conditionalPanel | |
| output$station_ready <- reactive({ !is.null(selected_station_obs_id()) }) | |
| outputOptions(output, "station_ready", suspendWhenHidden = FALSE) | |
| # Climate Observational Data Server Logic | |
| # Create a reactive to fetch stations once (shared between map and table) | |
| stations_data <- reactive({ | |
| get_dwd_stations() | |
| }) | |
| # Update Country choices on load | |
| observe({ | |
| req(stations_data()) | |
| countries <- unique(sort(stations_data()$Country)) | |
| # Keep current selection if it exists, but prioritize pending_country() if it was set on startup | |
| curr <- isolate(input$country_obs) | |
| if (!is.null(pending_country())) { | |
| curr <- pending_country() | |
| } | |
| if (is.null(curr)) curr <- "All" | |
| updateSelectInput(session, "country_obs", choices = c("All Countries" = "All", countries), selected = curr) | |
| }) | |
| # Reactive to filter stations by Country | |
| filtered_stations_by_country <- reactive({ | |
| req(stations_data()) | |
| stations_sf <- stations_data() | |
| if (is.null(input$country_obs) || input$country_obs == "All") { | |
| stations_sf | |
| } else { | |
| stations_sf[stations_sf$Country == input$country_obs, ] | |
| } | |
| }) | |
| # Update Station choices based on selected Country | |
| observeEvent(input$country_obs, { | |
| if (!is.null(pending_country())) { | |
| if (input$country_obs != pending_country()) { | |
| return() | |
| } else { | |
| pending_country(NULL) # Match found, proceed and clear | |
| } | |
| } | |
| filtered_data <- filtered_stations_by_country() | |
| if (!is.null(filtered_data) && nrow(filtered_data) > 0) { | |
| station_choices <- c("All Stations" = "All", unique(sort(filtered_data$StationName))) | |
| sel <- "All" | |
| if (!is.null(pending_station())) { | |
| if (pending_station() %in% station_choices) { | |
| sel <- pending_station() | |
| } | |
| } | |
| updateSelectizeInput(session, "station_obs", choices = station_choices, selected = sel, server = FALSE) | |
| } | |
| }, ignoreInit = FALSE) | |
| # MapLibre state for climate observational map | |
| obs_style_change_trigger <- reactiveVal(0) | |
| obs_map_initialized <- reactiveVal(FALSE) | |
| obs_current_raster_layers <- reactiveVal(character(0)) | |
| selected_station_obs_id <- reactiveVal(NULL) | |
| obs_label_layer_ids <- maplibre_label_layer_ids | |
| obs_non_label_layer_ids <- maplibre_non_label_layer_ids | |
| # Invalidate map size when switching tabs (fixes rendering issues) | |
| observeEvent(input$main_nav, { | |
| if (input$main_nav == "Map View") { | |
| shinyjs::runjs(maplibre_resize_script("map_obs")) | |
| } | |
| }) | |
| # Render the initial map | |
| output$map_obs <- mapgl::renderMaplibre({ | |
| initial_center <- c(19, 46) # default Europe/global center | |
| initial_zoom <- 5 | |
| # Check if we are starting with a deep-linked country / station selection | |
| country_val <- isolate(input$country_obs) | |
| station_val <- isolate(input$station_obs) | |
| stations_in_box <- isolate(filtered_stations_by_country()) | |
| if (!is.null(stations_in_box) && nrow(stations_in_box) > 0 && !is.null(country_val) && country_val != "All") { | |
| min_lon <- min(stations_in_box$Longitude, na.rm = TRUE) | |
| min_lat <- min(stations_in_box$Latitude, na.rm = TRUE) | |
| max_lon <- max(stations_in_box$Longitude, na.rm = TRUE) | |
| max_lat <- max(stations_in_box$Latitude, na.rm = TRUE) | |
| initial_center <- c((min_lon + max_lon)/2, (min_lat + max_lat)/2) | |
| initial_zoom <- 5.5 | |
| if (!is.null(station_val) && station_val != "All") { | |
| stn_sel <- stations_in_box[stations_in_box$StationName == station_val, ] | |
| if (nrow(stn_sel) > 0) { | |
| initial_center <- c(stn_sel$Longitude[1], stn_sel$Latitude[1]) | |
| initial_zoom <- 7.5 | |
| } | |
| } | |
| } | |
| maplibre_create_base_map(center = initial_center, zoom = initial_zoom) | |
| }) | |
| observe({ | |
| req(!obs_map_initialized()) | |
| req(input$map_obs_zoom) | |
| # Fit map to world only if no country is selected | |
| if (is.null(input$country_obs) || input$country_obs == "All") { | |
| mapgl::maplibre_proxy("map_obs") %>% | |
| mapgl::fit_bounds( | |
| bbox = c(-180, -60, 180, 85), | |
| animate = FALSE | |
| ) | |
| } | |
| obs_map_initialized(TRUE) | |
| }) | |
| observeEvent(input$zoom_home_obs, { | |
| req(obs_map_initialized()) | |
| stations_in_box <- filtered_stations_by_country() | |
| proxy <- mapgl::maplibre_proxy("map_obs") | |
| if (is.null(input$country_obs) || input$country_obs == "All") { | |
| proxy %>% mapgl::fit_bounds(bbox = c(-180, -60, 180, 85), animate = TRUE) | |
| } else { | |
| if (nrow(stations_in_box) > 0) { | |
| min_lon <- min(stations_in_box$Longitude, na.rm = TRUE) | |
| min_lat <- min(stations_in_box$Latitude, na.rm = TRUE) | |
| max_lon <- max(stations_in_box$Longitude, na.rm = TRUE) | |
| max_lat <- max(stations_in_box$Latitude, na.rm = TRUE) | |
| if (min_lon != max_lon && min_lat != max_lat) { | |
| proxy %>% mapgl::fit_bounds(bbox = c(min_lon, min_lat, max_lon, max_lat), animate = TRUE) | |
| } else { | |
| proxy %>% mapgl::fly_to(center = c(min_lon, min_lat), zoom = 6) | |
| } | |
| } | |
| } | |
| }) | |
| observeEvent(input$basemap_obs, { | |
| req(obs_map_initialized()) | |
| maplibre_switch_basemap( | |
| map_id = "map_obs", | |
| basemap = input$basemap_obs, | |
| current_basemap = function() input$basemap_obs, | |
| show_labels = function() input$show_labels_obs, | |
| current_raster_layers = obs_current_raster_layers, | |
| style_change_trigger = obs_style_change_trigger, | |
| sentinel_ids = function() { | |
| unique_suffix <- as.integer(as.numeric(Sys.time()) * 1000) | |
| list( | |
| source_id = paste0("obs_sentinel_source_", unique_suffix), | |
| layer_id = paste0("obs_sentinel_layer_", unique_suffix) | |
| ) | |
| }, | |
| label_layer_ids = obs_label_layer_ids, | |
| non_label_layer_ids = obs_non_label_layer_ids | |
| ) | |
| }) | |
| observeEvent(input$show_labels_obs, | |
| { | |
| req(obs_map_initialized()) | |
| maplibre_apply_label_visibility(mapgl::maplibre_proxy("map_obs"), input$show_labels_obs, obs_label_layer_ids) | |
| }, | |
| ignoreInit = TRUE | |
| ) | |
| observeEvent(input$country_obs, { | |
| updateTabsetPanel(session = session, inputId = "main_nav", selected = "Map View") | |
| stations_in_box <- filtered_stations_by_country() | |
| if (!is.null(stations_in_box) && nrow(stations_in_box) > 0 && input$country_obs != "All") { | |
| proxy <- mapgl::maplibre_proxy("map_obs") | |
| min_lon <- min(stations_in_box$Longitude, na.rm = TRUE) | |
| min_lat <- min(stations_in_box$Latitude, na.rm = TRUE) | |
| max_lon <- max(stations_in_box$Longitude, na.rm = TRUE) | |
| max_lat <- max(stations_in_box$Latitude, na.rm = TRUE) | |
| if (min_lon != max_lon && min_lat != max_lat) { | |
| proxy %>% mapgl::fit_bounds(bbox = c(min_lon, min_lat, max_lon, max_lat), animate = TRUE) | |
| } else { | |
| proxy %>% mapgl::fly_to(center = c(min_lon, min_lat), zoom = 6) | |
| } | |
| } else if (input$country_obs == "All") { | |
| mapgl::maplibre_proxy("map_obs") %>% mapgl::fit_bounds(bbox = c(-180, -60, 180, 85), animate = TRUE) | |
| } | |
| }, ignoreInit = TRUE) | |
| observeEvent(input$station_obs, { | |
| if (!is.null(input$station_obs) && input$station_obs != "All") { | |
| all_stations <- filtered_stations_by_country() | |
| selected_stn <- all_stations[all_stations$StationName == input$station_obs, ] | |
| if (nrow(selected_stn) > 0) { | |
| lon <- selected_stn$Longitude[1] | |
| lat <- selected_stn$Latitude[1] | |
| mapgl::maplibre_proxy("map_obs") %>% mapgl::fly_to(center = c(lon, lat), zoom = 8) | |
| # Mimic a map click to trigger the download | |
| station_id <- as.character(selected_stn$StationID[1]) | |
| session <- getDefaultReactiveDomain() | |
| station_name <- selected_stn$StationName[1] | |
| country <- selected_stn$Country[1] | |
| display_info <- paste0(station_name, " (", country, ")") | |
| selected_station_obs_id(station_id) | |
| observational_maplibre_highlight_station(mapgl::maplibre_proxy("map_obs"), selected_stn[1, ]) | |
| # Initialize fetch state | |
| fetch_station_id(station_id) | |
| fetch_station_name(display_info) | |
| fetch_parsed_data(list()) | |
| # Build parameter queue | |
| params <- list( | |
| "air_temperature_mean" = "MeanTemp", | |
| "air_temperature_absolute_max" = "MaxTempAbs", | |
| "air_temperature_absolute_min" = "MinTempAbs", | |
| "air_temperature_mean_of_daily_max" = "MeanMaxTemp", | |
| "air_temperature_mean_of_daily_min" = "MeanMinTemp", | |
| "precipitation_total" = "Precipitation", | |
| "precipGE1mm_days" = "PrecipDays", | |
| "sunshine_duration" = "SunshineDuration", | |
| "mean_sea_level_pressure" = "MeanSeaLevelPressure", | |
| "vapour_pressure" = "VapourPressure" | |
| ) | |
| fetch_queue(params) | |
| fetch_queue_idx(1) | |
| # Create new token | |
| fetch_token(as.numeric(Sys.time())) | |
| # Freeze UI | |
| session$sendCustomMessage("freezeUI", list( | |
| text = "Initializing download...", | |
| station = display_info | |
| )) | |
| # Start state machine | |
| fetch_stage(2) # Go to NextParam | |
| } | |
| } else { | |
| selected_station_obs_id(NULL) | |
| proxy <- mapgl::maplibre_proxy("map_obs") | |
| proxy %>% mapgl::clear_layer("selected-highlight") | |
| stations_in_box <- filtered_stations_by_country() | |
| if (!is.null(stations_in_box) && nrow(stations_in_box) > 0 && !is.null(input$country_obs) && input$country_obs != "All") { | |
| min_lon <- min(stations_in_box$Longitude, na.rm = TRUE) | |
| min_lat <- min(stations_in_box$Latitude, na.rm = TRUE) | |
| max_lon <- max(stations_in_box$Longitude, na.rm = TRUE) | |
| max_lat <- max(stations_in_box$Latitude, na.rm = TRUE) | |
| if (min_lon != max_lon && min_lat != max_lat) { | |
| proxy %>% mapgl::fit_bounds(bbox = c(min_lon, min_lat, max_lon, max_lat), animate = TRUE) | |
| } else { | |
| proxy %>% mapgl::fly_to(center = c(min_lon, min_lat), zoom = 6) | |
| } | |
| } else { | |
| proxy %>% mapgl::fit_bounds(bbox = c(-180, -60, 180, 85), animate = TRUE) | |
| } | |
| } | |
| }, ignoreInit = TRUE) | |
| observe({ | |
| req(obs_map_initialized()) | |
| obs_style_change_trigger() | |
| # No area layers to draw for global map | |
| proxy <- mapgl::maplibre_proxy("map_obs") | |
| # clear old area layers if they existed | |
| mapgl::clear_layer(proxy, "danube-line") | |
| mapgl::clear_layer(proxy, "selected-area") | |
| }) | |
| observe({ | |
| req(obs_map_initialized()) | |
| obs_style_change_trigger() | |
| stations_in_box <- filtered_stations_by_country() | |
| proxy <- observational_maplibre_draw_station_layers( | |
| proxy = mapgl::maplibre_proxy("map_obs"), | |
| stations_in_box = stations_in_box | |
| ) | |
| selected_id <- isolate(selected_station_obs_id()) | |
| if (!is.null(selected_id) && !is.null(stations_in_box)) { | |
| selected_station <- stations_in_box[as.character(stations_in_box$StationID) == as.character(selected_id), ] | |
| if (nrow(selected_station) > 0) { | |
| observational_maplibre_highlight_station(proxy, selected_station, move_map = FALSE) | |
| } else { | |
| proxy %>% mapgl::clear_layer("selected-highlight") | |
| } | |
| } else { | |
| proxy %>% mapgl::clear_layer("selected-highlight") | |
| } | |
| }) | |
| # Reactive value to store downloaded weather data | |
| weather_data_obs <- reactiveVal(NULL) | |
| # ============================================================================ | |
| # ASYNC FETCH STATE MACHINE (DWD-Style) | |
| # ============================================================================ | |
| # State Variables | |
| fetch_stage <- reactiveVal(0) # 0=Idle, 1=Init, 2=NextParam, 3=Fetch, 4=Merge | |
| fetch_token <- reactiveVal(NULL) # Cancellation token | |
| fetch_queue <- reactiveVal(list()) # List of parameters to fetch | |
| fetch_queue_idx <- reactiveVal(0) | |
| fetch_parsed_data <- reactiveVal(list()) # Accumulated parsed data | |
| fetch_station_id <- reactiveVal(NULL) | |
| fetch_station_name <- reactiveVal(NULL) | |
| # Helper: Reset fetch state | |
| reset_fetch <- function(session, msg = NULL) { | |
| fetch_stage(0) | |
| fetch_token(as.numeric(Sys.time())) # Invalidate current token | |
| fetch_queue(list()) | |
| fetch_queue_idx(0) | |
| fetch_parsed_data(list()) | |
| session$sendCustomMessage("unfreezeUI", list()) | |
| if (!is.null(msg)) { | |
| showNotification(msg, type = "warning", duration = 3) | |
| } | |
| } | |
| # Handle Cancel from Freeze Window | |
| observeEvent(input$cancel_loading, { | |
| session <- getDefaultReactiveDomain() | |
| reset_fetch(session, "Loading cancelled by user.") | |
| }) | |
| # Observe marker clicks to update dropdown selection (triggering highlight, fly-to, and fetch) | |
| observeEvent(input$map_obs_feature_click, { | |
| click <- input$map_obs_feature_click | |
| if (!is.null(click$layer) && identical(click$layer, "stations")) { | |
| props <- click$properties | |
| station_id <- props$StationID | |
| if (is.null(station_id) && !is.null(click$id)) { | |
| station_id <- click$id | |
| } | |
| if (is.null(station_id)) { | |
| return() | |
| } | |
| station_id <- as.character(station_id) | |
| session <- getDefaultReactiveDomain() | |
| # Get station metadata for display | |
| all_stations <- stations_data() | |
| meta <- all_stations[as.character(all_stations$StationID) == station_id, ] | |
| if (nrow(meta) > 0) { | |
| # Use pending variables to guard against transient selection resets during choices update | |
| pending_country(meta$Country[1]) | |
| pending_station(meta$StationName[1]) | |
| updateSelectizeInput(session, "country_obs", selected = meta$Country[1]) | |
| updateSelectizeInput(session, "station_obs", selected = meta$StationName[1]) | |
| } | |
| } | |
| }) | |
| # Stage 2: Next Parameter | |
| observe({ | |
| req(fetch_stage() == 2) | |
| session <- getDefaultReactiveDomain() | |
| token <- fetch_token() | |
| idx <- fetch_queue_idx() | |
| params <- fetch_queue() | |
| if (idx > length(params)) { | |
| # All done -> Merge | |
| fetch_stage(4) | |
| return() | |
| } | |
| param_names <- names(params) | |
| param_dir <- param_names[idx] | |
| col_name <- params[[param_dir]] | |
| # Update UI | |
| session$sendCustomMessage("freezeUI", list( | |
| text = paste0("Fetching ", col_name, " (", idx, "/", length(params), ")..."), | |
| station = fetch_station_name() | |
| )) | |
| # Move to Fetch stage after yielding control | |
| later::later(function() { | |
| # Check if cancelled | |
| if (!identical(isolate(fetch_token()), token)) { | |
| return() # Token invalidated, stop | |
| } | |
| isolate({ | |
| fetch_stage(3) | |
| }) | |
| }, 0.1) | |
| fetch_stage(-1) # Hold | |
| }) | |
| # Stage 3: Fetch current parameter | |
| observe({ | |
| req(fetch_stage() == 3) | |
| session <- getDefaultReactiveDomain() | |
| token <- fetch_token() | |
| idx <- fetch_queue_idx() | |
| params <- fetch_queue() | |
| station_id <- fetch_station_id() | |
| param_names <- names(params) | |
| param_dir <- param_names[idx] | |
| col_name <- params[[param_dir]] | |
| later::later(function() { | |
| # Check if cancelled | |
| if (!identical(isolate(fetch_token()), token)) { | |
| return() | |
| } | |
| isolate({ | |
| base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_global/CLIMAT/monthly/qc/" | |
| month_map <- c( | |
| "Jan" = 1, "Feb" = 2, "Mrz" = 3, "Apr" = 4, "Mai" = 5, "Jun" = 6, | |
| "Jul" = 7, "Aug" = 8, "Sep" = 9, "Okt" = 10, "Nov" = 11, "Dez" = 12 | |
| ) | |
| # Fetch recent | |
| url_recent <- paste0(base_url, param_dir, "/recent/", sprintf("%05d", as.integer(station_id)), ".txt") | |
| df_recent <- tryCatch(parse_dwd_file(url_recent, col_name, month_map), error = function(e) NULL) | |
| # Check again | |
| if (!identical(fetch_token(), token)) { | |
| return() | |
| } | |
| # Fetch historical | |
| url_hist <- tryCatch(get_historical_url(param_dir, sprintf("%05d", as.integer(station_id)), base_url, session), error = function(e) NULL) | |
| df_hist <- if (!is.null(url_hist)) tryCatch(parse_dwd_file(url_hist, col_name, month_map), error = function(e) NULL) else NULL | |
| # Check again | |
| if (!identical(fetch_token(), token)) { | |
| return() | |
| } | |
| # Combine | |
| df_combined <- rbind(df_recent, df_hist) | |
| if (!is.null(df_combined) && nrow(df_combined) > 0) { | |
| df_combined <- df_combined[!duplicated(df_combined$Date), ] | |
| current_data <- fetch_parsed_data() | |
| current_data[[col_name]] <- df_combined | |
| fetch_parsed_data(current_data) | |
| } | |
| # Move to next parameter | |
| fetch_queue_idx(idx + 1) | |
| fetch_stage(2) | |
| }) | |
| }, 0.1) | |
| fetch_stage(-1) # Hold | |
| }) | |
| # Stage 4: Merge and Finalize | |
| observe({ | |
| req(fetch_stage() == 4) | |
| session <- getDefaultReactiveDomain() | |
| token <- fetch_token() | |
| session$sendCustomMessage("freezeUI", list( | |
| text = "Merging data...", | |
| station = fetch_station_name() | |
| )) | |
| later::later(function() { | |
| if (!identical(isolate(fetch_token()), token)) { | |
| return() | |
| } | |
| isolate({ | |
| all_data <- fetch_parsed_data() | |
| if (length(all_data) == 0) { | |
| session$sendCustomMessage("unfreezeUI", list()) | |
| showNotification("No data found for this station.", type = "error", duration = 5) | |
| fetch_stage(0) | |
| return() | |
| } | |
| # Merge all parameters | |
| first_key <- names(all_data)[1] | |
| final_df <- all_data[[first_key]] | |
| if (length(all_data) > 1) { | |
| for (key in names(all_data)[-1]) { | |
| final_df <- merge(final_df, all_data[[key]], by = "Date", all = TRUE) | |
| } | |
| } | |
| final_df <- final_df[order(final_df$Date), ] | |
| # Create complete date sequence to show gaps in data | |
| min_date <- min(final_df$Date, na.rm = TRUE) | |
| max_date <- max(final_df$Date, na.rm = TRUE) | |
| complete_dates <- seq.Date( | |
| from = as.Date(paste0(format(min_date, "%Y-%m"), "-01")), | |
| to = as.Date(paste0(format(max_date, "%Y-%m"), "-01")), | |
| by = "month" | |
| ) | |
| complete_df <- data.frame(Date = complete_dates) | |
| final_df <- merge(complete_df, final_df, by = "Date", all.x = TRUE) | |
| final_df <- final_df[order(final_df$Date), ] | |
| final_df$Year <- as.numeric(format(final_df$Date, "%Y")) | |
| final_df$Month <- as.numeric(format(final_df$Date, "%m")) | |
| # Reorder columns | |
| params <- fetch_queue() | |
| cols <- c("Date", "Year", "Month", unlist(params, use.names = FALSE)) | |
| cols <- intersect(cols, names(final_df)) | |
| final_df <- final_df[, cols] | |
| weather_data_obs(final_df) | |
| weather_data_obs(final_df) | |
| # Switch tab to Dashboard | |
| updateTabsetPanel(session = session, inputId = "main_nav", selected = "Dashboard") | |
| session$sendCustomMessage("unfreezeUI", list()) | |
| fetch_stage(0) | |
| }) | |
| }, 0.1) | |
| fetch_stage(-1) | |
| }) | |
| # ============================================================================ | |
| # END ASYNC STATE MACHINE | |
| # ============================================================================ | |
| # Render weather plots - create reactive for shared plot list | |
| obs_plot_list <- reactive({ | |
| data <- weather_data_obs() | |
| station_name <- fetch_station_name() | |
| if (is.null(station_name)) station_name <- "Station Data" | |
| plot_climat_obs_data(data, station_name) | |
| }) | |
| # Render 4 separate plot outputs | |
| output$obs_plot_temp <- renderPlotly({ | |
| plots <- obs_plot_list() | |
| plots$temp | |
| }) | |
| output$obs_plot_precip <- renderPlotly({ | |
| plots <- obs_plot_list() | |
| plots$precip | |
| }) | |
| output$obs_plot_sun <- renderPlotly({ | |
| plots <- obs_plot_list() | |
| plots$sun | |
| }) | |
| output$obs_plot_pressure <- renderPlotly({ | |
| plots <- obs_plot_list() | |
| plots$pressure | |
| }) | |
| # Render Summary Cards | |
| output$obs_summary_cards <- renderUI({ | |
| df_clean <- weather_data_obs() | |
| req(df_clean) | |
| format_val <- function(val, suffix="", digits=1, divisor=1) { | |
| if (is.null(val) || is.na(val) || is.infinite(val)) return("—") | |
| paste0(round(val/divisor, digits), suffix) | |
| } | |
| format_ext_time <- function(idx) { | |
| if (length(idx) == 0 || is.na(idx) || is.na(idx[1])) return("") | |
| if (idx[1] > nrow(df_clean) || idx[1] < 1) return("") | |
| t <- df_clean$Date[idx[1]] | |
| if (is.null(t) || is.na(t)) return("") | |
| format(t, "%b %Y") | |
| } | |
| cards <- tagList() | |
| add_card <- function(title, value, subtext, icon, color_class) { | |
| cards <<- tagList(cards, div( | |
| class = paste("glass-card", color_class), | |
| div(class = "glass-card-icon", bsicons::bs_icon(icon)), | |
| div( | |
| class = "glass-card-content", | |
| div(class = "glass-card-title", title), | |
| div(class = "glass-card-value", value), | |
| div(class = "glass-card-subtext", HTML(subtext)) | |
| ) | |
| )) | |
| } | |
| # Temperature (MeanTemp, MaxTempAbs, MinTempAbs) | |
| if ("MeanTemp" %in% names(df_clean) && any(!is.na(df_clean$MeanTemp))) { | |
| latest <- tail(na.omit(df_clean$MeanTemp), 1) | |
| avg_t <- mean(df_clean$MeanTemp, na.rm = TRUE) | |
| min_t <- if ("MinTempAbs" %in% names(df_clean) && any(!is.na(df_clean$MinTempAbs))) min(df_clean$MinTempAbs, na.rm=TRUE) else min(df_clean$MeanTemp, na.rm=TRUE) | |
| max_t <- if ("MaxTempAbs" %in% names(df_clean) && any(!is.na(df_clean$MaxTempAbs))) max(df_clean$MaxTempAbs, na.rm=TRUE) else max(df_clean$MeanTemp, na.rm=TRUE) | |
| min_idx <- if ("MinTempAbs" %in% names(df_clean) && any(!is.na(df_clean$MinTempAbs))) which.min(df_clean$MinTempAbs) else which.min(df_clean$MeanTemp) | |
| max_idx <- if ("MaxTempAbs" %in% names(df_clean) && any(!is.na(df_clean$MaxTempAbs))) which.max(df_clean$MaxTempAbs) else which.max(df_clean$MeanTemp) | |
| add_card("Temperature", format_val(latest, "°C"), | |
| sprintf("Avg: %s | Min: %s (%s) | Max: %s (%s)", | |
| format_val(avg_t, "°C"), format_val(min_t, "°C"), format_ext_time(min_idx), format_val(max_t, "°C"), format_ext_time(max_idx)), | |
| "thermometer-half", "card-red") | |
| } | |
| # Precipitation | |
| if ("Precipitation" %in% names(df_clean) && any(!is.na(df_clean$Precipitation))) { | |
| latest <- tail(na.omit(df_clean$Precipitation), 1) | |
| avg_p <- mean(df_clean$Precipitation, na.rm = TRUE) | |
| max_p <- max(df_clean$Precipitation, na.rm = TRUE) | |
| max_idx <- which.max(df_clean$Precipitation) | |
| add_card("Precipitation", format_val(latest, " mm"), | |
| sprintf("Avg: %s | Peak: %s (%s)", format_val(avg_p, " mm"), format_val(max_p, " mm"), format_ext_time(max_idx)), | |
| "cloud-rain", "card-blue") | |
| } | |
| # Sunshine Duration | |
| if ("SunshineDuration" %in% names(df_clean) && any(!is.na(df_clean$SunshineDuration))) { | |
| latest <- tail(na.omit(df_clean$SunshineDuration), 1) | |
| avg_sun <- mean(df_clean$SunshineDuration, na.rm = TRUE) | |
| add_card("Sunshine", format_val(latest, " hrs", 1), | |
| sprintf("Avg: %s", format_val(avg_sun, " hrs", 1)), | |
| "sun", "card-amber") | |
| } | |
| # Pressure | |
| if ("MeanSeaLevelPressure" %in% names(df_clean) && any(!is.na(df_clean$MeanSeaLevelPressure))) { | |
| latest <- tail(na.omit(df_clean$MeanSeaLevelPressure), 1) | |
| avg_pr <- mean(df_clean$MeanSeaLevelPressure, na.rm=TRUE) | |
| min_pr <- min(df_clean$MeanSeaLevelPressure, na.rm=TRUE) | |
| max_pr <- max(df_clean$MeanSeaLevelPressure, na.rm=TRUE) | |
| min_idx <- which.min(df_clean$MeanSeaLevelPressure) | |
| max_idx <- which.max(df_clean$MeanSeaLevelPressure) | |
| add_card("Atm. Pressure", format_val(latest, " hPa", 0), | |
| sprintf("Avg: %s | Range: %s (%s) - %s (%s)", format_val(avg_pr, " hPa", 0), format_val(min_pr, " hPa", 0), format_ext_time(min_idx), format_val(max_pr, " hPa", 0), format_ext_time(max_idx)), | |
| "speedometer2", "card-purple") | |
| } | |
| if (length(cards) > 0) { | |
| date_range_str <- paste( | |
| format(min(df_clean$Date, na.rm=TRUE), "%b %Y"), "-", | |
| format(max(df_clean$Date, na.rm=TRUE), "%b %Y") | |
| ) | |
| latest_obs_str <- if (nrow(df_clean) > 0) paste("— Latest values as of:", format_ext_time(nrow(df_clean))) else "" | |
| div( | |
| class = "w-100 mb-3", | |
| div(class = "summary-cards-header", paste("Station Observations Summary", if (nchar(date_range_str) > 0) paste0("(", date_range_str, ")") else "", latest_obs_str)), | |
| div(class = "summary-cards-container", cards) | |
| ) | |
| } else { | |
| NULL | |
| } | |
| }) | |
| # Render Station Metadata Header | |
| output$obs_station_meta <- renderUI({ | |
| id <- fetch_station_id() | |
| stations <- stations_data() | |
| if (is.null(id) || is.null(stations)) { | |
| return(span("No station selected", style = "font-weight:bold; font-size:1.2em;")) | |
| } | |
| # Find station in data | |
| st <- stations[stations$StationID == id, ] | |
| if (nrow(st) == 0) { | |
| return(span("Station not found", style = "font-weight:bold; font-size:1.2em;")) | |
| } | |
| # Format coordinates | |
| lat <- round(st$Latitude, 4) | |
| lon <- round(st$Longitude, 4) | |
| lat_dir <- if (lat >= 0) "°N" else "°S" | |
| lon_dir <- if (lon >= 0) "°E" else "°W" | |
| coords <- paste0(abs(lat), lat_dir, ", ", abs(lon), lon_dir) | |
| # Create styled metadata display | |
| div( | |
| style = "display: flex; flex-direction: column; gap: 0.2rem;", | |
| div( | |
| style = "font-weight: bold; font-size: 1.2em;", | |
| st$StationName, | |
| span( | |
| style = "font-weight: normal; color: #666; font-size: 0.9em; margin-left: 0.5rem;", | |
| paste0("ID: ", st$StationID, " | ", st$Country) | |
| ) | |
| ), | |
| div( | |
| style = "font-size: 0.95em; color: #555;", | |
| span(style = "margin-right: 1rem;", paste0("⛰️ ", st$Height, "m")), | |
| span(paste0("📍 ", coords)) | |
| ) | |
| ) | |
| }) | |
| # Download Handler | |
| output$download_obs_data <- downloadHandler( | |
| filename = function() { | |
| station_name <- fetch_station_name() | |
| if (is.null(station_name)) station_name <- "station" | |
| # Sanitize filename | |
| safe_name <- gsub("[^a-zA-Z0-9]", "_", station_name) | |
| paste0("climat_", safe_name, "_", Sys.Date(), ".csv") | |
| }, | |
| content = function(file) { | |
| data <- weather_data_obs() | |
| if (!is.null(data)) { | |
| write.csv(data, file, row.names = FALSE) | |
| } | |
| } | |
| ) | |
| # Render weather data table | |
| output$weather_data_table <- DT::renderDataTable({ | |
| data <- weather_data_obs() | |
| print(paste("Rendering weather_data_table. Data is null?", is.null(data))) # Debugging | |
| if (is.null(data)) { | |
| DT::datatable(data.frame(Message = "No data selected. Click a station on the map to load data."), | |
| options = list(pageLength = 5, dom = "t") | |
| ) | |
| } else { | |
| DT::datatable(data, rownames = FALSE, class = "display nowrap", options = list( | |
| pageLength = 16, | |
| scrollX = TRUE, | |
| order = list(list(0, 'desc')) | |
| )) | |
| } | |
| }) | |
| # Render station info table | |
| output$stations_table <- DT::renderDataTable({ | |
| stations_in_box <- filtered_stations_by_country() | |
| if (is.null(stations_in_box) || nrow(stations_in_box) == 0) { | |
| DT::datatable(data.frame(Message = "No station data available"), options = list(pageLength = 5)) | |
| } else { | |
| # Convert sf to data.frame for DT display (drop geometry column) | |
| stations_df <- sf::st_drop_geometry(stations_in_box) | |
| stations_df_display <- stations_df %>% | |
| dplyr::select( | |
| "ID" = StationID, | |
| "Name" = StationName, | |
| "Country" = Country, | |
| "Elev." = Height, | |
| "Lat" = Latitude, | |
| "Lon" = Longitude | |
| ) | |
| DT::datatable(stations_df_display, | |
| selection = "none", | |
| rownames = FALSE, | |
| class = "display nowrap", | |
| options = list( | |
| pageLength = 16, | |
| scrollX = TRUE | |
| ), | |
| 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'}); | |
| } | |
| }); | |
| if (window.innerWidth <= 768) { | |
| table.on('click', '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) | |
| stations_in_box <- stations_data() | |
| s <- stations_in_box[as.character(stations_in_box$StationID) == as.character(id_val), ] | |
| req(nrow(s) > 0) | |
| updateSelectizeInput(session, "country_obs", selected = s$Country[1]) | |
| updateSelectizeInput(session, "station_obs", selected = s$StationName[1]) | |
| updateTabsetPanel(session, "main_nav", selected = "Dashboard") | |
| }) | |
| # Removed Zoom to selected area logic as it is not needed globally | |
| output$map_titl_obs <- renderText({ | |
| "Climate Observational Data Map" | |
| }) | |
| output$graph_titl_obs <- renderText({ | |
| "Timeseries Plot - Placeholder" | |
| }) | |
| } | |