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