Expressions / rework the interpolate operator
This operator is now able to map numbers to output ranges as well as colors, making the stretch operator unnecessary. Also allows giving multiple stops, like in Mapbox style spec.
This commit is contained in:
@@ -24,14 +24,22 @@ import {assign} from '../obj.js';
|
|||||||
* * `['+', value1, value1]` adds `value1` and `value2`
|
* * `['+', value1, value1]` adds `value1` and `value2`
|
||||||
* * `['-', value1, value1]` subtracts `value2` from `value1`
|
* * `['-', value1, value1]` subtracts `value2` from `value1`
|
||||||
* * `['clamp', value, low, high]` clamps `value` between `low` and `high`
|
* * `['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)
|
* * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo)
|
||||||
* * `['pow', value1, value1]` returns the value of `value1` raised to the `value2` power
|
* * `['pow', value1, value1]` returns the value of `value1` raised to the `value2` power
|
||||||
*
|
*
|
||||||
* * Color operators:
|
* * Transform operators:
|
||||||
* * `['interpolate', ratio, color1, color2]` returns a color through interpolation between `color1` and
|
* * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all
|
||||||
* `color2` with the given `ratio`
|
* 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:
|
* * Logical operators:
|
||||||
* * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise.
|
* * `['<', 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.
|
* * `['!', 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`
|
* * `['between', value1, value2, value3]` returns `1` if `value1` is contained between `value2` and `value3`
|
||||||
* (inclusively), or `0` otherwise.
|
* (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.
|
* Values can either be literals or another operator, as they will be evaluated recursively.
|
||||||
* Literal values can be of the following types:
|
* 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`);
|
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) {
|
function assertArgsEven(args) {
|
||||||
if (args.length % 2 !== 0) {
|
if (args.length % 2 !== 0) {
|
||||||
throw new Error(`An even amount of arguments was expected, got ${args} instead`);
|
throw new Error(`An even amount of arguments was expected, got ${args} instead`);
|
||||||
@@ -492,17 +500,43 @@ Operators['between'] = {
|
|||||||
};
|
};
|
||||||
Operators['interpolate'] = {
|
Operators['interpolate'] = {
|
||||||
getReturnType: function(args) {
|
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) {
|
toGlsl: function(context, args, opt_typeHint) {
|
||||||
assertArgsCount(args, 3);
|
assertArgsEven(args);
|
||||||
assertNumber(args[0]);
|
assertArgsMinCount(args, 6);
|
||||||
assertColor(args[1]);
|
|
||||||
assertColor(args[2]);
|
// validate interpolation type
|
||||||
const newContext = assign({}, context);
|
const type = args[0];
|
||||||
const start = expressionToGlsl(newContext, args[1], ValueTypes.COLOR);
|
let interpolation;
|
||||||
const end = expressionToGlsl(newContext, args[2], ValueTypes.COLOR);
|
switch (type[0]) {
|
||||||
return `mix(${start}, ${end}, ${expressionToGlsl(context, args[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'] = {
|
Operators['match'] = {
|
||||||
@@ -516,6 +550,7 @@ Operators['match'] = {
|
|||||||
},
|
},
|
||||||
toGlsl: function(context, args, opt_typeHint) {
|
toGlsl: function(context, args, opt_typeHint) {
|
||||||
assertArgsEven(args);
|
assertArgsEven(args);
|
||||||
|
assertArgsMinCount(args, 4);
|
||||||
|
|
||||||
// compute input/output types
|
// compute input/output types
|
||||||
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
|
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
|
||||||
|
|||||||
@@ -169,8 +169,6 @@ describe('ol.style.expressions', function() {
|
|||||||
expect(expressionToGlsl(context, ['==', 10, ['get', 'attr4']])).to.eql('(10.0 == a_attr4)');
|
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, ['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, ['!', ['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() {
|
it('correctly adapts output for fragment shaders', function() {
|
||||||
@@ -211,11 +209,6 @@ describe('ol.style.expressions', function() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
thrown = true;
|
thrown = true;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
expressionToGlsl(context, ['interpolate', ['get', 'attr4'], 1, '#3344FF']);
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
}
|
|
||||||
expect(thrown).to.be(true);
|
expect(thrown).to.be(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,6 +324,13 @@ describe('ol.style.expressions', function() {
|
|||||||
thrown = true;
|
thrown = true;
|
||||||
}
|
}
|
||||||
expect(thrown).to.be(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() {
|
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() {
|
describe('complex expressions', function() {
|
||||||
let context;
|
let context;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user