diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index 05406d65fc..5f2cdf3e3a 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -120,6 +120,93 @@ 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) { + const v = 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)) { + switch (value[0]) { + case 'get': + case 'var': + checkString(v[1]); + break; + case 'time': + break; + case '*': + case '+': + checkNumber(v[1]); + checkNumber(v[2]); + break; + case 'clamp': + checkNumber(v[1]); + checkNumber(v[2]); + checkNumber(v[3]); + break; + case 'stretch': + checkNumber(v[1]); + checkNumber(v[2]); + checkNumber(v[3]); + checkNumber(v[4]); + checkNumber(v[5]); + break; + case '>': + case '>=': + case '<': + case '<=': + case '==': + checkNumber(v[1]); + checkNumber(v[2]); + break; + case '!': + checkNumber(v[1]); + break; + case 'between': + checkNumber(v[1]); + checkNumber(v[2]); + checkNumber(v[3]); + break; + + case 'interpolate': + checkNumber(v[1]); + checkColor(v[2]); + checkColor(v[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)' @@ -139,6 +226,8 @@ export function isValueTypeColor(value) { * @returns {string} Assignment string. */ export function parse(value, attributes, attributePrefix, variables) { + check(value); + const v = value; function p(value) { return parse(value, attributes, attributePrefix, variables); diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index 944b72346a..f37bd646b9 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -1,4 +1,5 @@ import { + check, formatArray, formatColor, formatNumber, @@ -248,6 +249,88 @@ void main(void) { }); }); + describe('check', function() { + + it('does not throw on valid expressions', function(done) { + check(1); + check(['get', 'myAttr']); + check(['var', 'myValue']); + check(['time']); + check(['+', ['*', ['get', 'size'], 0.001], 12]); + check(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20]); + check(['stretch', ['get', 'size'], 10, 100, 4, 8]); + 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']]); + 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; + } + 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() { let attributes, prefix, variables, parseFn;