From 72903d6454d44b3132c6e8b96d408807830f007d Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 24 Oct 2019 11:31:40 +0200 Subject: [PATCH] Expressions / added operator declarations & type checking logic --- src/ol/style/expressions.js | 300 ++++++++++++++++++++++--- test/spec/ol/style/expressions.test.js | 22 ++ 2 files changed, 294 insertions(+), 28 deletions(-) diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 727c2d03c0..1dbeeb45ae 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -92,41 +92,285 @@ export function getValueType(value) { if (!Array.isArray(value)) { throw new Error(`Unhandled value type: ${JSON.stringify(value)}`); } - const onlyNumbers = value.every(function(v) { + const valueArr = /** @type {Array<*>} */(value); + const onlyNumbers = valueArr.every(function(v) { return typeof v === 'number'; }); if (onlyNumbers) { - if (value.length === 3 || value.length === 4) { + if (valueArr.length === 3 || valueArr.length === 4) { return ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY; } return ValueTypes.NUMBER_ARRAY; } - if (typeof value[0] !== 'string') { - throw new Error(`Expected an expression operator but received: ${JSON.stringify(value)}`); + if (typeof valueArr[0] !== 'string') { + throw new Error(`Expected an expression operator but received: ${JSON.stringify(valueArr)}`); } - 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: - throw new Error(`Unrecognized expression operator: ${JSON.stringify(value)}`); + const operator = Operators[valueArr[0]]; + if (operator === undefined) { + throw new Error(`Unrecognized expression operator: ${JSON.stringify(valueArr)}`); + } + return operator.getReturnType(valueArr.slice(1)); +} + +function assertNumber(value) { + if (!(getValueType(value) & ValueTypes.NUMBER)) { + throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`); } } +function assertColor(value) { + if (!(getValueType(value) & ValueTypes.COLOR)) { + throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`); + } +} +function assertString(value) { + if (!(getValueType(value) & ValueTypes.STRING)) { + throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`); + } +} +function assertBoolean(value) { + if (!(getValueType(value) & ValueTypes.BOOLEAN)) { + throw new Error(`A boolean value was expected, got ${JSON.stringify(value)} instead`); + } +} +function assertArgsCount(args, count) { + if (args.length !== count) { + throw new Error(`Exactly ${count} arguments were expected, got ${args.length} instead`); + } +} + +/** + * Context available during the parsing of an expression. + * @typedef {Object} ParsingContext + * @property {boolean} inFragmentShader If false, means the expression output should be made for a vertex shader + */ + +/** + * An operator declaration must contain two methods: `getReturnType` which returns a type based on + * the operator arguments, and `toGlsl` which returns a GLSL-compatible string. + * Note: both methods can process arguments recursively. + * @typedef {Object} Operator + * @property {function(...ExpressionValue): ValueTypes|number} getReturnType Returns one or several types + * @property {function(ParsingContext, ...ExpressionValue): string} toGlsl Returns a GLSL-compatible string + */ + +/** + * Operator declarations + * @type {Object} + */ +export const Operators = { + 'get': { + getReturnType: function(...args) { + return ValueTypes.ANY; + }, + toGlsl: function(context, ...args) { + const prefix = context.inFragmentShader ? 'v_' : 'a_'; + assertArgsCount(args, 1); + assertString(args[0]); + return prefix + args[0]; + } + }, + 'var': { + getReturnType: function(...args) { + return ValueTypes.ANY; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 1); + assertString(args[0]); + return `u_${args[0]}`; + } + }, + 'time': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 0); + return 'u_time'; + } + }, + '*': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} * ${args[1]})`; + } + }, + '/': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} / ${args[1]})`; + } + }, + '+': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} + ${args[1]})`; + } + }, + '-': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} - ${args[1]})`; + } + }, + 'clamp': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 3); + assertNumber(args[0]); + assertNumber(args[1]); + assertNumber(args[2]); + return `clamp(${args[0]}, ${args[1]}, ${args[2]})`; + } + }, + 'stretch': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 5); + assertNumber(args[0]); + assertNumber(args[1]); + assertNumber(args[2]); + assertNumber(args[3]); + assertNumber(args[4]); + const low1 = args[1]; + const high1 = args[2]; + const low2 = args[3]; + const high2 = args[4]; + return `((clamp(${args[0]}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`; + } + }, + 'mod': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `mod(${args[0]}, ${args[1]})`; + } + }, + 'pow': { + getReturnType: function(...args) { + return ValueTypes.NUMBER; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `pow(${args[0]}, ${args[1]})`; + } + }, + '>': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} > ${args[1]})`; + } + }, + '>=': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} >= ${args[1]})`; + } + }, + '<': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} < ${args[1]})`; + } + }, + '<=': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} <= ${args[1]})`; + } + }, + '==': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + assertNumber(args[1]); + return `(${args[0]} == ${args[1]})`; + } + }, + '!': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 1); + assertBoolean(args[0]); + return `(!${args[0]})`; + } + }, + 'between': { + getReturnType: function(...args) { + return ValueTypes.BOOLEAN; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 3); + assertNumber(args[0]); + assertNumber(args[1]); + assertNumber(args[2]); + return `(${args[0]} >= ${args[1]} && ${args[0]} <= ${args[2]})`; + } + }, + 'interpolate': { + getReturnType: function(...args) { + return ValueTypes.COLOR; + }, + toGlsl: function(context, ...args) { + assertArgsCount(args, 3); + assertNumber(args[0]); + assertColor(args[1]); + assertColor(args[2]); + return `mix(${args[1]}, ${args[2]}, ${args[0]})`; + } + } +}; diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index 6ec2f45cb5..9f6f864367 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -36,6 +36,28 @@ describe('ol.style.expressions', function() { done(true); }); + it('correctly analyzes operator return types', function() { + expect(getValueType(['get', 'myAttr'])).to.eql(ValueTypes.ANY); + expect(getValueType(['var', 'myValue'])).to.eql(ValueTypes.ANY); + expect(getValueType(['time'])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['+', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['-', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['/', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['*', ['get', 'size'], 12])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['stretch', ['get', 'size'], 10, 100, 4, 8])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['pow', 10, 2])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['mod', ['time'], 10])).to.eql(ValueTypes.NUMBER); + expect(getValueType(['>', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['>=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['<', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['<=', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['==', 10, ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['!', ['get', 'attr4']])).to.eql(ValueTypes.BOOLEAN); + expect(getValueType(['interpolate', ['get', 'attr4'], [255, 255, 255, 1], 'transparent'])).to.eql(ValueTypes.COLOR); + }); + }); });