Skip to content

Commit 28d71cb

Browse files
committed
Render single-band float COGs: custom r32float getTileData + CreateTexture pipeline
COGLayer's default inferRenderPipeline only supports unsigned-integer COGs (throws 'non-unsigned integers not yet supported' for SampleFormat 3/float). Supply our own getTileData (uploads the band as r32float) and renderTile ([CreateTexture, FilterNoDataVal?, LinearRescale, Colormap]) so _parseGeoTIFF skips that inference. Verified live: CMIP6 tasmax (float32) renders with gist_heat_r + rescale 225-315, no tile-server render requests.
1 parent 0c24dd6 commit 28d71cb

1 file changed

Lines changed: 74 additions & 8 deletions

File tree

src/essence/Basics/MapEngines/Adapters/DeckCOGLayer.ts

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { COGLayer } from '@developmentseed/deck.gl-geotiff'
2626
// the exports map). See deviation note in the file header.
2727
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2828
// @ts-ignore
29-
import { Colormap, LinearRescale, FilterNoDataVal, createColormapTexture } from '@developmentseed/deck.gl-raster/gpu-modules'
29+
import { Colormap, LinearRescale, FilterNoDataVal, CreateTexture, createColormapTexture } from '@developmentseed/deck.gl-raster/gpu-modules'
3030
import type { Texture } from '@luma.gl/core'
3131
import { buildColormapLUT } from './colormapLUT'
3232

@@ -107,6 +107,64 @@ function lutToImageData(lut: Uint8ClampedArray): ImageData {
107107
return new ImageData(lut, 256, 1)
108108
}
109109

110+
/**
111+
* Custom tile loader for single-band COGs. Uploads the band as a single-channel
112+
* `r32float` GPU texture so the RAW pixel value lands in `color.r` for the
113+
* downstream LinearRescale + Colormap modules.
114+
*
115+
* This deliberately replaces `@developmentseed/deck.gl-geotiff`'s default
116+
* `inferRenderPipeline`, whose auto-inference currently only supports unsigned
117+
* integer COGs (it throws `non-unsigned integers not yet supported` for float,
118+
* SampleFormat 3 — the common case for scientific single-band rasters). By
119+
* supplying our own `getTileData` + `renderTile`, `COGLayer._parseGeoTIFF`
120+
* skips that inference entirely.
121+
*
122+
* Values of any numeric type are converted to Float32 and uploaded as
123+
* `r32float`, so a single rescale (in raw data units) is correct for float and
124+
* integer COGs alike. Sampled with nearest filtering (float-linear filtering is
125+
* not guaranteed in WebGL2).
126+
*
127+
* @param image A `GeoTIFF` (full-res) or `Overview`, selected per-zoom by
128+
* COGLayer's `_getTileDataCallback` wrapper.
129+
* @param options `{ device, x, y, signal, pool }` supplied by the wrapper.
130+
*/
131+
async function cogGetTileData(
132+
image: any,
133+
options: { device: any; x: number; y: number; signal?: AbortSignal; pool?: any }
134+
): Promise<{ texture: any; width: number; height: number; byteLength: number } | null> {
135+
const { device, x, y, signal, pool } = options
136+
const tile = await image.fetchTile(x, y, { boundless: false, pool, signal })
137+
const array = tile?.array
138+
if (!array) return null
139+
const width = array.width
140+
const height = array.height
141+
// Single-band: prefer flat `data`; fall back to first band for band-separate.
142+
let src = array.data
143+
if (array.layout === 'band-separate' && array.bands && array.bands.length) {
144+
src = array.bands[0]
145+
}
146+
const f32 = src instanceof Float32Array ? src : Float32Array.from(src)
147+
const texture = device.createTexture({
148+
data: f32,
149+
format: 'r32float',
150+
width,
151+
height,
152+
mipmaps: false,
153+
sampler: {
154+
minFilter: 'nearest',
155+
magFilter: 'nearest',
156+
addressModeU: 'clamp-to-edge',
157+
addressModeV: 'clamp-to-edge',
158+
},
159+
})
160+
return { texture, width, height, byteLength: f32.byteLength }
161+
}
162+
163+
/** Truthy placeholder so COGLayer._parseGeoTIFF skips the (float-unsupported)
164+
* default `inferRenderPipeline`. The real render pipeline is produced by the
165+
* overridden `_renderTileCallback()` below, so this is never invoked. */
166+
const RENDER_TILE_PLACEHOLDER = (): null => null
167+
110168
// ---------------------------------------------------------------------------
111169
// ColormappedCOGLayer
112170
// ---------------------------------------------------------------------------
@@ -170,12 +228,15 @@ export class ColormappedCOGLayer extends COGLayer<any> {
170228
// Return type is `any` to avoid TypeScript's return-type assignability
171229
// check against the parent signature (which uses the full RenderTileResult
172230
// union from @developmentseed/deck.gl-raster). The actual runtime type
173-
// satisfies the union contract; this is validated by Task 8's render test.
231+
// satisfies the union contract.
232+
//
233+
// The pipeline is built from scratch — CreateTexture samples our r32float
234+
// tile texture into `color` (raw value in color.r), then composeColormapPipeline
235+
// appends [FilterNoDataVal?, LinearRescale, Colormap]. We do NOT call
236+
// super._renderTileCallback() because COGLayer's default renderTile pairs
237+
// with its default getTileData (inferRenderPipeline), which throws for float.
174238
// eslint-disable-next-line @typescript-eslint/no-explicit-any
175239
protected override _renderTileCallback(): any {
176-
const base = super._renderTileCallback()
177-
if (!base) return undefined
178-
179240
const p = this.props as any
180241
const { rescaleMin, rescaleMax, nodata } = p
181242
// Capture colormapTexture at callback-construction time; the updateTriggers
@@ -185,10 +246,9 @@ export class ColormappedCOGLayer extends COGLayer<any> {
185246
const colormapTexture = (this.state as any).colormapTexture as Texture | null
186247

187248
return (data: any) => {
188-
const result = base(data)
189-
if (!result) return null
249+
if (!data || !data.texture) return null
190250
return composeColormapPipeline(
191-
result as { image?: unknown; renderPipeline?: PipelineEntry[] },
251+
{ renderPipeline: [{ module: CreateTexture, props: { textureName: data.texture } }] },
192252
{ rescaleMin, rescaleMax, colormapTexture, nodata }
193253
)
194254
}
@@ -229,6 +289,12 @@ export function buildDeckCOGLayer(
229289
opacity: options.opacity ?? 1,
230290
minZoom: options.minZoom,
231291
maxZoom: options.maxZoom,
292+
// Supplying getTileData + renderTile makes COGLayer._parseGeoTIFF skip
293+
// its default inferRenderPipeline (which throws for float COGs).
294+
// getTileData uploads the band as r32float; renderTile is overridden by
295+
// the subclass (this placeholder only satisfies the parse-time guard).
296+
getTileData: cogGetTileData,
297+
renderTile: RENDER_TILE_PLACEHOLDER,
232298
// Custom props consumed in updateState and _renderTileCallback:
233299
colormapName,
234300
rescaleMin,

0 commit comments

Comments
 (0)