|
|
server <- function(input, output, session) { |
|
|
|
|
|
observe({ |
|
|
updateSelectInput(session, "stationSelect", choices = sort(meta$name)) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
selected_station_id <- reactiveVal(NULL) |
|
|
|
|
|
|
|
|
style_change_trigger <- reactiveVal(0) |
|
|
|
|
|
|
|
|
observeEvent(input$stationSelect, { |
|
|
print(paste("Dropdown Change - New Selection:", input$stationSelect)) |
|
|
|
|
|
selected_station <- meta %>% |
|
|
filter(name == input$stationSelect) %>% |
|
|
pull(id) |
|
|
|
|
|
print(paste("Dropdown Change - Resolved ID:", selected_station)) |
|
|
|
|
|
selected_station_id(selected_station) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$map_feature_click, { |
|
|
clicked_data <- input$map_feature_click |
|
|
print("Feature Click Event:") |
|
|
print(str(clicked_data)) |
|
|
|
|
|
|
|
|
|
|
|
if (!is.null(clicked_data) && (isTRUE(clicked_data$layer_id == "stations") || isTRUE(clicked_data$layer == "stations"))) { |
|
|
|
|
|
clicked_station <- clicked_data$properties$id |
|
|
|
|
|
if (!is.null(clicked_station)) { |
|
|
|
|
|
selected_station_name <- meta %>% |
|
|
filter(id == clicked_station) %>% |
|
|
pull(name) |
|
|
|
|
|
print(paste("Map Click - Station ID:", clicked_station)) |
|
|
print(paste("Map Click - Found Name:", selected_station_name)) |
|
|
|
|
|
updateSelectInput(session, "stationSelect", selected = selected_station_name) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
filtered_data <- reactive({ |
|
|
|
|
|
req(selected_station_id()) |
|
|
|
|
|
|
|
|
withProgress(message = "Processing data...", value = 0, { |
|
|
|
|
|
incProgress(0.2, detail = "Setting up filters...") |
|
|
|
|
|
year_range <- input$yearRange |
|
|
agg_type <- input$aggregation |
|
|
|
|
|
|
|
|
data_filtered <- combined_data %>% |
|
|
filter( |
|
|
altitude >= input$altitudeRange[1], |
|
|
altitude <= input$altitudeRange[2], |
|
|
variable == input$variable, |
|
|
year >= year_range[1], |
|
|
year <= year_range[2] |
|
|
) |
|
|
|
|
|
|
|
|
incProgress(0.4, detail = paste("Applying", agg_type, "aggregation...")) |
|
|
|
|
|
if (agg_type == "Monthly") { |
|
|
|
|
|
data_filtered <- data_filtered %>% |
|
|
filter(month == as.integer(input$month)) %>% |
|
|
group_by(id, name, latitude, longitude, altitude) %>% |
|
|
summarise(multi_annual_value = mean(value, na.rm = TRUE), .groups = "drop") |
|
|
} else if (agg_type == "Seasonal") { |
|
|
data_filtered <- data_filtered %>% |
|
|
add_seasonal_columns() %>% |
|
|
filter_seasonal_data(input$season) %>% |
|
|
aggregate_seasonal(input$variable) %>% |
|
|
group_by(id, name, latitude, longitude, altitude) %>% |
|
|
summarise(multi_annual_value = mean(value, na.rm = TRUE), .groups = "drop") %>% |
|
|
mutate(multi_annual_value = round(multi_annual_value, 1)) |
|
|
} else if (agg_type == "Annual") { |
|
|
|
|
|
data_filtered <- data_filtered %>% |
|
|
group_by(id, name, latitude, longitude, altitude, year) %>% |
|
|
summarise( |
|
|
annual_value = if (input$variable == "PREC") sum(value, na.rm = TRUE) else mean(value, na.rm = TRUE), |
|
|
.groups = "drop" |
|
|
) %>% |
|
|
group_by(id, name, latitude, longitude, altitude) %>% |
|
|
summarise(multi_annual_value = mean(annual_value, na.rm = TRUE), .groups = "drop") |
|
|
} |
|
|
|
|
|
|
|
|
incProgress(1, detail = "Finalizing...") |
|
|
|
|
|
return(data_filtered) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
time_series_data <- reactive({ |
|
|
|
|
|
req(selected_station_id()) |
|
|
|
|
|
|
|
|
year_range <- input$yearRange |
|
|
data_filtered <- combined_data %>% |
|
|
filter( |
|
|
id == selected_station_id(), |
|
|
variable == input$variable, |
|
|
year >= year_range[1], |
|
|
year <= year_range[2] |
|
|
) |
|
|
|
|
|
|
|
|
agg_type <- input$aggregation |
|
|
|
|
|
if (agg_type == "Monthly") { |
|
|
return( |
|
|
data_filtered %>% |
|
|
filter(month == as.integer(input$month)) %>% |
|
|
dplyr::select(name, year, month, value) %>% |
|
|
arrange(year, month) |
|
|
) |
|
|
} else if (agg_type == "Seasonal") { |
|
|
|
|
|
data_filtered <- data_filtered %>% |
|
|
add_seasonal_columns() %>% |
|
|
filter_seasonal_data(input$season) %>% |
|
|
aggregate_seasonal(input$variable) %>% |
|
|
mutate(value = round(value, 1)) %>% |
|
|
arrange(year) |
|
|
|
|
|
|
|
|
return(data_filtered) |
|
|
} else if (agg_type == "Annual") { |
|
|
return( |
|
|
data_filtered %>% |
|
|
group_by(name, year) %>% |
|
|
summarise(value = if (input$variable == "PREC") sum(value, na.rm = TRUE) else mean(value, na.rm = TRUE), .groups = "drop") %>% |
|
|
mutate(value = round(value, 1)) %>% |
|
|
arrange(year) |
|
|
) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
output$download_map_data <- downloadHandler( |
|
|
filename = function() { |
|
|
agg_label <- switch(input$aggregation, |
|
|
"Monthly" = sprintf("monthly_%02d", as.integer(input$month)), |
|
|
"Seasonal" = paste0("seasonal_", input$season), |
|
|
"Annual" = "annual" |
|
|
) |
|
|
paste0("roclihom_map_data_", tolower(input$variable), "_", agg_label, ".csv") |
|
|
}, |
|
|
content = function(file) { |
|
|
map_data <- filtered_data() |
|
|
req(nrow(map_data) > 0) |
|
|
write.csv(map_data, file, row.names = FALSE) |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
output$download_csv <- downloadHandler( |
|
|
filename = function() { |
|
|
station_slug <- if (is.null(input$stationSelect) || input$stationSelect == "") { |
|
|
"station" |
|
|
} else { |
|
|
gsub("[^A-Za-z0-9]+", "_", tolower(input$stationSelect)) |
|
|
} |
|
|
|
|
|
agg_label <- switch(input$aggregation, |
|
|
"Monthly" = sprintf("monthly_%02d", as.integer(input$month)), |
|
|
"Seasonal" = paste0("seasonal_", input$season), |
|
|
"Annual" = "annual" |
|
|
) |
|
|
|
|
|
paste0("roclihom_", station_slug, "_", tolower(input$variable), "_", agg_label, ".csv") |
|
|
}, |
|
|
content = function(file) { |
|
|
ts_data <- time_series_data() |
|
|
req(nrow(ts_data) > 0) |
|
|
|
|
|
export_df <- ts_data %>% |
|
|
mutate( |
|
|
station = input$stationSelect, |
|
|
variable = input$variable, |
|
|
aggregation = input$aggregation |
|
|
) %>% |
|
|
relocate(station, variable, aggregation) |
|
|
|
|
|
write.csv(export_df, file, row.names = FALSE) |
|
|
} |
|
|
) |
|
|
|
|
|
output$map <- renderMaplibre({ |
|
|
print("DEBUG: renderMaplibre called - Map is initializing/re-rendering") |
|
|
maplibre( |
|
|
style = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", |
|
|
center = c(25, 44), |
|
|
zoom = 6 |
|
|
) %>% |
|
|
add_navigation_control(show_compass = FALSE, visualize_pitch = FALSE, position = "top-left") |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
style_change_trigger <- reactiveVal(0) |
|
|
|
|
|
|
|
|
map_initialized <- reactiveVal(FALSE) |
|
|
|
|
|
|
|
|
observe({ |
|
|
req(!map_initialized()) |
|
|
|
|
|
req(input$map_zoom) |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds( |
|
|
c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max) |
|
|
) |
|
|
|
|
|
map_initialized(TRUE) |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$home_zoom, { |
|
|
req(map_bounds) |
|
|
maplibre_proxy("map") %>% |
|
|
fit_bounds( |
|
|
c(map_bounds$lng_min, map_bounds$lat_min, map_bounds$lng_max, map_bounds$lat_max), |
|
|
animate = TRUE |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
observe({ |
|
|
req(filtered_data()) |
|
|
|
|
|
|
|
|
style_change_trigger() |
|
|
|
|
|
|
|
|
|
|
|
selected_id <- selected_station_id() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
map_data <- filtered_data() |
|
|
|
|
|
|
|
|
color_pal2 <- get_color_palette(input$variable, domain = map_data$multi_annual_value, reverse = TRUE) |
|
|
|
|
|
|
|
|
map_data <- map_data %>% |
|
|
mutate( |
|
|
circle_color = color_pal2(multi_annual_value), |
|
|
circle_radius = ifelse(id == selected_id, 8, 5), |
|
|
circle_stroke_color = ifelse(id == selected_id, "#FF0000", "#00000000"), |
|
|
circle_stroke_width = ifelse(id == selected_id, 2, 1), |
|
|
|
|
|
is_selected = ifelse(id == selected_id, 1, 0), |
|
|
popup_content = paste0( |
|
|
"<strong>Name: </strong>", name, |
|
|
"<br><strong>", input$variable, ": </strong>", round(multi_annual_value, 1), |
|
|
"<br><span style='color:red;'>click to update</span>" |
|
|
) |
|
|
) %>% |
|
|
arrange(is_selected) %>% |
|
|
st_as_sf(coords = c("longitude", "latitude"), crs = 4326) |
|
|
|
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
clear_layer("stations") %>% |
|
|
add_circle_layer( |
|
|
id = "stations", |
|
|
source = map_data, |
|
|
circle_color = get_column("circle_color"), |
|
|
circle_radius = get_column("circle_radius"), |
|
|
circle_stroke_color = get_column("circle_stroke_color"), |
|
|
circle_stroke_width = get_column("circle_stroke_width"), |
|
|
circle_opacity = 0.9, |
|
|
|
|
|
tooltip = get_column("popup_content"), |
|
|
before_id = stations_before_id() |
|
|
) |
|
|
}) |
|
|
|
|
|
|
|
|
stations_before_id <- reactiveVal(NULL) |
|
|
|
|
|
|
|
|
current_raster_layers <- reactiveVal(character(0)) |
|
|
|
|
|
|
|
|
observeEvent(input$basemap, { |
|
|
print(paste("Basemap Change - Selection:", input$basemap)) |
|
|
|
|
|
proxy <- maplibre_proxy("map") |
|
|
|
|
|
|
|
|
old_layers <- isolate(current_raster_layers()) |
|
|
if (length(old_layers) > 0) { |
|
|
for (layer_id in old_layers) { |
|
|
proxy %>% clear_layer(layer_id) |
|
|
} |
|
|
current_raster_layers(character(0)) |
|
|
} |
|
|
|
|
|
if (input$basemap %in% c("carto_positron", "carto_voyager", "esri_imagery", "mapbox_satellite")) { |
|
|
|
|
|
|
|
|
print("Setting Vector Style for Carto...") |
|
|
|
|
|
style_url <- switch(input$basemap, |
|
|
"carto_positron" = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json", |
|
|
"carto_voyager" = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json", |
|
|
"esri_imagery" = "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json", |
|
|
"mapbox_satellite" = paste0("https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12?access_token=", mapbox_token) |
|
|
) |
|
|
|
|
|
proxy %>% |
|
|
set_style(style_url) |
|
|
|
|
|
|
|
|
stations_before_id("watername_ocean") |
|
|
|
|
|
|
|
|
if (input$basemap == "esri_imagery") { |
|
|
session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- input$basemap |
|
|
|
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(session, { |
|
|
|
|
|
current_basemap <- isolate(input$basemap) |
|
|
if (current_basemap != selected_basemap) { |
|
|
return() |
|
|
} |
|
|
|
|
|
unique_suffix <- as.numeric(Sys.time()) * 1000 |
|
|
source_id <- paste0("esri_imagery_source_", unique_suffix) |
|
|
layer_id <- paste0("esri_imagery_layer_", unique_suffix) |
|
|
|
|
|
esri_url <- "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" |
|
|
|
|
|
|
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
add_raster_source(id = source_id, tiles = c(esri_url), tileSize = 256) %>% |
|
|
add_layer( |
|
|
id = layer_id, |
|
|
type = "raster", |
|
|
source = source_id, |
|
|
paint = list("raster-opacity" = 1), |
|
|
before_id = "watername_ocean" |
|
|
) |
|
|
|
|
|
current_raster_layers(c(layer_id)) |
|
|
|
|
|
|
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.5) |
|
|
} else { |
|
|
|
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
} |
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
tile_url <- if (input$basemap %in% c("osm", "osm_gray")) { |
|
|
"https://tile.openstreetmap.org/{z}/{x}/{y}.png" |
|
|
} else { |
|
|
|
|
|
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}" |
|
|
} |
|
|
|
|
|
attribution_text <- if (input$basemap %in% c("osm", "osm_gray")) { |
|
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
|
|
} else { |
|
|
"Tiles © Esri" |
|
|
} |
|
|
|
|
|
|
|
|
paint_props <- list("raster-opacity" = 1) |
|
|
if (input$basemap == "osm_gray") { |
|
|
paint_props[["raster-saturation"]] <- -0.9 |
|
|
paint_props[["raster-contrast"]] <- 0.3 |
|
|
} |
|
|
|
|
|
|
|
|
blank_style <- list( |
|
|
version = 8, |
|
|
sources = list(), |
|
|
layers = list(), |
|
|
metadata = list(timestamp = as.numeric(Sys.time())) |
|
|
) |
|
|
json_blank <- jsonlite::toJSON(blank_style, auto_unbox = TRUE) |
|
|
blank_uri <- paste0("data:application/json,", URLencode(as.character(json_blank), reserved = TRUE)) |
|
|
|
|
|
proxy %>% |
|
|
set_style(blank_uri) |
|
|
|
|
|
|
|
|
session <- shiny::getDefaultReactiveDomain() |
|
|
selected_basemap <- input$basemap |
|
|
|
|
|
|
|
|
later::later(function() { |
|
|
shiny::withReactiveDomain(session, { |
|
|
|
|
|
current_basemap <- isolate(input$basemap) |
|
|
if (current_basemap != selected_basemap) { |
|
|
print(paste("Basemap changed during delay - aborting.")) |
|
|
return() |
|
|
} |
|
|
|
|
|
unique_suffix <- as.numeric(Sys.time()) * 1000 |
|
|
source_id <- paste0("raster_source_", unique_suffix) |
|
|
layer_id <- paste0("raster_layer_", unique_suffix) |
|
|
|
|
|
maplibre_proxy("map") %>% |
|
|
add_raster_source(id = source_id, tiles = c(tile_url), tileSize = 256, attribution = attribution_text) %>% |
|
|
add_layer( |
|
|
id = layer_id, |
|
|
type = "raster", |
|
|
source = source_id, |
|
|
paint = paint_props |
|
|
) |
|
|
|
|
|
|
|
|
stations_before_id(NULL) |
|
|
current_raster_layers(c(layer_id)) |
|
|
|
|
|
|
|
|
style_change_trigger(isolate(style_change_trigger()) + 1) |
|
|
}) |
|
|
}, delay = 0.5) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
observeEvent(input$show_labels, |
|
|
{ |
|
|
visibility <- if (input$show_labels) "visible" else "none" |
|
|
|
|
|
|
|
|
label_layers <- c( |
|
|
|
|
|
"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" |
|
|
) |
|
|
|
|
|
|
|
|
print(paste("Labels toggle - visibility:", visibility)) |
|
|
proxy <- maplibre_proxy("map") |
|
|
|
|
|
for (layer_id in label_layers) { |
|
|
tryCatch( |
|
|
{ |
|
|
proxy <- proxy %>% set_layout_property(layer_id, "visibility", visibility) |
|
|
}, |
|
|
error = function(e) { |
|
|
|
|
|
} |
|
|
) |
|
|
} |
|
|
}, |
|
|
ignoreInit = TRUE |
|
|
) |
|
|
|
|
|
|
|
|
output$plot_title <- renderText({ |
|
|
paste(input$stationSelect, "station") |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
output$time_series_plot <- renderPlotly({ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ts_data <- time_series_data() |
|
|
|
|
|
|
|
|
line_color <- if_else(input$variable == "PREC", "blue", "red") |
|
|
|
|
|
|
|
|
y_axis_label <- switch(input$variable, |
|
|
"PREC" = "mm", |
|
|
"Tavg" = "°C", |
|
|
"Tmin" = "°C", |
|
|
"Tmax" = "°C" |
|
|
) |
|
|
|
|
|
|
|
|
x_lim <- range(ts_data$year) |
|
|
|
|
|
|
|
|
kendall_test_result <- kendallTrendTest(ts_data$value ~ ts_data$year) |
|
|
theil_sen_slope <- kendall_test_result$estimate["slope"] |
|
|
p_value <- kendall_test_result$p.value |
|
|
intercept <- mean(ts_data$value) - theil_sen_slope * mean(ts_data$year) |
|
|
trend_line <- intercept + theil_sen_slope * ts_data$year |
|
|
|
|
|
|
|
|
p <- ggplot(ts_data, aes(x = year, y = value)) + |
|
|
geom_line(color = line_color) + |
|
|
xlim(x_lim) + |
|
|
geom_line(aes(y = trend_line), color = "#808080") + |
|
|
labs(x = NULL, y = y_axis_label) + |
|
|
|
|
|
theme_minimal() + |
|
|
annotate("text", |
|
|
x = x_lim[1] + 24, y = max(ts_data$value) * 1.05, |
|
|
label = |
|
|
paste0("Theil-Sen slope: ", round(theil_sen_slope * 10, 3), " ", y_axis_label, "/decade p.value:", round(p_value, 4)), |
|
|
hjust = 0, vjust = 1, color = "black", size = 3, fontface = "italic" |
|
|
) |
|
|
|
|
|
|
|
|
ggplotly(p) %>% |
|
|
layout(autosize = TRUE, hovermode = "closest") |
|
|
}) |
|
|
|
|
|
|
|
|
output$map_title <- renderText({ |
|
|
|
|
|
var_name <- switch(input$variable, |
|
|
"PREC" = "Precipitation", |
|
|
"Tavg" = "Average Temperature", |
|
|
"Tmin" = "Minimum Temperature", |
|
|
"Tmax" = "Maximum Temperature", |
|
|
"Variable" |
|
|
) |
|
|
|
|
|
|
|
|
agg_type <- input$aggregation |
|
|
|
|
|
|
|
|
year_range <- paste(input$yearRange[1], "-", input$yearRange[2]) |
|
|
|
|
|
|
|
|
title_text <- paste(var_name, tolower(agg_type), "from", year_range) |
|
|
|
|
|
|
|
|
if (agg_type == "Monthly") { |
|
|
month_name <- month.abb[as.integer(input$month)] |
|
|
title_text <- paste(var_name, tolower(agg_type), month_name, "from", year_range) |
|
|
} |
|
|
|
|
|
|
|
|
if (agg_type == "Seasonal") { |
|
|
title_text <- paste(var_name, tolower(agg_type), input$season, "from", year_range) |
|
|
} |
|
|
|
|
|
|
|
|
title_text |
|
|
}) |
|
|
} |
|
|
|