| |
| library(shiny) |
| library(bslib) |
| library(bsicons) |
| library(mapgl) |
| library(sf) |
| library(jsonlite) |
| library(dplyr) |
| library(readr) |
| library(DT) |
| library(plotly) |
| library(lubridate) |
| library(shinycssloaders) |
| library(curl) |
| library(stringr) |
| library(purrr) |
| library(shinyjs) |
| library(tibble) |
| library(data.table) |
| library(writexl) |
|
|
| |
| ofm_positron_style <- "https://tiles.openfreemap.org/styles/positron" |
| ofm_bright_style <- "https://tiles.openfreemap.org/styles/bright" |
|
|
| |
| sentinel_url <- "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2023_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.jpg" |
| sentinel_attribution <- '<a href="https://s2maps.eu" target="_blank">Sentinel-2 cloudless - by EOX IT Services GmbH</a> (Contains modified Copernicus Sentinel data 2023)' |
|
|
|
|
| |
| |
| dwd_10min_base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/10_minutes/" |
| dwd_base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/hourly/" |
| dwd_daily_base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/daily/" |
| dwd_monthly_base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/monthly/" |
| dwd_annual_base_url <- "https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/annual/" |
|
|
| |
| cache_ttl_days <- 30 |
|
|
| |
| dwd_10min_params <- list( |
| air_temperature = "air_temperature", |
| extreme_wind = "extreme_wind", |
| precipitation = "precipitation", |
| solar = "solar", |
| wind = "wind", |
| extreme_temperature = "extreme_temperature" |
| ) |
|
|
| |
| dwd_params <- list( |
| temp = "air_temperature", |
| dew_point = "dew_point", |
| moisture = "moisture", |
| precip = "precipitation", |
| wind = "wind", |
| visibility = "visibility", |
| solar = "solar", |
| pressure = "pressure", |
| cloudiness = "cloudiness", |
| cloud_type = "cloud_type", |
| extreme_wind = "extreme_wind", |
| weather_phenomena = "weather_phenomena", |
| soil = "soil_temperature", |
| sun = "sun" |
| ) |
|
|
| |
| dwd_daily_params <- list( |
| kl = "kl", |
| precip = "more_precip", |
| solar = "solar", |
| soil = "soil_temperature", |
| water_equiv = "water_equiv", |
| weather = "weather_phenomena", |
| more_weather = "more_weather_phenomena" |
| ) |
|
|
| |
| dwd_monthly_params <- list( |
| kl = "kl", |
| precip = "more_precip", |
| weather = "weather_phenomena" |
| ) |
|
|
| |
| dwd_annual_params <- list( |
| kl = "kl", |
| precip = "more_precip", |
| weather = "weather_phenomena" |
| ) |
|
|
| |
| normalize_dwd_resolution <- function(resolution) { |
| if (is.null(resolution) || length(resolution) == 0 || anyNA(resolution) || resolution == "") { |
| return(NULL) |
| } |
|
|
| res <- tolower(gsub("[[:space:]-]+", "_", as.character(resolution))) |
|
|
| if (res %in% c("10_minutes", "10_minute", "10_min", "10minutes")) { |
| return("10_minutes") |
| } |
| if (res == "yearly") { |
| return("annual") |
| } |
| res |
| } |
|
|
| get_dwd_resolution_suffix <- function(resolution) { |
| switch(normalize_dwd_resolution(resolution), |
| "daily" = "_daily", |
| "monthly" = "_monthly", |
| "annual" = "_annual", |
| "10_minutes" = "_10min", |
| "" |
| ) |
| } |
|
|
| format_dwd_resolution <- function(resolution) { |
| switch(normalize_dwd_resolution(resolution), |
| "10_minutes" = "10 Minutes", |
| "hourly" = "Hourly", |
| "daily" = "Daily", |
| "monthly" = "Monthly", |
| "annual" = "Annual", |
| { |
| res <- normalize_dwd_resolution(resolution) |
| if (is.null(res)) "Data" else str_to_title(gsub("_", " ", res)) |
| } |
| ) |
| } |
|
|
| get_dwd_resolution_config <- function(resolution = "hourly") { |
| res <- normalize_dwd_resolution(resolution) |
| if (is.null(res)) res <- "hourly" |
|
|
| switch(res, |
| "daily" = list( |
| base_url = dwd_daily_base_url, |
| params = dwd_daily_params, |
| station_url = paste0(dwd_daily_base_url, "kl/historical/"), |
| station_desc = "KL_Tageswerte_Beschreibung_Stationen.txt" |
| ), |
| "monthly" = list( |
| base_url = dwd_monthly_base_url, |
| params = dwd_monthly_params, |
| station_url = paste0(dwd_monthly_base_url, "kl/recent/"), |
| station_desc = "KL_Monatswerte_Beschreibung_Stationen.txt" |
| ), |
| "annual" = list( |
| base_url = dwd_annual_base_url, |
| params = dwd_annual_params, |
| station_url = paste0(dwd_annual_base_url, "kl/recent/"), |
| station_desc = "KL_Jahreswerte_Beschreibung_Stationen.txt" |
| ), |
| "10_minutes" = list( |
| base_url = dwd_10min_base_url, |
| params = dwd_10min_params, |
| station_url = paste0(dwd_base_url, "air_temperature/recent/"), |
| station_desc = "Beschreibung_Stationen.txt" |
| ), |
| list( |
| base_url = dwd_base_url, |
| params = dwd_params, |
| station_url = paste0(dwd_base_url, "air_temperature/recent/"), |
| station_desc = "Beschreibung_Stationen.txt" |
| ) |
| ) |
| } |
|
|
| |
| dwd_column_labels <- c( |
| |
| "datetime" = "Date/Time", |
| "datetime_end" = "Period End", |
|
|
| |
| "temp" = "Air Temperature [°C]", |
| "temp_min" = "Min Temperature [°C]", |
| "temp_max" = "Max Temperature [°C]", |
| "temp_min_avg" = "Avg Min Temperature [°C]", |
| "temp_max_avg" = "Avg Max Temperature [°C]", |
| "dew_point" = "Dew Point [°C]", |
| "wet_bulb_temp" = "Wet Bulb Temperature [°C]", |
| "temp_5cm" = "Temp at 5cm [°C]", |
|
|
| |
| "rh" = "Relative Humidity [%]", |
| "abs_humidity" = "Absolute Humidity [g/m³]", |
| "vapor_pressure" = "Vapor Pressure [hPa]", |
|
|
| |
| "precip" = "Precipitation [mm]", |
| "precip_max_day" = "Max Daily Precipitation [mm]", |
| "precip_ind" = "Precipitation Indicator", |
|
|
| |
| "wind_speed" = "Wind Speed [m/s]", |
| "wind_dir" = "Wind Direction [°]", |
| "wind_gust_max" = "Max Wind Gust [m/s]", |
| "wind_gust_min" = "Min Wind Gust [m/s]", |
|
|
| |
| "pressure" = "Pressure (Sea Level) [hPa]", |
| "station_pressure" = "Pressure (Station Level) [hPa]", |
|
|
| |
| "cloud_cover" = "Cloud Cover [oktas]", |
| "solar_global" = "Global Radiation [J/cm²]", |
| "sunshine_duration" = "Sunshine Duration [min]", |
| "diffuse_radiation" = "Diffuse Radiation [J/cm²]", |
| "longwave_radiation" = "Longwave Radiation [J/cm²]", |
|
|
| |
| "soil_temp_2cm" = "Soil Temp 2cm [°C]", |
| "soil_temp_5cm" = "Soil Temp 5cm [°C]", |
| "soil_temp_10cm" = "Soil Temp 10cm [°C]", |
| "soil_temp_20cm" = "Soil Temp 20cm [°C]", |
| "soil_temp_50cm" = "Soil Temp 50cm [°C]", |
| "soil_temp_100cm" = "Soil Temp 100cm [°C]", |
| "soil_temp_min_5cm" = "Min Soil Temp 5cm [°C]", |
|
|
| |
| "snow_depth" = "Snow Depth [cm]", |
| "snow_water_equiv" = "Snow Water Equivalent [mm]", |
| "snow_fresh_sum" = "Fresh Snow Sum [cm]", |
| "snow_depth_sum" = "Snow Depth Sum [cm]", |
|
|
| |
| "weather_code" = "Weather Code", |
| "weather_text" = "Weather Description", |
| "visibility" = "Visibility [m]", |
|
|
| |
| "thunderstorm" = "Thunderstorm", |
| "glaze" = "Glaze", |
| "graupel" = "Graupel", |
| "hail" = "Hail", |
| "fog" = "Fog", |
| "frost" = "Frost", |
| "storm_6" = "Storm (Bft 6)", |
| "storm_8" = "Storm (Bft 8)", |
| "dew" = "Dew" |
| ) |
|
|
| |
| |
| for (file in list.files("fun", pattern = "\\.R$", full.names = TRUE)) { |
| source(file) |
| } |
|
|
| |
|
|
| |
| if (!dir.exists("data")) dir.create("data") |
|
|
| |
| log_cache_status <- function() { |
| files <- list.files("data", pattern = "^dwd_.*[.]rds$", full.names = TRUE) |
| if (length(files) == 0) { |
| message("Cache TTL: ", cache_ttl_days, " days (no cache files found).") |
| return(invisible(NULL)) |
| } |
|
|
| info <- file.info(files) |
| age_days <- difftime(Sys.time(), info$mtime, units = "days") |
| age_labels <- paste0(basename(files), "=", sprintf("%.1f", as.numeric(age_days)), "d") |
| message("Cache TTL: ", cache_ttl_days, " days; cache ages: ", paste(age_labels, collapse = ", ")) |
| invisible(NULL) |
| } |
|
|
| log_cache_status() |
|
|
| |
| |
| granular_cache_path <- "www/tabs/dwd_granular_metadata.rds" |
|
|
| |
| |
| get_granular_cache <- function() { |
| if (file.exists(granular_cache_path)) { |
| tryCatch( |
| readRDS(granular_cache_path), |
| error = function(e) { |
| message("Failed to read granular cache: ", e$message) |
| list() |
| } |
| ) |
| } else { |
| list() |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| update_granular_cache <- function(station_id, resolution, meta_df) { |
| if (is.null(meta_df) || nrow(meta_df) == 0) { |
| return(invisible(NULL)) |
| } |
|
|
| |
| key <- paste0(tolower(resolution), "_", station_id) |
|
|
| cache <- get_granular_cache() |
|
|
| |
| cache[[key]] <- list( |
| station_id = station_id, |
| resolution = resolution, |
| params = meta_df, |
| updated = Sys.time() |
| ) |
|
|
| tryCatch( |
| { |
| saveRDS(cache, granular_cache_path) |
| message("Updated granular cache for ", key) |
| }, |
| error = function(e) { |
| message("Failed to save granular cache: ", e$message) |
| } |
| ) |
| invisible(NULL) |
| } |
|
|
| |
| |
| |
| |
| load_dwd_index <- function(resolution = "hourly") { |
| resolution <- normalize_dwd_resolution(resolution) |
| if (is.null(resolution)) resolution <- "hourly" |
| suffix <- get_dwd_resolution_suffix(resolution) |
| index_file <- paste0("www/tabs/dwd_file_index", suffix, ".rds") |
|
|
| if (file.exists(index_file)) { |
| tryCatch(readRDS(index_file), error = function(e) { |
| message("Failed to read file index: ", e$message) |
| message("Attempting to rebuild file index for ", resolution, "...") |
| get_dwd_index(resolution) |
| }) |
| } else { |
| message("File index not found: ", index_file, " - building on demand...") |
| get_dwd_index(resolution) |
| } |
| } |
|
|
| |
| |
| |
| get_dwd_stations <- function(resolution = "hourly") { |
| resolution <- normalize_dwd_resolution(resolution) |
| if (is.null(resolution)) resolution <- "hourly" |
| suffix <- get_dwd_resolution_suffix(resolution) |
| station_file <- paste0("www/tabs/dwd_stations", suffix, ".rds") |
|
|
| if (file.exists(station_file)) { |
| info <- file.info(station_file) |
| |
| age_days <- difftime(Sys.time(), info$mtime, units = "days") |
| if (age_days <= cache_ttl_days) { |
| return(readRDS(station_file)) |
| } |
| message(paste(resolution, "station metadata is old. Rebuilding...")) |
| } |
|
|
| message(paste("Fetching", resolution, "Station Metadata...")) |
| stations <- fetch_dwd_stations(resolution) |
| if (!is.null(stations)) { |
| saveRDS(stations, station_file) |
| } |
| stations |
| } |
|
|
| |
| |
| |
| |
| load_enriched_stations <- function(resolution = "daily") { |
| resolution <- normalize_dwd_resolution(resolution) |
| if (is.null(resolution)) resolution <- "daily" |
| |
| suffix <- get_dwd_resolution_suffix(resolution) |
|
|
| cache_file <- paste0("www/tabs/dwd_stations_enriched", suffix, ".rds") |
|
|
| if (file.exists(cache_file)) { |
| info <- file.info(cache_file) |
| if (difftime(Sys.time(), info$mtime, units = "days") <= cache_ttl_days) { |
| |
| tryCatch( |
| { |
| res <- readRDS(cache_file) |
| if (is.data.frame(res) && nrow(res) > 0) { |
| return(res) |
| } |
| }, |
| error = function(e) message("Enriched cache corrupted, rebuilding...") |
| ) |
| } |
| } |
|
|
| |
| st_df <- get_dwd_stations(resolution) |
| if (is.null(st_df) || nrow(st_df) == 0) { |
| message("No base station data available for ", resolution) |
| return(NULL) |
| } |
|
|
| |
| cache <- get_granular_cache() |
| file_index <- load_dwd_index(resolution) |
|
|
| |
| index_by_id <- if (!is.null(file_index)) split(file_index, file_index$id) else list() |
|
|
| |
| dir_map <- c( |
| "air_temperature" = "Temperature", |
| "cloudiness" = "Cloudiness", |
| "precipitation" = "Precipitation", |
| "pressure" = "Pressure", |
| "soil_temperature" = "Soil Temp", |
| "solar" = "Solar", |
| "sun" = "Sunshine", |
| "visibility" = "Visibility", |
| "wind" = "Wind", |
| "kl" = "Climate Data (KL)", |
| "more_precip" = "Precipitation (More)", |
| "water_equiv" = "Water Equiv", |
| "weather_phenomena" = "Weather Phenomena", |
| "more_weather" = "More Weather Phenomena", |
| "moisture" = "Moisture", |
| "dew_point" = "Dew Point", |
| "soil" = "Soil Temp", |
| "cloud_type" = "Cloud Type" |
| ) |
|
|
| |
| if (resolution == "10_minutes") { |
| dir_map["air_temperature"] <- "Temperature, Pressure" |
| } |
|
|
| |
| |
| summary_list <- lapply(st_df$id, function(sid) { |
| |
| cache_key <- paste0(resolution, "_", sid) |
| entry <- cache[[cache_key]] |
|
|
| gran_labels <- character(0) |
| gran_text <- character(0) |
|
|
| overall_starts <- character(0) |
| overall_ends <- character(0) |
|
|
| if (!is.null(entry) && !is.null(entry$params) && nrow(entry$params) > 0) { |
| params_df <- entry$params |
| param_map <- get_dwd_param_map() |
|
|
| |
| params_df$label <- sapply(params_df$param, function(p) { |
| if (p %in% names(param_map)) param_map[[p]] else p |
| }) |
|
|
| |
| params_df <- params_df[nchar(params_df$start_date) >= 4 & nchar(params_df$end_date) >= 4, ] |
| params_df <- params_df[!is.na(params_df$label), ] |
|
|
| if (nrow(params_df) > 0) { |
| |
| agg_summary <- params_df %>% |
| group_by(label) %>% |
| summarise( |
| date_start = min(start_date, na.rm = TRUE), |
| date_end = max(end_date, na.rm = TRUE), |
| .groups = "drop" |
| ) |
|
|
| |
| gran_text <- mapply(function(lbl, sd, ed) { |
| s_yr <- if (grepl("^[0-9]+$", substr(sd, 1, 4))) substr(sd, 1, 4) else "Unknown" |
| e_yr <- "Present" |
| if (grepl("^[0-9]+$", substr(ed, 1, 4))) { |
| if (as.numeric(substr(ed, 1, 4)) < as.numeric(format(Sys.Date(), "%Y")) - 1) { |
| e_yr <- substr(ed, 1, 4) |
| } |
| } |
| paste0(lbl, " (", s_yr, "-", e_yr, ")") |
| }, agg_summary$label, agg_summary$date_start, agg_summary$date_end) |
|
|
| gran_labels <- agg_summary$label |
| overall_starts <- params_df$start_date[grepl("^\\d{8}$", params_df$start_date)] |
| overall_ends <- params_df$end_date[grepl("^\\d{8}$", params_df$end_date)] |
| } |
| } |
|
|
| |
| index_text <- character(0) |
| idx_entry <- index_by_id[[sid]] |
|
|
| if (!is.null(idx_entry)) { |
| idx_params <- unique(idx_entry$param) |
|
|
| |
| for (ip in idx_params) { |
| |
| ip_label <- if (ip %in% names(dir_map)) dir_map[[ip]] else str_to_title(ip) |
|
|
|
|
| |
| if (any(grepl(ip_label, gran_labels, fixed = TRUE))) { |
| next |
| } |
|
|
| |
| if (ip_label == "Climate Data (KL)") { |
| |
| has_kl_content <- any(c("Temperature", "Precipitation", "Wind", "Sunshine") %in% gran_labels) |
| if (has_kl_content) { |
| next |
| } |
| } |
|
|
| |
| ip_rows <- idx_entry[idx_entry$param == ip, ] |
| min_s <- suppressWarnings(min(ip_rows$start_date, na.rm = TRUE)) |
| max_e <- suppressWarnings(max(ip_rows$end_date, na.rm = TRUE)) |
|
|
| s_yr <- "Unknown" |
| if (!is.na(min_s) && nchar(min_s) >= 4) s_yr <- substr(min_s, 1, 4) |
|
|
| e_yr <- "Present" |
| if (any(ip_rows$type == "recent")) { |
| e_yr <- "Present" |
| } else { |
| if (!is.na(max_e) && nchar(max_e) >= 4) e_yr <- substr(max_e, 1, 4) |
| } |
|
|
| index_text <- c(index_text, paste0(ip_label, " (", s_yr, "-", e_yr, ")")) |
|
|
| |
| if (!is.na(min_s) && grepl("^\\d{8}$", min_s)) overall_starts <- c(overall_starts, min_s) |
| |
| if (any(ip_rows$type == "recent")) { |
| overall_ends <- c(overall_ends, "99999999") |
| } else if (!is.na(max_e) && grepl("^\\d{8}$", max_e)) { |
| overall_ends <- c(overall_ends, max_e) |
| } |
| } |
| } |
|
|
| |
| full_summary <- c(gran_text, index_text) |
| detailed_summary <- paste(sort(unique(full_summary)), collapse = ", ") |
|
|
| |
| overall_start <- if (length(overall_starts) > 0) min(overall_starts) else "99999999" |
| overall_end <- if (length(overall_ends) > 0) max(overall_ends) else "00000000" |
|
|
| |
| all_params <- character(0) |
| if (!is.null(idx_entry)) all_params <- unique(idx_entry$param) |
| available_params <- paste(sort(all_params), collapse = ", ") |
|
|
| data.frame( |
| id = sid, |
| detailed_summary = detailed_summary, |
| station_overall_start = overall_start, |
| station_overall_end = overall_end, |
| available_params = available_params, |
| stringsAsFactors = FALSE |
| ) |
| }) |
|
|
| summary_df <- do.call(rbind, summary_list) |
|
|
| |
| st_df <- st_df %>% |
| left_join(summary_df, by = "id") %>% |
| mutate( |
| |
| detailed_summary = ifelse(is.na(detailed_summary) | detailed_summary == "", "No Data", detailed_summary), |
| station_overall_start = ifelse(is.na(station_overall_start) | station_overall_start == "", "18000101", station_overall_start), |
| station_overall_end = ifelse(is.na(station_overall_end) | station_overall_end == "", "99999999", station_overall_end), |
| available_params = ifelse(is.na(available_params) | available_params == "", "Unknown", available_params) |
| ) |
|
|
| res <- as.data.frame(st_df) |
| saveRDS(res, cache_file) |
| res |
| } |
|
|
|
|
| |
| |
| |
| max_year_data <- year(Sys.Date()) |
| default_end_date <- Sys.Date() |
| default_start_date <- default_end_date - (365 * 6) |
|
|
| |
|
|