diff --git a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html index 7cdceb940def..1ecedd0865a2 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html +++ b/Apps/Sandcastle/gallery/3D Tiles Batch Table Hierarchy.html @@ -138,12 +138,14 @@ }); addStyle('Color features by class name', { + "expressions" : { + "suffix" : "regExp('door(.*)').exec(getExactClassName())" + }, "color" : { - "expression" : "regExp('door(.*)').exec(getExactClassName())", "conditions" : [ - ["${expression} === 'knob'", "color('yellow')"], - ["${expression} === ''", "color('lime')"], - ["${expression} === null", "color('gray')"], + ["${suffix} === 'knob'", "color('yellow')"], + ["${suffix} === ''", "color('lime')"], + ["${suffix} === null", "color('gray')"], ["true", "color('blue'"] ] } diff --git a/Source/Scene/Cesium3DTileStyle.js b/Source/Scene/Cesium3DTileStyle.js index fb081757c4e0..f31e855b9e0c 100644 --- a/Source/Scene/Cesium3DTileStyle.js +++ b/Source/Scene/Cesium3DTileStyle.js @@ -63,6 +63,7 @@ define([ this._show = undefined; this._pointSize = undefined; this._meta = undefined; + this._expressions = undefined; this._colorShaderFunction = undefined; this._showShaderFunction = undefined; @@ -108,33 +109,35 @@ define([ var showExpression = defaultValue(styleJson.show, DEFAULT_JSON_BOOLEAN_EXPRESSION); var pointSizeExpression = defaultValue(styleJson.pointSize, DEFAULT_JSON_NUMBER_EXPRESSION); + var expressions = styleJson.expressions; + var color; if (typeof colorExpression === 'string') { - color = new Expression(colorExpression); + color = new Expression(colorExpression, expressions); } else if (defined(colorExpression.conditions)) { - color = new ConditionsExpression(colorExpression); + color = new ConditionsExpression(colorExpression, expressions); } that._color = color; var show; if (typeof showExpression === 'boolean') { - show = new Expression(String(showExpression)); + show = new Expression(String(showExpression), expressions); } else if (typeof showExpression === 'string') { - show = new Expression(showExpression); + show = new Expression(showExpression, expressions); } else if (defined(showExpression.conditions)) { - show = new ConditionsExpression(showExpression); + show = new ConditionsExpression(showExpression, expressions); } that._show = show; var pointSize; if (typeof pointSizeExpression === 'number') { - pointSize = new Expression(String(pointSizeExpression)); + pointSize = new Expression(String(pointSizeExpression), expressions); } else if (typeof pointSizeExpression === 'string') { - pointSize = new Expression(pointSizeExpression); + pointSize = new Expression(pointSizeExpression, expressions); } else if (defined(pointSizeExpression.conditions)) { - pointSize = new ConditionsExpression(pointSizeExpression); + pointSize = new ConditionsExpression(pointSizeExpression, expressions); } that._pointSize = pointSize; @@ -144,7 +147,7 @@ define([ var metaJson = defaultValue(styleJson.meta, defaultValue.EMPTY_OBJECT); for (var property in metaJson) { if (metaJson.hasOwnProperty(property)) { - meta[property] = new Expression(metaJson[property]); + meta[property] = new Expression(metaJson[property], expressions); } } } diff --git a/Source/Scene/ConditionsExpression.js b/Source/Scene/ConditionsExpression.js index 0d4318acae61..c1f646dc9493 100644 --- a/Source/Scene/ConditionsExpression.js +++ b/Source/Scene/ConditionsExpression.js @@ -26,13 +26,10 @@ define([ * @constructor * * @param {Object} [conditionsExpression] The conditions expression defined using the 3D Tiles Styling language. + * @param {Object} [expressions] Additional expressions defined in the style. * * @example - * var expression = new Cesium.Expression({ - * expressions : { - * id : "RegEx('^id_(\d+)$').exec(${name})", - * Area : "${length} * ${height}" - * }, + * var expression = new Cesium.ConditionsExpression({ * conditions : [ * ['${Area} > 10, 'color("#FF0000")'], * ['${id} !== "1"', 'color("#00FF00")'], @@ -43,16 +40,12 @@ define([ * * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} */ - function ConditionsExpression(conditionsExpression) { + function ConditionsExpression(conditionsExpression, expressions) { this._conditionsExpression = clone(conditionsExpression, true); this._conditions = conditionsExpression.conditions; - - // Insert expressions into conditions - // this._expressions has to stay in prototype for specs to keep passing, but it can be removed in the future. - this._expressions = defaultValue(conditionsExpression.expressions, defaultValue.EMPTY_OBJECT); this._runtimeConditions = undefined; - setRuntime(this); + setRuntime(this, expressions); } defineProperties(ConditionsExpression.prototype, { @@ -78,35 +71,18 @@ define([ this.expression = expression; } - function setRuntime(expression) { + function setRuntime(expression, expressions) { var runtimeConditions = []; var conditions = expression._conditions; if (defined(conditions)) { - var expressions = expression._expressions; var length = conditions.length; for (var i = 0; i < length; ++i) { var statement = conditions[i]; var cond = String(statement[0]); var condExpression = String(statement[1]); - - // Loop over all expressions for replacement - for (var key in expressions) { - if (expressions.hasOwnProperty(key)) { - var expressionPlaceholder = new RegExp('\\$\\{' + key + '\\}', 'g'); - var expressionReplace = expressions[key]; - if (expression) { - cond = cond.replace(expressionPlaceholder, expressionReplace); - condExpression = condExpression.replace(expressionPlaceholder, expressionReplace); - } else { - cond = cond.replace(expressionPlaceholder, 'undefined'); - condExpression = condExpression.replace(expressionPlaceholder, 'undefined'); - } - } - } - runtimeConditions.push(new Statement( - new Expression(cond), - new Expression(condExpression) + new Expression(cond, expressions), + new Expression(condExpression, expressions) )); } } diff --git a/Source/Scene/Expression.js b/Source/Scene/Expression.js index 659797fa5237..e87c52acf4ae 100644 --- a/Source/Scene/Expression.js +++ b/Source/Scene/Expression.js @@ -135,6 +135,7 @@ define([ * @constructor * * @param {String} [expression] The expression defined using the 3D Tiles Styling language. + * @param {Object} [expressions] Additional expressions defined in the style. * * @example * var expression = new Cesium.Expression('(regExp("^Chest").test(${County})) && (${YearBuilt} >= 1970)'); @@ -146,7 +147,7 @@ define([ * * @see {@link https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/Styling|3D Tiles Styling language} */ - function Expression(expression) { + function Expression(expression, expressions) { //>>includeStart('debug', pragmas.debug); if (typeof expression !== 'string') { throw new DeveloperError('expression must be a string.'); @@ -154,6 +155,7 @@ define([ //>>includeEnd('debug'); this._expression = expression; + expression = replaceExpressions(expression, expressions); expression = replaceVariables(removeBackslashes(expression)); // customize jsep operators @@ -279,6 +281,24 @@ define([ setEvaluateFunction(this); } + function replaceExpressions(expression, expressions) { + if (!defined(expressions) || (defined(expressions) && expressions.length === 0)) { + return expression; + } + var result = expression; + var match = variableRegex.exec(expression); + while (match !== null) { + var placeholder = match[0]; + var variableName = match[1]; + var expressionReplace = expressions[variableName]; + if (defined(expressionReplace)) { + result = result.replace(placeholder, expressionReplace); + } + match = variableRegex.exec(result); + } + return result; + } + function removeBackslashes(expression) { return expression.replace(backslashRegex, backslashReplacement); } diff --git a/Specs/Scene/Cesium3DTileStyleSpec.js b/Specs/Scene/Cesium3DTileStyleSpec.js index b553b8a063a9..a32dacb25624 100644 --- a/Specs/Scene/Cesium3DTileStyleSpec.js +++ b/Specs/Scene/Cesium3DTileStyleSpec.js @@ -350,15 +350,17 @@ defineSuite([ it('applies show style with complex conditional', function() { var style = new Cesium3DTileStyle({ + "expressions": { + "Height" : "${Height} * 1.01" + }, "show" : { - "expression" : "${Height}", "conditions" : [ - ["(${expression} >= 1.0) && (${expression} < 10.0)", "true"], - ["(${expression} >= 10.0) && (${expression} < 30.0)", "false"], - ["(${expression} >= 30.0) && (${expression} < 50.0)", "true"], - ["(${expression} >= 50.0) && (${expression} < 70.0)", "false"], - ["(${expression} >= 70.0) && (${expression} < 100.0)", "true"], - ["(${expression} >= 100.0)", "false"] + ["(${Height} >= 1.0) && (${Height} < 10.0)", "true"], + ["(${Height} >= 10.0) && (${Height} < 30.0)", "false"], + ["(${Height} >= 30.0) && (${Height} < 50.0)", "true"], + ["(${Height} >= 50.0) && (${Height} < 70.0)", "false"], + ["(${Height} >= 70.0) && (${Height} < 100.0)", "true"], + ["(${Height} >= 100.0)", "false"] ] } }); @@ -507,6 +509,36 @@ defineSuite([ expect(style.pointSize.evaluate(frameState, feature2)).toEqual(3); }); + it('applies with additional expressions', function() { + var style = new Cesium3DTileStyle({ + "expressions" : { + "halfHeight" : "${Height} / 2", + "quarterHeight" : "${Height} / 4", + "halfVolume" : "${volume} / 2" + }, + "color" : { + "conditions" : [ + ["(${halfHeight} >= 25.0)", "color('red')"], + ["(${Height} >= 1.0)", "color('blue')"] + ] + }, + "show" : "(${quarterHeight} >= 20.0)", + "pointSize" : "${halfVolume} + ${halfHeight}", + "meta" : { + "description" : "'Half height is ' + ${halfHeight}" + } + }); + + expect(style.color.evaluateColor(frameState, feature1)).toEqual(Color.RED); + expect(style.color.evaluateColor(frameState, feature2)).toEqual(Color.BLUE); + expect(style.show.evaluate(frameState, feature1)).toEqual(true); + expect(style.show.evaluate(frameState, feature2)).toEqual(false); + expect(style.pointSize.evaluate(frameState, feature1)).toEqual(114); + expect(style.pointSize.evaluate(frameState, feature2)).toEqual(44); + expect(style.meta.description.evaluate(frameState, feature1)).toEqual('Half height is 50'); + expect(style.meta.description.evaluate(frameState, feature2)).toEqual('Half height is 19'); + }); + it('return undefined shader functions when the style is empty', function() { // The default color style is white, the default show style is true, and the default pointSize is 1.0, // but the generated generated shader functions should just be undefined. We don't want all the points to be white. diff --git a/Specs/Scene/ConditionsExpressionSpec.js b/Specs/Scene/ConditionsExpressionSpec.js index dffd3bebc49f..233b37267b3e 100644 --- a/Specs/Scene/ConditionsExpressionSpec.js +++ b/Specs/Scene/ConditionsExpressionSpec.js @@ -25,22 +25,12 @@ defineSuite([ ] }; - var jsonExpWithExpression = { - expressions : { - halfHeight: '${Height}/2' - }, - conditions : [ - ['${expression} > 50', 'color("blue")'], - ['${expression} > 25', 'color("red")'], - ['true', 'color("lime")'] - ] + var additionalExpressions = { + halfHeight: '${Height}/2', + quarterHeight: '${Height}/4' }; - var jsonExpWithMultipleExpressions = { - expressions : { - halfHeight: '${Height}/2', - quarterHeight: '${Height}/4' - }, + var jsonExpWithAdditionalExpressions = { conditions : [ ['${halfHeight} > 50 && ${halfHeight} < 100', 'color("blue")'], ['${quarterHeight} > 50 && ${quarterHeight} < 52', 'color("red")'], @@ -48,38 +38,11 @@ defineSuite([ ] }; - var jsonExpWithUndefinedExpression = { - conditions : [ - ['${expression} === undefined', 'color("blue")'], - ['true', 'color("lime")'] - ] - }; - it('constructs', function() { var expression = new ConditionsExpression(jsonExp); expect(expression.conditionsExpression).toEqual(jsonExp); }); - it('constructs with expression', function() { - var expression = new ConditionsExpression(jsonExpWithExpression); - expect(expression._expression).toEqual('${Height}/2'); - expect(expression._conditions).toEqual([ - ['${expression} > 50', 'color("blue")'], - ['${expression} > 25', 'color("red")'], - ['true', 'color("lime")'] - ]); - }); - - it('evaluates undefined expression', function() { - var expression = new ConditionsExpression(jsonExpWithExpression); - expect(expression._expression).toEqual('${Height}/2'); - expect(expression._conditions).toEqual([ - ['${expression} > 50', 'color("blue")'], - ['${expression} > 25', 'color("red")'], - ['true', 'color("lime")'] - ]); - }); - it('evaluates conditional', function() { var expression = new ConditionsExpression(jsonExp); expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); @@ -87,8 +50,8 @@ defineSuite([ expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); }); - it('evaluates conditional with multiple expressions', function() { - var expression = new ConditionsExpression(jsonExpWithMultipleExpressions); + it('evaluates conditional with additional expressions', function() { + var expression = new ConditionsExpression(jsonExpWithAdditionalExpressions, additionalExpressions); expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); expect(expression.evaluateColor(frameState, new MockFeature(52))).toEqual(Color.LIME); expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); @@ -112,42 +75,16 @@ defineSuite([ expect(expression.evaluate(frameState, new MockFeature(3))).toEqual(undefined); }); - it('evaluates conditional with expression', function() { - var expression = new ConditionsExpression(jsonExpWithExpression); - expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); - expect(expression.evaluateColor(frameState, new MockFeature(52))).toEqual(Color.RED); - expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); - }); - - it('evaluates undefined conditional expression', function() { - var expression = new ConditionsExpression(jsonExpWithUndefinedExpression); - expect(expression._expression).toEqual(undefined); - expect(expression.evaluateColor(frameState, undefined)).toEqual(Color.BLUE); - }); - - it('constructs and evaluates conditional expression with multiple expressions', function() { - var expression = new ConditionsExpression(jsonExpWithMultipleExpressions); - expect(expression._expressions).toEqual({halfHeight: '${Height}/2', quarterHeight: '${Height}/4'}); - expect(expression._conditions).toEqual([ - ['${halfHeight} > 50 && ${halfHeight} < 100', 'color("blue")'], - ['${quarterHeight} > 50 && ${quarterHeight} < 52', 'color("red")'], - ['true', 'color("lime")'] - ]); - expect(expression.evaluateColor(frameState, new MockFeature(101))).toEqual(Color.BLUE); - expect(expression.evaluateColor(frameState, new MockFeature(52))).toEqual(Color.LIME); - expect(expression.evaluateColor(frameState, new MockFeature(3))).toEqual(Color.LIME); - }); - it('gets shader function', function() { - var expression = new ConditionsExpression(jsonExpWithExpression); + var expression = new ConditionsExpression(jsonExp); var shaderFunction = expression.getShaderFunction('getColor', '', {}, 'vec4'); var expected = 'vec4 getColor() \n' + '{ \n' + - ' if (((Height / 2.0) > 50.0)) \n' + + ' if ((Height > 100.0)) \n' + ' { \n' + ' return vec4(vec3(0.0, 0.0, 1.0), 1.0); \n' + ' } \n' + - ' else if (((Height / 2.0) > 25.0)) \n' + + ' else if ((Height > 50.0)) \n' + ' { \n' + ' return vec4(vec3(1.0, 0.0, 0.0), 1.0); \n' + ' } \n' + diff --git a/Specs/Scene/ExpressionSpec.js b/Specs/Scene/ExpressionSpec.js index 26219853c55b..304d841484b5 100644 --- a/Specs/Scene/ExpressionSpec.js +++ b/Specs/Scene/ExpressionSpec.js @@ -126,6 +126,17 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('evaluates with additional expressions', function() { + var additionalExpressions = { + halfHeight: '${Height}/2' + }; + var feature = new MockFeature(); + feature.addProperty('Height', 10); + + var expression = new Expression('${halfHeight}', additionalExpressions); + expect(expression.evaluate(frameState, feature)).toEqual(5); + }); + it('gets expressions', function() { var expressionString = "(regExp('^Chest').test(${County})) && (${YearBuilt} >= 1970)"; var expression = new Expression(expressionString);