import {
buildExpression,
newEvaluationContext,
} from '../../../../src/ol/expr/cpu.js';
import {
BooleanType,
ColorType,
NumberArrayType,
NumberType,
StringType,
newParsingContext,
} from '../../../../src/ol/expr/expression.js';
import expect from '../../expect.js';
describe('ol/expr/cpu.js', () => {
describe('buildExpression()', () => {
* @typedef {Object} Case
* @property {string} name The case name.
* @property {import('../../../../src/ol/expr/expression.js').EncodedExpression} expression The encoded expression.
* @property {import('../../../../src/ol/expr/cpu.js').EvaluationContext} [context] The evaluation context.
* @property {number} type The expression type.
* @property {import('../../../../src/ol/expr/expression.js').LiteralValue} expected The expected value.
* @property {number} [tolerance] Optional tolerance for numeric comparisons.
*/
* @type {Array<Case>}
*/
const cases = [
{
name: 'get',
context: {
properties: {
property: 42,
},
},
expression: ['get', 'property'],
type: NumberType,
expected: 42,
},
{
name: 'get (nested)',
context: {
properties: {
deeply: {nested: {property: 42}},
},
},
expression: ['get', 'deeply', 'nested', 'property'],
type: NumberType,
expected: 42,
},
{
name: 'get number (excess key)',
context: {
properties: {
property: 42,
},
},
expression: ['get', 'property', 'nothing_here'],
type: NumberType,
expected: undefined,
},
{
name: 'get array item',
context: {
properties: {
values: [17, 42],
},
},
expression: ['get', 'values', 1],
type: NumberType,
expected: 42,
},
{
name: 'get array',
context: {
properties: {
values: [17, 42],
},
},
expression: ['get', 'values'],
type: NumberArrayType,
expected: [17, 42],
},
{
name: 'boolean literal (true)',
type: BooleanType,
expression: true,
expected: true,
},
{
name: 'boolean literal (false)',
type: BooleanType,
expression: false,
expected: false,
},
{
name: 'number assertion',
type: NumberType,
expression: ['number', 'not', 'a', 'number', 42, false],
expected: 42,
},
{
name: 'string assertion',
type: StringType,
expression: ['string', 42, 'chicken', false],
expected: 'chicken',
},
{
name: 'id (number)',
type: NumberType,
expression: ['id'],
context: {
featureId: 42,
},
expected: 42,
},
{
name: 'id (string)',
type: StringType,
expression: ['id'],
context: {
featureId: 'forty-two',
},
expected: 'forty-two',
},
{
name: 'geometry-type',
type: StringType,
expression: ['geometry-type'],
context: {
geometryType: 'LineString',
},
expected: 'LineString',
},
{
name: 'geometry-type (empty)',
type: StringType,
expression: ['geometry-type'],
context: {
geometryType: '',
},
expected: '',
},
{
name: 'resolution',
type: NumberType,
expression: ['resolution'],
context: {
resolution: 10,
},
expected: 10,
},
{
name: 'resolution (comparison)',
type: BooleanType,
expression: ['>', ['resolution'], 10],
context: {
resolution: 11,
},
expected: true,
},
{
name: 'concat (2 arguments)',
type: StringType,
expression: ['concat', ['get', 'val'], ' '],
context: {
properties: {val: 'test'},
},
expected: 'test ',
},
{
name: 'concat (3 arguments)',
type: StringType,
expression: ['concat', ['get', 'val'], ' ', ['get', 'val2']],
context: {
properties: {val: 'test', val2: 'another'},
},
expected: 'test another',
},
{
name: 'concat (with id)',
type: StringType,
expression: ['concat', 'Feature ', ['id']],
context: {
featureId: 'foo',
},
expected: 'Feature foo',
},
{
name: 'concat (with string and number)',
type: StringType,
expression: ['concat', 'number ', 1],
expected: 'number 1',
},
{
name: 'coalesce (2 arguments, first has a value)',
type: StringType,
expression: ['coalesce', ['get', 'val'], 'default'],
context: {
properties: {val: 'test'},
},
expected: 'test',
},
{
name: 'coalesce (2 arguments, first has no value)',
type: StringType,
expression: ['coalesce', ['get', 'val'], 'default'],
context: {
properties: {},
},
expected: 'default',
},
{
name: 'coalesce (several arguments, first few have no value)',
type: StringType,
expression: [
'coalesce',
['get', 'val'],
['get', 'beer'],
['get', 'present'],
'last resort',
],
context: {
properties: {present: 'hello world'},
},
expected: 'hello world',
},
{
name: 'any (true)',
type: BooleanType,
expression: ['any', ['get', 'nope'], ['get', 'yep'], ['get', 'nope']],
context: {
properties: {nope: false, yep: true},
},
expected: true,
},
{
name: 'any (false)',
type: BooleanType,
expression: ['any', ['get', 'nope'], false, ['!', ['get', 'yep']]],
context: {
properties: {nope: false, yep: true},
},
expected: false,
},
{
name: 'all (true)',
type: BooleanType,
expression: ['all', ['get', 'yep'], true, ['!', ['get', 'nope']]],
context: {
properties: {yep: true, nope: false},
},
expected: true,
},
{
name: 'all (false)',
type: BooleanType,
expression: ['all', ['!', ['get', 'nope']], ['get', 'yep'], false],
context: {
properties: {nope: false, yep: true},
},
expected: false,
},
{
name: 'not (true)',
type: BooleanType,
expression: ['!', ['get', 'nope']],
context: {
properties: {nope: false, yep: true},
},
expected: true,
},
{
name: 'not (false)',
type: BooleanType,
expression: ['!', ['get', 'yep']],
context: {
properties: {nope: false, yep: true},
},
expected: false,
},
{
name: 'equal comparison (true)',
type: BooleanType,
expression: ['==', ['get', 'number'], 42],
context: {
properties: {number: 42},
},
expected: true,
},
{
name: 'equal comparison (false)',
type: BooleanType,
expression: ['==', ['get', 'number'], 1],
context: {
properties: {number: 42},
},
expected: false,
},
{
name: 'greater than comparison (true)',
type: BooleanType,
expression: ['>', ['get', 'number'], 40],
context: {
properties: {number: 42},
},
expected: true,
},
{
name: 'greater than comparison (false)',
type: BooleanType,
expression: ['>', ['get', 'number'], 44],
context: {
properties: {number: 42},
},
expected: false,
},
{
name: 'greater than or equal comparison (true)',
type: BooleanType,
expression: ['>=', ['get', 'number'], 42],
context: {
properties: {number: 42},
},
expected: true,
},
{
name: 'greater than or equal comparison (false)',
type: BooleanType,
expression: ['>=', ['get', 'number'], 43],
context: {
properties: {number: 42},
},
expected: false,
},
{
name: 'less than comparison (true)',
type: BooleanType,
expression: ['<', ['get', 'number'], 44],
context: {
properties: {number: 42},
},
expected: true,
},
{
name: 'less than comparison (false)',
type: BooleanType,
expression: ['<', ['get', 'number'], 1],
context: {
properties: {number: 42},
},
expected: false,
},
{
name: 'less than or equal comparison (true)',
type: BooleanType,
expression: ['<=', ['get', 'number'], 42],
context: {
properties: {number: 42},
},
expected: true,
},
{
name: 'less than or equal comparison (false)',
type: BooleanType,
expression: ['<=', ['get', 'number'], 41],
context: {
properties: {number: 42},
},
expected: false,
},
{
name: 'addition',
type: NumberType,
expression: ['+', ['get', 'number'], 1],
context: {
properties: {number: 42},
},
expected: 43,
},
{
name: 'addition (many values)',
type: NumberType,
expression: ['+', 1, 2, 3, 4],
expected: 1 + 2 + 3 + 4,
},
{
name: 'subtraction',
type: NumberType,
expression: ['-', ['get', 'number'], 1],
context: {
properties: {number: 42},
},
expected: 41,
},
{
name: 'subtraction',
type: NumberType,
expression: ['-', ['get', 'number'], 1],
context: {
properties: {number: 42},
},
expected: 41,
},
{
name: 'multiplication',
type: NumberType,
expression: ['*', ['get', 'number'], 2],
context: {
properties: {number: 42},
},
expected: 84,
},
{
name: 'multiplication (many values)',
type: NumberType,
expression: ['*', 2, 4, 6, 8],
expected: 2 * 4 * 6 * 8,
},
{
name: 'division',
type: NumberType,
expression: ['/', ['get', 'number'], 2],
context: {
properties: {number: 42},
},
expected: 21,
},
{
name: 'clamp (min)',
type: NumberType,
expression: ['clamp', -10, 0, 50],
expected: 0,
},
{
name: 'clamp (max)',
type: NumberType,
expression: ['clamp', 100, 0, 50],
expected: 50,
},
{
name: 'clamp (mid)',
type: NumberType,
expression: ['clamp', 25, 0, 50],
expected: 25,
},
{
name: 'clamp (mid)',
type: NumberType,
expression: ['clamp', 25, 0, 50],
expected: 25,
},
{
name: 'mod',
type: NumberType,
expression: ['%', ['get', 'number'], 10],
context: {
properties: {number: 42},
},
expected: 2,
},
{
name: 'pow',
type: NumberType,
expression: ['^', ['get', 'number'], 2],
context: {
properties: {number: 42},
},
expected: 1764,
},
{
name: 'abs',
type: NumberType,
expression: ['abs', ['get', 'number']],
context: {
properties: {number: -42},
},
expected: 42,
},
{
name: 'floor',
type: NumberType,
expression: ['floor', ['get', 'number']],
context: {
properties: {number: 42.9},
},
expected: 42,
},
{
name: 'ceil',
type: NumberType,
expression: ['ceil', ['get', 'number']],
context: {
properties: {number: 42.1},
},
expected: 43,
},
{
name: 'round',
type: NumberType,
expression: ['round', ['get', 'number']],
context: {
properties: {number: 42.5},
},
expected: 43,
},
{
name: 'sin',
type: NumberType,
expression: ['sin', ['get', 'angle']],
context: {
properties: {angle: Math.PI / 2},
},
expected: 1,
},
{
name: 'cos',
type: NumberType,
expression: ['cos', ['get', 'angle']],
context: {
properties: {angle: Math.PI},
},
expected: -1,
},
{
name: 'atan (1)',
type: NumberType,
expression: ['atan', 1],
expected: Math.atan(1),
},
{
name: 'atan (2)',
type: NumberType,
expression: ['atan', 1, 2],
expected: Math.atan2(1, 2),
},
{
name: 'sqrt',
type: NumberType,
expression: ['sqrt', ['get', 'number']],
context: {
properties: {number: 42},
},
expected: Math.sqrt(42),
},
{
name: 'case (first condition)',
type: StringType,
expression: [
'case',
['<', ['get', 'value'], 42],
'small',
['<', ['get', 'value'], 100],
'big',
'bigger',
],
context: {
properties: {value: 40},
},
expected: 'small',
},
{
name: 'case (second condition)',
type: StringType,
expression: [
'case',
['<', ['get', 'value'], 42],
'small',
['<', ['get', 'value'], 100],
'big',
'bigger',
],
context: {
properties: {value: 50},
},
expected: 'big',
},
{
name: 'case (fallback)',
type: StringType,
expression: [
'case',
['<', ['get', 'value'], 42],
'small',
['<', ['get', 'value'], 100],
'big',
'biggest',
],
context: {
properties: {value: 200},
},
expected: 'biggest',
},
{
name: 'match (string match)',
type: StringType,
expression: ['match', ['get', 'string'], 'foo', 'got foo', 'got other'],
context: {
properties: {string: 'foo'},
},
expected: 'got foo',
},
{
name: 'match (string fallback)',
type: StringType,
expression: ['match', ['get', 'string'], 'foo', 'got foo', 'got other'],
context: {
properties: {string: 'bar'},
},
expected: 'got other',
},
{
name: 'match (number match)',
type: StringType,
expression: ['match', ['get', 'number'], 42, 'got 42', 'got other'],
context: {
properties: {number: 42},
},
expected: 'got 42',
},
{
name: 'match (number fallback)',
type: StringType,
expression: ['match', ['get', 'number'], 42, 'got 42', 'got other'],
context: {
properties: {number: 43},
},
expected: 'got other',
},
{
name: 'match (input equals fallback value)',
type: NumberType,
expression: ['match', ['get', 'number'], 0, 1, 42],
context: {
properties: {number: 42},
},
expected: 42,
},
{
name: 'interpolate (linear number)',
type: NumberType,
expression: [
'interpolate',
['linear'],
['get', 'number'],
0,
0,
1,
100,
],
context: {
properties: {number: 0.5},
},
expected: 50,
},
{
name: 'interpolate (exponential base 2 number)',
type: NumberType,
expression: ['interpolate', ['exponential', 2], 0.5, 0, 0, 1, 100],
expected: 41.42135623730952,
tolerance: 1e-6,
},
{
name: 'interpolate (linear no delta)',
type: NumberType,
expression: ['interpolate', ['linear'], 42, 42, 1, 42, 2],
expected: 1,
},
{
name: 'to-string (string)',
type: StringType,
expression: ['to-string', 'foo'],
expected: 'foo',
},
{
name: 'to-string (number)',
type: StringType,
expression: ['to-string', 42.9],
expected: '42.9',
},
{
name: 'to-string (boolean)',
type: StringType,
expression: ['to-string', 1 < 2],
expected: 'true',
},
{
name: 'to-string (array)',
type: StringType,
expression: ['to-string', ['get', 'fill']],
context: {
properties: {fill: [0, 255, 0]},
},
expected: '0,255,0',
},
{
name: 'in (true)',
type: BooleanType,
expression: ['in', 3, [1, 2, 3]],
expected: true,
},
{
name: 'in (false)',
type: BooleanType,
expression: ['in', 'yellow', ['literal', ['red', 'green', 'blue']]],
expected: false,
},
{
name: 'between (true)',
type: BooleanType,
expression: ['between', 3, 3, 5],
expected: true,
},
{
name: 'between (false)',
type: BooleanType,
expression: ['between', 3, 4, 5],
expected: false,
},
{
name: 'has (true)',
context: {
properties: {
property: 42,
},
},
type: BooleanType,
expression: ['has', 'property'],
expected: true,
},
{
name: 'has (false)',
context: {
properties: {
property: 42,
},
},
type: BooleanType,
expression: ['has', 'notProperty'],
expected: false,
},
{
name: 'has (true - null)',
context: {
properties: {
property: null,
},
},
type: BooleanType,
expression: ['has', 'property'],
expected: true,
},
{
name: 'has (true - undefined)',
context: {
properties: {
property: undefined,
},
},
type: BooleanType,
expression: ['has', 'property'],
expected: true,
},
{
name: 'has (nested object true)',
context: {
properties: {
deeply: {nested: {property: true}},
},
},
type: BooleanType,
expression: ['has', 'deeply', 'nested', 'property'],
expected: true,
},
{
name: 'has (nested object false)',
context: {
properties: {
deeply: {nested: {property: true}},
},
},
type: BooleanType,
expression: ['has', 'deeply', 'not', 'property'],
expected: false,
},
{
name: 'has (nested array true)',
context: {
properties: {
property: [42, {foo: 'bar'}],
},
},
type: BooleanType,
expression: ['has', 'property', 1, 'foo'],
expected: true,
},
{
name: 'has (nested array false)',
context: {
properties: {
property: [42, {foo: 'bar'}],
},
},
type: BooleanType,
expression: ['has', 'property', 0, 'foo'],
expected: false,
},
];
for (const c of cases) {
it(`works for ${c.name}`, () => {
const parsingContext = newParsingContext();
const evaluator = buildExpression(c.expression, c.type, parsingContext);
const evaluationContext = c.context || newEvaluationContext();
const value = evaluator(evaluationContext);
if (c.tolerance !== undefined) {
expect(value).to.roughlyEqual(c.expected, c.tolerance);
} else {
expect(value).to.eql(c.expected);
}
});
}
});
describe('interpolate expressions', () => {
* @typedef {Object} InterpolateTest
* @property {Array} method The interpolation method.
* @property {Array} stops The stops.
* @property {Array<Array>} cases The test cases.
*/
* @type {Array<InterpolateTest>}
*/
const tests = [
{
method: ['linear'],
stops: [-1, -1, 0, 0, 1, 100, 2, 1000],
cases: [
[-2, -1],
[-1, -1],
[-0.5, -0.5],
[0, 0],
[0.25, 25],
[0.5, 50],
[0.9, 90],
[1, 100],
[1.5, 550],
[2, 1000],
[3, 1000],
],
},
{
method: ['exponential', 2],
stops: [0, 0, 1, 100],
cases: [
[-1, 0],
[0, 0],
[0.25, 18.920711500272102],
[0.5, 41.42135623730952],
[0.9, 86.60659830736148],
[1, 100],
[1.5, 100],
],
},
{
method: ['exponential', 3],
stops: [0, 0, 1, 100],
cases: [
[-1, 0],
[0, 0],
[0.25, 15.80370064762462],
[0.5, 36.60254037844386],
[0.9, 84.39376897611433],
[1, 100],
[1.5, 100],
],
},
];
for (const t of tests) {
const expression = [
'interpolate',
t.method,
['var', 'input'],
...t.stops,
];
const type = typeof t.stops[1] === 'number' ? NumberType : ColorType;
describe(JSON.stringify(expression), () => {
const parsingContext = newParsingContext();
const evaluator = buildExpression(expression, type, parsingContext);
const evaluationContext = newEvaluationContext();
for (const [input, output] of t.cases) {
it(`works for ${input}`, () => {
evaluationContext.variables.input = input;
const got = evaluator(evaluationContext);
expect(got).to.roughlyEqual(output, 1e-6);
});
}
});
}
});
});