Example that demonstrates a color expression using variables

This commit is contained in:
Tim Schaub
2021-10-29 11:36:50 -06:00
parent fdba3ecf0e
commit 0e19c9aa2b
6 changed files with 208 additions and 18 deletions

View File

@@ -3,6 +3,7 @@
"$": false,
"arc": false,
"common": false,
"chroma": false,
"createMapboxStreetsV6Style": false,
"d3": false,
"html2canvas": false,

10
examples/cog-colors.css Normal file
View File

@@ -0,0 +1,10 @@
.data {
text-align: right;
font-family: monospace;
}
td {
padding-right: 10px;
}
input[type="range"] {
vertical-align: text-bottom;
}

32
examples/cog-colors.html Normal file
View File

@@ -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
---
<div id="map" class="map"></div>
<table>
<tbody>
<tr>
<td>Min NDVI</td>
<td><input type="range" id="min-value-input" min="-1.0" max="-0.1" step="0.01"></td>
<td class="data" id="min-value-output"></td>
<td><input type="color" id="min-color"></td>
</tr>
<tr>
<td>Max NDVI</td>
<td><input type="range" id="max-value-input" min="0.1" max="1.0" step="0.01"></td>
<td class="data" id="max-value-output"></td>
<td><input type="color" id="max-color"></td>
</tr>
</tbody>
</table>

106
examples/cog-colors.js Normal file
View File

@@ -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(),
});

View File

@@ -18,7 +18,7 @@ import {assign} from '../obj.js';
* @typedef {Object} Style
* Translates tile data to rendered pixels.
*
* @property {Object<string, number>} [variables] Style variables. Each variable must hold a number. These
* @property {Object<string, (string|number)>} [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<string, (string|number)>}
* @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,

View File

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