diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 37bb7d8f7a..a44553b4a9 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -24,14 +24,22 @@ import {assign} from '../obj.js'; * * `['+', 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` + * * Transform operators: + * * `['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 of any kind. + * * `['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]` returns a value by interpolating between + * pairs of inputs and outputs; `interpolation` can either be `['linear']` or `['exponential', base]` where `base` is + * the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value + * of 1 is equivalent to `['linear']`. + * `input` and `stopX` values must all be of type `number`. `outputX` values can be `number` or `color` values. + * Note: `input` will be clamped between `stop1` and `stopN`, meaning that all output values will be comprised + * between `output1` and `outputN`. * * * Logical operators: * * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise. @@ -42,11 +50,6 @@ import {assign} from '../obj.js'; * * `['!', 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: @@ -263,6 +266,11 @@ function assertArgsCount(args, count) { throw new Error(`Exactly ${count} arguments were expected, got ${args.length} instead`); } } +function assertArgsMinCount(args, count) { + if (args.length < count) { + throw new Error(`At least ${count} arguments were expected, got ${args.length} instead`); + } +} function assertArgsEven(args) { if (args.length % 2 !== 0) { throw new Error(`An even amount of arguments was expected, got ${args} instead`); @@ -492,17 +500,43 @@ Operators['between'] = { }; Operators['interpolate'] = { getReturnType: function(args) { - return ValueTypes.COLOR; + let type = ValueTypes.COLOR | ValueTypes.NUMBER; + for (let i = 3; i < args.length; i += 2) { + type = type & getValueType(args[i]); + } + return type; }, - toGlsl: function(context, args) { - assertArgsCount(args, 3); - assertNumber(args[0]); - assertColor(args[1]); - assertColor(args[2]); - const newContext = assign({}, context); - const start = expressionToGlsl(newContext, args[1], ValueTypes.COLOR); - const end = expressionToGlsl(newContext, args[2], ValueTypes.COLOR); - return `mix(${start}, ${end}, ${expressionToGlsl(context, args[0])})`; + toGlsl: function(context, args, opt_typeHint) { + assertArgsEven(args); + assertArgsMinCount(args, 6); + + // validate interpolation type + const type = args[0]; + let interpolation; + switch (type[0]) { + case 'linear': interpolation = 1; break; + case 'exponential': interpolation = type[1]; break; + default: interpolation = null; + } + if (!interpolation) { + throw new Error(`Invalid interpolation type for "interpolate" operator, received: ${JSON.stringify(type)}`); + } + + // compute input/output types + const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY; + const outputType = Operators['interpolate'].getReturnType(args) & typeHint; + assertUniqueInferredType(args, outputType); + + const input = expressionToGlsl(context, args[1]); + let result = null; + for (let i = 2; i < args.length - 2; i += 2) { + const stop1 = expressionToGlsl(context, args[i]); + const output1 = expressionToGlsl(context, args[i + 1], outputType); + const stop2 = expressionToGlsl(context, args[i + 2]); + const output2 = expressionToGlsl(context, args[i + 3], outputType); + result = `mix(${result || output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${numberToGlsl(interpolation)}))`; + } + return result; } }; Operators['match'] = { @@ -516,6 +550,7 @@ Operators['match'] = { }, toGlsl: function(context, args, opt_typeHint) { assertArgsEven(args); + assertArgsMinCount(args, 4); // compute input/output types const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY; diff --git a/test/spec/ol/style/expressions.test.js b/test/spec/ol/style/expressions.test.js index 23222540ff..4346000695 100644 --- a/test/spec/ol/style/expressions.test.js +++ b/test/spec/ol/style/expressions.test.js @@ -169,8 +169,6 @@ describe('ol.style.expressions', function() { expect(expressionToGlsl(context, ['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4)'); expect(expressionToGlsl(context, ['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0)'); expect(expressionToGlsl(context, ['!', ['get', 'attr4']])).to.eql('(!a_attr4)'); - expect(expressionToGlsl(context, ['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)'); }); it('correctly adapts output for fragment shaders', function() { @@ -211,11 +209,6 @@ describe('ol.style.expressions', function() { } catch (e) { thrown = true; } - try { - expressionToGlsl(context, ['interpolate', ['get', 'attr4'], 1, '#3344FF']); - } catch (e) { - thrown = true; - } expect(thrown).to.be(true); }); @@ -331,6 +324,13 @@ describe('ol.style.expressions', function() { thrown = true; } expect(thrown).to.be(true); + + try { + expressionToGlsl(context, ['match', ['get', 'attr'], 0]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); }); it('correctly parses the expression (colors)', function() { @@ -358,6 +358,122 @@ describe('ol.style.expressions', function() { }); + describe('interpolate operator', function() { + let context; + + beforeEach(function() { + context = { + variables: [], + attributes: [], + stringLiteralsMap: {} + }; + }); + + it('correctly guesses the output type', function() { + expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 0, 'red', 100, 'yellow'])) + .to.eql(ValueTypes.COLOR); + expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, [0, 0, 0, 4]])) + .to.eql(ValueTypes.COLOR); + expect(getValueType(['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10])) + .to.eql(ValueTypes.NUMBER); + }); + + it('throws if no single output type could be inferred', function() { + let thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10], ValueTypes.COLOR); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + try { + expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, 222]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 0, [1, 2, 3], 1, [0, 0, 0, 4]], ValueTypes.NUMBER); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('throws if invalid argument count', function() { + let thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 10, 5000]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('throws if an invalid interpolation type is given', function() { + let thrown = false; + try { + expressionToGlsl(context, ['interpolate', 'linear', ['get', 'attr'], 1000, 0, 2000, 1]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['exponential'], ['get', 'attr'], 1000, -10, 2000, 1]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + + thrown = false; + try { + expressionToGlsl(context, ['interpolate', ['not_a_type'], ['get', 'attr'], 1000, -10, 2000, 1]); + } catch (e) { + thrown = true; + } + expect(thrown).to.be(true); + }); + + it('correctly parses the expression (colors, linear)', function() { + expect(expressionToGlsl(context, + ['interpolate', ['linear'], ['get', 'attr'], 1000, [255, 0, 0], 2000, [0, 255, 0]] + )).to.eql( + 'mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0))'); + expect(expressionToGlsl(context, + ['interpolate', ['linear'], ['get', 'attr'], 1000, [255, 0, 0], 2000, [0, 255, 0], 5000, [0, 0, 255]] + )).to.eql( + 'mix(mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 1.0, 0.0, 1.0), pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), vec4(0.0, 0.0, 1.0, 1.0), pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))'); + }); + + it('correctly parses the expression (number, linear)', function() { + expect(expressionToGlsl(context, + ['interpolate', ['linear'], ['get', 'attr'], 1000, -10, 2000, 0, 5000, 10] + )).to.eql( + 'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 1.0)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 1.0))'); + }); + + it('correctly parses the expression (number, exponential)', function() { + expect(expressionToGlsl(context, + ['interpolate', ['exponential', 0.5], ['get', 'attr'], 1000, -10, 2000, 0, 5000, 10] + )).to.eql( + 'mix(mix(-10.0, 0.0, pow(clamp((a_attr - 1000.0) / (2000.0 - 1000.0), 0.0, 1.0), 0.5)), 10.0, pow(clamp((a_attr - 2000.0) / (5000.0 - 2000.0), 0.0, 1.0), 0.5))'); + }); + + }); + describe('complex expressions', function() { let context;