# server.R server <- function(input, output, session) { stations <- reactive({ get_imgw_stations() }) # Map state style_change_trigger <- reactiveVal(0) stations_before_id <- reactiveVal("waterway_line_label") current_raster_layers <- reactiveVal(character(0)) map_initialized <- reactiveVal(FALSE) # Dashboard station state current_station_uid <- reactiveVal(NULL) station_data <- reactiveVal(NULL) station_fetching <- reactiveVal(FALSE) station_fetch_message <- reactiveVal("") station_fetch_error <- reactiveVal(NULL) prev_dates <- reactiveVal(NULL) # Fetch Async State Machine Variables fetch_stage <- reactiveVal(0) # 0=Idle, 1=Init, 2=Next, 4=Download/Parse, 6=Merge fetch_queue <- reactiveVal(NULL) fetch_queue_idx <- reactiveVal(0) parsed_data_list <- reactiveVal(list()) fetch_current_token <- reactiveVal(NULL) # Track if URL params have been applied url_initialized <- reactiveVal(FALSE) # Map bounds for Poland map_bounds <- list( lng_min = 14.0, lat_min = 49.0, lng_max = 24.5, lat_max = 55.0 ) # --- Loading State & Freeze Window --- loading_status <- reactiveVal(FALSE) # Observer: Freeze UI during loading observe({ is_loading <- loading_status() # List of inputs to disable/enable inputs_to_toggle <- c( "temporal_resolution", "date_range", "zoom_home", "main_nav", "basemap", "show_labels", "stations_table" # Also disable table interaction if possible (though specific row clicks are hard to prevent, visual disable is good) ) if (is_loading) { for (inp in inputs_to_toggle) { shinyjs::disable(inp) } # Optional: Show a global loading notification or overlay if desired, # but the inputs being disabled is the primary visual cue + the spinner in dashboard. } else { for (inp in inputs_to_toggle) { shinyjs::enable(inp) } } }) # Helper: Parse URL query string parse_url_params <- function(query_string) { if (is.null(query_string) || query_string == "" || query_string == "?") { return(list()) } # Remove leading "?" 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 } # Helper: Broadcast current state to parent page broadcast_state <- function(view_override = NULL) { st <- isolate(selected_station()) # Extract scalar values from tibble row station_id <- if (!is.null(st) && nrow(st) > 0) as.character(st$id[1]) else NULL station_name <- if (!is.null(st) && nrow(st) > 0) as.character(st$name[1]) else NULL landname <- if (!is.null(st) && nrow(st) > 0 && "station_type" %in% names(st) && "data_order" %in% names(st) && !is.na(st$station_type[1]) && !is.na(st$data_order[1])) paste0(st$station_type[1], "-", st$data_order[1]) else NULL resolution <- isolate(input$temporal_resolution) # Determine current view main_tab <- isolate(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 <- isolate(input$dashboard_subtabs) if (!is.null(subtab) && subtab == "Data") { "dashboard-data" } else { "dashboard-plots" } } else { "map" } } else { "map" } date_range_val <- isolate(input$date_range) start_date <- if (!is.null(date_range_val)) as.character(date_range_val[1]) else NULL end_date <- if (!is.null(date_range_val)) as.character(date_range_val[2]) else NULL session$sendCustomMessage("updateParentURL", list( station = station_id, stationName = station_name, landname = landname, resolution = resolution, view = view, start = start_date, end = end_date )) } # Observer: Apply URL params on app startup (runs once) observe({ req(!url_initialized()) # Use invalidateLater to retry if clientData isn't ready yet query <- session$clientData$url_search if (is.null(query)) { # clientData not ready yet — set initialized anyway to unblock url_initialized(TRUE) return() } params <- parse_url_params(query) if (length(params) == 0) { url_initialized(TRUE) return() } # Apply resolution if (!is.null(params$resolution) && params$resolution %in% c("hourly", "daily", "monthly")) { updateRadioButtons(session, "temporal_resolution", selected = params$resolution) } # Apply date range if (!is.null(params$start) && !is.null(params$end)) { start_date <- tryCatch(as.Date(params$start), error = function(e) NULL) end_date <- tryCatch(as.Date(params$end), error = function(e) NULL) if (!is.null(start_date) && !is.null(end_date)) { updateDateRangeInput(session, "date_range", start = start_date, end = end_date) prev_dates(c(start_date, end_date)) } } # Apply station selection (deferred to allow inputs to update) if (!is.null(params$station)) { station <- params$station # Match by name or ID all_stations <- stations() st <- all_stations %>% filter(id == station | name == station) %>% slice(1) if (nrow(st) > 0) { shinyjs::delay(200, { current_station_uid(paste(st$id[1], st$name[1], sep = "|")) }) } } # Apply view/tab navigation (deferred) if (!is.null(params$view)) { view <- params$view shinyjs::delay(600, { if (view == "map") { session$sendCustomMessage("switchTab", list(tabId = "Map View")) } else if (view == "station-info") { session$sendCustomMessage("switchTab", list(tabId = "Stations Info")) } else if (view %in% c("dashboard-plots", "dashboard-data")) { session$sendCustomMessage("switchTab", list(tabId = "Dashboard")) if (view == "dashboard-data") { shinyjs::delay(800, { shinyjs::runjs("$('a[data-value=\"Data\"]').tab('show');") }) } } }) } url_initialized(TRUE) }) resolution_key <- reactive({ res <- input$temporal_resolution if (is.null(res) || !res %in% c("hourly", "daily", "monthly")) "daily" else res }) params_by_resolution <- list( hourly = c( synop = "Temperature, humidity, wind, pressure, clouds, visibility, precipitation, snow, sunshine, soil", klimat = "Temperature, humidity, wind, clouds, visibility", opad = "Not available for hourly", `tylko dok. papierowe` = "Paper archives only" ), daily = c( synop = "Temperature, precipitation, snow, sunshine, weather phenomena, soil", klimat = "Temperature, precipitation, snow", opad = "Precipitation, snow depth, snowfall type", `tylko dok. papierowe` = "Paper archives only" ), monthly = c( synop = "Temperature, precipitation, snow, sunshine, wind, humidity, cloudiness, weather days", klimat = "Temperature, precipitation, snow, wind, humidity, cloudiness", opad = "Precipitation, snow days, snow cover days", `tylko dok. papierowe` = "Paper archives only" ) ) parse_station_uid <- function(uid) { uid <- as.character(uid)[1] if (is.na(uid) || uid == "") { return(list(id = NA_character_, name = NA_character_)) } if (!grepl("|", uid, fixed = TRUE)) { return(list(id = uid, name = NA_character_)) } list( id = sub("\\|.*$", "", uid), name = sub("^[^|]*\\|", "", uid) ) } build_station_popup <- function(st_row, res = NULL) { if (is.null(res)) res <- resolution_key() if (is.null(st_row) || nrow(st_row) == 0) { return("Station") } resolution_label <- switch(res, hourly = "Hourly", daily = "Daily", monthly = "Monthly", "Daily" ) selected_start <- switch(res, hourly = if ("date_start_hourly" %in% names(st_row)) st_row$date_start_hourly[1] else NA, daily = if ("date_start_daily" %in% names(st_row)) st_row$date_start_daily[1] else NA, monthly = if ("date_start_monthly" %in% names(st_row)) st_row$date_start_monthly[1] else NA, if ("date_start" %in% names(st_row)) st_row$date_start[1] else NA ) selected_end <- switch(res, hourly = if ("date_end_hourly" %in% names(st_row)) st_row$date_end_hourly[1] else NA, daily = if ("date_end_daily" %in% names(st_row)) st_row$date_end_daily[1] else NA, monthly = if ("date_end_monthly" %in% names(st_row)) st_row$date_end_monthly[1] else NA, if ("date_end" %in% names(st_row)) st_row$date_end[1] else NA ) if (is.na(selected_start) && "date_start" %in% names(st_row)) selected_start <- st_row$date_start[1] if (is.na(selected_end) && "date_end" %in% names(st_row)) selected_end <- st_row$date_end[1] if (!is.na(selected_end) && !is.na(selected_start) && selected_end < selected_start) selected_end <- NA start_fmt <- if (is.na(selected_start)) "Unknown" else as.character(selected_start) end_fmt <- if (is.na(selected_end)) "Present" else as.character(selected_end) date_range <- paste0(start_fmt, " to ", end_fmt) station_name <- if ("name" %in% names(st_row)) as.character(st_row$name[1]) else "Unknown" station_id <- if ("id" %in% names(st_row)) as.character(st_row$id[1]) else "N/A" station_wmo <- if ("id_9" %in% names(st_row)) as.character(st_row$id_9[1]) else "N/A" station_type <- if ("station_type" %in% names(st_row)) as.character(st_row$station_type[1]) else "Unknown" params_summary <- if ("params_summary" %in% names(st_row)) as.character(st_row$params_summary[1]) else "Unknown" elevation <- if ("elevation" %in% names(st_row)) as.character(st_row$elevation[1]) else "" elev_fmt <- if (is.na(elevation) || elevation == "") "N/A" else paste0(elevation, " m") paste0( "
", "", htmltools::htmlEscape(station_name), "
", "Code: ", htmltools::htmlEscape(station_id), "
", "WMO ID: ", htmltools::htmlEscape(station_wmo), "
", "Type: ", htmltools::htmlEscape(station_type), "
", "Parameters (", resolution_label, "): ", htmltools::htmlEscape(params_summary), "
", "Active: ", date_range, "
", "Elevation: ", elev_fmt, "", "
" ) } filtered_stations <- reactive({ req(stations(), input$date_range) st_data <- stations() res <- resolution_key() user_start <- input$date_range[1] user_end <- input$date_range[2] res_start_col <- paste0("date_start_", res) res_end_col <- paste0("date_end_", res) if (!res_end_col %in% names(st_data)) st_data[[res_end_col]] <- st_data$date_end if (!"date_end_hourly" %in% names(st_data)) st_data$date_end_hourly <- st_data$date_end if (!"date_end_daily" %in% names(st_data)) st_data$date_end_daily <- st_data$date_end if (!"date_end_monthly" %in% names(st_data)) st_data$date_end_monthly <- st_data$date_end st_data <- st_data %>% filter( !is.na(!!sym(res_start_col)), !!sym(res_start_col) <= user_end, is.na(!!sym(res_end_col)) | !!sym(res_end_col) >= user_start ) if (!"data_order" %in% names(st_data)) st_data$data_order <- NA_character_ data_order_trim <- tolower(trimws(st_data$data_order)) params_by_data_order <- params_by_resolution[[res]] params_by_station_type <- c( Synoptic = params_by_data_order[["synop"]], Climatological = params_by_data_order[["klimat"]], Precipitation = params_by_data_order[["opad"]] ) st_data$params_summary <- unname(params_by_data_order[data_order_trim]) missing_params <- is.na(st_data$params_summary) | st_data$params_summary == "" st_data$params_summary[missing_params] <- unname(params_by_station_type[st_data$station_type[missing_params]]) st_data$params_summary[is.na(st_data$params_summary) | st_data$params_summary == ""] <- "Unknown" st_data$station_uid <- paste(st_data$id, st_data$name, sep = "|") st_data }) # Server-side selectize for station search observe({ df <- filtered_stations() # Create named choices: display "Name (ID)" but value is station_uid choices <- setNames(df$station_uid, paste0(df$name, " (", df$id, ")")) updateSelectizeInput(session, "station_selector", choices = c("All" = "", choices), server = TRUE ) }) # Handle station selection from dropdown observeEvent(input$station_selector, { uid <- input$station_selector if (is.null(uid) || uid == "") { return() } current_station_uid(uid) station_fetch_error(NULL) st <- filtered_stations() %>% filter(station_uid == uid) %>% slice(1) if (nrow(st) > 0) { highlight_selected_station(st, move_map = TRUE) # Trigger broadcast broadcast_state() } }) selected_station <- reactive({ uid <- current_station_uid() req(uid) parsed <- parse_station_uid(uid) st <- stations() if (is.na(parsed$id) || parsed$id == "") { return(NULL) } if (!is.na(parsed$name) && parsed$name != "") { row <- st %>% filter(id == parsed$id, name == parsed$name) %>% slice(1) if (nrow(row) > 0) { return(row) } } row <- st %>% filter(id == parsed$id) %>% slice(1) if (nrow(row) == 0) { return(NULL) } row }) highlight_selected_station <- function(st_row, move_map = TRUE) { req(nrow(st_row) > 0) lon_val <- suppressWarnings(as.numeric(st_row$lon[1])) lat_val <- suppressWarnings(as.numeric(st_row$lat[1])) if (is.na(lon_val) || is.na(lat_val)) { return(invisible(NULL)) } popup_html <- NULL if ("popup_content" %in% names(st_row)) { popup_candidate <- as.character(st_row$popup_content[1]) if (!is.na(popup_candidate) && nzchar(popup_candidate)) { popup_html <- popup_candidate } } if (is.null(popup_html)) { popup_html <- build_station_popup(st_row, res = isolate(resolution_key())) } highlight_df <- data.frame( popup_content = popup_html, lon = lon_val, lat = lat_val, stringsAsFactors = FALSE ) highlight_sf <- sf::st_as_sf(highlight_df, coords = c("lon", "lat"), crs = 4326) proxy <- maplibre_proxy("map") if (move_map) { proxy <- proxy %>% fly_to(center = c(lon_val, lat_val), zoom = 8) } proxy %>% clear_layer("selected-highlight") %>% add_circle_layer( id = "selected-highlight", source = highlight_sf, circle_color = "#e53935", circle_radius = 10, circle_stroke_color = "#b71c1c", circle_stroke_width = 2, circle_opacity = 0.45, tooltip = get_column("popup_content") ) } output$station_count <- renderText({ req(filtered_stations()) paste(nrow(filtered_stations()), "stations displayed.") }) # Initialize Map output$map <- renderMaplibre({ maplibre( style = ofm_positron_style, center = c(19.1451, 51.9194), zoom = 5.5 ) %>% add_navigation_control(show_compass = FALSE, visualize_pitch = FALSE, position = "top-left") }) # Zoom to extent of all stations (home button) observeEvent(input$zoom_home, { maplibre_proxy("map") %>% fly_to(center = c(19.1451, 51.9194), zoom = 5.5) }) # --- Basemap Switching Logic --- label_layer_ids <- c( # OpenFreeMap Positron & Bright common labels "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", # Bright specific labels (POIs & Directions) "road_oneway", "road_oneway_opposite", "poi_r20", "poi_r7", "poi_r1", "poi_transit", # Dash variants (sometimes used in different versions) "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", # Legacy/Carto/OSM label names (for compatibility) "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" ) # Layers to hide in Satellite (Hybrid) mode to let the raster shows through # This includes all non-symbol layers (background, water, roads, boundaries, etc.) 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) { # Layer may not exist in current style, ignore silently } ) } } observeEvent(input$basemap, { proxy <- maplibre_proxy("map") basemap <- input$basemap # Explicitly remove any previously added raster layers old_layers <- isolate(current_raster_layers()) if (length(old_layers) > 0) { for (layer_id in old_layers) { proxy %>% clear_layer(layer_id) } current_raster_layers(character(0)) } if (basemap %in% c("ofm_positron", "ofm_bright")) { # VECTOR LOGIC (OpenFreeMap styles) style_url <- switch(basemap, "ofm_positron" = ofm_positron_style, "ofm_bright" = ofm_bright_style ) proxy %>% set_style(style_url, preserve_layers = FALSE) # For OpenFreeMap sandwich, we want stations below labels stations_before_id("waterway_line_label") current_session <- shiny::getDefaultReactiveDomain() selected_basemap <- basemap later::later(function() { shiny::withReactiveDomain(current_session, { # Race condition check current_basemap <- isolate(input$basemap) if (current_basemap != selected_basemap) { return() } # Trigger re-render of stations and apply label visibility apply_label_visibility(maplibre_proxy("map"), isolate(input$show_labels)) style_change_trigger(isolate(style_change_trigger()) + 1) }) }, delay = 0.5) } else if (basemap == "sentinel") { # HYBRID LOGIC (EOX Sentinel-2 Satellite + OpenFreeMap Labels/Roads) # Use Positron as the base for labels/roads 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) # Add Sentinel layer at the very bottom (before 'background') maplibre_proxy("map") %>% add_raster_source(id = source_id, tiles = 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" ) # Hide all non-label vector layers so only satellite + symbols show for (layer_id_kill in non_label_layer_ids) { tryCatch( { maplibre_proxy("map") %>% set_layout_property(layer_id_kill, "visibility", "none") }, error = function(e) { # Layer might not exist, ignore } ) } # Ensure labels according to user preference apply_label_visibility(maplibre_proxy("map"), isolate(input$show_labels)) # Stations should be above the satellite layer stations_before_id("waterway_line_label") current_raster_layers(c(layer_id)) style_change_trigger(isolate(style_change_trigger()) + 1) }) }, delay = 0.5) } }, ignoreInit = TRUE ) # Helper to calculate and enforce limits check_and_limit_dates <- function(start, end, resolution, anchor = "end") { limit_days <- Inf limit_msg <- "" if (resolution == "hourly") { limit_days <- 93 limit_msg <- "3 months" } else if (resolution == "daily") { limit_days <- 366 limit_msg <- "1 year" } else if (resolution == "monthly") { limit_days <- 2191 limit_msg <- "6 years" } diff <- as.numeric(end - start) if (diff > limit_days) { new_start <- start new_end <- end if (anchor == "start") { new_end <- start + limit_days } else { new_start <- end - limit_days } return(list(start = new_start, end = new_end, limited = TRUE, msg = limit_msg)) } return(list(start = start, end = end, limited = FALSE)) } # Auto-refetch when resolution changes observeEvent(input$temporal_resolution, { req(input$temporal_resolution) req(input$date_range) start <- as.Date(input$date_range[1]) end <- as.Date(input$date_range[2]) res_val <- input$temporal_resolution if (res_val == "monthly") { # Special rule for Monthly: Always default to 6 years back new_start <- end - 365 * 6 res <- list(start = new_start, end = end, limited = TRUE, msg = "6 years") } else { res <- check_and_limit_dates(start, end, res_val, anchor = "end") } if (res$limited) { updateDateRangeInput(session, "date_range", start = res$start, end = res$end) showNotification(paste0("Range adjusted to ", res$msg, " for ", res_val, " resolution."), type = "warning") prev_dates(c(res$start, res$end)) } }, ignoreInit = TRUE ) # Auto-refetch and Smart Limit when Date Range Changes observeEvent(input$date_range, { req(input$date_range) req(input$temporal_resolution) current_dates <- input$date_range current_start <- as.Date(current_dates[1]) current_end <- as.Date(current_dates[2]) last_dates <- prev_dates() anchor <- "end" if (!is.null(last_dates)) { last_start <- as.Date(last_dates[1]) if (current_start != last_start) { anchor <- "start" } } res <- check_and_limit_dates(current_start, current_end, input$temporal_resolution, anchor = anchor) if (res$limited) { updateDateRangeInput(session, "date_range", start = res$start, end = res$end) showNotification(paste0("Limit exceeded. Adjusted to ", res$msg, "."), type = "warning") prev_dates(c(res$start, res$end)) } else { prev_dates(current_dates) } }, ignoreInit = TRUE ) # Toggle Labels visibility observeEvent(input$show_labels, { apply_label_visibility(maplibre_proxy("map"), input$show_labels) }, ignoreInit = TRUE ) # Add Stations Layer observe({ req(filtered_stations()) style_change_trigger() st_data <- filtered_stations() res <- resolution_key() resolution_label <- switch(res, hourly = "Hourly", daily = "Daily", monthly = "Monthly", "Daily" ) st_data$popup_content <- vapply( seq_len(nrow(st_data)), function(i) build_station_popup(st_data[i, , drop = FALSE], res = res), FUN.VALUE = character(1) ) st_sf <- st_as_sf(st_data, coords = c("lon", "lat"), crs = 4326) st_sf$circle_color <- "navy" st_sf$circle_radius <- 6 st_sf$circle_stroke_width <- 0 st_sf$circle_opacity <- 0.7 before_id_value <- stations_before_id() current_session <- shiny::getDefaultReactiveDomain() later::later(function() { shiny::withReactiveDomain(current_session, { maplibre_proxy("map") %>% clear_layer("stations_layer") %>% add_circle_layer( id = "stations_layer", source = st_sf, circle_color = get_column("circle_color"), circle_radius = get_column("circle_radius"), circle_stroke_width = get_column("circle_stroke_width"), circle_opacity = 0.7, tooltip = get_column("popup_content"), before_id = before_id_value ) }) }, delay = 1.2) selected_uid <- isolate(current_station_uid()) if (!is.null(selected_uid) && selected_uid != "") { sel <- st_data %>% filter(station_uid == selected_uid) %>% slice(1) if (nrow(sel) > 0) { later::later(function() { shiny::withReactiveDomain(current_session, { highlight_selected_station(sel, move_map = FALSE) }) }, delay = 1.25) } else { later::later(function() { shiny::withReactiveDomain(current_session, { maplibre_proxy("map") %>% clear_layer("selected-highlight") }) }, delay = 1.25) } } }) # Selection from map click observeEvent(input$map_feature_click, { clicked <- input$map_feature_click if (is.null(clicked)) { return() } layer_name <- clicked$layer_id if (is.null(layer_name)) layer_name <- clicked$layer if (is.null(layer_name) || layer_name != "stations_layer") { return() } props <- clicked$properties if (is.null(props)) { return() } uid <- props$station_uid if (is.null(uid) || uid == "") { id_val <- props$id name_val <- props$name if (!is.null(id_val) && !is.null(name_val)) { uid <- paste0(id_val, "|", name_val) } else if (!is.null(id_val)) { uid <- as.character(id_val) } } if (is.null(uid) || uid == "") { return() } uid <- as.character(uid)[1] current_station_uid(uid) station_fetch_error(NULL) st <- filtered_stations() %>% filter(station_uid == uid) %>% slice(1) if (nrow(st) > 0) { highlight_selected_station(st, move_map = TRUE) broadcast_state() } }) # Stations table with double-click selection output$stations_table <- DT::renderDataTable({ req(filtered_stations(), input$temporal_resolution) st_data <- filtered_stations() resolution_col <- paste0("date_start_", resolution_key()) end_col <- paste0("date_end_", resolution_key()) if (!end_col %in% names(st_data)) st_data[[end_col]] <- st_data$date_end display_df <- st_data %>% transmute( UID = station_uid, ID = id, Name = name, `WMO ID` = id_9, Type = station_type, Parameters = params_summary, Lat = round(lat, 4), Lon = round(lon, 4), `Elevation (m)` = elevation, `Data Start` = as.character(!!sym(resolution_col)), `Data End` = ifelse(is.na(!!sym(end_col)), "Present", as.character(!!sym(end_col))) ) DT::datatable( display_df, selection = "none", rownames = FALSE, options = list( pageLength = 25, scrollX = TRUE, dom = "frtip", order = list(list(2, "asc")), columnDefs = list(list(visible = FALSE, targets = 0)) ), class = "compact stripe hover", callback = DT::JS(" table.on('dblclick', 'tr', function() { var rowData = table.row(this).data(); if (rowData !== undefined && rowData !== null) { Shiny.setInputValue('table_station_dblclick', rowData[0], {priority: 'event'}); } }); ") ) }) observeEvent(input$table_station_dblclick, { uid <- input$table_station_dblclick req(uid) uid <- as.character(uid)[1] current_station_uid(uid) station_fetch_error(NULL) st <- filtered_stations() %>% filter(station_uid == uid) %>% slice(1) if (nrow(st) > 0) { highlight_selected_station(st, move_map = TRUE) broadcast_state(view_override = "dashboard-plots") } }) reset_fetch <- function() { fetch_stage(0) fetch_current_token(as.numeric(Sys.time())) session$sendCustomMessage("unfreezeUI", list()) } # Cancel loading handler observeEvent(input$cancel_loading, { loading_status(FALSE) station_fetching(FALSE) reset_fetch() showNotification("Loading cancelled by user.", type = "warning") }) # Fetch station data whenever selection/date/resolution changes observeEvent(list(current_station_uid(), input$temporal_resolution, input$date_range), { req(url_initialized()) # Ensure URL params are applied first uid <- current_station_uid() req(uid, input$date_range) st <- selected_station() req(!is.null(st), nrow(st) > 0) station_data(NULL) station_fetching(TRUE) loading_status(TRUE) station_fetch_error(NULL) # Broadcast state on re-fetch broadcast_state() start_date <- as.Date(input$date_range[1]) end_date <- as.Date(input$date_range[2]) res <- resolution_key() session$sendCustomMessage("freezeUI", list( text = paste0("Initializing fetch for ", st$name[1], "..."), station = st$name[1] )) # Stage 1: Init - Find files queue filesQueue <- imgw_fetch_station_data( station_id = st$id[1], resolution = res, start_date = start_date, end_date = end_date, data_order = st$data_order[1], station_type = st$station_type[1], station_id_9 = st$id_9[1] ) if (is.null(filesQueue) || nrow(filesQueue) == 0) { if (isTRUE(attr(filesQueue, "server_unreachable"))) { station_fetch_error("The IMGW data server (danepubliczne.imgw.pl) is currently unavailable. Please try again later.") } else { station_fetch_error("No downloadable IMGW files were found for this station within the selected date range.") } station_fetching(FALSE) loading_status(FALSE) reset_fetch() updateNavbarPage(session, "main_nav", selected = "Dashboard") return() } fetch_queue(filesQueue) fetch_queue_idx(1) parsed_data_list(list()) fetch_current_token(as.numeric(Sys.time())) fetch_stage(2) # Go to Next }, ignoreInit = TRUE ) # Stage 2: Next File observe({ req(fetch_stage() == 2) idx <- fetch_queue_idx() q <- fetch_queue() if (idx > nrow(q)) { fetch_stage(6) # Merge } else { # UI Update item <- q[idx, ] msg <- sprintf("Fetching %s (%d/%d)...", basename(item$url), idx, nrow(q)) st <- selected_station() session$sendCustomMessage("freezeUI", list( text = msg, station = st$name[1] )) fetch_stage(4) # Download } }) # Stage 4: Download and Parse Configured File observe({ req(fetch_stage() == 4) idx <- fetch_queue_idx() q <- fetch_queue() item <- q[idx, ] token <- fetch_current_token() current_session <- shiny::getDefaultReactiveDomain() st <- selected_station() res <- resolution_key() # Yield thread later::later(function() { shiny::withReactiveDomain(current_session, { if (isolate(fetch_current_token()) != token) { return() } parsed_df <- tryCatch( { imgw_read_station_zip_url( zip_url = item$url, resolution = res, source_type = item$source, station_id = st$id[1], station_id_9 = st$id_9[1] ) }, error = function(e) { message("Error fetching data piece: ", conditionMessage(e)) NULL } ) if (!is.null(parsed_df) && nrow(parsed_df) > 0) { p_list <- isolate(parsed_data_list()) p_list[[length(p_list) + 1]] <- parsed_df parsed_data_list(p_list) } # Advance fetch_queue_idx(idx + 1) fetch_stage(2) # Back to Next }) }, delay = 0.05) }) # Stage 6: Merge Files observe({ req(fetch_stage() == 6) token <- fetch_current_token() current_session <- shiny::getDefaultReactiveDomain() st <- selected_station() start_date <- as.Date(input$date_range[1]) end_date <- as.Date(input$date_range[2]) res <- resolution_key() session$sendCustomMessage("freezeUI", list( text = "Merging datasets...", station = st$name[1] )) later::later(function() { shiny::withReactiveDomain(current_session, { if (isolate(fetch_current_token()) != token) { return() } p_list <- isolate(parsed_data_list()) merged_df <- tryCatch( { imgw_process_downloaded_parts( parts = p_list, station_id_value = st$id[1], station_id_9_value = st$id_9[1], resolution = res, start_date = start_date, end_date = end_date, data_order = st$data_order[1], station_type = st$station_type[1] ) }, error = function(e) { message("Error merging chunks: ", conditionMessage(e)) NULL } ) if (is.null(merged_df) || nrow(merged_df) == 0) { station_fetch_error("No valid data rows found after merging files within this date range.") station_data(NULL) } else { station_data(merged_df) } station_fetching(FALSE) loading_status(FALSE) fetch_stage(0) fetch_current_token(as.numeric(Sys.time())) if (!is.null(merged_df) && nrow(merged_df) > 0) { # Show "Plotting data..." while plots render session$sendCustomMessage("freezeUI", list( text = "Plotting data...", station = st$name[1] )) # Unfreeze after Shiny flush + client-side plotly rendering session$onFlushed(function() { session$sendCustomMessage("waitForPlots", list()) }, once = TRUE) } else { session$sendCustomMessage("unfreezeUI", list()) } # Navigate to Dashboard only after data is ready updateNavbarPage(session, "main_nav", selected = "Dashboard") broadcast_state() }) }, delay = 0.05) }) # Listen for tab changes explicitly to broadcast observeEvent(input$main_nav, { req(url_initialized()) broadcast_state() }) observeEvent(input$dashboard_subtabs, { req(url_initialized()) broadcast_state() }) # Dashboard UI states output$dashboard_station_header <- renderUI({ st <- selected_station() if (is.null(st)) { return(NULL) } loaded <- station_data() # Determine Resolution Badge res_label <- switch(resolution_key(), hourly = "Hourly", daily = "Daily", monthly = "Monthly", "Daily" ) res_class <- if (res_label == "Hourly") "bg-primary" else "bg-success" # Data Range Text if (is.null(loaded) || nrow(loaded) == 0) { if (isTRUE(station_fetching())) { dates_text <- "Loading..." } else { dates_text <- "No data loaded" } } else { dates_text <- paste(min(loaded$date, na.rm = TRUE), "to", max(loaded$date, na.rm = TRUE)) dates_text <- paste0(dates_text, " (", nrow(loaded), " rows)") } # Helper for NULL/NA val_safe <- function(x, suffix = "") { if (is.null(x) || is.na(x) || x == "") { return("-") } paste0(x, suffix) } # Render Card card( style = "margin-bottom: 20px; border-left: 5px solid #007bff;", card_body( padding = 15, layout_columns( fill = FALSE, # Col 1: Station div( strong("Station"), br(), span(st$name[1], style = "font-size: 1.1rem;"), br(), tags$small(class = "text-muted", paste("ID:", st$id[1], "| WMO:", val_safe(st$id_9[1]))) ), # Col 2: Location div( strong("Location"), br(), span(paste0(round(st$lat[1], 4), "°N, ", round(st$lon[1], 4), "°E")), br(), tags$small(class = "text-muted", paste("Type:", st$station_type[1])) ), # Col 3: Technical div( strong("Technical"), br(), span(val_safe(st$elevation[1], " m")), br(), span(class = paste("badge", res_class), res_label) ), # Col 4: Period/Data div( strong("Data Selection"), br(), span(dates_text) ), # Col 5: Actions div( class = "d-flex align-items-center justify-content-end", downloadButton( "downloadData", label = "Export Excel", class = "btn-sm btn-primary", icon = icon("file-excel") ) ) ) ) ) }) # Helper to prepare plot data with DateTime get_plot_df <- reactive({ df <- station_data() req(df) # Translate columns upfront for plotting so we have English labels df_translated <- translate_imgw_columns(df) # Ensure DateTime column exists for plotting functions if (!"DateTime" %in% names(df_translated)) { if ("Date/Time" %in% names(df_translated)) { df_translated$DateTime <- df_translated$`Date/Time` } else if ("Date" %in% names(df_translated)) { df_translated$DateTime <- as.POSIXct(df_translated$Date, tz = "UTC") } else if ("Year" %in% names(df_translated) && "Month" %in% names(df_translated) && "Day" %in% names(df_translated) && "Hour" %in% names(df_translated)) { df_translated$DateTime <- as.POSIXct(paste(df_translated$Year, df_translated$Month, df_translated$Day, df_translated$Hour, sep = "-"), format = "%Y-%m-%d-%H", tz = "UTC") } else if ("Year" %in% names(df_translated) && "Month" %in% names(df_translated) && "Day" %in% names(df_translated)) { df_translated$DateTime <- as.POSIXct(paste(df_translated$Year, df_translated$Month, df_translated$Day, sep = "-"), format = "%Y-%m-%d", tz = "UTC") } else if ("Year" %in% names(df_translated) && "Month" %in% names(df_translated)) { df_translated$DateTime <- as.POSIXct(paste(df_translated$Year, df_translated$Month, "01", sep = "-"), format = "%Y-%m-%d", tz = "UTC") } } df_translated }) output$dashboard_plots_ui <- renderUI({ df <- get_plot_df() req(df) plot_list <- tagList() # Temperature temp_cols <- c("Temp [°C]", "Mean Daily Temp [°C]", "Mean Monthly Temp [°C]", "Max Temp [°C]", "Min Temp [°C]") has_temp <- any(temp_cols %in% names(df)) && any(vapply(intersect(temp_cols, names(df)), function(col) any(!is.na(df[[col]])), logical(1))) if (has_temp) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("temperature_plot", height = "320px"))) } # Precipitation precip_col <- if ("Precip 6h [mm]" %in% names(df)) "Precip 6h [mm]" else if ("Daily Precip [mm]" %in% names(df)) "Daily Precip [mm]" else if ("Monthly Precip [mm]" %in% names(df)) "Monthly Precip [mm]" else NULL has_precip <- !is.null(precip_col) && !all(is.na(df[[precip_col]])) if (has_precip) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("precipitation_plot", height = "320px"))) } # Wind wind_col <- if ("Wind Speed [m/s]" %in% names(df)) "Wind Speed [m/s]" else if ("Mean Wind Speed [m/s]" %in% names(df)) "Mean Wind Speed [m/s]" else NULL has_wind <- !is.null(wind_col) && !all(is.na(df[[wind_col]])) if (has_wind) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("wind_plot", height = "320px"))) } # Humidity hum_col <- if ("Relative Humidity [%]" %in% names(df)) "Relative Humidity [%]" else if ("Mean Rel Humidity [%]" %in% names(df)) "Mean Rel Humidity [%]" else NULL has_hum <- !is.null(hum_col) && !all(is.na(df[[hum_col]])) if (has_hum) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("humidity_plot", height = "320px"))) } # Pressure has_press <- any(c("Station Pressure [hPa]", "Sea Level Pressure [hPa]") %in% names(df) & !all(is.na(df[[if ("Station Pressure [hPa]" %in% names(df)) "Station Pressure [hPa]" else "Sea Level Pressure [hPa]"]]))) if (has_press) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("pressure_plot", height = "320px"))) } # Sunshine sun_col <- if ("Sunshine [h]" %in% names(df)) "Sunshine [h]" else if ("Monthly Sunshine [h]" %in% names(df)) "Monthly Sunshine [h]" else NULL has_sun <- !is.null(sun_col) && !all(is.na(df[[sun_col]])) if (has_sun) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("sunshine_plot", height = "320px"))) } # Snow snow_col <- if ("Snow Depth [cm]" %in% names(df)) "Snow Depth [cm]" else if ("Fresh Snow Depth [cm]" %in% names(df)) "Fresh Snow Depth [cm]" else NULL has_snow <- !is.null(snow_col) && !all(is.na(df[[snow_col]])) if (has_snow) { plot_list <- tagList(plot_list, div(class = "col-12 col-lg-6", plotlyOutput("snow_plot", height = "320px"))) } div(class = "row g-3", plot_list) }) # Excel Download Handler output$downloadData <- downloadHandler( filename = function() { st <- selected_station() req(st) res <- resolution_key() sprintf("imgw_%s_%s_%s.xlsx", st$id, res, format(Sys.Date(), "%Y%m%d")) }, content = function(file) { df <- station_data() req(df) # Use same translation as table df_export <- translate_imgw_columns(df) # Ensure Date/Time format is Excel friendly (POSIXct usually works, but string is safer if consistency needed) # writexl handles dates well, so we can keep them as is or format them. # Let's keep them as native types for Excel calculation potential. writexl::write_xlsx(df_export, path = file) } ) output$temperature_plot <- renderPlotly({ create_temperature_plot_imgw(get_plot_df(), resolution_key()) }) output$precipitation_plot <- renderPlotly({ create_precipitation_plot_imgw(get_plot_df(), resolution_key()) }) output$wind_plot <- renderPlotly({ res <- resolution_key() if (res == "hourly") { create_wind_rose_imgw(get_plot_df(), res) } else { create_wind_plot_imgw(get_plot_df(), res) } }) output$pressure_plot <- renderPlotly({ create_pressure_plot_imgw(get_plot_df(), resolution_key()) }) output$humidity_plot <- renderPlotly({ create_humidity_plot_imgw(get_plot_df(), resolution_key()) }) output$sunshine_plot <- renderPlotly({ create_sunshine_plot_imgw(get_plot_df(), resolution_key()) }) output$snow_plot <- renderPlotly({ create_snow_plot_imgw(get_plot_df(), resolution_key()) }) output$station_data_table <- DT::renderDataTable({ df <- station_data() req(df) df_display <- as.data.frame(df, stringsAsFactors = FALSE) # 1. Combine flags with values while column names are still raw raw_cols <- names(df_display) status_candidates <- raw_cols[grepl("^w+", raw_cols)] for (scol in status_candidates) { base1 <- sub("^w", "", scol) base2 <- sub("^ww", "", scol) base_var <- NULL if (base1 %in% raw_cols && base1 != scol) { base_var <- base1 } else if (base2 %in% raw_cols && base2 != scol) { base_var <- base2 } else if (scol == "wwlgs" && "wlgs" %in% raw_cols) { base_var <- "wlgs" } if (!is.null(base_var)) { vals <- df_display[[base_var]] flags <- df_display[[scol]] formatted_vals <- vapply(seq_along(vals), function(i) { v <- vals[i] f <- flags[i] v_is_empty <- is.na(v) || trimws(as.character(v)) == "" f_is_empty <- is.na(f) || trimws(as.character(f)) == "" if (v_is_empty && f_is_empty) { return(NA_character_) } v_str <- if (v_is_empty) "" else as.character(v) # Translate precipitation type if (!v_is_empty && base_var == "ropt") { v_str <- switch(v_str, "5" = "Dew / Frost / Rime / Fog", "6" = "Rain / Drizzle", "7" = "Sleet / Snow / Ice Pellets", "8" = "Hail / Ice Grains", "9" = "Mixed Sleet / Hail / Snow", v_str ) } else if (!v_is_empty && base_var == "roop") { v_str <- switch(toupper(v_str), "S" = "Snow", "W" = "Rain / Water", "M" = "Mixed", "L" = "Lód / Ice", v_str ) } else if (!v_is_empty && base_var %in% c("rosw", "Dew Occurrence", "dzbl", "Lightning [0/1]", "dzps", "Snow Cover [0/1]", "Snow Cover")) { if (v_str == "0") { v_str <- "No" } else if (v_str == "1") v_str <- "Yes" } else if (!v_is_empty && base_var %in% c("sgr", "sgrn", "Ground State [code]", "Ground State")) { v_str <- switch(toupper(v_str), "Z" = "Frozen", "R" = "Thawed", v_str ) } else if (!v_is_empty && base_var %in% c("pksn", "Snow Depth [cm]")) { if (v_str == "997") { v_str <- "< 0.5" } else if (v_str == "998") { v_str <- "Patchy" } else if (v_str == "999") v_str <- "Unmeasurable" } else if (!v_is_empty && base_var %in% c("pogb", "pogu", "Current Weather [code]", "Past Weather [code]", "Current Weather", "Past Weather")) { if (exists("WMO_WEATHER_CODES")) { # Remove leading zeros to match the WMO_WEATHER_CODES index format num_key <- as.character(as.numeric(v_str)) if (num_key %in% names(WMO_WEATHER_CODES)) { v_str <- WMO_WEATHER_CODES[[num_key]] } } } if (!f_is_empty) { f_val <- trimws(as.character(f)) if (f_val == "9" && v_is_empty) { # If flag is 9 (no phenomena) and we have no value, insert 0 v_str <- "0" f_html <- "" } else { f_display <- switch(f_val, "8" = "no observations", "9" = "0", paste0("[", f_val, "]") ) # Only show as a superscript/muted flag if it's not "0" if (f_display == "0") { if (v_str == "") v_str <- "0" f_html <- "" } else { f_html <- paste0("", f_display, "") } } } else { f_html <- "" } paste0(v_str, f_html) }, character(1)) df_display[[base_var]] <- formatted_vals df_display[[scol]] <- NULL } } # Fallback translation for precipitation type columns without status flags for (col in c("ropt", "Precip Type 6h")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } switch(trimws(as.character(v)), "5" = "Dew / Frost / Rime / Fog", "6" = "Rain / Drizzle", "7" = "Sleet / Snow / Ice Pellets", "8" = "Hail / Ice Grains", "9" = "Mixed Sleet / Hail / Snow", as.character(v) ) }, character(1)) } } for (col in c("roop", "Precip Type")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } switch(trimws(toupper(as.character(v))), "S" = "Snow", "W" = "Rain / Water", "M" = "Mixed", "L" = "Lód / Ice", as.character(v) ) }, character(1)) } } for (col in c("rosw", "Dew Occurrence", "dzbl", "Lightning [0/1]", "dzps", "Snow Cover [0/1]", "Snow Cover")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } val <- trimws(as.character(v)) if (val == "0") { return("No") } if (val == "1") { return("Yes") } return(val) }, character(1)) } } for (col in c("sgr", "sgrn", "Ground State [code]", "Ground State")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } switch(trimws(toupper(as.character(v))), "Z" = "Frozen", "R" = "Thawed", as.character(v) ) }, character(1)) } } for (col in c("pksn", "Snow Depth [cm]")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } val <- trimws(as.character(v)) if (val == "997") { return("< 0.5") } if (val == "998") { return("Patchy") } if (val == "999") { return("Unmeasurable") } return(val) }, character(1)) } } for (col in c("pogb", "pogu", "Current Weather [code]", "Past Weather [code]", "Current Weather", "Past Weather")) { if (col %in% names(df_display)) { df_display[[col]] <- vapply(df_display[[col]], function(v) { if (is.na(v) || trimws(as.character(v)) == "") { return("") } if (exists("WMO_WEATHER_CODES")) { num_key <- as.character(as.numeric(trimws(as.character(v)))) if (num_key %in% names(WMO_WEATHER_CODES)) { return(WMO_WEATHER_CODES[[num_key]]) } } return(as.character(v)) }, character(1)) } } # Use helper function for renaming df_display <- translate_imgw_columns(df_display) # Remove structural and status clutter from the UI table cols_to_remove <- c( "Station ID", "Station Name", "Year", "Month", "Day", "Hour", "Minute", "station_id", "station_name", "year", "month", "day", "hour", "minute", "Date", "date", "Source", "source", "Source File", "source_file", "Resolution", "resolution", "Source Rank", "source_rank" ) df_display <- df_display[, !names(df_display) %in% cols_to_remove, drop = FALSE] # Hide columns that are ALL NA or empty empty_cols <- names(df_display)[vapply(df_display, function(x) { if (is.character(x)) { all(is.na(x) | x == "") } else { all(is.na(x)) } }, logical(1))] if (length(empty_cols) > 0) { df_display <- df_display[, !names(df_display) %in% empty_cols, drop = FALSE] } # Clean up any leftover status columns status_cols <- grep("^Status:", names(df_display), value = TRUE) if (length(status_cols) > 0) { df_display <- df_display[, !names(df_display) %in% status_cols, drop = FALSE] } list_cols <- vapply(df_display, is.list, logical(1)) if (any(list_cols)) { df_display[list_cols] <- lapply(df_display[list_cols], function(x) { vapply(x, function(v) paste(v, collapse = ", "), character(1)) }) } if ("Date/Time" %in% names(df_display)) { df_display$`Date/Time` <- format(df_display$`Date/Time`, "%Y-%m-%d %H:%M:%S") } # Reorder columns by meteorological relevance col_priority_patterns <- c( "Date/Time", # Temperature "Temp \\[", "Max Temp", "Min Temp", "Mean.*Temp", "Dew Point", "Wet Bulb", # Precipitation "Precip", "Rain Duration", "Snow Duration", "Rain\\+Snow", # Pressure "Station Pressure", "Sea Level Pressure", "Pressure Tend", "Pressure Change", # Humidity "Relative Humidity", "Vapour Pressure", "Humidity Deficit", # Wind "Wind Speed", "Wind Dir", "Wind Gust", "Max Gust", "Gust Hour", "Gust Minute", "Wind >=", # Sunshine / Radiation "Sunshine", "Actinometry", # Snow "Snow Depth", "Snow Water", "Fresh Snow", "Snow Plot", "Snow Type", "Snow Cover", # Clouds / Visibility "Cloud", "Visibility", "Auto Visibility", # Ground / Soil temp "Ground State", "Soil Temp", "Lower Isotherm", "Upper Isotherm", # Ice / misc "Ice Indicator", "Ventilation", # Phenomena durations "Hail", "Fog", "Mist", "Rime", "Glaze", "Blizzard", "Haze", "Thunder", "Dew", "Frost", "Lightning", # Weather codes "Current Weather", "Past Weather", "Precip Type" ) current_cols <- names(df_display) ordered_cols <- character(0) for (pat in col_priority_patterns) { matches <- grep(pat, current_cols, ignore.case = TRUE, value = TRUE) matches <- setdiff(matches, ordered_cols) ordered_cols <- c(ordered_cols, matches) } remaining <- setdiff(current_cols, ordered_cols) df_display <- df_display[, c(ordered_cols, remaining), drop = FALSE] DT::datatable( df_display, escape = FALSE, options = list( pageLength = 25, scrollX = TRUE, dom = "frtip", order = if ("Date/Time" %in% names(df_display)) list(list(which(names(df_display) == "Date/Time") - 1, "desc")) else list(list(0, "asc")) ), rownames = FALSE, class = "compact stripe hover" ) }) output$dashboard_content <- renderUI({ uid <- current_station_uid() if (is.null(uid) || uid == "") { return( tags$div( style = "height: 560px; display: flex; align-items: center; justify-content: center; color: #999;", "Select a station from the map or Stations Info table to load data." ) ) } if (isTRUE(station_fetching())) { return( tags$div( style = "padding: 20px;", uiOutput("dashboard_station_header"), tags$div( class = "alert alert-info d-flex align-items-center gap-2", tags$div(class = "spinner-border spinner-border-sm", role = "status"), tags$span(station_fetch_message()) ) ) ) } err <- station_fetch_error() if (!is.null(err) && nzchar(err)) { return( tags$div( style = "padding: 20px;", uiOutput("dashboard_station_header"), tags$div(class = "alert alert-warning", err) ) ) } df <- station_data() if (is.null(df) || nrow(df) == 0) { return( tags$div( style = "padding: 20px;", uiOutput("dashboard_station_header"), tags$div(class = "alert alert-secondary", "No data loaded for the current selection.") ) ) } tagList( uiOutput("dashboard_station_header"), navset_card_pill( id = "dashboard_subtabs", nav_panel( title = "Plots", uiOutput("dashboard_plots_ui") ), nav_panel( title = "Data", div( style = "margin-top: 10px;", DT::dataTableOutput("station_data_table") ) ) ) ) }) }