From 0e19c9aa2b05eb9b28b502aa8d3cd590dabacafa Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 29 Oct 2021 11:36:50 -0600 Subject: [PATCH] Example that demonstrates a color expression using variables --- examples/.eslintrc | 1 + examples/cog-colors.css | 10 ++ examples/cog-colors.html | 32 ++++++ examples/cog-colors.js | 106 +++++++++++++++++++ src/ol/layer/WebGLTile.js | 10 +- test/browser/spec/ol/layer/WebGLTile.test.js | 67 +++++++++--- 6 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 examples/cog-colors.css create mode 100644 examples/cog-colors.html create mode 100644 examples/cog-colors.js diff --git a/examples/.eslintrc b/examples/.eslintrc index 96581d11d6..c809855c70 100644 --- a/examples/.eslintrc +++ b/examples/.eslintrc @@ -3,6 +3,7 @@ "$": false, "arc": false, "common": false, + "chroma": false, "createMapboxStreetsV6Style": false, "d3": false, "html2canvas": false, diff --git a/examples/cog-colors.css b/examples/cog-colors.css new file mode 100644 index 0000000000..f322f37d75 --- /dev/null +++ b/examples/cog-colors.css @@ -0,0 +1,10 @@ +.data { + text-align: right; + font-family: monospace; +} +td { + padding-right: 10px; +} +input[type="range"] { + vertical-align: text-bottom; +} \ No newline at end of file diff --git a/examples/cog-colors.html b/examples/cog-colors.html new file mode 100644 index 0000000000..4120cccade --- /dev/null +++ b/examples/cog-colors.html @@ -0,0 +1,32 @@ +--- +layout: example.html +title: NDVI with a Dynamic Color Ramp +shortdesc: NDVI from a COG with a dynamic color ramp +docs: > + The GeoTIFF layer in this example draws from two Sentinel 2 sources: a red band and a near infrared band. + The layer style includes a `color` expression that calculates the Normalized Difference Vegetation Index (NDVI) + from values in the two bands. The `interpolate` expression is used to map NDVI values to colors. The "stop" values + for the color ramp are derived from application provided style variables. Using the inputs above, the min and max + colors and values can be adjusted. The `layer.updateStyleVariables()` method is called to update the + variables used in the interpolated color expression. +tags: "cog, ndvi" +resources: + - https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.2/chroma.min.js +--- +
+ + + + + + + + + + + + + + + +
Min NDVI
Max NDVI
diff --git a/examples/cog-colors.js b/examples/cog-colors.js new file mode 100644 index 0000000000..fc71bd2c3d --- /dev/null +++ b/examples/cog-colors.js @@ -0,0 +1,106 @@ +import GeoTIFF from '../src/ol/source/GeoTIFF.js'; +import Map from '../src/ol/Map.js'; +import TileLayer from '../src/ol/layer/WebGLTile.js'; + +const segments = 10; + +const defaultMinColor = '#0300AD'; +const defaultMaxColor = '#00ff00'; + +const defaultMinValue = -0.5; +const defaultMaxValue = 0.7; + +const minColorInput = document.getElementById('min-color'); +minColorInput.value = defaultMinColor; + +const maxColorInput = document.getElementById('max-color'); +maxColorInput.value = defaultMaxColor; + +const minValueOutput = document.getElementById('min-value-output'); +const minValueInput = document.getElementById('min-value-input'); +minValueInput.value = defaultMinValue.toString(); + +const maxValueOutput = document.getElementById('max-value-output'); +const maxValueInput = document.getElementById('max-value-input'); +maxValueInput.value = defaultMaxValue.toString(); + +function getVariables() { + const variables = {}; + + const minColor = minColorInput.value; + const maxColor = maxColorInput.value; + const scale = chroma.scale([minColor, maxColor]).mode('lab'); + + const minValue = parseFloat(minValueInput.value); + const maxValue = parseFloat(maxValueInput.value); + const delta = (maxValue - minValue) / segments; + + for (let i = 0; i <= segments; ++i) { + const color = scale(i / segments).rgb(); + const value = minValue + i * delta; + variables[`value${i}`] = value; + variables[`red${i}`] = color[0]; + variables[`green${i}`] = color[1]; + variables[`blue${i}`] = color[2]; + } + return variables; +} + +function colors() { + const stops = []; + for (let i = 0; i <= segments; ++i) { + stops[i * 2] = ['var', `value${i}`]; + const red = ['var', `red${i}`]; + const green = ['var', `green${i}`]; + const blue = ['var', `blue${i}`]; + stops[i * 2 + 1] = ['color', red, green, blue]; + } + return stops; +} + +const ndvi = [ + '/', + ['-', ['band', 2], ['band', 1]], + ['+', ['band', 2], ['band', 1]], +]; + +const source = new GeoTIFF({ + sources: [ + { + // visible red, band 1 in the style expression above + url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B04.tif', + max: 10000, + }, + { + // near infrared, band 2 in the style expression above + url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B08.tif', + max: 10000, + }, + ], +}); + +const layer = new TileLayer({ + style: { + variables: getVariables(), + color: ['interpolate', ['linear'], ndvi, ...colors()], + }, + source: source, +}); + +function update() { + layer.updateStyleVariables(getVariables()); + minValueOutput.innerText = parseFloat(minValueInput.value).toFixed(1); + maxValueOutput.innerText = parseFloat(maxValueInput.value).toFixed(1); +} + +minColorInput.addEventListener('input', update); +maxColorInput.addEventListener('input', update); +minValueInput.addEventListener('input', update); +maxValueInput.addEventListener('input', update); +update(); + +const map = new Map({ + target: 'map', + layers: [layer], + view: source.getView(), +}); diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js index 51d15fe80d..f92bcfd2a8 100644 --- a/src/ol/layer/WebGLTile.js +++ b/src/ol/layer/WebGLTile.js @@ -18,7 +18,7 @@ import {assign} from '../obj.js'; * @typedef {Object} Style * Translates tile data to rendered pixels. * - * @property {Object} [variables] Style variables. Each variable must hold a number. These + * @property {Object} [variables] Style variables. Each variable must hold a number or string. These * variables can be used in the `color`, `brightness`, `contrast`, `exposure`, `saturation` and `gamma` * {@link import("../style/expressions.js").ExpressionValue expressions}, using the `['var', 'varName']` operator. * To update style variables, use the {@link import("./WebGLTile.js").default#updateStyleVariables} method. @@ -287,6 +287,12 @@ class WebGLTileLayer extends BaseTileLayer { * @private */ this.cacheSize_ = cacheSize; + + /** + * @type {Object} + * @private + */ + this.styleVariables_ = this.style_.variables || {}; } /** @@ -301,8 +307,6 @@ class WebGLTileLayer extends BaseTileLayer { 'bandCount' in source ? source.bandCount : 4 ); - this.styleVariables_ = this.style_.variables || {}; - return new WebGLTileLayerRenderer(this, { vertexShader: parsedStyle.vertexShader, fragmentShader: parsedStyle.fragmentShader, diff --git a/test/browser/spec/ol/layer/WebGLTile.test.js b/test/browser/spec/ol/layer/WebGLTile.test.js index 56f394885b..0675807bb8 100644 --- a/test/browser/spec/ol/layer/WebGLTile.test.js +++ b/test/browser/spec/ol/layer/WebGLTile.test.js @@ -110,23 +110,60 @@ describe('ol/layer/WebGLTile', function () { ); }); - it('updates style variables', function (done) { - layer.updateStyleVariables({ - r: 255, - g: 0, - b: 255, + describe('updateStyleVariables()', function () { + it('updates style variables', function (done) { + layer.updateStyleVariables({ + r: 255, + g: 0, + b: 255, + }); + expect(layer.styleVariables_['r']).to.be(255); + const targetContext = createCanvasContext2D(100, 100); + layer.on('postrender', () => { + targetContext.clearRect(0, 0, 100, 100); + targetContext.drawImage(target.querySelector('.testlayer'), 0, 0); + }); + map.once('rendercomplete', () => { + expect(Array.from(targetContext.getImageData(0, 0, 1, 1).data)).to.eql([ + 255, 0, 255, 255, + ]); + done(); + }); }); - expect(layer.styleVariables_['r']).to.be(255); - const targetContext = createCanvasContext2D(100, 100); - layer.on('postrender', () => { - targetContext.clearRect(0, 0, 100, 100); - targetContext.drawImage(target.querySelector('.testlayer'), 0, 0); + + it('can be called before the layer is rendered', function () { + const layer = new WebGLTileLayer({ + style: { + variables: { + foo: 'bar', + }, + }, + source: new DataTileSource({ + loader(z, x, y) { + return new Promise((resolve) => { + resolve(new ImageData(256, 256)); + }); + }, + }), + }); + + layer.updateStyleVariables({foo: 'bam'}); + expect(layer.styleVariables_.foo).to.be('bam'); }); - map.once('rendercomplete', () => { - expect(Array.from(targetContext.getImageData(0, 0, 1, 1).data)).to.eql([ - 255, 0, 255, 255, - ]); - done(); + + it('can be called even if no initial variables are provided', function () { + const layer = new WebGLTileLayer({ + source: new DataTileSource({ + loader(z, x, y) { + return new Promise((resolve) => { + resolve(new ImageData(256, 256)); + }); + }, + }), + }); + + layer.updateStyleVariables({foo: 'bam'}); + expect(layer.styleVariables_.foo).to.be('bam'); }); });