library(httr2)
library(htmltools)
library(jsonlite)
erddap_base <- "https://coastwatch.pfeg.noaa.gov/erddap/griddap/jplMURSST41anom1day"
dataset_id <- "jplMURSST41anom1day"
n_days <- 5
# Florida Keys and adjacent South Florida shelf
lat_min <- 24.3
lat_max <- 26.5
lon_min <- -83.0
lon_max <- -79.5
fetch_times <- function() {
resp <- request(paste0(erddap_base, ".json?time")) |>
req_perform()
rows <- resp_body_json(resp)$table$rows
vapply(rows, function(row) row[[1]], character(1))
}
mur_image_url <- function(time_iso) {
sprintf(
paste0(
"%s.smallPng?sstAnom[(%s)][(%.1f):(%.1f)][(%.1f):(%.1f)]",
"&.draw=surface",
"&.vars=longitude|latitude|sstAnom",
"&.colorBar=|||||",
"&.land=over"
),
erddap_base,
time_iso,
lat_min,
lat_max,
lon_min,
lon_max
)
}
format_time_label <- function(time_iso) {
format(
as.POSIXct(time_iso, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC"),
"%Y-%m-%d UTC"
)
}
mur_image_row <- function(times) {
tags$div(
class = "mur-sst-row",
lapply(times, function(time_iso) {
tags$div(
class = "mur-sst-panel",
tags$img(
src = mur_image_url(time_iso),
alt = paste("SST anomaly on", format_time_label(time_iso))
),
tags$p(format_time_label(time_iso))
)
})
)
}
mur_image_animator <- function(times, interval_ms = 900) {
frame_urls <- vapply(times, mur_image_url, character(1))
frame_labels <- vapply(times, format_time_label, character(1))
widget_id <- "mur-sst-anim"
tags$div(
class = "mur-sst-anim",
id = widget_id,
`data-frame-urls` = as.character(jsonlite::toJSON(frame_urls, auto_unbox = TRUE)),
`data-frame-labels` = as.character(jsonlite::toJSON(frame_labels, auto_unbox = TRUE)),
`data-interval-ms` = interval_ms,
tags$div(
class = "mur-sst-anim-frame",
tags$img(
id = paste0(widget_id, "-img"),
src = frame_urls[[1]],
alt = paste("SST anomaly animation starting", frame_labels[[1]])
)
),
tags$div(
class = "mur-sst-anim-controls",
tags$button("Prev", type = "button", class = "btn btn-default mur-sst-prev"),
tags$button("Pause", type = "button", class = "btn btn-primary mur-sst-toggle"),
tags$button("Next", type = "button", class = "btn btn-default mur-sst-next"),
tags$span(
class = "mur-sst-anim-label",
id = paste0(widget_id, "-label"),
frame_labels[[1]]
)
),
tags$script(HTML("
(function() {
function initMurSstAnim(root) {
if (!root || root.dataset.murSstInit === 'true') return;
root.dataset.murSstInit = 'true';
const urls = JSON.parse(root.dataset.frameUrls);
const labels = JSON.parse(root.dataset.frameLabels);
const intervalMs = Number(root.dataset.intervalMs || 900);
const img = root.querySelector('img');
const label = root.querySelector('.mur-sst-anim-label');
const toggleBtn = root.querySelector('.mur-sst-toggle');
const prevBtn = root.querySelector('.mur-sst-prev');
const nextBtn = root.querySelector('.mur-sst-next');
let index = 0;
let playing = true;
let timer = null;
function showFrame(nextIndex) {
index = (nextIndex + urls.length) % urls.length;
img.src = urls[index];
img.alt = 'SST anomaly on ' + labels[index];
label.textContent = labels[index];
}
function setPlaying(state) {
playing = state;
toggleBtn.textContent = playing ? 'Pause' : 'Play';
if (playing) {
timer = window.setInterval(function() {
showFrame(index + 1);
}, intervalMs);
} else if (timer !== null) {
window.clearInterval(timer);
timer = null;
}
}
toggleBtn.addEventListener('click', function() {
setPlaying(!playing);
});
prevBtn.addEventListener('click', function() {
showFrame(index - 1);
});
nextBtn.addEventListener('click', function() {
showFrame(index + 1);
});
setPlaying(true);
}
document.querySelectorAll('.mur-sst-anim').forEach(initMurSstAnim);
})();
"))
)
}
available_times <- fetch_times()
latest_times <- tail(available_times, n_days)
latest_time <- tail(available_times, 1)