Support expressions for band arguments
This commit is contained in:
6
examples/cog-stretch.css
Normal file
6
examples/cog-stretch.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr;
|
||||
align-items: baseline;
|
||||
gap: 0 1em;
|
||||
}
|
||||
44
examples/cog-stretch.html
Normal file
44
examples/cog-stretch.html
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Band Constrast Stretch
|
||||
shortdesc: Choosing bands and applying constrast stretch
|
||||
docs: >
|
||||
This example uses the `layer.updateStyleVariables()` method to update the rendering
|
||||
of a GeoTIFF based on user selected bands and contrast stretch parameters.
|
||||
tags: "cog, webgl, style"
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
<div class="controls">
|
||||
<label for="red">Red channel</label>
|
||||
<select id="red">
|
||||
<option value="1" selected>visible red</option>
|
||||
<option value="2">visible green</option>
|
||||
<option value="3">visible blue</option>
|
||||
<option value="4">near infrared</option>
|
||||
</select>
|
||||
<label>max
|
||||
<input type="range" id="redMax" value="3000" min="2000" max="5000">
|
||||
</label>
|
||||
|
||||
<label for="green">Green channel</label>
|
||||
<select id="green">
|
||||
<option value="1">visible red</option>
|
||||
<option value="2" selected>visible green</option>
|
||||
<option value="3">visible blue</option>
|
||||
<option value="4">near infrared</option>
|
||||
</select>
|
||||
<label>max
|
||||
<input type="range" id="greenMax" value="3000" min="2000" max="5000">
|
||||
</label>
|
||||
|
||||
<label for="blue">Blue channel</label>
|
||||
<select id="blue">
|
||||
<option value="1">visible red</option>
|
||||
<option value="2">visible green</option>
|
||||
<option value="3" selected>visible blue</option>
|
||||
<option value="4">near infrared</option>
|
||||
</select>
|
||||
<label>max
|
||||
<input type="range" id="blueMax" value="3000" min="2000" max="5000">
|
||||
</label>
|
||||
</div>
|
||||
62
examples/cog-stretch.js
Normal file
62
examples/cog-stretch.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import GeoTIFF from '../src/ol/source/GeoTIFF.js';
|
||||
import Map from '../src/ol/Map.js';
|
||||
import TileLayer from '../src/ol/layer/WebGLTile.js';
|
||||
import View from '../src/ol/View.js';
|
||||
|
||||
const channels = ['red', 'green', 'blue'];
|
||||
for (const channel of channels) {
|
||||
const selector = document.getElementById(channel);
|
||||
selector.addEventListener('change', update);
|
||||
|
||||
const input = document.getElementById(`${channel}Max`);
|
||||
input.addEventListener('input', update);
|
||||
}
|
||||
|
||||
function getVariables() {
|
||||
const variables = {};
|
||||
for (const channel of channels) {
|
||||
const selector = document.getElementById(channel);
|
||||
variables[channel] = parseInt(selector.value, 10);
|
||||
|
||||
const inputId = `${channel}Max`;
|
||||
const input = document.getElementById(inputId);
|
||||
variables[inputId] = parseInt(input.value, 10);
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
const layer = new TileLayer({
|
||||
style: {
|
||||
variables: getVariables(),
|
||||
color: [
|
||||
'array',
|
||||
['/', ['band', ['var', 'red']], ['var', 'redMax']],
|
||||
['/', ['band', ['var', 'green']], ['var', 'greenMax']],
|
||||
['/', ['band', ['var', 'blue']], ['var', 'blueMax']],
|
||||
1,
|
||||
],
|
||||
},
|
||||
source: new GeoTIFF({
|
||||
normalize: false,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://s2downloads.eox.at/demo/EOxCloudless/2020/rgbnir/s2cloudless2020-16bits_sinlge-file_z0-4.tif',
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
function update() {
|
||||
layer.updateStyleVariables(getVariables());
|
||||
}
|
||||
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [layer],
|
||||
view: new View({
|
||||
projection: 'EPSG:4326',
|
||||
center: [0, 0],
|
||||
zoom: 2,
|
||||
maxZoom: 6,
|
||||
}),
|
||||
});
|
||||
@@ -105,6 +105,7 @@ function parseStyle(style, bandCount) {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
bandCount: bandCount,
|
||||
};
|
||||
|
||||
@@ -203,14 +204,15 @@ function parseStyle(style, bandCount) {
|
||||
});
|
||||
|
||||
const textureCount = Math.ceil(bandCount / 4);
|
||||
const colorAssignments = new Array(textureCount);
|
||||
for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
|
||||
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
|
||||
uniformDeclarations.push(`uniform sampler2D ${uniformName};`);
|
||||
colorAssignments[
|
||||
textureIndex
|
||||
] = `vec4 color${textureIndex} = texture2D(${uniformName}, v_textureCoord);`;
|
||||
}
|
||||
uniformDeclarations.push(
|
||||
`uniform sampler2D ${Uniforms.TILE_TEXTURE_ARRAY}[${textureCount}];`
|
||||
);
|
||||
|
||||
const functionDefintions = Object.keys(context.functions).map(function (
|
||||
name
|
||||
) {
|
||||
return context.functions[name];
|
||||
});
|
||||
|
||||
const fragmentShader = `
|
||||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
@@ -228,10 +230,12 @@ function parseStyle(style, bandCount) {
|
||||
|
||||
${uniformDeclarations.join('\n')}
|
||||
|
||||
void main() {
|
||||
${colorAssignments.join('\n')}
|
||||
${functionDefintions.join('\n')}
|
||||
|
||||
vec4 color = color0;
|
||||
void main() {
|
||||
vec4 color = texture2D(${
|
||||
Uniforms.TILE_TEXTURE_ARRAY
|
||||
}[0], v_textureCoord);
|
||||
|
||||
${pipeline.join('\n')}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import {numberSafeCompareFunction} from '../../array.js';
|
||||
import {toSize} from '../../size.js';
|
||||
|
||||
export const Uniforms = {
|
||||
TILE_TEXTURE_PREFIX: 'u_tileTexture',
|
||||
TILE_TEXTURE_ARRAY: 'u_tileTextures',
|
||||
TILE_TRANSFORM: 'u_tileTransform',
|
||||
TRANSITION_ALPHA: 'u_transitionAlpha',
|
||||
DEPTH: 'u_depth',
|
||||
@@ -516,7 +516,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
|
||||
++textureIndex
|
||||
) {
|
||||
const textureProperty = 'TEXTURE' + textureIndex;
|
||||
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
|
||||
const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureIndex}]`;
|
||||
gl.activeTexture(gl[textureProperty]);
|
||||
gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]);
|
||||
gl.uniform1i(
|
||||
|
||||
@@ -184,6 +184,7 @@ export function isTypeUnique(valueType) {
|
||||
* @property {Array<string>} variables List of variables used in the expression; contains **unprefixed names**
|
||||
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
|
||||
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
|
||||
* @property {Object<string, string>} functions Lookup of functions used by the style.
|
||||
* @property {number} [bandCount] Number of bands per pixel.
|
||||
*/
|
||||
|
||||
@@ -417,6 +418,8 @@ Operators['var'] = {
|
||||
},
|
||||
};
|
||||
|
||||
const GET_BAND_VALUE_FUNC = 'getBandValue';
|
||||
|
||||
Operators['band'] = {
|
||||
getReturnType: function (args) {
|
||||
return ValueTypes.NUMBER;
|
||||
@@ -425,32 +428,38 @@ Operators['band'] = {
|
||||
assertArgsMinCount(args, 1);
|
||||
assertArgsMaxCount(args, 3);
|
||||
const band = args[0];
|
||||
if (typeof band !== 'number') {
|
||||
throw new Error('Band index must be a number');
|
||||
}
|
||||
const zeroBasedBand = band - 1;
|
||||
const colorIndex = Math.floor(zeroBasedBand / 4);
|
||||
let bandIndex = zeroBasedBand % 4;
|
||||
if (band === context.bandCount && bandIndex === 1) {
|
||||
// LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
|
||||
bandIndex = 3;
|
||||
}
|
||||
if (args.length === 1) {
|
||||
return `color${colorIndex}[${bandIndex}]`;
|
||||
} else {
|
||||
const xOffset = args[1];
|
||||
const yOffset = args[2] || 0;
|
||||
assertNumber(xOffset);
|
||||
assertNumber(yOffset);
|
||||
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + colorIndex;
|
||||
return `texture2D(${uniformName}, v_textureCoord + vec2(${expressionToGlsl(
|
||||
context,
|
||||
xOffset
|
||||
)} / ${Uniforms.TEXTURE_PIXEL_WIDTH}, ${expressionToGlsl(
|
||||
context,
|
||||
yOffset
|
||||
)} / ${Uniforms.TEXTURE_PIXEL_HEIGHT}))[${bandIndex}]`;
|
||||
|
||||
if (!(GET_BAND_VALUE_FUNC in context.functions)) {
|
||||
let ifBlocks = '';
|
||||
const bandCount = context.bandCount || 1;
|
||||
for (let i = 0; i < bandCount; i++) {
|
||||
const colorIndex = Math.floor(i / 4);
|
||||
let bandIndex = i % 4;
|
||||
if (bandIndex === bandCount - 1 && bandIndex === 1) {
|
||||
// LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
|
||||
bandIndex = 3;
|
||||
}
|
||||
const textureName = `${Uniforms.TILE_TEXTURE_ARRAY}[${colorIndex}]`;
|
||||
ifBlocks += `
|
||||
if (band == ${i + 1}.0) {
|
||||
return texture2D(${textureName}, v_textureCoord + vec2(dx, dy))[${bandIndex}];
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
context.functions[GET_BAND_VALUE_FUNC] = `
|
||||
float getBandValue(float band, float xOffset, float yOffset) {
|
||||
float dx = xOffset / ${Uniforms.TEXTURE_PIXEL_WIDTH};
|
||||
float dy = yOffset / ${Uniforms.TEXTURE_PIXEL_HEIGHT};
|
||||
${ifBlocks}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const bandExpression = expressionToGlsl(context, band);
|
||||
const xOffsetExpression = expressionToGlsl(context, args[1] || 0);
|
||||
const yOffsetExpression = expressionToGlsl(context, args[2] || 0);
|
||||
return `${GET_BAND_VALUE_FUNC}(${bandExpression}, ${xOffsetExpression}, ${yOffsetExpression})`;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -441,6 +441,7 @@ export function parseLiteralStyle(style) {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
const parsedSize = expressionToGlsl(
|
||||
vertContext,
|
||||
@@ -471,6 +472,7 @@ export function parseLiteralStyle(style) {
|
||||
variables: vertContext.variables,
|
||||
attributes: [],
|
||||
stringLiteralsMap: vertContext.stringLiteralsMap,
|
||||
functions: {},
|
||||
};
|
||||
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
|
||||
const parsedOpacity = expressionToGlsl(
|
||||
|
||||
@@ -96,10 +96,9 @@ describe('ol/layer/WebGLTile', function () {
|
||||
uniform float u_var_r;
|
||||
uniform float u_var_g;
|
||||
uniform float u_var_b;
|
||||
uniform sampler2D u_tileTexture0;
|
||||
uniform sampler2D u_tileTextures[1];
|
||||
void main() {
|
||||
vec4 color0 = texture2D(u_tileTexture0, v_textureCoord);
|
||||
vec4 color = color0;
|
||||
vec4 color = texture2D(u_tileTextures[0], v_textureCoord);
|
||||
color = vec4(u_var_r / 255.0, u_var_g / 255.0, u_var_b / 255.0, 1.0);
|
||||
if (color.a == 0.0) {
|
||||
discard;
|
||||
@@ -125,6 +124,82 @@ describe('ol/layer/WebGLTile', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a getBandValue function to the fragment shaders', function () {
|
||||
const max = 3000;
|
||||
function normalize(value) {
|
||||
return ['/', value, max];
|
||||
}
|
||||
|
||||
const red = normalize(['band', 1]);
|
||||
const green = normalize(['band', 2]);
|
||||
const nir = normalize(['band', 4]);
|
||||
|
||||
layer.setStyle({
|
||||
color: ['array', nir, red, green, 1],
|
||||
});
|
||||
|
||||
const compileShaderSpy = sinon.spy(WebGLHelper.prototype, 'compileShader');
|
||||
const renderer = layer.createRenderer();
|
||||
const viewState = map.getView().getState();
|
||||
const size = map.getSize();
|
||||
const frameState = {
|
||||
viewState: viewState,
|
||||
extent: getForViewAndSize(
|
||||
viewState.center,
|
||||
viewState.resolution,
|
||||
viewState.rotation,
|
||||
size
|
||||
),
|
||||
layerStatesArray: map.getLayerGroup().getLayerStatesArray(),
|
||||
layerIndex: 0,
|
||||
};
|
||||
renderer.prepareFrame(frameState);
|
||||
compileShaderSpy.restore();
|
||||
expect(compileShaderSpy.callCount).to.be(2);
|
||||
expect(compileShaderSpy.getCall(0).args[0].replace(/[ \n]+/g, ' ')).to.be(
|
||||
`
|
||||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
precision highp float;
|
||||
#else
|
||||
precision mediump float;
|
||||
#endif varying vec2 v_textureCoord;
|
||||
uniform float u_transitionAlpha;
|
||||
uniform float u_texturePixelWidth;
|
||||
uniform float u_texturePixelHeight;
|
||||
uniform float u_resolution;
|
||||
uniform float u_zoom;
|
||||
uniform sampler2D u_tileTextures[1];
|
||||
|
||||
float getBandValue(float band, float xOffset, float yOffset) {
|
||||
float dx = xOffset / u_texturePixelWidth;
|
||||
float dy = yOffset / u_texturePixelHeight;
|
||||
if (band == 1.0) {
|
||||
return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[0];
|
||||
}
|
||||
if (band == 2.0) {
|
||||
return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[1];
|
||||
}
|
||||
if (band == 3.0) {
|
||||
return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[2];
|
||||
}
|
||||
if (band == 4.0) {
|
||||
return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[3];
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = texture2D(u_tileTextures[0], v_textureCoord);
|
||||
color = vec4((getBandValue(4.0, 0.0, 0.0) / 3000.0), (getBandValue(1.0, 0.0, 0.0) / 3000.0), (getBandValue(2.0, 0.0, 0.0) / 3000.0), 1.0);
|
||||
if (color.a == 0.0) {
|
||||
discard;
|
||||
}
|
||||
gl_FragColor = color;
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
gl_FragColor *= u_transitionAlpha;
|
||||
}`.replace(/[ \n]+/g, ' ')
|
||||
);
|
||||
});
|
||||
|
||||
describe('updateStyleVariables()', function () {
|
||||
it('updates style variables', function (done) {
|
||||
layer.updateStyleVariables({
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
uniformNameForVariable,
|
||||
} from '../../../../../src/ol/style/expressions.js';
|
||||
|
||||
describe('ol.style.expressions', function () {
|
||||
describe('ol/style/expressions', function () {
|
||||
describe('numberToGlsl', function () {
|
||||
it('does a simple transform when a fraction is present', function () {
|
||||
expect(numberToGlsl(1.3456)).to.eql('1.3456');
|
||||
@@ -70,6 +70,7 @@ describe('ol.style.expressions', function () {
|
||||
beforeEach(function () {
|
||||
context = {
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -207,6 +208,7 @@ describe('ol.style.expressions', function () {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -289,9 +291,11 @@ describe('ol.style.expressions', function () {
|
||||
expect(
|
||||
expressionToGlsl(context, ['color', ['get', 'attr4'], 1, 2, 0.5])
|
||||
).to.eql('vec4(a_attr4 / 255.0, 1.0 / 255.0, 2.0 / 255.0, 0.5)');
|
||||
expect(expressionToGlsl(context, ['band', 1])).to.eql('color0[0]');
|
||||
expect(expressionToGlsl(context, ['band', 1])).to.eql(
|
||||
'getBandValue(1.0, 0.0, 0.0)'
|
||||
);
|
||||
expect(expressionToGlsl(context, ['band', 1, -1, 2])).to.eql(
|
||||
'texture2D(u_tileTexture0, v_textureCoord + vec2(-1.0 / u_texturePixelWidth, 2.0 / u_texturePixelHeight))[0]'
|
||||
'getBandValue(1.0, -1.0, 2.0)'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -464,6 +468,7 @@ describe('ol.style.expressions', function () {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -608,6 +613,7 @@ describe('ol.style.expressions', function () {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -821,6 +827,7 @@ describe('ol.style.expressions', function () {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1067,6 +1074,7 @@ describe('ol.style.expressions', function () {
|
||||
variables: [],
|
||||
attributes: [],
|
||||
stringLiteralsMap: {},
|
||||
functions: {},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user