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
+---
+
+
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');
});
});