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