Appendix - Land Use Land Cover Change
library(drift)
library(sf)
library(terra)
library(tmap)
library(maptiles)
library(ggplot2)
library(dplyr)
library(tidyr)
sf::sf_use_s2(FALSE)
lulc_dir <- here::here("data", "lulc")
# Pre-computed by scripts/lulc_classify.R
lulc_all <- readRDS(file.path(lulc_dir, "lulc_summary.rds"))
# Spatial inputs
subbasins <- sf::st_read(
file.path(lulc_dir, "subbasins.gpkg"), quiet = TRUE
) |> sf::st_transform(4326)
fp_file <- file.path(lulc_dir, "floodplain_neexdzii_co_ff06.gpkg")
floodplain <- sf::st_read(fp_file, quiet = TRUE) |> sf::st_transform(4326)
# name_basin comes from break_points.csv via fresh::frs_watershed_split5.1 Modelled Floodplain
Land cover change is assessed within the modelled floodplain — the valley bottom area delineated by the Valley Confinement Algorithm (VCA, flooded package) for coho rearing/spawning habitat on streams of 3rd order and greater, with lakes and wetlands included. The VCA uses a bankfull regression scaled by flood_factor = 6 to approximate the functional floodplain — the area regularly influenced by high flows that sustains riparian vegetation, off-channel habitat, and large wood recruitment processes critical to salmon. The modelled floodplain covers 210.2 km² along the Neexdzii Kwa (Bulkley River) and tributaries. All percentages and areas reported below refer to land cover within this modelled floodplain, not the full watershed.
# Use scenario-specific raster dir, fall back to legacy
fp_dir <- here::here("data", "lulc", "rasters", "co_ff06")
if (!dir.exists(fp_dir)) fp_dir <- here::here("data", "lulc", "rasters", "floodplain")
# Load classified rasters as named list
classified <- list(
"2017" = terra::rast(file.path(fp_dir, "classified_2017.tif")),
"2020" = terra::rast(file.path(fp_dir, "classified_2020.tif")),
"2023" = terra::rast(file.path(fp_dir, "classified_2023.tif"))
)
# Load transition raster, filter summary to agriculture classes only
ag_classes <- c("Crops", "Rangeland", "Bare Ground")
trans_rast <- terra::rast(file.path(fp_dir, "transition.tif"))
trans_cats <- terra::cats(trans_rast)[[1]]
trans_summary <- tibble::tibble(
id = trans_cats$id,
transition = trans_cats$transition,
from_class = "Trees",
to_class = sub("Trees -> ", "", trans_cats$transition)
) |>
dplyr::filter(to_class %in% ag_classes)
transition <- list(raster = trans_rast, summary = trans_summary)
# Build interactive map with drift
m <- drift::dft_map_interactive(
classified,
aoi = floodplain,
transition = transition,
source = "io-lulc"
)
# Transition groups for layer control
trans_groups <- trans_summary$transition
# High-contrast sub-basin palette for interactive map (Brewer Set3, 9 classes)
sb_pal <- RColorBrewer::brewer.pal(9, "Set3")
# Zoom to floodplain extent on load
bb <- sf::st_bbox(sf::st_transform(floodplain, 4326))
m <- m |>
leaflet::fitBounds(bb[["xmin"]], bb[["ymin"]], bb[["xmax"]], bb[["ymax"]]) |>
leaflet::addPolygons(
data = subbasins,
fillColor = sb_pal[seq_len(nrow(subbasins))],
fillOpacity = 0.3,
color = "#333333",
weight = 2, opacity = 0.8, group = "Sub-basins",
label = ~name_basin
) |>
leaflet::addLegend(
position = "bottomleft",
colors = sb_pal[seq_len(nrow(subbasins))],
labels = subbasins$name_basin,
title = "Sub-basin",
opacity = 0.8,
group = "Sub-basins"
) |>
leaflet::addProviderTiles("OpenTopoMap", group = "OpenTopoMap") |>
leaflet::addLayersControl(
baseGroups = c("Light", "Esri Satellite", "Google Satellite", "OpenTopoMap"),
overlayGroups = c(names(classified), trans_groups, "AOI", "Sub-basins"),
options = leaflet::layersControlOptions(collapsed = FALSE)
) |>
leaflet::hideGroup(setdiff(names(classified), names(classified)[1]))
m