Example that demonstrates a color expression using variables
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"$": false,
|
"$": false,
|
||||||
"arc": false,
|
"arc": false,
|
||||||
"common": false,
|
"common": false,
|
||||||
|
"chroma": false,
|
||||||
"createMapboxStreetsV6Style": false,
|
"createMapboxStreetsV6Style": false,
|
||||||
"d3": false,
|
"d3": false,
|
||||||
"html2canvas": false,
|
"html2canvas": false,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.data {
|
||||||
|
text-align: right;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
input[type="range"] {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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(),
|
||||||
|
});
|
||||||
@@ -18,7 +18,7 @@ import {assign} from '../obj.js';
|
|||||||
* @typedef {Object} Style
|
* @typedef {Object} Style
|
||||||
* Translates tile data to rendered pixels.
|
* 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`
|
* 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.
|
* {@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.
|
* To update style variables, use the {@link import("./WebGLTile.js").default#updateStyleVariables} method.
|
||||||
@@ -287,6 +287,12 @@ class WebGLTileLayer extends BaseTileLayer {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this.cacheSize_ = cacheSize;
|
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
|
'bandCount' in source ? source.bandCount : 4
|
||||||
);
|
);
|
||||||
|
|
||||||
this.styleVariables_ = this.style_.variables || {};
|
|
||||||
|
|
||||||
return new WebGLTileLayerRenderer(this, {
|
return new WebGLTileLayerRenderer(this, {
|
||||||
vertexShader: parsedStyle.vertexShader,
|
vertexShader: parsedStyle.vertexShader,
|
||||||
fragmentShader: parsedStyle.fragmentShader,
|
fragmentShader: parsedStyle.fragmentShader,
|
||||||
|
|||||||
@@ -110,23 +110,60 @@ describe('ol/layer/WebGLTile', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates style variables', function (done) {
|
describe('updateStyleVariables()', function () {
|
||||||
layer.updateStyleVariables({
|
it('updates style variables', function (done) {
|
||||||
r: 255,
|
layer.updateStyleVariables({
|
||||||
g: 0,
|
r: 255,
|
||||||
b: 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);
|
it('can be called before the layer is rendered', function () {
|
||||||
layer.on('postrender', () => {
|
const layer = new WebGLTileLayer({
|
||||||
targetContext.clearRect(0, 0, 100, 100);
|
style: {
|
||||||
targetContext.drawImage(target.querySelector('.testlayer'), 0, 0);
|
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([
|
it('can be called even if no initial variables are provided', function () {
|
||||||
255, 0, 255, 255,
|
const layer = new WebGLTileLayer({
|
||||||
]);
|
source: new DataTileSource({
|
||||||
done();
|
loader(z, x, y) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(new ImageData(256, 256));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.updateStyleVariables({foo: 'bam'});
|
||||||
|
expect(layer.styleVariables_.foo).to.be('bam');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user