@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,500 lines (1,373 loc) • 72.1 kB
JavaScript
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 "jsep";
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/main/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
* const expression = new Cesium.Expression('(regExp("^Chest").test(${County})) && (${YearBuilt} >= 1970)');
* expression.evaluate(feature); // returns true or false depending on the feature's properties
*
* @example
* const 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);
let 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
const 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([]);
}
const 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/main/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();
const 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();
const 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} functionSignature Signature of the generated function.
* @param {object} variableSubstitutionMap Maps variable names to shader variable names.
* @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 (
functionSignature,
variableSubstitutionMap,
shaderState,
returnType,
) {
let shaderExpression = this.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
shaderExpression =
`${returnType} ${functionSignature}\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 {object} variableSubstitutionMap Maps variable names to shader variable names.
* @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 (
variableSubstitutionMap,
shaderState,
) {
return this._runtimeAst.getShaderExpression(
variableSubstitutionMap,
shaderState,
);
};
/**
* Gets the variables used by the expression.
*
* @returns {string[]} The variables used by the expression.
*
* @private
*/
Expression.prototype.getVariables = function () {
let variables = [];
this._runtimeAst.getVariables(variables);
// Remove duplicates
variables = variables.filter(function (variable, index, variables) {
return variables.indexOf(variable) === index;
});
return variables;
};
const unaryOperators = ["!", "-", "+"];
const binaryOperators = [
"+",
"-",
"*",
"/",
"%",
"===",
"!==",
">",
">=",
"<",
"<=",
"&&",
"||",
"!~",
"=~",
];
const variableRegex = /\${(.*?)}/g; // Matches ${variable_name}
const backslashRegex = /\\/g;
const backslashReplacement = "@#%";
const replacementRegex = /@#%/g;
const scratchColor = new Color();
const 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,
};
const 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,
};
const 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 (const key in defines) {
if (defines.hasOwnProperty(key)) {
const definePlaceholder = new RegExp(`\\$\\{${key}\\}`, "g");
const 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) {
let exp = expression;
let result = "";
let i = exp.indexOf("${");
while (i >= 0) {
// Check if string is inside quotes
const openSingleQuote = exp.indexOf("'");
const openDoubleQuote = exp.indexOf('"');
let 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);
const 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) {
const 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) {
const args = ast.arguments;
const argsLength = args.length;
let call;
let val, left, right;
// Member function calls
if (ast.callee.type === "MemberExpression") {
call = ast.callee.property.name;
const object = ast.callee.object;
if (call === "test" || call === "exec") {
// Make sure this is called on a valid type
if (!defined(object.callee) || 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])) {
const 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 (let 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]);
const 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) {
const args = ast.arguments;
// no arguments, return default regex
if (args.length === 0) {
return new Node(ExpressionNodeType.LITERAL_REGEX, new RegExp());
}
const pattern = createRuntimeAst(expression, args[0]);
let exp;
// optional flag argument supplied
if (args.length > 1) {
const 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)) {
const 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) {
const 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) {
const 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);
}
let val;
const 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) {
let node;
let op;
let left;
let 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;
const 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") {
const 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") {
const val = [];
for (let 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) {
const evaluate = unaryFunctions[call];
return function (feature) {
const left = this._left.evaluate(feature);
return evaluate(call, left);
};
}
function getEvaluateBinaryFunction(call) {
const evaluate = binaryFunctions[call];
return function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
return evaluate(call, left, right);
};
}
function getEvaluateTernaryFunction(call) {
const evaluate = ternaryFunctions[call];
return function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
const 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.getPropertyInherited(name);
}
}
Node.prototype._evaluateLiteral = function () {
return this._value;
};
Node.prototype._evaluateLiteralColor = function (feature) {
const color = scratchColor;
const 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)
const 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
const components = scratchStorage.getArray();
const call = this._value;
const args = this._left;
const argsLength = args.length;
for (let i = 0; i < argsLength; ++i) {
const 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}.`,
);
}
}
const componentsLength = components.length;
const 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
const 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) {
let result = this._value;
let match = variableRegex.exec(result);
while (match !== null) {
const placeholder = match[0];
const variableName = match[1];
let property = getFeatureProperty(feature, variableName);
if (!defined(property)) {
property = "";
}
result = result.replace(placeholder, property);
variableRegex.lastIndex += property.length - placeholder.length;
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));
}
const property = this._left.evaluate(feature);
if (!defined(property)) {
return undefined;
}
const 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));
}
const property = this._left.evaluate(feature);
if (!defined(property)) {
return undefined;
}
const 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) {
const array = [];
for (let 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) {
const left = this._left.evaluate(feature);
if (typeof left !== "boolean") {
throw new RuntimeError(
`Operator "!" requires a boolean argument. Argument is ${left}.`,
);
}
return !left;
};
Node.prototype._evaluateNegative = function (feature) {
const left = this._left.evaluate(feature);
if (left instanceof Cartesian2) {
return Cartesian2.negate(left, scratchStorage.getCartesian2());
} else if (left instanceof Cartesian3) {
return Cartesian3.negate(left, scratchStorage.getCartesian3());
} else if (left instanceof Cartesian4) {
return Cartesian4.negate(left, scratchStorage.getCartesian4());
} else if (typeof left === "number") {
return -left;
}
throw new RuntimeError(
`Operator "-" requires a vector or number argument. Argument is ${left}.`,
);
};
Node.prototype._evaluatePositive = function (feature) {
const left = this._left.evaluate(feature);
if (
!(
left instanceof Cartesian2 ||
left instanceof Cartesian3 ||
left instanceof Cartesian4 ||
typeof left === "number"
)
) {
throw new RuntimeError(
`Operator "+" requires a vector or number argument. Argument is ${left}.`,
);
}
return left;
};
Node.prototype._evaluateLessThan = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (typeof left !== "number" || typeof right !== "number") {
throw new RuntimeError(
`Operator "<" requires number arguments. Arguments are ${left} and ${right}.`,
);
}
return left < right;
};
Node.prototype._evaluateLessThanOrEquals = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (typeof left !== "number" || typeof right !== "number") {
throw new RuntimeError(
`Operator "<=" requires number arguments. Arguments are ${left} and ${right}.`,
);
}
return left <= right;
};
Node.prototype._evaluateGreaterThan = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (typeof left !== "number" || typeof right !== "number") {
throw new RuntimeError(
`Operator ">" requires number arguments. Arguments are ${left} and ${right}.`,
);
}
return left > right;
};
Node.prototype._evaluateGreaterThanOrEquals = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (typeof left !== "number" || typeof right !== "number") {
throw new RuntimeError(
`Operator ">=" requires number arguments. Arguments are ${left} and ${right}.`,
);
}
return left >= right;
};
Node.prototype._evaluateOr = function (feature) {
const left = this._left.evaluate(feature);
if (typeof left !== "boolean") {
throw new RuntimeError(
`Operator "||" requires boolean arguments. First argument is ${left}.`,
);
}
// short circuit the expression
if (left) {
return true;
}
const right = this._right.evaluate(feature);
if (typeof right !== "boolean") {
throw new RuntimeError(
`Operator "||" requires boolean arguments. Second argument is ${right}.`,
);
}
return left || right;
};
Node.prototype._evaluateAnd = function (feature) {
const left = this._left.evaluate(feature);
if (typeof left !== "boolean") {
throw new RuntimeError(
`Operator "&&" requires boolean arguments. First argument is ${left}.`,
);
}
// short circuit the expression
if (!left) {
return false;
}
const right = this._right.evaluate(feature);
if (typeof right !== "boolean") {
throw new RuntimeError(
`Operator "&&" requires boolean arguments. Second argument is ${right}.`,
);
}
return left && right;
};
Node.prototype._evaluatePlus = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (right instanceof Cartesian2 && left instanceof Cartesian2) {
return Cartesian2.add(left, right, scratchStorage.getCartesian2());
} else if (right instanceof Cartesian3 && left instanceof Cartesian3) {
return Cartesian3.add(left, right, scratchStorage.getCartesian3());
} else if (right instanceof Cartesian4 && left instanceof Cartesian4) {
return Cartesian4.add(left, right, scratchStorage.getCartesian4());
} else if (typeof left === "string" || typeof right === "string") {
// If only one argument is a string the other argument calls its toString function.
return left + right;
} else if (typeof left === "number" && typeof right === "number") {
return left + right;
}
throw new RuntimeError(
`Operator "+" requires vector or number arguments of matching types, or at least one string argument. Arguments are ${left} and ${right}.`,
);
};
Node.prototype._evaluateMinus = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
if (right instanceof Cartesian2 && left instanceof Cartesian2) {
return Cartesian2.subtract(left, right, scratchStorage.getCartesian2());
} else if (right instanceof Cartesian3 && left instanceof Cartesian3) {
return Cartesian3.subtract(left, right, scratchStorage.getCartesian3());
} else if (right instanceof Cartesian4 && left instanceof Cartesian4) {
return Cartesian4.subtract(left, right, scratchStorage.getCartesian4());
} else if (typeof left === "number" && typeof right === "number") {
return left - right;
}
throw new RuntimeError(
`Operator "-" requires vector or number arguments of matching types. Arguments are ${left} and ${right}.`,
);
};
Node.prototype._evaluateTimes = function (feature) {
const left = this._left.evaluate(feature);
const right = this._right.evaluate(feature);
i