feat: Enhance DWD metadata with date ranges and station availability, add 24h index expiry, remove parse date filtering, and improve solar plot with daily aggregation and dual axes.
Browse files- funs/dwd_metadata.R +9 -0
- funs/parse_dwd.R +7 -7
- funs/plot_weather_dwd.R +84 -28
- global.R +65 -5
- server.R +114 -22
- ui.R +2 -2
funs/dwd_metadata.R
CHANGED
|
@@ -58,6 +58,13 @@ fetch_dwd_file_index <- function() {
|
|
| 58 |
}
|
| 59 |
|
| 60 |
if (length(zip_files) > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# Create index DataFrame
|
| 62 |
df <- data.frame(
|
| 63 |
id = ids,
|
|
@@ -65,6 +72,8 @@ fetch_dwd_file_index <- function() {
|
|
| 65 |
url = paste0(url, zip_files), # Absolute URL
|
| 66 |
param = param,
|
| 67 |
type = subtype,
|
|
|
|
|
|
|
| 68 |
stringsAsFactors = FALSE
|
| 69 |
)
|
| 70 |
index_list[[paste(param, subtype, sep = "_")]] <- df
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
if (length(zip_files) > 0) {
|
| 61 |
+
# Extract dates if possible (format: _YYYYMMDD_YYYYMMDD_)
|
| 62 |
+
# Pattern: _(17|18|19|20)\d{6}_(17|18|19|20)\d{6}
|
| 63 |
+
date_pattern <- "_((?:17|18|19|20)\\d{6})_((?:17|18|19|20)\\d{6})"
|
| 64 |
+
|
| 65 |
+
start_dates <- str_match(zip_files, date_pattern)[, 2]
|
| 66 |
+
end_dates <- str_match(zip_files, date_pattern)[, 3]
|
| 67 |
+
|
| 68 |
# Create index DataFrame
|
| 69 |
df <- data.frame(
|
| 70 |
id = ids,
|
|
|
|
| 72 |
url = paste0(url, zip_files), # Absolute URL
|
| 73 |
param = param,
|
| 74 |
type = subtype,
|
| 75 |
+
start_date = start_dates,
|
| 76 |
+
end_date = end_dates,
|
| 77 |
stringsAsFactors = FALSE
|
| 78 |
)
|
| 79 |
index_list[[paste(param, subtype, sep = "_")]] <- df
|
funs/parse_dwd.R
CHANGED
|
@@ -100,13 +100,13 @@ read_dwd_data <- function(zip_path, start_date = NULL, end_date = NULL) {
|
|
| 100 |
# Filter valid dates
|
| 101 |
df <- df[!is.na(df$datetime), ]
|
| 102 |
|
| 103 |
-
# Window Filter
|
| 104 |
-
if (!is.null(start_date)) {
|
| 105 |
-
|
| 106 |
-
}
|
| 107 |
-
if (!is.null(end_date)) {
|
| 108 |
-
|
| 109 |
-
}
|
| 110 |
|
| 111 |
# Column Mapping
|
| 112 |
weather_cols <- c("temp", "rh", "precip", "wind_speed", "wind_dir", "pressure", "station_pressure", "cloud_cover", "wind_gust_max", "solar_global", "sunshine_duration")
|
|
|
|
| 100 |
# Filter valid dates
|
| 101 |
df <- df[!is.na(df$datetime), ]
|
| 102 |
|
| 103 |
+
# Window Filter - REMOVED to allow caller to inspect full range
|
| 104 |
+
# if (!is.null(start_date)) {
|
| 105 |
+
# df <- df[df$datetime >= (as.POSIXct(start_date) - days(1)), ]
|
| 106 |
+
# }
|
| 107 |
+
# if (!is.null(end_date)) {
|
| 108 |
+
# df <- df[df$datetime <= (as.POSIXct(end_date) + days(1)), ]
|
| 109 |
+
# }
|
| 110 |
|
| 111 |
# Column Mapping
|
| 112 |
weather_cols <- c("temp", "rh", "precip", "wind_speed", "wind_dir", "pressure", "station_pressure", "cloud_cover", "wind_gust_max", "solar_global", "sunshine_duration")
|
funs/plot_weather_dwd.R
CHANGED
|
@@ -628,7 +628,7 @@ create_diurnal_plot <- function(df, offset_hours = 0) {
|
|
| 628 |
plotly::config(displaylogo = FALSE)
|
| 629 |
}
|
| 630 |
|
| 631 |
-
#' Create Solar Plot (
|
| 632 |
create_solar_plot <- function(df) {
|
| 633 |
if (is.null(df) || nrow(df) == 0) {
|
| 634 |
return(create_empty_plot("No data available for the selected period"))
|
|
@@ -637,49 +637,105 @@ create_solar_plot <- function(df) {
|
|
| 637 |
# Centralized cleaning
|
| 638 |
df <- clean_dwd_data(df)
|
| 639 |
|
|
|
|
| 640 |
has_global <- "solar_global" %in% names(df) && any(!is.na(df$solar_global))
|
| 641 |
has_sun <- "sunshine_duration" %in% names(df) && any(!is.na(df$sunshine_duration))
|
| 642 |
|
| 643 |
if (!has_global && !has_sun) {
|
| 644 |
-
|
|
|
|
| 645 |
}
|
| 646 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
date_range_str <- paste(
|
| 648 |
-
format(min(
|
| 649 |
-
format(max(
|
| 650 |
)
|
| 651 |
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
}
|
| 664 |
|
| 665 |
-
# Sunshine
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
}
|
| 675 |
|
|
|
|
| 676 |
p %>%
|
| 677 |
plotly::layout(
|
| 678 |
-
title = list(text = paste("Solar
|
| 679 |
-
xaxis = list(
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
hovermode = "x unified",
|
| 682 |
-
hoverlabel = list()
|
|
|
|
|
|
|
|
|
|
| 683 |
) %>%
|
| 684 |
plotly::config(displaylogo = FALSE)
|
| 685 |
}
|
|
|
|
| 628 |
plotly::config(displaylogo = FALSE)
|
| 629 |
}
|
| 630 |
|
| 631 |
+
#' Create Solar Plot (Enhanced - Daily Aggregation)
|
| 632 |
create_solar_plot <- function(df) {
|
| 633 |
if (is.null(df) || nrow(df) == 0) {
|
| 634 |
return(create_empty_plot("No data available for the selected period"))
|
|
|
|
| 637 |
# Centralized cleaning
|
| 638 |
df <- clean_dwd_data(df)
|
| 639 |
|
| 640 |
+
# Check availability
|
| 641 |
has_global <- "solar_global" %in% names(df) && any(!is.na(df$solar_global))
|
| 642 |
has_sun <- "sunshine_duration" %in% names(df) && any(!is.na(df$sunshine_duration))
|
| 643 |
|
| 644 |
if (!has_global && !has_sun) {
|
| 645 |
+
# It's possible the data exists in the file but not this window.
|
| 646 |
+
return(create_empty_plot("No solar/sunshine data in selected time range"))
|
| 647 |
}
|
| 648 |
|
| 649 |
+
# --- Daily Aggregation ---
|
| 650 |
+
df_daily <- df %>%
|
| 651 |
+
dplyr::mutate(date = as.Date(datetime)) %>%
|
| 652 |
+
dplyr::group_by(date) %>%
|
| 653 |
+
dplyr::summarise(
|
| 654 |
+
# Sum J/cm^2 per hour -> Total Daily Energy J/cm^2
|
| 655 |
+
daily_solar = if (all(is.na(solar_global))) NA_real_ else sum(solar_global, na.rm = TRUE),
|
| 656 |
+
|
| 657 |
+
# Sum Minutes -> Convert to Hours
|
| 658 |
+
daily_sun_hours = if (all(is.na(sunshine_duration))) NA_real_ else sum(sunshine_duration, na.rm = TRUE) / 60,
|
| 659 |
+
.groups = "drop"
|
| 660 |
+
)
|
| 661 |
+
|
| 662 |
date_range_str <- paste(
|
| 663 |
+
format(min(df_daily$date), "%d %b %Y"), "-",
|
| 664 |
+
format(max(df_daily$date), "%d %b %Y")
|
| 665 |
)
|
| 666 |
|
| 667 |
+
# Base Plot
|
| 668 |
+
p <- plotly::plot_ly()
|
| 669 |
+
|
| 670 |
+
# 1. Global Radiation (Filled Area, Primary Y-Axis)
|
| 671 |
+
# Intuition: "Total Energy per Day"
|
| 672 |
+
if (has_global) {
|
| 673 |
+
sub_g <- df_daily %>% dplyr::filter(!is.na(daily_solar))
|
| 674 |
+
if (nrow(sub_g) > 0) {
|
| 675 |
+
p <- p %>% plotly::add_trace(
|
| 676 |
+
data = sub_g,
|
| 677 |
+
x = ~date,
|
| 678 |
+
y = ~daily_solar,
|
| 679 |
+
name = "Daily Global Radiation",
|
| 680 |
+
type = "scatter",
|
| 681 |
+
mode = "lines",
|
| 682 |
+
fill = "tozeroy",
|
| 683 |
+
line = list(color = "rgba(255, 179, 0, 0.9)", width = 1), # Amber
|
| 684 |
+
fillcolor = "rgba(255, 179, 0, 0.3)",
|
| 685 |
+
hovertemplate = "Daily Rad: %{y:,.0f} J/cm²<extra></extra>", # Comma for thousands
|
| 686 |
+
yaxis = "y"
|
| 687 |
+
)
|
| 688 |
+
}
|
| 689 |
}
|
| 690 |
|
| 691 |
+
# 2. Sunshine Duration (Bars, Secondary Y-Axis)
|
| 692 |
+
# Intuition: "Total Hours per Day"
|
| 693 |
+
if (has_sun) {
|
| 694 |
+
sub_s <- df_daily %>% dplyr::filter(!is.na(daily_sun_hours))
|
| 695 |
+
if (nrow(sub_s) > 0) {
|
| 696 |
+
# We use bar chart for sunshine
|
| 697 |
+
p <- p %>% plotly::add_bars(
|
| 698 |
+
data = sub_s,
|
| 699 |
+
x = ~date,
|
| 700 |
+
y = ~daily_sun_hours,
|
| 701 |
+
name = "Daily Sunshine",
|
| 702 |
+
marker = list(color = "rgba(0, 0, 0, 0.3)", line = list(color = "#FFD700", width = 1.5)), # Gold/Yellow outline
|
| 703 |
+
hovertemplate = "Sunshine: %{y:.1f} h<extra></extra>",
|
| 704 |
+
yaxis = "y2"
|
| 705 |
+
)
|
| 706 |
+
}
|
| 707 |
}
|
| 708 |
|
| 709 |
+
# Layout Configuration for Dual Axis
|
| 710 |
p %>%
|
| 711 |
plotly::layout(
|
| 712 |
+
title = list(text = paste("Daily Solar Energy & Sunshine:", date_range_str), font = list(size = 14)),
|
| 713 |
+
xaxis = list(
|
| 714 |
+
title = "",
|
| 715 |
+
type = "date",
|
| 716 |
+
domain = c(0.05, 0.95)
|
| 717 |
+
),
|
| 718 |
+
yaxis = list(
|
| 719 |
+
title = "Daily Radiation (J/cm²)",
|
| 720 |
+
titlefont = list(color = "#ffb300"),
|
| 721 |
+
tickfont = list(color = "#ffb300"),
|
| 722 |
+
side = "left",
|
| 723 |
+
showgrid = TRUE
|
| 724 |
+
),
|
| 725 |
+
yaxis2 = list(
|
| 726 |
+
title = "Sunshine (Hours)",
|
| 727 |
+
titlefont = list(color = "#FFD700"),
|
| 728 |
+
tickfont = list(color = "#FFD700"),
|
| 729 |
+
side = "right",
|
| 730 |
+
overlaying = "y",
|
| 731 |
+
range = c(0, 18), # Max theoretical sunshine is < 17h in Germany
|
| 732 |
+
showgrid = FALSE
|
| 733 |
+
),
|
| 734 |
hovermode = "x unified",
|
| 735 |
+
hoverlabel = list(),
|
| 736 |
+
legend = list(orientation = "h", x = 0.5, xanchor = "center", y = 1.1),
|
| 737 |
+
margin = list(t = 60, b = 40, l = 60, r = 60),
|
| 738 |
+
modebar = list(orientation = "h")
|
| 739 |
) %>%
|
| 740 |
plotly::config(displaylogo = FALSE)
|
| 741 |
}
|
global.R
CHANGED
|
@@ -42,13 +42,23 @@ if (!dir.exists("data")) dir.create("data")
|
|
| 42 |
index_file <- "data/dwd_file_index.rds"
|
| 43 |
station_file <- "data/dwd_stations.rds"
|
| 44 |
|
| 45 |
-
# 1. File Index
|
| 46 |
-
if (
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
dwd_index <- fetch_dwd_file_index()
|
| 49 |
saveRDS(dwd_index, index_file)
|
| 50 |
-
} else {
|
| 51 |
-
dwd_index <- readRDS(index_file)
|
| 52 |
}
|
| 53 |
|
| 54 |
# 2. Station List
|
|
@@ -60,6 +70,56 @@ if (!file.exists(station_file)) {
|
|
| 60 |
stations <- readRDS(station_file)
|
| 61 |
}
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# 3. Enrich Station List with Available Parameters
|
| 64 |
if (exists("dwd_index") && exists("stations")) {
|
| 65 |
param_summary <- dwd_index %>%
|
|
|
|
| 42 |
index_file <- "data/dwd_file_index.rds"
|
| 43 |
station_file <- "data/dwd_stations.rds"
|
| 44 |
|
| 45 |
+
# 1. File Index (with 24h Expiry)
|
| 46 |
+
if (file.exists(index_file)) {
|
| 47 |
+
# Check age
|
| 48 |
+
info <- file.info(index_file)
|
| 49 |
+
age_hours <- difftime(Sys.time(), info$mtime, units = "hours")
|
| 50 |
+
|
| 51 |
+
if (age_hours > 24) {
|
| 52 |
+
message(paste("File index is", round(age_hours, 1), "hours old. Rebuilding..."))
|
| 53 |
+
dwd_index <- fetch_dwd_file_index()
|
| 54 |
+
saveRDS(dwd_index, index_file)
|
| 55 |
+
} else {
|
| 56 |
+
dwd_index <- readRDS(index_file)
|
| 57 |
+
}
|
| 58 |
+
} else {
|
| 59 |
+
message("Building DWD File Index (first run)...")
|
| 60 |
dwd_index <- fetch_dwd_file_index()
|
| 61 |
saveRDS(dwd_index, index_file)
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
# 2. Station List
|
|
|
|
| 70 |
stations <- readRDS(station_file)
|
| 71 |
}
|
| 72 |
|
| 73 |
+
# 3. Enhance Station Metadata with Detailed Availability
|
| 74 |
+
# Group DWD Index by ID to get ranges per parameter
|
| 75 |
+
if (!is.null(dwd_index) && nrow(stations) > 0) {
|
| 76 |
+
param_summary <- dwd_index %>%
|
| 77 |
+
group_by(id, param) %>%
|
| 78 |
+
summarise(
|
| 79 |
+
# Combine ranges
|
| 80 |
+
# Suppress warnings for Inf/NA when no hist data exists
|
| 81 |
+
min_start = suppressWarnings(min(start_date, na.rm = TRUE)),
|
| 82 |
+
max_end = suppressWarnings(max(end_date, na.rm = TRUE)),
|
| 83 |
+
has_recent = any(type == "recent"),
|
| 84 |
+
has_hist = any(type == "historical"),
|
| 85 |
+
.groups = "drop"
|
| 86 |
+
) %>%
|
| 87 |
+
mutate(
|
| 88 |
+
# Logic:
|
| 89 |
+
# If has_hist, we have a start date (e.g. 19800101) as character
|
| 90 |
+
# If has_recent only, min_start is Inf (numeric)
|
| 91 |
+
|
| 92 |
+
# Start Year
|
| 93 |
+
s_year = ifelse(is.character(min_start), substr(min_start, 1, 4), "Recent"),
|
| 94 |
+
|
| 95 |
+
# End Year
|
| 96 |
+
# If has_recent, assume "Present"
|
| 97 |
+
# If only hist, uses max_end (character)
|
| 98 |
+
e_year = ifelse(has_recent, "Present",
|
| 99 |
+
ifelse(is.character(max_end), substr(max_end, 1, 4), "Unknown")
|
| 100 |
+
),
|
| 101 |
+
|
| 102 |
+
# Formatting
|
| 103 |
+
# Avoid "Recent-Present", just say "Recent"
|
| 104 |
+
label = ifelse(s_year == "Recent" & e_year == "Present",
|
| 105 |
+
paste0(str_to_title(param), " (Recent)"),
|
| 106 |
+
paste0(str_to_title(param), " (", s_year, "-", e_year, ")")
|
| 107 |
+
)
|
| 108 |
+
) %>%
|
| 109 |
+
group_by(id) %>%
|
| 110 |
+
summarise(
|
| 111 |
+
detailed_summary = paste(label, collapse = ", ")
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# Join back to stations
|
| 115 |
+
stations <- stations %>%
|
| 116 |
+
left_join(param_summary, by = "id") %>%
|
| 117 |
+
mutate(
|
| 118 |
+
# Fallback
|
| 119 |
+
detailed_summary = ifelse(is.na(detailed_summary), "No Data", detailed_summary)
|
| 120 |
+
)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
# 3. Enrich Station List with Available Parameters
|
| 124 |
if (exists("dwd_index") && exists("stations")) {
|
| 125 |
param_summary <- dwd_index %>%
|
server.R
CHANGED
|
@@ -8,27 +8,75 @@ server <- function(input, output, session) {
|
|
| 8 |
|
| 9 |
# Filtered stations based on Sidebar inputs
|
| 10 |
filtered_stations <- reactive({
|
| 11 |
-
req(stations)
|
| 12 |
df <- stations
|
| 13 |
|
| 14 |
-
# Filter by State
|
| 15 |
-
if (!is.null(input$state_filter)) {
|
| 16 |
-
df <- df %>% filter(state %in% input$state_filter)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
# Filter by Date (metadata availability)
|
| 20 |
-
#
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
df
|
| 25 |
})
|
| 26 |
|
| 27 |
-
# Initialize
|
| 28 |
observe({
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
})
|
| 33 |
|
| 34 |
# Station Count
|
|
@@ -83,8 +131,8 @@ server <- function(input, output, session) {
|
|
| 83 |
# Pre-process label with HTML
|
| 84 |
df <- df %>% mutate(
|
| 85 |
label_html = purrr::pmap_chr(
|
| 86 |
-
list(name, id, state, start_date, end_date,
|
| 87 |
-
function(name, id, state, start_date, end_date,
|
| 88 |
tryCatch(
|
| 89 |
{
|
| 90 |
s <- as.Date(as.character(start_date), format = "%Y%m%d")
|
|
@@ -95,11 +143,11 @@ server <- function(input, output, session) {
|
|
| 95 |
"<b>", htmltools::htmlEscape(name), "</b> (", id, ")<br>",
|
| 96 |
"<span style='font-size:80%;'>State: ", htmltools::htmlEscape(state), "</span><br>",
|
| 97 |
"<span style='font-size:80%; color:#555;'>",
|
| 98 |
-
"
|
| 99 |
"</span><br>",
|
| 100 |
-
"<div style='font-size:80%; color:#333; font-weight:bold; margin-top:5px;'>
|
| 101 |
-
"<div style='font-size:75%; color:#333;
|
| 102 |
-
gsub(", ", "<br>• ", htmltools::htmlEscape(
|
| 103 |
"</div>",
|
| 104 |
"</div>"
|
| 105 |
)
|
|
@@ -131,6 +179,19 @@ server <- function(input, output, session) {
|
|
| 131 |
# Set ID
|
| 132 |
current_station_id(id_val)
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
# Trigger Panel
|
| 135 |
output$show_details_panel <- reactive(TRUE)
|
| 136 |
outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
|
|
@@ -503,12 +564,43 @@ server <- function(input, output, session) {
|
|
| 503 |
distinct(datetime, .keep_all = TRUE) %>%
|
| 504 |
arrange(datetime)
|
| 505 |
|
| 506 |
-
# Final Window Filter (safety)
|
| 507 |
s_date <- window_debounced()$start
|
| 508 |
e_date <- window_debounced()$end
|
|
|
|
| 509 |
if (!is.null(s_date) && !is.null(e_date)) {
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
}
|
| 513 |
|
| 514 |
station_data(clean_df)
|
|
|
|
| 8 |
|
| 9 |
# Filtered stations based on Sidebar inputs
|
| 10 |
filtered_stations <- reactive({
|
| 11 |
+
req(stations, input$date_range)
|
| 12 |
df <- stations
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# Filter by Date (metadata availability)
|
| 15 |
+
# We check if the station was active during any part of the selected range
|
| 16 |
+
range_start <- as.numeric(format(input$date_range[1], "%Y%m%d"))
|
| 17 |
+
range_end <- as.numeric(format(input$date_range[2], "%Y%m%d"))
|
| 18 |
+
|
| 19 |
+
df <- df %>% filter(
|
| 20 |
+
as.numeric(start_date) <= range_end &
|
| 21 |
+
as.numeric(end_date) >= range_start
|
| 22 |
+
)
|
| 23 |
|
| 24 |
df
|
| 25 |
})
|
| 26 |
|
| 27 |
+
# Initialize / Update Station Selector Choices
|
| 28 |
observe({
|
| 29 |
+
df <- filtered_stations()
|
| 30 |
+
req(df)
|
| 31 |
+
|
| 32 |
+
# Create choices: "Station Name (ID)" = "ID"
|
| 33 |
+
choices <- setNames(df$id, paste0(df$name, " (", df$id, ")"))
|
| 34 |
+
|
| 35 |
+
# Preserve selection if still in filtered list
|
| 36 |
+
current_sel <- input$station_selector
|
| 37 |
+
|
| 38 |
+
updateSelectizeInput(session, "station_selector",
|
| 39 |
+
choices = choices,
|
| 40 |
+
selected = current_sel,
|
| 41 |
+
server = TRUE
|
| 42 |
+
)
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
# --- Selection Logic via Dropdown ---
|
| 46 |
+
observeEvent(input$station_selector, {
|
| 47 |
+
req(input$station_selector)
|
| 48 |
+
id_val <- input$station_selector
|
| 49 |
+
|
| 50 |
+
# Find station details
|
| 51 |
+
s_meta <- stations %>% filter(id == id_val)
|
| 52 |
+
req(nrow(s_meta) > 0)
|
| 53 |
+
|
| 54 |
+
lat_val <- s_meta$latitude[1]
|
| 55 |
+
lng_val <- s_meta$longitude[1]
|
| 56 |
+
|
| 57 |
+
# Set ID
|
| 58 |
+
current_station_id(id_val)
|
| 59 |
+
|
| 60 |
+
# Highlight & Zoom
|
| 61 |
+
leafletProxy("map") %>%
|
| 62 |
+
setView(lng = lng_val + 0.8, lat = lat_val, zoom = 9) %>%
|
| 63 |
+
removeMarker(layerId = "selected-highlight") %>%
|
| 64 |
+
addCircleMarkers(
|
| 65 |
+
lng = lng_val, lat = lat_val,
|
| 66 |
+
layerId = "selected-highlight",
|
| 67 |
+
radius = 8, color = "red", fill = FALSE, opacity = 1, weight = 3
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Trigger Panel
|
| 71 |
+
output$show_details_panel <- reactive(TRUE)
|
| 72 |
+
outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
|
| 73 |
+
|
| 74 |
+
# Update Dates to last 366 days of available data for this station
|
| 75 |
+
e_date <- tryCatch(as.Date(as.character(s_meta$end_date[1]), format = "%Y%m%d"), error = function(e) Sys.Date())
|
| 76 |
+
if (is.na(e_date)) e_date <- Sys.Date()
|
| 77 |
+
s_date <- e_date - 366
|
| 78 |
+
updateDateInput(session, "modal_date_start", value = s_date)
|
| 79 |
+
updateDateInput(session, "modal_date_end", value = e_date)
|
| 80 |
})
|
| 81 |
|
| 82 |
# Station Count
|
|
|
|
| 131 |
# Pre-process label with HTML
|
| 132 |
df <- df %>% mutate(
|
| 133 |
label_html = purrr::pmap_chr(
|
| 134 |
+
list(name, id, state, start_date, end_date, detailed_summary),
|
| 135 |
+
function(name, id, state, start_date, end_date, detailed_summary) {
|
| 136 |
tryCatch(
|
| 137 |
{
|
| 138 |
s <- as.Date(as.character(start_date), format = "%Y%m%d")
|
|
|
|
| 143 |
"<b>", htmltools::htmlEscape(name), "</b> (", id, ")<br>",
|
| 144 |
"<span style='font-size:80%;'>State: ", htmltools::htmlEscape(state), "</span><br>",
|
| 145 |
"<span style='font-size:80%; color:#555;'>",
|
| 146 |
+
"Station Active: ", format(s, "%Y-%m-%d"), " – ", format(e, "%Y-%m-%d"),
|
| 147 |
"</span><br>",
|
| 148 |
+
"<div style='font-size:80%; color:#333; font-weight:bold; margin-top:5px;'>Data Availability:</div>",
|
| 149 |
+
"<div style='font-size:75%; color:#333; line-height: 1.1;'>• ",
|
| 150 |
+
gsub(", ", "<br>• ", htmltools::htmlEscape(detailed_summary)),
|
| 151 |
"</div>",
|
| 152 |
"</div>"
|
| 153 |
)
|
|
|
|
| 179 |
# Set ID
|
| 180 |
current_station_id(id_val)
|
| 181 |
|
| 182 |
+
# Highlight & Zoom
|
| 183 |
+
leafletProxy("map") %>%
|
| 184 |
+
setView(lng = click$lng + 0.8, lat = click$lat, zoom = 9) %>%
|
| 185 |
+
removeMarker(layerId = "selected-highlight") %>%
|
| 186 |
+
addCircleMarkers(
|
| 187 |
+
lng = click$lng, lat = click$lat,
|
| 188 |
+
layerId = "selected-highlight",
|
| 189 |
+
radius = 8, color = "red", fill = FALSE, opacity = 1, weight = 3
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Sync dropdown
|
| 193 |
+
updateSelectizeInput(session, "station_selector", selected = id_val)
|
| 194 |
+
|
| 195 |
# Trigger Panel
|
| 196 |
output$show_details_panel <- reactive(TRUE)
|
| 197 |
outputOptions(output, "show_details_panel", suspendWhenHidden = FALSE)
|
|
|
|
| 564 |
distinct(datetime, .keep_all = TRUE) %>%
|
| 565 |
arrange(datetime)
|
| 566 |
|
| 567 |
+
# Final Window Filter (safety) with Auto-Correction
|
| 568 |
s_date <- window_debounced()$start
|
| 569 |
e_date <- window_debounced()$end
|
| 570 |
+
|
| 571 |
if (!is.null(s_date) && !is.null(e_date)) {
|
| 572 |
+
req_start <- as.POSIXct(s_date)
|
| 573 |
+
req_end <- as.POSIXct(e_date) + days(1)
|
| 574 |
+
|
| 575 |
+
filtered_df <- clean_df %>%
|
| 576 |
+
filter(datetime >= req_start, datetime <= req_end)
|
| 577 |
+
|
| 578 |
+
# Check for mismatch: Data exists but not in window
|
| 579 |
+
if (nrow(filtered_df) == 0 && nrow(clean_df) > 0) {
|
| 580 |
+
data_max <- max(clean_df$datetime, na.rm = TRUE)
|
| 581 |
+
data_min <- min(clean_df$datetime, na.rm = TRUE)
|
| 582 |
+
|
| 583 |
+
# If the requested window is completely outside the data range
|
| 584 |
+
if (req_start > data_max || req_end < data_min) {
|
| 585 |
+
msg <- paste0("No data in selected range. Auto-adjusting to latest available: ", format(data_max, "%Y-%m-%d"))
|
| 586 |
+
loading_diagnostics(msg)
|
| 587 |
+
|
| 588 |
+
# Auto-adjust dates
|
| 589 |
+
# This will trigger a re-fetch via the observer
|
| 590 |
+
new_end <- as.Date(data_max)
|
| 591 |
+
new_start <- new_end - 366
|
| 592 |
+
|
| 593 |
+
updateDateInput(session, "modal_date_start", value = new_start)
|
| 594 |
+
# Small delay for end date to ensure sliding window logic doesn't fight us
|
| 595 |
+
# confusing interaction possible, but setting both usually works
|
| 596 |
+
updateDateInput(session, "modal_date_end", value = new_end)
|
| 597 |
+
|
| 598 |
+
# Stop this cycle
|
| 599 |
+
return()
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
clean_df <- filtered_df
|
| 604 |
}
|
| 605 |
|
| 606 |
station_data(clean_df)
|
ui.R
CHANGED
|
@@ -86,8 +86,8 @@ ui <- page_navbar(
|
|
| 86 |
theme = bs_theme(version = 5, bootswatch = "cosmo"),
|
| 87 |
sidebar = sidebar(
|
| 88 |
title = "Filters",
|
| 89 |
-
#
|
| 90 |
-
|
| 91 |
div(
|
| 92 |
style = "display: flex; align-items: center; gap: 8px;",
|
| 93 |
tags$label("Select Date Range"),
|
|
|
|
| 86 |
theme = bs_theme(version = 5, bootswatch = "cosmo"),
|
| 87 |
sidebar = sidebar(
|
| 88 |
title = "Filters",
|
| 89 |
+
# Station Search
|
| 90 |
+
selectizeInput("station_selector", "Find Station", choices = NULL, multiple = FALSE, options = list(placeholder = "Type to search...")),
|
| 91 |
div(
|
| 92 |
style = "display: flex; align-items: center; gap: 8px;",
|
| 93 |
tags$label("Select Date Range"),
|