UNPKG

cesium

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,128 lines (1,034 loc) 79.2 kB
import Cartesian2 from '../Core/Cartesian2.js'; import Cartesian3 from '../Core/Cartesian3.js'; import Cartesian4 from '../Core/Cartesian4.js'; import Check from '../Core/Check.js'; import Color from '../Core/Color.js'; import defined from '../Core/defined.js'; import DeveloperError from '../Core/DeveloperError.js'; import CesiumMath from '../Core/Math.js'; import RuntimeError from '../Core/RuntimeError.js'; import jsep from '../ThirdParty/jsep.js'; import ExpressionNodeType from './ExpressionNodeType.js'; /** * An expression for a style applied to a {@link Cesium3DTileset}. * <p> * Evaluates an expression defined using the * {@link https://github.com/CesiumGS/3d-tiles/tree/master/specification/Styling|3D Tiles Styling language}. * </p> * <p> * Implements the {@link StyleExpression} interface. * </p> * * @alias Expression * @constructor * * @param {String} [expression] The expression defined using the 3D Tiles Styling language. * @param {Object} [defines] Defines in the style. * * @example * var expression = new Cesium.Expression('(regExp("^Chest").test(${County})) && (${YearBuilt} >= 1970)'); * expression.evaluate(feature); // returns true or false depending on the feature's properties * * @example * var expression = new Cesium.Expression('(${Temperature} > 90) ? color("red") : color("white")'); * expression.evaluateColor(feature, result); // returns a Cesium.Color object */ function Expression(expression, defines) { //>>includeStart('debug', pragmas.debug); Check.typeOf.string('expression', expression); //>>includeEnd('debug'); this._expression = expression; expression = replaceDefines(expression, defines); expression = replaceVariables(removeBackslashes(expression)); // customize jsep operators jsep.addBinaryOp('=~', 0); jsep.addBinaryOp('!~', 0); var ast; try { ast = jsep(expression); } catch (e) { throw new RuntimeError(e); } this._runtimeAst = createRuntimeAst(this, ast); } Object.defineProperties(Expression.prototype, { /** * Gets the expression defined in the 3D Tiles Styling language. * * @memberof Expression.prototype * * @type {String} * @readonly * * @default undefined */ expression : { get : function() { return this._expression; } } }); // Scratch storage manager while evaluating deep expressions. // For example, an expression like dot(vec4(${red}), vec4(${green}) * vec4(${blue}) requires 3 scratch Cartesian4's var scratchStorage = { arrayIndex : 0, arrayArray : [[]], cartesian2Index : 0, cartesian3Index : 0, cartesian4Index : 0, cartesian2Array : [new Cartesian2()], cartesian3Array : [new Cartesian3()], cartesian4Array : [new Cartesian4()], reset : function() { this.arrayIndex = 0; this.cartesian2Index = 0; this.cartesian3Index = 0; this.cartesian4Index = 0; }, getArray : function() { if (this.arrayIndex >= this.arrayArray.length) { this.arrayArray.push([]); } var array = this.arrayArray[this.arrayIndex++]; array.length = 0; return array; }, getCartesian2 : function() { if (this.cartesian2Index >= this.cartesian2Array.length) { this.cartesian2Array.push(new Cartesian2()); } return this.cartesian2Array[this.cartesian2Index++]; }, getCartesian3 : function() { if (this.cartesian3Index >= this.cartesian3Array.length) { this.cartesian3Array.push(new Cartesian3()); } return this.cartesian3Array[this.cartesian3Index++]; }, getCartesian4 : function() { if (this.cartesian4Index >= this.cartesian4Array.length) { this.cartesian4Array.push(new Cartesian4()); } return this.cartesian4Array[this.cartesian4Index++]; } }; /** * Evaluates the result of an expression, optionally using the provided feature's properties. If the result of * the expression in the * {@link https://github.com/CesiumGS/3d-tiles/tree/master/specification/Styling|3D Tiles Styling language} * is of type <code>Boolean</code>, <code>Number</code>, or <code>String</code>, the corresponding JavaScript * primitive type will be returned. If the result is a <code>RegExp</code>, a Javascript <code>RegExp</code> * object will be returned. If the result is a <code>Cartesian2</code>, <code>Cartesian3</code>, or <code>Cartesian4</code>, * a {@link Cartesian2}, {@link Cartesian3}, or {@link Cartesian4} object will be returned. If the <code>result</code> argument is * a {@link Color}, the {@link Cartesian4} value is converted to a {@link Color} and then returned. * * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. * @param {Object} [result] The object onto which to store the result. * @returns {Boolean|Number|String|RegExp|Cartesian2|Cartesian3|Cartesian4|Color} The result of evaluating the expression. */ Expression.prototype.evaluate = function(feature, result) { scratchStorage.reset(); var value = this._runtimeAst.evaluate(feature); if ((result instanceof Color) && (value instanceof Cartesian4)) { return Color.fromCartesian4(value, result); } if ((value instanceof Cartesian2) || (value instanceof Cartesian3) || (value instanceof Cartesian4)) { return value.clone(result); } return value; }; /** * Evaluates the result of a Color expression, optionally using the provided feature's properties. * <p> * This is equivalent to {@link Expression#evaluate} but always returns a {@link Color} object. * </p> * * @param {Cesium3DTileFeature} feature The feature whose properties may be used as variables in the expression. * @param {Color} [result] The object in which to store the result * @returns {Color} The modified result parameter or a new Color instance if one was not provided. */ Expression.prototype.evaluateColor = function(feature, result) { scratchStorage.reset(); var color = this._runtimeAst.evaluate(feature); return Color.fromCartesian4(color, result); }; /** * Gets the shader function for this expression. * Returns undefined if the shader function can't be generated from this expression. * * @param {String} functionName Name to give to the generated function. * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. * @param {String} returnType The return type of the generated function. * * @returns {String} The shader function. * * @private */ Expression.prototype.getShaderFunction = function(functionName, attributePrefix, shaderState, returnType) { var shaderExpression = this.getShaderExpression(attributePrefix, shaderState); shaderExpression = returnType + ' ' + functionName + '() \n' + '{ \n' + ' return ' + shaderExpression + '; \n' + '} \n'; return shaderExpression; }; /** * Gets the shader expression for this expression. * Returns undefined if the shader expression can't be generated from this expression. * * @param {String} attributePrefix Prefix that is added to any variable names to access vertex attributes. * @param {Object} shaderState Stores information about the generated shader function, including whether it is translucent. * * @returns {String} The shader expression. * * @private */ Expression.prototype.getShaderExpression = function(attributePrefix, shaderState) { return this._runtimeAst.getShaderExpression(attributePrefix, shaderState); }; var unaryOperators = ['!', '-', '+']; var binaryOperators = ['+', '-', '*', '/', '%', '===', '!==', '>', '>=', '<', '<=', '&&', '||', '!~', '=~']; var variableRegex = /\${(.*?)}/g; // Matches ${variable_name} var backslashRegex = /\\/g; var backslashReplacement = '@#%'; var replacementRegex = /@#%/g; var scratchColor = new Color(); var unaryFunctions = { abs : getEvaluateUnaryComponentwise(Math.abs), sqrt : getEvaluateUnaryComponentwise(Math.sqrt), cos : getEvaluateUnaryComponentwise(Math.cos), sin : getEvaluateUnaryComponentwise(Math.sin), tan : getEvaluateUnaryComponentwise(Math.tan), acos : getEvaluateUnaryComponentwise(Math.acos), asin : getEvaluateUnaryComponentwise(Math.asin), atan : getEvaluateUnaryComponentwise(Math.atan), radians : getEvaluateUnaryComponentwise(CesiumMath.toRadians), degrees : getEvaluateUnaryComponentwise(CesiumMath.toDegrees), sign : getEvaluateUnaryComponentwise(CesiumMath.sign), floor : getEvaluateUnaryComponentwise(Math.floor), ceil : getEvaluateUnaryComponentwise(Math.ceil), round : getEvaluateUnaryComponentwise(Math.round), exp : getEvaluateUnaryComponentwise(Math.exp), exp2 : getEvaluateUnaryComponentwise(exp2), log : getEvaluateUnaryComponentwise(Math.log), log2 : getEvaluateUnaryComponentwise(log2), fract : getEvaluateUnaryComponentwise(fract), length : length, normalize: normalize }; var binaryFunctions = { atan2 : getEvaluateBinaryComponentwise(Math.atan2, false), pow : getEvaluateBinaryComponentwise(Math.pow, false), min : getEvaluateBinaryComponentwise(Math.min, true), max : getEvaluateBinaryComponentwise(Math.max, true), distance : distance, dot : dot, cross : cross }; var ternaryFunctions = { clamp : getEvaluateTernaryComponentwise(CesiumMath.clamp, true), mix : getEvaluateTernaryComponentwise(CesiumMath.lerp, true) }; function fract(number) { return number - Math.floor(number); } function exp2(exponent) { return Math.pow(2.0,exponent); } function log2(number) { return CesiumMath.log2(number); } function getEvaluateUnaryComponentwise(operation) { return function(call, left) { if (typeof left === 'number') { return operation(left); } else if (left instanceof Cartesian2) { return Cartesian2.fromElements(operation(left.x), operation(left.y), scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3) { return Cartesian3.fromElements(operation(left.x), operation(left.y), operation(left.z), scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4) { return Cartesian4.fromElements(operation(left.x), operation(left.y), operation(left.z), operation(left.w), scratchStorage.getCartesian4()); } throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); }; } function getEvaluateBinaryComponentwise(operation, allowScalar) { return function(call, left, right) { if (allowScalar && typeof right === 'number') { if (typeof left === 'number') { return operation(left, right); } else if (left instanceof Cartesian2) { return Cartesian2.fromElements(operation(left.x, right), operation(left.y, right), scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3) { return Cartesian3.fromElements(operation(left.x, right), operation(left.y, right), operation(left.z, right), scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4) { return Cartesian4.fromElements(operation(left.x, right), operation(left.y, right), operation(left.z, right), operation(left.w, right), scratchStorage.getCartesian4()); } } if (typeof left === 'number' && typeof right === 'number') { return operation(left, right); } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { return Cartesian2.fromElements(operation(left.x, right.x), operation(left.y, right.y), scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { return Cartesian3.fromElements(operation(left.x, right.x), operation(left.y, right.y), operation(left.z, right.z), scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { return Cartesian4.fromElements(operation(left.x, right.x), operation(left.y, right.y), operation(left.z, right.z), operation(left.w, right.w), scratchStorage.getCartesian4()); } throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); }; } function getEvaluateTernaryComponentwise(operation, allowScalar) { return function(call, left, right, test) { if (allowScalar && typeof test === 'number') { if (typeof left === 'number' && typeof right === 'number') { return operation(left, right, test); } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { return Cartesian2.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { return Cartesian3.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), operation(left.z, right.z, test), scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { return Cartesian4.fromElements(operation(left.x, right.x, test), operation(left.y, right.y, test), operation(left.z, right.z, test), operation(left.w, right.w, test), scratchStorage.getCartesian4()); } } if (typeof left === 'number' && typeof right === 'number' && typeof test === 'number') { return operation(left, right, test); } else if (left instanceof Cartesian2 && right instanceof Cartesian2 && test instanceof Cartesian2) { return Cartesian2.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3 && right instanceof Cartesian3 && test instanceof Cartesian3) { return Cartesian3.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), operation(left.z, right.z, test.z), scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4 && right instanceof Cartesian4 && test instanceof Cartesian4) { return Cartesian4.fromElements(operation(left.x, right.x, test.x), operation(left.y, right.y, test.y), operation(left.z, right.z, test.z), operation(left.w, right.w, test.w), scratchStorage.getCartesian4()); } throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ', ' + right + ', and ' + test + '.'); }; } function length(call, left) { if (typeof left === 'number') { return Math.abs(left); } else if (left instanceof Cartesian2) { return Cartesian2.magnitude(left); } else if (left instanceof Cartesian3) { return Cartesian3.magnitude(left); } else if (left instanceof Cartesian4) { return Cartesian4.magnitude(left); } throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); } function normalize(call, left) { if (typeof left === 'number') { return 1.0; } else if (left instanceof Cartesian2) { return Cartesian2.normalize(left, scratchStorage.getCartesian2()); } else if (left instanceof Cartesian3) { return Cartesian3.normalize(left, scratchStorage.getCartesian3()); } else if (left instanceof Cartesian4) { return Cartesian4.normalize(left, scratchStorage.getCartesian4()); } throw new RuntimeError('Function "' + call + '" requires a vector or number argument. Argument is ' + left + '.'); } function distance(call, left, right) { if (typeof left === 'number' && typeof right === 'number') { return Math.abs(left - right); } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { return Cartesian2.distance(left, right); } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { return Cartesian3.distance(left, right); } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { return Cartesian4.distance(left, right); } throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); } function dot(call, left, right) { if (typeof left === 'number' && typeof right === 'number') { return left * right; } else if (left instanceof Cartesian2 && right instanceof Cartesian2) { return Cartesian2.dot(left, right); } else if (left instanceof Cartesian3 && right instanceof Cartesian3) { return Cartesian3.dot(left, right); } else if (left instanceof Cartesian4 && right instanceof Cartesian4) { return Cartesian4.dot(left, right); } throw new RuntimeError('Function "' + call + '" requires vector or number arguments of matching types. Arguments are ' + left + ' and ' + right + '.'); } function cross(call, left, right) { if (left instanceof Cartesian3 && right instanceof Cartesian3) { return Cartesian3.cross(left, right, scratchStorage.getCartesian3()); } throw new RuntimeError('Function "' + call + '" requires vec3 arguments. Arguments are ' + left + ' and ' + right + '.'); } function Node(type, value, left, right, test) { this._type = type; this._value = value; this._left = left; this._right = right; this._test = test; this.evaluate = undefined; setEvaluateFunction(this); } function replaceDefines(expression, defines) { if (!defined(defines)) { return expression; } for (var key in defines) { if (defines.hasOwnProperty(key)) { var definePlaceholder = new RegExp('\\$\\{' + key + '\\}', 'g'); var defineReplace = '(' + defines[key] + ')'; if (defined(defineReplace)) { expression = expression.replace(definePlaceholder, defineReplace); } } } return expression; } function removeBackslashes(expression) { return expression.replace(backslashRegex, backslashReplacement); } function replaceBackslashes(expression) { return expression.replace(replacementRegex, '\\'); } function replaceVariables(expression) { var exp = expression; var result = ''; var i = exp.indexOf('${'); while (i >= 0) { // Check if string is inside quotes var openSingleQuote = exp.indexOf('\''); var openDoubleQuote = exp.indexOf('"'); var closeQuote; if (openSingleQuote >= 0 && openSingleQuote < i) { closeQuote = exp.indexOf('\'', openSingleQuote + 1); result += exp.substr(0, closeQuote + 1); exp = exp.substr(closeQuote + 1); i = exp.indexOf('${'); } else if (openDoubleQuote >= 0 && openDoubleQuote < i) { closeQuote = exp.indexOf('"', openDoubleQuote + 1); result += exp.substr(0, closeQuote + 1); exp = exp.substr(closeQuote + 1); i = exp.indexOf('${'); } else { result += exp.substr(0, i); var j = exp.indexOf('}'); if (j < 0) { throw new RuntimeError('Unmatched {.'); } result += 'czm_' + exp.substr(i + 2, j - (i + 2)); exp = exp.substr(j + 1); i = exp.indexOf('${'); } } result += exp; return result; } function parseLiteral(ast) { var type = typeof ast.value; if (ast.value === null) { return new Node(ExpressionNodeType.LITERAL_NULL, null); } else if (type === 'boolean') { return new Node(ExpressionNodeType.LITERAL_BOOLEAN, ast.value); } else if (type === 'number') { return new Node(ExpressionNodeType.LITERAL_NUMBER, ast.value); } else if (type === 'string') { if (ast.value.indexOf('${') >= 0) { return new Node(ExpressionNodeType.VARIABLE_IN_STRING, ast.value); } return new Node(ExpressionNodeType.LITERAL_STRING, replaceBackslashes(ast.value)); } } function parseCall(expression, ast) { var args = ast.arguments; var argsLength = args.length; var call; var val, left, right; // Member function calls if (ast.callee.type === 'MemberExpression') { call = ast.callee.property.name; var object = ast.callee.object; if (call === 'test' || call === 'exec') { // Make sure this is called on a valid type if (object.callee.name !== 'regExp') { throw new RuntimeError(call + ' is not a function.'); } if (argsLength === 0) { if (call === 'test') { return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); } return new Node(ExpressionNodeType.LITERAL_NULL, null); } left = createRuntimeAst(expression, object); right = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.FUNCTION_CALL, call, left, right); } else if (call === 'toString') { val = createRuntimeAst(expression, object); return new Node(ExpressionNodeType.FUNCTION_CALL, call, val); } throw new RuntimeError('Unexpected function call "' + call + '".'); } // Non-member function calls call = ast.callee.name; if (call === 'color') { if (argsLength === 0) { return new Node(ExpressionNodeType.LITERAL_COLOR, call); } val = createRuntimeAst(expression, args[0]); if (defined(args[1])) { var alpha = createRuntimeAst(expression, args[1]); return new Node(ExpressionNodeType.LITERAL_COLOR, call, [val, alpha]); } return new Node(ExpressionNodeType.LITERAL_COLOR, call, [val]); } else if (call === 'rgb' || call === 'hsl') { if (argsLength < 3) { throw new RuntimeError(call + ' requires three arguments.'); } val = [ createRuntimeAst(expression, args[0]), createRuntimeAst(expression, args[1]), createRuntimeAst(expression, args[2]) ]; return new Node(ExpressionNodeType.LITERAL_COLOR, call, val); } else if (call === 'rgba' || call === 'hsla') { if (argsLength < 4) { throw new RuntimeError(call + ' requires four arguments.'); } val = [ createRuntimeAst(expression, args[0]), createRuntimeAst(expression, args[1]), createRuntimeAst(expression, args[2]), createRuntimeAst(expression, args[3]) ]; return new Node(ExpressionNodeType.LITERAL_COLOR, call, val); } else if (call === 'vec2' || call === 'vec3' || call === 'vec4') { // Check for invalid constructors at evaluation time val = new Array(argsLength); for (var i = 0; i < argsLength; ++i) { val[i] = createRuntimeAst(expression, args[i]); } return new Node(ExpressionNodeType.LITERAL_VECTOR, call, val); } else if (call === 'isNaN' || call === 'isFinite') { if (argsLength === 0) { if (call === 'isNaN') { return new Node(ExpressionNodeType.LITERAL_BOOLEAN, true); } return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (call === 'isExactClass' || call === 'isClass') { if (argsLength < 1 || argsLength > 1) { throw new RuntimeError(call + ' requires exactly one argument.'); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (call === 'getExactClassName') { if (argsLength > 0) { throw new RuntimeError(call + ' does not take any argument.'); } return new Node(ExpressionNodeType.UNARY, call); } else if (defined(unaryFunctions[call])) { if (argsLength !== 1) { throw new RuntimeError(call + ' requires exactly one argument.'); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (defined(binaryFunctions[call])) { if (argsLength !== 2) { throw new RuntimeError(call + ' requires exactly two arguments.'); } left = createRuntimeAst(expression, args[0]); right = createRuntimeAst(expression, args[1]); return new Node(ExpressionNodeType.BINARY, call, left, right); } else if (defined(ternaryFunctions[call])) { if (argsLength !== 3) { throw new RuntimeError(call + ' requires exactly three arguments.'); } left = createRuntimeAst(expression, args[0]); right = createRuntimeAst(expression, args[1]); var test = createRuntimeAst(expression, args[2]); return new Node(ExpressionNodeType.TERNARY, call, left, right, test); } else if (call === 'Boolean') { if (argsLength === 0) { return new Node(ExpressionNodeType.LITERAL_BOOLEAN, false); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (call === 'Number') { if (argsLength === 0) { return new Node(ExpressionNodeType.LITERAL_NUMBER, 0); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (call === 'String') { if (argsLength === 0) { return new Node(ExpressionNodeType.LITERAL_STRING, ''); } val = createRuntimeAst(expression, args[0]); return new Node(ExpressionNodeType.UNARY, call, val); } else if (call === 'regExp') { return parseRegex(expression, ast); } throw new RuntimeError('Unexpected function call "' + call + '".'); } function parseRegex(expression, ast) { var args = ast.arguments; // no arguments, return default regex if (args.length === 0) { return new Node(ExpressionNodeType.LITERAL_REGEX, new RegExp()); } var pattern = createRuntimeAst(expression, args[0]); var exp; // optional flag argument supplied if (args.length > 1) { var flags = createRuntimeAst(expression, args[1]); if (isLiteralType(pattern) && isLiteralType(flags)) { try { exp = new RegExp(replaceBackslashes(String(pattern._value)), flags._value); } catch (e) { throw new RuntimeError(e); } return new Node(ExpressionNodeType.LITERAL_REGEX, exp); } return new Node(ExpressionNodeType.REGEX, pattern, flags); } // only pattern argument supplied if (isLiteralType(pattern)) { try { exp = new RegExp(replaceBackslashes(String(pattern._value))); } catch (e) { throw new RuntimeError(e); } return new Node(ExpressionNodeType.LITERAL_REGEX, exp); } return new Node(ExpressionNodeType.REGEX, pattern); } function parseKeywordsAndVariables(ast) { if (isVariable(ast.name)) { var name = getPropertyName(ast.name); if (name.substr(0, 8) === 'tiles3d_') { return new Node(ExpressionNodeType.BUILTIN_VARIABLE, name); } return new Node(ExpressionNodeType.VARIABLE, name); } else if (ast.name === 'NaN') { return new Node(ExpressionNodeType.LITERAL_NUMBER, NaN); } else if (ast.name === 'Infinity') { return new Node(ExpressionNodeType.LITERAL_NUMBER, Infinity); } else if (ast.name === 'undefined') { return new Node(ExpressionNodeType.LITERAL_UNDEFINED, undefined); } throw new RuntimeError(ast.name + ' is not defined.'); } function parseMathConstant(ast) { var name = ast.property.name; if (name === 'PI') { return new Node(ExpressionNodeType.LITERAL_NUMBER, Math.PI); } else if (name === 'E') { return new Node(ExpressionNodeType.LITERAL_NUMBER, Math.E); } } function parseNumberConstant(ast) { var name = ast.property.name; if (name === 'POSITIVE_INFINITY') { return new Node(ExpressionNodeType.LITERAL_NUMBER, Number.POSITIVE_INFINITY); } } function parseMemberExpression(expression, ast) { if (ast.object.name === 'Math') { return parseMathConstant(ast); } else if (ast.object.name === 'Number') { return parseNumberConstant(ast); } var val; var obj = createRuntimeAst(expression, ast.object); if (ast.computed) { val = createRuntimeAst(expression, ast.property); return new Node(ExpressionNodeType.MEMBER, 'brackets', obj, val); } val = new Node(ExpressionNodeType.LITERAL_STRING, ast.property.name); return new Node(ExpressionNodeType.MEMBER, 'dot', obj, val); } function isLiteralType(node) { return (node._type >= ExpressionNodeType.LITERAL_NULL); } function isVariable(name) { return (name.substr(0, 4) === 'czm_'); } function getPropertyName(variable) { return variable.substr(4); } function createRuntimeAst(expression, ast) { var node; var op; var left; var right; if (ast.type === 'Literal') { node = parseLiteral(ast); } else if (ast.type === 'CallExpression') { node = parseCall(expression, ast); } else if (ast.type === 'Identifier') { node = parseKeywordsAndVariables(ast); } else if (ast.type === 'UnaryExpression') { op = ast.operator; var child = createRuntimeAst(expression, ast.argument); if (unaryOperators.indexOf(op) > -1) { node = new Node(ExpressionNodeType.UNARY, op, child); } else { throw new RuntimeError('Unexpected operator "' + op + '".'); } } else if (ast.type === 'BinaryExpression') { op = ast.operator; left = createRuntimeAst(expression, ast.left); right = createRuntimeAst(expression, ast.right); if (binaryOperators.indexOf(op) > -1) { node = new Node(ExpressionNodeType.BINARY, op, left, right); } else { throw new RuntimeError('Unexpected operator "' + op + '".'); } } else if (ast.type === 'LogicalExpression') { op = ast.operator; left = createRuntimeAst(expression, ast.left); right = createRuntimeAst(expression, ast.right); if (binaryOperators.indexOf(op) > -1) { node = new Node(ExpressionNodeType.BINARY, op, left, right); } } else if (ast.type === 'ConditionalExpression') { var test = createRuntimeAst(expression, ast.test); left = createRuntimeAst(expression, ast.consequent); right = createRuntimeAst(expression, ast.alternate); node = new Node(ExpressionNodeType.CONDITIONAL, '?', left, right, test); } else if (ast.type === 'MemberExpression') { node = parseMemberExpression(expression, ast); } else if (ast.type === 'ArrayExpression') { var val = []; for (var i = 0; i < ast.elements.length; i++) { val[i] = createRuntimeAst(expression, ast.elements[i]); } node = new Node(ExpressionNodeType.ARRAY, val); } else if (ast.type === 'Compound') { // empty expression or multiple expressions throw new RuntimeError('Provide exactly one expression.'); } else { throw new RuntimeError('Cannot parse expression.'); } return node; } function setEvaluateFunction(node) { if (node._type === ExpressionNodeType.CONDITIONAL) { node.evaluate = node._evaluateConditional; } else if (node._type === ExpressionNodeType.FUNCTION_CALL) { if (node._value === 'test') { node.evaluate = node._evaluateRegExpTest; } else if (node._value === 'exec') { node.evaluate = node._evaluateRegExpExec; } else if (node._value === 'toString') { node.evaluate = node._evaluateToString; } } else if (node._type === ExpressionNodeType.UNARY) { if (node._value === '!') { node.evaluate = node._evaluateNot; } else if (node._value === '-') { node.evaluate = node._evaluateNegative; } else if (node._value === '+') { node.evaluate = node._evaluatePositive; } else if (node._value === 'isNaN') { node.evaluate = node._evaluateNaN; } else if (node._value === 'isFinite') { node.evaluate = node._evaluateIsFinite; } else if (node._value === 'isExactClass') { node.evaluate = node._evaluateIsExactClass; } else if (node._value === 'isClass') { node.evaluate = node._evaluateIsClass; } else if (node._value === 'getExactClassName') { node.evaluate = node._evaluateGetExactClassName; } else if (node._value === 'Boolean') { node.evaluate = node._evaluateBooleanConversion; } else if (node._value === 'Number') { node.evaluate = node._evaluateNumberConversion; } else if (node._value === 'String') { node.evaluate = node._evaluateStringConversion; } else if (defined(unaryFunctions[node._value])) { node.evaluate = getEvaluateUnaryFunction(node._value); } } else if (node._type === ExpressionNodeType.BINARY) { if (node._value === '+') { node.evaluate = node._evaluatePlus; } else if (node._value === '-') { node.evaluate = node._evaluateMinus; } else if (node._value === '*') { node.evaluate = node._evaluateTimes; } else if (node._value === '/') { node.evaluate = node._evaluateDivide; } else if (node._value === '%') { node.evaluate = node._evaluateMod; } else if (node._value === '===') { node.evaluate = node._evaluateEqualsStrict; } else if (node._value === '!==') { node.evaluate = node._evaluateNotEqualsStrict; } else if (node._value === '<') { node.evaluate = node._evaluateLessThan; } else if (node._value === '<=') { node.evaluate = node._evaluateLessThanOrEquals; } else if (node._value === '>') { node.evaluate = node._evaluateGreaterThan; } else if (node._value === '>=') { node.evaluate = node._evaluateGreaterThanOrEquals; } else if (node._value === '&&') { node.evaluate = node._evaluateAnd; } else if (node._value === '||') { node.evaluate = node._evaluateOr; } else if (node._value === '=~') { node.evaluate = node._evaluateRegExpMatch; } else if (node._value === '!~') { node.evaluate = node._evaluateRegExpNotMatch; } else if (defined(binaryFunctions[node._value])) { node.evaluate = getEvaluateBinaryFunction(node._value); } } else if (node._type === ExpressionNodeType.TERNARY) { node.evaluate = getEvaluateTernaryFunction(node._value); } else if (node._type === ExpressionNodeType.MEMBER) { if (node._value === 'brackets') { node.evaluate = node._evaluateMemberBrackets; } else { node.evaluate = node._evaluateMemberDot; } } else if (node._type === ExpressionNodeType.ARRAY) { node.evaluate = node._evaluateArray; } else if (node._type === ExpressionNodeType.VARIABLE) { node.evaluate = node._evaluateVariable; } else if (node._type === ExpressionNodeType.VARIABLE_IN_STRING) { node.evaluate = node._evaluateVariableString; } else if (node._type === ExpressionNodeType.LITERAL_COLOR) { node.evaluate = node._evaluateLiteralColor; } else if (node._type === ExpressionNodeType.LITERAL_VECTOR) { node.evaluate = node._evaluateLiteralVector; } else if (node._type === ExpressionNodeType.LITERAL_STRING) { node.evaluate = node._evaluateLiteralString; } else if (node._type === ExpressionNodeType.REGEX) { node.evaluate = node._evaluateRegExp; } else if (node._type === ExpressionNodeType.BUILTIN_VARIABLE) { if (node._value === 'tiles3d_tileset_time') { node.evaluate = evaluateTilesetTime; } } else { node.evaluate = node._evaluateLiteral; } } function evaluateTilesetTime(feature) { if (!defined(feature)) { return 0.0; } return feature.content.tileset.timeSinceLoad; } function getEvaluateUnaryFunction(call) { var evaluate = unaryFunctions[call]; return function(feature) { var left = this._left.evaluate(feature); return evaluate(call, left); }; } function getEvaluateBinaryFunction(call) { var evaluate = binaryFunctions[call]; return function(feature) { var left = this._left.evaluate(feature); var right = this._right.evaluate(feature); return evaluate(call, left, right); }; } function getEvaluateTernaryFunction(call) { var evaluate = ternaryFunctions[call]; return function(feature) { var left = this._left.evaluate(feature); var right = this._right.evaluate(feature); var test = this._test.evaluate(feature); return evaluate(call, left, right, test); }; } function getFeatureProperty(feature, name) { // Returns undefined if the feature is not defined or the property name is not defined for that feature if (defined(feature)) { return feature.getProperty(name); } } Node.prototype._evaluateLiteral = function() { return this._value; }; Node.prototype._evaluateLiteralColor = function(feature) { var color = scratchColor; var args = this._left; if (this._value === 'color') { if (!defined(args)) { Color.fromBytes(255, 255, 255, 255, color); } else if (args.length > 1) { Color.fromCssColorString(args[0].evaluate(feature), color); color.alpha = args[1].evaluate(feature); } else { Color.fromCssColorString(args[0].evaluate(feature), color); } } else if (this._value === 'rgb') { Color.fromBytes( args[0].evaluate(feature), args[1].evaluate(feature), args[2].evaluate(feature), 255, color); } else if (this._value === 'rgba') { // convert between css alpha (0 to 1) and cesium alpha (0 to 255) var a = args[3].evaluate(feature) * 255; Color.fromBytes( args[0].evaluate(feature), args[1].evaluate(feature), args[2].evaluate(feature), a, color); } else if (this._value === 'hsl') { Color.fromHsl( args[0].evaluate(feature), args[1].evaluate(feature), args[2].evaluate(feature), 1.0, color); } else if (this._value === 'hsla') { Color.fromHsl( args[0].evaluate(feature), args[1].evaluate(feature), args[2].evaluate(feature), args[3].evaluate(feature), color); } return Cartesian4.fromColor(color, scratchStorage.getCartesian4()); }; Node.prototype._evaluateLiteralVector = function(feature) { // Gather the components that make up the vector, which includes components from interior vectors. // For example vec3(1, 2, 3) or vec3(vec2(1, 2), 3) are both valid. // // If the number of components does not equal the vector's size, then a RuntimeError is thrown - with two exceptions: // 1. A vector may be constructed from a larger vector and drop the extra components. // 2. A vector may be constructed from a single component - vec3(1) will become vec3(1, 1, 1). // // Examples of invalid constructors include: // vec4(1, 2) // not enough components // vec3(vec2(1, 2)) // not enough components // vec3(1, 2, 3, 4) // too many components // vec2(vec4(1), 1) // too many components var components = scratchStorage.getArray(); var call = this._value; var args = this._left; var argsLength = args.length; for (var i = 0; i < argsLength; ++i) { var value = args[i].evaluate(feature); if (typeof value === 'number') { components.push(value); } else if (value instanceof Cartesian2) { components.push(value.x, value.y); } else if (value instanceof Cartesian3) { components.push(value.x, value.y, value.z); } else if (value instanceof Cartesian4) { components.push(value.x, value.y, value.z, value.w); } else { throw new RuntimeError(call + ' argument must be a vector or number. Argument is ' + value + '.'); } } var componentsLength = components.length; var vectorLength = parseInt(call.charAt(3)); if (componentsLength === 0) { throw new RuntimeError('Invalid ' + call + ' constructor. No valid arguments.'); } else if ((componentsLength < vectorLength) && (componentsLength > 1)) { throw new RuntimeError('Invalid ' + call + ' constructor. Not enough arguments.'); } else if ((componentsLength > vectorLength) && (argsLength > 1)) { throw new RuntimeError('Invalid ' + call + ' constructor. Too many arguments.'); } if (componentsLength === 1) { // Add the same component 3 more times var component = components[0]; components.push(component, component, component); } if (call === 'vec2') { return Cartesian2.fromArray(components, 0, scratchStorage.getCartesian2()); } else if (call === 'vec3') { return Cartesian3.fromArray(components, 0, scratchStorage.getCartesian3()); } else if (call === 'vec4') { return Cartesian4.fromArray(components, 0, scratchStorage.getCartesian4()); } }; Node.prototype._evaluateLiteralString = function() { return this._value; }; Node.prototype._evaluateVariableString = function(feature) { var result = this._value; var match = variableRegex.exec(result); while (match !== null) { var placeholder = match[0]; var variableName = match[1]; var property = getFeatureProperty(feature, variableName); if (!defined(property)) { property = ''; } result = result.replace(placeholder, property); match = variableRegex.exec(result); } return result; }; Node.prototype._evaluateVariable = function(feature) { // evaluates to undefined if the property name is not defined for that feature return getFeatureProperty(feature, this._value); }; function checkFeature (ast) { return (ast._value === 'feature'); } // PERFORMANCE_IDEA: Determine if parent property needs to be computed before runtime Node.prototype._evaluateMemberDot = function(feature) { if (checkFeature(this._left)) { return getFeatureProperty(feature, this._right.evaluate(feature)); } var property = this._left.evaluate(feature); if (!defined(property)) { return undefined; } var member = this._right.evaluate(feature); if ((property instanceof Cartesian2) || (property instanceof Cartesian3) || (property instanceof Cartesian4)) { // Vector components may be accessed with .r, .g, .b, .a and implicitly with .x, .y, .z, .w if (member === 'r') { return property.x; } else if (member === 'g') { return property.y; } else if (member === 'b') { return property.z; } else if (member === 'a') { return property.w; } } return property[member]; }; Node.prototype._evaluateMemberBrackets = function(feature) { if (checkFeature(this._left)) { return getFeatureProperty(feature, this._right.evaluate(feature)); } var property = this._left.evaluate(feature); if (!defined(property)) { return undefined; } var member = this._right.evaluate(feature); if ((property instanceof Cartesian2) || (property instanceof Cartesian3) || (property instanceof Cartesian4)) { // Vector components may be accessed with [0][1][2][3], ['r']['g']['b']['a'] and implicitly with ['x']['y']['z']['w'] // For Cartesian2 and Cartesian3 out-of-range components will just return undefined if (member === 0 || member === 'r') { return property.x; } else if (member === 1 || member === 'g') { return property.y; } else if (member === 2 || member === 'b') { return property.z; } else if (member === 3 || member === 'a') { return property.w; } } return property[member]; }; Node.prototype._evaluateArray = function(feature) { var array = []; for (var i = 0; i < this._value.length; i++) { array[i] = this._value[i].evaluate(feature); } return array; }; // PERFORMANCE_IDEA: Have "fast path" functions that deal only with specific types // that we can assign if we know the types before runtime Node.prototype._evaluateNot = function(feature) { var left = this._left.evaluate(feature); if (typeof left !== 'boolean') { throw new RuntimeError('Operator "!" requires a boolean argument. Argument is ' + left + '.'); }