feat: Implement URL parameter parsing for app initialization and synchronize app state with the parent page URL.
Browse files- server.R +199 -1
- www/app.js +31 -0
server.R
CHANGED
|
@@ -90,6 +90,123 @@ server <- function(input, output, session) {
|
|
| 90 |
loading_diagnostics <- reactiveVal("")
|
| 91 |
previous_station_choices <- reactiveVal(NULL) # Track previous choices to avoid blink
|
| 92 |
previous_date_range <- reactiveVal(NULL) # Track previous date range for bi-directional sync
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# --- Reactive Data Sources (Resolution Dependent) ---
|
| 95 |
observeEvent(input$data_resolution,
|
|
@@ -240,6 +357,59 @@ server <- function(input, output, session) {
|
|
| 240 |
}
|
| 241 |
})
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
# --- Selection Logic via Dropdown ---
|
| 244 |
observeEvent(input$station_selector, {
|
| 245 |
req(input$station_selector)
|
|
@@ -1011,7 +1181,9 @@ server <- function(input, output, session) {
|
|
| 1011 |
fetch_stage(0)
|
| 1012 |
|
| 1013 |
# Navigate to Dashboard tab
|
| 1014 |
-
|
|
|
|
|
|
|
| 1015 |
|
| 1016 |
# Show rendering message and then unfreeze after delay
|
| 1017 |
session$sendCustomMessage("freezeUI", list(text = "Rendering plots..."))
|
|
@@ -1407,4 +1579,30 @@ server <- function(input, output, session) {
|
|
| 1407 |
plot_list
|
| 1408 |
)
|
| 1409 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1410 |
}
|
|
|
|
| 90 |
loading_diagnostics <- reactiveVal("")
|
| 91 |
previous_station_choices <- reactiveVal(NULL) # Track previous choices to avoid blink
|
| 92 |
previous_date_range <- reactiveVal(NULL) # Track previous date range for bi-directional sync
|
| 93 |
+
url_initialized <- reactiveVal(FALSE)
|
| 94 |
+
|
| 95 |
+
# --- URL Parameter Parsing ---
|
| 96 |
+
parse_url_params <- function(query) {
|
| 97 |
+
params <- list()
|
| 98 |
+
if (length(query) == 0) return(params)
|
| 99 |
+
|
| 100 |
+
# Helper to decode
|
| 101 |
+
decode <- function(x) URLdecode(x)
|
| 102 |
+
|
| 103 |
+
# Parse query string
|
| 104 |
+
pairs <- strsplit(query, "&")[[1]]
|
| 105 |
+
for (pair in pairs) {
|
| 106 |
+
parts <- strsplit(pair, "=")[[1]]
|
| 107 |
+
if (length(parts) == 2) {
|
| 108 |
+
key <- parts[1]
|
| 109 |
+
val <- decode(parts[2])
|
| 110 |
+
params[[key]] <- val
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
params
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
# Observer: Apply URL params on app startup
|
| 117 |
+
observe({
|
| 118 |
+
req(!url_initialized())
|
| 119 |
+
query <- session$clientData$url_search
|
| 120 |
+
# Wait for query to be available (it might be empty string initially)
|
| 121 |
+
# But we also want to handle no-params case to set initialized=TRUE
|
| 122 |
+
|
| 123 |
+
# Parse query (even if empty to confirm no params)
|
| 124 |
+
# Note: session$clientData$url_search usually starts with "?"
|
| 125 |
+
q_str <- sub("^\\?", "", query)
|
| 126 |
+
params <- parse_url_params(q_str)
|
| 127 |
+
|
| 128 |
+
# If params exist, apply them
|
| 129 |
+
if (length(params) > 0) {
|
| 130 |
+
|
| 131 |
+
# 1. Resolution
|
| 132 |
+
if (!is.null(params$resolution)) {
|
| 133 |
+
updateRadioButtons(session, "data_resolution", selected = params$resolution)
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# 2. Date Range
|
| 137 |
+
if (!is.null(params$start) && !is.null(params$end)) {
|
| 138 |
+
updateDateRangeInput(session, "date_range", start = params$start, end = params$end)
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
# 3. Station Selection
|
| 142 |
+
if (!is.null(params$station)) {
|
| 143 |
+
station_ref <- params$station
|
| 144 |
+
# We need to wait for stations to load?
|
| 145 |
+
# The all_stations() reactive depends on resolution.
|
| 146 |
+
# Just updating the input might trigger the search.
|
| 147 |
+
# Note: station_selector choices are updated dynamically.
|
| 148 |
+
# We might need to handle this carefully if stations aren't loaded yet.
|
| 149 |
+
# However, updateSelectizeInput usually works nicely.
|
| 150 |
+
|
| 151 |
+
# Check if we need to set selected
|
| 152 |
+
shinyjs::delay(500, {
|
| 153 |
+
# Try to find ID if name was passed
|
| 154 |
+
# For now just set the value, assuming ID or Name match
|
| 155 |
+
# DWD station selector choices are "Name (ID)" = ID.
|
| 156 |
+
|
| 157 |
+
# If passed param is a name, we might need to lookup.
|
| 158 |
+
# But if we rely on broadcast sending Name, URL has Name.
|
| 159 |
+
# DWD selector needs ID.
|
| 160 |
+
|
| 161 |
+
# Search in current all_stations()
|
| 162 |
+
current_st <- isolate(all_stations())
|
| 163 |
+
if (!is.null(current_st)) {
|
| 164 |
+
# Try match ID
|
| 165 |
+
match <- current_st %>% filter(id == station_ref)
|
| 166 |
+
if (nrow(match) == 0) {
|
| 167 |
+
# Try match Name
|
| 168 |
+
match <- current_st %>% filter(name == station_ref)
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
if (nrow(match) > 0) {
|
| 172 |
+
target_id <- match$id[1]
|
| 173 |
+
updateSelectizeInput(session, "station_selector", selected = target_id)
|
| 174 |
+
|
| 175 |
+
# Also trigger details panel if view implies it
|
| 176 |
+
# The observer for station_selector will run and trigger map logic
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
})
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# 4. View/Tab
|
| 183 |
+
if (!is.null(params$view)) {
|
| 184 |
+
view <- params$view
|
| 185 |
+
shinyjs::delay(800, {
|
| 186 |
+
if (view == "map") {
|
| 187 |
+
nav_select("main_nav", "Map View") # using bslib way or updateNavbarPage
|
| 188 |
+
# DWD ui uses page_navbar id="main_nav".
|
| 189 |
+
# updateNavbarPage(session, "main_nav", selected = "Map View") should work?
|
| 190 |
+
# Or custom message if updateNavbarPage isn't standard in bslib setups?
|
| 191 |
+
# UI line 3: id="main_nav".
|
| 192 |
+
} else if (view == "station-info") {
|
| 193 |
+
updateNavbarPage(session, "main_nav", selected = "Stations Info")
|
| 194 |
+
} else if (grepl("dashboard", view)) {
|
| 195 |
+
updateNavbarPage(session, "main_nav", selected = "Dashboard")
|
| 196 |
+
|
| 197 |
+
if (view == "dashboard-data") {
|
| 198 |
+
shinyjs::delay(200, {
|
| 199 |
+
nav_select("dashboard_subtabs", "Data")
|
| 200 |
+
# navset_card_pill id="dashboard_subtabs"
|
| 201 |
+
})
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
})
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
url_initialized(TRUE)
|
| 209 |
+
})
|
| 210 |
|
| 211 |
# --- Reactive Data Sources (Resolution Dependent) ---
|
| 212 |
observeEvent(input$data_resolution,
|
|
|
|
| 357 |
}
|
| 358 |
})
|
| 359 |
|
| 360 |
+
|
| 361 |
+
# Helper: Broadcast current state to parent page
|
| 362 |
+
broadcast_state <- function(view_override = NULL) {
|
| 363 |
+
# Get active station
|
| 364 |
+
sid <- current_station_id()
|
| 365 |
+
st_meta <- NULL
|
| 366 |
+
if (!is.null(sid)) {
|
| 367 |
+
all <- isolate(all_stations()) # Use isolate to avoid dependency loop if called inside observer?
|
| 368 |
+
# Actually active observers call this, so it's fine.
|
| 369 |
+
if (!is.null(all)) {
|
| 370 |
+
st_meta <- all %>% filter(id == sid) %>% head(1)
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
station_id <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$id) else NULL
|
| 375 |
+
station_name <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$name) else NULL
|
| 376 |
+
landname <- if (!is.null(st_meta) && nrow(st_meta) > 0) as.character(st_meta$state) else NULL
|
| 377 |
+
|
| 378 |
+
# Ensure we have a valid resolution string (Capitalized from UI?)
|
| 379 |
+
resolution <- input$data_resolution
|
| 380 |
+
|
| 381 |
+
# Determine current view
|
| 382 |
+
main_tab <- input$main_nav
|
| 383 |
+
view <- if (!is.null(view_override)) {
|
| 384 |
+
view_override
|
| 385 |
+
} else if (!is.null(main_tab)) {
|
| 386 |
+
if (main_tab == "Map View") "map"
|
| 387 |
+
else if (main_tab == "Stations Info") "station-info"
|
| 388 |
+
else if (main_tab == "Dashboard") {
|
| 389 |
+
subtab <- input$dashboard_subtabs
|
| 390 |
+
# If subtab is NULL/loading, default to plots
|
| 391 |
+
if (!is.null(subtab) && subtab == "Data") "dashboard-data"
|
| 392 |
+
else "dashboard-plots"
|
| 393 |
+
}
|
| 394 |
+
else "map"
|
| 395 |
+
} else {
|
| 396 |
+
"map"
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
start_date <- if (!is.null(input$date_range)) as.character(input$date_range[1]) else NULL
|
| 400 |
+
end_date <- if (!is.null(input$date_range)) as.character(input$date_range[2]) else NULL
|
| 401 |
+
|
| 402 |
+
session$sendCustomMessage("updateParentURL", list(
|
| 403 |
+
station = station_id,
|
| 404 |
+
stationName = station_name,
|
| 405 |
+
landname = landname,
|
| 406 |
+
resolution = resolution,
|
| 407 |
+
view = view,
|
| 408 |
+
start = start_date,
|
| 409 |
+
end = end_date
|
| 410 |
+
))
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
# --- Selection Logic via Dropdown ---
|
| 414 |
observeEvent(input$station_selector, {
|
| 415 |
req(input$station_selector)
|
|
|
|
| 1181 |
fetch_stage(0)
|
| 1182 |
|
| 1183 |
# Navigate to Dashboard tab
|
| 1184 |
+
if (input$main_nav != "Dashboard") {
|
| 1185 |
+
updateNavbarPage(session, "main_nav", selected = "Dashboard")
|
| 1186 |
+
}
|
| 1187 |
|
| 1188 |
# Show rendering message and then unfreeze after delay
|
| 1189 |
session$sendCustomMessage("freezeUI", list(text = "Rendering plots..."))
|
|
|
|
| 1579 |
plot_list
|
| 1580 |
)
|
| 1581 |
})
|
| 1582 |
+
# --- URL Synchronization Observers ---
|
| 1583 |
+
|
| 1584 |
+
# 1. Tab changes
|
| 1585 |
+
observeEvent(input$main_nav, {
|
| 1586 |
+
broadcast_state()
|
| 1587 |
+
}, ignoreInit = TRUE)
|
| 1588 |
+
|
| 1589 |
+
# 2. Station ID change (covers map click and selector)
|
| 1590 |
+
observeEvent(current_station_id(), {
|
| 1591 |
+
broadcast_state()
|
| 1592 |
+
}, ignoreInit = TRUE)
|
| 1593 |
+
|
| 1594 |
+
# 3. Dashboard subtab changes
|
| 1595 |
+
observeEvent(input$dashboard_subtabs, {
|
| 1596 |
+
broadcast_state()
|
| 1597 |
+
}, ignoreInit = TRUE)
|
| 1598 |
+
|
| 1599 |
+
# 4. Resolution changes
|
| 1600 |
+
observeEvent(input$data_resolution, {
|
| 1601 |
+
broadcast_state()
|
| 1602 |
+
}, ignoreInit = TRUE)
|
| 1603 |
+
|
| 1604 |
+
# 5. Date Range changes
|
| 1605 |
+
observeEvent(input$date_range, {
|
| 1606 |
+
broadcast_state()
|
| 1607 |
+
}, ignoreInit = TRUE)
|
| 1608 |
}
|
www/app.js
CHANGED
|
@@ -38,3 +38,34 @@ Shiny.addCustomMessageHandler('unfreezeUI', function (message) {
|
|
| 38 |
$('body').removeClass('ui-frozen');
|
| 39 |
$('.frozen-overlay').removeClass('active');
|
| 40 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
$('body').removeClass('ui-frozen');
|
| 39 |
$('.frozen-overlay').removeClass('active');
|
| 40 |
});
|
| 41 |
+
|
| 42 |
+
// Switch Tab helper
|
| 43 |
+
Shiny.addCustomMessageHandler('switchTab', function (message) {
|
| 44 |
+
console.log("Switching to tab: " + message.tabId);
|
| 45 |
+
// Find the nav link
|
| 46 |
+
var tabLink = $('a[data-value="' + message.tabId + '"]');
|
| 47 |
+
if (tabLink.length > 0) {
|
| 48 |
+
// Trigger click natively to ensure Shiny inputs update
|
| 49 |
+
tabLink[0].click();
|
| 50 |
+
} else {
|
| 51 |
+
console.warn("Tab not found: " + message.tabId);
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
// Update parent page URL (for SEO/AdSense integration)
|
| 56 |
+
Shiny.addCustomMessageHandler('updateParentURL', function (message) {
|
| 57 |
+
console.log("Updating parent URL with state:", message);
|
| 58 |
+
// Send message to parent window (Quarto page)
|
| 59 |
+
if (window.parent !== window) {
|
| 60 |
+
window.parent.postMessage({
|
| 61 |
+
type: 'dwd-state-update', // Distinct type for DWD
|
| 62 |
+
station: message.station || null,
|
| 63 |
+
stationName: message.stationName || null,
|
| 64 |
+
landname: message.landname || null,
|
| 65 |
+
resolution: message.resolution || null,
|
| 66 |
+
view: message.view || null,
|
| 67 |
+
start: message.start || null,
|
| 68 |
+
end: message.end || null
|
| 69 |
+
}, '*');
|
| 70 |
+
}
|
| 71 |
+
});
|