---
title: "Build app support files — zone gpkgs, layers registry, metrics raster"
msens:
target_name: build_app_support
workflow_type: release
dependency: [score_zone_metrics] # terminal score target — transitively covers score_cell_metrics (cell_metric) + score_zones (zone_cell), the two tables this reads
output: data/manifests/build_app_support.json
editor_options:
chunk_output_type: console
---
Reproducibly generate the per-version files the Shiny apps (`scores`, `species`) load at
startup — which the v8 scoring pipeline doesn't otherwise emit:
- **`ply_ecoregions_2025.gpkg` / `ply_programareas_2026_{ver}.gpkg`** — the zone polygons
(shared with `ver_prev`; copied to the version dir the apps expect).
- **`layers_{ver}.csv`** — the app's layer registry (order/category/label/key); adapted from
the prior version for the v8 `sp_cat` set (`other` → `primary_producer`).
- **`r_metrics_{ver}.tif`** — a raster stack of the per-cell metrics + `ecoregion_key`/
`programarea_key`, rasterized from `cell_metric` onto the **global cell-id COG** using the
COG-value = `cell_id` lookup (NOT `r[cell_id]<-` pixel-index, which is a v7-grid assumption).
## Design
```{mermaid}
%%| label: fig-design
%%| fig-cap: "Per-version Shiny app-support files: zone gpkgs, layer registry, and the cell-metrics raster"
flowchart LR
prev["ver_prev files<br/>(gpkgs, layers, subregions)"] --> copy["copy + adapt<br/>(other → primary_producer)"]
db["sdm.duckdb<br/>cell_metric + zone_cell"] --> rast["rasterize onto cell-id COG"]
copy --> out[("{ver}/ dir<br/>gpkgs · layers.csv · r_metrics.tif")]
rast --> out
out --> mf["hash_query(r_metrics) → content-addressed manifest"]
```
## Setup
```{r}
#| label: setup
librarian::shelf(DBI, dplyr, duckdb, fs, glue, here, jsonlite, logger, readr, sf, terra, quiet = T)
source(here("libs/paths.R"))
dir_v <- glue("{dir_derived}/{ver}"); dir_create(dir_v)
manifest <- here("data/manifests/build_app_support.json"); dir_create(path_dir(manifest))
stopifnot(all(file_exists(c(sdm_db, cellid_tif))))
```
## Zone polygons → version dir
```{r}
#| label: gpkgs
er_src <- glue("{dir_derived}/{ver_prev}/ply_ecoregions_2025.gpkg")
pa_src <- glue("{dir_derived}/{ver_prev}/ply_programareas_2026_{ver_prev}.gpkg")
stopifnot(all(file_exists(c(er_src, pa_src))))
file_copy(er_src, glue("{dir_v}/ply_ecoregions_2025.gpkg"), overwrite = TRUE)
file_copy(pa_src, glue("{dir_v}/ply_programareas_2026_{ver}.gpkg"), overwrite = TRUE)
log_info("copied ecoregion + programarea gpkgs to {dir_v}")
```
## Layer registry (`layers_{ver}.csv`)
```{r}
#| label: layers
lyrs <- read_csv(glue("{dir_derived}/{ver_prev}/layers_{ver_prev}.csv"), show_col_types = FALSE) |>
mutate(
lyr = gsub("_other", "_primary_producer", lyr), # extrisk_other_* -> extrisk_primary_producer_*
layer = gsub("^other:", "primary producer:", layer)) # relabel
write_csv(lyrs, glue("{dir_v}/layers_{ver}.csv"))
log_info("layers_{ver}.csv: {nrow(lyrs)} layers")
```
## Subregion → programarea map (`subregion_programareas.csv`)
The subregion → programarea map (USA/AK/GA/PA regional groupings) is version-agnostic — copy
`{ver_prev}`'s. `score_zones` builds the matching subregion **zones** (PA unions) so the apps'
"Study area" filter works.
```{r}
#| label: subregions
sr_src <- glue("{dir_derived}/{ver_prev}/subregion_programareas.csv")
if (file_exists(sr_src)) {
file_copy(sr_src, glue("{dir_v}/subregion_programareas.csv"), overwrite = TRUE)
} else { # fallback: whole-study-area USA -> all PRAs
con <- dbConnect(duckdb(sdm_db, read_only = TRUE))
pras <- dbGetQuery(con, "SELECT DISTINCT val FROM zone WHERE fld='programarea_key' ORDER BY val")$val
dbDisconnect(con, shutdown = TRUE)
write_csv(tibble::tibble(subregion_key = "USA", programarea_key = pras), glue("{dir_v}/subregion_programareas.csv"))
}
log_info("subregion_programareas.csv written")
```
## Metrics raster (`r_metrics_{ver}.tif`)
```{r}
#| label: r_metrics
con <- dbConnect(duckdb(sdm_db, read_only = TRUE))
r_cell <- rast(cellid_tif) # pixel value = cell_id
# crop to the programarea polygons' extent (US study area) so it isn't a full global grid
pa <- st_read(glue("{dir_v}/ply_programareas_2026_{ver}.gpkg"), quiet = TRUE)
r_cell <- crop(r_cell, ext(project(vect(pa), r_cell)))
cidx <- values(r_cell, mat = FALSE) # cell_id per pixel (NA on land)
maxid <- max(cidx, na.rm = TRUE)
mk_lut <- function(cell_id, v) { lut <- rep(NA_real_, maxid); lut[cell_id] <- v; lut }
# one band per app layer, plus the raw composite key already in lyrs$lyr
lst <- list()
for (k in unique(lyrs$lyr)) {
d <- dbGetQuery(con, glue("SELECT cm.cell_id, cm.val FROM cell_metric cm
JOIN metric m USING(metric_seq) WHERE m.metric_key = '{k}'"))
if (!nrow(d)) { log_warn("no cells for layer {k}"); next }
r <- setValues(rast(r_cell), mk_lut(d$cell_id, d$val)[cidx]); names(r) <- k
lst[[k]] <- r
}
# categorical zone keys from zone_cell
for (zf in c("ecoregion_key", "programarea_key")) {
d <- dbGetQuery(con, glue("SELECT zc.cell_id, z.val AS key FROM zone_cell zc
JOIN zone z USING(zone_seq) WHERE z.fld = '{zf}'"))
lvls <- sort(unique(d$key)); d$id <- match(d$key, lvls)
r <- setValues(rast(r_cell), mk_lut(d$cell_id, d$id)[cidx]); names(r) <- zf
levels(r) <- data.frame(id = seq_along(lvls), key = lvls)
lst[[zf]] <- r
}
r_metrics <- rast(lst)
if ("primprod" %in% names(r_metrics))
r_metrics[["primprod_log"]] <- log(r_metrics[["primprod"]])
writeRaster(r_metrics, glue("{dir_v}/r_metrics_{ver}.tif"), overwrite = TRUE,
gdal = c("COMPRESS=ZSTD", "TILED=YES"))
n_lyr <- nlyr(r_metrics)
dbDisconnect(con, shutdown = TRUE)
log_info("r_metrics_{ver}.tif: {n_lyr} bands")
```
## Outputs + manifest
```{r}
#| label: manifest
# content fingerprint of the notebook's authoritative output (the metrics raster):
# reduce its non-NA cell values order-independently via DuckDB — deterministic, so no
# wall-clock and no machine paths leak into the tracked manifest.
con <- dbConnect(duckdb())
duckdb::duckdb_register(con, "r_metrics_df", as.data.frame(r_metrics, cells = TRUE, na.rm = TRUE))
h <- msens::hash_query(con, "r_metrics_df")
dbDisconnect(con, shutdown = TRUE)
smry <- tibble::tibble(output = c("layers", "r_metrics_bands"), n = c(nrow(lyrs), n_lyr))
msens::report_table(smry, caption = "app-support outputs")
msens::write_manifest(
manifest, target = "build_app_support", content_hash = h,
stats = list(ver = ver, layers = nrow(lyrs), r_metrics_bands = n_lyr),
force = msens::force_target("build_app_support"))
```