stac-elevation-bc serves British
Columbia’s LidarBC elevation data
as a SpatioTemporal Asset Catalog
(STAC) — 102,460 tiles on the provincial
objectstore, searchable by location and time from the rstac R
package, QGIS (v3.42+), or any STAC-compliant client. The endpoint
is https://images.a11s.one.
Each item carries up to two assets from the same flight over the same footprint:
| asset | what it is |
|---|---|
dem |
bare-earth digital elevation model — every item has one |
dsm |
digital surface model, where the delivery published one — 95,888 of 102,460 |
An item’s geometry is its footprint — the part of
the tile that holds data, not the raster’s extent (#2).
For tiles named on the BC Geographic System 1:2,500 grid it is the grid
cell minus any no-data inside it, so a full tile’s footprint is exactly
its cell and neighbours meet without overlapping; for the rest it is the
valid data. A search for an area a tile has no data over no longer
returns that tile. The dem asset carries
raster:bands[0].statistics.valid_percent (the share of the
raster holding elevations) and, where the whole file was read,
file:checksum (sha256) and file:size. The DSM
is assumed to cover what the DEM covers; its own coverage is not
measured. Every item from a LidarBC delivery carries
lidarbc:delivery ("092/092g/2016"), the key stac-pointcloud-bc
items share, so a tile matches its delivery’s point clouds with one
filter (#55);
the lidarbc: fields are defined in crate.
The catalog refreshes monthly: a scheduled GitHub Actions workflow detects
new tiles on the objectstore and appends them incrementally, with each
month’s changes landing as a commit to data/ — a git audit trail of what was
added, removed, and validated. How the automation works, its failure
modes, and where the evidence lives are documented in scripts/, alongside a
plain-language guide to the concepts behind the pipeline (COG, STAC,
pgstac, validation caching).
Use bcdata
to define an area of interest, then query the
stac-elevation-bc collection for elevation tiles
intersecting it. Below: all DEMs covering the Bulkley River watershed
group between 2018 and 2020.
aoi <- bcdata::bcdc_query_geodata("freshwater-atlas-watershed-groups") |>
bcdata::filter(WATERSHED_GROUP_NAME == "Bulkley River") |>
bcdata::collect() |>
sf::st_transform(crs = 4326)
date_start <- "2018-01-01T00:00:00Z"
date_end <- "2020-12-31T00:00:00Z"
# use rstac to query the collection
q <- rstac::stac("https://images.a11s.one/") |>
rstac::stac_search(
collections = "stac-elevation-bc",
intersects = jsonlite::fromJSON(
geojsonsf::sf_geojson(
aoi, atomise = TRUE, simplify = FALSE
),
simplifyVector = FALSE
) |> (\(x) x$geometry)(),
datetime = paste0(date_start, "/", date_end)
) |>
rstac::post_request()
# get details of the items
r <- q |>
rstac::items_fetch()
# burn the results locally so we can serve it instantly on index.html builds
saveRDS(r, "data/stac_result.rds")
r <- readRDS("data/stac_result.rds")
# One row per ASSET, not per item. Every item carries a bare-earth `dem`, and
# most also carry a `dsm` from the same flight -- a dem-only column hid half of
# what the collection serves.
tab <- purrr::map_dfr(r$features, function(f) {
purrr::imap_dfr(f$assets, function(a, key) {
tibble::tibble(
tile = f$id,
date = substr(f$properties$datetime, 1, 10),
type = key,
download = glue::glue('<a href="{a$href}" target="_blank">{basename(a$href)}</a>')
)
})
}) |>
dplyr::arrange(tile, type)
Download links for every asset returned by the query above — one row
per asset, so a paired item shows its dem and
dsm together. NOTE: To view all columns in the table -
please click on one of the sort arrows within column headers before
scrolling to the right.
Every row above is a dem, because the Bulkley was flown
between 2000 and 2020 and those deliveries published no surface model.
Province-wide 95,888 of 102,460 items carry a
dsm — the same flight over the same footprint at
the same time — but the coverage is concentrated in the 2024 deliveries,
so whether you get one depends on when your area was last flown rather
than on where it is.
QGIS 3.42 added native STAC support — connect directly to the catalog and filter by the current map view. See Lutra Consulting’s STAC-in-QGIS blog post for a walk-through.
QGIS draws each item’s bbox, not its geometry. Since the
bbox is the bounds of the footprint, a partly empty tile’s box now
shrinks to its data, but a tile whose data is a diagonal strip still
draws as a rectangle.
Connecting to https://images.a11s.one
Using the field of view in QGIS to filter results
The same images.a11s.one STAC API serves several
complementary BC collections:
stac_floodplains_bc
— floodplain land-cover change (stac-floodplains-bc)stac_airphoto_bc
— historic airphoto thumbnails, 1963–2019
(stac-airphoto-bc)stac_uav_bc
— UAV imagery, organized by watershed
(stac-imagery-uav-bc)The big one landed in 2026: the catalog is now self-updating (#23) — the goal open since the first build — and the July catch-up grew the collection from 58k to ~98k fully-validated items. Items now also carry the digital surface model alongside the bare-earth DEM (#31), paired on tile id and acquisition date, and each item’s geometry is its footprint — the tile’s grid cell minus its no-data — rather than its raster extent (#2). Still ahead:
stacs
package, configured by stacs.toml), but it still runs from
a laptop: no GitHub Actions runner can reach the STAC host today.
Closing that needs a tailnet or deploy-key decision in the
infrastructure repo, and it unblocks every catalogue repo at once.dsm/
directory holding only .laz. Recorded as a declared
coverage gap in data/dsm_pairing_report.md; deriving a
raster from the point cloud is unscoped.Browse open issues for the full backlog.
MIT.