developmentseed / developmentseed/deck.gl-raster

ZarrLayer: support band/channel-last arrays ([y, x, band]) — multiscale GeoZarr RGB(A) pyramids fail validateSpatialDimOrder

Open
#604 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.