feat: Titiler .npy tile example with RasterTileLayer#469
Conversation
Design for a new example that renders titiler .npy tiles via RasterTileLayer directly (the first example using that layer without a COGLayer/ZarrLayer subclass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copies the cog-basic scaffolding (package.json, tsconfig, vite, index, main) and adds a blank-map App. Adds npyjs and proj4 as deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract titiler helpers (URLs, buildDescriptor, decode, getTileData, renderTile) into src/titiler.ts. App.tsx now only handles the React UI and layer wiring. - Drop unused @developmentseed/proj dep; add peer dep @deck.gl/mesh-layers. - Replace `parsed.data as Uint8Array` cast with an instanceof guard. - Note texture-destroy semantics in getTileData docstring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n TMS Two runtime fixes: 1. /cog/info returns bounds in the COG's native CRS (UTM for Sentinel-2), which MapLibre's fitBounds rejects with "Invalid LngLat latitude value". Switch to /cog/tilejson.json, whose bounds are in WGS84 per the TileJSON spec. Also thread the tilejson's minzoom/maxzoom onto the layer so we don't request tiles past the COG's resolution. 2. Titiler's /tileMatrixSets/WebMercatorQuad response omits the optional boundingBox field, which TileMatrixSetAdaptor requires for viewport culling. Inject one built from the tilejson's geographic bounds projected to EPSG:3857. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Titiler's route is /cog/{tileMatrixSetId}/tilejson.json, not
/cog/tilejson.json?tileMatrixSetId=...; the latter 404s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@kylebarron do you think developmentseed/titiler#1242 could be of any help, like having the number of bands or the datatype ... |
| - `GET /cog/tiles/WebMercatorQuad/{z}/{x}/{y}.npy?url=<COG_URL>` — returns | ||
| a numpy `.npy` file for one tile, shape `(bands, height, width)`, | ||
| dtype `uint8` for an RGB COG. For a 3-band RGB source titiler returns | ||
| 4 bands: R, G, B, mask. |
There was a problem hiding this comment.
where does this limitation comes from? Titiler could server tiles with nBands+masks
There was a problem hiding this comment.
Might be easier to support NPZ files which are binary files with one or two arrays (data and mask) https://github.com/cogeotiff/rio-tiler/blob/ebacf0aeee47f44c2f321bd0a362212a374813ee/rio_tiler/utils.py#L615-L622
https://numpy.org/doc/stable/reference/generated/numpy.savez.html
There was a problem hiding this comment.
This limitation isn't "required"; it was just a simplifying assumption to get some sort of titiler example working.
NPZ would be fine; we'd just have to unzip the files
|
|
||
| - `GET /cog/info?url=<COG_URL>` — returns metadata including | ||
| `bounds: [west, south, east, north]` in WGS84. Used to fit the map. | ||
| - `GET /tileMatrixSets/WebMercatorQuad` — returns an OGC TMS 2.0 JSON |
There was a problem hiding this comment.
Also, we should keep in mind the difference between a:
RasterTileLayer: our concept, which supports arbitrary tile grids- upstream
TileLayer: an official deck.gl layer which only supports the Web Mercator tiling grid but is a bit more optimized.
If a user only wants to render in web mercator, they can use the upstream TileLayer directly (with our RasterLayer as the generated sub layer).
@vincentsarago are there specific times when rendering in non-web mercator would be faster? Like if we rendered from the specific UTM zone a Sentinel tile was generated, for example?
There was a problem hiding this comment.
yeah it will be faster to reduce the re-projection pipeline, so if you choose a close CRS to the dataset CRS it should be faster (in theory)
First, take this implementation with a grain of salt. We don't have to use the I'm not sure I love overloading the tilejson with that other metadata, but perhaps it's better than making two different requests |
Yeah I'm not super fan as well but at the moment there is no solution, both /tiles/{tileMatrixSetId} and /tilejson.json do not send information about the data they are giving access to. Application that does client side rendering will have to know in advance how many bands they will have in each tile and what is the datatype (at minimum). If you you the |
|
Also linking @hrodmn 's branch on https://github.com/developmentseed/titiler-cmr-browser/tree/feat/deck.gl-raster, which is titiler-based. |
Validated that this does render from titiler.xyz
Closes #461
Summary
examples/titiler-cog/that renders tiles fetched fromtitiler.xyzas.npynumpy arrays, decoded client-side and drawn viaRasterTileLayer.RasterTileLayerdirectly (not viaCOGLayer/ZarrLayer). Demonstrates the flow for any server that hands back per-tile binary arrays for an OGC tile matrix set./cog/WebMercatorQuad/tilejson.json(WGS84 bounds + min/max zoom) and/tileMatrixSets/WebMercatorQuad(tile pyramid). Builds aTileMatrixSetAdaptorwith identity EPSG:3857 projections and proj4-backed EPSG:4326 projections, injects aboundingBoxon the TMS, and fits the map to the dataset bounds.fetch .npy→ decode withnpyjs→ repack band-separate uint8(B, H, W)to interleaved RGBA → upload RGB texture + optionalr8unormmask texture → pipelineCreateTexture+MaskTexture.Design doc:
dev-docs/specs/2026-04-24-titiler-cog-example-design.md.Test plan
pnpm --filter deck.gl-titiler-cog-example devrenders the Sentinel-2 TCI RGB tiles over the dark basemap and fits the map to the scene.titiler.xyz.pnpm --filter deck.gl-titiler-cog-example typecheckpasses.pnpm --filter deck.gl-titiler-cog-example buildproduces a working production bundle.🤖 Generated with Claude Code