| server <- function(input, output, session) { |
| parse_query_date <- function(value) { |
| if (is.null(value) || !nzchar(value)) { |
| return(NULL) |
| } |
| parsed <- suppressWarnings(as.Date(value)) |
| if (is.na(parsed)) NULL else parsed |
| } |
|
|
| |
| pending_country <- reactiveVal(NULL) |
| pending_station <- reactiveVal(NULL) |
| current_station_id <- reactiveVal(NULL) |
| active_export_id <- reactiveVal(NULL) |
|
|
| |
| fetch_stage <- reactiveVal(0) |
| fetch_retry_count <- reactiveVal(0) |
| current_fetch_token <- reactiveVal(NULL) |
|
|
| full_station_data <- reactiveVal(NULL) |
| station_info <- reactiveVal(NULL) |
| loading_station <- reactiveVal(FALSE) |
| fetch_message <- reactiveVal("Fetching high-resolution hourly data...") |
| fetch_tmp_path <- reactiveVal(NULL) |
| current_station_label <- reactiveVal("") |
| previous_station_choice <- reactiveVal(NULL) |
| previous_station_choices_list <- reactiveVal(NULL) |
|
|
| |
| |
| prev_start_date <- reactiveVal(default_start_date) |
| prev_end_date <- reactiveVal(default_end_date) |
|
|
| |
| fetch_total_size <- reactiveVal(0) |
| fetch_current_pos <- reactiveVal(0) |
|
|
| |
| map_initialized <- reactiveVal(FALSE) |
| stations_loaded <- reactiveVal(FALSE) |
| current_raster_layers <- reactiveVal(character(0)) |
| style_change_trigger <- reactiveVal(0) |
| stations_before_id <- reactiveVal(NULL) |
| basemap_debounced <- shiny::debounce(reactive(input$basemap), 200) |
| filters_initialized <- reactiveVal(FALSE) |
| deep_link_applied <- reactiveVal(FALSE) |
|
|
| |
| |
| |
| |
| |
|
|
| |
| day_tick <- reactiveTimer(3600000) |
|
|
| observe({ |
| day_tick() |
| |
| req(filters_initialized()) |
|
|
| today <- Sys.Date() |
| current_start <- tryCatch(as.Date(input$date_range[1]), error = function(e) today - 1096) |
| current_end <- tryCatch(as.Date(input$date_range[2]), error = function(e) today) |
| if (is.null(current_end) || is.na(current_end)) { |
| return() |
| } |
|
|
| |
| if (current_end >= today) { |
| return() |
| } |
|
|
| |
| |
| |
| days_behind <- as.numeric(difftime(today, current_end, units = "days")) |
| if (days_behind > 2) { |
| return() |
| } |
|
|
| |
| window_days <- as.numeric(difftime(current_end, current_start, units = "days")) |
| new_end <- today |
| new_start <- new_end - window_days |
|
|
| message( |
| "[midnight-rollover] Updating date range: ", |
| current_end, " -> ", new_end, |
| " (start: ", current_start, " -> ", new_start, ")" |
| ) |
|
|
| updateDateRangeInput(session, "date_range", |
| start = new_start, end = new_end, |
| max = today |
| ) |
| }) |
|
|
| |
| initial_lat <- 10 |
| initial_lng <- 5 |
| initial_zoom <- 2 |
|
|
| |
|
|
| |
| session$sendCustomMessage( |
| "freezeUI", |
| list( |
| text = "Loading stations...", |
| station = "" |
| ) |
| ) |
|
|
| |
| observe({ |
| req( |
| !filters_initialized(), |
| !is.null(stations), |
| !is.null(session$clientData$url_search) |
| ) |
|
|
| query <- shiny::parseQueryString(session$clientData$url_search %||% "") |
| country_choices <- sort(unique(na.omit(stations$country_name))) |
| |
| selected_country <- character(0) |
| if (!is.null(query$country) && nzchar(query$country)) { |
| decoded_country <- URLdecode(query$country) |
| c_match <- country_choices[tolower(country_choices) == tolower(decoded_country)] |
| if (length(c_match) > 0) { |
| selected_country <- c_match[1] |
| pending_country(selected_country) |
| } |
| } |
|
|
| start_date <- parse_query_date(query$start) |
| end_date <- parse_query_date(query$end) |
|
|
| |
| if (!is.null(query$station) && nzchar(query$station)) { |
| |
| decoded_station <- URLdecode(query$station) |
|
|
| meta <- stations %>% |
| dplyr::filter(tolower(as.character(.data$ssod_id)) == tolower(decoded_station)) |
| |
| if (nrow(meta) == 0) { |
| meta <- stations %>% |
| dplyr::filter(tolower(.data$name) == tolower(decoded_station)) |
| } |
|
|
| if (!is.null(query$country) && nzchar(query$country)) { |
| decoded_country <- URLdecode(query$country) |
| meta <- meta %>% |
| dplyr::filter(tolower(.data$country_name) == tolower(decoded_country)) |
| } |
|
|
| if (nrow(meta) > 0) { |
| |
| meta <- meta %>% head(1) |
| |
| pending_station(as.character(meta$ssod_id[1])) |
|
|
| if (is.null(end_date)) { |
| end_date <- as.Date(paste0(meta$end_year, "-12-31")) |
| |
| if (!is.na(end_date) && end_date > Sys.Date()) { |
| end_date <- Sys.Date() |
| } |
| } |
|
|
| if (is.null(start_date) && !is.na(end_date)) { |
| start_date <- max( |
| as.Date(paste0(meta$start_year, "-01-01"), na.rm = TRUE), |
| end_date - 1096 |
| ) |
| } |
| } |
| } |
|
|
| |
| |
| today <- Sys.Date() |
| if (is.null(end_date)) { |
| end_date <- today |
| } |
| if (is.null(start_date)) { |
| start_date <- end_date - lubridate::days(1096) |
| } |
|
|
| updateSelectInput( |
| session, |
| "country", |
| choices = country_choices, |
| selected = selected_country |
| ) |
| updateDateRangeInput( |
| session, |
| "date_range", |
| start = start_date, |
| end = end_date, |
| max = today |
| ) |
| |
| view <- query$view |
| if (!is.null(view)) { |
| later::later(function() { |
| if (!session$isClosed()) { |
| shiny::withReactiveDomain(session, { |
| if (identical(view, "station-info")) { |
| updateNavbarPage(session, "main_nav", selected = "Stations Info") |
| } else if (identical(view, "dashboard-plots") || identical(view, "dashboard-data")) { |
| updateNavbarPage(session, "main_nav", selected = "Dashboard") |
| } |
| }) |
| } |
| }, delay = 0.5) |
| } |
|
|
| filters_initialized(TRUE) |
| }) |
|
|
| |
| filtered_stations <- reactive({ |
| if (is.null(stations)) { |
| return(NULL) |
| } |
| data <- stations |
| if (!is.null(input$country) && length(input$country) > 0) { |
| data <- data %>% filter(.data$country_name %in% input$country) |
| } |
| sel_start_yr <- lubridate::year(input$date_range[1]) |
| sel_end_yr <- lubridate::year(input$date_range[2]) |
| data <- data %>% |
| filter( |
| .data$start_year <= sel_end_yr, |
| (.data$end_year >= sel_start_yr | .data$end_year >= max_year_data) |
| ) |
| data %>% filter(!is.na(.data$latitude), !is.na(.data$longitude)) |
| }) |
|
|
| |
| filtered_stations_sf <- reactive({ |
| df <- filtered_stations() |
| req(df) |
| sf::st_as_sf( |
| df, |
| coords = c("longitude", "latitude"), |
| crs = 4326, |
| remove = FALSE |
| ) |
| }) |
|
|
| |
| country_stations <- reactive({ |
| if (is.null(stations)) { |
| return(NULL) |
| } |
| sel_start_yr <- lubridate::year(input$date_range[1]) |
| sel_end_yr <- lubridate::year(input$date_range[2]) |
| data <- stations %>% |
| filter( |
| .data$start_year <= sel_end_yr, |
| (.data$end_year >= sel_start_yr | .data$end_year >= max_year_data) |
| ) |
| if (!is.null(input$country) && length(input$country) > 0) { |
| data <- data %>% filter(.data$country_name %in% input$country) |
| } |
| data |
| }) |
|
|
| |
| observeEvent( |
| input$country, |
| { |
| if (!is.null(pending_country())) { |
| if (isTRUE(input$country == pending_country())) { |
| pending_country(NULL) |
| } else { |
| return() |
| } |
| } |
|
|
| df <- country_stations() |
| if (is.null(df) || nrow(df) == 0) { |
| maplibre_proxy("map") %>% |
| fly_to(center = c(initial_lng, initial_lat), zoom = initial_zoom) |
| return() |
| } |
| if (!is.null(input$country) && length(input$country) > 0) { |
| rng_lat <- range(df$latitude, na.rm = TRUE) |
| rng_lng <- range(df$longitude, na.rm = TRUE) |
| maplibre_proxy("map") %>% |
| fit_bounds( |
| c(rng_lng[1], rng_lat[1], rng_lng[2], rng_lat[2]), |
| animate = TRUE |
| ) |
| } else { |
| maplibre_proxy("map") %>% |
| fly_to(center = c(initial_lng, initial_lat), zoom = initial_zoom) |
| } |
| }, |
| ignoreNULL = FALSE |
| ) |
|
|
| output$station_count_filtered <- renderText({ |
| df <- visible_stations() |
| if (is.null(df)) { |
| return("Loading data...") |
| } |
| paste("Stations showing:", scales::comma(nrow(df))) |
| }) |
|
|
| output$map <- renderMaplibre({ |
| maplibre( |
| style = ofm_positron_style, |
| center = c(initial_lng, initial_lat), |
| zoom = initial_zoom |
| ) %>% |
| add_navigation_control( |
| show_compass = FALSE, |
| visualize_pitch = FALSE, |
| position = "top-left" |
| ) |
| }) |
|
|
| |
| |
| observe({ |
| req(!map_initialized()) |
| req(input$map_zoom) |
| later::later( |
| function() { |
| if (session$isClosed()) { |
| return() |
| } |
| map_initialized(TRUE) |
| }, |
| delay = 0.5 |
| ) |
| }) |
|
|
| observe({ |
| is_loading <- loading_station() |
| inputs_to_toggle <- c( |
| "country", |
| "date_range", |
| "zoom_home", |
| "main_nav", |
| "download_hourly", |
| "download_daily" |
| ) |
|
|
| if (is_loading) { |
| for (inp in inputs_to_toggle) { |
| shinyjs::disable(inp) |
| } |
| } else { |
| for (inp in inputs_to_toggle) { |
| shinyjs::enable(inp) |
| } |
| } |
| }) |
|
|
| |
|
|
| |
| observe({ |
| df <- filtered_stations() |
| req(df) |
|
|
| |
| ids <- as.character(df$ssod_id) |
| names <- paste0(as.character(df$name), " (", ids, ")") |
|
|
| if (length(ids) > 0) { |
| new_choices <- setNames(ids, names) |
| } else { |
| new_choices <- character(0) |
| } |
|
|
| |
| |
| prev_choices <- previous_station_choices_list() |
| |
| new_ids_sorted <- sort(unname(new_choices)) |
| prev_ids_sorted <- if (!is.null(prev_choices)) { |
| sort(unname(prev_choices)) |
| } else { |
| NULL |
| } |
|
|
| if ( |
| is.null(prev_ids_sorted) || !identical(new_ids_sorted, prev_ids_sorted) |
| ) { |
| |
| current_sel <- input$station_selector |
|
|
| |
| sel_arg <- if (is.null(current_sel) || current_sel == "") { |
| character(0) |
| } else { |
| current_sel |
| } |
|
|
| if (!is.null(pending_station())) { |
| if (pending_station() %in% ids) { |
| sel_arg <- pending_station() |
| } |
| } |
|
|
| updateSelectizeInput( |
| session, |
| "station_selector", |
| choices = new_choices, |
| selected = sel_arg, |
| server = FALSE |
| ) |
| previous_station_choices_list(new_choices) |
| } |
| }) |
|
|
|
|
| |
| observeEvent(input$station_selector, { |
| req(input$station_selector) |
| id_val <- input$station_selector |
|
|
| if (!is.null(pending_station())) { |
| if (isTRUE(id_val == pending_station())) { |
| pending_station(NULL) |
| } else { |
| id_val <- pending_station() |
| } |
| } |
|
|
| |
| prev <- previous_station_choice() |
| if (!is.null(prev) && prev == id_val) { |
| return() |
| } |
|
|
| |
| curr <- current_station_id() |
| if (!is.null(curr) && curr == id_val) { |
| return() |
| } |
|
|
| |
| meta <- stations %>% dplyr::filter(.data$ssod_id == id_val) |
| if (nrow(meta) > 0) { |
| select_station(id_val) |
| maplibre_proxy("map") %>% |
| fly_to(center = c(meta$longitude[1], meta$latitude[1]), zoom = 9) |
|
|
| previous_station_choice(id_val) |
| } |
| }) |
|
|
| |
| observeEvent(input$zoom_home, { |
| df <- filtered_stations() |
| req(df) |
| if (nrow(df) > 0) { |
| |
| lons <- range(df$longitude, na.rm = TRUE) |
| lats <- range(df$latitude, na.rm = TRUE) |
|
|
| |
| if (nrow(df) == 1) { |
| maplibre_proxy("map") %>% |
| fly_to(center = c(df$longitude[1], df$latitude[1]), zoom = 12) |
| } else { |
| maplibre_proxy("map") %>% |
| fit_bounds(c(lons[1], lats[1], lons[2], lats[2]), animate = TRUE) |
| } |
| } else { |
| |
| maplibre_proxy("map") %>% |
| fly_to(center = c(initial_lng, initial_lat), zoom = initial_zoom) |
| } |
| }) |
|
|
| |
| |
| observeEvent(c(input$country, input$station_selector), { |
| c_val <- input$country |
| s_val <- input$station_selector |
| |
| country_out <- if (!is.null(c_val) && length(c_val) > 0 && nzchar(c_val[1])) c_val[1] else NULL |
| station_out <- if (!is.null(s_val) && nzchar(s_val)) s_val else NULL |
| |
| st_name_out <- NULL |
| if (!is.null(station_out) && !is.null(stations)) { |
| meta <- stations %>% dplyr::filter(.data$ssod_id == station_out) %>% head(1) |
| if (nrow(meta) > 0) { |
| st_name_out <- as.character(meta$name[1]) |
| } |
| } |
|
|
| if (!is.null(pending_country())) { |
| country_out <- pending_country() |
| } |
| if (!is.null(pending_station())) { |
| station_out <- pending_station() |
| if (!is.null(stations)) { |
| meta <- stations %>% dplyr::filter(.data$ssod_id == station_out) %>% head(1) |
| if (nrow(meta) > 0) st_name_out <- as.character(meta$name[1]) |
| } |
| } |
|
|
| session$sendCustomMessage("ssod-state-update", list( |
| type = "ssod-state-update", |
| country = country_out, |
| station = station_out, |
| stationName = st_name_out |
| )) |
| }, ignoreInit = FALSE, ignoreNULL = FALSE) |
|
|
| |
| broadcast_state <- function(view_override = NULL) { |
| |
| sid <- isolate(current_station_id()) |
| st_meta <- NULL |
| if (!is.null(sid)) { |
| st_meta <- stations %>% |
| dplyr::filter(.data$ssod_id == sid) %>% |
| head(1) |
| } |
|
|
| station_id <- if (!is.null(st_meta) && nrow(st_meta) > 0) { |
| as.character(st_meta$ssod_id) |
| } else { |
| NULL |
| } |
| station_name <- if (!is.null(st_meta) && nrow(st_meta) > 0) { |
| as.character(st_meta$name) |
| } else { |
| NULL |
| } |
| country <- if (!is.null(st_meta) && nrow(st_meta) > 0) { |
| as.character(st_meta$country_name) |
| } else { |
| NULL |
| } |
|
|
| |
| 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" |
| } |
|
|
| start_date <- if (!is.null(isolate(input$date_range))) { |
| as.character(isolate(input$date_range)[1]) |
| } else { |
| NULL |
| } |
| end_date <- if (!is.null(isolate(input$date_range))) { |
| as.character(isolate(input$date_range)[2]) |
| } else { |
| NULL |
| } |
|
|
| session$sendCustomMessage( |
| "updateParentURL", |
| list( |
| station = station_id, |
| stationName = station_name, |
| country = country, |
| view = view, |
| start = start_date, |
| end = end_date |
| ) |
| ) |
| } |
|
|
| |
| observeEvent(input$main_nav, { |
| if (input$main_nav == "Map View") { |
| |
| shinyjs::runjs( |
| " |
| setTimeout(function() { |
| var map = HTMLWidgets.find('#map'); |
| if (map) { |
| // mapgl uses resize() which is roughly equivalent to invalidateSize() |
| map.getMap().resize(); |
| } |
| }, 200); |
| " |
| ) |
| } |
| |
| broadcast_state() |
| }) |
|
|
| |
| observeEvent( |
| input$dashboard_subtabs, |
| { |
| broadcast_state() |
| }, |
| ignoreInit = TRUE |
| ) |
|
|
| |
| label_layer_ids <- c( |
| |
| "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", |
| |
| "road_oneway", |
| "road_oneway_opposite", |
| "poi_r20", |
| "poi_r7", |
| "poi_r1", |
| "poi_transit", |
| |
| "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", |
| |
| "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" |
| ) |
|
|
| 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) {} |
| ) |
| } |
| } |
|
|
| observeEvent(basemap_debounced(), { |
| basemap <- basemap_debounced() |
| proxy <- maplibre_proxy("map") |
|
|
| if (basemap %in% c("ofm_positron", "ofm_bright")) { |
| style_url <- if (basemap == "ofm_positron") { |
| ofm_positron_style |
| } else { |
| ofm_bright_style |
| } |
| proxy %>% set_style(style_url, preserve_layers = FALSE) |
| stations_before_id("waterway_line_label") |
|
|
| current_session <- shiny::getDefaultReactiveDomain() |
| selected_basemap <- basemap |
|
|
| later::later( |
| function() { |
| if (current_session$isClosed()) { |
| return() |
| } |
| shiny::withReactiveDomain(current_session, { |
| current_basemap <- isolate(input$basemap) |
| if (current_basemap != selected_basemap) { |
| return() |
| } |
| apply_label_visibility( |
| maplibre_proxy("map"), |
| isolate(input$show_labels) |
| ) |
| style_change_trigger(isolate(style_change_trigger()) + 1) |
| }) |
| }, |
| delay = 0.35 |
| ) |
| } else if (basemap == "sentinel") { |
| proxy %>% set_style(ofm_positron_style, preserve_layers = FALSE) |
|
|
| current_session <- shiny::getDefaultReactiveDomain() |
| selected_basemap <- basemap |
|
|
| later::later( |
| function() { |
| if (current_session$isClosed()) { |
| return() |
| } |
| 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) |
|
|
| maplibre_proxy("map") %>% |
| add_raster_source( |
| id = source_id, |
| tiles = c(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" |
| ) |
|
|
| for (layer_id_kill in non_label_layer_ids) { |
| tryCatch( |
| { |
| maplibre_proxy("map") %>% |
| set_layout_property(layer_id_kill, "visibility", "none") |
| }, |
| error = function(e) {} |
| ) |
| } |
|
|
| apply_label_visibility( |
| maplibre_proxy("map"), |
| isolate(input$show_labels) |
| ) |
| stations_before_id("waterway_line_label") |
| style_change_trigger(isolate(style_change_trigger()) + 1) |
| }) |
| }, |
| delay = 0.5 |
| ) |
| } |
| }, ignoreInit = TRUE) |
|
|
| observeEvent( |
| input$show_labels, |
| { |
| apply_label_visibility(maplibre_proxy("map"), input$show_labels) |
| }, |
| ignoreInit = TRUE |
| ) |
|
|
| |
| observe({ |
| df <- filtered_stations_sf() |
| req(df, map_initialized()) |
| style_change_trigger() |
|
|
| |
|
|
| proxy <- maplibre_proxy("map") |
| before <- stations_before_id() |
|
|
| proxy %>% |
| clear_layer("stations_layer") %>% |
| add_circle_layer( |
| id = "stations_layer", |
| source = df, |
| circle_color = "navy", |
| circle_radius = 6, |
| circle_opacity = 0.7, |
| circle_stroke_width = 0, |
| before_id = before, |
| tooltip = "popup_content" |
| ) |
|
|
| |
| sel_id <- isolate(current_station_id()) |
| if (!is.null(sel_id)) { |
| sel_meta <- stations %>% dplyr::filter(.data$ssod_id == sel_id) |
| if (nrow(sel_meta) > 0) { |
| highlight_selected_station(proxy, sel_meta[1, ]) |
| } |
| } |
|
|
| |
| if (!isolate(stations_loaded())) { |
| stations_loaded(TRUE) |
| nav <- isolate(input$main_nav) |
| is_map_active <- is.null(nav) || nav == "Map" |
| |
| if (is_map_active) { |
| session$sendCustomMessage("unfreezeWhenMapIdle", list()) |
| } else { |
| session$sendCustomMessage("unfreezeUI", list()) |
| } |
| } |
| }) |
|
|
| |
|
|
| observeEvent( |
| input$date_range, |
| { |
| req(input$date_range) |
| start <- input$date_range[1] |
| end <- input$date_range[2] |
|
|
| |
| if (is.na(start) || is.na(end)) { |
| return() |
| } |
|
|
| start_changed <- !identical(start, prev_start_date()) |
| end_changed <- !identical(end, prev_end_date()) |
|
|
| if (!start_changed && !end_changed) { |
| return() |
| } |
|
|
| |
| diff_days <- as.numeric(difftime(end, start, units = "days")) |
|
|
| |
| if (diff_days > 1096) { |
| if (start_changed && !end_changed) { |
| |
| new_end <- start + 1096 |
| updateDateRangeInput( |
| session, |
| "date_range", |
| start = start, |
| end = new_end |
| ) |
| showNotification( |
| "Date range cannot exceed 1096 days. Adjusting end date.", |
| type = "warning", |
| duration = 4 |
| ) |
| } else if (end_changed && !start_changed) { |
| |
| new_start <- end - 1096 |
| updateDateRangeInput( |
| session, |
| "date_range", |
| start = new_start, |
| end = end |
| ) |
| showNotification( |
| "Date range cannot exceed 1096 days. Adjusting start date.", |
| type = "warning", |
| duration = 4 |
| ) |
| } else { |
| |
| new_end <- start + 1096 |
| updateDateRangeInput( |
| session, |
| "date_range", |
| start = start, |
| end = new_end |
| ) |
| showNotification( |
| "Date range cannot exceed 1096 days. Adjusting end date.", |
| type = "warning", |
| duration = 4 |
| ) |
| } |
|
|
| |
| msg <- "Date range limited to 1096 days. Updating..." |
| session$sendCustomMessage( |
| "freezeUI", |
| list( |
| text = msg, |
| station = current_station_label() |
| ) |
| ) |
| } else { |
| |
| prev_start_date(start) |
| prev_end_date(end) |
|
|
| |
| |
| if (!is.null(current_station_id())) { |
| select_station(current_station_id()) |
| return() |
| } |
| } |
|
|
| |
| session$sendCustomMessage( |
| "freezeUI", |
| list(text = "Rendering plots...", allowCancel = TRUE) |
| ) |
| session$onFlushed( |
| function() { |
| session$sendCustomMessage("waitForPlots", list()) |
| }, |
| once = TRUE |
| ) |
| }, |
| ignoreInit = TRUE |
| ) |
|
|
| |
| full_station_data <- reactiveVal(NULL) |
|
|
| station_data <- reactive({ |
| df <- full_station_data() |
| req(df, input$date_range) |
|
|
| |
| |
| diff_days <- as.numeric(difftime( |
| input$date_range[2], |
| input$date_range[1], |
| units = "days" |
| )) |
| req(diff_days <= 1096) |
|
|
| |
| start_dt <- lubridate::as_datetime(input$date_range[1]) |
| end_dt <- lubridate::as_datetime(input$date_range[2]) + |
| lubridate::hours(23) + |
| lubridate::minutes(59) |
|
|
| if (length(start_dt) == 0 || length(end_dt) == 0) { |
| return(df) |
| } |
|
|
| df %>% |
| dplyr::filter(.data$datetime >= start_dt, .data$datetime <= end_dt) %>% |
| dplyr::arrange(.data$datetime) |
| }) |
|
|
| |
|
|
| reset_fetch <- function(msg = NULL) { |
| if (!is.null(msg)) { |
| message("reset_fetch called with message: ", msg) |
| |
| if (msg != "Cancelled by user") { |
| showNotification(msg, type = "error", duration = NULL) |
| } |
| } |
|
|
| |
| current_fetch_token(as.numeric(Sys.time())) |
|
|
| loading_station(FALSE) |
| fetch_stage(0) |
| fetch_retry_count(0) |
| fetch_total_size(0) |
| fetch_current_pos(0) |
| if (!is.null(msg)) { |
| station_info(list(name = msg, id = current_station_id())) |
| } |
| tmp <- fetch_tmp_path() |
| if (!is.null(tmp) && file.exists(tmp)) { |
| unlink(tmp) |
| } |
| fetch_tmp_path(NULL) |
| |
| session$sendCustomMessage("unfreezeUI", list()) |
| } |
|
|
| |
| observeEvent(input$cancel_loading, { |
| reset_fetch("Cancelled by user") |
| showNotification("Loading cancelled by user.", type = "warning") |
| }) |
|
|
| |
| select_station <- function(id) { |
| req(id) |
|
|
| |
| meta <- stations %>% dplyr::filter(.data$ssod_id == id) |
| if (nrow(meta) > 0) { |
| s_name <- meta$name |
| s_country <- ifelse( |
| is.na(meta$country_name), |
| "Unknown", |
| meta$country_name |
| ) |
| current_station_label(paste0(s_name, ", ", s_country)) |
| } else { |
| current_station_label(paste("Station", id)) |
| } |
|
|
| full_station_data(NULL) |
| loading_station(TRUE) |
| fetch_message("Fetching high-resolution hourly data...") |
| |
| session$sendCustomMessage( |
| "freezeUI", |
| list( |
| text = "Downloading station data...", |
| station = current_station_label() |
| ) |
| ) |
| fetch_total_size(0) |
| fetch_current_pos(0) |
|
|
| |
| new_token <- as.numeric(Sys.time()) |
| current_fetch_token(new_token) |
| current_station_id(id) |
| message("Station selected: station=", id, " token=", new_token) |
|
|
| active_export_id(id) |
|
|
| if (nrow(meta) == 0) { |
| loading_station(FALSE) |
| session$sendCustomMessage("unfreezeUI", list()) |
| return() |
| } |
| station_info(list(name = meta$name, id = meta$ssod_id)) |
|
|
| |
| fetch_stage(3) |
|
|
| |
| highlight_selected_station(maplibre_proxy("map"), meta) |
|
|
| |
| broadcast_state() |
| } |
|
|
| |
| observeEvent(input$map_feature_click, { |
| clicked_data <- input$map_feature_click |
| if (is.null(clicked_data)) { |
| return() |
| } |
|
|
| |
| layer_id <- clicked_data$layer_id %||% clicked_data$layer |
|
|
| |
| if (isTRUE(layer_id == "selected_highlight")) { |
| if (!is.null(full_station_data())) { |
| updateNavbarPage(session, "main_nav", selected = "Dashboard") |
| } |
| return() |
| } |
|
|
| |
| if (!isTRUE(layer_id == "stations_layer")) { |
| return() |
| } |
|
|
| |
| id <- clicked_data$properties$ssod_id |
| if (is.null(id)) { |
| return() |
| } |
|
|
| select_station(id) |
|
|
| |
| meta <- stations %>% |
| dplyr::filter(.data$ssod_id == id) %>% |
| head(1) |
|
|
| if (nrow(meta) > 0) { |
| maplibre_proxy("map") %>% |
| fly_to(center = c(meta$longitude, meta$latitude), zoom = 9) |
| } |
|
|
| |
| updateSelectizeInput(session, "station_selector", selected = id) |
| previous_station_choice(id) |
| }) |
|
|
| |
| output$table <- DT::renderDataTable({ |
| df <- visible_stations() |
| if (is.null(df)) { |
| return(NULL) |
| } |
| df %>% |
| dplyr::select( |
| .data$ssod_id, |
| .data$name, |
| .data$country_name, |
| .data$start_year, |
| .data$end_year, |
| .data$elevation, |
| .data$state |
| ) %>% |
| DT::datatable( |
| options = list( |
| pageLength = 25, |
| columnDefs = list( |
| list( |
| targets = c(0, 5, 6), |
| responsivePriority = 10000 |
| ) |
| ), |
| responsive = TRUE |
| ), |
| rownames = FALSE, |
| selection = "none", |
| colnames = c( |
| "ID", |
| "Station Name", |
| "Country", |
| "Start Year", |
| "End Year", |
| "Elevation (m)", |
| "State/Prov" |
| ), |
| 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.collapseSidebarOnMobile) window.collapseSidebarOnMobile(); |
| } |
| }); |
| table.on('click', 'tr', function() { |
| if (window.innerWidth <= 768) { |
| 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.collapseSidebarOnMobile) window.collapseSidebarOnMobile(); |
| } |
| } |
| }); |
| " |
| ) |
| ) |
| }) |
|
|
| observeEvent(input$table_station_dblclick, { |
| id_val <- input$table_station_dblclick |
| req(id_val) |
|
|
| |
| meta <- stations %>% dplyr::filter(.data$ssod_id == id_val) |
| if (nrow(meta) == 0) { |
| return() |
| } |
|
|
| select_station(id_val) |
|
|
| |
| maplibre_proxy("map") %>% |
| fly_to(center = c(meta$longitude[1], meta$latitude[1]), zoom = 9) |
|
|
| |
| updateSelectizeInput(session, "station_selector", selected = id_val) |
| previous_station_choice(id_val) |
| }) |
|
|
| |
| output$panel_station_name <- renderText({ |
| id <- current_station_id() |
| if (is.null(id)) { |
| return("") |
| } |
| meta <- stations %>% |
| filter(.data$ssod_id == id) %>% |
| first() |
| if (is.null(meta)) { |
| return("") |
| } |
| country <- ifelse( |
| is.na(meta$country_name) || meta$country_name == "", |
| "Unknown", |
| meta$country_name |
| ) |
| paste0(meta$name, ", ", country, " (", id, ")") |
| }) |
|
|
| output$panel_station_meta <- renderText({ |
| id <- current_station_id() |
| if (is.null(id)) { |
| return("") |
| } |
| meta <- stations %>% |
| filter(.data$ssod_id == id) %>% |
| first() |
| if (is.null(meta)) { |
| return("") |
| } |
| period <- ifelse( |
| is.na(meta$start_year), |
| "Unknown", |
| paste0(meta$start_year, " - ", meta$end_year) |
| ) |
| paste0("Period: ", period, " | Elev: ", meta$elevation, "m") |
| }) |
|
|
| |
|
|
| |
| years_to_fetch <- reactiveVal(numeric(0)) |
| fetched_data <- reactiveVal(list()) |
| current_year_idx <- reactiveVal(1) |
|
|
| |
| observe({ |
| req(fetch_stage() == 3) |
| station_id <- current_station_id() |
| if (is.null(station_id)) { |
| reset_fetch() |
| return() |
| } |
|
|
| current_start <- isolate(input$date_range[1]) |
| current_end <- isolate(input$date_range[2]) |
|
|
| if (is.null(current_start) || is.null(current_end)) { |
| reset_fetch("Invalid date range.") |
| return() |
| } |
|
|
| start_yr <- as.integer(format(as.Date(current_start), "%Y")) |
| end_yr <- as.integer(format(as.Date(current_end), "%Y")) |
| years <- seq(start_yr, end_yr) |
|
|
| years_to_fetch(years) |
| fetched_data(list()) |
| current_year_idx(1) |
| fetch_retry_count(0) |
|
|
| fetch_message(paste("Checking availability for", length(years), "years...")) |
|
|
| this_token <- current_fetch_token() |
| later::later( |
| function() { |
| if (session$isClosed()) { |
| return() |
| } |
| if (identical(isolate(current_fetch_token()), this_token)) { |
| fetch_stage(4) |
| } |
| }, |
| 0.1 |
| ) |
| fetch_stage(-1) |
| }) |
|
|
| |
| observe({ |
| req(fetch_stage() == 4) |
| station_id <- current_station_id() |
| if (is.null(station_id)) { |
| reset_fetch() |
| return() |
| } |
|
|
| idx <- current_year_idx() |
| years <- years_to_fetch() |
|
|
| if (idx > length(years)) { |
| |
| fetch_stage(6) |
| return() |
| } |
|
|
| year <- years[idx] |
| fetch_message(paste0("Fetching data for ", year, " (", idx, "/", length(years), ")...")) |
| session$sendCustomMessage("freezeUI", list(text = paste0("Fetching data for ", year, " (", idx, "/", length(years), ")..."), station = current_station_label())) |
|
|
| res <- fetch_ssod_year_mem(station_id, year) |
|
|
| if (!is.null(res$error)) { |
| if (res$error == "transient") { |
| if (fetch_retry_count() < 3) { |
| fetch_retry_count(fetch_retry_count() + 1) |
| fetch_message(paste("Network drop. Retrying year", year, "- Attempt", fetch_retry_count())) |
| this_token <- current_fetch_token() |
| later::later(function() { |
| if (session$isClosed()) { |
| return() |
| } |
| if (identical(isolate(current_fetch_token()), this_token)) { |
| fetch_stage(4) |
| } |
| }, 1.0) |
| fetch_stage(-1) |
| return() |
| } else { |
| reset_fetch(paste("Connection failed after retries for year", year)) |
| return() |
| } |
| } else { |
| reset_fetch(paste("Fatal error fetching year", year, ":", res$message)) |
| return() |
| } |
| } |
|
|
| if (res$status == 200) { |
| |
| tmp <- tempfile(fileext = ".csv") |
| writeBin(res$content, tmp) |
|
|
| tryCatch( |
| { |
| df <- parse_ssod_data(tmp) |
| if (!is.null(df) && nrow(df) > 0) { |
| cur_data <- fetched_data() |
| cur_data[[as.character(year)]] <- df |
| fetched_data(cur_data) |
| } |
| }, |
| error = function(e) { |
| |
| } |
| ) |
| unlink(tmp) |
| } |
|
|
| |
| fetch_retry_count(0) |
| current_year_idx(idx + 1) |
|
|
| this_token <- current_fetch_token() |
| later::later(function() { |
| if (session$isClosed()) { |
| return() |
| } |
| if (identical(isolate(current_fetch_token()), this_token)) { |
| fetch_stage(4) |
| } |
| }, 0.1) |
| fetch_stage(-1) |
| }) |
|
|
| |
| observe({ |
| req(fetch_stage() == 6) |
| dfs <- fetched_data() |
|
|
| if (length(dfs) > 0) { |
| df_combined <- bind_rows(dfs) %>% arrange(datetime) |
| full_station_data(df_combined) |
|
|
| |
| current_start <- isolate(input$date_range[1]) |
| current_end <- isolate(input$date_range[2]) |
| if (!is.null(current_start) && !is.null(current_end)) { |
| actual_max_date <- as.Date(max(df_combined$datetime, na.rm = TRUE)) |
| actual_min_date <- as.Date(min(df_combined$datetime, na.rm = TRUE)) |
|
|
| if (as.Date(current_start) > actual_max_date || as.Date(current_end) < actual_min_date) { |
| new_end <- actual_max_date |
| new_start <- max(actual_min_date, new_end - 1096) |
| updateDateRangeInput(session, "date_range", start = new_start, end = new_end) |
| prev_start_date(new_start) |
| prev_end_date(new_end) |
| } |
| } |
|
|
| mem_size <- format(object.size(df_combined), units = "Mb") |
| final_msg <- paste0("Processing Complete!<br><span style='font-size: 0.9em; color: #555;'>In-Memory Data: <b>", mem_size, "</b></span>") |
| fetch_message(final_msg) |
|
|
| loading_station(FALSE) |
| fetch_stage(0) |
|
|
| this_token <- current_fetch_token() |
| session$sendCustomMessage("freezeUI", list(text = "Rendering plots...", station = current_station_label())) |
| updateNavbarPage(session, "main_nav", selected = "Dashboard") |
|
|
| session$sendCustomMessage("freezeUI", list(text = "Rendering plots...", allowCancel = TRUE)) |
| session$onFlushed(function() { |
| if (identical(isolate(current_fetch_token()), this_token)) { |
| session$sendCustomMessage("waitForPlots", list()) |
| } |
| }, once = TRUE) |
| } else { |
| reset_fetch("No valid records found in the selected date range.") |
| } |
| fetch_stage(-1) |
| }) |
|
|
| output$station_ready <- reactive({ |
| !is.null(full_station_data()) |
| }) |
| outputOptions(output, "station_ready", suspendWhenHidden = FALSE) |
| output$is_loading <- reactive({ |
| loading_station() |
| }) |
| outputOptions(output, "is_loading", suspendWhenHidden = FALSE) |
|
|
| |
|
|
| output$temp_plot <- renderPlotly({ |
| df <- station_data() |
| create_temperature_plot(df) |
| }) |
|
|
| output$humidity_plot <- renderPlotly({ |
| df <- station_data() |
| create_humidity_plot(df) |
| }) |
|
|
| output$wind_overview_plot <- renderPlotly({ |
| df <- station_data() |
| create_wind_overview_plot(df) |
| }) |
|
|
| output$pressure_plot <- renderPlotly({ |
| df <- station_data() |
| create_pressure_plot(df) |
| }) |
|
|
| output$visibility_plot <- renderPlotly({ |
| df <- station_data() |
| create_visibility_plot(df) |
| }) |
|
|
| output$precip_plot <- renderPlotly({ |
| df <- station_data() |
| create_precipitation_plot(df) |
| }) |
|
|
| output$wind_rose <- renderPlotly({ |
| df <- station_data() |
| create_wind_rose_plot(df) |
| }) |
|
|
|
|
| |
| output$station_info_header <- renderUI({ |
| id <- current_station_id() |
| if (is.null(id)) { |
| return(NULL) |
| } |
|
|
| |
| meta <- stations %>% |
| dplyr::filter(.data$ssod_id == id) %>% |
| dplyr::first() |
| if (is.null(meta)) { |
| return(NULL) |
| } |
|
|
| s_name <- meta$name |
| s_country <- ifelse( |
| is.na(meta$country_name) || meta$country_name == "", |
| "Unknown", |
| meta$country_name |
| ) |
| s_elev <- meta$elevation |
|
|
| |
| df <- station_data() |
| completeness_text <- "" |
| if (is.null(df) || nrow(df) == 0) { |
| dates_text <- "No data loaded" |
| } else { |
| date_range <- range(as.Date(df$datetime), na.rm = TRUE) |
| dates_text <- paste(date_range[1], "to", date_range[2]) |
|
|
| completeness_text <- calculate_data_completeness(df, input$date_range) |
| } |
|
|
| |
| meta_start <- ifelse(is.null(meta$start_year) || is.na(meta$start_year), "?", meta$start_year) |
| meta_end <- ifelse(is.null(meta$end_year) || is.na(meta$end_year), "?", meta$end_year) |
| avail_text <- paste(meta_start, "to", meta_end) |
|
|
| |
| card( |
| style = "margin-bottom: 20px; border-left: 5px solid #007bff;", |
| card_body( |
| padding = 15, |
| layout_columns( |
| fill = FALSE, |
| |
| div( |
| strong("Station"), |
| br(), |
| span(s_name, style = "font-size: 1.1rem;"), |
| br(), |
| tags$small(class = "text-muted", paste("ID:", id)) |
| ), |
| |
| div( |
| strong("Location"), |
| br(), |
| span(s_country), |
| br(), |
| tags$small( |
| class = "text-muted", |
| paste0(meta$latitude, "°N, ", meta$longitude, "°E") |
| ) |
| ), |
| |
| div( |
| strong("Technical"), |
| br(), |
| span(paste0(s_elev, " m")), |
| br(), |
| span(class = "badge bg-primary", "Hourly") |
| ), |
| |
| div( |
| strong("Station Availability"), |
| br(), |
| span(avail_text), |
| br(), |
| tags$small(class = "text-muted", paste("Viewing:", dates_text)), |
| if (completeness_text != "") br() else NULL, |
| if (completeness_text != "") tags$small(class = "text-success fw-bold", completeness_text) else NULL |
| ) |
| ) |
| ) |
| ) |
| }) |
|
|
| |
| |
| output$details_tabs <- renderUI({ |
| df <- station_data() |
| req(df) |
|
|
| |
| t_vals <- if ("temp" %in% names(df)) suppressWarnings(as.numeric(df$temp)) else rep(NA_real_, nrow(df)) |
| tmin_vals <- if ("temp_min" %in% names(df)) suppressWarnings(as.numeric(df$temp_min)) else rep(NA_real_, nrow(df)) |
| tmax_vals <- if ("temp_max" %in% names(df)) suppressWarnings(as.numeric(df$temp_max)) else rep(NA_real_, nrow(df)) |
|
|
| t_vals[t_vals < -90 | t_vals > 60] <- NA_real_ |
| tmin_vals[tmin_vals < -90 | tmin_vals > 60] <- NA_real_ |
| tmax_vals[tmax_vals < -90 | tmax_vals > 60] <- NA_real_ |
|
|
| invalid_rel <- !is.na(tmin_vals) & !is.na(tmax_vals) & (tmax_vals < tmin_vals) |
| tmin_vals[invalid_rel] <- NA_real_ |
| tmax_vals[invalid_rel] <- NA_real_ |
|
|
| has_temp <- any(!is.na(t_vals)) |
| has_tmin <- any(!is.na(tmin_vals)) |
| has_tmax <- any(!is.na(tmax_vals)) |
| has_humidity <- ("rh" %in% names(df) && any(!is.na(df$rh))) || |
| ("dew_point" %in% names(df) && any(!is.na(df$dew_point))) |
| has_wind <- ("wind_speed" %in% names(df) && any(!is.na(df$wind_speed))) || |
| ("wind_gust" %in% names(df) && any(!is.na(df$wind_gust))) |
| has_wind_rose <- has_wind && |
| "wind_dir" %in% names(df) && |
| any(!is.na(df$wind_dir)) |
| has_pressure <- ("pressure" %in% names(df) && any(!is.na(df$pressure))) || |
| ("station_pressure" %in% names(df) && any(!is.na(df$station_pressure))) |
| has_vis <- "vis" %in% names(df) && any(!is.na(df$vis)) |
|
|
| |
| precip_numeric_names <- c( |
| "precip", "precip_3h", "precip_6h", "precip_9h", |
| "precip_12h", "precip_15h", "precip_18h", |
| "precip_21h", "precip_24h" |
| ) |
| precip_cols <- intersect(precip_numeric_names, names(df)) |
| has_precip <- length(precip_cols) > 0 && |
| any(sapply(df[precip_cols], function(x) any(!is.na(x)))) |
|
|
|
|
| |
| format_ext_time <- function(idx) { |
| if (length(idx) == 0 || is.na(idx)) { |
| return("") |
| } |
| dt <- df$datetime[idx] |
| format(dt, "%d %b %H:%M") |
| } |
|
|
| format_val <- function(val, suffix = "", digits = 1, divisor = 1) { |
| if (is.null(val) || length(val) == 0 || is.na(val) || is.infinite(val)) { |
| return("—") |
| } |
| paste0(round(val / divisor, digits), suffix) |
| } |
|
|
| 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)) |
| ) |
| )) |
| } |
|
|
| if (has_temp || has_tmin || has_tmax) { |
| avg_t <- if (has_temp) mean(t_vals, na.rm = TRUE) else NA_real_ |
| |
| if (has_tmin) { |
| min_t <- min(tmin_vals, na.rm = TRUE) |
| min_idx <- which.min(tmin_vals) |
| } else if (has_temp) { |
| min_t <- min(t_vals, na.rm = TRUE) |
| min_idx <- which.min(t_vals) |
| } else { |
| min_t <- NA_real_ |
| min_idx <- NA_integer_ |
| } |
|
|
| if (has_tmax) { |
| max_t <- max(tmax_vals, na.rm = TRUE) |
| max_idx <- which.max(tmax_vals) |
| } else if (has_temp) { |
| max_t <- max(t_vals, na.rm = TRUE) |
| max_idx <- which.max(t_vals) |
| } else { |
| max_t <- NA_real_ |
| max_idx <- NA_integer_ |
| } |
| |
| valid_idx <- which(!is.na(t_vals) | !is.na(tmax_vals) | !is.na(tmin_vals)) |
| last_valid_idx <- tail(valid_idx, 1) |
| |
| if (length(last_valid_idx) > 0) { |
| if (!is.na(t_vals[last_valid_idx])) { |
| latest <- t_vals[last_valid_idx] |
| } else if (!is.na(tmax_vals[last_valid_idx])) { |
| latest <- tmax_vals[last_valid_idx] |
| } else { |
| latest <- tmin_vals[last_valid_idx] |
| } |
| } else { |
| latest <- NA_real_ |
| } |
|
|
| 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" |
| ) |
| } |
|
|
| if (has_wind) { |
| w_col <- if ("wind_speed" %in% names(df) && any(!is.na(df$wind_speed))) "wind_speed" else "wind_gust" |
| if (!is.null(w_col) && any(!is.na(df[[w_col]]))) { |
| w_vals <- as.numeric(df[[w_col]]) |
| w_vals[w_vals < 0] <- NA |
| latest <- tail(na.omit(w_vals), 1) |
| avg_w <- mean(w_vals, na.rm = TRUE) |
| max_col <- if ("wind_gust" %in% names(df) && any(!is.na(df$wind_gust))) "wind_gust" else w_col |
| max_vals <- as.numeric(df[[max_col]]) |
| max_vals[max_vals < 0] <- NA |
| max_w <- max(max_vals, na.rm = TRUE) |
| max_idx <- which.max(max_vals) |
|
|
| add_card( |
| "Wind Speed", format_val(latest, " m/s"), |
| sprintf("Avg: %s | Max Gust: %s (%s)", format_val(avg_w, " m/s"), format_val(max_w, " m/s"), format_ext_time(max_idx)), |
| "wind", "card-emerald" |
| ) |
| } |
| } |
|
|
| if (has_humidity) { |
| if ("rh" %in% names(df) && any(!is.na(df$rh))) { |
| h_col <- "rh" |
| h_vals <- as.numeric(df[[h_col]]) |
| h_vals[h_vals < 0] <- NA |
| latest <- tail(na.omit(h_vals), 1) |
| avg_h <- mean(h_vals, na.rm = TRUE) |
| min_h <- min(h_vals, na.rm = TRUE) |
| min_idx <- which.min(h_vals) |
|
|
| add_card( |
| "Relative Humidity", format_val(latest, "%", 0), |
| sprintf("Avg: %s | Min: %s (%s)", format_val(avg_h, "%", 0), format_val(min_h, "%", 0), format_ext_time(min_idx)), |
| "droplet", "card-cyan" |
| ) |
| } else if ("dew_point" %in% names(df) && any(!is.na(df$dew_point))) { |
| d_col <- "dew_point" |
| d_vals <- as.numeric(df[[d_col]]) |
| |
| latest <- tail(na.omit(d_vals), 1) |
| avg_d <- mean(d_vals, na.rm = TRUE) |
| min_d <- min(d_vals, na.rm = TRUE) |
| min_idx <- which.min(d_vals) |
|
|
| add_card( |
| "Dew Point", format_val(latest, "°C"), |
| sprintf("Avg: %s | Min: %s (%s)", format_val(avg_d, "°C"), format_val(min_d, "°C"), format_ext_time(min_idx)), |
| "droplet", "card-cyan" |
| ) |
| } |
| } |
|
|
| if (has_pressure) { |
| pr_col <- if ("pressure" %in% names(df) && any(!is.na(df$pressure))) "pressure" else if ("station_pressure" %in% names(df) && any(!is.na(df$station_pressure))) "station_pressure" else NULL |
| if (!is.null(pr_col) && any(!is.na(df[[pr_col]]))) { |
| pr_vals <- as.numeric(df[[pr_col]]) |
| pr_vals[pr_vals < 0] <- NA |
| latest <- tail(na.omit(pr_vals), 1) |
| avg_pr <- mean(pr_vals, na.rm = TRUE) |
| min_pr <- min(pr_vals, na.rm = TRUE) |
| max_pr <- max(pr_vals, na.rm = TRUE) |
| min_idx <- which.min(pr_vals) |
| max_idx <- which.max(pr_vals) |
|
|
| add_card( |
| if (pr_col == "pressure") "Atm. Pressure" else "Station Pressure", format_val(latest, " hPa", 0), |
| sprintf("Avg: %s | Range: %s - %s", format_val(avg_pr, " hPa", 0), format_val(min_pr, " hPa", 0), format_val(max_pr, " hPa", 0)), |
| "speedometer2", "card-purple" |
| ) |
| } |
| } |
|
|
| if (has_vis) { |
| v_vals <- as.numeric(df$vis) |
| v_vals[v_vals < 0] <- NA |
| latest <- tail(na.omit(v_vals), 1) |
| avg_v <- mean(v_vals, na.rm = TRUE) |
| add_card( |
| "Visibility", format_val(latest, " km", 0), |
| sprintf("Avg: %s", format_val(avg_v, " km", 0)), |
| "eye", "card-slate" |
| ) |
| } |
|
|
| if (has_precip && "precip" %in% names(df)) { |
| p_vals <- as.numeric(df$precip) |
| p_vals[p_vals < 0] <- NA |
|
|
| if (any(!is.na(p_vals))) { |
| max_p <- max(p_vals, na.rm = TRUE) |
| max_idx <- which.max(p_vals) |
|
|
| yearly_totals <- df %>% |
| dplyr::mutate(p_val = p_vals) %>% |
| dplyr::group_by(year) %>% |
| dplyr::summarize(total = sum(p_val, na.rm = TRUE), .groups = "drop") %>% |
| dplyr::filter(!is.na(year)) |
|
|
| yearly_str <- paste( |
| sapply(1:nrow(yearly_totals), function(i) { |
| sprintf("%s: %s", yearly_totals$year[i], format_val(yearly_totals$total[i], " mm", 0)) |
| }), |
| collapse = " | " |
| ) |
|
|
| latest <- tail(na.omit(p_vals), 1) |
| add_card( |
| "Precipitation", format_val(latest, " mm"), |
| sprintf("Max 24h Precip: %s<br/>Recorded: %s<br/>Yearly Totals: %s", format_val(max_p, " mm"), format_ext_time(max_idx), yearly_str), |
| "cloud-rain", "card-indigo" |
| ) |
| } |
| } |
|
|
| date_range_str <- "" |
| if (nrow(df) > 0) { |
| date_range_str <- paste( |
| format(min(df$datetime, na.rm = TRUE), "%d %b %Y"), "-", |
| format(max(df$datetime, na.rm = TRUE), "%d %b %Y") |
| ) |
| } |
|
|
| plot_list <- tagList() |
|
|
| if (has_temp) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("temp_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_humidity) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("humidity_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_wind) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("wind_overview_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_pressure) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("pressure_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_vis) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("visibility_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_precip) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("precip_plot", height = "320px") |
| ) |
| ) |
| } |
| if (has_wind_rose) { |
| plot_list <- tagList( |
| plot_list, |
| div( |
| class = "col-12 col-lg-6", |
| plotlyOutput("wind_rose", height = "320px") |
| ) |
| ) |
| } |
|
|
| tagList( |
| if (length(cards) > 0) { |
| latest_obs_str <- if (nrow(df) > 0) paste("— Latest values as of:", format_ext_time(nrow(df))) else "" |
| div( |
| class = "w-100", |
| 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 |
| }, |
| div( |
| class = "row g-3", |
| style = "padding: 10px;", |
| plot_list |
| ) |
| ) |
| }) |
|
|
|
|
| output$daily_summary_main <- DT::renderDataTable({ |
| df <- station_data() |
| summary_df <- df |
| if (is.null(summary_df) || nrow(summary_df) == 0) { |
| return(NULL) |
| } |
| |
| display_summary_df <- summary_df %>% |
| dplyr::mutate(date = as.Date(datetime)) %>% |
| dplyr::select( |
| date, |
| avg_temp = temp, |
| max_temp = temp_max, |
| min_temp = temp_min, |
| max_wind = wind_gust, |
| avg_vis = vis, |
| avg_pressure = pressure, |
| precip_sum = precip |
| ) |
|
|
| DT::datatable( |
| display_summary_df, |
| colnames = unname(c( |
| col_names_map["date"], |
| col_names_map["avg_temp"], |
| col_names_map["max_temp"], |
| col_names_map["min_temp"], |
| col_names_map["max_wind"], |
| col_names_map["avg_vis"], |
| col_names_map["avg_pressure"], |
| col_names_map["precip_sum"] |
| )), |
| options = list( |
| pageLength = 12, |
| dom = "lrtip", |
| columnDefs = list(list(className = "dt-center", targets = "_all")), |
| order = list(list(0, "desc")) |
| ), |
| rownames = FALSE, |
| selection = "none" |
| ) |
| }) |
|
|
| |
| last_valid_bounds <- reactiveVal(NULL) |
|
|
| observe({ |
| |
| |
| req(input$map_bounds) |
| if (is.null(input$main_nav) || input$main_nav == "Map View") { |
| b <- input$map_bounds |
| |
| if (!is.null(b$north) && !is.null(b$south) && b$north != b$south) { |
| last_valid_bounds(b) |
| } |
| } |
| }) |
|
|
| |
| visible_stations <- reactive({ |
| data <- filtered_stations() |
| if (is.null(data)) { |
| return(NULL) |
| } |
|
|
| |
| bounds <- last_valid_bounds() |
|
|
| |
| if (is.null(bounds)) { |
| bounds <- input$map_bounds |
| } |
|
|
| if (is.null(bounds)) { |
| return(data) |
| } |
|
|
| data %>% |
| dplyr::filter( |
| latitude >= bounds$south, |
| latitude <= bounds$north, |
| longitude >= bounds$west, |
| longitude <= bounds$east |
| ) |
| }) |
|
|
| output$plot_info <- renderUI({ |
| info <- station_info() |
| if (is.null(info)) { |
| return(NULL) |
| } |
| tagList(h5(info$name), p(tags$small("Station ID: ", info$id))) |
| }) |
|
|
| output$download_stations <- downloadHandler( |
| filename = function() { |
| paste("ssod_stations_", Sys.Date(), ".xlsx", sep = "") |
| }, |
| content = function(file) { |
| df <- visible_stations() |
| if (is.null(df)) { |
| return(NULL) |
| } |
| write_xlsx(df, path = file) |
| } |
| ) |
|
|
|
|
| |
| output$download_daily <- downloadHandler( |
| filename = function() { |
| st_id <- active_export_id() |
| d_start <- input$date_range[1] |
| d_end <- input$date_range[2] |
| if (!isTruthy(st_id)) { |
| return(paste0("ssod_daily_", Sys.Date(), ".xlsx")) |
| } |
| paste0("SSOD_daily_", st_id, "_", d_start, "_to_", d_end, ".xlsx") |
| }, |
| content = function(file) { |
| df <- station_data() |
| summary_df <- df |
| if (is.null(summary_df) || nrow(summary_df) == 0) { |
| write_xlsx(data.frame(Message = "No data found"), path = file) |
| } else { |
| write_xlsx(summary_df, path = file) |
| } |
| } |
| ) |
| } |
|
|