From ba4b88e020da5a53019eedb01b38f22f477df5f5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 19:08:02 +0000
Subject: [PATCH 1/3] Initial plan
From fc19cab82ce50cdca5338ca962758d7c21dc3905 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 19:09:53 +0000
Subject: [PATCH 2/3] Add R Shiny SST app and updated README
Agent-Logs-Url: https://github.com/nmfs-opensci/copilot-workshop-demo/sessions/169cb6c3-4d09-4ea4-8ad1-5e9a70d07ed3
Co-authored-by: eeholmes <2545978+eeholmes@users.noreply.github.com>
---
README.md | 93 ++++++++++++++++++++-
app.R | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 334 insertions(+), 2 deletions(-)
create mode 100644 app.R
diff --git a/README.md b/README.md
index ae641bc..5b5e1f6 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,92 @@
-# copilot-workshop
+# NWFSC Survey Grid SST Shiny App
-create a shiny app
+An R Shiny application that visualises **Mean Sea Surface Temperature (SST)**
+on NWFSC trawl-survey grid points along the US West Coast.
+
+---
+
+## What it does
+
+* Loads the NWFSC Combo survey grid (lat/lon points) from the
+ [`surveyjoin`](https://github.com/DFO-NOAA-Pacific/surveyjoin) package.
+* Fetches daily SST from the NOAA CoastWatch ERDDAP server
+ (dataset `ncdcOisst21Agg_LonPM180`) for any date the user selects.
+* Displays an interactive **Leaflet** map centred on the WA/OR coast.
+* Colours each grid point by its SST value using the **viridis** palette and
+ shows a colour legend.
+* Clicking a point opens a popup with the **Grid Cell ID** and **Mean SST (°C)**.
+* Gracefully handles unavailable dates with an informative message instead of
+ crashing.
+
+---
+
+## Required packages
+
+| Package | Source |
+|---------|--------|
+| `shiny` | CRAN |
+| `leaflet` | CRAN |
+| `rerddap` | CRAN |
+| `viridis` | CRAN |
+| `dplyr` | CRAN |
+| `surveyjoin` | GitHub – `DFO-NOAA-Pacific/surveyjoin` |
+
+---
+
+## Installation
+
+### 1 – Install system libraries (Linux / Ubuntu)
+
+```bash
+sudo apt-get update
+sudo apt-get install -y libcurl4-openssl-dev libssl-dev libxml2-dev \
+ libgdal-dev libgeos-dev libproj-dev libudunits2-dev
+```
+
+### 2 – Install R packages
+
+```r
+install.packages("pak", repos = "https://r-lib.github.io/p/pak/dev/")
+pak::pkg_install(c("shiny", "leaflet", "rerddap", "dplyr", "viridis"))
+pak::pkg_install("DFO-NOAA-Pacific/surveyjoin")
+```
+
+---
+
+## Running the app
+
+### From the R console
+
+```r
+shiny::runApp("app.R")
+```
+
+### From the command line
+
+```bash
+Rscript -e "shiny::runApp('app.R')"
+```
+
+The app will open in your default browser.
+If it does not open automatically, navigate to the URL shown in the console
+(e.g. `http://127.0.0.1:XXXX`).
+
+---
+
+## Using the app
+
+1. The date picker is pre-populated with the full range of dates available on
+ the ERDDAP server. Select a date and click **Fetch SST**.
+2. Grid points will appear on the map coloured by SST.
+3. Click any point to see its **Grid Cell ID** and **Mean SST (°C)** in a popup.
+4. If the selected date has no data on the server a message is displayed in the
+ sidebar – simply choose another date.
+
+---
+
+## Data sources
+
+* **SST** – NOAA OISSTv2.1 daily composites via ERDDAP
+ Dataset ID: `ncdcOisst21Agg_LonPM180`
+ URL:
+* **Survey grid** – `surveyjoin::nwfsc_grid` (NWFSC.Combo survey)
diff --git a/app.R b/app.R
new file mode 100644
index 0000000..dc2d4ae
--- /dev/null
+++ b/app.R
@@ -0,0 +1,243 @@
+library(shiny)
+library(leaflet)
+library(rerddap)
+library(surveyjoin)
+library(viridis)
+library(dplyr)
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+#' Return available SST dates from the ERDDAP dataset metadata.
+#' Returns a character vector of "YYYY-MM-DD" strings, or NULL on failure.
+get_erddap_dates <- function() {
+ tryCatch({
+ info_obj <- info("ncdcOisst21Agg_LonPM180",
+ url = "https://coastwatch.pfeg.noaa.gov/erddap/")
+ time_meta <- info_obj$alldata$time
+ # actual_range row contains "start, end" epoch seconds
+ range_row <- time_meta[time_meta$attribute_name == "actual_range", "value"]
+ # nValues row gives the count and spacing
+ nval_row <- time_meta[time_meta$attribute_name == "" &
+ grepl("nValues", time_meta$value), "value"]
+
+ # Parse start / end from actual_range (two doubles separated by ", ")
+ parts <- as.numeric(strsplit(trimws(range_row), ",\\s*")[[1]])
+ t_start <- as.Date(as.POSIXct(parts[1], origin = "1970-01-01", tz = "UTC"))
+ t_end <- as.Date(as.POSIXct(parts[2], origin = "1970-01-01", tz = "UTC"))
+
+ seq(t_start, t_end, by = "day")
+ }, error = function(e) {
+ NULL
+ })
+}
+
+#' Fetch SST for a single date and match it to the nwfsc_grid points.
+#' @param grid data.frame with columns lon, lat (NWFSC.Combo subset)
+#' @param date Date object
+#' @return data.frame: grid with an added "sst" column, or NULL on failure
+fetch_sst <- function(grid, date) {
+ tryCatch({
+ lon_range <- range(grid$lon, na.rm = TRUE)
+ lat_range <- range(grid$lat, na.rm = TRUE)
+ date_str <- format(date, "%Y-%m-%d")
+
+ raw <- griddap(
+ "ncdcOisst21Agg_LonPM180",
+ url = "https://coastwatch.pfeg.noaa.gov/erddap/",
+ time = c(date_str, date_str),
+ longitude = lon_range,
+ latitude = lat_range,
+ fields = "sst"
+ )
+
+ sst_df <- raw$data
+ if (is.null(sst_df) || nrow(sst_df) == 0) return(NULL)
+
+ # Rename to common column names
+ names(sst_df) <- tolower(names(sst_df))
+ sst_df <- sst_df[, c("longitude", "latitude", "sst")]
+ sst_df <- sst_df[!is.na(sst_df$sst), ]
+
+ # For each grid point find the nearest SST raster cell
+ # (vectorised nearest-neighbour via outer difference)
+ matched_sst <- vapply(seq_len(nrow(grid)), function(i) {
+ dx <- (sst_df$longitude - grid$lon[i])^2
+ dy <- (sst_df$latitude - grid$lat[i])^2
+ idx <- which.min(dx + dy)
+ if (length(idx) == 0) NA_real_ else sst_df$sst[idx]
+ }, numeric(1))
+
+ grid$sst <- matched_sst
+ grid
+ }, error = function(e) {
+ message("fetch_sst error: ", conditionMessage(e))
+ NULL
+ })
+}
+
+# ---------------------------------------------------------------------------
+# Pre-load static data (runs once at startup)
+# ---------------------------------------------------------------------------
+
+nwfsc <- surveyjoin::nwfsc_grid
+nwfsc <- nwfsc[nwfsc$survey == "NWFSC.Combo", ]
+nwfsc$cell_id <- seq_len(nrow(nwfsc))
+
+# ---------------------------------------------------------------------------
+# UI
+# ---------------------------------------------------------------------------
+
+ui <- fluidPage(
+ titlePanel("NWFSC Survey Grid – Sea Surface Temperature"),
+
+ sidebarLayout(
+ sidebarPanel(
+ width = 3,
+ h4("Select a Date"),
+ uiOutput("date_ui"),
+ br(),
+ actionButton("fetch_btn", "Fetch SST", class = "btn-primary"),
+ br(), br(),
+ helpText(
+ "Points are colored by SST (°C) using the viridis palette.",
+ "Click a point to see its Mean SST and Grid Cell ID."
+ ),
+ br(),
+ verbatimTextOutput("status_msg")
+ ),
+
+ mainPanel(
+ width = 9,
+ leafletOutput("map", height = "80vh")
+ )
+ )
+)
+
+# ---------------------------------------------------------------------------
+# Server
+# ---------------------------------------------------------------------------
+
+server <- function(input, output, session) {
+
+ # -- Fetch available dates once per session --------------------------------
+ available_dates <- reactive({
+ withProgress(message = "Loading available dates…", value = 0.3, {
+ d <- get_erddap_dates()
+ if (is.null(d)) {
+ showNotification(
+ "Could not retrieve date range from ERDDAP. Using last 30 days as fallback.",
+ type = "warning", duration = 10
+ )
+ d <- seq(Sys.Date() - 30, Sys.Date(), by = "day")
+ }
+ d
+ })
+ })
+
+ # -- Render date picker ----------------------------------------------------
+ output$date_ui <- renderUI({
+ dates <- available_dates()
+ dateInput(
+ "sel_date",
+ label = NULL,
+ value = max(dates) - 1L, # default: most-recent minus 1 (often available)
+ min = min(dates),
+ max = max(dates)
+ )
+ })
+
+ # -- Base leaflet map (rendered once) --------------------------------------
+ output$map <- renderLeaflet({
+ leaflet() %>%
+ addTiles() %>%
+ setView(lng = -125, lat = 46, zoom = 5)
+ })
+
+ # -- Status message --------------------------------------------------------
+ status <- reactiveVal("")
+ output$status_msg <- renderText(status())
+
+ # -- Fetch SST on button click ---------------------------------------------
+ sst_data <- eventReactive(input$fetch_btn, {
+ req(input$sel_date)
+
+ sel_date <- as.Date(input$sel_date)
+
+ # Validate against available dates
+ dates <- available_dates()
+ if (!is.null(dates) && !sel_date %in% dates) {
+ status(paste0(
+ "Date ", sel_date,
+ " is not available on the ERDDAP server. Please choose another date."
+ ))
+ return(NULL)
+ }
+
+ status("Fetching SST data…")
+
+ result <- withProgress(message = "Fetching SST from ERDDAP…", value = 0.5, {
+ fetch_sst(nwfsc, sel_date)
+ })
+
+ if (is.null(result)) {
+ status(paste0(
+ "No SST data found for ", sel_date,
+ ". Please choose another date."
+ ))
+ return(NULL)
+ }
+
+ status(paste0("SST loaded for ", sel_date,
+ " (", sum(!is.na(result$sst)), " points)."))
+ result
+ })
+
+ # -- Update map whenever sst_data changes ----------------------------------
+ observe({
+ df <- sst_data()
+ req(!is.null(df), nrow(df) > 0)
+
+ df_valid <- df[!is.na(df$sst), ]
+ if (nrow(df_valid) == 0) return()
+
+ sst_vals <- df_valid$sst
+ pal <- colorNumeric(
+ palette = viridis(256),
+ domain = sst_vals,
+ na.color = "transparent"
+ )
+
+ leafletProxy("map", data = df_valid) %>%
+ clearMarkers() %>%
+ clearControls() %>%
+ addCircleMarkers(
+ lng = ~lon,
+ lat = ~lat,
+ radius = 4,
+ color = ~pal(sst),
+ stroke = FALSE,
+ fillOpacity = 0.85,
+ popup = ~paste0(
+ "Grid Cell ID: ", cell_id, "
",
+ "Mean SST: ", round(sst, 2), " °C
",
+ "Lon: ", round(lon, 3), "
",
+ "Lat: ", round(lat, 3)
+ )
+ ) %>%
+ addLegend(
+ position = "bottomright",
+ pal = pal,
+ values = sst_vals,
+ title = "SST (°C)",
+ opacity = 0.85
+ )
+ })
+}
+
+# ---------------------------------------------------------------------------
+# Launch
+# ---------------------------------------------------------------------------
+
+shinyApp(ui = ui, server = server)
From 537f0faf4e315898c09bfa17ab5366ee68ad0591 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 19:10:59 +0000
Subject: [PATCH 3/3] Fix misleading 'Mean SST' label and add metadata comment
Agent-Logs-Url: https://github.com/nmfs-opensci/copilot-workshop-demo/sessions/169cb6c3-4d09-4ea4-8ad1-5e9a70d07ed3
Co-authored-by: eeholmes <2545978+eeholmes@users.noreply.github.com>
---
README.md | 6 +++---
app.R | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 5b5e1f6..44bcbc2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# NWFSC Survey Grid SST Shiny App
-An R Shiny application that visualises **Mean Sea Surface Temperature (SST)**
+An R Shiny application that visualises **Sea Surface Temperature (SST)**
on NWFSC trawl-survey grid points along the US West Coast.
---
@@ -14,7 +14,7 @@ on NWFSC trawl-survey grid points along the US West Coast.
* Displays an interactive **Leaflet** map centred on the WA/OR coast.
* Colours each grid point by its SST value using the **viridis** palette and
shows a colour legend.
-* Clicking a point opens a popup with the **Grid Cell ID** and **Mean SST (°C)**.
+* Clicking a point opens a popup with the **Grid Cell ID** and **SST (°C)**.
* Gracefully handles unavailable dates with an informative message instead of
crashing.
@@ -78,7 +78,7 @@ If it does not open automatically, navigate to the URL shown in the console
1. The date picker is pre-populated with the full range of dates available on
the ERDDAP server. Select a date and click **Fetch SST**.
2. Grid points will appear on the map coloured by SST.
-3. Click any point to see its **Grid Cell ID** and **Mean SST (°C)** in a popup.
+3. Click any point to see its **Grid Cell ID** and **SST (°C)** in a popup.
4. If the selected date has no data on the server a message is displayed in the
sidebar – simply choose another date.
diff --git a/app.R b/app.R
index dc2d4ae..bbbf68d 100644
--- a/app.R
+++ b/app.R
@@ -18,7 +18,7 @@ get_erddap_dates <- function() {
time_meta <- info_obj$alldata$time
# actual_range row contains "start, end" epoch seconds
range_row <- time_meta[time_meta$attribute_name == "actual_range", "value"]
- # nValues row gives the count and spacing
+ # Dimension rows have an empty attribute_name; the nValues row describes spacing
nval_row <- time_meta[time_meta$attribute_name == "" &
grepl("nValues", time_meta$value), "value"]
@@ -221,7 +221,7 @@ server <- function(input, output, session) {
fillOpacity = 0.85,
popup = ~paste0(
"Grid Cell ID: ", cell_id, "
",
- "Mean SST: ", round(sst, 2), " °C
",
+ "SST: ", round(sst, 2), " °C
",
"Lon: ", round(lon, 3), "
",
"Lat: ", round(lat, 3)
)