developmentseed / developmentseed/deck.gl-raster
ZarrLayer: support band/channel-last arrays ([y, x, band]) — multiscale GeoZarr RGB(A) pyramids fail validateSpatialDimOrder
- Dominant language
- TypeScript
- Stars
- 228
- Forks
- 29
- Avg merge
- 12h 27m
- Merged PRs (30d)
- 4
Description
# `ZarrLayer`: support band/channel-last arrays (`[y, x, band]`) — multiscale GeoZarr RGB(A) pyramids fail `validateSpatialDimOrder`
## Summary
`ZarrLayer` requires the two spatial dimensions to be the **last** entries in an
array's `dimension_names`. A GeoZarr store that lays its data out as
`[lat, lon, band]` (spatial first, channel/band last) — a natural layout for a
pre-rendered RGB(A) pyramid, where each pixel's RGBA is contiguous — cannot be
rendered. `prepare`/`_parseZarr` throws before any tile loads.
This blocks an otherwise-valid, georeferenced, multiscale GeoZarr pyramid whose
only "problem" is channel-last axis order.
## Environment
- `@developmentseed/deck.gl-zarr` **0.6.1** (monorepo `developmentseed/deck.gl-raster`)
- zarrita v3 store, consolidated metadata
## Reproduction
Public store (Zarr v3, no auth):
```
https://tessera-embeddings.s3.us-west-2.amazonaws.com/v1.1/cambridge.zarr
```
The renderable layer is the multiscale RGB pyramid subgroup `global_rgb/`:
- Root group attrs declare the `zarr-conventions/multiscales` v1 layout
(`multiscales: { layout: [{ asset: "0", transform: { scale, translation } }, …] }`,
levels `0`=native … `9`=coarsest) plus the `spatial:`/`proj:` conventions
(`proj:code: "EPSG:4326"`, `spatial:bbox: [-180, -90, 180, 90]`).
- Each level is an array at `global_rgb//rgb` with:
- `dimension_names: ["lat", "lon", "band"]`
- `shape: [H, W, 4]` (e.g. level 0 = `[1800000, 3600000, 4]`, level 9 = `[3515, 7031, 4]`)
- `dtype: uint8`, `chunk_shape: [512, 512, 4]`
Minimal driver (pass the `global_rgb` group as `node`, with synthesized GeoZarr
`metadata` whose `spatial:dimensions` = `["lat", "lon"]` and one `layout` entry
per level pointing at `/rgb`):
```ts
new ZarrLayer({
node: globalRgbGroup, // zarr.Group
metadata: synthesizedGeoZarr, // multiscales.layout → "/rgb", spatial:dimensions ["lat","lon"]
selection: { band: null }, // band is the only non-spatial dim
getTileData, // would upload [H, W, 4] uint8 as rgba8unorm
renderTile,
});
```
## What happens
`_parseZarr()` → `validateSpatialDimOrder()` throws:
```
Spatial dims must be the last two entries in dimension_names:
expected ["lat", "lon"] at positions [1, 2], got ["lon", "band"].
Transpose is not implemented yet.
```
Even if that check were relaxed, a second assumption blocks it — tile sizes are
read from the trailing dims in `zarr-layer.js`:
```js
const chunkSizes = arrays.map((arr) => ({
width: arr.chunks[arr.chunks.length - 1], // = 4 (band), should be 512 (lon)
height: arr.chunks[arr.chunks.length - 2], // = 512 (lat), should be 512
}));
```
So the tiler would derive a 4-px-wide tile from the band chunk.
## Why it matters
`[y, x, band]` (channel-last) is a common interleaved layout for RGB(A) imagery —
each pixel's channels are contiguous, which is exactly what a GPU `rgba8unorm`
texture upload wants (no transpose, no per-channel gather). A producer writing a
pre-rendered web pyramid will reach for it. Today such stores are unrenderable
through `ZarrLayer` regardless of being fully GeoZarr-compliant.
## Expected
`ZarrLayer` should render a georeferenced multiscale array whose spatial dims are
not the last two — at minimum the common channel-last RGB(A) case
(`[y, x, band]`).
## Possible directions
1. **Honor `spatial:dimensions` positionally.** Use the declared spatial-dim
indices (`yAxisIndex`/`xAxisIndex`) to drive both the `sliceSpec` and the
`chunkSizes` (`arr.chunks[yAxisIndex]` / `arr.chunks[xAxisIndex]`) instead of
assuming `len-2` / `len-1`. The non-spatial dims (and their order) then fall
out of `selection`, and `getTileData` receives the chunk in the store's native
axis order — the consumer can upload it as-is.
2. **Implement the transpose the error message already anticipates** ("Transpose
is not implemented yet") — at least for a trailing channel dim.
3. If neither is in scope short-term, a documented escape hatch (e.g. let the
caller supply `chunkSizes` / a tile-shape override and skip
`validateSpatialDimOrder`) would unblock channel-last sources.
## Code pointers (0.6.1)
- `dist/validation.js` → `validateSpatialDimOrder` (the throw)
- `dist/zarr-layer.js` → `_parseZarr`, `chunkSizes = arr.chunks[len-1] / [len-2]`
## Workaround
None through `ZarrLayer` without rewriting the store to `[band, y, x]`. The
alternative is to bypass `ZarrLayer` and drive `RasterTileLayer` with a
hand-built `AffineTileset` descriptor, doing the channel-last chunk upload in a
custom `getTileData`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with dist/validation.js and dist/zarr-layer.js, then reproduce the failure using the linked GeoZarr store and the documented global_rgb layout. Trace how spatial dimension positions feed validation, slice specifications, and chunkSizes. Done means a multiscale [lat, lon, band] RGB(A) array passes validation, derives spatial tile sizes correctly, and can render without rewriting the store.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- data, frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100