diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e92b661..969a78b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## UNRELEASED ### Fixed +- Fix `value`, `eval()` and `getLegendData()` for binary operations - Support mixed multi geometries in GeoJSON: LineString & MultiLineString, Polygon & MultiPolygon ## [1.3.1] 2019-06-17 diff --git a/debug/advanced/bivariate-legends.html b/debug/advanced/bivariate-legends.html new file mode 100644 index 000000000..e979d4c2e --- /dev/null +++ b/debug/advanced/bivariate-legends.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + +
+ +
+
+ + + +
+
+ + + + + diff --git a/src/client/windshaft-filtering.js b/src/client/windshaft-filtering.js index 246bc7c85..70f04a701 100644 --- a/src/client/windshaft-filtering.js +++ b/src/client/windshaft-filtering.js @@ -1,4 +1,11 @@ -import { And, Or, Equals, NotEquals, LessThan, LessThanOrEqualTo, GreaterThan, GreaterThanOrEqualTo } from '../renderer/viz/expressions/binary'; +import GreaterThan from '../renderer/viz/expressions/binary/GreaterThan'; +import GreaterThanOrEqualTo from '../renderer/viz/expressions/binary/GreaterThanOrEqualTo'; +import LessThan from '../renderer/viz/expressions/binary/LessThan'; +import LessThanOrEqualTo from '../renderer/viz/expressions/binary/LessThanOrEqualTo'; +import And from '../renderer/viz/expressions/binary/And'; +import Or from '../renderer/viz/expressions/binary/Or'; +import Equals from '../renderer/viz/expressions/binary/Equals'; +import NotEquals from '../renderer/viz/expressions/binary/NotEquals'; import { In, Nin } from '../renderer/viz/expressions/belongs'; import Between from '../renderer/viz/expressions/between'; import Property from '../renderer/viz/expressions/basic/property'; diff --git a/src/interactivity/featureVizProperty.js b/src/interactivity/featureVizProperty.js index 115c91816..7349ea375 100644 --- a/src/interactivity/featureVizProperty.js +++ b/src/interactivity/featureVizProperty.js @@ -22,6 +22,17 @@ export default class FeatureVizProperty { } get value () { - return this._viz[this._propertyName].eval(this._properties); + return this._viz[this._propertyName].value; + } + + eval (...properties) { + const props = []; + properties.forEach((property) => { + const prop = {}; + prop[property] = this._properties[property]; + props.push(prop); + }); + + return this._viz[this._propertyName].eval(props); } } diff --git a/src/renderer/viz/expressions.js b/src/renderer/viz/expressions.js index ff672880d..a51ddbd99 100644 --- a/src/renderer/viz/expressions.js +++ b/src/renderer/viz/expressions.js @@ -451,20 +451,21 @@ import { Nin } from './expressions/belongs'; import Between from './expressions/between'; -import { Mul } from './expressions/binary'; -import { Div } from './expressions/binary'; -import { Add } from './expressions/binary'; -import { Sub } from './expressions/binary'; -import { Mod } from './expressions/binary'; -import { Pow } from './expressions/binary'; -import { GreaterThan } from './expressions/binary'; -import { GreaterThanOrEqualTo } from './expressions/binary'; -import { LessThan } from './expressions/binary'; -import { LessThanOrEqualTo } from './expressions/binary'; -import { Equals } from './expressions/binary'; -import { NotEquals } from './expressions/binary'; -import { Or } from './expressions/binary'; -import { And } from './expressions/binary'; +// Binary Operations +import Add from './expressions/binary/Add'; +import And from './expressions/binary/And'; +import Div from './expressions/binary/Div'; +import Equals from './expressions/binary/Equals'; +import GreaterThan from './expressions/binary/GreaterThan'; +import GreaterThanOrEqualTo from './expressions/binary/GreaterThanOrEqualTo'; +import LessThan from './expressions/binary/LessThan'; +import LessThanOrEqualTo from './expressions/binary/LessThanOrEqualTo'; +import Mod from './expressions/binary/Mod'; +import Mul from './expressions/binary/Mul'; +import NotEquals from './expressions/binary/NotEquals'; +import Or from './expressions/binary/Or'; +import Pow from './expressions/binary/Pow'; +import Sub from './expressions/binary/Sub'; import Blend from './expressions/blend'; diff --git a/src/renderer/viz/expressions/Ramp.js b/src/renderer/viz/expressions/Ramp.js index ce7142c3c..2e6f37d75 100644 --- a/src/renderer/viz/expressions/Ramp.js +++ b/src/renderer/viz/expressions/Ramp.js @@ -129,6 +129,7 @@ export default class Ramp extends BaseExpression { super({ input, palette }); this.palette = palette; this.others = others; + this.type = palette.type; this._defaultOthers = others === DEFAULT_RAMP_OTHERS; } diff --git a/src/renderer/viz/expressions/basic/List.js b/src/renderer/viz/expressions/basic/List.js index e08fd9930..9ba0d44e3 100644 --- a/src/renderer/viz/expressions/basic/List.js +++ b/src/renderer/viz/expressions/basic/List.js @@ -63,6 +63,7 @@ export default class List extends Base { super(elems); this.elems = elems; + this.type = elems[0].type; } get value () { diff --git a/src/renderer/viz/expressions/binary.js b/src/renderer/viz/expressions/binary.js deleted file mode 100644 index 084f950ed..000000000 --- a/src/renderer/viz/expressions/binary.js +++ /dev/null @@ -1,525 +0,0 @@ -import { number } from '../expressions'; -import { implicitCast, checkMaxArguments } from './utils'; -import BaseExpression from './base'; -import CartoValidationError, { CartoValidationErrorTypes } from '../../../errors/carto-validation-error'; - -// Each binary expression can have a set of the following signatures (OR'ed flags) -const UNSUPPORTED_SIGNATURE = 0; -const NUMBERS_TO_NUMBER = 1; -const NUMBER_AND_COLOR_TO_COLOR = 2; -const COLORS_TO_COLOR = 4; -const CATEGORIES_TO_NUMBER = 8; -const IMAGES_TO_IMAGE = 16; - -/** - * Multiply two numeric expressions. - * - * @param {Number|Color} x - First value to multiply - * @param {Number|Color} y - Second value to multiply - * @return {Number|Color} Result of the multiplication - * - * @example Number multiplication. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.mul(5, 5) // 25 - * }); - * - * @example Number multiplication. (String) - * const viz = new carto.Viz(` - * width: 5 * 5 // Equivalent to mul(5, 5) - * `); - * - * @memberof carto.expressions - * @name mul - * @function - * @api - */ -export const Mul = genBinaryOp('mul', - NUMBERS_TO_NUMBER | NUMBER_AND_COLOR_TO_COLOR | COLORS_TO_COLOR | IMAGES_TO_IMAGE, - (x, y) => x * y, - (x, y) => `(${x} * ${y})` -); - -/** - * Divide two numeric expressions. - * - * @param {Number|Color} numerator - Numerator of the division - * @param {Number|Color} denominator - Denominator of the division - * @return {Number|Color} Result of the division - * - * @example Number division. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.div(10, 2) // 5 - * }); - * - * @example Number division. (String) - * const viz = new carto.Viz(` - * width: 10 / 2 // Equivalent to div(10, 2) - * `); - * - * @memberof carto.expressions - * @name div - * @function - * @api - */ -export const Div = genBinaryOp('div', - NUMBERS_TO_NUMBER | NUMBER_AND_COLOR_TO_COLOR | COLORS_TO_COLOR | IMAGES_TO_IMAGE, - (x, y) => x / y, - (x, y) => `(${x} / ${y})` -); - -/** - * Add two numeric expressions. - * - * @param {Number|Color} x - First value to add - * @param {Number|Color} y - Second value to add - * @return {Number|Color} Result of the addition - * - * @example Number addition. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.add(10, 2) // 12 - * }); - * - * @example Number addition. (String) - * const viz = new carto.Viz(` - * width: 10 + 2 // Equivalent to add(10, 2) - * `); - * - * @memberof carto.expressions - * @name add - * @function - * @api - */ -export const Add = genBinaryOp('add', - NUMBERS_TO_NUMBER | COLORS_TO_COLOR | IMAGES_TO_IMAGE, - (x, y) => x + y, - (x, y) => `(${x} + ${y})` -); - -/** - * Substract two numeric expressions. - * - * @param {Number|Color} minuend - The minuend of the subtraction - * @param {Number|Color} subtrahend - The subtrahend of the subtraction - * @return {Number|Color} Result of the substraction - * - * @example Number subtraction. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.sub(10, 2) // 8 - * }); - * - * @example Number subtraction. (String) - * const viz = new carto.Viz(` - * width: 10 - 2 // Equivalent to sub(10, 2) - * `); - * - * @memberof carto.expressions - * @name sub - * @function - * @api - */ -export const Sub = genBinaryOp('sub', - NUMBERS_TO_NUMBER | COLORS_TO_COLOR | IMAGES_TO_IMAGE, - (x, y) => x - y, - (x, y) => `(${x} - ${y})` -); - -/** - * Modulus of two numeric expressions, mod returns a numeric expression with the value of x modulo y. This is computed as x - y * floor(x/y). - * - * @param {Number} x - First value of the modulus - * @param {Number} y - Second value of the modulus - * @return {Number} Result of the modulus - * - * @example Number modulus. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.mod(10, 6) // 4 - * }); - * - * @example Number modulus. (String) - * const viz = new carto.Viz(` - * width: 10 % 6 // Equivalent to mod(10, 6) - * `); - * - * @memberof carto.expressions - * @name mod - * @function - * @api - */ -export const Mod = genBinaryOp('mod', - NUMBERS_TO_NUMBER, - (x, y) => x % y, - (x, y) => `mod(${x}, ${y})` -); - -/** - * Compute the base to the exponent power, return a numeric expression with the value of the first parameter raised to the power of the second. - * The result is undefined if x<0 or if x=0 and y≤0. - * - * @param {Number} base - Base of the power - * @param {Number} exponent - Exponent of the power - * @return {Number} Result of the power - * - * @example Number power. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * width: s.pow(2, 3) // 8 - * }); - * - * @example Number power. (String) - * const viz = new carto.Viz(` - * width: 2 ^ 3 // Equivalent to pow(2, 3) - * `); - * - * @memberof carto.expressions - * @name pow - * @function - * @api - */ -export const Pow = genBinaryOp('pow', - NUMBERS_TO_NUMBER, - (x, y) => Math.pow(x, y), - (x, y) => `pow(${x}, ${y})` -); - -/** - * Compare if x is greater than y. - * - * This returns a numeric expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - Firt value of the comparison - * @param {Number} y - Firt value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price greater than 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.gt(s.prop('price'), 30) - * }); - * - * @example Compare two numbers to show only elements with price greater than 30. (String) - * const viz = new carto.Viz(` - * filter: $price > 30 // Equivalent to gt($price, 30) - * `); - * - * @memberof carto.expressions - * @name gt - * @function - * @api - */ -export const GreaterThan = genBinaryOp('greaterThan', - NUMBERS_TO_NUMBER, - (x, y) => x > y ? 1 : 0, - (x, y) => `(${x}>${y}? 1.:0.)` -); - -/** - * Compare if x is greater than or equal to y. - * - * This returns a numeric expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - Firt value of the comparison - * @param {Number} y - Second value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price greater than or equal to 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.gte(s.prop('price'), 30) - * }); - * - * @example Compare two numbers to show only elements with price greater than or equal to 30. (String) - * const viz = new carto.Viz(` - * filter: $price >= 30 // Equivalent to gte($price, 30) - * `); - * - * @memberof carto.expressions - * @name gte - * @function - * @api - */ -export const GreaterThanOrEqualTo = genBinaryOp('greaterThanOrEqualTo', - NUMBERS_TO_NUMBER, - (x, y) => x >= y ? 1 : 0, - (x, y) => `(${x}>=${y}? 1.:0.)` -); - -/** - * Compare if x is lower than y. - * - * This returns a numeric expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - Firt value of the comparison - * @param {Number} y - Second value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price less than 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.lt(s.prop('price'), 30) - * }); - * - * @example Compare two numbers to show only elements with price less than 30. (String) - * const viz = new carto.Viz(` - * filter: $price < 30 // Equivalent to lt($price, 30) - * `); - * - * @memberof carto.expressions - * @name lt - * @function - * @api - */ -export const LessThan = genBinaryOp('lessThan', - NUMBERS_TO_NUMBER, - (x, y) => x < y ? 1 : 0, - (x, y) => `(${x}<${y}? 1.:0.)` -); - -/** - * Compare if x is lower than or equal to y. - * - * This returns a numeric expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - Firt value of the comparison - * @param {Number} y - Second value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price less than or equal to 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.lte(s.prop('price'), 30) - * }); - * - * @example Compare two numbers to show only elements with price less than or equal to 30. (String) - * const viz = new carto.Viz(` - * filter: $price <= 30 // Equivalent to lte($price, 30) - * `); - * - * @memberof carto.expressions - * @name lte - * @function - * @api - */ -export const LessThanOrEqualTo = genBinaryOp('lessThanOrEqualTo', - NUMBERS_TO_NUMBER, - (x, y) => x <= y ? 1 : 0, - (x, y) => `(${x}<=${y}? 1.:0.)` -); - -/** - * Compare if x is equal to a y. - * - * This returns a numeric expression where 0 means `false` and 1 means `true`. - * - * @param {Number|Category} x - Firt value of the comparison - * @param {Number|Category} y - Second value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price equal to 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.eq(s.prop('price'), 30) - * }); - * - * @example Compare two numbers to show only elements with price equal to 30. (String) - * const viz = new carto.Viz(` - * filter: $price == 30 // Equivalent to eq($price, 30) - * `); - * - * @memberof carto.expressions - * @name eq - * @function - * @api - */ -export const Equals = genBinaryOp('equals', - NUMBERS_TO_NUMBER | CATEGORIES_TO_NUMBER, - (x, y) => x === y ? 1 : 0, - (x, y) => `(${x}==${y}? 1.:0.)` -); - -/** - * Compare if x is different than y. - * - * This returns a number expression where 0 means `false` and 1 means `true`. - * - * @param {Number|Category} x - Firt value of the comparison - * @param {Number|Category} y - Second value of the comparison - * @return {Number} Result of the comparison: 0 or 1 - * - * @example Compare two numbers to show only elements with price not equal to 30. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.neq(s.prop('price'), 30); - * }); - * - * @example Compare two numbers to show only elements with price not equal to 30. (String) - * const viz = new carto.Viz(` - * filter: $price != 30 // Equivalent to neq($price, 30) - * `); - * - * @memberof carto.expressions - * @name neq - * @function - * @api - */ -export const NotEquals = genBinaryOp('notEquals', - NUMBERS_TO_NUMBER | CATEGORIES_TO_NUMBER, - (x, y) => x !== y ? 1 : 0, - (x, y) => `(${x}!=${y}? 1.:0.)` -); - -/** - * Perform a binary OR between two numeric expressions. - * If the numbers are different from 0 or 1 this performs a [fuzzy or operation](https://en.wikipedia.org/wiki/Fuzzy_logic#Fuzzification). - * This fuzzy behavior will allow transitions to work in a continuos, non-discrete, fashion. - * - * This returns a number expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - First value of the expression - * @param {Number} y - Second value of the expression - * @return {Number} Result of the expression - * - * @example Show only elements with price < 30 OR price > 1000. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.or( - * s.lt(s.prop('price'), 30), - * s.gt(s.prop('price'), 1000) - * ) - * }); - * - * @example Show only elements with price < 30 OR price > 1000. (String) - * const viz = new carto.Viz(` - * filter: $price < 30 or $price > 1000 // Equivalent to or(lt($price, 30), gt($price, 1000)) - * `); - * - * @memberof carto.expressions - * @name or - * @function - * @api - */ -export const Or = genBinaryOp('or', - NUMBERS_TO_NUMBER, - (x, y) => Math.min(x + y, 1), - (x, y) => `min(${x} + ${y}, 1.)` -); - -/** - * Perform a binary AND between two numeric expressions. - * If the numbers are different from 0 or 1 this performs a [fuzzy and operation](https://en.wikipedia.org/wiki/Fuzzy_logic#Fuzzification). - * This fuzzy behavior will allow transitions to work in a continuos, non-discrete, fashion. - * - * This returns a number expression where 0 means `false` and 1 means `true`. - * - * @param {Number} x - First value of the expression - * @param {Number} y - Second value of the expression - * @return {Number} Result of the expression - * - * @example Show only elements with price < 30 AND category === 'fruit'. - * const s = carto.expressions; - * const viz = new carto.Viz({ - * filter: s.and( - * s.lt(s.prop('price'), 30), - * s.eq(s.prop('category'), 'fruit') - * ) - * }); - * - * @example Show only elements with price < 30 AND category === 'fruit'. (String) - * const viz = new carto.Viz(` - * filter: $price < 30 and $category === 'fruit' // Equivalent to and(lt($price, 30), eq($category, 'fruit')) - * `); - * - * @memberof carto.expressions - * @name and - * @function - * @api - */ -export const And = genBinaryOp('and', - NUMBERS_TO_NUMBER, - (x, y) => Math.min(x * y, 1), - (x, y) => `min(${x} * ${y}, 1.)` -); - -function genBinaryOp (name, allowedSignature, jsFn, glsl) { - return class BinaryOperation extends BaseExpression { - constructor (a, b) { - checkMaxArguments(arguments, 2, name); - - if (Number.isFinite(a) && Number.isFinite(b)) { - return number(jsFn(a, b)); - } - a = implicitCast(a); - b = implicitCast(b); - - super({ a, b }); - this.expressionName = name; - this.inlineMaker = inline => glsl(inline.a, inline.b); - } - - get value () { - return jsFn(this.a.value, this.b.value); - } - - eval (feature) { - return jsFn(this.a.eval(feature), this.b.eval(feature)); - } - - _bindMetadata (meta) { - super._bindMetadata(meta); - const [a, b] = [this.a, this.b]; - - const signature = getSignature(a, b); - if (signature === UNSUPPORTED_SIGNATURE || !(signature & allowedSignature)) { - throw new CartoValidationError( - `${name}(): invalid parameter types\n'x' type was ${a.type}, 'y' type was ${b.type}`, - CartoValidationErrorTypes.INCORRECT_TYPE - ); - } - this.type = getReturnTypeFromSignature(signature); - } - }; -} - -function getSignature (a, b) { - if (!a.type || !b.type) { - return undefined; - } else if (a.type === 'number' && b.type === 'number') { - return NUMBERS_TO_NUMBER; - } else if (a.type === 'number' && b.type === 'color') { - return NUMBER_AND_COLOR_TO_COLOR; - } else if (a.type === 'color' && b.type === 'number') { - return NUMBER_AND_COLOR_TO_COLOR; - } else if (a.type === 'color' && b.type === 'color') { - return COLORS_TO_COLOR; - } else if (a.type === 'category' && b.type === 'category') { - return CATEGORIES_TO_NUMBER; - } else if ((a.type === 'image' && b.type === 'color') || - (a.type === 'image' && b.type === 'color') || - (a.type === 'image' && b.type === 'image') || - (a.type === 'color' && b.type === 'image')) { - return IMAGES_TO_IMAGE; - } else { - return UNSUPPORTED_SIGNATURE; - } -} - -function getReturnTypeFromSignature (signature) { - switch (signature) { - case NUMBERS_TO_NUMBER: - return 'number'; - case NUMBER_AND_COLOR_TO_COLOR: - return 'color'; - case COLORS_TO_COLOR: - return 'color'; - case CATEGORIES_TO_NUMBER: - return 'number'; - case IMAGES_TO_IMAGE: - return 'image'; - default: - return undefined; - } -} diff --git a/src/renderer/viz/expressions/binary/Add.js b/src/renderer/viz/expressions/binary/Add.js new file mode 100644 index 000000000..3dc1af315 --- /dev/null +++ b/src/renderer/viz/expressions/binary/Add.js @@ -0,0 +1,51 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER, COLORS_TO_COLOR, IMAGES_TO_IMAGE } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Add two numeric expressions. + * + * @param {Number|Color} x - First value to add + * @param {Number|Color} y - Second value to add + * @return {Number|Color} Result of the addition + * + * @example Number addition. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.add(10, 2) // 12 + * }); + * + * @example Number addition. (String) + * const viz = new carto.Viz(` + * width: 10 + 2 // Equivalent to add(10, 2) + * `); + * + * @memberof carto.expressions + * @name add + * @function + * @api + */ +export default class Add extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x + y, // NUMBERS_TO_NUMBER + 4: _addColors // COLORS_TO_COLOR + }; + + const glsl = (x, y) => `(${x} + ${y})`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | COLORS_TO_COLOR | IMAGES_TO_IMAGE; + } +} + +function _addColors (colorA, colorB) { + const r = colorA.r + colorB.r < 255 ? colorA.r + colorB.r : 255; + const g = colorA.g + colorB.g < 255 ? colorA.g + colorB.g : 255; + const b = colorA.b + colorB.b < 255 ? colorA.b + colorB.b : 255; + const a = colorA.a; + + return { r, g, b, a }; +} diff --git a/src/renderer/viz/expressions/binary/And.js b/src/renderer/viz/expressions/binary/And.js new file mode 100644 index 000000000..d6a937bf5 --- /dev/null +++ b/src/renderer/viz/expressions/binary/And.js @@ -0,0 +1,51 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { implicitCast, checkMaxArguments } from '../utils'; + +/** + * Perform a binary AND between two numeric expressions. + * If the numbers are different from 0 or 1 this performs a [fuzzy and operation](https://en.wikipedia.org/wiki/Fuzzy_logic#Fuzzification). + * This fuzzy behavior will allow transitions to work in a continuos, non-discrete, fashion. + * + * This returns a number expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - First value of the expression + * @param {Number} y - Second value of the expression + * @return {Number} Result of the expression + * + * @example Show only elements with price < 30 AND category === 'fruit'. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.and( + * s.lt(s.prop('price'), 30), + * s.eq(s.prop('category'), 'fruit') + * ) + * }); + * + * @example Show only elements with price < 30 AND category === 'fruit'. (String) + * const viz = new carto.Viz(` + * filter: $price < 30 and $category === 'fruit' // Equivalent to and(lt($price, 30), eq($category, 'fruit')) + * `); + * + * @memberof carto.expressions + * @name and + * @function + * @api + */ +export default class Or extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => Math.min(x * y, 1) // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `min(${x} * ${y}, 1.)`; + + a = implicitCast(a); + b = implicitCast(b); + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/BinaryOperation.js b/src/renderer/viz/expressions/binary/BinaryOperation.js new file mode 100644 index 000000000..4fab42cc6 --- /dev/null +++ b/src/renderer/viz/expressions/binary/BinaryOperation.js @@ -0,0 +1,140 @@ +import BaseExpression from '../base'; +import { implicitCast, checkMaxArguments } from '../utils'; +import CartoValidationError, { CartoValidationErrorTypes } from '../../../../errors/carto-validation-error'; +import { number } from '../../expressions'; + +export const UNSUPPORTED_SIGNATURE = 0; +export const NUMBERS_TO_NUMBER = 1; +export const NUMBER_AND_COLOR_TO_COLOR = 2; +export const COLORS_TO_COLOR = 4; +export const CATEGORIES_TO_NUMBER = 8; +export const IMAGES_TO_IMAGE = 16; + +export class BinaryOperation extends BaseExpression { + constructor (a, b, signatureMethods, glsl) { + checkMaxArguments(arguments, 4); + + if (Number.isFinite(a) && Number.isFinite(b)) { + return number(signatureMethods[NUMBERS_TO_NUMBER](a, b)); + } + + a = implicitCast(a); + b = implicitCast(b); + + super({ a, b }); + + this.signatureMethods = signatureMethods; + this._signature = getSignature(a, b); + this.glsl = glsl; + this.allowedSignature = UNSUPPORTED_SIGNATURE; + + this.inlineMaker = inline => glsl(inline.a, inline.b); + } + + get value () { + return this.operation(this.a.value, this.b.value); + } + + get operation () { + return this.signatureMethods[this._signature] || this.signatureMethods[NUMBERS_TO_NUMBER]; + } + + eval (...features) { + if (Number.isFinite(this.a) && Number.isFinite(this.b)) { + return this.operation(this.a.value, this.b.value); + } + + if (Number.isFinite(this.a)) { + return this.operation(this.a.value, this.b.eval(features[0])); + } + + if (Number.isFinite(this.b)) { + return this.operation(this.a.eval(features[0]), this.b.value); + } + + const { featureA, featureB } = this._getDependentFeatures(features); + const valueA = this.a.eval(featureA); + const valueB = this.b.eval(featureB); + + return this.operation(valueA, valueB); + } + + getLegendData (options) { + const legendDataA = this.a.getLegendData(options); + const legendDataB = this.b.getLegendData(options); + const SIZE_A = legendDataA.data.length; + const SIZE_B = legendDataB.data.length; + const data = []; + + for (let i = 0; i < SIZE_A; i++) { + for (let j = 0; j < SIZE_B; j++) { + const value = this.operation(legendDataA.data[i].value, legendDataB.data[j].value); + data.push({ value }); + } + } + + return { n: SIZE_A, m: SIZE_B, data }; + } + + _getDependentFeatures (features) { + const { featureA, featureB } = features.length > 1 + ? { featureA: features[0], featureB: features[1] } + : { featureA: features[0], featureB: features[0] }; + + return { featureA, featureB }; + } + + _bindMetadata (meta) { + super._bindMetadata(meta); + const [a, b] = [this.a, this.b]; + + this._signature = getSignature(a, b); + if (this._signature === UNSUPPORTED_SIGNATURE || !(this._signature & this.allowedSignature)) { + throw new CartoValidationError( + `${this.expressionName}(): invalid parameter types\n'x' type was ${a.type}, 'y' type was ${b.type}`, + CartoValidationErrorTypes.INCORRECT_TYPE + ); + } + this.type = getReturnTypeFromSignature(this._signature); + } +} + +function getSignature (a, b) { + if (!a.type || !b.type) { + return undefined; + } else if (a.type === 'number' && b.type === 'number') { + return NUMBERS_TO_NUMBER; + } else if (a.type === 'number' && b.type === 'color') { + return NUMBER_AND_COLOR_TO_COLOR; + } else if (a.type === 'color' && b.type === 'number') { + return NUMBER_AND_COLOR_TO_COLOR; + } else if (a.type === 'color' && b.type === 'color') { + return COLORS_TO_COLOR; + } else if (a.type === 'category' && b.type === 'category') { + return CATEGORIES_TO_NUMBER; + } else if ((a.type === 'image' && b.type === 'color') || + (a.type === 'image' && b.type === 'color') || + (a.type === 'image' && b.type === 'image') || + (a.type === 'color' && b.type === 'image')) { + return IMAGES_TO_IMAGE; + } else { + return UNSUPPORTED_SIGNATURE; + } +} + +function getReturnTypeFromSignature (signature) { + switch (signature) { + case NUMBERS_TO_NUMBER: + return 'number'; + case NUMBER_AND_COLOR_TO_COLOR: + return 'color'; + case COLORS_TO_COLOR: + return 'color'; + case CATEGORIES_TO_NUMBER: + return 'number'; + case IMAGES_TO_IMAGE: + return 'image'; + default: + return undefined; + } +} diff --git a/src/renderer/viz/expressions/binary/Div.js b/src/renderer/viz/expressions/binary/Div.js new file mode 100644 index 000000000..2e2913c12 --- /dev/null +++ b/src/renderer/viz/expressions/binary/Div.js @@ -0,0 +1,79 @@ +import { number } from '../../expressions'; +import { + BinaryOperation, + NUMBERS_TO_NUMBER, + NUMBER_AND_COLOR_TO_COLOR, + COLORS_TO_COLOR, + IMAGES_TO_IMAGE +} from './BinaryOperation'; + +import { implicitCast, checkMaxArguments } from '../utils'; + +/** + * Divide two numeric expressions. + * + * @param {Number|Color} numerator - Numerator of the division + * @param {Number|Color} denominator - Denominator of the division + * @return {Number|Color} Result of the division + * + * @example Number division. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.div(10, 2) // 5 + * }); + * + * @example Number division. (String) + * const viz = new carto.Viz(` + * width: 10 / 2 // Equivalent to div(10, 2) + * `); + * + * @memberof carto.expressions + * @name div + * @function + * @api + */ +export default class Div extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + if (Number.isFinite(a) && Number.isFinite(b)) { + return number(a / b); + } + + const signatureMethods = { + 1: (x, y) => x / y, // NUMBERS_TO_NUMBER + 2: _divNumberColor, // NUMBER_AND_COLOR_TO_COLOR + 4: _divColors // COLORS_TO_COLOR + }; + + const glsl = (x, y) => `(${x} / ${y})`; + + a = implicitCast(a); + b = implicitCast(b); + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | NUMBER_AND_COLOR_TO_COLOR | COLORS_TO_COLOR | IMAGES_TO_IMAGE; + } +} + +function _divColors (colorA, colorB) { + return { + r: Math.round(colorA.r / colorB.r), + g: Math.round(colorA.g / colorB.g), + b: Math.round(colorA.b / colorB.b), + a: colorA.a + }; +} + +function _divNumberColor (valueA, valueB) { + const { n, color } = typeof valueA === 'number' + ? { n: valueA, color: valueB } + : { n: valueB, color: valueA }; + + return { + r: Math.round(color.r / n), + g: Math.round(color.g / n), + b: Math.round(color.b / n), + a: color.a + }; +} diff --git a/src/renderer/viz/expressions/binary/Equals.js b/src/renderer/viz/expressions/binary/Equals.js new file mode 100644 index 000000000..3a7fd0ba1 --- /dev/null +++ b/src/renderer/viz/expressions/binary/Equals.js @@ -0,0 +1,44 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER, CATEGORIES_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is equal to a y. + * + * This returns a numeric expression where 0 means `false` and 1 means `true`. + * + * @param {Number|Category} x - Firt value of the comparison + * @param {Number|Category} y - Second value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price equal to 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.eq(s.prop('price'), 30) + * }); + * + * @example Compare two numbers to show only elements with price equal to 30. (String) + * const viz = new carto.Viz(` + * filter: $price == 30 // Equivalent to eq($price, 30) + * `); + * + * @memberof carto.expressions + * @name eq + * @function + * @api + */ +export default class Equals extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x === y ? 1 : 0, // NUMBERS_TO_NUMBER, + 8: (x, y) => x === y ? 1 : 0 // CATEGORIES_TO_NUMBER + }; + + const glsl = (x, y) => `(${x}==${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | CATEGORIES_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/GreaterThan.js b/src/renderer/viz/expressions/binary/GreaterThan.js new file mode 100644 index 000000000..33b26e26f --- /dev/null +++ b/src/renderer/viz/expressions/binary/GreaterThan.js @@ -0,0 +1,43 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is greater than y. + * + * This returns a numeric expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - Firt value of the comparison + * @param {Number} y - Firt value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price greater than 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.gt(s.prop('price'), 30) + * }); + * + * @example Compare two numbers to show only elements with price greater than 30. (String) + * const viz = new carto.Viz(` + * filter: $price > 30 // Equivalent to gt($price, 30) + * `); + * + * @memberof carto.expressions + * @name gt + * @function + * @api + */ +export default class GreaterThan extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x > y ? 1 : 0 // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `(${x}>${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/GreaterThanOrEqualTo.js b/src/renderer/viz/expressions/binary/GreaterThanOrEqualTo.js new file mode 100644 index 000000000..b9bcadcec --- /dev/null +++ b/src/renderer/viz/expressions/binary/GreaterThanOrEqualTo.js @@ -0,0 +1,43 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is greater than or equal to y. + * + * This returns a numeric expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - Firt value of the comparison + * @param {Number} y - Second value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price greater than or equal to 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.gte(s.prop('price'), 30) + * }); + * + * @example Compare two numbers to show only elements with price greater than or equal to 30. (String) + * const viz = new carto.Viz(` + * filter: $price >= 30 // Equivalent to gte($price, 30) + * `); + * + * @memberof carto.expressions + * @name gte + * @function + * @api + */ +export default class GreaterThanOrEqualTo extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x >= y ? 1 : 0 // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `(${x}>=${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/LessThan.js b/src/renderer/viz/expressions/binary/LessThan.js new file mode 100644 index 000000000..355f2be0a --- /dev/null +++ b/src/renderer/viz/expressions/binary/LessThan.js @@ -0,0 +1,43 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is lower than y. + * + * This returns a numeric expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - Firt value of the comparison + * @param {Number} y - Second value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price less than 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.lt(s.prop('price'), 30) + * }); + * + * @example Compare two numbers to show only elements with price less than 30. (String) + * const viz = new carto.Viz(` + * filter: $price < 30 // Equivalent to lt($price, 30) + * `); + * + * @memberof carto.expressions + * @name lt + * @function + * @api + */ +export default class LessThan extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x < y ? 1 : 0 // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `(${x}<${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/LessThanOrEqualTo.js b/src/renderer/viz/expressions/binary/LessThanOrEqualTo.js new file mode 100644 index 000000000..723a942e9 --- /dev/null +++ b/src/renderer/viz/expressions/binary/LessThanOrEqualTo.js @@ -0,0 +1,43 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is lower than or equal to y. + * + * This returns a numeric expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - Firt value of the comparison + * @param {Number} y - Second value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price less than or equal to 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.lte(s.prop('price'), 30) + * }); + * + * @example Compare two numbers to show only elements with price less than or equal to 30. (String) + * const viz = new carto.Viz(` + * filter: $price <= 30 // Equivalent to lte($price, 30) + * `); + * + * @memberof carto.expressions + * @name lte + * @function + * @api + */ +export default class LessThanOrEqualTo extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x <= y ? 1 : 0 // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `(${x}<=${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/Mod.js b/src/renderer/viz/expressions/binary/Mod.js new file mode 100644 index 000000000..45002daa2 --- /dev/null +++ b/src/renderer/viz/expressions/binary/Mod.js @@ -0,0 +1,44 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { implicitCast, checkMaxArguments } from '../utils'; + +/** + * Modulus of two numeric expressions, mod returns a numeric expression with the value of x module y. This is computed as x - y * floor(x/y). + * + * @param {Number} x - First value of the modulus + * @param {Number} y - Second value of the modulus + * @return {Number} Result of the modulus + * + * @example Number modulus. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.mod(10, 6) // 4 + * }); + * + * @example Number modulus. (String) + * const viz = new carto.Viz(` + * width: 10 % 6 // Equivalent to mod(10, 6) + * `); + * + * @memberof carto.expressions + * @name mod + * @function + * @api + */ +export default class Mod extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x % y // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `mod(${x}, ${y})`; + + a = implicitCast(a); + b = implicitCast(b); + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/Mul.js b/src/renderer/viz/expressions/binary/Mul.js new file mode 100644 index 000000000..d8ee3692b --- /dev/null +++ b/src/renderer/viz/expressions/binary/Mul.js @@ -0,0 +1,71 @@ +import { + BinaryOperation, + NUMBERS_TO_NUMBER, + NUMBER_AND_COLOR_TO_COLOR, + COLORS_TO_COLOR, + IMAGES_TO_IMAGE +} from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Multiply two numeric expressions. + * + * @param {Number|Color} x - First value to multiply + * @param {Number|Color} y - Second value to multiply + * @return {Number|Color} Result of the multiplication + * + * @example Number multiplication. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.mul(5, 5) // 25 + * }); + * + * @example Number multiplication. (String) + * const viz = new carto.Viz(` + * width: 5 * 5 // Equivalent to mul(5, 5) + * `); + * + * @memberof carto.expressions + * @name mul + * @function + * @api + */ +export default class Mul extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x * y, // NUMBERS_TO_NUMBER + 2: _mulNumberColor, // NUMBER_AND_COLOR_TO_COLOR + 4: _mulColors // COLORS_TO_COLOR + }; + + const glsl = (x, y) => `(${x} * ${y})`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | NUMBER_AND_COLOR_TO_COLOR | COLORS_TO_COLOR | IMAGES_TO_IMAGE; + } +} + +function _mulColors (colorA, colorB) { + return { + r: Math.round(colorA.r * colorB.r / 255), + g: Math.round(colorA.g * colorB.g / 255), + b: Math.round(colorA.b * colorB.b / 255), + a: colorA.a + }; +} + +function _mulNumberColor (valueA, valueB) { + const { n, color } = typeof valueA === 'number' + ? { n: valueA, color: valueB } + : { n: valueB, color: valueA }; + + return { + r: Math.round(n * color.r / 255), + g: Math.round(n * color.g / 255), + b: Math.round(n * color.b / 255), + a: color.a + }; +} diff --git a/src/renderer/viz/expressions/binary/NotEquals.js b/src/renderer/viz/expressions/binary/NotEquals.js new file mode 100644 index 000000000..b6cebdf0f --- /dev/null +++ b/src/renderer/viz/expressions/binary/NotEquals.js @@ -0,0 +1,44 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER, CATEGORIES_TO_NUMBER } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Compare if x is different than y. + * + * This returns a number expression where 0 means `false` and 1 means `true`. + * + * @param {Number|Category} x - Firt value of the comparison + * @param {Number|Category} y - Second value of the comparison + * @return {Number} Result of the comparison: 0 or 1 + * + * @example Compare two numbers to show only elements with price not equal to 30. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.neq(s.prop('price'), 30); + * }); + * + * @example Compare two numbers to show only elements with price not equal to 30. (String) + * const viz = new carto.Viz(` + * filter: $price != 30 // Equivalent to neq($price, 30) + * `); + * + * @memberof carto.expressions + * @name neq + * @function + * @api + */ +export default class NotEquals extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x !== y ? 1 : 0, // NUMBERS_TO_NUMBER + 8: (x, y) => x !== y ? 1 : 0 + }; + + const glsl = (x, y) => `(${x}!=${y}? 1.:0.)`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | CATEGORIES_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/Or.js b/src/renderer/viz/expressions/binary/Or.js new file mode 100644 index 000000000..2d5a57001 --- /dev/null +++ b/src/renderer/viz/expressions/binary/Or.js @@ -0,0 +1,51 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; + +import { implicitCast, checkMaxArguments } from '../utils'; + +/** + * Perform a binary OR between two numeric expressions. + * If the numbers are different from 0 or 1 this performs a [fuzzy or operation](https://en.wikipedia.org/wiki/Fuzzy_logic#Fuzzification). + * This fuzzy behavior will allow transitions to work in a continuos, non-discrete, fashion. + * + * This returns a number expression where 0 means `false` and 1 means `true`. + * + * @param {Number} x - First value of the expression + * @param {Number} y - Second value of the expression + * @return {Number} Result of the expression + * + * @example Show only elements with price < 30 OR price > 1000. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * filter: s.or( + * s.lt(s.prop('price'), 30), + * s.gt(s.prop('price'), 1000) + * ) + * }); + * + * @example Show only elements with price < 30 OR price > 1000. (String) + * const viz = new carto.Viz(` + * filter: $price < 30 or $price > 1000 // Equivalent to or(lt($price, 30), gt($price, 1000)) + * `); + * + * @memberof carto.expressions + * @name or + * @function + * @api + */ +export default class Or extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => Math.min(x + y, 1) // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `min(${x} + ${y}, 1.)`; + + a = implicitCast(a); + b = implicitCast(b); + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/Pow.js b/src/renderer/viz/expressions/binary/Pow.js new file mode 100644 index 000000000..02049af8c --- /dev/null +++ b/src/renderer/viz/expressions/binary/Pow.js @@ -0,0 +1,41 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER } from './BinaryOperation'; +import { checkMaxArguments } from '../utils'; + +/** + * Compute the base to the exponent power, return a numeric expression with the value of the first parameter raised to the power of the second. + * The result is undefined if x<0 or if x=0 and y≤0. + * + * @param {Number} base - Base of the power + * @param {Number} exponent - Exponent of the power + * @return {Number} Result of the power + * + * @example Number power. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.pow(2, 3) // 8 + * }); + * + * @example Number power. (String) + * const viz = new carto.Viz(` + * width: 2 ^ 3 // Equivalent to pow(2, 3) + * `); + * + * @memberof carto.expressions + * @name pow + * @function + * @api + */ +export default class Pow extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => Math.pow(x, y) // NUMBERS_TO_NUMBER + }; + + const glsl = (x, y) => `pow(${x}, ${y})`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER; + } +} diff --git a/src/renderer/viz/expressions/binary/Sub.js b/src/renderer/viz/expressions/binary/Sub.js new file mode 100644 index 000000000..4f114335a --- /dev/null +++ b/src/renderer/viz/expressions/binary/Sub.js @@ -0,0 +1,51 @@ +import { BinaryOperation, NUMBERS_TO_NUMBER, COLORS_TO_COLOR, IMAGES_TO_IMAGE } from './BinaryOperation'; + +import { checkMaxArguments } from '../utils'; + +/** + * Substract two numeric expressions. + * + * @param {Number|Color} minuend - The minuend of the subtraction + * @param {Number|Color} subtrahend - The subtrahend of the subtraction + * @return {Number|Color} Result of the substraction + * + * @example Number subtraction. + * const s = carto.expressions; + * const viz = new carto.Viz({ + * width: s.sub(10, 2) // 8 + * }); + * + * @example Number subtraction. (String) + * const viz = new carto.Viz(` + * width: 10 - 2 // Equivalent to sub(10, 2) + * `); + * + * @memberof carto.expressions + * @name sub + * @function + * @api + */ +export default class Sub extends BinaryOperation { + constructor (a, b) { + checkMaxArguments(arguments, 2); + + const signatureMethods = { + 1: (x, y) => x - y, // NUMBERS_TO_NUMBER + 4: _subColors // COLORS_TO_COLOR + }; + + const glsl = (x, y) => `(${x} - ${y})`; + + super(a, b, signatureMethods, glsl); + this.allowedSignature = NUMBERS_TO_NUMBER | COLORS_TO_COLOR | IMAGES_TO_IMAGE; + } +} + +function _subColors (colorA, colorB) { + const r = colorA.r - colorB.r > 0 ? colorA.r - colorB.r : 0; + const g = colorA.g - colorB.g > 0 ? colorA.g - colorB.g : 0; + const b = colorA.b - colorB.b > 0 ? colorA.b - colorB.b : 0; + const a = colorA.a; + + return { r, g, b, a }; +} diff --git a/src/renderer/viz/expressions/classification/Classifier.js b/src/renderer/viz/expressions/classification/Classifier.js index f5f6d116e..3dd0819c1 100644 --- a/src/renderer/viz/expressions/classification/Classifier.js +++ b/src/renderer/viz/expressions/classification/Classifier.js @@ -100,7 +100,7 @@ export default class Classifier extends BaseExpression { _genBreakpoints () { } _applyToShaderSource (getGLSLforProperty) { - return this._GLSLhelper.applyToShaderSource(getGLSLforProperty); + return this._GLSLhelper ? this._GLSLhelper.applyToShaderSource(getGLSLforProperty) : null; } _preDraw (program, drawMetadata, gl) { diff --git a/src/renderer/viz/expressions/classification/ClassifierGLSLHelper.js b/src/renderer/viz/expressions/classification/ClassifierGLSLHelper.js index 39bc32ae0..759531f4b 100644 --- a/src/renderer/viz/expressions/classification/ClassifierGLSLHelper.js +++ b/src/renderer/viz/expressions/classification/ClassifierGLSLHelper.js @@ -18,7 +18,9 @@ export default class ClassifierGLSLHelper { `${index > 0 ? 'else' : ''} if (x<(${childInlines[`arg${index}`]})){ return ${(index / divisor).toFixed(20)}; }`; - const funcBody = this.classifier.breakpoints.map(elif).join(''); + const funcBody = this.classifier.breakpoints + ? this.classifier.breakpoints.map(elif).join('') + : ''; const preface = `float ${funcName}(float x){ ${funcBody} diff --git a/test/unit/renderer/viz/expressions/binary.test.js b/test/unit/renderer/viz/expressions/binary.test.js index 229ddd850..4e114ef3a 100644 --- a/test/unit/renderer/viz/expressions/binary.test.js +++ b/test/unit/renderer/viz/expressions/binary.test.js @@ -1,5 +1,6 @@ import * as s from '../../../../../src/renderer/viz/expressions'; import { validateDynamicType, validateMaxArgumentsError, validateTypeErrors } from './utils'; +import Metadata from '../../../../../src/renderer/Metadata'; // Add custom toString function to improve test output. s.TRUE.toString = () => 's.TRUE'; @@ -75,134 +76,496 @@ describe('src/renderer/viz/expressions/binary', () => { }); }); - describe('eval', () => { - describe('and', () => { - test('and', s.TRUE, s.TRUE, 1); - test('and', s.TRUE, s.FALSE, 0); - test('and', s.FALSE, s.FALSE, 0); - test('and', 0.5, s.TRUE, 0.5); - test('and', 0.5, 0.5, 0.25); + describe('.value', () => { + function testValue (fn, param1, param2, expected) { + it(`${fn}(${param1}, ${param2}) should return ${expected}`, () => { + const actual = s[fn](param1, param2).value; + expect(actual).toEqual(expected); + }); + } + + describe('mul', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('mul', 0, 0, 0); + testValue('mul', 1, 0, 0); + testValue('mul', 1, 1, 1); + testValue('mul', 1, 2, 2); + testValue('mul', -1, 2, -2); + }); + + describe('NUMBER_AND_COLOR_TO_COLOR', () => { + testValue('mul', 10, s.rgba(255, 15, 12, 1), { r: 10, g: 1, b: 0, a: 1 }); + testValue('mul', s.rgba(255, 15, 12, 1), 10, { r: 10, g: 1, b: 0, a: 1 }); + }); + + describe('COLORS_TO_COLOR', () => { + testValue('mul', s.rgba(255, 15, 12, 1), s.rgba(35, 20, 240, 1), { r: 35, g: 1, b: 11, a: 1 }); + }); + }); + + describe('div', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('div', 1, 0, Infinity); + testValue('div', -1, 0, -Infinity); + testValue('div', 0, 0, NaN); + testValue('div', 0, 1, 0); + testValue('div', 4, 2, 2); + testValue('div', -4, 2, -2); + }); + + describe('NUMBER_AND_COLOR_TO_COLOR', () => { + + }); + + describe('COLORS_TO_COLOR', () => { + + }); + }); + + describe('add', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('add', 0, 0, 0); + testValue('add', 0, 1, 1); + testValue('add', 2, 2, 4); + testValue('add', -2, 2, 0); + testValue('add', -2, -3, -5); + }); + + describe('COLORS_TO_COLOR', () => { + testValue('add', s.rgba(255, 15, 12, 1), s.rgba(35, 20, 240, 1), { r: 255, g: 35, b: 252, a: 1 }); + }); + }); + + describe('sub', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('sub', 0, 0, 0); + testValue('sub', 0, 1, -1); + testValue('sub', 2, 2, 0); + testValue('sub', -2, 2, -4); + testValue('sub', -2, -3, 1); + }); + + describe('COLORS_TO_COLOR', () => { + testValue('sub', s.rgba(255, 15, 12, 1), s.rgba(35, 20, 240, 1), { r: 220, g: 0, b: 0, a: 1 }); + }); + }); + + describe('mod', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('mod', 0, 1, 0); + testValue('mod', 2, 1, 0); + testValue('mod', 2, 2, 0); + testValue('mod', 6, 4, 2); + testValue('mod', -6, 4, -2); + }); + }); + + describe('pow', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('pow', 0, 0, 1); + testValue('pow', 0, 1, 0); + testValue('pow', 2, 2, 4); + testValue('pow', -2, 2, 4); + testValue('pow', -2, -3, -0.125); + }); }); describe('or', () => { - test('or', 0, 0, 0); - test('or', 0, 1, 1); - test('or', 1, 1, 1); - test('or', 0.5, 1, 1); + describe('NUMBERS_TO_NUMBER', () => { + testValue('or', 0, 0, 0); + testValue('or', 0, 1, 1); + testValue('or', 1, 1, 1); + testValue('or', 0.5, 1, 1); + }); }); + describe('and', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('and', s.TRUE, s.TRUE, 1); + testValue('and', s.TRUE, s.FALSE, 0); + testValue('and', s.FALSE, s.FALSE, 0); + testValue('and', 0.5, s.TRUE, 0.5); + testValue('and', 0.5, 0.5, 0.25); + }); + }); + + describe('greaterThan', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('gt', 0, 0, 0); + testValue('gt', 0, 1, 0); + testValue('gt', 1, 0, 1); + testValue('gt', 2, 2, 0); + testValue('gt', 2, 3, 0); + testValue('gt', 3, 2, 1); + testValue('gt', -3, 2, 0); + }); + }); + + describe('greaterThanOrEqualTo', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('gte', 0, 0, 1); + testValue('gte', 0, 1, 0); + testValue('gte', 1, 0, 1); + testValue('gte', 2, 2, 1); + testValue('gte', 2, 3, 0); + testValue('gte', 3, 2, 1); + testValue('gte', -3, 2, 0); + }); + }); + + describe('lessThan', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('lt', 0, 0, 0); + testValue('lt', 0, 1, 1); + testValue('lt', 1, 0, 0); + testValue('lt', 2, 2, 0); + testValue('lt', 2, 3, 1); + testValue('lt', 3, 2, 0); + testValue('lt', -3, 2, 1); + }); + }); + + describe('lessThanOrEqualTo', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('lte', 0, 0, 1); + testValue('lte', 0, 1, 1); + testValue('lte', 1, 0, 0); + testValue('lte', 2, 2, 1); + testValue('lte', 2, 3, 1); + testValue('lte', 3, 2, 0); + testValue('lte', -3, 2, 1); + }); + }); + + describe('equals', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('eq', 0, 0, 1); + testValue('eq', 0, 1, 0); + testValue('eq', 1, 0, 0); + testValue('eq', 2, 2, 1); + testValue('eq', 2, 3, 0); + }); + + describe('CATEGORIES_TO_NUMBER', () => { + testValue('eq', '0', '0', 1); + testValue('eq', '0', '1', 0); + testValue('eq', '1', '0', 0); + testValue('eq', '2', '2', 1); + testValue('eq', '2', '3', 0); + }); + }); + + describe('notEquals', () => { + describe('NUMBERS_TO_NUMBER', () => { + testValue('neq', 0, 0, 0); + testValue('neq', 0, 1, 1); + testValue('neq', 1, 0, 1); + testValue('neq', 2, 2, 0); + testValue('neq', 2, 3, 1); + }); + + describe('CATEGORIES_TO_NUMBER', () => { + testValue('neq', '0', '0', 0); + testValue('neq', '0', '1', 1); + testValue('neq', '1', '0', 1); + testValue('neq', '2', '2', 0); + testValue('neq', '2', '3', 1); + }); + }); + }); + + describe('.eval', () => { + const METADATA = new Metadata({ + properties: { + color: { + type: 'category', + categories: [ + { name: 'red' }, + { name: 'blue' } + ] + } + } + }); + + function testEval (fn, param1, param2, features, expected) { + it(`${fn}(${param1}, ${param2}).eval(featureA, featureB) should return ${expected}`, () => { + const actual = s[fn](param1, param2).eval(...features); + expect(actual).toEqual(expected); + }); + } + describe('mul', () => { - test('mul', 0, 0, 0); - test('mul', 1, 0, 0); - test('mul', 1, 1, 1); - test('mul', 1, 2, 2); - test('mul', -1, 2, -2); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 2, c: 5 }; + + testEval('mul', s.prop('a'), s.prop('b'), [featureA, featureB], 2); + testEval('mul', 2, s.prop('b'), [featureB], 4); + testEval('mul', s.prop('b'), 2, [featureB], 4); + testEval('mul', s.prop('a'), s.prop('c'), [featureC], 10); + }); + + describe('NUMBER_AND_COLOR_TO_COLOR', () => { + const featureA = { color: 'red' }; + const ramp = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + + ramp._bindMetadata(METADATA); + + testEval('mul', 10, ramp, [featureA], { r: 10, g: 1, b: 0, a: 1 }); + testEval('mul', ramp, 10, [featureA], { r: 10, g: 1, b: 0, a: 1 }); + }); + + describe('COLORS_TO_COLOR', () => { + const featureA = { color: 'red' }; + const featureB = { color: 'blue' }; + const rampA = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + const rampB = s.ramp(s.buckets(s.prop('color'), ['blue']), [s.rgba(35, 20, 240, 1)]); + + rampA._bindMetadata(METADATA); + rampB._bindMetadata(METADATA); + + testEval('mul', rampA, rampB, [featureA, featureB], { r: 35, g: 1, b: 11, a: 1 }); + }); }); describe('div', () => { - test('div', 1, 0, Infinity); - test('div', -1, 0, -Infinity); - test('div', 0, 0, NaN); - test('div', 0, 1, 0); - test('div', 4, 2, 2); - test('div', -4, 2, -2); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 8, c: 4 }; + + testEval('div', s.prop('a'), s.prop('b'), [featureA, featureB], 0.5); + testEval('div', 2, s.prop('b'), [featureB], 1); + testEval('div', s.prop('b'), 2, [featureB], 1); + testEval('div', s.prop('a'), s.prop('c'), [featureC], 2); + }); + + describe('NUMBER_AND_COLOR_TO_COLOR', () => { + const featureA = { color: 'red' }; + const ramp = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + + ramp._bindMetadata(METADATA); + + testEval('div', 10, ramp, [featureA], { r: 26, g: 2, b: 1, a: 1 }); + testEval('div', ramp, 10, [featureA], { r: 26, g: 2, b: 1, a: 1 }); + }); + + describe('COLORS_TO_COLOR', () => { + const featureA = { color: 'red' }; + const featureB = { color: 'blue' }; + const rampA = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + const rampB = s.ramp(s.buckets(s.prop('color'), ['blue']), [s.rgba(35, 20, 240, 1)]); + + rampA._bindMetadata(METADATA); + rampB._bindMetadata(METADATA); + + testEval('div', rampA, rampB, [featureA, featureB], { r: 7, g: 1, b: 0, a: 1 }); + }); }); describe('add', () => { - test('add', 0, 0, 0); - test('add', 0, 1, 1); - test('add', 2, 2, 4); - test('add', -2, 2, 0); - test('add', -2, -3, -5); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 8, c: 4 }; + + testEval('add', s.prop('a'), s.prop('b'), [featureA, featureB], 3); + testEval('add', 2, s.prop('b'), [featureB], 4); + testEval('add', s.prop('b'), 2, [featureB], 4); + testEval('add', s.prop('a'), s.prop('c'), [featureC], 12); + }); + + describe('COLORS_TO_COLOR', () => { + const featureA = { color: 'red' }; + const featureB = { color: 'blue' }; + + const rampA = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + const rampB = s.ramp(s.buckets(s.prop('color'), ['blue']), [s.rgba(35, 20, 240, 1)]); + + rampA._bindMetadata(METADATA); + rampB._bindMetadata(METADATA); + + testEval('add', rampA, rampB, [featureA, featureB], { r: 255, g: 35, b: 252, a: 1 }); + }); }); describe('sub', () => { - test('sub', 0, 0, 0); - test('sub', 0, 1, -1); - test('sub', 2, 2, 0); - test('sub', -2, 2, -4); - test('sub', -2, -3, 1); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 8, c: 4 }; + + testEval('sub', s.prop('a'), s.prop('b'), [featureA, featureB], -1); + testEval('sub', 2, s.prop('b'), [featureB], 0); + testEval('sub', s.prop('b'), 2, [featureB], 0); + testEval('sub', s.prop('a'), s.prop('c'), [featureC], 4); + }); + + describe('COLORS_TO_COLOR', () => { + const featureA = { color: 'red' }; + const featureB = { color: 'blue' }; + + const rampA = s.ramp(s.buckets(s.prop('color'), ['red']), [s.rgba(255, 15, 12, 1)]); + const rampB = s.ramp(s.buckets(s.prop('color'), ['blue']), [s.rgba(35, 20, 240, 1)]); + + rampA._bindMetadata(METADATA); + rampB._bindMetadata(METADATA); + + testEval('sub', rampA, rampB, [featureA, featureB], { r: 220, g: 0, b: 0, a: 1 }); + }); }); describe('mod', () => { - test('mod', 0, 1, 0); - test('mod', 2, 1, 0); - test('mod', 2, 2, 0); - test('mod', 6, 4, 2); - test('mod', -6, 4, -2); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 8, c: 4 }; + + testEval('mod', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('mod', 2, s.prop('b'), [featureB], 0); + testEval('mod', s.prop('b'), 2, [featureB], 0); + testEval('mod', s.prop('a'), s.prop('c'), [featureC], 0); + }); }); describe('pow', () => { - test('pow', 0, 0, 1); - test('pow', 0, 1, 0); - test('pow', 2, 2, 4); - test('pow', -2, 2, 4); - test('pow', -2, -3, -0.125); - }); - - describe('gt', () => { - test('gt', 0, 0, 0); - test('gt', 0, 1, 0); - test('gt', 1, 0, 1); - test('gt', 2, 2, 0); - test('gt', 2, 3, 0); - test('gt', 3, 2, 1); - test('gt', -3, 2, 0); - }); - - describe('gte', () => { - test('gte', 0, 0, 1); - test('gte', 0, 1, 0); - test('gte', 1, 0, 1); - test('gte', 2, 2, 1); - test('gte', 2, 3, 0); - test('gte', 3, 2, 1); - test('gte', -3, 2, 0); - }); - - describe('lt', () => { - test('lt', 0, 0, 0); - test('lt', 0, 1, 1); - test('lt', 1, 0, 0); - test('lt', 2, 2, 0); - test('lt', 2, 3, 1); - test('lt', 3, 2, 0); - test('lt', -3, 2, 1); - }); - - describe('lte', () => { - test('lte', 0, 0, 1); - test('lte', 0, 1, 1); - test('lte', 1, 0, 0); - test('lte', 2, 2, 1); - test('lte', 2, 3, 1); - test('lte', 3, 2, 0); - test('lte', -3, 2, 1); - }); - - describe('eq', () => { - test('eq', 0, 0, 1); - test('eq', 0, 1, 0); - test('eq', 1, 0, 0); - test('eq', 2, 2, 1); - test('eq', 2, 3, 0); - }); - - describe('neq', () => { - test('neq', 0, 0, 0); - test('neq', 0, 1, 1); - test('neq', 1, 0, 1); - test('neq', 2, 2, 0); - test('neq', 2, 3, 1); - }); - - function test (fn, param1, param2, expected) { - it(`${fn}(${param1}, ${param2}) should return ${expected}`, () => { - let actual = s[fn](param1, param2).value; - expect(actual).toEqual(expected); - actual = s[fn](param1, param2).value; - expect(actual).toEqual(expected); + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 1 }; + const featureB = { b: 2 }; + const featureC = { a: 8, c: 4 }; + + testEval('pow', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('pow', 2, s.prop('b'), [featureB], 4); + testEval('pow', s.prop('b'), 2, [featureB], 4); + testEval('pow', s.prop('a'), s.prop('c'), [featureC], 4096); }); - } + }); + + describe('or', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 1 }; + const featureC = { a: 0.5, c: 4 }; + + testEval('or', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('or', 2, s.prop('b'), [featureB], 1); + testEval('or', s.prop('b'), 2, [featureB], 1); + testEval('or', s.prop('a'), s.prop('c'), [featureC], 1); + }); + }); + + describe('and', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: true }; + const featureB = { b: false }; + const featureC = { a: true, c: true }; + + testEval('and', s.prop('a'), s.prop('b'), [featureA, featureB], 0); + testEval('and', s.FALSE, s.prop('b'), [featureB], 0); + testEval('and', s.prop('b'), s.TRUE, [featureB], 0); + testEval('and', s.prop('a'), s.prop('c'), [featureC], 1); + }); + }); + + describe('greaterThan', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 20 }; + + testEval('gt', s.prop('a'), s.prop('b'), [featureA, featureB], 0); + testEval('gt', 15, s.prop('b'), [featureB], 1); + testEval('gt', s.prop('b'), 10, [featureB], 0); + testEval('gt', s.prop('c'), s.prop('a'), [featureC], 1); + }); + }); + + describe('greaterThanOrEqualTo', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 5 }; + + testEval('gte', s.prop('a'), s.prop('b'), [featureA, featureB], 0); + testEval('gte', 15, s.prop('b'), [featureB], 1); + testEval('gte', s.prop('b'), 10, [featureB], 1); + testEval('gte', s.prop('c'), s.prop('a'), [featureC], 1); + }); + }); + + describe('lessThan', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 20 }; + + testEval('lt', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('lt', 15, s.prop('b'), [featureB], 0); + testEval('lt', s.prop('b'), 10, [featureB], 0); + testEval('lt', s.prop('c'), s.prop('a'), [featureC], 0); + }); + }); + + describe('lessThanOrEqualTo', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 20 }; + + testEval('lte', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('lte', 15, s.prop('b'), [featureB], 0); + testEval('lte', s.prop('b'), 10, [featureB], 1); + testEval('lte', s.prop('c'), s.prop('a'), [featureC], 0); + }); + }); + + describe('equals', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 5 }; + + testEval('eq', s.prop('a'), s.prop('b'), [featureA, featureB], 0); + testEval('eq', 15, s.prop('b'), [featureB], 0); + testEval('eq', s.prop('b'), 10, [featureB], 1); + testEval('eq', s.prop('c'), s.prop('a'), [featureC], 1); + }); + + describe('CATEGORIES_TO_NUMBER', () => { + const featureA = { a: 'a' }; + const featureB = { b: 'b' }; + const featureC = { a: 'a', c: 'c' }; + + testEval('eq', s.prop('a'), s.prop('b'), [featureA, featureB], 0); + testEval('eq', 'c', s.prop('b'), [featureB], 0); + testEval('eq', s.prop('b'), 'b', [featureB], 1); + testEval('eq', s.prop('c'), s.prop('a'), [featureC], 0); + }); + }); + + describe('notEquals', () => { + describe('NUMBERS_TO_NUMBER', () => { + const featureA = { a: 0 }; + const featureB = { b: 10 }; + const featureC = { a: 5, c: 5 }; + + testEval('neq', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('neq', 15, s.prop('b'), [featureB], 1); + testEval('neq', s.prop('b'), 10, [featureB], 0); + testEval('neq', s.prop('c'), s.prop('a'), [featureC], 0); + }); + + describe('CATEGORIES_TO_NUMBER', () => { + const featureA = { a: 'a' }; + const featureB = { b: 'b' }; + const featureC = { a: 'a', c: 'c' }; + + testEval('neq', s.prop('a'), s.prop('b'), [featureA, featureB], 1); + testEval('neq', 'c', s.prop('b'), [featureB], 1); + testEval('neq', s.prop('b'), 'b', [featureB], 0); + testEval('neq', s.prop('c'), s.prop('a'), [featureC], 1); + }); + }); }); });