Merge pull request #10168 from jahow/webgl-style-add-filter

Webgl / Add support for filtering and color interpolation in style expressions
This commit is contained in:
Olivier Guyot
2019-10-23 09:45:16 +02:00
committed by GitHub
12 changed files with 1211 additions and 466 deletions
+4 -4
View File
@@ -3,14 +3,14 @@ layout: example.html
title: Filtering features with WebGL title: Filtering features with WebGL
shortdesc: Using WebGL to filter large quantities of features shortdesc: Using WebGL to filter large quantities of features
docs: > docs: >
This example shows how to use `ol/renderer/webgl/PointsLayer` to dynamically filter a large amount This example shows how to use `ol/layer/WebGLPoints` with a literal style to dynamically filter a large amount
of point geometries. The above map is based on a dataset from the NASA containing 45k recorded meteorite of point geometries. The above map is based on a dataset from the NASA containing 45k recorded meteorite
landing sites. Each meteorite is marked by a circle on the map (the bigger the circle, the heavier landing sites. Each meteorite is marked by a circle on the map (the bigger the circle, the heavier
the object). A pulse effect has been added, which is slightly offset by the year of the impact. the object). A pulse effect has been added, which is slightly offset by the year of the impact.
Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done using Adjusting the sliders causes the objects outside of the date range to be filtered out of the map. This is done
a custom fragment shader on the layer renderer, and by using the `v_opacity` attribute of the rendered objects by mutating the variables in the `style` object provided to the WebGL layer. Also note that the last snippet
to store the year of impact. of code is necessary to make sure the map refreshes itself every frame.
tags: "webgl, icon, sprite, filter, feature" tags: "webgl, icon, sprite, filter, feature"
experimental: true experimental: true
+77 -132
View File
@@ -3,162 +3,106 @@ import View from '../src/ol/View.js';
import TileLayer from '../src/ol/layer/Tile.js'; import TileLayer from '../src/ol/layer/Tile.js';
import Feature from '../src/ol/Feature.js'; import Feature from '../src/ol/Feature.js';
import Point from '../src/ol/geom/Point.js'; import Point from '../src/ol/geom/Point.js';
import VectorLayer from '../src/ol/layer/Vector.js';
import {Vector} from '../src/ol/source.js'; import {Vector} from '../src/ol/source.js';
import {fromLonLat} from '../src/ol/proj.js'; import {fromLonLat} from '../src/ol/proj.js';
import WebGLPointsLayerRenderer from '../src/ol/renderer/webgl/PointsLayer.js';
import {clamp} from '../src/ol/math.js';
import Stamen from '../src/ol/source/Stamen.js'; import Stamen from '../src/ol/source/Stamen.js';
import {formatColor} from '../src/ol/webgl/ShaderBuilder.js'; import WebGLPointsLayer from '../src/ol/layer/WebGLPoints.js';
const vectorSource = new Vector({ const vectorSource = new Vector({
attributions: 'NASA' attributions: 'NASA'
}); });
const oldColor = [180, 140, 140]; const oldColor = 'rgba(242,56,22,0.61)';
const newColor = [255, 80, 80]; const newColor = '#ffe52c';
const period = 12; // animation period in seconds
const animRatio =
['pow',
['/',
['mod',
['+',
['time'],
['stretch', ['get', 'year'], 1850, 2020, 0, period]
],
period
],
period
],
0.5
];
const startTime = Date.now() * 0.001; const style = {
variables: {
minYear: 1850,
maxYear: 2015
},
filter: ['between', ['get', 'year'], ['var', 'minYear'], ['var', 'maxYear']],
symbol: {
symbolType: 'circle',
size: ['*',
['stretch', ['get', 'mass'], 0, 200000, 8, 26],
['-', 1.5, ['*', animRatio, 0.5]]
],
color: ['interpolate',
animRatio,
newColor, oldColor],
opacity: ['-', 1.0, ['*', animRatio, 0.75]]
}
};
// hanle input values & events // handle input values & events
const minYearInput = document.getElementById('min-year'); const minYearInput = document.getElementById('min-year');
const maxYearInput = document.getElementById('max-year'); const maxYearInput = document.getElementById('max-year');
function updateMinYear() {
style.variables.minYear = parseInt(minYearInput.value);
updateStatusText();
}
function updateMaxYear() {
style.variables.maxYear = parseInt(maxYearInput.value);
updateStatusText();
}
function updateStatusText() { function updateStatusText() {
const div = document.getElementById('status'); const div = document.getElementById('status');
div.querySelector('span.min-year').textContent = minYearInput.value; div.querySelector('span.min-year').textContent = minYearInput.value;
div.querySelector('span.max-year').textContent = maxYearInput.value; div.querySelector('span.max-year').textContent = maxYearInput.value;
} }
minYearInput.addEventListener('input', updateStatusText);
minYearInput.addEventListener('change', updateStatusText); minYearInput.addEventListener('input', updateMinYear);
maxYearInput.addEventListener('input', updateStatusText); minYearInput.addEventListener('change', updateMinYear);
maxYearInput.addEventListener('change', updateStatusText); maxYearInput.addEventListener('input', updateMaxYear);
maxYearInput.addEventListener('change', updateMaxYear);
updateStatusText(); updateStatusText();
class WebglPointsLayer extends VectorLayer { // load data
createRenderer() { const client = new XMLHttpRequest();
return new WebGLPointsLayerRenderer(this, { client.open('GET', 'data/csv/meteorite_landings.csv');
attributes: [ client.onload = function() {
{ const csv = client.responseText;
name: 'size', const features = [];
callback: function(feature) {
return 18 * clamp(feature.get('mass') / 200000, 0, 1) + 8;
}
},
{
name: 'year',
callback: function(feature) {
return feature.get('year');
}
}
],
vertexShader: [
'precision mediump float;',
'uniform mat4 u_projectionMatrix;', let prevIndex = csv.indexOf('\n') + 1; // scan past the header line
'uniform mat4 u_offsetScaleMatrix;',
'uniform mat4 u_offsetRotateMatrix;',
'attribute vec2 a_position;',
'attribute float a_index;',
'attribute float a_size;',
'attribute float a_year;',
'varying vec2 v_texCoord;',
'varying float v_year;',
'void main(void) {', let curIndex;
' mat4 offsetMatrix = u_offsetScaleMatrix;', while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) {
' float offsetX = a_index == 0.0 || a_index == 3.0 ? -a_size / 2.0 : a_size / 2.0;', const line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
' float offsetY = a_index == 0.0 || a_index == 1.0 ? -a_size / 2.0 : a_size / 2.0;', prevIndex = curIndex + 1;
' vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);',
' gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;',
' float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;',
' float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0;',
' v_texCoord = vec2(u, v);',
' v_year = a_year;',
'}'
].join(' '),
fragmentShader: [
'precision mediump float;',
'uniform float u_time;', const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]);
'uniform float u_minYear;', if (isNaN(coords[0]) || isNaN(coords[1])) {
'uniform float u_maxYear;', // guard against bad data
'varying vec2 v_texCoord;', continue;
'varying float v_year;',
'void main(void) {',
// filter out pixels if the year is outside of the given range
' if (v_year < u_minYear || v_year > u_maxYear) {',
' discard;',
' }',
' vec2 texCoord = v_texCoord * 2.0 - vec2(1.0, 1.0);',
' float sqRadius = texCoord.x * texCoord.x + texCoord.y * texCoord.y;',
' float value = 2.0 * (1.0 - sqRadius);',
' float alpha = smoothstep(0.0, 1.0, value);',
// color is interpolated based on year
' float ratio = clamp((v_year - 1800.0) / (2013.0 - 1800.0), 0.0, 1.1);',
' vec3 color = mix(vec3(' + formatColor(oldColor) + '),',
' vec3(' + formatColor(newColor) + '), ratio);',
' float period = 8.0;',
' color.g *= 2.0 * (1.0 - sqrt(mod(u_time + v_year * 0.025, period) / period));',
' gl_FragColor = vec4(color, 1.0);',
' gl_FragColor.a *= alpha;',
' gl_FragColor.rgb *= gl_FragColor.a;',
'}'
].join(' '),
uniforms: {
u_time: function() {
return Date.now() * 0.001 - startTime;
},
u_minYear: function() {
return parseInt(minYearInput.value);
},
u_maxYear: function() {
return parseInt(maxYearInput.value);
}
}
});
}
}
function loadData() {
const client = new XMLHttpRequest();
client.open('GET', 'data/csv/meteorite_landings.csv');
client.onload = function() {
const csv = client.responseText;
const features = [];
let prevIndex = csv.indexOf('\n') + 1; // scan past the header line
let curIndex;
while ((curIndex = csv.indexOf('\n', prevIndex)) != -1) {
const line = csv.substr(prevIndex, curIndex - prevIndex).split(',');
prevIndex = curIndex + 1;
const coords = fromLonLat([parseFloat(line[4]), parseFloat(line[3])]);
if (isNaN(coords[0]) || isNaN(coords[1])) {
// guard against bad data
continue;
}
features.push(new Feature({
mass: parseFloat(line[1]) || 0,
year: parseInt(line[2]) || 0,
geometry: new Point(coords)
}));
} }
vectorSource.addFeatures(features); features.push(new Feature({
}; mass: parseFloat(line[1]) || 0,
client.send(); year: parseInt(line[2]) || 0,
} geometry: new Point(coords)
}));
}
loadData(); vectorSource.addFeatures(features);
};
client.send();
const map = new Map({ const map = new Map({
layers: [ layers: [
@@ -167,7 +111,8 @@ const map = new Map({
layer: 'toner' layer: 'toner'
}) })
}), }),
new WebglPointsLayer({ new WebGLPointsLayer({
style: style,
source: vectorSource source: vectorSource
}) })
], ],
+4 -4
View File
@@ -27,10 +27,10 @@ const predefinedStyles = {
symbolType: 'triangle', symbolType: 'triangle',
size: 18, size: 18,
color: [ color: [
['stretch', ['get', 'population'], 20000, 300000, 0.1, 1.0], 'interpolate',
['stretch', ['get', 'population'], 20000, 300000, 0.6, 0.3], ['stretch', ['get', 'population'], 20000, 300000, 0, 1],
0.6, '#5aca5b',
1.0 '#ff6a19'
], ],
rotateWithView: true rotateWithView: true
} }
+11
View File
@@ -223,3 +223,14 @@ export function toString(color) {
const a = color[3] === undefined ? 1 : color[3]; const a = color[3] === undefined ? 1 : color[3];
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
} }
/**
* @param {string} s String.
* @return {boolean} Whether the string is actually a valid color
*/
export function isStringColor(s) {
if (NAMED_COLOR_RE_.test(s)) {
s = fromNamed(s);
}
return HEX_COLOR_RE_.test(s) || s.indexOf('rgba(') === 0 || s.indexOf('rgb(') === 0;
}
+4 -4
View File
@@ -3,7 +3,7 @@
*/ */
import {assign} from '../obj.js'; import {assign} from '../obj.js';
import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js'; import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js';
import {getSymbolFragmentShader, getSymbolVertexShader, parseSymbolStyle} from '../webgl/ShaderBuilder.js'; import {parseLiteralStyle} from '../webgl/ShaderBuilder.js';
import Layer from './Layer.js'; import Layer from './Layer.js';
@@ -74,7 +74,7 @@ class WebGLPointsLayer extends Layer {
* @private * @private
* @type {import('../webgl/ShaderBuilder.js').StyleParseResult} * @type {import('../webgl/ShaderBuilder.js').StyleParseResult}
*/ */
this.parseResult_ = parseSymbolStyle(options.style.symbol); this.parseResult_ = parseLiteralStyle(options.style);
} }
/** /**
@@ -82,8 +82,8 @@ class WebGLPointsLayer extends Layer {
*/ */
createRenderer() { createRenderer() {
return new WebGLPointsLayerRenderer(this, { return new WebGLPointsLayerRenderer(this, {
vertexShader: getSymbolVertexShader(this.parseResult_.params), vertexShader: this.parseResult_.builder.getSymbolVertexShader(),
fragmentShader: getSymbolFragmentShader(this.parseResult_.params), fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(),
uniforms: this.parseResult_.uniforms, uniforms: this.parseResult_.uniforms,
attributes: this.parseResult_.attributes attributes: this.parseResult_.attributes
}); });
+4
View File
@@ -132,6 +132,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
options.vertexShader options.vertexShader
); );
if (this.getShaderCompileErrors()) {
throw new Error(this.getShaderCompileErrors());
}
/** /**
* @type {boolean} * @type {boolean}
* @private * @private
+55 -5
View File
@@ -4,8 +4,58 @@
* @module ol/style/LiteralStyle * @module ol/style/LiteralStyle
*/ */
/**
* Base type used for literal style parameters; can be a number literal or the output of an operator,
* which in turns takes {@link ExpressionValue} arguments.
*
* The following operators can be used:
*
* * Reading operators:
* * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader)
* Note: those will be taken from the attributes provided to the renderer
* * `['var', 'varName']` fetches a value from the style variables, or 0 if undefined
* * `['time']` returns the time in seconds since the creation of the layer
*
* * Math operators:
* * `['*', value1, value1]` multiplies `value1` by `value2`
* * `['/', value1, value1]` divides `value1` by `value2`
* * `['+', value1, value1]` adds `value1` and `value2`
* * `['-', value1, value1]` subtracts `value2` from `value1`
* * `['clamp', value, low, high]` clamps `value` between `low` and `high`
* * `['stretch', value, low1, high1, low2, high2]` maps `value` from [`low1`, `high1`] range to
* [`low2`, `high2`] range, clamping values along the way
* * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo)
* * `['pow', value1, value1]` returns the value of `value1` raised to the `value2` power
*
* * Color operators:
* * `['interpolate', ratio, color1, color2]` returns a color through interpolation between `color1` and
* `color2` with the given `ratio`
*
* * Logical operators:
* * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise.
* * `['<=', value1, value2]` returns `1` if `value1` is lower than or equals value 2, or `0` otherwise.
* * `['>', value1, value2]` returns `1` if `value1` is strictly greater than value 2, or `0` otherwise.
* * `['>=', value1, value2]` returns `1` if `value1` is greater than or equals value 2, or `0` otherwise.
* * `['==', value1, value2]` returns `1` if `value1` equals value 2, or `0` otherwise.
* * `['!', value1]` returns `0` if `value1` strictly greater than `0`, or `1` otherwise.
* * `['between', value1, value2, value3]` returns `1` if `value1` is contained between `value2` and `value3`
* (inclusively), or `0` otherwise.
*
* Values can either be literals or another operator, as they will be evaluated recursively.
* Literal values can be of the following types:
* * `number`
* * `string`
* * {@link module:ol/color~Color}
*
* @typedef {Array<*>|import("../color.js").Color|string|number} ExpressionValue
*/
/** /**
* @typedef {Object} LiteralStyle * @typedef {Object} LiteralStyle
* @property {ExpressionValue} [filter] Filter expression. If it resolves to a number strictly greater than 0, the
* point will be displayed. If undefined, all points will show.
* @property {Object<string, number>} [variables] Style variables; each variable must hold a number.
* Note: **this object is meant to be mutated**: changes to the values will immediately be visible on the rendered features
* @property {LiteralSymbolStyle} [symbol] Symbol representation. * @property {LiteralSymbolStyle} [symbol] Symbol representation.
*/ */
@@ -22,12 +72,12 @@ export const SymbolType = {
/** /**
* @typedef {Object} LiteralSymbolStyle * @typedef {Object} LiteralSymbolStyle
* @property {number|Array.<number, number>} size Size, mandatory. * @property {ExpressionValue|Array<ExpressionValue, ExpressionValue>} size Size, mandatory.
* @property {SymbolType} symbolType Symbol type to use, either a regular shape or an image. * @property {SymbolType} symbolType Symbol type to use, either a regular shape or an image.
* @property {string} [src] Path to the image to be used for the symbol. Only required with `symbolType: 'image'`. * @property {string} [src] Path to the image to be used for the symbol. Only required with `symbolType: 'image'`.
* @property {import("../color.js").Color|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol). * @property {import("../color.js").Color|Array<ExpressionValue>|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol).
* @property {number} [opacity=1] Opacity. * @property {ExpressionValue} [opacity=1] Opacity.
* @property {Array.<number, number>} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered. * @property {Array<ExpressionValue, ExpressionValue>} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered.
* @property {Array.<number, number, number, number>} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes). * @property {Array<ExpressionValue, ExpressionValue, ExpressionValue, ExpressionValue>} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes).
* @property {boolean} [rotateWithView=false] Specify whether the symbol must rotate with the view or stay upwards. * @property {boolean} [rotateWithView=false] Specify whether the symbol must rotate with the view or stay upwards.
*/ */
+10 -1
View File
@@ -42,7 +42,8 @@ export const ShaderType = {
export const DefaultUniform = { export const DefaultUniform = {
PROJECTION_MATRIX: 'u_projectionMatrix', PROJECTION_MATRIX: 'u_projectionMatrix',
OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix', OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix',
OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix' OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix',
TIME: 'u_time'
}; };
/** /**
@@ -355,6 +356,12 @@ class WebGLHelper extends Disposable {
* @private * @private
*/ */
this.shaderCompileErrors_ = null; this.shaderCompileErrors_ = null;
/**
* @type {number}
* @private
*/
this.startTime_ = Date.now();
} }
/** /**
@@ -548,6 +555,8 @@ class WebGLHelper extends Disposable {
this.setUniformMatrixValue(DefaultUniform.OFFSET_SCALE_MATRIX, fromTransform(this.tmpMat4_, offsetScaleMatrix)); this.setUniformMatrixValue(DefaultUniform.OFFSET_SCALE_MATRIX, fromTransform(this.tmpMat4_, offsetScaleMatrix));
this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix)); this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix));
this.setUniformFloatValue(DefaultUniform.TIME, (Date.now() - this.startTime_) * 0.001);
} }
/** /**
+624 -184
View File
@@ -1,9 +1,9 @@
/** /**
* Utilities for generating shaders from literal style objects * Classes and utilities for generating shaders from literal style objects
* @module ol/webgl/ShaderBuilder * @module ol/webgl/ShaderBuilder
*/ */
import {asArray} from '../color.js'; import {asArray, isStringColor} from '../color.js';
/** /**
* Will return the number as a float with a dot separator, which is required by GLSL. * Will return the number as a float with a dot separator, which is required by GLSL.
@@ -18,23 +18,302 @@ export function formatNumber(v) {
/** /**
* Will return the number array as a float with a dot separator, concatenated with ', '. * Will return the number array as a float with a dot separator, concatenated with ', '.
* @param {Array<number>} array Numerical values array. * @param {Array<number>} array Numerical values array.
* @returns {string} The array as string, e. g.: `1.0, 2.0, 3.0`. * @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
*/ */
export function formatArray(array) { export function formatArray(array) {
return array.map(formatNumber).join(', '); if (array.length < 2 || array.length > 4) {
throw new Error('`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.');
}
return `vec${array.length}(${array.map(formatNumber).join(', ')})`;
} }
/** /**
* Will normalize and converts to string a color array compatible with GLSL. * Will normalize and converts to string a `vec4` color array compatible with GLSL.
* @param {Array<number>} colorArray Color in [r, g, b, a] array form, with RGB components in the * @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format,
* 0..255 range and the alpha component in the 0..1 range. Note that if the A component is * with RGB components in the 0..255 range and the alpha component in the 0..1 range.
* missing, only 3 values will be output. * Note that the final array will always have 4 components.
* @returns {string} The color components concatenated in `1.0, 1.0, 1.0, 1.0` form. * @returns {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
*/ */
export function formatColor(colorArray) { export function formatColor(color) {
return colorArray.map(function(c, i) { const array = asArray(color).slice();
return i < 3 ? c / 255 : c; if (array.length < 4) {
}).map(formatNumber).join(', '); array.push(1);
}
return formatArray(
array.map(function(c, i) {
return i < 3 ? c / 255 : c;
})
);
}
/**
* Possible inferred types from a given value or expression
* @enum {number}
*/
export const ValueTypes = {
UNKNOWN: -1,
NUMBER: 0,
STRING: 1,
COLOR: 2,
COLOR_OR_STRING: 3
};
function getValueType(value) {
if (typeof value === 'number') {
return ValueTypes.NUMBER;
}
if (typeof value === 'string') {
if (isStringColor(value)) {
return ValueTypes.COLOR_OR_STRING;
}
return ValueTypes.STRING;
}
if (!Array.isArray(value)) {
throw new Error(`Unrecognized value type: ${JSON.stringify(value)}`);
}
if (value.length === 3 || value.length === 4) {
const onlyNumbers = value.every(function(v) {
return typeof v === 'number';
});
if (onlyNumbers) {
return ValueTypes.COLOR;
}
}
if (typeof value[0] !== 'string') {
return ValueTypes.UNKNOWN;
}
switch (value[0]) {
case 'get':
case 'var':
case 'time':
case '*':
case '/':
case '+':
case '-':
case 'clamp':
case 'stretch':
case 'mod':
case 'pow':
case '>':
case '>=':
case '<':
case '<=':
case '==':
case '!':
case 'between':
return ValueTypes.NUMBER;
case 'interpolate':
return ValueTypes.COLOR;
default:
return ValueTypes.UNKNOWN;
}
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a numeric value, false otherwise
*/
export function isValueTypeNumber(value) {
return getValueType(value) === ValueTypes.NUMBER;
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a string value, false otherwise
*/
export function isValueTypeString(value) {
return getValueType(value) === ValueTypes.STRING || getValueType(value) === ValueTypes.COLOR_OR_STRING;
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a color value, false otherwise
*/
export function isValueTypeColor(value) {
return getValueType(value) === ValueTypes.COLOR || getValueType(value) === ValueTypes.COLOR_OR_STRING;
}
/**
* Check that the provided value or expression is valid, and that the types used are compatible.
*
* Will throw an exception if found to be invalid.
*
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
*/
export function check(value) {
// these will be used to validate types in the expressions
function checkNumber(value) {
if (!isValueTypeNumber(value)) {
throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`);
}
}
function checkColor(value) {
if (!isValueTypeColor(value)) {
throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`);
}
}
function checkString(value) {
if (!isValueTypeString(value)) {
throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`);
}
}
// first check that the value is of a recognized kind
if (!isValueTypeColor(value) && !isValueTypeNumber(value) && !isValueTypeString(value)) {
throw new Error(`No type could be inferred from the following expression: ${JSON.stringify(value)}`);
}
// check operator arguments
if (Array.isArray(value) && typeof value[0] === 'string') {
switch (value[0]) {
case 'get':
case 'var':
checkString(value[1]);
break;
case 'time':
break;
case '*':
case '/':
case '+':
case '-':
case 'mod':
case 'pow':
checkNumber(value[1]);
checkNumber(value[2]);
break;
case 'clamp':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
break;
case 'stretch':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
checkNumber(value[4]);
checkNumber(value[5]);
break;
case '>':
case '>=':
case '<':
case '<=':
case '==':
checkNumber(value[1]);
checkNumber(value[2]);
break;
case '!':
checkNumber(value[1]);
break;
case 'between':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
break;
case 'interpolate':
checkNumber(value[1]);
checkColor(value[2]);
checkColor(value[3]);
break;
default: throw new Error(`Unrecognized operator in style expression: ${JSON.stringify(value)}`);
}
}
}
/**
* Parses the provided expressions and produces a GLSL-compatible assignment string, such as:
* `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)'
*
* Also takes in two arrays where new attributes and variables will be pushed, so that the user of the `parse` function
* knows which attributes/variables are expected to be available at evaluation time.
*
* For attributes, a prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in
* the final assignment string (depending on whether we're outputting a vertex or fragment shader).
*
* If a wrong value type is supplied to an operator (i. e. using colors with the `clamp` operator), an exception
* will be thrown.
*
* Note that by default, the `string` value type will be given precedence over `color`, so for example the
* `'yellow'` literal value will be parsed as a `string` while being a valid CSS color. This can be changed with
* the `typeHint` optional parameter which disambiguates what kind of value is expected.
*
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @param {Array<string>} attributes Array containing the attribute names **without a prefix**;
* it is passed along recursively
* @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`).
* @param {Array<string>} variables Array containing the variable names **without a prefix**;
* it is passed along recursively
* @param {ValueTypes} [typeHint] Hint for inferred type
* @returns {string} Assignment string.
*/
export function parse(value, attributes, attributePrefix, variables, typeHint) {
check(value);
function p(value) {
return parse(value, attributes, attributePrefix, variables);
}
function pC(value) {
return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR);
}
// operator
if (Array.isArray(value) && typeof value[0] === 'string') {
switch (value[0]) {
// reading operators
case 'get':
if (attributes.indexOf(value[1]) === -1) {
attributes.push(value[1]);
}
return attributePrefix + value[1];
case 'var':
if (variables.indexOf(value[1]) === -1) {
variables.push(value[1]);
}
return `u_${value[1]}`;
case 'time':
return 'u_time';
// math operators
case '*':
case '/':
case '+':
case '-':
return `(${p(value[1])} ${value[0]} ${p(value[2])})`;
case 'clamp': return `clamp(${p(value[1])}, ${p(value[2])}, ${p(value[3])})`;
case 'stretch':
const low1 = p(value[2]);
const high1 = p(value[3]);
const low2 = p(value[4]);
const high2 = p(value[5]);
return `((clamp(${p(value[1])}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`;
case 'mod': return `mod(${p(value[1])}, ${p(value[2])})`;
case 'pow': return `pow(${p(value[1])}, ${p(value[2])})`;
// color operators
case 'interpolate':
return `mix(${pC(value[2])}, ${pC(value[3])}, ${p(value[1])})`;
// logical operators
case '>':
case '>=':
case '<':
case '<=':
case '==':
return `(${p(value[1])} ${value[0]} ${p(value[2])} ? 1.0 : 0.0)`;
case '!':
return `(${p(value[1])} > 0.0 ? 0.0 : 1.0)`;
case 'between':
return `(${p(value[1])} >= ${p(value[2])} && ${p(value[1])} <= ${p(value[3])} ? 1.0 : 0.0)`;
default: throw new Error('Invalid style expression: ' + JSON.stringify(value));
}
} else if (isValueTypeNumber(value)) {
return formatNumber(/** @type {number} */(value));
} else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) {
return `"${value}"`;
} else {
return formatColor(/** @type {number[]|string} */(value));
}
} }
/** /**
@@ -46,209 +325,357 @@ export function formatColor(colorArray) {
*/ */
/** /**
* @typedef {Object} ShaderParameters * @classdesc
* @property {Array<string>} [uniforms] Uniforms; these will be declared in the header (should include the type). * This class implements a classic builder pattern for generating many different types of shaders.
* @property {Array<string>} [attributes] Attributes; these will be declared in the header (should include the type). * Methods can be chained, e. g.:
* @property {Array<VaryingDescription>} [varyings] Varyings with a name, a type and an expression. *
* @property {string} sizeExpression This will be assigned to a `vec2 size` variable. * ```js
* @property {string} offsetExpression This will be assigned to a `vec2 offset` variable. * const shader = new ShaderBuilder()
* @property {string} colorExpression This will be the value assigned to gl_FragColor * .addVarying('v_width', 'float', 'a_width')
* @property {string} texCoordExpression This will be the value assigned to the `vec4 v_texCoord` varying. * .addUniform('u_time')
* @property {boolean} [rotateWithView=false] Whether symbols should rotate with view * .setColorExpression('...')
* .setSizeExpression('...')
* .outputSymbolFragmentShader();
* ```
*/ */
export class ShaderBuilder {
constructor() {
/**
* Uniforms; these will be declared in the header (should include the type).
* @type {Array<string>}
* @private
*/
this.uniforms = [];
/** /**
* Generates a symbol vertex shader from a set of parameters, * Attributes; these will be declared in the header (should include the type).
* intended to be used on point geometries. * @type {Array<string>}
* * @private
* Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix` and */
* `u_offsetRotateMatrix`. this.attributes = [];
*
* The following attributes are hardcoded and expected to be present in the vertex buffers:
* `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3).
*
* The following varyings are hardcoded and gives the coordinate of the pixel both in the quad on the texture:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @param {ShaderParameters} parameters Parameters for the shader.
* @returns {string} The full shader as a string.
*/
export function getSymbolVertexShader(parameters) {
const offsetMatrix = parameters.rotateWithView ?
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
'u_offsetScaleMatrix';
const uniforms = parameters.uniforms || []; /**
const attributes = parameters.attributes || []; * Varyings with a name, a type and an expression.
const varyings = parameters.varyings || []; * @type {Array<VaryingDescription>}
* @private
*/
this.varyings = [];
const body = `precision mediump float; /**
* @type {string}
* @private
*/
this.sizeExpression = 'vec2(1.0)';
/**
* @type {string}
* @private
*/
this.offsetExpression = 'vec2(0.0)';
/**
* @type {string}
* @private
*/
this.colorExpression = 'vec4(1.0)';
/**
* @type {string}
* @private
*/
this.texCoordExpression = 'vec4(0.0, 0.0, 1.0, 1.0)';
/**
* @type {string}
* @private
*/
this.discardExpression = 'false';
/**
* @type {boolean}
* @private
*/
this.rotateWithView = false;
}
/**
* Adds a uniform accessible in both fragment and vertex shaders.
* The given name should include a type, such as `sampler2D u_texture`.
* @param {string} name Uniform name
* @return {ShaderBuilder} the builder object
*/
addUniform(name) {
this.uniforms.push(name);
return this;
}
/**
* Adds an attribute accessible in the vertex shader, read from the geometry buffer.
* The given name should include a type, such as `vec2 a_position`.
* @param {string} name Attribute name
* @return {ShaderBuilder} the builder object
*/
addAttribute(name) {
this.attributes.push(name);
return this;
}
/**
* Adds a varying defined in the vertex shader and accessible from the fragment shader.
* The type and expression of the varying have to be specified separately.
* @param {string} name Varying name
* @param {'float'|'vec2'|'vec3'|'vec4'} type Type
* @param {string} expression Expression used to assign a value to the varying.
* @return {ShaderBuilder} the builder object
*/
addVarying(name, type, expression) {
this.varyings.push({
name: name,
type: type,
expression: expression
});
return this;
}
/**
* Sets an expression to compute the size of the shape.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec2` value.
* @param {string} expression Size expression
* @return {ShaderBuilder} the builder object
*/
setSizeExpression(expression) {
this.sizeExpression = expression;
return this;
}
/**
* Sets an expression to compute the offset of the symbol from the point center.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec2` value.
* Note: will only be used for point geometry shaders.
* @param {string} expression Offset expression
* @return {ShaderBuilder} the builder object
*/
setSymbolOffsetExpression(expression) {
this.offsetExpression = expression;
return this;
}
/**
* Sets an expression to compute the color of the shape.
* This expression can use all the uniforms, varyings and attributes available
* in the fragment shader, and should evaluate to a `vec4` value.
* @param {string} expression Color expression
* @return {ShaderBuilder} the builder object
*/
setColorExpression(expression) {
this.colorExpression = expression;
return this;
}
/**
* Sets an expression to compute the texture coordinates of the vertices.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec4` value.
* @param {string} expression Texture coordinate expression
* @return {ShaderBuilder} the builder object
*/
setTextureCoordinateExpression(expression) {
this.texCoordExpression = expression;
return this;
}
/**
* Sets an expression to determine whether a fragment (pixel) should be discarded,
* i.e. not drawn at all.
* This expression can use all the uniforms, varyings and attributes available
* in the fragment shader, and should evaluate to a `bool` value (it will be
* used in an `if` statement)
* @param {string} expression Fragment discard expression
* @return {ShaderBuilder} the builder object
*/
setFragmentDiscardExpression(expression) {
this.discardExpression = expression;
return this;
}
/**
* Sets whether the symbols should rotate with the view or stay aligned with the map.
* Note: will only be used for point geometry shaders.
* @param {boolean} rotateWithView Rotate with view
* @return {ShaderBuilder} the builder object
*/
setSymbolRotateWithView(rotateWithView) {
this.rotateWithView = rotateWithView;
return this;
}
/**
* @returns {string} Previously set size expression
*/
getSizeExpression() {
return this.sizeExpression;
}
/**
* @returns {string} Previously set symbol offset expression
*/
getOffsetExpression() {
return this.offsetExpression;
}
/**
* @returns {string} Previously set color expression
*/
getColorExpression() {
return this.colorExpression;
}
/**
* @returns {string} Previously set texture coordinate expression
*/
getTextureCoordinateExpression() {
return this.texCoordExpression;
}
/**
* @returns {string} Previously set fragment discard expression
*/
getFragmentDiscardExpression() {
return this.discardExpression;
}
/**
* Generates a symbol vertex shader from the builder parameters,
* intended to be used on point geometries.
*
* Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix`,
* `u_offsetRotateMatrix`, `u_time`.
*
* The following attributes are hardcoded and expected to be present in the vertex buffers:
* `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3).
*
* The following varyings are hardcoded and gives the coordinate of the pixel both in the quad and on the texture:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @returns {string} The full shader as a string.
*/
getSymbolVertexShader() {
const offsetMatrix = this.rotateWithView ?
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
'u_offsetScaleMatrix';
return `precision mediump float;
uniform mat4 u_projectionMatrix; uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
${uniforms.map(function(uniform) { uniform float u_time;
${this.uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';'; return 'uniform ' + uniform + ';';
}).join('\n')} }).join('\n')}
attribute vec2 a_position; attribute vec2 a_position;
attribute float a_index; attribute float a_index;
${attributes.map(function(attribute) { ${this.attributes.map(function(attribute) {
return 'attribute ' + attribute + ';'; return 'attribute ' + attribute + ';';
}).join('\n')} }).join('\n')}
varying vec2 v_texCoord; varying vec2 v_texCoord;
varying vec2 v_quadCoord; varying vec2 v_quadCoord;
${varyings.map(function(varying) { ${this.varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';'; return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')} }).join('\n')}
void main(void) { void main(void) {
mat4 offsetMatrix = ${offsetMatrix}; mat4 offsetMatrix = ${offsetMatrix};
vec2 size = ${parameters.sizeExpression}; vec2 size = ${this.sizeExpression};
vec2 offset = ${parameters.offsetExpression}; vec2 offset = ${this.offsetExpression};
float offsetX = a_index == 0.0 || a_index == 3.0 ? offset.x - size.x / 2.0 : offset.x + size.x / 2.0; float offsetX = a_index == 0.0 || a_index == 3.0 ? offset.x - size.x / 2.0 : offset.x + size.x / 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? offset.y - size.y / 2.0 : offset.y + size.y / 2.0; float offsetY = a_index == 0.0 || a_index == 1.0 ? offset.y - size.y / 2.0 : offset.y + size.y / 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0); vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets; gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
vec4 texCoord = ${parameters.texCoordExpression}; vec4 texCoord = ${this.texCoordExpression};
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.q; float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.q;
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.p; float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.p;
v_texCoord = vec2(u, v); v_texCoord = vec2(u, v);
u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0; u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;
v = a_index == 2.0 || a_index == 3.0 ? 0.0 : 1.0; v = a_index == 2.0 || a_index == 3.0 ? 0.0 : 1.0;
v_quadCoord = vec2(u, v); v_quadCoord = vec2(u, v);
${varyings.map(function(varying) { ${this.varyings.map(function(varying) {
return ' ' + varying.name + ' = ' + varying.expression + ';'; return ' ' + varying.name + ' = ' + varying.expression + ';';
}).join('\n')} }).join('\n')}
}`; }`;
}
return body; /**
} * Generates a symbol fragment shader from the builder parameters,
* intended to be used on point geometries.
/** *
* Generates a symbol fragment shader intended to be used on point geometries. * Expects the following varyings to be transmitted by the vertex shader:
* * `vec2 v_quadCoord`, `vec2 v_texCoord`
* Expected the following varyings to be transmitted by the vertex shader: *
* `vec2 v_texCoord` * @returns {string} The full shader as a string.
* */
* @param {ShaderParameters} parameters Parameters for the shader. getSymbolFragmentShader() {
* @returns {string} The full shader as a string. return `precision mediump float;
*/ uniform float u_time;
export function getSymbolFragmentShader(parameters) { ${this.uniforms.map(function(uniform) {
const uniforms = parameters.uniforms || [];
const varyings = parameters.varyings || [];
const body = `precision mediump float;
${uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';'; return 'uniform ' + uniform + ';';
}).join('\n')} }).join('\n')}
varying vec2 v_texCoord; varying vec2 v_texCoord;
varying vec2 v_quadCoord; varying vec2 v_quadCoord;
${varyings.map(function(varying) { ${this.varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';'; return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')} }).join('\n')}
void main(void) { void main(void) {
gl_FragColor = ${parameters.colorExpression}; if (${this.discardExpression}) { discard; }
gl_FragColor = ${this.colorExpression};
gl_FragColor.rgb *= gl_FragColor.a; gl_FragColor.rgb *= gl_FragColor.a;
}`; }`;
return body;
}
/**
* Base type for values fed to operators; can be a number literal or the output of another operator
* @typedef {Array<*>|number} OperatorValue
*/
/**
* Parses the provided expressions and produces a GLSL-compatible assignment string, such as:
* `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)'
*
* The following operators can be used:
* * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader)
* * `['*', value1, value1]` multiplies value1 by value2
* * `['+', value1, value1]` adds value1 and value2
* * `['clamp', value1, value2, value3]` clamps value1 between values2 and value3
* * `['stretch', value1, value2, value3, value4, value5]` maps value1 from [value2, value3] range to
* [value4, value5] range, clamping values along the way
*
* Values can either be literals (numbers) or another operator, as they will be evaluated recursively.
*
* Also takes in an array where new attributes will be pushed, so that the user of the `parse` function
* knows which attributes are expected to be available at evaluation time.
*
* A prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in
* the final assignment string.
*
* @param {OperatorValue} value Either literal or an operator.
* @param {Array<string>} attributes Array containing the attribute names prefixed with `a_`; it
* it passed along recursively
* @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`).
* @returns {string} Assignment string.
*/
export function parse(value, attributes, attributePrefix) {
const v = value;
function p(value) {
return parse(value, attributes, attributePrefix);
}
if (Array.isArray(v)) {
switch (v[0]) {
case 'get':
if (attributes.indexOf(v[1]) === -1) {
attributes.push(v[1]);
}
return attributePrefix + v[1];
case '*': return `(${p(v[1])} * ${p(v[2])})`;
case '+': return `(${p(v[1])} + ${p(v[2])})`;
case 'clamp': return `clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])})`;
case 'stretch': return `(clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])}) * ((${p(v[5])} - ${p(v[4])}) / (${p(v[3])} - ${p(v[2])})) + ${p(v[4])})`;
default: throw new Error('Unrecognized literal style expression: ' + JSON.stringify(value));
}
} else if (typeof value === 'number') {
return formatNumber(value);
} else {
throw new Error('Invalid value type in expression: ' + JSON.stringify(value));
} }
} }
/** /**
* @typedef {Object} StyleParseResult * @typedef {Object} StyleParseResult
* @property {ShaderParameters} params Symbol shader params. * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style
* @property {Object.<string,import("./Helper").UniformValue>} uniforms Uniform definitions. * @property {Object.<string,import("./Helper").UniformValue>} uniforms Uniform definitions.
* @property {Array<import("../renderer/webgl/PointsLayer").CustomAttribute>} attributes Attribute descriptions. * @property {Array<import("../renderer/webgl/PointsLayer").CustomAttribute>} attributes Attribute descriptions.
*/ */
/** /**
* Parses a {@link import("../style/LiteralStyle").LiteralSymbolStyle} object and outputs shader parameters to be * Parses a {@link import("../style/LiteralStyle").LiteralStyle} object and returns a {@link ShaderBuilder}
* then fed to {@link getSymbolVertexShader} and {@link getSymbolFragmentShader}. * object that has been configured according to the given style, as well as `attributes` and `uniforms`
* arrays to be fed to the `WebGLPointsRenderer` class.
* *
* Also returns `uniforms` and `attributes` properties as expected by the * Also returns `uniforms` and `attributes` properties as expected by the
* {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}. * {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
* *
* @param {import("../style/LiteralStyle").LiteralSymbolStyle} style Symbol style. * @param {import("../style/LiteralStyle").LiteralStyle} style Literal style.
* @returns {StyleParseResult} Result containing shader params, attributes and uniforms. * @returns {StyleParseResult} Result containing shader params, attributes and uniforms.
*/ */
export function parseSymbolStyle(style) { export function parseLiteralStyle(style) {
const size = Array.isArray(style.size) && typeof style.size[0] == 'number' ? const symbStyle = style.symbol;
style.size : [style.size, style.size]; const size = Array.isArray(symbStyle.size) && typeof symbStyle.size[0] == 'number' ?
const color = (typeof style.color === 'string' ? symbStyle.size : [symbStyle.size, symbStyle.size];
asArray(style.color).map(function(c, i) { const color = symbStyle.color || 'white';
return i < 3 ? c / 255 : c; const texCoord = symbStyle.textureCoord || [0, 0, 1, 1];
}) : const offset = symbStyle.offset || [0, 0];
style.color || [255, 255, 255, 1]); const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1;
const texCoord = style.textureCoord || [0, 0, 1, 1];
const offset = style.offset || [0, 0];
const opacity = style.opacity !== undefined ? style.opacity : 1;
let attributes = []; const variables = [];
const varyings = []; const vertAttributes = [];
function pA(value) { // parse function for vertex shader
return parse(value, attributes, 'a_'); function pVert(value) {
return parse(value, vertAttributes, 'a_', variables);
} }
function pV(value) {
return parse(value, varyings, 'v_'); const fragAttributes = [];
// parse function for fragment shader
function pFrag(value, type) {
return parse(value, fragAttributes, 'v_', variables, type);
} }
let opacityFilter = '1.0'; let opacityFilter = '1.0';
const visibleSize = pV(size[0]); const visibleSize = pFrag(size[0]);
switch (style.symbolType) { switch (symbStyle.symbolType) {
case 'square': break; case 'square': break;
case 'image': break; case 'image': break;
// taken from https://thebookofshaders.com/07/ // taken from https://thebookofshaders.com/07/
@@ -261,49 +688,62 @@ export function parseSymbolStyle(style) {
opacityFilter = `(1.0-smoothstep(.5-3./${visibleSize},.5,cos(floor(.5+${a}/2.094395102)*2.094395102-${a})*length(${st})))`; opacityFilter = `(1.0-smoothstep(.5-3./${visibleSize},.5,cos(floor(.5+${a}/2.094395102)*2.094395102-${a})*length(${st})))`;
break; break;
default: throw new Error('Unexpected symbol type: ' + style.symbolType); default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
} }
/** @type {import('../webgl/ShaderBuilder.js').ShaderParameters} */ const parsedColor = pFrag(color, ValueTypes.COLOR);
const params = {
uniforms: [],
colorExpression: `vec4(${pV(color[0])}, ${pV(color[1])}, ${pV(color[2])}, ${pV(color[3])})` +
` * vec4(1.0, 1.0, 1.0, ${pV(opacity)} * ${opacityFilter})`,
sizeExpression: `vec2(${pA(size[0])}, ${pA(size[1])})`,
offsetExpression: `vec2(${pA(offset[0])}, ${pA(offset[1])})`,
texCoordExpression: `vec4(${pA(texCoord[0])}, ${pA(texCoord[1])}, ${pA(texCoord[2])}, ${pA(texCoord[3])})`,
rotateWithView: !!style.rotateWithView
};
attributes = attributes.concat(varyings).filter(function(attrName, index, arr) { const builder = new ShaderBuilder()
return arr.indexOf(attrName) === index; .setSizeExpression(`vec2(${pVert(size[0])}, ${pVert(size[1])})`)
}); .setSymbolOffsetExpression(`vec2(${pVert(offset[0])}, ${pVert(offset[1])})`)
params.attributes = attributes.map(function(attributeName) { .setTextureCoordinateExpression(
return `float a_${attributeName}`; `vec4(${pVert(texCoord[0])}, ${pVert(texCoord[1])}, ${pVert(texCoord[2])}, ${pVert(texCoord[3])})`)
}); .setSymbolRotateWithView(!!symbStyle.rotateWithView)
params.varyings = varyings.map(function(attributeName) { .setColorExpression(
return { `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`);
name: `v_${attributeName}`,
type: 'float', if (style.filter) {
expression: `a_${attributeName}` builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`);
}; }
});
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */ /** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
const uniforms = {}; const uniforms = {};
if (style.symbolType === 'image' && style.src) { // define one uniform per variable
variables.forEach(function(varName) {
builder.addUniform(`float u_${varName}`);
uniforms[`u_${varName}`] = function() {
return style.variables && style.variables[varName] !== undefined ?
style.variables[varName] : 0;
};
});
if (symbStyle.symbolType === 'image' && symbStyle.src) {
const texture = new Image(); const texture = new Image();
texture.src = style.src; texture.src = symbStyle.src;
params.uniforms.push('sampler2D u_texture'); builder.addUniform('sampler2D u_texture')
params.colorExpression = params.colorExpression + .setColorExpression(builder.getColorExpression() +
' * texture2D(u_texture, v_texCoord)'; ' * texture2D(u_texture, v_texCoord)');
uniforms['u_texture'] = texture; uniforms['u_texture'] = texture;
} }
// for each feature attribute used in the fragment shader, define a varying that will be used to pass data
// from the vertex to the fragment shader, as well as an attribute in the vertex shader (if not already present)
fragAttributes.forEach(function(attrName) {
if (vertAttributes.indexOf(attrName) === -1) {
vertAttributes.push(attrName);
}
builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`);
});
// for each feature attribute used in the vertex shader, define an attribute in the vertex shader.
vertAttributes.forEach(function(attrName) {
builder.addAttribute(`float a_${attrName}`);
});
return { return {
params: params, builder: builder,
attributes: attributes.map(function(attributeName) { attributes: vertAttributes.map(function(attributeName) {
return { return {
name: attributeName, name: attributeName,
callback: function(feature) { callback: function(feature) {
+15
View File
@@ -2,6 +2,7 @@ import {
asArray, asArray,
asString, asString,
fromString, fromString,
isStringColor,
normalize, normalize,
toString toString
} from '../../../src/ol/color.js'; } from '../../../src/ol/color.js';
@@ -159,4 +160,18 @@ describe('ol.color', function() {
}); });
}); });
describe('isValid()', function() {
it('correctly detects valid colors', function() {
expect(isStringColor('rgba(1,3,4,0.4)')).to.be(true);
expect(isStringColor('rgb(1,3,4)')).to.be(true);
expect(isStringColor('lightgreen')).to.be(true);
expect(isStringColor('yellow')).to.be(true);
expect(isStringColor('GREEN')).to.be(true);
expect(isStringColor('notacolor')).to.be(false);
expect(isStringColor('red_')).to.be(false);
});
});
}); });
+9 -3
View File
@@ -1,4 +1,4 @@
import WebGLHelper from '../../../../src/ol/webgl/Helper.js'; import WebGLHelper, {DefaultUniform} from '../../../../src/ol/webgl/Helper.js';
import { import {
create as createTransform, create as createTransform,
rotate as rotateTransform, rotate as rotateTransform,
@@ -11,9 +11,9 @@ import {FLOAT} from '../../../../src/ol/webgl.js';
const VERTEX_SHADER = ` const VERTEX_SHADER = `
precision mediump float; precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
attribute float a_test; attribute float a_test;
uniform float u_test; uniform float u_test;
@@ -25,9 +25,9 @@ const VERTEX_SHADER = `
const INVALID_VERTEX_SHADER = ` const INVALID_VERTEX_SHADER = `
precision mediump float; precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
bla bla
uniform float u_test; uniform float u_test;
@@ -123,6 +123,12 @@ describe('ol.webgl.WebGLHelper', function() {
expect(h.getCanvas().height).to.eql(160); expect(h.getCanvas().height).to.eql(160);
}); });
it('has processed default uniforms', function() {
expect(h.uniformLocations_[DefaultUniform.OFFSET_ROTATION_MATRIX]).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.OFFSET_SCALE_MATRIX]).not.to.eql(undefined);
expect(h.uniformLocations_[DefaultUniform.TIME]).not.to.eql(undefined);
});
it('has processed uniforms', function() { it('has processed uniforms', function() {
expect(h.uniforms_.length).to.eql(4); expect(h.uniforms_.length).to.eql(4);
expect(h.uniforms_[0].name).to.eql('u_test1'); expect(h.uniforms_[0].name).to.eql('u_test1');
+394 -129
View File
@@ -1,8 +1,15 @@
import { import {
getSymbolVertexShader, check,
formatArray,
formatColor,
formatNumber, formatNumber,
getSymbolFragmentShader, isValueTypeColor,
formatColor, formatArray, parse, parseSymbolStyle isValueTypeNumber,
isValueTypeString,
parse,
parseLiteralStyle,
ShaderBuilder,
ValueTypes
} from '../../../../src/ol/webgl/ShaderBuilder.js'; } from '../../../../src/ol/webgl/ShaderBuilder.js';
describe('ol.webgl.ShaderBuilder', function() { describe('ol.webgl.ShaderBuilder', function() {
@@ -19,39 +26,93 @@ describe('ol.webgl.ShaderBuilder', function() {
describe('formatArray', function() { describe('formatArray', function() {
it('outputs numbers with dot separators', function() { it('outputs numbers with dot separators', function() {
expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('1.0, 0.0, 3.45, 0.8888'); expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('vec4(1.0, 0.0, 3.45, 0.8888)');
expect(formatArray([3, 4])).to.eql('vec2(3.0, 4.0)');
});
it('throws on invalid lengths', function() {
let thrown = false;
try {
formatArray([3]);
} catch (e) {
thrown = true;
}
try {
formatArray([3, 2, 1, 0, -1]);
} catch (e) {
thrown = true;
}
expect(thrown).to.be(true);
}); });
}); });
describe('formatColor', function() { describe('formatColor', function() {
it('normalizes color and outputs numbers with dot separators', function() { it('normalizes color and outputs numbers with dot separators', function() {
expect(formatColor([100, 0, 255, 1])).to.eql('0.39215686274509803, 0.0, 1.0, 1.0'); expect(formatColor([100, 0, 255])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
expect(formatColor([100, 0, 255, 1])).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
});
it('handles colors in string format', function() {
expect(formatColor('red')).to.eql('vec4(1.0, 0.0, 0.0, 1.0)');
expect(formatColor('#00ff99')).to.eql('vec4(0.0, 1.0, 0.6, 1.0)');
expect(formatColor('rgb(100, 0, 255)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 1.0)');
expect(formatColor('rgba(100, 0, 255, 0.3)')).to.eql('vec4(0.39215686274509803, 0.0, 1.0, 0.3)');
});
});
describe('value type checking', function() {
it('correctly recognizes a number value', function() {
expect(isValueTypeNumber(1234)).to.eql(true);
expect(isValueTypeNumber(['time'])).to.eql(true);
expect(isValueTypeNumber(['clamp', ['get', 'attr'], -1, 1])).to.eql(true);
expect(isValueTypeNumber(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false);
expect(isValueTypeNumber('yellow')).to.eql(false);
expect(isValueTypeNumber('#113366')).to.eql(false);
expect(isValueTypeNumber('rgba(252,171,48,0.62)')).to.eql(false);
});
it('correctly recognizes a color value', function() {
expect(isValueTypeColor(1234)).to.eql(false);
expect(isValueTypeColor(['time'])).to.eql(false);
expect(isValueTypeColor(['clamp', ['get', 'attr'], -1, 1])).to.eql(false);
expect(isValueTypeColor(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(true);
expect(isValueTypeColor('yellow')).to.eql(true);
expect(isValueTypeColor('#113366')).to.eql(true);
expect(isValueTypeColor('rgba(252,171,48,0.62)')).to.eql(true);
expect(isValueTypeColor('abcd')).to.eql(false);
});
it('correctly recognizes a string value', function() {
expect(isValueTypeString(1234)).to.eql(false);
expect(isValueTypeString(['time'])).to.eql(false);
expect(isValueTypeString(['clamp', ['get', 'attr'], -1, 1])).to.eql(false);
expect(isValueTypeString(['interpolate', ['get', 'attr'], 'red', 'green'])).to.eql(false);
expect(isValueTypeString('yellow')).to.eql(true);
expect(isValueTypeString('#113366')).to.eql(true);
expect(isValueTypeString('rgba(252,171,48,0.62)')).to.eql(true);
expect(isValueTypeString('abcd')).to.eql(true);
});
it('throws on an unsupported type', function(done) {
try {
isValueTypeColor(true);
} catch (e) {
done();
}
done(true);
}); });
}); });
describe('getSymbolVertexShader', function() { describe('getSymbolVertexShader', function() {
it('generates a symbol vertex shader (with varying)', function() { it('generates a symbol vertex shader (with varying)', function() {
const parameters = { const builder = new ShaderBuilder();
varyings: [{ builder.addVarying('v_opacity', 'float', formatNumber(0.4));
name: 'v_opacity', builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3]));
type: 'float', builder.setSizeExpression(`vec2(${formatNumber(6)})`);
expression: formatNumber(0.4) builder.setSymbolOffsetExpression(formatArray([5, -7]));
}, { builder.setColorExpression(formatColor([80, 0, 255, 1]));
name: 'v_test', builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
type: 'vec3',
expression: 'vec3(' + formatArray([1, 2, 3]) + ')'
}],
sizeExpression: 'vec2(' + formatNumber(6) + ')',
offsetExpression: 'vec2(' + formatArray([5, -7]) + ')',
colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')',
texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')',
rotateWithView: false
};
expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
uniform mat4 u_projectionMatrix; uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
attribute vec2 a_position; attribute vec2 a_position;
attribute float a_index; attribute float a_index;
@@ -80,19 +141,19 @@ void main(void) {
}`); }`);
}); });
it('generates a symbol vertex shader (with uniforms and attributes)', function() { it('generates a symbol vertex shader (with uniforms and attributes)', function() {
const parameters = { const builder = new ShaderBuilder();
uniforms: ['float u_myUniform'], builder.addUniform('float u_myUniform');
attributes: ['vec2 a_myAttr'], builder.addAttribute('vec2 a_myAttr');
sizeExpression: 'vec2(' + formatNumber(6) + ')', builder.setSizeExpression(`vec2(${formatNumber(6)})`);
offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', builder.setSymbolOffsetExpression(formatArray([5, -7]));
colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', builder.setColorExpression(formatColor([80, 0, 255, 1]));
texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
};
expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
uniform mat4 u_projectionMatrix; uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
uniform float u_myUniform; uniform float u_myUniform;
attribute vec2 a_position; attribute vec2 a_position;
attribute float a_index; attribute float a_index;
@@ -119,18 +180,18 @@ void main(void) {
}`); }`);
}); });
it('generates a symbol vertex shader (with rotateWithView)', function() { it('generates a symbol vertex shader (with rotateWithView)', function() {
const parameters = { const builder = new ShaderBuilder();
sizeExpression: 'vec2(' + formatNumber(6) + ')', builder.setSizeExpression(`vec2(${formatNumber(6)})`);
offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', builder.setSymbolOffsetExpression(formatArray([5, -7]));
colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', builder.setColorExpression(formatColor([80, 0, 255, 1]));
texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
rotateWithView: true builder.setSymbolRotateWithView(true);
};
expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; expect(builder.getSymbolVertexShader()).to.eql(`precision mediump float;
uniform mat4 u_projectionMatrix; uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix; uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
attribute vec2 a_position; attribute vec2 a_position;
attribute float a_index; attribute float a_index;
@@ -160,161 +221,365 @@ void main(void) {
describe('getSymbolFragmentShader', function() { describe('getSymbolFragmentShader', function() {
it('generates a symbol fragment shader (with varying)', function() { it('generates a symbol fragment shader (with varying)', function() {
const parameters = { const builder = new ShaderBuilder();
varyings: [{ builder.addVarying('v_opacity', 'float', formatNumber(0.4));
name: 'v_opacity', builder.addVarying('v_test', 'vec3', formatArray([1, 2, 3]));
type: 'float', builder.setSizeExpression(`vec2(${formatNumber(6)})`);
expression: formatNumber(0.4) builder.setSymbolOffsetExpression(formatArray([5, -7]));
}, { builder.setColorExpression(formatColor([80, 0, 255]));
name: 'v_test', builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
type: 'vec3',
expression: 'vec3(' + formatArray([1, 2, 3]) + ')'
}],
sizeExpression: 'vec2(' + formatNumber(6) + ')',
offsetExpression: 'vec2(' + formatArray([5, -7]) + ')',
colorExpression: 'vec4(' + formatColor([80, 0, 255]) + ', v_opacity)',
texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')',
rotateWithView: false
};
expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
uniform float u_time;
varying vec2 v_texCoord; varying vec2 v_texCoord;
varying vec2 v_quadCoord; varying vec2 v_quadCoord;
varying float v_opacity; varying float v_opacity;
varying vec3 v_test; varying vec3 v_test;
void main(void) { void main(void) {
gl_FragColor = vec4(0.3137254901960784, 0.0, 1.0, v_opacity); if (false) { discard; }
gl_FragColor = vec4(0.3137254901960784, 0.0, 1.0, 1.0);
gl_FragColor.rgb *= gl_FragColor.a; gl_FragColor.rgb *= gl_FragColor.a;
}`); }`);
}); });
it('generates a symbol fragment shader (with uniforms)', function() { it('generates a symbol fragment shader (with uniforms)', function() {
const parameters = { const builder = new ShaderBuilder();
uniforms: ['float u_myUniform', 'vec2 u_myUniform2'], builder.addUniform('float u_myUniform');
sizeExpression: 'vec2(' + formatNumber(6) + ')', builder.addUniform('vec2 u_myUniform2');
offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', builder.setSizeExpression(`vec2(${formatNumber(6)})`);
colorExpression: 'vec4(' + formatColor([255, 255, 255, 1]) + ')', builder.setSymbolOffsetExpression(formatArray([5, -7]));
texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' builder.setColorExpression(formatColor([255, 255, 255, 1]));
}; builder.setTextureCoordinateExpression(formatArray([0, 0.5, 0.5, 1]));
builder.setFragmentDiscardExpression('u_myUniform > 0.5');
expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; expect(builder.getSymbolFragmentShader()).to.eql(`precision mediump float;
uniform float u_time;
uniform float u_myUniform; uniform float u_myUniform;
uniform vec2 u_myUniform2; uniform vec2 u_myUniform2;
varying vec2 v_texCoord; varying vec2 v_texCoord;
varying vec2 v_quadCoord; varying vec2 v_quadCoord;
void main(void) { void main(void) {
if (u_myUniform > 0.5) { discard; }
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
gl_FragColor.rgb *= gl_FragColor.a; gl_FragColor.rgb *= gl_FragColor.a;
}`); }`);
}); });
}); });
describe('check', function() {
it('does not throw on valid expressions', function(done) {
check(1);
check('attr');
check('rgba(12, 34, 56, 0.5)');
check([255, 255, 255, 1]);
check([255, 255, 255]);
check(['get', 'myAttr']);
check(['var', 'myValue']);
check(['time']);
check(['+', ['*', ['get', 'size'], 0.001], 12]);
check(['/', ['-', ['get', 'size'], 0.001], 12]);
check(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20]);
check(['stretch', ['get', 'size'], 10, 100, 4, 8]);
check(['mod', ['pow', ['get', 'size'], 0.5], 12]);
check(['>', 10, ['get', 'attr4']]);
check(['>=', 10, ['get', 'attr4']]);
check(['<', 10, ['get', 'attr4']]);
check(['<=', 10, ['get', 'attr4']]);
check(['==', 10, ['get', 'attr4']]);
check(['between', ['get', 'attr4'], -4.0, 5.0]);
check(['!', ['get', 'attr4']]);
check(['interpolate', ['get', 'attr4'], 'green', '#3344FF']);
check(['interpolate', 0.2, [10, 20, 30], [255, 255, 255, 1]]);
done();
});
it('throws on unsupported types for operators', function() {
let thrown = false;
try {
check(['var', 1234]);
} catch (e) {
thrown = true;
}
try {
check(['<', 0, 'aa']);
} catch (e) {
thrown = true;
}
try {
check(['+', true, ['get', 'attr']]);
} catch (e) {
thrown = true;
}
try {
check(['interpolate', ['get', 'attr4'], 1, '#3344FF']);
} catch (e) {
thrown = true;
}
expect(thrown).to.be(true);
});
it('throws with the wrong number of arguments', function() {
let thrown = false;
try {
check(['var', 1234, 456]);
} catch (e) {
thrown = true;
}
try {
check(['<', 4]);
} catch (e) {
thrown = true;
}
try {
check(['+']);
} catch (e) {
thrown = true;
}
expect(thrown).to.be(true);
});
it('throws on invalid expressions', function() {
let thrown = false;
try {
check(true);
} catch (e) {
thrown = true;
}
try {
check([123, 456]);
} catch (e) {
thrown = true;
}
try {
check(null);
} catch (e) {
thrown = true;
}
expect(thrown).to.be(true);
});
});
describe('parse', function() { describe('parse', function() {
let attributes, prefix, parseFn; let attributes, prefix, variables, parseFn;
beforeEach(function() { beforeEach(function() {
attributes = []; attributes = [];
variables = [];
prefix = 'a_'; prefix = 'a_';
parseFn = function(value) { parseFn = function(value, type) {
return parse(value, attributes, prefix); return parse(value, attributes, prefix, variables, type);
}; };
}); });
it('parses expressions & literal values', function() { it('parses expressions & literal values', function() {
expect(parseFn(1)).to.eql('1.0'); expect(parseFn(1)).to.eql('1.0');
expect(parseFn('a_random_string')).to.eql('"a_random_string"');
expect(parseFn([255, 127.5, 63.75, 0.1])).to.eql('vec4(1.0, 0.5, 0.25, 0.1)');
expect(parseFn([255, 127.5, 63.75])).to.eql('vec4(1.0, 0.5, 0.25, 1.0)');
expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr'); expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr');
expect(parseFn(['var', 'myValue'])).to.eql('u_myValue');
expect(parseFn(['time'])).to.eql('u_time');
expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)'); expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)');
expect(parseFn(['/', ['-', ['get', 'size'], 20], 100])).to.eql('((a_size - 20.0) / 100.0)');
expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)'); expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)');
expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('(clamp(a_size, 10.0, 100.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)'); expect(parseFn(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql('((clamp(a_size, 10.0, 100.0) - 10.0) * ((8.0 - 4.0) / (100.0 - 10.0)) + 4.0)');
expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3']); expect(parseFn(['pow', ['mod', ['time'], 10], 2])).to.eql('pow(mod(u_time, 10.0), 2.0)');
expect(parseFn(['>', 10, ['get', 'attr4']])).to.eql('(10.0 > a_attr4 ? 1.0 : 0.0)');
expect(parseFn(['>=', 10, ['get', 'attr4']])).to.eql('(10.0 >= a_attr4 ? 1.0 : 0.0)');
expect(parseFn(['<', 10, ['get', 'attr4']])).to.eql('(10.0 < a_attr4 ? 1.0 : 0.0)');
expect(parseFn(['<=', 10, ['get', 'attr4']])).to.eql('(10.0 <= a_attr4 ? 1.0 : 0.0)');
expect(parseFn(['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4 ? 1.0 : 0.0)');
expect(parseFn(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0 ? 1.0 : 0.0)');
expect(parseFn(['!', ['get', 'attr4']])).to.eql('(a_attr4 > 0.0 ? 0.0 : 1.0)');
expect(parseFn(['interpolate', ['get', 'attr4'], [255, 255, 255, 1], 'transparent'])).to.eql(
'mix(vec4(1.0, 1.0, 1.0, 1.0), vec4(0.0, 0.0, 0.0, 0.0), a_attr4)');
expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3', 'attr4']);
expect(variables).to.eql(['myValue']);
});
it('gives precedence to the string type unless asked otherwise', function() {
expect(parseFn('lightgreen')).to.eql('"lightgreen"');
expect(parseFn('lightgreen', ValueTypes.COLOR)).to.eql('vec4(0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1.0)');
}); });
it('does not register an attribute several times', function() { it('does not register an attribute several times', function() {
parseFn(['get', 'myAttr']); parseFn(['get', 'myAttr']);
parseFn(['var', 'myVar']);
parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]); parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]);
parseFn(['*', ['get', 'attr2'], ['var', 'myVar']]);
expect(attributes).to.eql(['myAttr', 'attr2']); expect(attributes).to.eql(['myAttr', 'attr2']);
expect(variables).to.eql(['myVar']);
}); });
}); });
describe('parseSymbolStyle', function() { describe('parseSymbolStyle', function() {
it('parses a style without expressions', function() { it('parses a style without expressions', function() {
const result = parseSymbolStyle({ const result = parseLiteralStyle({
symbolType: 'square', symbol: {
size: [4, 8], symbolType: 'square',
color: '#336699', size: [4, 8],
rotateWithView: true color: '#ff0000',
}); rotateWithView: true
expect(result.params).to.eql({ }
uniforms: [],
attributes: [],
varyings: [],
colorExpression: 'vec4(0.2, 0.4, 0.6, 1.0) * vec4(1.0, 1.0, 1.0, 1.0 * 1.0)',
sizeExpression: 'vec2(4.0, 8.0)',
offsetExpression: 'vec2(0.0, 0.0)',
texCoordExpression: 'vec4(0.0, 0.0, 1.0, 1.0)',
rotateWithView: true
}); });
expect(result.builder.uniforms).to.eql([]);
expect(result.builder.attributes).to.eql([]);
expect(result.builder.varyings).to.eql([]);
expect(result.builder.colorExpression).to.eql(
'vec4(vec4(1.0, 0.0, 0.0, 1.0).rgb, vec4(1.0, 0.0, 0.0, 1.0).a * 1.0 * 1.0)');
expect(result.builder.sizeExpression).to.eql('vec2(4.0, 8.0)');
expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
expect(result.builder.rotateWithView).to.eql(true);
expect(result.attributes).to.eql([]); expect(result.attributes).to.eql([]);
expect(result.uniforms).to.eql({}); expect(result.uniforms).to.eql({});
}); });
it('parses a style with expressions', function() { it('parses a style with expressions', function() {
const result = parseSymbolStyle({ const result = parseLiteralStyle({
symbolType: 'square', symbol: {
size: ['get', 'attr1'], symbolType: 'square',
color: [ size: ['get', 'attr1'],
1.0, 0.0, 0.5, ['get', 'attr2'] color: [255, 127.5, 63.75, 0.25],
], textureCoord: [0.5, 0.5, 0.5, 1],
textureCoord: [0.5, 0.5, 0.5, 1], offset: [3, ['get', 'attr3']]
offset: [3, ['get', 'attr3']] }
}); });
expect(result.params).to.eql({
uniforms: [], expect(result.builder.uniforms).to.eql([]);
attributes: ['float a_attr1', 'float a_attr3', 'float a_attr2'], expect(result.builder.attributes).to.eql(['float a_attr1', 'float a_attr3']);
varyings: [{ expect(result.builder.varyings).to.eql([{
name: 'v_attr1', name: 'v_attr1',
type: 'float', type: 'float',
expression: 'a_attr1' expression: 'a_attr1'
}, { }]);
name: 'v_attr2', expect(result.builder.colorExpression).to.eql(
type: 'float', 'vec4(vec4(1.0, 0.5, 0.25, 0.25).rgb, vec4(1.0, 0.5, 0.25, 0.25).a * 1.0 * 1.0)'
expression: 'a_attr2' );
}], expect(result.builder.sizeExpression).to.eql('vec2(a_attr1, a_attr1)');
colorExpression: 'vec4(1.0, 0.0, 0.5, v_attr2) * vec4(1.0, 1.0, 1.0, 1.0 * 1.0)', expect(result.builder.offsetExpression).to.eql('vec2(3.0, a_attr3)');
sizeExpression: 'vec2(a_attr1, a_attr1)', expect(result.builder.texCoordExpression).to.eql('vec4(0.5, 0.5, 0.5, 1.0)');
offsetExpression: 'vec2(3.0, a_attr3)', expect(result.builder.rotateWithView).to.eql(false);
texCoordExpression: 'vec4(0.5, 0.5, 0.5, 1.0)', expect(result.attributes.length).to.eql(2);
rotateWithView: false
});
expect(result.attributes.length).to.eql(3);
expect(result.attributes[0].name).to.eql('attr1'); expect(result.attributes[0].name).to.eql('attr1');
expect(result.attributes[1].name).to.eql('attr3'); expect(result.attributes[1].name).to.eql('attr3');
expect(result.attributes[2].name).to.eql('attr2');
expect(result.uniforms).to.eql({}); expect(result.uniforms).to.eql({});
}); });
it('parses a style with a uniform (texture)', function() { it('parses a style with a uniform (texture)', function() {
const result = parseSymbolStyle({ const result = parseLiteralStyle({
symbolType: 'image', symbol: {
src: '../data/image.png', symbolType: 'image',
size: 6, src: '../data/image.png',
color: '#336699', size: 6,
opacity: 0.5 color: '#336699',
}); opacity: 0.5
expect(result.params).to.eql({ }
uniforms: ['sampler2D u_texture'],
attributes: [],
varyings: [],
colorExpression: 'vec4(0.2, 0.4, 0.6, 1.0) * vec4(1.0, 1.0, 1.0, 0.5 * 1.0) * texture2D(u_texture, v_texCoord)',
sizeExpression: 'vec2(6.0, 6.0)',
offsetExpression: 'vec2(0.0, 0.0)',
texCoordExpression: 'vec4(0.0, 0.0, 1.0, 1.0)',
rotateWithView: false
}); });
expect(result.builder.uniforms).to.eql(['sampler2D u_texture']);
expect(result.builder.attributes).to.eql([]);
expect(result.builder.varyings).to.eql([]);
expect(result.builder.colorExpression).to.eql(
'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 0.5 * 1.0) * texture2D(u_texture, v_texCoord)'
);
expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)');
expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
expect(result.builder.rotateWithView).to.eql(false);
expect(result.attributes).to.eql([]); expect(result.attributes).to.eql([]);
expect(result.uniforms).to.have.property('u_texture'); expect(result.uniforms).to.have.property('u_texture');
}); });
it('parses a style with variables', function() {
const result = parseLiteralStyle({
variables: {
lower: 100,
higher: 400
},
symbol: {
symbolType: 'square',
size: ['stretch', ['get', 'population'], ['var', 'lower'], ['var', 'higher'], 4, 8],
color: '#336699',
opacity: 0.5
}
});
expect(result.builder.uniforms).to.eql(['float u_lower', 'float u_higher']);
expect(result.builder.attributes).to.eql(['float a_population']);
expect(result.builder.varyings).to.eql([{
name: 'v_population',
type: 'float',
expression: 'a_population'
}]);
expect(result.builder.colorExpression).to.eql(
'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 0.5 * 1.0)'
);
expect(result.builder.sizeExpression).to.eql(
'vec2(((clamp(a_population, u_lower, u_higher) - u_lower) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0), ((clamp(a_population, u_lower, u_higher) - u_lower) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0))'
);
expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
expect(result.builder.rotateWithView).to.eql(false);
expect(result.attributes.length).to.eql(1);
expect(result.attributes[0].name).to.eql('population');
expect(result.uniforms).to.have.property('u_lower');
expect(result.uniforms).to.have.property('u_higher');
});
it('parses a style with a filter', function() {
const result = parseLiteralStyle({
filter: ['between', ['get', 'attr0'], 0, 10],
symbol: {
symbolType: 'square',
size: 6,
color: '#336699'
}
});
expect(result.builder.attributes).to.eql(['float a_attr0']);
expect(result.builder.varyings).to.eql([{
name: 'v_attr0',
type: 'float',
expression: 'a_attr0'
}]);
expect(result.builder.colorExpression).to.eql(
'vec4(vec4(0.2, 0.4, 0.6, 1.0).rgb, vec4(0.2, 0.4, 0.6, 1.0).a * 1.0 * 1.0)'
);
expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)');
expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
expect(result.builder.discardExpression).to.eql('(v_attr0 >= 0.0 && v_attr0 <= 10.0 ? 1.0 : 0.0) <= 0.0');
expect(result.builder.rotateWithView).to.eql(false);
expect(result.attributes.length).to.eql(1);
expect(result.attributes[0].name).to.eql('attr0');
});
it('parses a style with a color interpolation', function() {
const result = parseLiteralStyle({
symbol: {
symbolType: 'square',
size: 6,
color: ['interpolate', ['var', 'ratio'], [255, 255, 0], 'red']
}
});
expect(result.builder.attributes).to.eql([]);
expect(result.builder.varyings).to.eql([]);
expect(result.builder.colorExpression).to.eql(
'vec4(mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), u_ratio).rgb, mix(vec4(1.0, 1.0, 0.0, 1.0), vec4(1.0, 0.0, 0.0, 1.0), u_ratio).a * 1.0 * 1.0)'
);
expect(result.builder.sizeExpression).to.eql('vec2(6.0, 6.0)');
expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)');
expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)');
expect(result.builder.rotateWithView).to.eql(false);
expect(result.attributes).to.eql([]);
expect(result.uniforms).to.have.property('u_ratio');
});
}); });
}); });