diff --git a/src/ol/expression/expression.js b/src/ol/expression/expression.js
index 51e3aaab52..90f05219e0 100644
--- a/src/ol/expression/expression.js
+++ b/src/ol/expression/expression.js
@@ -1,3 +1,4 @@
+goog.provide('ol.expression.Call');
goog.provide('ol.expression.Comparison');
goog.provide('ol.expression.ComparisonOp');
goog.provide('ol.expression.Expression');
@@ -29,11 +30,62 @@ ol.expression.Expression = function() {};
*
* @param {Object} scope Evaluation scope. All properties of this object
* will be available as variables when evaluating the expression.
+ * @param {Object=} opt_fns Optional scope for looking up functions. If not
+ * provided, functions will be looked in the evaluation scope.
+ * @param {Object=} opt_this Object to use as this when evaluating call
+ * expressions. If not provided, `this` will resolve to a new object.
* @return {*} Result of the expression.
*/
ol.expression.Expression.prototype.evaluate = goog.abstractMethod;
+
+/**
+ * A call expression (e.g. `foo(bar)`).
+ *
+ * @constructor
+ * @extends {ol.expression.Expression}
+ * @param {expr} expr An expression that resolves to a function.
+ * @param {Array.
} args Arguments.
+ */
+ol.expression.Call = function(expr, args) {
+
+ /**
+ * @type {expr}
+ * @private
+ */
+ this.expr_ = expr;
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.args_ = args;
+
+};
+goog.inherits(ol.expression.Call, ol.expression.Expression);
+
+
+/**
+ * @inheritDoc
+ */
+ol.expression.Call.prototype.evaluate = function(scope, opt_fns, opt_this) {
+ var fnScope = goog.isDefAndNotNull(opt_fns) ? opt_fns : scope;
+ var fn = this.expr_.evaluate(fnScope);
+ if (!fn || !goog.isFunction(fn)) {
+ throw new Error('No function in provided scope: ' + this.name_);
+ }
+ var thisArg = goog.isDef(opt_this) ? opt_this : {};
+
+ var len = this.args_.length;
+ var values = new Array(len);
+ for (var i = 0; i < len; ++i) {
+ values[i] = this.args_[i].evaluate(scope, opt_fns, opt_this);
+ }
+ return fn.apply(thisArg, values);
+};
+
+
/**
* @enum {string}
*/
@@ -86,10 +138,11 @@ goog.inherits(ol.expression.Comparison, ol.expression.Expression);
/**
* @inheritDoc
*/
-ol.expression.Comparison.prototype.evaluate = function(scope) {
+ol.expression.Comparison.prototype.evaluate = function(scope, opt_this,
+ opt_fns) {
var result;
- var rightVal = this.right_.evaluate(scope);
- var leftVal = this.left_.evaluate(scope);
+ var rightVal = this.right_.evaluate(scope, opt_fns, opt_this);
+ var leftVal = this.left_.evaluate(scope, opt_fns, opt_this);
switch (this.operator_) {
case ol.expression.ComparisonOp.EQ:
@@ -174,7 +227,7 @@ goog.inherits(ol.expression.Literal, ol.expression.Expression);
/**
* @inheritDoc
*/
-ol.expression.Literal.prototype.evaluate = function(scope) {
+ol.expression.Literal.prototype.evaluate = function() {
return this.value_;
};
@@ -225,10 +278,10 @@ goog.inherits(ol.expression.Logical, ol.expression.Expression);
/**
* @inheritDoc
*/
-ol.expression.Logical.prototype.evaluate = function(scope) {
+ol.expression.Logical.prototype.evaluate = function(scope, opt_fns, opt_this) {
var result;
- var rightVal = this.right_.evaluate(scope);
- var leftVal = this.left_.evaluate(scope);
+ var rightVal = this.right_.evaluate(scope, opt_fns, opt_this);
+ var leftVal = this.left_.evaluate(scope, opt_fns, opt_this);
if (this.operator_ === ol.expression.LogicalOp.AND) {
result = leftVal && rightVal;
@@ -290,10 +343,10 @@ goog.inherits(ol.expression.Math, ol.expression.Expression);
/**
* @inheritDoc
*/
-ol.expression.Math.prototype.evaluate = function(scope) {
+ol.expression.Math.prototype.evaluate = function(scope, opt_fns, opt_this) {
var result;
- var rightVal = this.right_.evaluate(scope);
- var leftVal = this.left_.evaluate(scope);
+ var rightVal = this.right_.evaluate(scope, opt_fns, opt_this);
+ var leftVal = this.left_.evaluate(scope, opt_fns, opt_this);
/**
* TODO: throw if rightVal, leftVal not numbers - this would require the use
* of a concat function for strings but it would let us serialize these as
@@ -346,7 +399,6 @@ goog.inherits(ol.expression.Not, ol.expression.Expression);
/**
* @inheritDoc
*/
-ol.expression.Not.prototype.evaluate = function(scope) {
- return !this.expr_.evaluate(scope);
+ol.expression.Not.prototype.evaluate = function(scope, opt_fns, opt_this) {
+ return !this.expr_.evaluate(scope, opt_fns, opt_this);
};
-
diff --git a/src/ol/expression/lexer.js b/src/ol/expression/lexer.js
index cefb950cff..e4a2138515 100644
--- a/src/ol/expression/lexer.js
+++ b/src/ol/expression/lexer.js
@@ -761,7 +761,6 @@ ol.expression.Lexer.prototype.scanStringLiteral_ = function(quote) {
quote === ol.expression.Char.DOUBLE_QUOTE,
'Strings must start with a quote: ' + String.fromCharCode(quote));
- var start = this.index_;
this.increment_(1);
var str = '';
diff --git a/src/ol/expression/parser.js b/src/ol/expression/parser.js
index a7d85db958..8cddb0d177 100644
--- a/src/ol/expression/parser.js
+++ b/src/ol/expression/parser.js
@@ -17,6 +17,7 @@ goog.provide('ol.expression.Parser');
goog.require('goog.asserts');
+goog.require('ol.expression.Call');
goog.require('ol.expression.Comparison');
goog.require('ol.expression.ComparisonOp');
goog.require('ol.expression.Expression');
@@ -148,10 +149,11 @@ ol.expression.Parser.prototype.createBinaryExpression_ = function(operator,
*
* @param {ol.expression.Identifier} expr Identifier expression for function.
* @param {Array.} args Arguments array.
+ * @return {ol.expression.Call} Call expression.
* @private
*/
ol.expression.Parser.prototype.createCallExpression_ = function(expr, args) {
- throw new Error('Not implemented');
+ return new ol.expressions.Call(expr, args);
};
diff --git a/test/spec/ol/expression/expression.test.js b/test/spec/ol/expression/expression.test.js
index b88ea17f3c..7080c55a5c 100644
--- a/test/spec/ol/expression/expression.test.js
+++ b/test/spec/ol/expression/expression.test.js
@@ -1,6 +1,75 @@
goog.provide('ol.test.expression.Expression');
+describe('ol.expression.Call', function() {
+
+ describe('constructor', function() {
+ it('creates a new expression', function() {
+ var expr = new ol.expression.Call(
+ new ol.expression.Identifier('sqrt'),
+ [new ol.expression.Literal(42)]);
+ expect(expr).to.be.a(ol.expression.Expression);
+ expect(expr).to.be.a(ol.expression.Call);
+ });
+ });
+
+ describe('#evaluate()', function() {
+ var fns = {
+ sqrt: function(value) {
+ return Math.sqrt(value);
+ },
+ strConcat: function() {
+ return Array.prototype.join.call(arguments, '');
+ },
+ discouraged: function() {
+ return this.message;
+ }
+ };
+
+ it('calls method on scope with literal args', function() {
+ var expr = new ol.expression.Call(
+ new ol.expression.Identifier('sqrt'),
+ [new ol.expression.Literal(42)]);
+ expect(expr.evaluate(fns)).to.be(Math.sqrt(42));
+ });
+
+ it('accepts a separate scope for functions', function() {
+ var expr = new ol.expression.Call(
+ new ol.expression.Identifier('sqrt'),
+ [new ol.expression.Identifier('foo')]);
+ expect(expr.evaluate({foo: 42}, fns)).to.be(Math.sqrt(42));
+ });
+
+ it('accepts multiple expression arguments', function() {
+ var expr = new ol.expression.Call(
+ new ol.expression.Identifier('strConcat'),
+ [
+ new ol.expression.Identifier('foo'),
+ new ol.expression.Literal(' comes after '),
+ new ol.expression.Math(
+ ol.expression.MathOp.SUBTRACT,
+ new ol.expression.Identifier('foo'),
+ new ol.expression.Literal(1))
+ ]);
+ expect(expr.evaluate({foo: 42}, fns)).to.be('42 comes after 41');
+ });
+
+ it('accepts optional this arg', function() {
+ var expr = new ol.expression.Call(
+ new ol.expression.Identifier('discouraged'), []);
+
+ var thisArg = {
+ message: 'avoid this'
+ };
+
+ expect(expr.evaluate(fns, null, thisArg)).to.be('avoid this');
+ });
+
+ });
+
+});
+
+
describe('ol.expression.Comparison', function() {
describe('constructor', function() {
@@ -367,6 +436,7 @@ describe('ol.expression.Not', function() {
});
+goog.require('ol.expression.Call');
goog.require('ol.expression.Comparison');
goog.require('ol.expression.ComparisonOp');
goog.require('ol.expression.Expression');