From 35af80b433f3566b4f6e3b01e3da7420b037c00a Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Thu, 24 Oct 2019 09:26:13 +0200 Subject: [PATCH] Style / add an ol/style/expressions module This module will contain the utilities and operator declarations for literal style expressions, boh for type checking and parsing --- src/ol/style/LiteralStyle.js | 44 +-------- src/ol/style/expressions.js | 132 +++++++++++++++++++++++++ test/spec/ol/style/expressions.test.js | 41 ++++++++ 3 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 src/ol/style/expressions.js create mode 100644 test/spec/ol/style/expressions.test.js diff --git a/src/ol/style/LiteralStyle.js b/src/ol/style/LiteralStyle.js index c0ee1e085a..f4fd388464 100644 --- a/src/ol/style/LiteralStyle.js +++ b/src/ol/style/LiteralStyle.js @@ -5,49 +5,7 @@ */ /** - * 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 {import("./expressions.js").ExpressionValue} ExpressionValue */ /** diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js new file mode 100644 index 0000000000..727c2d03c0 --- /dev/null +++ b/src/ol/style/expressions.js @@ -0,0 +1,132 @@ +/** + * Operators and utilities used for style expressions + * @module ol/style/expressions + */ + +import {isStringColor} from '../color.js'; + +/** + * 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. + * * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all + * provided `matchX` values, returning the output associated with the first valid match. If no match is found, + * returns the `fallback` value. + * `input` and `matchX` values must all be of the same type, and can be `number` or `string`. `outputX` and + * `fallback` values must be of the same type, and can be any kind. + * + * 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|boolean} ExpressionValue + */ + +/** + * Possible inferred types from a given value or expression. + * Note: these are binary flags. + * @enum {number} + */ +export const ValueTypes = { + NUMBER: 0b00001, + STRING: 0b00010, + COLOR: 0b00100, + BOOLEAN: 0b01000, + NUMBER_ARRAY: 0b10000, + ANY: 0b11111 +}; + +/** + * Returns the possible types for a given value (each type being a binary flag) + * To test a value use e.g. `getValueType(v) & ValueTypes.BOOLEAN` + * @param {ExpressionValue} value Value + * @returns {ValueTypes|number} Type or types inferred from the value + */ +export function getValueType(value) { + if (typeof value === 'number') { + return ValueTypes.NUMBER; + } + if (typeof value === 'boolean') { + return ValueTypes.BOOLEAN; + } + if (typeof value === 'string') { + if (isStringColor(value)) { + return ValueTypes.COLOR | ValueTypes.STRING; + } + return ValueTypes.STRING; + } + if (!Array.isArray(value)) { + throw new Error(`Unhandled value type: ${JSON.stringify(value)}`); + } + const onlyNumbers = value.every(function(v) { + return typeof v === 'number'; + }); + if (onlyNumbers) { + if (value.length === 3 || value.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)}`); + } + 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)}`); + } +} diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js new file mode 100644 index 0000000000..6ec2f45cb5 --- /dev/null +++ b/test/spec/ol/style/expressions.test.js @@ -0,0 +1,41 @@ +import {getValueType, ValueTypes} from '../../../../src/ol/style/expressions.js'; + + +describe('ol.style.expressions', function() { + + describe('getValueType', function() { + + it('correctly analyzes a literal value', function() { + expect(getValueType(1234)).to.eql(ValueTypes.NUMBER); + expect(getValueType([1, 2, 3, 4])).to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY); + expect(getValueType([1, 2, 3])).to.eql(ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY); + expect(getValueType([1, 2])).to.eql(ValueTypes.NUMBER_ARRAY); + expect(getValueType([1, 2, 3, 4, 5])).to.eql(ValueTypes.NUMBER_ARRAY); + expect(getValueType('yellow')).to.eql(ValueTypes.COLOR | ValueTypes.STRING); + expect(getValueType('#113366')).to.eql(ValueTypes.COLOR | ValueTypes.STRING); + expect(getValueType('rgba(252,171,48,0.62)')).to.eql(ValueTypes.COLOR | ValueTypes.STRING); + expect(getValueType('abcd')).to.eql(ValueTypes.STRING); + expect(getValueType(true)).to.eql(ValueTypes.BOOLEAN); + }); + + it('throws on an unsupported type (object)', function(done) { + try { + getValueType(new Object()); + } catch (e) { + done(); + } + done(true); + }); + + it('throws on an unsupported type (mixed array)', function(done) { + try { + getValueType([1, true, 'aa']); + } catch (e) { + done(); + } + done(true); + }); + + }); + +});