Skip to content

feat: Titiler .npy tile example with RasterTileLayer#469

Open
kylebarron wants to merge 10 commits into
mainfrom
titiler-example
Open

feat: Titiler .npy tile example with RasterTileLayer#469
kylebarron wants to merge 10 commits into
mainfrom
titiler-example

Conversation

@kylebarron
Copy link
Copy Markdown
Member

@kylebarron kylebarron commented Apr 24, 2026

image

Validated that this does render from titiler.xyz

Closes #461

Summary

  • New example at examples/titiler-cog/ that renders tiles fetched from titiler.xyz as .npy numpy arrays, decoded client-side and drawn via RasterTileLayer.
  • First example in the repo that uses RasterTileLayer directly (not via COGLayer / ZarrLayer). Demonstrates the flow for any server that hands back per-tile binary arrays for an OGC tile matrix set.
  • Startup fetches /cog/WebMercatorQuad/tilejson.json (WGS84 bounds + min/max zoom) and /tileMatrixSets/WebMercatorQuad (tile pyramid). Builds a TileMatrixSetAdaptor with identity EPSG:3857 projections and proj4-backed EPSG:4326 projections, injects a boundingBox on the TMS, and fits the map to the dataset bounds.
  • Per tile: fetch .npy → decode with npyjs → repack band-separate uint8 (B, H, W) to interleaved RGBA → upload RGB texture + optional r8unorm mask texture → pipeline CreateTexture + MaskTexture.

Design doc: dev-docs/specs/2026-04-24-titiler-cog-example-design.md.

Test plan

  • pnpm --filter deck.gl-titiler-cog-example dev renders the Sentinel-2 TCI RGB tiles over the dark basemap and fits the map to the scene.
  • Zooming and panning loads new tiles from titiler.xyz.
  • "Show Debug Mesh" toggle + "Debug Opacity" slider behave the same as in other examples.
  • pnpm --filter deck.gl-titiler-cog-example typecheck passes.
  • pnpm --filter deck.gl-titiler-cog-example build produces a working production bundle.

🤖 Generated with Claude Code

kylebarron and others added 9 commits April 24, 2026 14:01
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 kylebarron changed the title Titiler .npy tile example with RasterTileLayer feat: Titiler .npy tile example with RasterTileLayer Apr 24, 2026
@github-actions github-actions Bot added the feat label Apr 24, 2026
@vincentsarago
Copy link
Copy Markdown
Member

@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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does this limitation comes from? Titiler could server tiles with nBands+masks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@kylebarron
Copy link
Copy Markdown
Member Author

@kylebarron do you think developmentseed/titiler#1242 could be of any help, like having the number of bands or the datatype ...

First, take this implementation with a grain of salt. We don't have to use the tilejson endpoint in this app if there's a different endpoint that makes more sense.

I'm not sure I love overloading the tilejson with that other metadata, but perhaps it's better than making two different requests

@vincentsarago
Copy link
Copy Markdown
Member

vincentsarago commented Apr 27, 2026

First, take this implementation with a grain of salt. We don't have to use the tilejson endpoint in this app if there's a different endpoint that makes more sense.

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 /info endpoint you get this information but they you also need to call the one of the previous endpoint to get the TMS coverage (bounds, min/max zoom)

@kylebarron
Copy link
Copy Markdown
Member Author

Also linking @hrodmn 's branch on https://github.com/developmentseed/titiler-cmr-browser/tree/feat/deck.gl-raster, which is titiler-based.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create example reading from titiler backend

2 participants