diff --git a/doc/errors/index.md b/doc/errors/index.md index 21a033cd23..37f59aa553 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -235,7 +235,7 @@ A `WebGLArrayBuffer` must either be of type `ELEMENT_ARRAY_BUFFER` or `ARRAY_BUF ### 63 -Support for the `OES_element_index_uint` WebGL extension is mandatory for WebGL layers. +Support for the `OES_element_index_uint`, `OES_texture_float`, and `OES_texture_float_linear` WebGL extensions are mandatory for WebGL layers. ### 64 diff --git a/examples/numpytile.css b/examples/numpytile.css new file mode 100644 index 0000000000..a1b691a4c2 --- /dev/null +++ b/examples/numpytile.css @@ -0,0 +1,3 @@ +input[type="range"] { + vertical-align: text-bottom; +} diff --git a/examples/numpytile.html b/examples/numpytile.html new file mode 100644 index 0000000000..964649c04a --- /dev/null +++ b/examples/numpytile.html @@ -0,0 +1,22 @@ +--- +layout: example.html +title: Rendering 16-bit NumpyTiles +shortdesc: Renders a multi-byte depth source image directly using WebGL. +docs: > + This example uses a ol/source/DataTile source to load multi-byte raster data in the + NumpyTile format. + The source is rendered with a ol/layer/WebGLTile layer. Adjusting the sliders above + performs a contrast stretch by adjusting the style variables set on the layer. +tags: "numpytiles, webgl" +resources: + - https://unpkg.com/@planet/ol-numpytiles@2.0.2/umd/NumpyLoader.js +--- +
+ +
+
Contrast stretch
+ +
diff --git a/examples/numpytile.js b/examples/numpytile.js new file mode 100644 index 0000000000..78df71634d --- /dev/null +++ b/examples/numpytile.js @@ -0,0 +1,106 @@ +import Map from '../src/ol/Map.js'; +import TileLayer from '../src/ol/layer/WebGLTile.js'; +import View from '../src/ol/View.js'; + +import DataTileSource from '../src/ol/source/DataTile.js'; +import {fromLonLat} from '../src/ol/proj.js'; + +// 16-bit COG +// Which will be served as NumpyTiles. +const COG = + 'https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif'; + +function numpyTileLoader(z, x, y) { + const url = `https://api.cogeo.xyz/cog/tiles/WebMercatorQuad/${z}/${x}/${y}@1x?format=npy&url=${encodeURIComponent( + COG + )}`; + + return fetch(url) + .then((r) => r.arrayBuffer()) + .then((buffer) => NumpyLoader.fromArrayBuffer(buffer)) // eslint-disable-line no-undef + .then((numpyData) => { + // flatten the numpy data + const dataTile = new Float32Array(256 * 256 * 5); + const bandSize = 256 * 256; + for (let x = 0; x < 256; x++) { + for (let y = 0; y < 256; y++) { + const px = x + y * 256; + dataTile[px * 5 + 0] = numpyData.data[y * 256 + x]; + dataTile[px * 5 + 1] = numpyData.data[bandSize + y * 256 + x]; + dataTile[px * 5 + 2] = numpyData.data[bandSize * 2 + y * 256 + x]; + dataTile[px * 5 + 3] = numpyData.data[bandSize * 3 + y * 256 + x]; + dataTile[px * 5 + 4] = + numpyData.data[bandSize * 4 + y * 256 + x] > 0 ? 1.0 : 0; + } + } + return dataTile; + }); +} + +const interpolateBand = (bandIdx) => [ + 'interpolate', + ['linear'], + ['band', bandIdx], + ['var', 'bMin'], + 0, + ['var', 'bMax'], + 1, +]; + +const initialMin = 3000; +const initialMax = 18000; + +const numpyLayer = new TileLayer({ + style: { + color: [ + 'array', + interpolateBand(3), + interpolateBand(2), + interpolateBand(1), + ['band', 5], + ], + variables: { + 'bMin': initialMin, + 'bMax': initialMax, + }, + }, + source: new DataTileSource({ + loader: numpyTileLoader, + bandCount: 5, + }), +}); + +const map = new Map({ + target: 'map', + layers: [numpyLayer], + view: new View({ + center: fromLonLat([172.933, 1.3567]), + zoom: 15, + }), +}); + +const inputMin = document.getElementById('input-min'); +const inputMax = document.getElementById('input-max'); +const outputMin = document.getElementById('output-min'); +const outputMax = document.getElementById('output-max'); + +inputMin.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(evt.target.value), + 'bMax': parseFloat(inputMax.value), + }); + outputMin.innerText = evt.target.value; +}); + +inputMax.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(inputMin.value), + 'bMax': parseFloat(evt.target.value), + }); + outputMax.innerText = evt.target.value; +}); + +inputMin.value = initialMin; +inputMax.value = initialMax; +outputMin.innerText = initialMin; +outputMax.innerText = initialMax; diff --git a/src/ol/DataTile.js b/src/ol/DataTile.js index c63eeb5b49..8b2972b173 100644 --- a/src/ol/DataTile.js +++ b/src/ol/DataTile.js @@ -6,7 +6,7 @@ import TileState from './TileState.js'; /** * Data that can be used with a DataTile. - * @typedef {Uint8Array|Uint8ClampedArray|DataView} Data + * @typedef {Uint8Array|Uint8ClampedArray|Float32Array|DataView} Data */ /** diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js index 0540d0a69f..3565df7117 100644 --- a/src/ol/renderer/webgl/TileLayer.js +++ b/src/ol/renderer/webgl/TileLayer.js @@ -426,7 +426,10 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer { const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex; gl.activeTexture(gl[textureProperty]); gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]); - gl.uniform1i(this.helper.getUniformLocation(uniformName), 0); + gl.uniform1i( + this.helper.getUniformLocation(uniformName), + textureIndex + ); } const alpha = diff --git a/src/ol/source/DataTile.js b/src/ol/source/DataTile.js index cc1acc26aa..7f8e570b3c 100644 --- a/src/ol/source/DataTile.js +++ b/src/ol/source/DataTile.js @@ -26,6 +26,7 @@ import {getUid} from '../util.js'; * @property {number} [tilePixelRatio] Tile pixel ratio. * @property {boolean} [wrapX=true] Render tiles beyond the antimeridian. * @property {number} [transition] Transition time when fading in new tiles (in miliseconds). + * @property {number} [bandCount=4] Number of bands represented in the data. */ /** @@ -81,7 +82,7 @@ class DataTileSource extends TileSource { /** * @type {number} */ - this.bandCount = 4; // assume RGBA + this.bandCount = options.bandCount === undefined ? 4 : options.bandCount; // assume RGBA if undefined } /** diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 25e21de657..9fdb5e0573 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -279,7 +279,12 @@ class WebGLHelper extends Disposable { this.currentProgram_ = null; assert(includes(getSupportedExtensions(), 'OES_element_index_uint'), 63); + assert(includes(getSupportedExtensions(), 'OES_texture_float'), 63); + assert(includes(getSupportedExtensions(), 'OES_texture_float_linear'), 63); + gl.getExtension('OES_element_index_uint'); + gl.getExtension('OES_texture_float'); + gl.getExtension('OES_texture_float_linear'); this.canvas_.addEventListener( ContextEventType.LOST, diff --git a/src/ol/webgl/TileTexture.js b/src/ol/webgl/TileTexture.js index 794c646464..27d6589cdc 100644 --- a/src/ol/webgl/TileTexture.js +++ b/src/ol/webgl/TileTexture.js @@ -15,6 +15,7 @@ function bindAndConfigure(gl, texture) { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); } /** @@ -61,6 +62,8 @@ function uploadDataTexture(gl, texture, data, size, bandCount) { } } + const texType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE; + gl.texImage2D( gl.TEXTURE_2D, 0, @@ -69,7 +72,7 @@ function uploadDataTexture(gl, texture, data, size, bandCount) { size[1], 0, format, - gl.UNSIGNED_BYTE, + texType, data ); } @@ -149,8 +152,12 @@ class TileTexture extends EventTarget { } const data = tile.getData(); + const isFloat = data instanceof Float32Array; const pixelCount = this.size[0] * this.size[1]; - this.bandCount = data.byteLength / pixelCount; + // Float arrays feature four bytes per element, + // BYTES_PER_ELEMENT throws a TypeScript exception but would handle + // this better for more varied typed arrays. + this.bandCount = data.byteLength / (isFloat ? 4 : 1) / pixelCount; const textureCount = Math.ceil(this.bandCount / 4); if (textureCount === 1) { @@ -160,6 +167,8 @@ class TileTexture extends EventTarget { return; } + const DataType = isFloat ? Float32Array : Uint8Array; + const textureDataArrays = new Array(textureCount); for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) { const texture = gl.createTexture(); @@ -167,7 +176,7 @@ class TileTexture extends EventTarget { const bandCount = textureIndex < textureCount - 1 ? 4 : this.bandCount % 4; - textureDataArrays[textureIndex] = new Uint8Array(pixelCount * bandCount); + textureDataArrays[textureIndex] = new DataType(pixelCount * bandCount); } const valueCount = pixelCount * this.bandCount;