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" }) }