diff --git a/examples/style-rules.js b/examples/style-rules.js index de8839a7b7..eafea47bd7 100644 --- a/examples/style-rules.js +++ b/examples/style-rules.js @@ -1,11 +1,8 @@ -goog.require('ol.Expression'); goog.require('ol.Map'); goog.require('ol.RendererHint'); goog.require('ol.View2D'); goog.require('ol.control.defaults'); -goog.require('ol.filter.Filter'); -goog.require('ol.filter.Geometry'); -goog.require('ol.geom.GeometryType'); +goog.require('ol.expr'); goog.require('ol.layer.Vector'); goog.require('ol.parser.GeoJSON'); goog.require('ol.proj'); @@ -19,21 +16,17 @@ goog.require('ol.style.Text'); var style = new ol.style.Style({rules: [ new ol.style.Rule({ - filter: new ol.filter.Filter(function(feature) { - return feature.get('where') == 'outer'; - }), + filter: 'where == "outer"', symbolizers: [ new ol.style.Line({ - strokeColor: new ol.Expression('color'), + strokeColor: ol.expr.parse('color'), strokeWidth: 4, opacity: 1 }) ] }), new ol.style.Rule({ - filter: new ol.filter.Filter(function(feature) { - return feature.get('where') == 'inner'; - }), + filter: 'where == "inner"', symbolizers: [ new ol.style.Line({ strokeColor: '#013', @@ -41,14 +34,14 @@ var style = new ol.style.Style({rules: [ opacity: 1 }), new ol.style.Line({ - strokeColor: new ol.Expression('color'), + strokeColor: ol.expr.parse('color'), strokeWidth: 2, opacity: 1 }) ] }), new ol.style.Rule({ - filter: new ol.filter.Geometry(ol.geom.GeometryType.POINT), + filter: 'geometryType("point")', symbolizers: [ new ol.style.Shape({ size: 40, @@ -56,7 +49,7 @@ var style = new ol.style.Style({rules: [ }), new ol.style.Text({ color: '#bada55', - text: new ol.Expression('label'), + text: ol.expr.parse('label'), fontFamily: 'Calibri,sans-serif', fontSize: 14 }) diff --git a/examples/vector-layer.js b/examples/vector-layer.js index afd596c286..05774f56f5 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -1,8 +1,7 @@ -goog.require('ol.Expression'); goog.require('ol.Map'); goog.require('ol.RendererHint'); goog.require('ol.View2D'); -goog.require('ol.filter.Filter'); +goog.require('ol.expr'); goog.require('ol.layer.TileLayer'); goog.require('ol.layer.Vector'); goog.require('ol.parser.GeoJSON'); @@ -19,6 +18,11 @@ var raster = new ol.layer.TileLayer({ source: new ol.source.MapQuestOpenAerial() }); +// TODO: discuss scale dependent rules +ol.expr.register('resolution', function() { + return map.getView().getView2D().getResolution(); +}); + var vector = new ol.layer.Vector({ source: new ol.source.Vector({ projection: ol.proj.get('EPSG:4326') @@ -32,13 +36,11 @@ var vector = new ol.layer.Vector({ ] }), new ol.style.Rule({ - filter: new ol.filter.Filter(function() { - return map.getView().getResolution() < 5000; - }), + filter: 'resolution() < 5000', symbolizers: [ new ol.style.Text({ color: '#bada55', - text: new ol.Expression('name'), + text: ol.expr.parse('name'), fontFamily: 'Calibri,sans-serif', fontSize: 12 }) diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index b56997527f..85a6db7493 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -519,53 +519,54 @@ /** * @typedef {Object} ol.style.IconOptions - * @property {string|ol.Expression} url Icon image url. - * @property {number|ol.Expression|undefined} width Width of the icon in pixels. - * Default is the width of the icon image. - * @property {number|ol.Expression|undefined} height Height of the icon in - * pixels. Default is the height of the icon image. - * @property {number|ol.Expression|undefined} opacity Icon opacity (0-1). - * @property {number|ol.Expression|undefined} rotation Rotation in degrees - * (0-360). + * @property {string|ol.expr.Expression} url Icon image url. + * @property {number|ol.expr.Expression|undefined} width Width of the icon + * in pixels. Default is the width of the icon image. + * @property {number|ol.expr.Expression|undefined} height Height of the + * icon in pixels. Default is the height of the icon image. + * @property {number|ol.expr.Expression|undefined} opacity Icon opacity + * (0-1). + * @property {number|ol.expr.Expression|undefined} rotation Rotation in + * degrees (0-360). */ /** * @typedef {Object} ol.style.LineOptions - * @property {string|ol.Expression|undefined} strokeColor Stroke color as hex - * color code. - * @property {number|ol.Expression|undefined} strokeWidth Stroke width in - * pixels. - * @property {number|ol.Expression|undefined} opacity Opacity (0-1). + * @property {string|ol.expr.Expression|undefined} strokeColor Stroke + * color as hex color code. + * @property {number|ol.expr.Expression|undefined} strokeWidth Stroke + * width in pixels. + * @property {number|ol.expr.Expression|undefined} opacity Opacity (0-1). */ /** * @typedef {Object} ol.style.PolygonOptions - * @property {string|ol.Expression|undefined} fillColor Fill color as hex color - * code. - * @property {string|ol.Expression|undefined} strokeColor Stroke color as hex - * color code. - * @property {number|ol.Expression|undefined} strokeWidth Stroke width in - * pixels. - * @property {number|ol.Expression|undefined} opacity Opacity (0-1). + * @property {string|ol.expr.Expression|undefined} fillColor Fill color as + * hex color code. + * @property {string|ol.expr.Expression|undefined} strokeColor Stroke + * color as hex color code. + * @property {number|ol.expr.Expression|undefined} strokeWidth Stroke + * width in pixels. + * @property {number|ol.expr.Expression|undefined} opacity Opacity (0-1). */ /** * @typedef {Object} ol.style.RuleOptions - * @property {ol.filter.Filter|undefined} filter Filter. + * @property {ol.expr.Expression|string|undefined} filter Filter. * @property {Array.|undefined} symbolizers Symbolizers. */ /** * @typedef {Object} ol.style.ShapeOptions * @property {ol.style.ShapeType|undefined} type Type. - * @property {number|ol.Expression|undefined} size Size in pixels. - * @property {string|ol.Expression|undefined} fillColor Fill color as hex color - * code. - * @property {string|ol.Expression|undefined} strokeColor Stroke color as hex - * color code. - * @property {number|ol.Expression|undefined} strokeWidth Stroke width in - * pixels. - * @property {number|ol.Expression|undefined} opacity Opacity (0-1). + * @property {number|ol.expr.Expression|undefined} size Size in pixels. + * @property {string|ol.expr.Expression|undefined} fillColor Fill color as + * hex color code. + * @property {string|ol.expr.Expression|undefined} strokeColor Stroke + * color as hex color code. + * @property {number|ol.expr.Expression|undefined} strokeWidth Stroke + * width in pixels. + * @property {number|ol.expr.Expression|undefined} opacity Opacity (0-1). */ /** @@ -575,11 +576,11 @@ /** * @typedef {Object} ol.style.TextOptions - * @property {string|ol.Expression|undefined} color Color. - * @property {string|ol.Expression|undefined} fontFamily Font family. - * @property {number|ol.Expression|undefined} fontSize Font size in pixels. - * @property {string|ol.Expression} text Text for the label. - * @property {number|ol.Expression|undefined} opacity Opacity (0-1). + * @property {string|ol.expr.Expression|undefined} color Color. + * @property {string|ol.expr.Expression|undefined} fontFamily Font family. + * @property {number|ol.expr.Expression|undefined} fontSize Font size in pixels. + * @property {string|ol.expr.Expression} text Text for the label. + * @property {number|ol.expr.Expression|undefined} opacity Opacity (0-1). */ /** diff --git a/src/ol/expr.jsdoc b/src/ol/expr.jsdoc new file mode 100644 index 0000000000..3147c2784e --- /dev/null +++ b/src/ol/expr.jsdoc @@ -0,0 +1,3 @@ +/** + * @namespace ol.expr + */ diff --git a/src/ol/expr/expression.exports b/src/ol/expr/expression.exports new file mode 100644 index 0000000000..14b6f57d46 --- /dev/null +++ b/src/ol/expr/expression.exports @@ -0,0 +1,2 @@ +@exportSymbol ol.expr.parse +@exportSymbol ol.expr.register diff --git a/src/ol/expr/expression.js b/src/ol/expr/expression.js new file mode 100644 index 0000000000..81a511bc26 --- /dev/null +++ b/src/ol/expr/expression.js @@ -0,0 +1,158 @@ +goog.provide('ol.expr'); +goog.provide('ol.expr.functions'); + +goog.require('ol.Extent'); +goog.require('ol.Feature'); +goog.require('ol.expr.Call'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Parser'); +goog.require('ol.extent'); +goog.require('ol.geom.GeometryType'); + + +/** + * Evaluate an expression with a feature. The feature attributes will be used + * as the evaluation scope. The `ol.expr.lib` functions will be used as + * function scope. The feature itself will be used as the `this` argument. + * + * @param {ol.expr.Expression} expr The expression. + * @param {ol.Feature=} opt_feature The feature. + * @return {*} The result of the expression. + */ +ol.expr.evaluateFeature = function(expr, opt_feature) { + var result; + if (goog.isDef(opt_feature)) { + result = expr.evaluate( + opt_feature.getAttributes(), ol.expr.lib, opt_feature); + } else { + result = expr.evaluate(); + } + return result; +}; + + +/** + * Parse an expression. + * @param {string} source The expression source (e.g. `'foo + 2'`). + * @return {ol.expr.Expression} An expression instance that can be + * evaluated within some scope to provide a value. + */ +ol.expr.parse = function(source) { + var parser = new ol.expr.Parser(); + return parser.parse(source); +}; + + +/** + * Register a library function to be used in expressions. + * @param {string} name The function name (e.g. 'myFunc'). + * @param {function(this:ol.Feature)} func The function to be called in an + * expression. This function will be called with a feature as the `this` + * argument when the expression is evaluated in the context of a features. + */ +ol.expr.register = function(name, func) { + ol.expr.lib[name] = func; +}; + + +/** + * Determines whether an expression is a call expression that calls one of the + * `ol.expr.lib` functions. + * + * @param {ol.expr.Expression} expr The candidate expression. + * @return {string|undefined} If the candidate expression is a call to a lib + * function, the return will be the function name. If not, the return will be + * `undefined`. + */ +ol.expr.isLibCall = function(expr) { + var name; + if (expr instanceof ol.expr.Call) { + var callee = expr.getCallee(); + if (callee instanceof ol.expr.Identifier) { + name = callee.getName(); + if (!ol.expr.lib.hasOwnProperty(name)) { + name = undefined; + } + } + } + return name; +}; + + +/** + * Library of well-known functions. These are available to expressions parsed + * with `ol.expr.parse`. + * + * @type {Object.} + */ +ol.expr.lib = {}; + + +/** + * Enumeration of library function names. + * + * @enum {string} + */ +ol.expr.functions = { + EXTENT: 'extent', + FID: 'fid', + GEOMETRY_TYPE: 'geometryType' +}; + + +/** + * Determine if a feature's extent intersects the provided extent. + * @param {number} minX Minimum x-coordinate value. + * @param {number} maxX Maximum x-coordinate value. + * @param {number} minY Minimum y-coordinate value. + * @param {number} maxY Maximum y-coordinate value. + * @return {boolean} The provided extent intersects the feature's extent. + * @this {ol.Feature} + */ +ol.expr.lib[ol.expr.functions.EXTENT] = function(minX, maxX, minY, maxY) { + var intersects = false; + var geometry = this.getGeometry(); + if (geometry) { + intersects = ol.extent.intersects(geometry.getBounds(), + [minX, maxX, minY, maxY]); + } + return intersects; +}; + + +/** + * Determine if the feature identifier matches any of the provided values. + * @param {...string} var_args Feature identifiers. + * @return {boolean} The feature's identifier matches one of the given values. + * @this {ol.Feature} + */ +ol.expr.lib[ol.expr.functions.FID] = function(var_args) { + var matches = false; + var id = this.getFeatureId(); + if (goog.isDef(id)) { + for (var i = 0, ii = arguments.length; i < ii; ++i) { + if (arguments[i] === id) { + matches = true; + break; + } + } + } + return matches; +}; + + +/** + * Determine if a feature's default geometry is of the given type. + * @param {ol.geom.GeometryType} type Geometry type. + * @return {boolean} The feature's default geometry is of the given type. + * @this {ol.Feature} + */ +ol.expr.lib[ol.expr.functions.GEOMETRY_TYPE] = function(type) { + var same = false; + var geometry = this.getGeometry(); + if (geometry) { + same = geometry.getType() === type; + } + return same; +}; diff --git a/src/ol/expr/expressions.js b/src/ol/expr/expressions.js new file mode 100644 index 0000000000..bd827885ea --- /dev/null +++ b/src/ol/expr/expressions.js @@ -0,0 +1,631 @@ +goog.provide('ol.expr.Call'); +goog.provide('ol.expr.Comparison'); +goog.provide('ol.expr.ComparisonOp'); +goog.provide('ol.expr.Expression'); +goog.provide('ol.expr.Identifier'); +goog.provide('ol.expr.Literal'); +goog.provide('ol.expr.Logical'); +goog.provide('ol.expr.LogicalOp'); +goog.provide('ol.expr.Math'); +goog.provide('ol.expr.MathOp'); +goog.provide('ol.expr.Member'); +goog.provide('ol.expr.Not'); + + + +/** + * Base class for all expressions. Instances of ol.expr.Expression + * correspond to a limited set of ECMAScript 5.1 expressions. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11 + * + * This base class should not be constructed directly. Instead, use one of + * the subclass constructors. + * + * @constructor + */ +ol.expr.Expression = function() {}; + + +/** + * Evaluate the expression and return the result. + * + * @param {Object=} opt_scope Evaluation scope. All properties of this object + * will be available as variables when evaluating the expression. If not + * provided, `null` will be used. + * @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.expr.Expression.prototype.evaluate = goog.abstractMethod; + + + +/** + * A call expression (e.g. `foo(bar)`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.Expression} callee An expression that resolves to a + * function. + * @param {Array.} args Arguments. + */ +ol.expr.Call = function(callee, args) { + + /** + * @type {ol.expr.Expression} + * @private + */ + this.callee_ = callee; + + /** + * @type {Array.} + * @private + */ + this.args_ = args; + +}; +goog.inherits(ol.expr.Call, ol.expr.Expression); + + +/** + * @inheritDoc + */ +ol.expr.Call.prototype.evaluate = function(opt_scope, opt_fns, opt_this) { + var fnScope = goog.isDefAndNotNull(opt_fns) ? opt_fns : opt_scope; + var fn = this.callee_.evaluate(fnScope); + if (!fn || !goog.isFunction(fn)) { + throw new Error('Expected function but found ' + fn); + } + 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(opt_scope, opt_fns, opt_this); + } + return fn.apply(thisArg, values); +}; + + +/** + * Get the argument list. + * @return {Array.} The argument. + */ +ol.expr.Call.prototype.getArgs = function() { + return this.args_; +}; + + +/** + * Get the callee expression. + * @return {ol.expr.Expression} The callee expression. + */ +ol.expr.Call.prototype.getCallee = function() { + return this.callee_; +}; + + +/** + * @enum {string} + */ +ol.expr.ComparisonOp = { + EQ: '==', + NEQ: '!=', + STRICT_EQ: '===', + STRICT_NEQ: '!==', + GT: '>', + LT: '<', + GTE: '>=', + LTE: '<=' +}; + + + +/** + * A comparison expression (e.g. `foo >= 42`, `bar != "chicken"`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.ComparisonOp} operator Comparison operator. + * @param {ol.expr.Expression} left Left expression. + * @param {ol.expr.Expression} right Right expression. + */ +ol.expr.Comparison = function(operator, left, right) { + + /** + * @type {ol.expr.ComparisonOp} + * @private + */ + this.operator_ = operator; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.left_ = left; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.right_ = right; + +}; +goog.inherits(ol.expr.Comparison, ol.expr.Expression); + + +/** + * Determine if a given string is a valid comparison operator. + * @param {string} candidate Operator to test. + * @return {boolean} The operator is valid. + */ +ol.expr.Comparison.isValidOp = (function() { + var valid = {}; + for (var key in ol.expr.ComparisonOp) { + valid[ol.expr.ComparisonOp[key]] = true; + } + return function isValidOp(candidate) { + return !!valid[candidate]; + }; +}()); + + +/** + * @inheritDoc + */ +ol.expr.Comparison.prototype.evaluate = function(opt_scope, opt_fns, opt_this) { + var result; + var rightVal = this.right_.evaluate(opt_scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(opt_scope, opt_fns, opt_this); + + var op = this.operator_; + if (op === ol.expr.ComparisonOp.EQ) { + result = leftVal == rightVal; + } else if (op === ol.expr.ComparisonOp.NEQ) { + result = leftVal != rightVal; + } else if (op === ol.expr.ComparisonOp.STRICT_EQ) { + result = leftVal === rightVal; + } else if (op === ol.expr.ComparisonOp.STRICT_NEQ) { + result = leftVal !== rightVal; + } else if (op === ol.expr.ComparisonOp.GT) { + result = leftVal > rightVal; + } else if (op === ol.expr.ComparisonOp.LT) { + result = leftVal < rightVal; + } else if (op === ol.expr.ComparisonOp.GTE) { + result = leftVal >= rightVal; + } else if (op === ol.expr.ComparisonOp.LTE) { + result = leftVal <= rightVal; + } else { + throw new Error('Unsupported comparison operator: ' + this.operator_); + } + return result; +}; + + +/** + * Get the comparison operator. + * @return {string} The comparison operator. + */ +ol.expr.Comparison.prototype.getOperator = function() { + return this.operator_; +}; + + +/** + * Get the left expression. + * @return {ol.expr.Expression} The left expression. + */ +ol.expr.Comparison.prototype.getLeft = function() { + return this.left_; +}; + + +/** + * Get the right expression. + * @return {ol.expr.Expression} The right expression. + */ +ol.expr.Comparison.prototype.getRight = function() { + return this.right_; +}; + + + +/** + * An identifier expression (e.g. `foo`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {string} name An identifier name. + */ +ol.expr.Identifier = function(name) { + + /** + * @type {string} + * @private + */ + this.name_ = name; + +}; +goog.inherits(ol.expr.Identifier, ol.expr.Expression); + + +/** + * @inheritDoc + */ +ol.expr.Identifier.prototype.evaluate = function(opt_scope) { + if (!goog.isDefAndNotNull(opt_scope)) { + throw new Error('Attempt to evaluate identifier with no scope'); + } + return opt_scope[this.name_]; +}; + + +/** + * Get the identifier name. + * @return {string} The identifier name. + */ +ol.expr.Identifier.prototype.getName = function() { + return this.name_; +}; + + + +/** + * A literal expression (e.g. `"chicken"`, `42`, `true`, `null`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {string|number|boolean|null} value A literal value. + */ +ol.expr.Literal = function(value) { + + /** + * @type {string|number|boolean|null} + * @private + */ + this.value_ = value; + +}; +goog.inherits(ol.expr.Literal, ol.expr.Expression); + + +/** + * @inheritDoc + */ +ol.expr.Literal.prototype.evaluate = function() { + return this.value_; +}; + + +/** + * Get the literal value. + * @return {string|number|boolean|null} The literal value. + */ +ol.expr.Literal.prototype.getValue = function() { + return this.value_; +}; + + +/** + * @enum {string} + */ +ol.expr.LogicalOp = { + AND: '&&', + OR: '||' +}; + + + +/** + * A binary logical expression (e.g. `foo && bar`, `bar || "chicken"`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.LogicalOp} operator Logical operator. + * @param {ol.expr.Expression} left Left expression. + * @param {ol.expr.Expression} right Right expression. + */ +ol.expr.Logical = function(operator, left, right) { + + /** + * @type {ol.expr.LogicalOp} + * @private + */ + this.operator_ = operator; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.left_ = left; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.right_ = right; + +}; +goog.inherits(ol.expr.Logical, ol.expr.Expression); + + +/** + * Determine if a given string is a valid logical operator. + * @param {string} candidate Operator to test. + * @return {boolean} The operator is valid. + */ +ol.expr.Logical.isValidOp = (function() { + var valid = {}; + for (var key in ol.expr.LogicalOp) { + valid[ol.expr.LogicalOp[key]] = true; + } + return function isValidOp(candidate) { + return !!valid[candidate]; + }; +}()); + + +/** + * @inheritDoc + */ +ol.expr.Logical.prototype.evaluate = function(opt_scope, opt_fns, + opt_this) { + var result; + var rightVal = this.right_.evaluate(opt_scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(opt_scope, opt_fns, opt_this); + + if (this.operator_ === ol.expr.LogicalOp.AND) { + result = leftVal && rightVal; + } else if (this.operator_ === ol.expr.LogicalOp.OR) { + result = leftVal || rightVal; + } else { + throw new Error('Unsupported logical operator: ' + this.operator_); + } + return result; +}; + + +/** + * Get the logical operator. + * @return {string} The logical operator. + */ +ol.expr.Logical.prototype.getOperator = function() { + return this.operator_; +}; + + +/** + * Get the left expression. + * @return {ol.expr.Expression} The left expression. + */ +ol.expr.Logical.prototype.getLeft = function() { + return this.left_; +}; + + +/** + * Get the right expression. + * @return {ol.expr.Expression} The right expression. + */ +ol.expr.Logical.prototype.getRight = function() { + return this.right_; +}; + + +/** + * @enum {string} + */ +ol.expr.MathOp = { + ADD: '+', + SUBTRACT: '-', + MULTIPLY: '*', + DIVIDE: '/', + MOD: '%' +}; + + + +/** + * A math expression (e.g. `foo + 42`, `bar % 10`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.MathOp} operator Math operator. + * @param {ol.expr.Expression} left Left expression. + * @param {ol.expr.Expression} right Right expression. + */ +ol.expr.Math = function(operator, left, right) { + + /** + * @type {ol.expr.MathOp} + * @private + */ + this.operator_ = operator; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.left_ = left; + + /** + * @type {ol.expr.Expression} + * @private + */ + this.right_ = right; + +}; +goog.inherits(ol.expr.Math, ol.expr.Expression); + + +/** + * Determine if a given string is a valid math operator. + * @param {string} candidate Operator to test. + * @return {boolean} The operator is valid. + */ +ol.expr.Math.isValidOp = (function() { + var valid = {}; + for (var key in ol.expr.MathOp) { + valid[ol.expr.MathOp[key]] = true; + } + return function isValidOp(candidate) { + return !!valid[candidate]; + }; +}()); + + +/** + * @inheritDoc + */ +ol.expr.Math.prototype.evaluate = function(opt_scope, opt_fns, opt_this) { + var result; + var rightVal = this.right_.evaluate(opt_scope, opt_fns, opt_this); + var leftVal = this.left_.evaluate(opt_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 + * math functions where available elsewhere + */ + + var op = this.operator_; + if (op === ol.expr.MathOp.ADD) { + result = leftVal + rightVal; + } else if (op === ol.expr.MathOp.SUBTRACT) { + result = Number(leftVal) - Number(rightVal); + } else if (op === ol.expr.MathOp.MULTIPLY) { + result = Number(leftVal) * Number(rightVal); + } else if (op === ol.expr.MathOp.DIVIDE) { + result = Number(leftVal) / Number(rightVal); + } else if (op === ol.expr.MathOp.MOD) { + result = Number(leftVal) % Number(rightVal); + } else { + throw new Error('Unsupported math operator: ' + this.operator_); + } + return result; +}; + + +/** + * Get the math operator. + * @return {string} The math operator. + */ +ol.expr.Math.prototype.getOperator = function() { + return this.operator_; +}; + + +/** + * Get the left expression. + * @return {ol.expr.Expression} The left expression. + */ +ol.expr.Math.prototype.getLeft = function() { + return this.left_; +}; + + +/** + * Get the right expression. + * @return {ol.expr.Expression} The right expression. + */ +ol.expr.Math.prototype.getRight = function() { + return this.right_; +}; + + + +/** + * A member expression (e.g. `foo.bar`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.Expression} object An expression that resolves to an + * object. + * @param {ol.expr.Identifier} property Identifier with name of property. + */ +ol.expr.Member = function(object, property) { + + /** + * @type {ol.expr.Expression} + * @private + */ + this.object_ = object; + + /** + * @type {ol.expr.Identifier} + * @private + */ + this.property_ = property; + +}; +goog.inherits(ol.expr.Member, ol.expr.Expression); + + +/** + * @inheritDoc + */ +ol.expr.Member.prototype.evaluate = function(opt_scope, opt_fns, + opt_this) { + var obj = this.object_.evaluate(opt_scope, opt_fns, opt_this); + if (!goog.isObject(obj)) { + throw new Error('Expected member expression to evaluate to an object ' + + 'but got ' + obj); + } + return this.property_.evaluate(/** @type {Object} */ (obj)); +}; + + +/** + * Get the object expression. + * @return {ol.expr.Expression} The object. + */ +ol.expr.Member.prototype.getObject = function() { + return this.object_; +}; + + +/** + * Get the property expression. + * @return {ol.expr.Identifier} The property. + */ +ol.expr.Member.prototype.getProperty = function() { + return this.property_; +}; + + + +/** + * A logical not expression (e.g. `!foo`). + * + * @constructor + * @extends {ol.expr.Expression} + * @param {ol.expr.Expression} argument Expression to negate. + */ +ol.expr.Not = function(argument) { + + /** + * @type {ol.expr.Expression} + * @private + */ + this.argument_ = argument; + +}; +goog.inherits(ol.expr.Not, ol.expr.Expression); + + +/** + * @inheritDoc + */ +ol.expr.Not.prototype.evaluate = function(opt_scope, opt_fns, opt_this) { + return !this.argument_.evaluate(opt_scope, opt_fns, opt_this); +}; + + +/** + * Get the argument (the negated expression). + * @return {ol.expr.Expression} The argument. + */ +ol.expr.Not.prototype.getArgument = function() { + return this.argument_; +}; diff --git a/src/ol/expr/lexer.js b/src/ol/expr/lexer.js new file mode 100644 index 0000000000..c1af520c34 --- /dev/null +++ b/src/ol/expr/lexer.js @@ -0,0 +1,900 @@ +/** + * The logic and naming of methods here are inspired by Esprima (BSD Licensed). + * Esprima (http://esprima.org) includes the following copyright notices: + * + * Copyright (C) 2013 Ariya Hidayat + * Copyright (C) 2013 Thaddee Tyl + * Copyright (C) 2012 Ariya Hidayat + * Copyright (C) 2012 Mathias Bynens + * Copyright (C) 2012 Joost-Wim Boekesteijn + * Copyright (C) 2012 Kris Kowal + * Copyright (C) 2012 Yusuke Suzuki + * Copyright (C) 2012 Arpad Borsos + * Copyright (C) 2011 Ariya Hidayat + */ + +goog.provide('ol.expr.Char'); // TODO: remove this - see #785 +goog.provide('ol.expr.Lexer'); +goog.provide('ol.expr.Token'); +goog.provide('ol.expr.TokenType'); +goog.provide('ol.expr.UnexpectedToken'); + +goog.require('goog.asserts'); +goog.require('goog.debug.Error'); + + +/** + * @enum {number} + */ +ol.expr.Char = { + AMPERSAND: 38, + BACKSLASH: 92, + BANG: 33, // ! + CARRIAGE_RETURN: 13, + COMMA: 44, + DIGIT_0: 48, + DIGIT_7: 55, + DIGIT_9: 57, + DOLLAR: 36, + DOUBLE_QUOTE: 34, + DOT: 46, + EQUAL: 61, + FORM_FEED: 0xC, + GREATER: 62, + LEFT_PAREN: 40, + LESS: 60, + LINE_FEED: 10, + LINE_SEPARATOR: 0x2028, + LOWER_A: 97, + LOWER_E: 101, + LOWER_F: 102, + LOWER_X: 120, + LOWER_Z: 122, + MINUS: 45, + NONBREAKING_SPACE: 0xA0, + PARAGRAPH_SEPARATOR: 0x2029, + PERCENT: 37, + PIPE: 124, + PLUS: 43, + RIGHT_PAREN: 41, + SINGLE_QUOTE: 39, + SLASH: 47, + SPACE: 32, + STAR: 42, + TAB: 9, + TILDE: 126, + UNDERSCORE: 95, + UPPER_A: 65, + UPPER_E: 69, + UPPER_F: 70, + UPPER_X: 88, + UPPER_Z: 90, + VERTICAL_TAB: 0xB +}; + + +/** + * @enum {string} + */ +ol.expr.TokenType = { + BOOLEAN_LITERAL: 'Boolean', + EOF: '', + IDENTIFIER: 'Identifier', + KEYWORD: 'Keyword', + NULL_LITERAL: 'Null', + NUMERIC_LITERAL: 'Numeric', + PUNCTUATOR: 'Punctuator', + STRING_LITERAL: 'String', + UNKNOWN: 'Unknown' +}; + + +/** + * @typedef {{type: (ol.expr.TokenType), + * value: (string|number|boolean|null), + * index: (number)}} + */ +ol.expr.Token; + + + +/** + * Lexer constructor. Provides a tokenizer for a limited subset of ECMAScript + * 5.1 expressions (http://www.ecma-international.org/ecma-262/5.1/#sec-11). + * + * @constructor + * @param {string} source Source code. + */ +ol.expr.Lexer = function(source) { + + /** + * Source code. + * @type {string} + * @private + */ + this.source_ = source; + + /** + * Source length. + * @type {number} + * @private + */ + this.length_ = source.length; + + /** + * Current character index. + * @type {number} + * @private + */ + this.index_ = 0; + + /** + * Next character index (only set after `peek`ing). + * @type {number} + * @private + */ + this.nextIndex_ = 0; + +}; + + +/** + * Scan the next token and throw if it isn't a punctuator that matches input. + * @param {string} value Token value. + */ +ol.expr.Lexer.prototype.expect = function(value) { + var match = this.match(value); + if (!match) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: this.getCurrentChar_(), + index: this.index_ + }); + } + this.skip(); +}; + + +/** + * Increment the current character index. + * + * @param {number} delta Delta by which the index is advanced. + * @private + */ +ol.expr.Lexer.prototype.increment_ = function(delta) { + this.index_ += delta; +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.3 + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is a decimal digit. + * @private + */ +ol.expr.Lexer.prototype.isDecimalDigit_ = function(code) { + return ( + code >= ol.expr.Char.DIGIT_0 && + code <= ol.expr.Char.DIGIT_9); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.6.1.2 + * + * @param {string} id A string identifier. + * @return {boolean} The identifier is a future reserved word. + * @private + */ +ol.expr.Lexer.prototype.isFutureReservedWord_ = function(id) { + return ( + id === 'class' || + id === 'enum' || + id === 'export' || + id === 'extends' || + id === 'import' || + id === 'super'); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.3 + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is a hex digit. + * @private + */ +ol.expr.Lexer.prototype.isHexDigit_ = function(code) { + return this.isDecimalDigit_(code) || + (code >= ol.expr.Char.LOWER_A && + code <= ol.expr.Char.LOWER_F) || + (code >= ol.expr.Char.UPPER_A && + code <= ol.expr.Char.UPPER_F); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.6 + * Doesn't deal with non-ascii identifiers. + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is a valid identifier part. + * @private + */ +ol.expr.Lexer.prototype.isIdentifierPart_ = function(code) { + return this.isIdentifierStart_(code) || + (code >= ol.expr.Char.DIGIT_0 && + code <= ol.expr.Char.DIGIT_9); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.6 + * Doesn't yet deal with non-ascii identifiers. + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is a valid identifier start. + * @private + */ +ol.expr.Lexer.prototype.isIdentifierStart_ = function(code) { + return (code === ol.expr.Char.DOLLAR) || + (code === ol.expr.Char.UNDERSCORE) || + (code >= ol.expr.Char.UPPER_A && + code <= ol.expr.Char.UPPER_Z) || + (code >= ol.expr.Char.LOWER_A && + code <= ol.expr.Char.LOWER_Z); +}; + + +/** + * Determine if the given identifier is an ECMAScript keyword. These cannot + * be used as identifiers in programs. There is no real reason these could not + * be used in ol.exprs - but they are reserved for future use. + * + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.6.1.1 + * + * @param {string} id Identifier. + * @return {boolean} The identifier is a keyword. + * @private + */ +ol.expr.Lexer.prototype.isKeyword_ = function(id) { + return ( + id === 'break' || + id === 'case' || + id === 'catch' || + id === 'continue' || + id === 'debugger' || + id === 'default' || + id === 'delete' || + id === 'do' || + id === 'else' || + id === 'finally' || + id === 'for' || + id === 'function' || + id === 'if' || + id === 'in' || + id === 'instanceof' || + id === 'new' || + id === 'return' || + id === 'switch' || + id === 'this' || + id === 'throw' || + id === 'try' || + id === 'typeof' || + id === 'var' || + id === 'void' || + id === 'while' || + id === 'with'); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is a line terminator. + * @private + */ +ol.expr.Lexer.prototype.isLineTerminator_ = function(code) { + return (code === ol.expr.Char.LINE_FEED) || + (code === ol.expr.Char.CARRIAGE_RETURN) || + (code === ol.expr.Char.LINE_SEPARATOR) || + (code === ol.expr.Char.PARAGRAPH_SEPARATOR); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.3 + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is an octal digit. + * @private + */ +ol.expr.Lexer.prototype.isOctalDigit_ = function(code) { + return ( + code >= ol.expr.Char.DIGIT_0 && + code <= ol.expr.Char.DIGIT_7); +}; + + +/** + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.2 + * + * @param {number} code The unicode of a character. + * @return {boolean} The character is whitespace. + * @private + */ +ol.expr.Lexer.prototype.isWhitespace_ = function(code) { + return (code === ol.expr.Char.SPACE) || + (code === ol.expr.Char.TAB) || + (code === ol.expr.Char.VERTICAL_TAB) || + (code === ol.expr.Char.FORM_FEED) || + (code === ol.expr.Char.NONBREAKING_SPACE) || + (code >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005' + + '\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF' + .indexOf(String.fromCharCode(code)) > 0); +}; + + +/** + * Get the unicode of the character at the given offset from the current index. + * + * @param {number} delta Offset from current index. + * @return {number} The character code. + * @private + */ +ol.expr.Lexer.prototype.getCharCode_ = function(delta) { + return this.source_.charCodeAt(this.index_ + delta); +}; + + +/** + * Get the character at the current index. + * + * @return {string} The current character. + * @private + */ +ol.expr.Lexer.prototype.getCurrentChar_ = function() { + return this.source_[this.index_]; +}; + + +/** + * Get the unicode of the character at the current index. + * + * @return {number} The current character code. + * @private + */ +ol.expr.Lexer.prototype.getCurrentCharCode_ = function() { + return this.getCharCode_(0); +}; + + +/** + * Determine whether the upcoming token matches the given punctuator. + * @param {string} value Punctuator value. + * @return {boolean} The token matches. + */ +ol.expr.Lexer.prototype.match = function(value) { + var token = this.peek(); + return ( + token.type === ol.expr.TokenType.PUNCTUATOR && + token.value === value); +}; + + +/** + * Scan the next token. + * + * @return {ol.expr.Token} Next token. + */ +ol.expr.Lexer.prototype.next = function() { + var code = this.skipWhitespace_(); + + if (this.index_ >= this.length_) { + return { + type: ol.expr.TokenType.EOF, + value: null, + index: this.index_ + }; + } + + // check for common punctuation + if (code === ol.expr.Char.LEFT_PAREN || + code === ol.expr.Char.RIGHT_PAREN) { + return this.scanPunctuator_(code); + } + + // check for string literal + if (code === ol.expr.Char.SINGLE_QUOTE || + code === ol.expr.Char.DOUBLE_QUOTE) { + return this.scanStringLiteral_(code); + } + + // check for identifier + if (this.isIdentifierStart_(code)) { + return this.scanIdentifier_(code); + } + + // check dot punctuation or decimal + if (code === ol.expr.Char.DOT) { + if (this.isDecimalDigit_(this.getCharCode_(1))) { + return this.scanNumericLiteral_(code); + } + return this.scanPunctuator_(code); + } + + // check for numeric literal + if (this.isDecimalDigit_(code)) { + return this.scanNumericLiteral_(code); + } + + // all the rest is punctuation + return this.scanPunctuator_(code); +}; + + +/** + * Peek at the next token, but don't advance the index. + * + * @return {ol.expr.Token} The upcoming token. + */ +ol.expr.Lexer.prototype.peek = function() { + var currentIndex = this.index_; + var token = this.next(); + this.nextIndex_ = this.index_; + this.index_ = currentIndex; + return token; +}; + + +/** + * Scan hex literal as numeric token. + * + * @param {number} code The current character code. + * @return {ol.expr.Token} Numeric literal token. + * @private + */ +ol.expr.Lexer.prototype.scanHexLiteral_ = function(code) { + var str = ''; + var start = this.index_ - 2; + + while (this.index_ < this.length_) { + if (!this.isHexDigit_(code)) { + break; + } + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + } + + if (str.length === 0 || this.isIdentifierStart_(code)) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(code), + index: this.index_ + }); + } + + goog.asserts.assert(!isNaN(parseInt('0x' + str, 16)), 'Valid hex: ' + str); + + return { + type: ol.expr.TokenType.NUMERIC_LITERAL, + value: parseInt('0x' + str, 16), + index: start + }; +}; + + +/** + * Scan identifier token. + * + * @param {number} code The current character code. + * @return {ol.expr.Token} Identifier token. + * @private + */ +ol.expr.Lexer.prototype.scanIdentifier_ = function(code) { + goog.asserts.assert(this.isIdentifierStart_(code), + 'Must be called with a valid identifier'); + + var start = this.index_; + this.increment_(1); + + while (this.index_ < this.length_) { + code = this.getCurrentCharCode_(); + if (this.isIdentifierPart_(code)) { + this.increment_(1); + } else { + break; + } + } + var id = this.source_.slice(start, this.index_); + + var type; + if (id.length === 1) { + type = ol.expr.TokenType.IDENTIFIER; + } else if (this.isKeyword_(id)) { + type = ol.expr.TokenType.KEYWORD; + } else if (id === 'null') { + type = ol.expr.TokenType.NULL_LITERAL; + } else if (id === 'true' || id === 'false') { + type = ol.expr.TokenType.BOOLEAN_LITERAL; + } else { + type = ol.expr.TokenType.IDENTIFIER; + } + + return { + type: type, + value: id, + index: start + }; +}; + + +/** + * Scan numeric literal token. + * http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.3 + * + * @param {number} code The current character code. + * @return {ol.expr.Token} Numeric literal token. + * @private + */ +ol.expr.Lexer.prototype.scanNumericLiteral_ = function(code) { + goog.asserts.assert( + code === ol.expr.Char.DOT || this.isDecimalDigit_(code), + 'Valid start for numeric literal: ' + String.fromCharCode(code)); + + // start assembling numeric string + var str = ''; + var start = this.index_; + + if (code !== ol.expr.Char.DOT) { + + if (code === ol.expr.Char.DIGIT_0) { + var nextCode = this.getCharCode_(1); + + // hex literals start with 0X or 0x + if (nextCode === ol.expr.Char.UPPER_X || + nextCode === ol.expr.Char.LOWER_X) { + this.increment_(2); + return this.scanHexLiteral_(this.getCurrentCharCode_()); + } + + // octals start with 0 + if (this.isOctalDigit_(nextCode)) { + this.increment_(1); + return this.scanOctalLiteral_(nextCode); + } + + // numbers like 09 not allowed + if (this.isDecimalDigit_(nextCode)) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(nextCode), + index: this.index_ + }); + } + } + + // scan all decimal chars + while (this.isDecimalDigit_(code)) { + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + } + } + + // scan fractional part + if (code === ol.expr.Char.DOT) { + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + + // scan all decimal chars + while (this.isDecimalDigit_(code)) { + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + } + } + + // scan exponent + if (code === ol.expr.Char.UPPER_E || + code === ol.expr.Char.LOWER_E) { + str += 'E'; + this.increment_(1); + + code = this.getCurrentCharCode_(); + if (code === ol.expr.Char.PLUS || + code === ol.expr.Char.MINUS) { + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + } + + if (!this.isDecimalDigit_(code)) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(code), + index: this.index_ + }); + } + + // scan all decimal chars (TODO: unduplicate this) + while (this.isDecimalDigit_(code)) { + str += String.fromCharCode(code); + this.increment_(1); + code = this.getCurrentCharCode_(); + } + } + + if (this.isIdentifierStart_(code)) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(code), + index: this.index_ + }); + } + + goog.asserts.assert(!isNaN(parseFloat(str)), 'Valid number: ' + str); + + return { + type: ol.expr.TokenType.NUMERIC_LITERAL, + value: parseFloat(str), + index: start + }; + +}; + + +/** + * Scan octal literal as numeric token. + * + * @param {number} code The current character code. + * @return {ol.expr.Token} Numeric literal token. + * @private + */ +ol.expr.Lexer.prototype.scanOctalLiteral_ = function(code) { + goog.asserts.assert(this.isOctalDigit_(code)); + + var str = '0' + String.fromCharCode(code); + var start = this.index_ - 1; + this.increment_(1); + + while (this.index_ < this.length_) { + code = this.getCurrentCharCode_(); + if (!this.isOctalDigit_(code)) { + break; + } + str += String.fromCharCode(code); + this.increment_(1); + } + + code = this.getCurrentCharCode_(); + if (this.isIdentifierStart_(code) || + this.isDecimalDigit_(code)) { + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(code), + index: this.index_ + }); + } + + goog.asserts.assert(!isNaN(parseInt(str, 8)), 'Valid octal: ' + str); + + return { + type: ol.expr.TokenType.NUMERIC_LITERAL, + value: parseInt(str, 8), + index: start + }; +}; + + +/** + * Scan punctuator token (a subset of allowed tokens in 7.7). + * + * @param {number} code The current character code. + * @return {ol.expr.Token} Punctuator token. + * @private + */ +ol.expr.Lexer.prototype.scanPunctuator_ = function(code) { + var start = this.index_; + + // single char punctuation that also doesn't start longer punctuation + // (we disallow assignment, so no += etc.) + if (code === ol.expr.Char.DOT || + code === ol.expr.Char.LEFT_PAREN || + code === ol.expr.Char.RIGHT_PAREN || + code === ol.expr.Char.COMMA || + code === ol.expr.Char.PLUS || + code === ol.expr.Char.MINUS || + code === ol.expr.Char.STAR || + code === ol.expr.Char.SLASH || + code === ol.expr.Char.PERCENT || + code === ol.expr.Char.TILDE) { + + this.increment_(1); + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: String.fromCharCode(code), + index: start + }; + } + + // check for 2-character punctuation + var nextCode = this.getCharCode_(1); + + // assignment or comparison (and we don't allow assignment) + if (nextCode === ol.expr.Char.EQUAL) { + if (code === ol.expr.Char.BANG || code === ol.expr.Char.EQUAL) { + // we're looking at !=, ==, !==, or === + this.increment_(2); + + // check for triple + if (this.getCurrentCharCode_() === ol.expr.Char.EQUAL) { + this.increment_(1); + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: String.fromCharCode(code) + '==', + index: start + }; + } else { + // != or == + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: String.fromCharCode(code) + '=', + index: start + }; + } + } + + if (code === ol.expr.Char.GREATER || + code === ol.expr.Char.LESS) { + this.increment_(2); + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: String.fromCharCode(code) + '=', + index: start + }; + } + } + + // remaining 2-charcter punctuators are || and && + if (code === nextCode && + (code === ol.expr.Char.PIPE || + code === ol.expr.Char.AMPERSAND)) { + + this.increment_(2); + var str = String.fromCharCode(code); + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: str + str, + index: start + }; + } + + // we don't allow 4-character punctuator (>>>=) + // and the allowed 3-character punctuators (!==, ===) are already consumed + + // other single character punctuators + if (code === ol.expr.Char.GREATER || + code === ol.expr.Char.LESS || + code === ol.expr.Char.BANG || + code === ol.expr.Char.AMPERSAND || + code === ol.expr.Char.PIPE) { + + this.increment_(1); + return { + type: ol.expr.TokenType.PUNCTUATOR, + value: String.fromCharCode(code), + index: start + }; + } + + throw new ol.expr.UnexpectedToken({ + type: ol.expr.TokenType.UNKNOWN, + value: String.fromCharCode(code), + index: this.index_ + }); +}; + + +/** + * Scan string literal token. + * + * @param {number} quote The current character code. + * @return {ol.expr.Token} String literal token. + * @private + */ +ol.expr.Lexer.prototype.scanStringLiteral_ = function(quote) { + goog.asserts.assert(quote === ol.expr.Char.SINGLE_QUOTE || + quote === ol.expr.Char.DOUBLE_QUOTE, + 'Strings must start with a quote: ' + String.fromCharCode(quote)); + + var start = this.index_; + this.increment_(1); + + var str = ''; + var code; + while (this.index_ < this.length_) { + code = this.getCurrentCharCode_(); + this.increment_(1); + if (code === quote) { + quote = 0; + break; + } + // look for escaped quote or backslash + if (code === ol.expr.Char.BACKSLASH) { + str += this.getCurrentChar_(); + this.increment_(1); + } else { + str += String.fromCharCode(code); + } + } + + if (quote !== 0) { + // unterminated string literal + throw new ol.expr.UnexpectedToken(this.peek()); + } + + return { + type: ol.expr.TokenType.STRING_LITERAL, + value: str, + index: start + }; +}; + + +/** + * After peeking, skip may be called to advance the cursor without re-scanning. + */ +ol.expr.Lexer.prototype.skip = function() { + this.index_ = this.nextIndex_; +}; + + +/** + * Skip all whitespace. + * @return {number} The character code of the first non-whitespace character. + * @private + */ +ol.expr.Lexer.prototype.skipWhitespace_ = function() { + var code = NaN; + while (this.index_ < this.length_) { + code = this.getCurrentCharCode_(); + if (this.isWhitespace_(code)) { + this.increment_(1); + } else { + break; + } + } + return code; +}; + + + +/** + * Error object for unexpected tokens. + * @param {ol.expr.Token} token The unexpected token. + * @param {string=} opt_message Custom error message. + * @constructor + * @extends {goog.debug.Error} + */ +ol.expr.UnexpectedToken = function(token, opt_message) { + var message = goog.isDef(opt_message) ? opt_message : + 'Unexpected token ' + token.value + ' at index ' + token.index; + + goog.debug.Error.call(this, message); + + /** + * @type {ol.expr.Token} + */ + this.token = token; + +}; +goog.inherits(ol.expr.UnexpectedToken, goog.debug.Error); + + +/** @override */ +ol.expr.UnexpectedToken.prototype.name = 'UnexpectedToken'; diff --git a/src/ol/expr/parser.js b/src/ol/expr/parser.js new file mode 100644 index 0000000000..f11cdd5603 --- /dev/null +++ b/src/ol/expr/parser.js @@ -0,0 +1,478 @@ +/** + * The logic and naming of methods here are inspired by Esprima (BSD Licensed). + * Esprima (http://esprima.org) includes the following copyright notices: + * + * Copyright (C) 2013 Ariya Hidayat + * Copyright (C) 2013 Thaddee Tyl + * Copyright (C) 2012 Ariya Hidayat + * Copyright (C) 2012 Mathias Bynens + * Copyright (C) 2012 Joost-Wim Boekesteijn + * Copyright (C) 2012 Kris Kowal + * Copyright (C) 2012 Yusuke Suzuki + * Copyright (C) 2012 Arpad Borsos + * Copyright (C) 2011 Ariya Hidayat + */ + +goog.provide('ol.expr.Parser'); + +goog.require('goog.asserts'); + +goog.require('ol.expr.Call'); +goog.require('ol.expr.Comparison'); +goog.require('ol.expr.ComparisonOp'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Lexer'); +goog.require('ol.expr.Literal'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.LogicalOp'); +goog.require('ol.expr.Math'); +goog.require('ol.expr.MathOp'); +goog.require('ol.expr.Member'); +goog.require('ol.expr.Not'); +goog.require('ol.expr.Token'); +goog.require('ol.expr.TokenType'); +goog.require('ol.expr.UnexpectedToken'); + + + +/** + * Instances of ol.expr.Parser parse a very limited set of ECMAScript + * expressions (http://www.ecma-international.org/ecma-262/5.1/#sec-11). + * + * - Primary Expression (11.1): + * - Identifier (e.g. `foo`) + * - Literal (e.g. `"some string"` or `42`) + * - Grouped (e.g. `(foo)`) + * - Left-Hand-Side Expression (11.2): + * - Property Accessors + * - Dot notation only + * - Function Calls + * - Identifier with arguments only (e.g. `foo(bar, 42)`) + * - Unary Operators (11.4) + * - Logical Not (e.g. `!foo`) + * - Multiplicitave Operators (11.5) + * - Additive Operators (11.6) + * - Relational Operators (11.8) + * - <, >, <=, and >= only + * - Equality Operators (11.9) + * - Binary Logical Operators (11.11) + * + * @constructor + */ +ol.expr.Parser = function() { +}; + + +/** + * Determine the precedence for the given token. + * + * @param {ol.expr.Token} token A token. + * @return {number} The precedence for the given token. Higher gets more + * precedence. + * @private + */ +ol.expr.Parser.prototype.binaryPrecedence_ = function(token) { + var precedence = 0; + if (token.type !== ol.expr.TokenType.PUNCTUATOR) { + return precedence; + } + + switch (token.value) { + case ol.expr.LogicalOp.OR: + precedence = 1; + break; + case ol.expr.LogicalOp.AND: + precedence = 2; + break; + case ol.expr.ComparisonOp.EQ: + case ol.expr.ComparisonOp.NEQ: + case ol.expr.ComparisonOp.STRICT_EQ: + case ol.expr.ComparisonOp.STRICT_NEQ: + precedence = 3; + break; + case ol.expr.ComparisonOp.GT: + case ol.expr.ComparisonOp.LT: + case ol.expr.ComparisonOp.GTE: + case ol.expr.ComparisonOp.LTE: + precedence = 4; + break; + case ol.expr.MathOp.ADD: + case ol.expr.MathOp.SUBTRACT: + precedence = 5; + break; + case ol.expr.MathOp.MULTIPLY: + case ol.expr.MathOp.DIVIDE: + case ol.expr.MathOp.MOD: + precedence = 6; + break; + default: + // punctuator is not a supported binary operator, that's fine + break; + } + + return precedence; +}; + + +/** + * Create a binary expression. + * + * @param {string} operator Operator. + * @param {ol.expr.Expression} left Left expression. + * @param {ol.expr.Expression} right Right expression. + * @return {ol.expr.Expression} The expression. + * @private + */ +ol.expr.Parser.prototype.createBinaryExpression_ = function(operator, + left, right) { + var expr; + if (ol.expr.Comparison.isValidOp(operator)) { + expr = new ol.expr.Comparison( + /** @type {ol.expr.ComparisonOp.} */ (operator), + left, right); + } else if (ol.expr.Logical.isValidOp(operator)) { + expr = new ol.expr.Logical( + /** @type {ol.expr.LogicalOp.} */ (operator), + left, right); + } else if (ol.expr.Math.isValidOp(operator)) { + expr = new ol.expr.Math( + /** @type {ol.expr.MathOp.} */ (operator), + left, right); + } else { + throw new Error('Unsupported binary operator: ' + operator); + } + return expr; +}; + + +/** + * Create a call expression. + * + * @param {ol.expr.Expression} callee Expression for function. + * @param {Array.} args Arguments array. + * @return {ol.expr.Call} Call expression. + * @private + */ +ol.expr.Parser.prototype.createCallExpression_ = function(callee, args) { + return new ol.expr.Call(callee, args); +}; + + +/** + * Create an identifier expression. + * + * @param {string} name Identifier name. + * @return {ol.expr.Identifier} Identifier expression. + * @private + */ +ol.expr.Parser.prototype.createIdentifier_ = function(name) { + return new ol.expr.Identifier(name); +}; + + +/** + * Create a literal expression. + * + * @param {string|number|boolean|null} value Literal value. + * @return {ol.expr.Literal} The literal expression. + * @private + */ +ol.expr.Parser.prototype.createLiteral_ = function(value) { + return new ol.expr.Literal(value); +}; + + +/** + * Create a member expression. + * + * // TODO: make exp {ol.expr.Member|ol.expr.Identifier} + * @param {ol.expr.Expression} object Expression. + * @param {ol.expr.Identifier} property Member name. + * @return {ol.expr.Member} The member expression. + * @private + */ +ol.expr.Parser.prototype.createMemberExpression_ = function(object, + property) { + return new ol.expr.Member(object, property); +}; + + +/** + * Create a unary expression. The only true unary operator supported here is + * "!". For +/-, we apply the operator to literal expressions and return + * another literal. + * + * @param {ol.expr.Token} op Operator. + * @param {ol.expr.Expression} argument Expression. + * @return {ol.expr.Expression} The unary expression. + * @private + */ +ol.expr.Parser.prototype.createUnaryExpression_ = function(op, argument) { + goog.asserts.assert(op.value === '!' || op.value === '+' || op.value === '-'); + var expr; + if (op.value === '!') { + expr = new ol.expr.Not(argument); + } else if (!(argument instanceof ol.expr.Literal)) { + throw new ol.expr.UnexpectedToken(op); + } else { + // we've got +/- literal + if (op.value === '+') { + expr = this.createLiteral_( + + /** @type {number|string|boolean|null} */ (argument.evaluate())); + } else { + expr = this.createLiteral_( + - /** @type {number|string|boolean|null} */ (argument.evaluate())); + } + } + return expr; +}; + + +/** + * Parse an expression. + * + * @param {string} source Expression source. + * @return {ol.expr.Expression} Expression. + */ +ol.expr.Parser.prototype.parse = function(source) { + var lexer = new ol.expr.Lexer(source); + var expr = this.parseExpression_(lexer); + var token = lexer.peek(); + if (token.type !== ol.expr.TokenType.EOF) { + throw new ol.expr.UnexpectedToken(token); + } + return expr; +}; + + +/** + * Parse call arguments + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.2.4 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {Array.} Arguments. + * @private + */ +ol.expr.Parser.prototype.parseArguments_ = function(lexer) { + var args = []; + + lexer.expect('('); + + if (!lexer.match(')')) { + while (true) { + args.push(this.parseBinaryExpression_(lexer)); + if (lexer.match(')')) { + break; + } + lexer.expect(','); + } + } + lexer.skip(); + + return args; +}; + + +/** + * Parse a binary expression. Supported binary expressions: + * + * - Multiplicative Operators (`*`, `/`, `%`) + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.5 + + * - Additive Operators (`+`, `-`) + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.6 + * + * - Relational Operators (`<`, `>`, `<=`, `>=`) + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.8 + * + * - Equality Operators (`==`, `!=`, `===`, `!==`) + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.9 + * + * - Binary Logical Operators (`&&`, `||`) + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.11 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parseBinaryExpression_ = function(lexer) { + var left = this.parseUnaryExpression_(lexer); + + var operator = lexer.peek(); + var precedence = this.binaryPrecedence_(operator); + if (precedence === 0) { + // not a supported binary operator + return left; + } + lexer.skip(); + + var right = this.parseUnaryExpression_(lexer); + var stack = [left, operator, right]; + + precedence = this.binaryPrecedence_(lexer.peek()); + while (precedence > 0) { + // TODO: cache operator precedence in stack + while (stack.length > 2 && + (precedence <= this.binaryPrecedence_(stack[stack.length - 2]))) { + right = stack.pop(); + operator = stack.pop(); + left = stack.pop(); + stack.push(this.createBinaryExpression_(operator.value, left, right)); + } + stack.push(lexer.next()); + stack.push(this.parseUnaryExpression_(lexer)); + precedence = this.binaryPrecedence_(lexer.peek()); + } + + var i = stack.length - 1; + var expr = stack[i]; + while (i > 1) { + expr = this.createBinaryExpression_(stack[i - 1].value, stack[i - 2], expr); + i -= 2; + } + + return expr; +}; + + +/** + * Parse a group expression. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.1.6 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parseGroupExpression_ = function(lexer) { + lexer.expect('('); + var expr = this.parseExpression_(lexer); + lexer.expect(')'); + return expr; +}; + + +/** + * Parse left-hand-side expression. Limited to Member Expressions + * and Call Expressions. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.2 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parseLeftHandSideExpression_ = function(lexer) { + var expr = this.parsePrimaryExpression_(lexer); + var token = lexer.peek(); + if (token.value === '(') { + // only allow calls on identifiers (e.g. `foo()` not `foo.bar()`) + if (!(expr instanceof ol.expr.Identifier)) { + // TODO: more helpful error messages for restricted syntax + throw new ol.expr.UnexpectedToken(token); + } + var args = this.parseArguments_(lexer); + expr = this.createCallExpression_(expr, args); + } else { + // TODO: throw if not Identifier + while (token.value === '.') { + var property = this.parseNonComputedMember_(lexer); + expr = this.createMemberExpression_(expr, property); + token = lexer.peek(); + } + } + return expr; +}; + + +/** + * Parse non-computed member. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.2 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Identifier} Expression. + * @private + */ +ol.expr.Parser.prototype.parseNonComputedMember_ = function(lexer) { + lexer.expect('.'); + + var token = lexer.next(); + if (token.type !== ol.expr.TokenType.IDENTIFIER && + token.type !== ol.expr.TokenType.KEYWORD && + token.type !== ol.expr.TokenType.BOOLEAN_LITERAL && + token.type !== ol.expr.TokenType.NULL_LITERAL) { + throw new ol.expr.UnexpectedToken(token); + } + + return this.createIdentifier_(String(token.value)); +}; + + +/** + * Parse primary expression. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.1 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parsePrimaryExpression_ = function(lexer) { + var token = lexer.peek(); + if (token.value === '(') { + return this.parseGroupExpression_(lexer); + } + lexer.skip(); + var expr; + var type = token.type; + if (type === ol.expr.TokenType.IDENTIFIER) { + expr = this.createIdentifier_(/** @type {string} */ (token.value)); + } else if (type === ol.expr.TokenType.STRING_LITERAL || + type === ol.expr.TokenType.NUMERIC_LITERAL) { + // numeric and string literals are already the correct type + expr = this.createLiteral_(token.value); + } else if (type === ol.expr.TokenType.BOOLEAN_LITERAL) { + // because booleans are valid member properties, tokens are still string + expr = this.createLiteral_(token.value === 'true'); + } else if (type === ol.expr.TokenType.NULL_LITERAL) { + expr = this.createLiteral_(null); + } else { + throw new ol.expr.UnexpectedToken(token); + } + return expr; +}; + + +/** + * Parse expression with a unary operator. Limited to logical not operator. + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.4 + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parseUnaryExpression_ = function(lexer) { + var expr; + var operator = lexer.peek(); + if (operator.type !== ol.expr.TokenType.PUNCTUATOR) { + expr = this.parseLeftHandSideExpression_(lexer); + } else if (operator.value === '!' || operator.value === '-' || + operator.value === '+') { + lexer.skip(); + expr = this.parseUnaryExpression_(lexer); + expr = this.createUnaryExpression_(operator, expr); + } else { + expr = this.parseLeftHandSideExpression_(lexer); + } + return expr; +}; + + +/** + * Parse an expression. + * + * @param {ol.expr.Lexer} lexer Lexer. + * @return {ol.expr.Expression} Expression. + * @private + */ +ol.expr.Parser.prototype.parseExpression_ = function(lexer) { + return this.parseBinaryExpression_(lexer); +}; diff --git a/src/ol/expression.exports b/src/ol/expression.exports deleted file mode 100644 index 911b8a12c6..0000000000 --- a/src/ol/expression.exports +++ /dev/null @@ -1 +0,0 @@ -@exportSymbol ol.Expression diff --git a/src/ol/expression.js b/src/ol/expression.js deleted file mode 100644 index 7b1370ae0d..0000000000 --- a/src/ol/expression.js +++ /dev/null @@ -1,80 +0,0 @@ -goog.provide('ol.Expression'); -goog.provide('ol.ExpressionLiteral'); - - - -/** - * Create a new expression. Expressions are used for instance to bind - * symbolizer properties to feature attributes. - * - * Example: - * - * // take the color from the color attribute - * color: new ol.Expression('color'); - * // take the strokeWidth from the width attribute and multiply by 2. - * strokeWidth: new ol.Expression('width*2'); - * - * @constructor - * @param {string} source Expression to be evaluated. - */ -ol.Expression = function(source) { - - /** - * @type {string} - * @private - */ - this.source_ = source; - -}; - - -/** - * Evaluate the expression and return the result. - * - * @param {Object=} opt_thisArg Object to use as this when evaluating the - * expression. If not provided, the global object will be used. - * @param {Object=} opt_scope Evaluation scope. All properties of this object - * will be available as variables when evaluating the expression. If not - * provided, the global object will be used. - * @return {*} Result of the expression. - */ -ol.Expression.prototype.evaluate = function(opt_thisArg, opt_scope) { - var thisArg = goog.isDef(opt_thisArg) ? opt_thisArg : goog.global, - scope = goog.isDef(opt_scope) ? opt_scope : goog.global, - names = [], - values = []; - - for (var name in scope) { - names.push(name); - values.push(scope[name]); - } - - var evaluator = new Function(names.join(','), 'return ' + this.source_); - return evaluator.apply(thisArg, values); -}; - - - -/** - * @constructor - * @extends {ol.Expression} - * @param {*} value Literal value. - */ -ol.ExpressionLiteral = function(value) { - - /** - * @type {*} - * @private - */ - this.value_ = value; - -}; -goog.inherits(ol.ExpressionLiteral, ol.Expression); - - -/** - * @inheritDoc - */ -ol.ExpressionLiteral.prototype.evaluate = function(opt_thisArg, opt_scope) { - return this.value_; -}; diff --git a/src/ol/filter.exports b/src/ol/filter.exports deleted file mode 100644 index 696edee702..0000000000 --- a/src/ol/filter.exports +++ /dev/null @@ -1,7 +0,0 @@ -@exportSymbol ol.filter.Filter -@exportSymbol ol.filter.Geometry -@exportSymbol ol.filter.Logical - -@exportSymbol ol.filter.LogicalOperator -@exportProperty ol.filter.LogicalOperator.AND -@exportProperty ol.filter.LogicalOperator.OR diff --git a/src/ol/filter.jsdoc b/src/ol/filter.jsdoc deleted file mode 100644 index b9fad2900f..0000000000 --- a/src/ol/filter.jsdoc +++ /dev/null @@ -1,3 +0,0 @@ -/** - * @namespace ol.filter - */ diff --git a/src/ol/filter/extentfilter.js b/src/ol/filter/extentfilter.js deleted file mode 100644 index 2892246519..0000000000 --- a/src/ol/filter/extentfilter.js +++ /dev/null @@ -1,39 +0,0 @@ -goog.provide('ol.filter.Extent'); - -goog.require('ol.extent'); -goog.require('ol.filter.Filter'); - - - -/** - * @constructor - * @extends {ol.filter.Filter} - * @param {ol.Extent} extent The extent. - */ -ol.filter.Extent = function(extent) { - goog.base(this); - - /** - * @type {ol.Extent} - * @private - */ - this.extent_ = extent; - -}; -goog.inherits(ol.filter.Extent, ol.filter.Filter); - - -/** - * @return {ol.Extent} The filter extent. - */ -ol.filter.Extent.prototype.getExtent = function() { - return this.extent_; -}; - - -/** - * @inheritDoc - */ -ol.filter.Extent.prototype.applies = function(feature) { - return ol.extent.intersects(feature.getGeometry().getBounds(), this.extent_); -}; diff --git a/src/ol/filter/filter.js b/src/ol/filter/filter.js deleted file mode 100644 index 0d37a211dd..0000000000 --- a/src/ol/filter/filter.js +++ /dev/null @@ -1,36 +0,0 @@ -goog.provide('ol.filter.Filter'); - -goog.require('ol.Feature'); - - - -/** - * Create a new filter which can be used to filter features based on a - * function. - * - * Example: - * - * new ol.style.Rule({ - * filter: new ol.filter.Filter(function(feature) { - * return feature.get('where') == 'outer'; - * }), - * symbolizers: [ - * ... - * - * @constructor - * @param {function(this:ol.filter.Filter, ol.Feature)=} opt_filterFunction - * Filter function. Should return true if the passed feature passes the - * filter, false otherwise. - */ -ol.filter.Filter = function(opt_filterFunction) { - if (goog.isDef(opt_filterFunction)) { - this.applies = opt_filterFunction; - } -}; - - -/** - * @param {ol.Feature} feature Feature to evaluate the filter against. - * @return {boolean} The provided feature passes this filter. - */ -ol.filter.Filter.prototype.applies = goog.abstractMethod; diff --git a/src/ol/filter/geometryfilter.js b/src/ol/filter/geometryfilter.js deleted file mode 100644 index 1f6707f7e3..0000000000 --- a/src/ol/filter/geometryfilter.js +++ /dev/null @@ -1,42 +0,0 @@ -goog.provide('ol.filter.Geometry'); -goog.provide('ol.filter.GeometryType'); - -goog.require('ol.filter.Filter'); -goog.require('ol.geom.GeometryType'); - - - -/** - * Filter features by geometry type. - * @constructor - * @extends {ol.filter.Filter} - * @param {ol.geom.GeometryType} type The geometry type. - */ -ol.filter.Geometry = function(type) { - goog.base(this); - - /** - * @type {ol.geom.GeometryType} - * @private - */ - this.type_ = type; - -}; -goog.inherits(ol.filter.Geometry, ol.filter.Filter); - - -/** - * @inheritDoc - */ -ol.filter.Geometry.prototype.applies = function(feature) { - var geometry = feature.getGeometry(); - return goog.isNull(geometry) ? false : geometry.getType() === this.type_; -}; - - -/** - * @return {ol.geom.GeometryType} The geometry type. - */ -ol.filter.Geometry.prototype.getType = function() { - return this.type_; -}; diff --git a/src/ol/filter/logicalfilter.js b/src/ol/filter/logicalfilter.js deleted file mode 100644 index 1e97edad1e..0000000000 --- a/src/ol/filter/logicalfilter.js +++ /dev/null @@ -1,128 +0,0 @@ -goog.provide('ol.filter.Logical'); -goog.provide('ol.filter.LogicalOperator'); -goog.provide('ol.filter.and'); -goog.provide('ol.filter.not'); -goog.provide('ol.filter.or'); - -goog.require('goog.asserts'); -goog.require('ol.filter.Filter'); - - - -/** - * A filter to group (a) subfilter(s) by a logical operator (AND/OR/NOT). - * - * Example: - * - * var f1 = new ol.filter.Filter(myFunc1); - * var f2 = new ol.filter.Filter(myFunc2); - * // match one of the two filters above - * var logical = new ol.filter.Logical([f1, f2], - * ol.filter.LogicalOperator.OR); - * - * @constructor - * @extends {ol.filter.Filter} - * @param {Array.} filters Filters to combine. - * @param {ol.filter.LogicalOperator} operator Operator. - */ -ol.filter.Logical = function(filters, operator) { - goog.base(this); - - /** - * @type {Array.} - * @private - */ - this.filters_ = filters; - goog.asserts.assert(filters.length > 0, 'Must supply at least one filter'); - - /** - * @type {ol.filter.LogicalOperator} - */ - this.operator = operator; - -}; -goog.inherits(ol.filter.Logical, ol.filter.Filter); - - -/** - * @inheritDoc - */ -ol.filter.Logical.prototype.applies = function(feature) { - var filters = this.filters_, - i = 0, ii = filters.length, - result; - switch (this.operator) { - case ol.filter.LogicalOperator.AND: - result = true; - while (result && i < ii) { - result = result && filters[i].applies(feature); - ++i; - } - break; - case ol.filter.LogicalOperator.OR: - result = false; - while (!result && i < ii) { - result = result || filters[i].applies(feature); - ++i; - } - break; - case ol.filter.LogicalOperator.NOT: - result = !filters[i].applies(feature); - break; - default: - goog.asserts.assert(false, 'Unsupported operation: ' + this.operator); - } - return !!result; -}; - - -/** - * @return {Array.} The filter's filters. - */ -ol.filter.Logical.prototype.getFilters = function() { - return this.filters_; -}; - - -/** - * @enum {string} - */ -ol.filter.LogicalOperator = { - AND: '&&', - OR: '||', - NOT: '!' -}; - - -/** - * Create a filter that evaluates to true if all of the provided filters - * evaluate to true. - * @param {...ol.filter.Filter} var_args Filters. - * @return {ol.filter.Logical} A logical AND filter. - */ -ol.filter.and = function(var_args) { - var filters = Array.prototype.slice.call(arguments); - return new ol.filter.Logical(filters, ol.filter.LogicalOperator.AND); -}; - - -/** - * Create a new filter that is the logical compliment of another. - * @param {ol.filter.Filter} filter The filter to negate. - * @return {ol.filter.Logical} A logical NOT filter. - */ -ol.filter.not = function(filter) { - return new ol.filter.Logical([filter], ol.filter.LogicalOperator.NOT); -}; - - -/** - * Create a filter that evaluates to true if any of the provided filters - * evaluate to true. - * @param {...ol.filter.Filter} var_args Filters. - * @return {ol.filter.Logical} A logical OR filter. - */ -ol.filter.or = function(var_args) { - var filters = Array.prototype.slice.call(arguments); - return new ol.filter.Logical(filters, ol.filter.LogicalOperator.OR); -}; diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index 12ffb250d5..264c4c9fa4 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -5,6 +5,11 @@ goog.require('goog.asserts'); goog.require('goog.events.EventType'); goog.require('goog.object'); goog.require('ol.Feature'); +goog.require('ol.expr'); +goog.require('ol.expr.Literal'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.LogicalOp'); +goog.require('ol.expr.functions'); goog.require('ol.geom.GeometryType'); goog.require('ol.geom.SharedVertices'); goog.require('ol.layer.Layer'); @@ -79,35 +84,65 @@ ol.layer.FeatureCache.prototype.add = function(feature) { /** - * @param {ol.filter.Filter=} opt_filter Optional filter. + * @param {ol.expr.Expression=} opt_expr Expression for filtering. * @return {Object.} Object of features, keyed by id. */ -ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_filter) { - var i, features; - if (!goog.isDef(opt_filter)) { +ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_expr) { + var features; + if (!goog.isDef(opt_expr)) { features = this.idLookup_; } else { - if (opt_filter instanceof ol.filter.Geometry) { - features = this.geometryTypeIndex_[opt_filter.getType()]; - } else if (opt_filter instanceof ol.filter.Extent) { - features = this.rTree_.searchReturningObject(opt_filter.getExtent()); - } else if (opt_filter instanceof ol.filter.Logical && - opt_filter.operator === ol.filter.LogicalOperator.AND) { - var filters = opt_filter.getFilters(); - if (filters.length === 2) { - var filter, geometryFilter, extentFilter; - for (i = 0; i <= 1; ++i) { - filter = filters[i]; - if (filter instanceof ol.filter.Geometry) { - geometryFilter = filter; - } else if (filter instanceof ol.filter.Extent) { - extentFilter = filter; + // check for geometryType or extent expression + var name = ol.expr.isLibCall(opt_expr); + if (name === ol.expr.functions.GEOMETRY_TYPE) { + var args = /** @type {ol.expr.Call} */ (opt_expr).getArgs(); + goog.asserts.assert(args.length === 1); + goog.asserts.assert(args[0] instanceof ol.expr.Literal); + var type = /** @type {ol.expr.Literal } */ (args[0]).evaluate(); + goog.asserts.assertString(type); + features = this.geometryTypeIndex_[type]; + } else if (name === ol.expr.functions.EXTENT) { + var args = /** @type {ol.expr.Call} */ (opt_expr).getArgs(); + goog.asserts.assert(args.length === 4); + var extent = []; + for (var i = 0; i < 4; ++i) { + goog.asserts.assert(args[i] instanceof ol.expr.Literal); + extent[i] = /** @type {ol.expr.Literal} */ (args[i]).evaluate(); + goog.asserts.assertNumber(extent[i]); + } + features = this.rTree_.searchReturningObject(extent); + } else { + // not a call expression, check logical + if (opt_expr instanceof ol.expr.Logical) { + var op = /** @type {ol.expr.Logical} */ (opt_expr).getOperator(); + if (op === ol.expr.LogicalOp.AND) { + var expressions = [opt_expr.getLeft(), opt_expr.getRight()]; + var expr, args, type, extent; + for (var i = 0; i <= 1; ++i) { + expr = expressions[i]; + name = ol.expr.isLibCall(expr); + if (name === ol.expr.functions.GEOMETRY_TYPE) { + args = /** @type {ol.expr.Call} */ (expr).getArgs(); + goog.asserts.assert(args.length === 1); + goog.asserts.assert(args[0] instanceof ol.expr.Literal); + type = /** @type {ol.expr.Literal } */ (args[0]).evaluate(); + goog.asserts.assertString(type); + } else if (name === ol.expr.functions.EXTENT) { + args = /** @type {ol.expr.Call} */ (expr).getArgs(); + goog.asserts.assert(args.length === 4); + extent = []; + for (var j = 0; j < 4; ++j) { + goog.asserts.assert(args[j] instanceof ol.expr.Literal); + extent[j] = + /** @type {ol.expr.Literal} */ (args[j]).evaluate(); + goog.asserts.assertNumber(extent[j]); + } + } + } + if (type && extent) { + features = this.getFeaturesObjectForExtent(extent, + /** @type {ol.geom.GeometryType} */ (type)); } - } - if (extentFilter && geometryFilter) { - var type = geometryFilter.getType(); - features = goog.object.isEmpty(this.geometryTypeIndex_[type]) ? {} : - this.rTree_.searchReturningObject(extentFilter.getExtent(), type); } } } @@ -118,7 +153,7 @@ ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_filter) { features = {}; for (i in candidates) { feature = candidates[i]; - if (opt_filter.applies(feature) === true) { + if (ol.expr.evaluateFeature(opt_expr, feature)) { features[i] = feature; } } @@ -129,12 +164,22 @@ ol.layer.FeatureCache.prototype.getFeaturesObject = function(opt_filter) { /** - * @param {ol.filter.Geometry} filter Geometry type filter. - * @return {Array.} Array of features. - * @private + * Get all features whose bounding box intersects the provided extent. + * + * @param {ol.Extent} extent Bounding extent. + * @param {ol.geom.GeometryType=} opt_type Optional geometry type. + * @return {Object.} Features. */ -ol.layer.FeatureCache.prototype.getFeaturesByGeometryType_ = function(filter) { - return goog.object.getValues(this.geometryTypeIndex_[filter.getType()]); +ol.layer.FeatureCache.prototype.getFeaturesObjectForExtent = function(extent, + opt_type) { + var features; + if (goog.isDef(opt_type) && + goog.object.isEmpty(this.geometryTypeIndex_[opt_type])) { + features = {}; + } else { + features = this.rTree_.searchReturningObject(extent, opt_type); + } + return features; }; @@ -234,21 +279,34 @@ ol.layer.Vector.prototype.getVectorSource = function() { /** - * @param {ol.filter.Filter=} opt_filter Optional filter. + * @param {ol.expr.Expression=} opt_expr Expression for filtering. * @return {Array.} Array of features. */ -ol.layer.Vector.prototype.getFeatures = function(opt_filter) { +ol.layer.Vector.prototype.getFeatures = function(opt_expr) { return goog.object.getValues( - this.featureCache_.getFeaturesObject(opt_filter)); + this.featureCache_.getFeaturesObject(opt_expr)); }; /** - * @param {ol.filter.Filter=} opt_filter Optional filter. + * @param {ol.expr.Expression=} opt_expr Expression for filtering. * @return {Object.} Features. */ -ol.layer.Vector.prototype.getFeaturesObject = function(opt_filter) { - return this.featureCache_.getFeaturesObject(opt_filter); +ol.layer.Vector.prototype.getFeaturesObject = function(opt_expr) { + return this.featureCache_.getFeaturesObject(opt_expr); +}; + + +/** + * Get all features whose bounding box intersects the provided extent. + * + * @param {ol.Extent} extent Bounding extent. + * @param {ol.geom.GeometryType=} opt_type Optional geometry type. + * @return {Object.} Features. + */ +ol.layer.Vector.prototype.getFeaturesObjectForExtent = function(extent, + opt_type) { + return this.featureCache_.getFeaturesObjectForExtent(extent, opt_type); }; @@ -415,10 +473,3 @@ ol.layer.Vector.uidTransformFeatureInfo = function(features) { function(feature) { return goog.getUid(feature); }); return featureIds.join(', '); }; - - -goog.require('ol.filter.Extent'); -goog.require('ol.filter.Geometry'); -goog.require('ol.filter.Logical'); -goog.require('ol.filter.LogicalOperator'); -goog.require('ol.geom.GeometryType'); diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index f690fbc589..343d783f72 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -12,10 +12,6 @@ goog.require('ol.TileCoord'); goog.require('ol.TileRange'); goog.require('ol.ViewHint'); goog.require('ol.extent'); -goog.require('ol.filter.Extent'); -goog.require('ol.filter.Geometry'); -goog.require('ol.filter.Logical'); -goog.require('ol.filter.LogicalOperator'); goog.require('ol.geom.GeometryType'); goog.require('ol.layer.Vector'); goog.require('ol.renderer.canvas.Layer'); @@ -101,18 +97,18 @@ ol.renderer.canvas.VectorLayer = function(mapRenderer, layer) { this.tileArchetype_ = null; /** - * Geometry filters in rendering order. + * Geometry types in rendering order. * TODO: these will go away shortly (in favor of one call per symbolizer type) * @private - * @type {Array.} + * @type {Array.} */ - this.geometryFilters_ = [ - new ol.filter.Geometry(ol.geom.GeometryType.POINT), - new ol.filter.Geometry(ol.geom.GeometryType.MULTIPOINT), - new ol.filter.Geometry(ol.geom.GeometryType.LINESTRING), - new ol.filter.Geometry(ol.geom.GeometryType.MULTILINESTRING), - new ol.filter.Geometry(ol.geom.GeometryType.POLYGON), - new ol.filter.Geometry(ol.geom.GeometryType.MULTIPOLYGON) + this.geometryTypes_ = [ + ol.geom.GeometryType.POINT, + ol.geom.GeometryType.MULTIPOINT, + ol.geom.GeometryType.LINESTRING, + ol.geom.GeometryType.MULTILINESTRING, + ol.geom.GeometryType.POLYGON, + ol.geom.GeometryType.MULTIPOLYGON ]; /** @@ -251,13 +247,12 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = var locationMin = [location[0] - halfMaxWidth, location[1] - halfMaxHeight]; var locationMax = [location[0] + halfMaxWidth, location[1] + halfMaxHeight]; var locationBbox = ol.extent.boundingExtent([locationMin, locationMax]); - var filter = new ol.filter.Extent(locationBbox); - var candidates = layer.getFeatures(filter); + var candidates = layer.getFeaturesObjectForExtent(locationBbox); var candidate, geom, type, symbolBounds, symbolSize, halfWidth, halfHeight, coordinates, j; - for (var i = 0, ii = candidates.length; i < ii; ++i) { - candidate = candidates[i]; + for (var id in candidates) { + candidate = candidates[id]; geom = candidate.getGeometry(); type = geom.getType(); if (type === ol.geom.GeometryType.POINT || @@ -445,11 +440,11 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = var tileGutter = 15 * tileResolution; var tile, tileCoord, key, x, y; // render features by geometry type - var filters = this.geometryFilters_, - numFilters = filters.length, + var types = this.geometryTypes_, + numTypes = types.length, deferred = false, dirty = false, - i, geomFilter, tileExtent, extentFilter, type, + i, type, tileExtent, groups, group, j, numGroups, featuresObject, tileHasFeatures; for (x = tileRange.minX; x <= tileRange.maxX; ++x) { for (y = tileRange.minY; y <= tileRange.maxY; ++y) { @@ -463,16 +458,13 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = tileExtent[1] += tileGutter; tileExtent[2] -= tileGutter; tileExtent[3] += tileGutter; - extentFilter = new ol.filter.Extent(tileExtent); tileHasFeatures = false; - for (i = 0; i < numFilters; ++i) { - geomFilter = filters[i]; - type = geomFilter.getType(); + for (i = 0; i < numTypes; ++i) { + type = types[i]; if (!goog.isDef(featuresToRender[type])) { featuresToRender[type] = {}; } - featuresObject = layer.getFeaturesObject(new ol.filter.Logical( - [geomFilter, extentFilter], ol.filter.LogicalOperator.AND)); + featuresObject = layer.getFeaturesObjectForExtent(tileExtent, type); tileHasFeatures = tileHasFeatures || !goog.object.isEmpty(featuresObject); goog.object.extend(featuresToRender[type], featuresObject); diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index 02d4924b3c..183412b679 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -272,22 +272,12 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = ol.renderer.canvas.VectorRenderer.prototype.renderText_ = function(features, text, texts) { var context = this.context_, - fontArray = [], - color = text.color, vecs, vec; - if (color) { - context.fillStyle = color; - } - if (goog.isDef(text.fontSize)) { - fontArray.push(text.fontSize + 'px'); - } - if (goog.isDef(text.fontFamily)) { - fontArray.push(text.fontFamily); - } - if (fontArray.length) { - context.font = fontArray.join(' '); + if (context.fillStyle !== text.color) { + context.fillStyle = text.color; } + context.font = text.fontSize + 'px ' + text.fontFamily; context.globalAlpha = text.opacity; // TODO: make alignments configurable diff --git a/src/ol/style/icon.js b/src/ol/style/icon.js index 2be1658c24..9be3af41a8 100644 --- a/src/ol/style/icon.js +++ b/src/ol/style/icon.js @@ -3,8 +3,9 @@ goog.provide('ol.style.IconLiteral'); goog.provide('ol.style.IconType'); goog.require('goog.asserts'); -goog.require('ol.Expression'); -goog.require('ol.ExpressionLiteral'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Point'); goog.require('ol.style.PointLiteral'); @@ -69,47 +70,47 @@ ol.style.Icon = function(options) { goog.asserts.assert(options.url, 'url must be set'); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ - this.url_ = (options.url instanceof ol.Expression) ? - options.url : new ol.ExpressionLiteral(options.url); + this.url_ = (options.url instanceof ol.expr.Expression) ? + options.url : new ol.expr.Literal(options.url); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.width_ = !goog.isDef(options.width) ? null : - (options.width instanceof ol.Expression) ? - options.width : new ol.ExpressionLiteral(options.width); + (options.width instanceof ol.expr.Expression) ? + options.width : new ol.expr.Literal(options.width); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.height_ = !goog.isDef(options.height) ? null : - (options.height instanceof ol.Expression) ? - options.height : new ol.ExpressionLiteral(options.height); + (options.height instanceof ol.expr.Expression) ? + options.height : new ol.expr.Literal(options.height); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.opacity_ = !goog.isDef(options.opacity) ? - new ol.ExpressionLiteral(ol.style.IconDefaults.opacity) : - (options.opacity instanceof ol.Expression) ? - options.opacity : new ol.ExpressionLiteral(options.opacity); + new ol.expr.Literal(ol.style.IconDefaults.opacity) : + (options.opacity instanceof ol.expr.Expression) ? + options.opacity : new ol.expr.Literal(options.opacity); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.rotation_ = !goog.isDef(options.rotation) ? - new ol.ExpressionLiteral(ol.style.IconDefaults.rotation) : - (options.rotation instanceof ol.Expression) ? - options.rotation : new ol.ExpressionLiteral(options.rotation); + new ol.expr.Literal(ol.style.IconDefaults.rotation) : + (options.rotation instanceof ol.expr.Expression) ? + options.rotation : new ol.expr.Literal(options.rotation); }; @@ -118,27 +119,28 @@ ol.style.Icon = function(options) { * @inheritDoc * @return {ol.style.IconLiteral} Literal shape symbolizer. */ -ol.style.Icon.prototype.createLiteral = function(feature) { - var attrs = feature && feature.getAttributes(); +ol.style.Icon.prototype.createLiteral = function(opt_feature) { - var url = /** @type {string} */ (this.url_.evaluate(feature, attrs)); - goog.asserts.assert(goog.isString(url) && url != '#', 'url must be a string'); + var url = ol.expr.evaluateFeature(this.url_, opt_feature); + goog.asserts.assertString(url, 'url must be a string'); + goog.asserts.assert(url != '#', 'url must not be "#"'); - var width = /** @type {number|undefined} */ (goog.isNull(this.width_) ? - undefined : this.width_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(width) || goog.isNumber(width), - 'width must be undefined or a number'); + var width; + if (!goog.isNull(this.width_)) { + width = ol.expr.evaluateFeature(this.width_, opt_feature); + goog.asserts.assertNumber(width, 'width must be a number'); + } - var height = /** @type {number|undefined} */ (goog.isNull(this.height_) ? - undefined : this.height_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(height) || goog.isNumber(height), - 'height must be undefined or a number'); + var height; + if (!goog.isNull(this.height_)) { + height = ol.expr.evaluateFeature(this.height_, opt_feature); + goog.asserts.assertNumber(height, 'height must be a number'); + } - var opacity = /** {@type {number} */ (this.opacity_.evaluate(feature, attrs)); + var opacity = ol.expr.evaluateFeature(this.opacity_, opt_feature); goog.asserts.assertNumber(opacity, 'opacity must be a number'); - var rotation = - /** {@type {number} */ (this.rotation_.evaluate(feature, attrs)); + var rotation = ol.expr.evaluateFeature(this.rotation_, opt_feature); goog.asserts.assertNumber(rotation, 'rotation must be a number'); return new ol.style.IconLiteral({ diff --git a/src/ol/style/line.js b/src/ol/style/line.js index 95460b8d45..5fc17a2e7a 100644 --- a/src/ol/style/line.js +++ b/src/ol/style/line.js @@ -2,8 +2,9 @@ goog.provide('ol.style.Line'); goog.provide('ol.style.LineLiteral'); goog.require('goog.asserts'); -goog.require('ol.Expression'); -goog.require('ol.ExpressionLiteral'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Symbolizer'); goog.require('ol.style.SymbolizerLiteral'); @@ -63,31 +64,31 @@ ol.style.Line = function(options) { goog.base(this); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeColor_ = !goog.isDef(options.strokeColor) ? - new ol.ExpressionLiteral(ol.style.LineDefaults.strokeColor) : - (options.strokeColor instanceof ol.Expression) ? - options.strokeColor : new ol.ExpressionLiteral(options.strokeColor); + new ol.expr.Literal(ol.style.LineDefaults.strokeColor) : + (options.strokeColor instanceof ol.expr.Expression) ? + options.strokeColor : new ol.expr.Literal(options.strokeColor); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeWidth_ = !goog.isDef(options.strokeWidth) ? - new ol.ExpressionLiteral(ol.style.LineDefaults.strokeWidth) : - (options.strokeWidth instanceof ol.Expression) ? - options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth); + new ol.expr.Literal(ol.style.LineDefaults.strokeWidth) : + (options.strokeWidth instanceof ol.expr.Expression) ? + options.strokeWidth : new ol.expr.Literal(options.strokeWidth); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.opacity_ = !goog.isDef(options.opacity) ? - new ol.ExpressionLiteral(ol.style.LineDefaults.opacity) : - (options.opacity instanceof ol.Expression) ? - options.opacity : new ol.ExpressionLiteral(options.opacity); + new ol.expr.Literal(ol.style.LineDefaults.opacity) : + (options.opacity instanceof ol.expr.Expression) ? + options.opacity : new ol.expr.Literal(options.opacity); }; goog.inherits(ol.style.Line, ol.style.Symbolizer); @@ -98,19 +99,16 @@ goog.inherits(ol.style.Line, ol.style.Symbolizer); * @return {ol.style.LineLiteral} Literal line symbolizer. */ ol.style.Line.prototype.createLiteral = function(opt_feature) { - var attrs, - feature = opt_feature; - if (goog.isDef(feature)) { - attrs = feature.getAttributes(); - } - var strokeColor = this.strokeColor_.evaluate(feature, attrs); + var strokeColor = ol.expr.evaluateFeature( + this.strokeColor_, opt_feature); goog.asserts.assertString(strokeColor, 'strokeColor must be a string'); - var strokeWidth = this.strokeWidth_.evaluate(feature, attrs); + var strokeWidth = ol.expr.evaluateFeature( + this.strokeWidth_, opt_feature); goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number'); - var opacity = this.opacity_.evaluate(feature, attrs); + var opacity = ol.expr.evaluateFeature(this.opacity_, opt_feature); goog.asserts.assertNumber(opacity, 'opacity must be a number'); return new ol.style.LineLiteral({ diff --git a/src/ol/style/polygon.js b/src/ol/style/polygon.js index b207a1ebec..57fa4afb6b 100644 --- a/src/ol/style/polygon.js +++ b/src/ol/style/polygon.js @@ -2,8 +2,9 @@ goog.provide('ol.style.Polygon'); goog.provide('ol.style.PolygonLiteral'); goog.require('goog.asserts'); -goog.require('ol.Expression'); -goog.require('ol.ExpressionLiteral'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Symbolizer'); goog.require('ol.style.SymbolizerLiteral'); @@ -80,13 +81,13 @@ ol.style.Polygon = function(options) { goog.base(this); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.fillColor_ = !goog.isDefAndNotNull(options.fillColor) ? null : - (options.fillColor instanceof ol.Expression) ? - options.fillColor : new ol.ExpressionLiteral(options.fillColor); + (options.fillColor instanceof ol.expr.Expression) ? + options.fillColor : new ol.expr.Literal(options.fillColor); // stroke handling - if any stroke property is supplied, use defaults var strokeColor = null, @@ -95,25 +96,33 @@ ol.style.Polygon = function(options) { if (goog.isDefAndNotNull(options.strokeColor) || goog.isDefAndNotNull(options.strokeWidth)) { - strokeColor = !goog.isDefAndNotNull(options.strokeColor) ? - new ol.ExpressionLiteral(ol.style.PolygonDefaults.strokeColor) : - (options.strokeColor instanceof ol.Expression) ? - options.strokeColor : new ol.ExpressionLiteral(options.strokeColor); + if (goog.isDefAndNotNull(options.strokeColor)) { + strokeColor = (options.strokeColor instanceof ol.expr.Expression) ? + options.strokeColor : + new ol.expr.Literal(options.strokeColor); + } else { + strokeColor = new ol.expr.Literal( + /** @type {string} */ (ol.style.PolygonDefaults.strokeColor)); + } - strokeWidth = !goog.isDef(options.strokeWidth) ? - new ol.ExpressionLiteral(ol.style.PolygonDefaults.strokeWidth) : - (options.strokeWidth instanceof ol.Expression) ? - options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth); + if (goog.isDefAndNotNull(options.strokeWidth)) { + strokeWidth = (options.strokeWidth instanceof ol.expr.Expression) ? + options.strokeWidth : + new ol.expr.Literal(options.strokeWidth); + } else { + strokeWidth = new ol.expr.Literal( + /** @type {number} */ (ol.style.PolygonDefaults.strokeWidth)); + } } /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeColor_ = strokeColor; /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeWidth_ = strokeWidth; @@ -124,13 +133,13 @@ ol.style.Polygon = function(options) { 'Stroke or fill properties must be provided'); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.opacity_ = !goog.isDef(options.opacity) ? - new ol.ExpressionLiteral(ol.style.PolygonDefaults.opacity) : - (options.opacity instanceof ol.Expression) ? - options.opacity : new ol.ExpressionLiteral(options.opacity); + new ol.expr.Literal(ol.style.PolygonDefaults.opacity) : + (options.opacity instanceof ol.expr.Expression) ? + options.opacity : new ol.expr.Literal(options.opacity); }; goog.inherits(ol.style.Polygon, ol.style.Symbolizer); @@ -141,33 +150,31 @@ goog.inherits(ol.style.Polygon, ol.style.Symbolizer); * @return {ol.style.PolygonLiteral} Literal shape symbolizer. */ ol.style.Polygon.prototype.createLiteral = function(opt_feature) { - var attrs, - feature = opt_feature; - if (goog.isDef(feature)) { - attrs = feature.getAttributes(); + + var fillColor; + if (!goog.isNull(this.fillColor_)) { + fillColor = ol.expr.evaluateFeature(this.fillColor_, opt_feature); + goog.asserts.assertString(fillColor, 'fillColor must be a string'); } - var fillColor = goog.isNull(this.fillColor_) ? - undefined : - /** @type {string} */ (this.fillColor_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(fillColor) || goog.isString(fillColor)); + var strokeColor; + if (!goog.isNull(this.strokeColor_)) { + strokeColor = ol.expr.evaluateFeature(this.strokeColor_, opt_feature); + goog.asserts.assertString(strokeColor, 'strokeColor must be a string'); + } - var strokeColor = goog.isNull(this.strokeColor_) ? - undefined : - /** @type {string} */ (this.strokeColor_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(strokeColor) || goog.isString(strokeColor)); - - var strokeWidth = goog.isNull(this.strokeWidth_) ? - undefined : - /** @type {number} */ (this.strokeWidth_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(strokeWidth) || goog.isNumber(strokeWidth)); + var strokeWidth; + if (!goog.isNull(this.strokeWidth_)) { + strokeWidth = ol.expr.evaluateFeature(this.strokeWidth_, opt_feature); + goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number'); + } goog.asserts.assert( goog.isDef(fillColor) || (goog.isDef(strokeColor) && goog.isDef(strokeWidth)), - 'either fill style or strokeColor and strokeWidth must be defined'); + 'either fillColor or strokeColor and strokeWidth must be defined'); - var opacity = this.opacity_.evaluate(feature, attrs); + var opacity = ol.expr.evaluateFeature(this.opacity_, opt_feature); goog.asserts.assertNumber(opacity, 'opacity must be a number'); return new ol.style.PolygonLiteral({ diff --git a/src/ol/style/rule.js b/src/ol/style/rule.js index 0234fd9540..97ecb5b84c 100644 --- a/src/ol/style/rule.js +++ b/src/ol/style/rule.js @@ -1,7 +1,10 @@ goog.provide('ol.style.Rule'); +goog.require('goog.asserts'); + goog.require('ol.Feature'); -goog.require('ol.filter.Filter'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); goog.require('ol.style.Symbolizer'); @@ -12,11 +15,22 @@ goog.require('ol.style.Symbolizer'); */ ol.style.Rule = function(options) { + + var filter = null; + if (goog.isDef(options.filter)) { + if (goog.isString(options.filter)) { + filter = ol.expr.parse(options.filter); + } else { + goog.asserts.assert(options.filter instanceof ol.expr.Expression); + filter = options.filter; + } + } + /** - * @type {ol.filter.Filter} + * @type {ol.expr.Expression} * @private */ - this.filter_ = goog.isDef(options.filter) ? options.filter : null; + this.filter_ = filter; /** * @type {Array.} @@ -33,7 +47,8 @@ ol.style.Rule = function(options) { * @return {boolean} Does the rule apply to the feature? */ ol.style.Rule.prototype.applies = function(feature) { - return goog.isNull(this.filter_) ? true : this.filter_.applies(feature); + return goog.isNull(this.filter_) ? + true : !!ol.expr.evaluateFeature(this.filter_, feature); }; diff --git a/src/ol/style/shape.js b/src/ol/style/shape.js index acc8a98f8d..751cf66ab2 100644 --- a/src/ol/style/shape.js +++ b/src/ol/style/shape.js @@ -3,8 +3,9 @@ goog.provide('ol.style.ShapeLiteral'); goog.provide('ol.style.ShapeType'); goog.require('goog.asserts'); -goog.require('ol.Expression'); -goog.require('ol.ExpressionLiteral'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Point'); goog.require('ol.style.PointLiteral'); @@ -106,22 +107,22 @@ ol.style.Shape = function(options) { options.type : ol.style.ShapeDefaults.type); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.size_ = !goog.isDef(options.size) ? - new ol.ExpressionLiteral(ol.style.ShapeDefaults.size) : - (options.size instanceof ol.Expression) ? - options.size : new ol.ExpressionLiteral(options.size); + new ol.expr.Literal(ol.style.ShapeDefaults.size) : + (options.size instanceof ol.expr.Expression) ? + options.size : new ol.expr.Literal(options.size); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.fillColor_ = !goog.isDefAndNotNull(options.fillColor) ? null : - (options.fillColor instanceof ol.Expression) ? - options.fillColor : new ol.ExpressionLiteral(options.fillColor); + (options.fillColor instanceof ol.expr.Expression) ? + options.fillColor : new ol.expr.Literal(options.fillColor); // stroke handling - if any stroke property is supplied, use defaults var strokeColor = null, @@ -130,25 +131,34 @@ ol.style.Shape = function(options) { if (goog.isDefAndNotNull(options.strokeColor) || goog.isDefAndNotNull(options.strokeWidth)) { - strokeColor = !goog.isDefAndNotNull(options.strokeColor) ? - new ol.ExpressionLiteral(ol.style.ShapeDefaults.strokeColor) : - (options.strokeColor instanceof ol.Expression) ? - options.strokeColor : new ol.ExpressionLiteral(options.strokeColor); + if (goog.isDefAndNotNull(options.strokeColor)) { + strokeColor = (options.strokeColor instanceof ol.expr.Expression) ? + options.strokeColor : + new ol.expr.Literal(options.strokeColor); + } else { + strokeColor = new ol.expr.Literal( + /** @type {string} */ (ol.style.ShapeDefaults.strokeColor)); + } + + if (goog.isDefAndNotNull(options.strokeWidth)) { + strokeWidth = (options.strokeWidth instanceof ol.expr.Expression) ? + options.strokeWidth : + new ol.expr.Literal(options.strokeWidth); + } else { + strokeWidth = new ol.expr.Literal( + /** @type {number} */ (ol.style.ShapeDefaults.strokeWidth)); + } - strokeWidth = !goog.isDef(options.strokeWidth) ? - new ol.ExpressionLiteral(ol.style.ShapeDefaults.strokeWidth) : - (options.strokeWidth instanceof ol.Expression) ? - options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth); } /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeColor_ = strokeColor; /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.strokeWidth_ = strokeWidth; @@ -159,13 +169,13 @@ ol.style.Shape = function(options) { 'Stroke or fill properties must be provided'); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.opacity_ = !goog.isDef(options.opacity) ? - new ol.ExpressionLiteral(ol.style.ShapeDefaults.opacity) : - (options.opacity instanceof ol.Expression) ? - options.opacity : new ol.ExpressionLiteral(options.opacity); + new ol.expr.Literal(ol.style.ShapeDefaults.opacity) : + (options.opacity instanceof ol.expr.Expression) ? + options.opacity : new ol.expr.Literal(options.opacity); }; @@ -175,36 +185,34 @@ ol.style.Shape = function(options) { * @return {ol.style.ShapeLiteral} Literal shape symbolizer. */ ol.style.Shape.prototype.createLiteral = function(opt_feature) { - var attrs, - feature = opt_feature; - if (goog.isDef(feature)) { - attrs = feature.getAttributes(); - } - var size = this.size_.evaluate(feature, attrs); + var size = ol.expr.evaluateFeature(this.size_, opt_feature); goog.asserts.assertNumber(size, 'size must be a number'); - var fillColor = goog.isNull(this.fillColor_) ? - undefined : - /** @type {string} */ (this.fillColor_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(fillColor) || goog.isString(fillColor)); + var fillColor; + if (!goog.isNull(this.fillColor_)) { + fillColor = ol.expr.evaluateFeature(this.fillColor_, opt_feature); + goog.asserts.assertString(fillColor, 'fillColor must be a string'); + } - var strokeColor = goog.isNull(this.strokeColor_) ? - undefined : - /** @type {string} */ (this.strokeColor_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(strokeColor) || goog.isString(strokeColor)); + var strokeColor; + if (!goog.isNull(this.strokeColor_)) { + strokeColor = ol.expr.evaluateFeature(this.strokeColor_, opt_feature); + goog.asserts.assertString(strokeColor, 'strokeColor must be a string'); + } - var strokeWidth = goog.isNull(this.strokeWidth_) ? - undefined : - /** @type {number} */ (this.strokeWidth_.evaluate(feature, attrs)); - goog.asserts.assert(!goog.isDef(strokeWidth) || goog.isNumber(strokeWidth)); + var strokeWidth; + if (!goog.isNull(this.strokeWidth_)) { + strokeWidth = ol.expr.evaluateFeature(this.strokeWidth_, opt_feature); + goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number'); + } goog.asserts.assert( goog.isDef(fillColor) || (goog.isDef(strokeColor) && goog.isDef(strokeWidth)), - 'either fill style or strokeColor and strokeWidth must be defined'); + 'either fillColor or strokeColor and strokeWidth must be defined'); - var opacity = this.opacity_.evaluate(feature, attrs); + var opacity = ol.expr.evaluateFeature(this.opacity_, opt_feature); goog.asserts.assertNumber(opacity, 'opacity must be a number'); return new ol.style.ShapeLiteral({ diff --git a/src/ol/style/text.js b/src/ol/style/text.js index 7528c158dc..a1862bc776 100644 --- a/src/ol/style/text.js +++ b/src/ol/style/text.js @@ -2,8 +2,9 @@ goog.provide('ol.style.Text'); goog.provide('ol.style.TextLiteral'); goog.require('goog.asserts'); -goog.require('ol.Expression'); -goog.require('ol.ExpressionLiteral'); +goog.require('ol.expr'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Symbolizer'); goog.require('ol.style.SymbolizerLiteral'); @@ -26,18 +27,13 @@ ol.style.TextLiteralOptions; */ ol.style.TextLiteral = function(options) { + goog.asserts.assertString(options.color, 'color must be a string'); /** @type {string} */ this.color = options.color; - if (goog.isDef(options.color)) { - goog.asserts.assertString(options.color, 'color must be a string'); - } + goog.asserts.assertString(options.fontFamily, 'fontFamily must be a string'); /** @type {string} */ this.fontFamily = options.fontFamily; - if (goog.isDef(options.fontFamily)) { - goog.asserts.assertString(options.fontFamily, - 'fontFamily must be a string'); - } goog.asserts.assertNumber(options.fontSize, 'fontSize must be a number'); /** @type {number} */ @@ -75,47 +71,47 @@ ol.style.TextLiteral.prototype.equals = function(textLiteral) { ol.style.Text = function(options) { /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.color_ = !goog.isDef(options.color) ? - new ol.ExpressionLiteral(ol.style.TextDefaults.color) : - (options.color instanceof ol.Expression) ? - options.color : new ol.ExpressionLiteral(options.color); + new ol.expr.Literal(ol.style.TextDefaults.color) : + (options.color instanceof ol.expr.Expression) ? + options.color : new ol.expr.Literal(options.color); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.fontFamily_ = !goog.isDef(options.fontFamily) ? - new ol.ExpressionLiteral(ol.style.TextDefaults.fontFamily) : - (options.fontFamily instanceof ol.Expression) ? - options.fontFamily : new ol.ExpressionLiteral(options.fontFamily); + new ol.expr.Literal(ol.style.TextDefaults.fontFamily) : + (options.fontFamily instanceof ol.expr.Expression) ? + options.fontFamily : new ol.expr.Literal(options.fontFamily); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.fontSize_ = !goog.isDef(options.fontSize) ? - new ol.ExpressionLiteral(ol.style.TextDefaults.fontSize) : - (options.fontSize instanceof ol.Expression) ? - options.fontSize : new ol.ExpressionLiteral(options.fontSize); + new ol.expr.Literal(ol.style.TextDefaults.fontSize) : + (options.fontSize instanceof ol.expr.Expression) ? + options.fontSize : new ol.expr.Literal(options.fontSize); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ - this.text_ = (options.text instanceof ol.Expression) ? - options.text : new ol.ExpressionLiteral(options.text); + this.text_ = (options.text instanceof ol.expr.Expression) ? + options.text : new ol.expr.Literal(options.text); /** - * @type {ol.Expression} + * @type {ol.expr.Expression} * @private */ this.opacity_ = !goog.isDef(options.opacity) ? - new ol.ExpressionLiteral(ol.style.TextDefaults.opacity) : - (options.opacity instanceof ol.Expression) ? - options.opacity : new ol.ExpressionLiteral(options.opacity); + new ol.expr.Literal(ol.style.TextDefaults.opacity) : + (options.opacity instanceof ol.expr.Expression) ? + options.opacity : new ol.expr.Literal(options.opacity); }; goog.inherits(ol.style.Text, ol.style.Symbolizer); @@ -126,25 +122,20 @@ goog.inherits(ol.style.Text, ol.style.Symbolizer); * @return {ol.style.TextLiteral} Literal text symbolizer. */ ol.style.Text.prototype.createLiteral = function(opt_feature) { - var attrs, - feature = opt_feature; - if (goog.isDef(feature)) { - attrs = feature.getAttributes(); - } - var color = this.color_.evaluate(feature, attrs); + var color = ol.expr.evaluateFeature(this.color_, opt_feature); goog.asserts.assertString(color, 'color must be a string'); - var fontFamily = this.fontFamily_.evaluate(feature, attrs); + var fontFamily = ol.expr.evaluateFeature(this.fontFamily_, opt_feature); goog.asserts.assertString(fontFamily, 'fontFamily must be a string'); - var fontSize = this.fontSize_.evaluate(feature, attrs); + var fontSize = ol.expr.evaluateFeature(this.fontSize_, opt_feature); goog.asserts.assertNumber(fontSize, 'fontSize must be a number'); - var text = this.text_.evaluate(feature, attrs); + var text = ol.expr.evaluateFeature(this.text_, opt_feature); goog.asserts.assertString(text, 'text must be a string'); - var opacity = this.opacity_.evaluate(feature, attrs); + var opacity = ol.expr.evaluateFeature(this.opacity_, opt_feature); goog.asserts.assertNumber(opacity, 'opacity must be a number'); return new ol.style.TextLiteral({ diff --git a/test/spec/ol/expr/expression.test.js b/test/spec/ol/expr/expression.test.js new file mode 100644 index 0000000000..30b52945d5 --- /dev/null +++ b/test/spec/ol/expr/expression.test.js @@ -0,0 +1,828 @@ +goog.provide('ol.test.expression'); + + +describe('ol.expr.parse()', function() { + + it('parses a subset of ECMAScript 5.1 expressions', function() { + var expr = ol.expr.parse('foo'); + expect(expr).to.be.a(ol.expr.Expression); + }); + + describe('11.1 - primary expressions', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.1 + + it('parses identifier expressions', function() { + var expr = ol.expr.parse('foo'); + expect(expr).to.be.a(ol.expr.Identifier); + expect(expr.evaluate({foo: 'bar'})).to.be('bar'); + }); + + it('consumes whitespace as expected', function() { + var expr = ol.expr.parse(' foo '); + expect(expr).to.be.a(ol.expr.Identifier); + expect(expr.evaluate({foo: 'bar'})).to.be('bar'); + }); + + it('throws on invalid identifier expressions', function() { + expect(function() { + ol.expr.parse('3foo'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('f'); + expect(token.index).to.be(1); + }); + }); + + it('parses string literal expressions', function() { + var expr = ol.expr.parse('"foo"'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be('foo'); + }); + + it('throws on unterminated string', function() { + expect(function() { + ol.expr.parse('"foo'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.type).to.be(ol.expr.TokenType.EOF); + expect(token.index).to.be(4); + }); + }); + + it('parses numeric literal expressions', function() { + var expr = ol.expr.parse('.42e+2'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(42); + }); + + it('throws on invalid number', function() { + expect(function() { + ol.expr.parse('.42eX'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('X'); + expect(token.index).to.be(4); + }); + }); + + it('parses boolean literal expressions', function() { + var expr = ol.expr.parse('false'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(false); + }); + + it('parses null literal expressions', function() { + var expr = ol.expr.parse('null'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(null); + }); + + }); + + describe('11.2 - left-hand-side expressions', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.2 + + it('parses member expressions with dot notation', function() { + var expr = ol.expr.parse('foo.bar.baz'); + expect(expr).to.be.a(ol.expr.Member); + var scope = {foo: {bar: {baz: 42}}}; + expect(expr.evaluate(scope)).to.be(42); + }); + + it('consumes whitespace as expected', function() { + var expr = ol.expr.parse(' foo . bar . baz '); + expect(expr).to.be.a(ol.expr.Member); + var scope = {foo: {bar: {baz: 42}}}; + expect(expr.evaluate(scope)).to.be(42); + }); + + it('throws on invalid member expression', function() { + expect(function() { + ol.expr.parse('foo.4bar'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('b'); + expect(token.index).to.be(5); + }); + }); + + it('parses call expressions with literal arguments', function() { + var expr = ol.expr.parse('foo(42, "bar")'); + expect(expr).to.be.a(ol.expr.Call); + var scope = { + foo: function(num, str) { + expect(num).to.be(42); + expect(str).to.be('bar'); + return str + num; + } + }; + expect(expr.evaluate(scope)).to.be('bar42'); + }); + + it('throws on calls with unterminated arguments', function() { + expect(function() { + ol.expr.parse('foo(42,)'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be(')'); + expect(token.index).to.be(7); + }); + }); + + }); + + describe('11.3 - postfix expressions', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.3 + it('not supported'); + }); + + + describe('11.4 - unary operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.4 + + it('parses logical not operator', function() { + var expr = ol.expr.parse('!foo'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate({foo: true})).to.be(false); + expect(expr.evaluate({foo: false})).to.be(true); + expect(expr.evaluate({foo: ''})).to.be(true); + expect(expr.evaluate({foo: 'foo'})).to.be(false); + }); + + it('consumes whitespace as expected', function() { + var expr = ol.expr.parse(' ! foo'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate({foo: true})).to.be(false); + expect(expr.evaluate({foo: false})).to.be(true); + }); + + it('parses not preceeding call expression', function() { + var lib = { + foo: function() { + return true; + } + }; + var expr = ol.expr.parse('!foo()'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate(null, lib)).to.be(false); + }); + + it('parses not in call argument', function() { + var lib = { + foo: function(arg) { + return arg; + } + }; + var expr = ol.expr.parse('foo(!bar)'); + expect(expr).to.be.a(ol.expr.Call); + expect(expr.evaluate({bar: true}, lib)).to.be(false); + expect(expr.evaluate({bar: false}, lib)).to.be(true); + }); + + }); + + describe('11.5 - multiplicitave operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.5 + + it('parses * operator', function() { + var expr = ol.expr.parse('foo*bar'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(200); + }); + + it('consumes whitespace as expected with *', function() { + var expr = ol.expr.parse(' foo * bar '); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 15, bar: 2})).to.be(30); + }); + + it('parses / operator', function() { + var expr = ol.expr.parse('foo/12'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 10})).to.be(10 / 12); + }); + + it('consumes whitespace as expected with /', function() { + var expr = ol.expr.parse(' 4 / bar '); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({bar: 3})).to.be(4 / 3); + }); + + it('parses % operator', function() { + var expr = ol.expr.parse('12%foo'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 10})).to.be(2); + }); + + it('consumes whitespace as expected with %', function() { + var expr = ol.expr.parse(' 4 %bar '); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({bar: 3})).to.be(1); + }); + + it('parses * in call argument', function() { + var lib = { + foo: function(arg) { + return arg; + } + }; + var expr = ol.expr.parse('foo(2 * bar)'); + expect(expr).to.be.a(ol.expr.Call); + expect(expr.evaluate({bar: 3}, lib)).to.be(6); + expect(expr.evaluate({bar: 4}, lib)).to.be(8); + }); + + it('evaluates left to right for equal precedence', function() { + var expr = ol.expr.parse('2 / 4 * 20 % 15'); + expect(expr.evaluate()).to.be(10); + }); + + it('respects group precedence', function() { + expect(ol.expr.parse('2 / 4 * (20 % 15)').evaluate()).to.be(2.5); + expect(ol.expr.parse('2 / (4 * (20 % 15))').evaluate()).to.be(0.1); + expect(ol.expr.parse('2 / ((4 * 20) % 15)').evaluate()).to.be(0.4); + expect(ol.expr.parse('2 / (4 * 20) % 15').evaluate()).to.be(0.025); + expect(ol.expr.parse('(2 / (4 * 20)) % 15').evaluate()).to.be(0.025); + expect(ol.expr.parse('(2 / 4) * 20 % 15').evaluate()).to.be(10); + expect(ol.expr.parse('((2 / 4) * 20) % 15').evaluate()).to.be(10); + }); + + it('parses * in left side of comparison expression', function() { + var expr = ol.expr.parse('foo * 2 >bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 4, bar: 7})).to.be(true); + expect(expr.evaluate({foo: 4, bar: 8})).to.be(false); + }); + + it('parses * in right side of comparison expression', function() { + var expr = ol.expr.parse('foo > 2 * bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 4, bar: 1})).to.be(true); + expect(expr.evaluate({foo: 4, bar: 2})).to.be(false); + }); + + }); + + describe('11.6 - additive operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.6 + + it('parses + operator', function() { + var expr = ol.expr.parse('foo+bar'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(30); + }); + + it('consumes whitespace as expected with +', function() { + var expr = ol.expr.parse(' foo +10 '); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 15})).to.be(25); + }); + + it('parses - operator', function() { + var expr = ol.expr.parse('foo-bar'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(-10); + }); + + it('consumes whitespace as expected with -', function() { + var expr = ol.expr.parse(' foo- 10 '); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 15})).to.be(5); + }); + + it('respects precedence', function() { + expect(ol.expr.parse('2 + 4 * 20 - 15').evaluate()).to.be(67); + expect(ol.expr.parse('(2 + 4) * 20 - 15').evaluate()).to.be(105); + expect(ol.expr.parse('((2 + 4) * 20) - 15').evaluate()).to.be(105); + expect(ol.expr.parse('(2 + (4 * 20)) - 15').evaluate()).to.be(67); + expect(ol.expr.parse('2 + (4 * 20) - 15').evaluate()).to.be(67); + expect(ol.expr.parse('2 + ((4 * 20) - 15)').evaluate()).to.be(67); + expect(ol.expr.parse('2 + (4 * (20 - 15))').evaluate()).to.be(22); + expect(ol.expr.parse('2 + 4 * (20 - 15)').evaluate()).to.be(22); + }); + + it('parses + in call argument', function() { + var lib = { + foo: function(arg) { + return arg; + } + }; + var expr = ol.expr.parse('foo(2 + bar)'); + expect(expr).to.be.a(ol.expr.Call); + expect(expr.evaluate({bar: 3}, lib)).to.be(5); + expect(expr.evaluate({bar: 4}, lib)).to.be(6); + }); + + it('parses + in left side of comparison expression', function() { + var expr = ol.expr.parse('foo+2>bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 4, bar: 5})).to.be(true); + expect(expr.evaluate({foo: 4, bar: 6})).to.be(false); + }); + + it('parses + in right side of comparison expression', function() { + var expr = ol.expr.parse('foo >2 +bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 4, bar: 1})).to.be(true); + expect(expr.evaluate({foo: 4, bar: 2})).to.be(false); + }); + + }); + + describe('11.7 - bitwise shift operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.7 + it('not supported'); + }); + + describe('11.8 - relational operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.8 + + it('parses < operator', function() { + var expr = ol.expr.parse('foo operator', function() { + var expr = ol.expr.parse('foo>bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(false); + expect(expr.evaluate({foo: 100, bar: 20})).to.be(true); + }); + + it('consumes whitespace as expected with >', function() { + var expr = ol.expr.parse(' foo> 10 '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 15})).to.be(true); + expect(expr.evaluate({foo: 5})).to.be(false); + }); + + it('parses <= operator', function() { + var expr = ol.expr.parse('foo<=bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(true); + expect(expr.evaluate({foo: 100, bar: 20})).to.be(false); + expect(expr.evaluate({foo: 20, bar: 20})).to.be(true); + }); + + it('consumes whitespace as expected with <=', function() { + var expr = ol.expr.parse(' foo<= 10 '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 15})).to.be(false); + expect(expr.evaluate({foo: 5})).to.be(true); + expect(expr.evaluate({foo: 10})).to.be(true); + }); + + it('throws for invalid spacing with <=', function() { + expect(function() { + ol.expr.parse(' foo< = 10 '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('='); + expect(token.index).to.be(6); + }); + }); + + it('parses >= operator', function() { + var expr = ol.expr.parse('foo>=bar'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 10, bar: 20})).to.be(false); + expect(expr.evaluate({foo: 100, bar: 20})).to.be(true); + expect(expr.evaluate({foo: 20, bar: 20})).to.be(true); + }); + + it('consumes whitespace as expected with >=', function() { + var expr = ol.expr.parse(' foo >=10 '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 15})).to.be(true); + expect(expr.evaluate({foo: 5})).to.be(false); + expect(expr.evaluate({foo: 10})).to.be(true); + }); + + it('throws for invalid spacing with >=', function() { + expect(function() { + ol.expr.parse(' 10 > =foo '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('='); + expect(token.index).to.be(6); + }); + }); + + }); + + describe('11.9 - equality operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.9 + + it('parses == operator', function() { + var expr = ol.expr.parse('foo==42'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(true); + }); + + it('consumes whitespace as expected with ==', function() { + var expr = ol.expr.parse(' 42 ==foo '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(true); + }); + + it('throws for invalid spacing with ==', function() { + expect(function() { + ol.expr.parse(' 10 = =foo '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('='); + expect(token.index).to.be(4); + }); + }); + + it('parses != operator', function() { + var expr = ol.expr.parse('foo!=42'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(false); + }); + + it('consumes whitespace as expected with !=', function() { + var expr = ol.expr.parse(' 42 !=foo '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(false); + }); + + it('throws for invalid spacing with !=', function() { + expect(function() { + ol.expr.parse(' 10! =foo '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('!'); + expect(token.index).to.be(3); + }); + }); + + it('parses === operator', function() { + var expr = ol.expr.parse('42===foo'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(false); + }); + + it('consumes whitespace as expected with ===', function() { + var expr = ol.expr.parse(' foo ===42 '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(false); + }); + + it('throws for invalid spacing with ===', function() { + expect(function() { + ol.expr.parse(' 10 = == foo '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('='); + expect(token.index).to.be(4); + }); + }); + + it('parses !== operator', function() { + var expr = ol.expr.parse('foo!==42'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(true); + }); + + it('consumes whitespace as expected with !==', function() { + var expr = ol.expr.parse(' 42 !== foo '); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(true); + }); + + it('throws for invalid spacing with !==', function() { + expect(function() { + ol.expr.parse(' 10 != = foo '); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('='); + expect(token.index).to.be(7); + }); + }); + }); + + describe('11.10 - binary bitwise operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.10 + it('not supported'); + }); + + describe('11.11 - binary logical operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.11 + + it('parses && operator', function() { + var expr = ol.expr.parse('foo&&bar'); + expect(expr).to.be.a(ol.expr.Logical); + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(false); + expect(expr.evaluate({foo: false, bar: true})).to.be(false); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + + it('consumes space as expected with &&', function() { + var expr = ol.expr.parse(' foo && bar '); + expect(expr).to.be.a(ol.expr.Logical); + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(false); + expect(expr.evaluate({foo: false, bar: true})).to.be(false); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + + it('throws for invalid spacing with &&', function() { + expect(function() { + ol.expr.parse('true & & false'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('&'); + expect(token.index).to.be(5); + }); + }); + + it('parses || operator', function() { + var expr = ol.expr.parse('foo||bar'); + expect(expr).to.be.a(ol.expr.Logical); + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(true); + expect(expr.evaluate({foo: false, bar: true})).to.be(true); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + + it('consumes space as expected with ||', function() { + var expr = ol.expr.parse(' foo || bar '); + expect(expr).to.be.a(ol.expr.Logical); + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(true); + expect(expr.evaluate({foo: false, bar: true})).to.be(true); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + + it('throws for invalid spacing with ||', function() { + expect(function() { + ol.expr.parse('true | | false'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('|'); + expect(token.index).to.be(5); + }); + }); + + }); + + describe('11.12 - conditional operator', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.12 + it('not supported'); + }); + + describe('11.13 - assignment operators', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.13 + it('not supported'); + }); + + describe('11.14 - comma operator', function() { + // http://www.ecma-international.org/ecma-262/5.1/#sec-11.14 + it('not supported'); + }); + +}); + +describe('ol.expr.lib', function() { + + var parse = ol.expr.parse; + var evaluate = ol.expr.evaluateFeature; + + describe('extent()', function() { + + var nw = new ol.Feature({ + geom: new ol.geom.Polygon([[ + [-180, 90], [0, 90], [0, 0], [-180, 0], [-180, 90] + ]]) + }); + + var se = new ol.Feature({ + geom: new ol.geom.Polygon([[ + [180, -90], [0, -90], [0, 0], [180, 0], [180, -90] + ]]) + }); + + var north = parse('extent(-100, 100, 40, 60)'); + var south = parse('extent(-100, 100, -60, -40)'); + var east = parse('extent(80, 100, -50, 50)'); + var west = parse('extent(-100, -80, -50, 50)'); + + it('evaluates to true for features within given extent', function() { + + expect(evaluate(north, nw), true); + expect(evaluate(south, nw), false); + expect(evaluate(east, nw), false); + expect(evaluate(west, nw), true); + + expect(evaluate(north, se), false); + expect(evaluate(south, se), true); + expect(evaluate(east, se), true); + expect(evaluate(west, se), false); + + }); + + }); + + describe('fid()', function() { + + var one = new ol.Feature(); + one.setFeatureId('one'); + + var two = new ol.Feature(); + two.setFeatureId('two'); + + var three = new ol.Feature(); + three.setFeatureId('three'); + + var four = new ol.Feature(); + four.setFeatureId('four'); + + var odd = parse('fid("one", "three")'); + var even = parse('fid("two", "four")'); + var first = parse('fid("one")'); + var last = parse('fid("four")'); + var none = parse('fid("foo")'); + + it('evaluates to true if feature id matches', function() { + expect(evaluate(odd, one), true); + expect(evaluate(odd, three), true); + expect(evaluate(even, two), true); + expect(evaluate(even, four), true); + expect(evaluate(first, one), true); + expect(evaluate(last, four), true); + }); + + it('evaluates to false if feature id doesn\'t match', function() { + expect(evaluate(odd, two), false); + expect(evaluate(odd, four), false); + expect(evaluate(even, one), false); + expect(evaluate(even, three), false); + expect(evaluate(first, two), false); + expect(evaluate(first, three), false); + expect(evaluate(first, four), false); + expect(evaluate(last, one), false); + expect(evaluate(last, two), false); + expect(evaluate(last, three), false); + expect(evaluate(none, one), false); + expect(evaluate(none, two), false); + expect(evaluate(none, three), false); + expect(evaluate(none, four), false); + }); + + }); + + describe('geometryType()', function() { + + var point = new ol.Feature({ + geom: new ol.geom.Point([0, 0]) + }); + + var line = new ol.Feature({ + geom: new ol.geom.LineString([[180, -90], [-180, 90]]) + }); + + var poly = new ol.Feature({ + geom: new ol.geom.Polygon([[ + [180, -90], [0, -90], [0, 0], [180, 0], [180, -90] + ]]) + }); + + var isPoint = parse('geometryType("point")'); + var isLine = parse('geometryType("linestring")'); + var isPoly = parse('geometryType("polygon")'); + var pointOrPoly = parse('geometryType("point") || geometryType("polygon")'); + + it('distinguishes point features', function() { + expect(evaluate(isPoint, point), true); + expect(evaluate(isPoint, line), false); + expect(evaluate(isPoint, poly), false); + }); + + it('distinguishes line features', function() { + expect(evaluate(isLine, point), false); + expect(evaluate(isLine, line), true); + expect(evaluate(isLine, poly), false); + }); + + it('distinguishes polygon features', function() { + expect(evaluate(isPoly, point), false); + expect(evaluate(isPoly, line), false); + expect(evaluate(isPoly, poly), true); + }); + + it('can be composed in a logical expression', function() { + expect(evaluate(pointOrPoly, point), true); + expect(evaluate(pointOrPoly, line), false); + expect(evaluate(pointOrPoly, poly), true); + }); + + }); + +}); + +describe('ol.expr.register()', function() { + + var spy; + beforeEach(function() { + spy = sinon.spy(); + }); + + it('registers custom functions in ol.expr.lib', function() { + ol.expr.register('someFunc', spy); + expect(ol.expr.lib.someFunc).to.be(spy); + }); + + it('allows custom functions to be called', function() { + ol.expr.register('myFunc', spy); + var expr = ol.expr.parse('myFunc(42)'); + expr.evaluate(null, ol.expr.lib); + expect(spy.calledOnce); + expect(spy.calledWithExactly(42)); + }); + + it('allows custom functions to be called with identifiers', function() { + ol.expr.register('myFunc', spy); + var expr = ol.expr.parse('myFunc(foo, 42)'); + expr.evaluate({foo: 'bar'}, ol.expr.lib); + expect(spy.calledOnce); + expect(spy.calledWithExactly('bar', 42)); + }); + + it('allows custom functions to be called with custom this obj', function() { + ol.expr.register('myFunc', spy); + var expr = ol.expr.parse('myFunc(foo, 42)'); + var that = {}; + expr.evaluate({foo: 'bar'}, ol.expr.lib, that); + expect(spy.calledOnce); + expect(spy.calledWithExactly('bar', 42)); + expect(spy.calledOn(that)); + }); + + it('allows overriding existing ol.expr.lib functions', function() { + var orig = ol.expr.lib.extent; + expect(orig).not.to.be(spy); + ol.expr.register('extent', spy); + expect(ol.expr.lib.extent).to.be(spy); + ol.expr.lib.extent = orig; + }); + +}); + + + +goog.require('ol.Feature'); +goog.require('ol.expr'); +goog.require('ol.expr.Call'); +goog.require('ol.expr.Comparison'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Literal'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.Math'); +goog.require('ol.expr.Member'); +goog.require('ol.expr.Not'); +goog.require('ol.expr.TokenType'); +goog.require('ol.expr.UnexpectedToken'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.Polygon'); diff --git a/test/spec/ol/expr/expressions.test.js b/test/spec/ol/expr/expressions.test.js new file mode 100644 index 0000000000..c64ea7cd52 --- /dev/null +++ b/test/spec/ol/expr/expressions.test.js @@ -0,0 +1,638 @@ +goog.provide('ol.test.expression.Expression'); + + +describe('ol.expr.Call', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Call( + new ol.expr.Identifier('sqrt'), + [new ol.expr.Literal(42)]); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.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.expr.Call( + new ol.expr.Identifier('sqrt'), + [new ol.expr.Literal(42)]); + expect(expr.evaluate(fns)).to.be(Math.sqrt(42)); + }); + + it('accepts a separate scope for functions', function() { + var expr = new ol.expr.Call( + new ol.expr.Identifier('sqrt'), + [new ol.expr.Identifier('foo')]); + expect(expr.evaluate({foo: 42}, fns)).to.be(Math.sqrt(42)); + }); + + it('accepts multiple expression arguments', function() { + var expr = new ol.expr.Call( + new ol.expr.Identifier('strConcat'), + [ + new ol.expr.Identifier('foo'), + new ol.expr.Literal(' comes after '), + new ol.expr.Math( + ol.expr.MathOp.SUBTRACT, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(1)) + ]); + expect(expr.evaluate({foo: 42}, fns)).to.be('42 comes after 41'); + }); + + it('accepts optional this arg', function() { + var expr = new ol.expr.Call( + new ol.expr.Identifier('discouraged'), []); + + var thisArg = { + message: 'avoid this' + }; + + expect(expr.evaluate(fns, null, thisArg)).to.be('avoid this'); + }); + + }); + + var callee = new ol.expr.Identifier('sqrt'); + var args = [new ol.expr.Literal(42)]; + var expr = new ol.expr.Call(callee, args); + + describe('#getArgs()', function() { + it('gets the callee expression', function() { + expect(expr.getArgs()).to.be(args); + }); + }); + + describe('#getCallee()', function() { + it('gets the callee expression', function() { + expect(expr.getCallee()).to.be(callee); + }); + }); + +}); + + +describe('ol.expr.Comparison', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.EQ, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Comparison); + }); + }); + + describe('#evaluate()', function() { + it('compares with ==', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.EQ, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(true); + expect(expr.evaluate({foo: true})).to.be(false); + expect(expr.evaluate({bar: true})).to.be(false); + }); + + it('compares with !=', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.NEQ, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(false); + expect(expr.evaluate({foo: true})).to.be(true); + expect(expr.evaluate({bar: true})).to.be(true); + }); + + it('compares with ===', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.STRICT_EQ, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: '42'})).to.be(false); + expect(expr.evaluate({foo: true})).to.be(false); + expect(expr.evaluate({bar: true})).to.be(false); + }); + + it('compares with !==', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.STRICT_NEQ, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: '42'})).to.be(true); + expect(expr.evaluate({foo: true})).to.be(true); + expect(expr.evaluate({bar: true})).to.be(true); + }); + + it('compares with >', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.GT, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: 43})).to.be(true); + }); + + it('compares with <', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.LT, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(false); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: 43})).to.be(false); + }); + + it('compares with >=', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.GTE, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(false); + expect(expr.evaluate({foo: 43})).to.be(true); + }); + + it('compares with <=', function() { + var expr = new ol.expr.Comparison( + ol.expr.ComparisonOp.LTE, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(42)); + + expect(expr.evaluate({foo: 42})).to.be(true); + expect(expr.evaluate({foo: 41})).to.be(true); + expect(expr.evaluate({foo: 43})).to.be(false); + }); + }); + + describe('#isValidOp()', function() { + it('determines if a string is a valid operator', function() { + expect(ol.expr.Comparison.isValidOp('<')).to.be(true); + expect(ol.expr.Comparison.isValidOp('<')).to.be(true); + expect(ol.expr.Comparison.isValidOp('<=')).to.be(true); + expect(ol.expr.Comparison.isValidOp('<=')).to.be(true); + expect(ol.expr.Comparison.isValidOp('==')).to.be(true); + expect(ol.expr.Comparison.isValidOp('!=')).to.be(true); + expect(ol.expr.Comparison.isValidOp('===')).to.be(true); + expect(ol.expr.Comparison.isValidOp('!==')).to.be(true); + + expect(ol.expr.Comparison.isValidOp('')).to.be(false); + expect(ol.expr.Comparison.isValidOp('+')).to.be(false); + expect(ol.expr.Comparison.isValidOp('-')).to.be(false); + expect(ol.expr.Comparison.isValidOp('&&')).to.be(false); + }); + }); + + var op = ol.expr.ComparisonOp.LTE; + var left = new ol.expr.Identifier('foo'); + var right = new ol.expr.Literal(42); + var expr = new ol.expr.Comparison(op, left, right); + + describe('#getOperator()', function() { + it('gets the operator', function() { + expect(expr.getOperator()).to.be(op); + }); + }); + + describe('#getLeft()', function() { + it('gets the left expression', function() { + expect(expr.getLeft()).to.be(left); + }); + }); + + describe('#getRight()', function() { + it('gets the right expression', function() { + expect(expr.getRight()).to.be(right); + }); + }); + +}); + +describe('ol.expr.Identifier', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Identifier('foo'); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Identifier); + }); + }); + + describe('#evaluate()', function() { + it('returns a number from the scope', function() { + var expr = new ol.expr.Identifier('foo'); + expect(expr.evaluate({foo: 42})).to.be(42); + }); + + it('returns a string from the scope', function() { + var expr = new ol.expr.Identifier('foo'); + expect(expr.evaluate({foo: 'chicken'})).to.be('chicken'); + }); + + it('returns a boolean from the scope', function() { + var expr = new ol.expr.Identifier('bar'); + expect(expr.evaluate({bar: false})).to.be(false); + expect(expr.evaluate({bar: true})).to.be(true); + }); + + it('returns a null from the scope', function() { + var expr = new ol.expr.Identifier('nada'); + expect(expr.evaluate({nada: null})).to.be(null); + }); + + it('works for unicode identifiers', function() { + var expr = new ol.expr.Identifier('\u03c0'); + expect(expr.evaluate({'\u03c0': Math.PI})).to.be(Math.PI); + }); + }); + + describe('#getName()', function() { + var expr = new ol.expr.Identifier('asdf'); + expect(expr.getName()).to.be('asdf'); + }); + +}); + +describe('ol.expr.Literal', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Literal(true); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Literal); + }); + }); + + describe('#evaluate()', function() { + it('works for numeric literal', function() { + var expr = new ol.expr.Literal(42e-11); + expect(expr.evaluate()).to.be(4.2e-10); + }); + + it('works for string literal', function() { + var expr = new ol.expr.Literal('asdf'); + expect(expr.evaluate()).to.be('asdf'); + }); + + it('works for boolean literal', function() { + var expr = new ol.expr.Literal(true); + expect(expr.evaluate()).to.be(true); + }); + + it('works for null literal', function() { + var expr = new ol.expr.Literal(null); + expect(expr.evaluate()).to.be(null); + }); + }); + + describe('#getValue()', function() { + var expr = new ol.expr.Literal('asdf'); + expect(expr.getValue()).to.be('asdf'); + }); + +}); + + +describe('ol.expr.Logical', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Logical( + ol.expr.LogicalOp.OR, + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Logical); + }); + }); + + describe('#evaluate()', function() { + it('applies || to resolved identifiers', function() { + var expr = new ol.expr.Logical( + ol.expr.LogicalOp.OR, + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(true); + expect(expr.evaluate({foo: false, bar: true})).to.be(true); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + + it('applies && to resolved identifiers', function() { + var expr = new ol.expr.Logical( + ol.expr.LogicalOp.AND, + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + + expect(expr.evaluate({foo: true, bar: true})).to.be(true); + expect(expr.evaluate({foo: true, bar: false})).to.be(false); + expect(expr.evaluate({foo: false, bar: true})).to.be(false); + expect(expr.evaluate({foo: false, bar: false})).to.be(false); + }); + }); + + describe('#isValidOp()', function() { + it('determines if a string is a valid operator', function() { + expect(ol.expr.Logical.isValidOp('||')).to.be(true); + expect(ol.expr.Logical.isValidOp('&&')).to.be(true); + + expect(ol.expr.Logical.isValidOp('')).to.be(false); + expect(ol.expr.Logical.isValidOp('+')).to.be(false); + expect(ol.expr.Logical.isValidOp('<')).to.be(false); + expect(ol.expr.Logical.isValidOp('|')).to.be(false); + }); + }); + + var op = ol.expr.LogicalOp.AND; + var left = new ol.expr.Identifier('foo'); + var right = new ol.expr.Literal(false); + var expr = new ol.expr.Logical(op, left, right); + + describe('#getOperator()', function() { + it('gets the operator', function() { + expect(expr.getOperator()).to.be(op); + }); + }); + + describe('#getLeft()', function() { + it('gets the left expression', function() { + expect(expr.getLeft()).to.be(left); + }); + }); + + describe('#getRight()', function() { + it('gets the right expression', function() { + expect(expr.getRight()).to.be(right); + }); + }); + +}); + +describe('ol.expr.Math', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.ADD, + new ol.expr.Literal(40), + new ol.expr.Literal(2)); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Math); + }); + }); + + describe('#evaluate()', function() { + it('does + with numeric literal', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.ADD, + new ol.expr.Literal(40), + new ol.expr.Literal(2)); + + expect(expr.evaluate()).to.be(42); + }); + + it('does + with string literal (note: subject to change)', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.ADD, + new ol.expr.Literal('foo'), + new ol.expr.Literal('bar')); + + expect(expr.evaluate()).to.be('foobar'); + }); + + it('does + with identifiers', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.ADD, + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + + expect(expr.evaluate({foo: 40, bar: 2})).to.be(42); + }); + + it('does - with identifiers', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.SUBTRACT, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(2)); + + expect(expr.evaluate({foo: 40})).to.be(38); + }); + + it('casts to number with - (note: this may throw later)', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.SUBTRACT, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(2)); + + expect(expr.evaluate({foo: '40'})).to.be(38); + }); + + it('does * with identifiers', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.MULTIPLY, + new ol.expr.Literal(2), + new ol.expr.Identifier('foo')); + + expect(expr.evaluate({foo: 21})).to.be(42); + }); + + it('casts to number with * (note: this may throw later)', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.MULTIPLY, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(2)); + + expect(expr.evaluate({foo: '21'})).to.be(42); + }); + + it('does % with identifiers', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.MOD, + new ol.expr.Literal(97), + new ol.expr.Identifier('foo')); + + expect(expr.evaluate({foo: 55})).to.be(42); + }); + + it('casts to number with % (note: this may throw later)', function() { + var expr = new ol.expr.Math( + ol.expr.MathOp.MOD, + new ol.expr.Identifier('foo'), + new ol.expr.Literal(100)); + + expect(expr.evaluate({foo: '150'})).to.be(50); + }); + }); + + describe('#isValidOp()', function() { + it('determines if a string is a valid operator', function() { + expect(ol.expr.Math.isValidOp('+')).to.be(true); + expect(ol.expr.Math.isValidOp('-')).to.be(true); + expect(ol.expr.Math.isValidOp('*')).to.be(true); + expect(ol.expr.Math.isValidOp('/')).to.be(true); + expect(ol.expr.Math.isValidOp('%')).to.be(true); + + expect(ol.expr.Math.isValidOp('')).to.be(false); + expect(ol.expr.Math.isValidOp('|')).to.be(false); + expect(ol.expr.Math.isValidOp('&')).to.be(false); + expect(ol.expr.Math.isValidOp('<')).to.be(false); + expect(ol.expr.Math.isValidOp('||')).to.be(false); + expect(ol.expr.Math.isValidOp('.')).to.be(false); + }); + }); + + var op = ol.expr.MathOp.MOD; + var left = new ol.expr.Identifier('foo'); + var right = new ol.expr.Literal(20); + var expr = new ol.expr.Math(op, left, right); + + describe('#getOperator()', function() { + it('gets the operator', function() { + expect(expr.getOperator()).to.be(op); + }); + }); + + describe('#getLeft()', function() { + it('gets the left expression', function() { + expect(expr.getLeft()).to.be(left); + }); + }); + + describe('#getRight()', function() { + it('gets the right expression', function() { + expect(expr.getRight()).to.be(right); + }); + }); + +}); + +describe('ol.expr.Member', function() { + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Member( + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Member); + }); + }); + + describe('#evaluate()', function() { + it('accesses an object property', function() { + + var expr = new ol.expr.Member( + new ol.expr.Identifier('foo'), + new ol.expr.Identifier('bar')); + + var scope = {foo: {bar: 42}}; + expect(expr.evaluate(scope)).to.be(42); + }); + }); + + var object = new ol.expr.Identifier('foo'); + var property = new ol.expr.Identifier('bar'); + var expr = new ol.expr.Member(object, property); + + describe('#getObject()', function() { + expect(expr.getObject()).to.be(object); + }); + + describe('#getProperty()', function() { + expect(expr.getProperty()).to.be(property); + }); + +}); + + +describe('ol.expr.Not', function() { + + describe('constructor', function() { + it('creates a new expression', function() { + var expr = new ol.expr.Not( + new ol.expr.Literal(true)); + expect(expr).to.be.a(ol.expr.Expression); + expect(expr).to.be.a(ol.expr.Not); + }); + }); + + describe('#evaluate()', function() { + it('returns the logical complement', function() { + var expr = new ol.expr.Not(new ol.expr.Literal(true)); + expect(expr.evaluate()).to.be(false); + + expr = new ol.expr.Not(new ol.expr.Literal(false)); + expect(expr.evaluate()).to.be(true); + }); + + it('negates a truthy string', function() { + var expr = new ol.expr.Not(new ol.expr.Literal('asdf')); + expect(expr.evaluate()).to.be(false); + }); + + it('negates a falsy string', function() { + var expr = new ol.expr.Not(new ol.expr.Literal('')); + expect(expr.evaluate()).to.be(true); + }); + + it('negates a truthy number', function() { + var expr = new ol.expr.Not(new ol.expr.Literal(42)); + expect(expr.evaluate()).to.be(false); + }); + + it('negates a falsy number', function() { + var expr = new ol.expr.Not(new ol.expr.Literal(NaN)); + expect(expr.evaluate()).to.be(true); + }); + }); + + describe('#getArgument()', function() { + var argument = new ol.expr.Literal(true); + var expr = new ol.expr.Not(argument); + expect(expr.getArgument()).to.be(argument); + }); + +}); + + +goog.require('ol.expr.Call'); +goog.require('ol.expr.Comparison'); +goog.require('ol.expr.ComparisonOp'); +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Literal'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.LogicalOp'); +goog.require('ol.expr.Math'); +goog.require('ol.expr.MathOp'); +goog.require('ol.expr.Member'); +goog.require('ol.expr.Not'); diff --git a/test/spec/ol/expr/lexer.test.js b/test/spec/ol/expr/lexer.test.js new file mode 100644 index 0000000000..2bf0b86b76 --- /dev/null +++ b/test/spec/ol/expr/lexer.test.js @@ -0,0 +1,509 @@ +goog.provide('ol.test.expression.Lexer'); + +describe('ol.expr.Lexer', function() { + + describe('constructor', function() { + it('creates a new lexer', function() { + var lexer = new ol.expr.Lexer('foo'); + expect(lexer).to.be.a(ol.expr.Lexer); + }); + }); + + describe('#next()', function() { + + it('returns one token at a time', function() { + var source = 'foo === "bar"'; + var lexer = new ol.expr.Lexer(source); + + // scan first token + var token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + + // scan second token + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + expect(token.value).to.be('==='); + + // scan third token + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + expect(token.value).to.be('bar'); + + // scan again + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.EOF); + + // and again + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.EOF); + }); + + }); + + describe('#peek()', function() { + + var lexer; + beforeEach(function() { + lexer = new ol.expr.Lexer('foo > 42 && bar == "chicken"'); + }); + + it('looks ahead without consuming token', function() { + var token = lexer.peek(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + }); + + it('works after a couple scans', function() { + var token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + expect(token.value).to.be('>'); + + token = lexer.peek(); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + expect(token.value).to.be(42); + + token = lexer.next(); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + expect(token.value).to.be(42); + }); + + it('returns the same thing when called multiple times', function() { + var token = lexer.peek(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + + for (var i = 0; i < 10; ++i) { + token = lexer.peek(); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + expect(token.value).to.be('foo'); + } + }); + + }); + + + describe('#scanIdentifier_()', function() { + + function scan(source, part) { + var lexer = new ol.expr.Lexer(source); + var token = lexer.scanIdentifier_(lexer.getCurrentCharCode_()); + if (!part) { + expect(token.index).to.be(0); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + } + return token; + } + + it('works for short identifiers', function() { + var token = scan('a'); + expect(token.value).to.be('a'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('works for longer identifiers', function() { + var token = scan('foo'); + expect(token.value).to.be('foo'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('works for $ anywhere', function() { + var token = scan('$foo$bar$'); + expect(token.value).to.be('$foo$bar$'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('works for _ anywhere', function() { + var token = scan('_foo_bar_'); + expect(token.value).to.be('_foo_bar_'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('works for keywords', function() { + var token = scan('delete'); + expect(token.value).to.be('delete'); + expect(token.type).to.be(ol.expr.TokenType.KEYWORD); + }); + + it('works for null', function() { + var token = scan('null'); + expect(token.value).to.be('null'); + expect(token.type).to.be(ol.expr.TokenType.NULL_LITERAL); + }); + + it('works for boolean true', function() { + var token = scan('true'); + expect(token.value).to.be('true'); + expect(token.type).to.be(ol.expr.TokenType.BOOLEAN_LITERAL); + }); + + it('works for boolean false', function() { + var token = scan('false'); + expect(token.value).to.be('false'); + expect(token.type).to.be(ol.expr.TokenType.BOOLEAN_LITERAL); + }); + + it('works with unicode escape sequences', function() { + var token = scan('\u006f\u006c\u0033'); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('works with hex escape sequences', function() { + var token = scan('\x6f\x6c\x33'); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + it('throws for identifiers starting with a number', function() { + expect(function() { + scan('4foo'); + }).throwException(); + }); + + it('throws for identifiers starting with a punctuation char', function() { + expect(function() { + scan('!foo'); + }).throwException(); + }); + + it('only scans valid identifier part', function() { + var token = scan('foo>bar', true); + expect(token.value).to.be('foo'); + expect(token.type).to.be(ol.expr.TokenType.IDENTIFIER); + }); + + }); + + describe('#scanNumericLiteral_()', function() { + + function scan(source) { + var lexer = new ol.expr.Lexer(source); + var token = lexer.scanNumericLiteral_(lexer.getCurrentCharCode_()); + expect(token.index).to.be(0); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return token; + } + + it('works for integers', function() { + var token = scan('123'); + expect(token.value).to.be(123); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('throws for bogus integer', function() { + expect(function() { + scan('123z'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('z'); + expect(token.index).to.be(3); + }); + }); + + it('works for float', function() { + var token = scan('123.456'); + expect(token.value).to.be(123.456); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('throws for bogus float', function() { + expect(function() { + scan('123.4x4'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('x'); + expect(token.index).to.be(5); + }); + }); + + it('works with exponent', function() { + var token = scan('1.234e5'); + expect(token.value).to.be(1.234e5); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('works with explicit positive exponent', function() { + var token = scan('1.234e+5'); + expect(token.value).to.be(1.234e5); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('works with negative exponent', function() { + var token = scan('1.234e-5'); + expect(token.value).to.be(1.234e-5); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('throws for bogus float', function() { + expect(function() { + scan('1.234eo4'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('o'); + expect(token.index).to.be(6); + }); + }); + + it('works with octals', function() { + var token = scan('02322'); + expect(token.value).to.be(1234); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('throws for bogus octal', function() { + // note that this is more strict than most es5 engines + expect(function() { + scan('02392'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('9'); + expect(token.index).to.be(3); + }); + }); + + it('works with hex', function() { + var token = scan('0x4d2'); + expect(token.value).to.be(1234); + expect(token.type).to.be(ol.expr.TokenType.NUMERIC_LITERAL); + }); + + it('throws for bogus hex', function() { + // note that this is more strict than most es5 engines + expect(function() { + scan('0x4G'); + }).throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.value).to.be('G'); + expect(token.index).to.be(3); + }); + }); + + }); + + describe('#scanPunctuator_()', function() { + + function scan(source) { + var lexer = new ol.expr.Lexer(source); + var token = lexer.scanPunctuator_(lexer.getCurrentCharCode_()); + expect(token.index).to.be(0); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return token; + } + + it('works for dot', function() { + var token = scan('.'); + expect(token.value).to.be('.'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for bang', function() { + var token = scan('!'); + expect(token.value).to.be('!'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for double equal', function() { + var token = scan('=='); + expect(token.value).to.be('=='); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for triple equal', function() { + var token = scan('==='); + expect(token.value).to.be('==='); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for not double equal', function() { + var token = scan('!='); + expect(token.value).to.be('!='); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for not triple equal', function() { + var token = scan('!=='); + expect(token.value).to.be('!=='); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for logical or', function() { + var token = scan('||'); + expect(token.value).to.be('||'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for logical and', function() { + var token = scan('&&'); + expect(token.value).to.be('&&'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for plus', function() { + var token = scan('+'); + expect(token.value).to.be('+'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for minus', function() { + var token = scan('-'); + expect(token.value).to.be('-'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for star', function() { + var token = scan('*'); + expect(token.value).to.be('*'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for slash', function() { + var token = scan('/'); + expect(token.value).to.be('/'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + it('works for percent', function() { + var token = scan('%'); + expect(token.value).to.be('%'); + expect(token.type).to.be(ol.expr.TokenType.PUNCTUATOR); + }); + + }); + + describe('#scanStringLiteral_()', function() { + + function scan(source) { + var lexer = new ol.expr.Lexer(source); + var token = lexer.scanStringLiteral_(lexer.getCurrentCharCode_()); + expect(token.index).to.be(0); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return token; + } + + it('parses double quoted string', function() { + var token = scan('"my string"'); + expect(token.value).to.be('my string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with internal single quotes', function() { + var token = scan('"my \'quoted\' string"'); + expect(token.value).to.be('my \'quoted\' string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with escaped double quotes', function() { + var token = scan('"my \\"quoted\\" string"'); + expect(token.value).to.be('my "quoted" string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with escaped backslash', function() { + var token = scan('"my \\\ string"'); + expect(token.value).to.be('my \ string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with unicode escape sequences', function() { + var token = scan('"\u006f\u006c\u0033"'); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with hex escape sequences', function() { + var token = scan('"\x6f\x6c\x33"'); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses double quoted string with tab', function() { + var token = scan('"a\ttab"'); + expect(token.value).to.be('a\ttab'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('throws on unterminated double quote', function() { + expect(function() { + scan('"never \'ending\' string'); + }).to.throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.type).to.be(ol.expr.TokenType.EOF); + expect(token.index).to.be(22); + }); + }); + + it('parses single quoted string', function() { + var token = scan('\'my string\''); + expect(token.value).to.be('my string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with internal double quotes', function() { + var token = scan('\'my "quoted" string\''); + expect(token.value).to.be('my "quoted" string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with escaped single quotes', function() { + var token = scan('\'my \\\'quoted\\\' string\''); + expect(token.value).to.be('my \'quoted\' string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with escaped backslash', function() { + var token = scan('\'my \\\ string\''); + expect(token.value).to.be('my \ string'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with unicode escape sequences', function() { + var token = scan('\'\u006f\u006c\u0033\''); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with hex escape sequences', function() { + var token = scan('\'\x6f\x6c\x33\''); + expect(token.value).to.be('ol3'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('parses single quoted string with tab', function() { + var token = scan('\'a\ttab\''); + expect(token.value).to.be('a\ttab'); + expect(token.type).to.be(ol.expr.TokenType.STRING_LITERAL); + }); + + it('throws on unterminated single quote', function() { + expect(function() { + scan('\'never "ending" string'); + }).to.throwException(function(err) { + expect(err).to.be.an(ol.expr.UnexpectedToken); + var token = err.token; + expect(token.type).to.be(ol.expr.TokenType.EOF); + expect(token.index).to.be(22); + }); + }); + + }); + +}); + +goog.require('ol.expr.Lexer'); +goog.require('ol.expr.TokenType'); +goog.require('ol.expr.UnexpectedToken'); diff --git a/test/spec/ol/expr/parser.test.js b/test/spec/ol/expr/parser.test.js new file mode 100644 index 0000000000..ac0f640a46 --- /dev/null +++ b/test/spec/ol/expr/parser.test.js @@ -0,0 +1,275 @@ +goog.provide('ol.test.expression.Parser'); + +describe('ol.expr.Parser', function() { + + describe('constructor', function() { + it('creates a new expression parser', function() { + var parser = new ol.expr.Parser(); + expect(parser).to.be.a(ol.expr.Parser); + }); + }); + + describe('#parseArguments_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parseArguments_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('parses comma separated expressions in parens', function() { + var args = parse('(1/3, "foo", true)'); + expect(args).length(3); + + expect(args[0]).to.be.a(ol.expr.Math); + expect(args[0].evaluate()).to.be(1 / 3); + + expect(args[1]).to.be.a(ol.expr.Literal); + expect(args[1].evaluate()).to.be('foo'); + + expect(args[2]).to.be.a(ol.expr.Literal); + expect(args[2].evaluate()).to.be(true); + }); + + it('throws on invalid arg expression', function() { + expect(function() { + parse('(6e)'); + }).throwException(); + }); + + it('throws on unterminated args', function() { + expect(function() { + parse('("foo", 42, )'); + }).throwException(); + }); + + }); + + describe('#parseBinaryExpression_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parseBinaryExpression_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('works with multiplicitave operators', function() { + var expr = parse('4 * 1e4'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate()).to.be(40000); + + expect(parse('10/3').evaluate()).to.be(10 / 3); + }); + + it('works with additive operators', function() { + var expr = parse('4 +1e4'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate()).to.be(10004); + + expect(parse('10-3').evaluate()).to.be(7); + }); + + it('works with relational operators', function() { + var expr = parse('4 < 1e4'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate()).to.be(true); + + expect(parse('10<3').evaluate()).to.be(false); + expect(parse('10 <= "10"').evaluate()).to.be(true); + expect(parse('10 > "10"').evaluate()).to.be(false); + expect(parse('10 >= 9').evaluate()).to.be(true); + }); + + it('works with equality operators', function() { + var expr = parse('4 == 1e4'); + expect(expr).to.be.a(ol.expr.Comparison); + expect(expr.evaluate()).to.be(false); + + expect(parse('10!=3').evaluate()).to.be(true); + expect(parse('10 == "10"').evaluate()).to.be(true); + expect(parse('10 === "10"').evaluate()).to.be(false); + expect(parse('10 !== "10"').evaluate()).to.be(true); + }); + + it('works with binary logical operators', function() { + var expr = parse('true && false'); + expect(expr).to.be.a(ol.expr.Logical); + expect(expr.evaluate()).to.be(false); + + expect(parse('false||true').evaluate()).to.be(true); + expect(parse('false || false').evaluate()).to.be(false); + expect(parse('true &&true').evaluate()).to.be(true); + }); + + it('throws for invalid binary expression', function() { + expect(function() { + parse('4 * / 2'); + }).throwException(); + + expect(function() { + parse('4 < / 2'); + }).throwException(); + + expect(function() { + parse('4 * && 2'); + }).throwException(); + + }); + + }); + + describe('#parseGroupExpression_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parseGroupExpression_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('parses grouped expressions', function() { + var expr = parse('(3 * (foo + 2))'); + expect(expr).to.be.a(ol.expr.Math); + expect(expr.evaluate({foo: 3})).to.be(15); + }); + + }); + + describe('#parseLeftHandSideExpression_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parseLeftHandSideExpression_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('parses member expressions', function() { + var expr = parse('foo.bar.bam'); + expect(expr).to.be.a(ol.expr.Member); + }); + + it('throws on invalid member expression', function() { + expect(function() { + parse('foo.4'); + }).throwException(); + }); + + it('parses call expressions', function() { + var expr = parse('foo(bar)'); + expect(expr).to.be.a(ol.expr.Call); + var fns = { + foo: function(arg) { + expect(arguments).length(1); + expect(arg).to.be('chicken'); + return 'got ' + arg; + } + }; + var scope = { + bar: 'chicken' + }; + expect(expr.evaluate(scope, fns)).to.be('got chicken'); + }); + + it('throws on invalid call expression', function() { + expect(function() { + parse('foo(*)'); + }).throwException(); + }); + + }); + + + describe('#parsePrimaryExpression_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parsePrimaryExpression_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('parses string literal', function() { + var expr = parse('"foo"'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be('foo'); + }); + + it('parses numeric literal', function() { + var expr = parse('.42e2'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(42); + }); + + it('parses boolean literal', function() { + var expr = parse('.42e2'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(42); + }); + + it('parses null literal', function() { + var expr = parse('null'); + expect(expr).to.be.a(ol.expr.Literal); + expect(expr.evaluate()).to.be(null); + }); + + }); + + describe('#parseUnaryExpression_()', function() { + + function parse(source) { + var lexer = new ol.expr.Lexer(source); + var parser = new ol.expr.Parser(); + var expr = parser.parseUnaryExpression_(lexer); + expect(lexer.peek().type).to.be(ol.expr.TokenType.EOF); + return expr; + } + + it('parses logical not', function() { + var expr = parse('!foo'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate({foo: true})).to.be(false); + }); + + it('works with string literal', function() { + var expr = parse('!"foo"'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate()).to.be(false); + }); + + it('works with empty string', function() { + var expr = parse('!""'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate()).to.be(true); + }); + + it('works with null', function() { + var expr = parse('!null'); + expect(expr).to.be.a(ol.expr.Not); + expect(expr.evaluate()).to.be(true); + }); + + }); + + +}); + + +goog.require('ol.expr.Expression'); +goog.require('ol.expr.Call'); +goog.require('ol.expr.Comparison'); +goog.require('ol.expr.Lexer'); +goog.require('ol.expr.Literal'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.Math'); +goog.require('ol.expr.Member'); +goog.require('ol.expr.Not'); +goog.require('ol.expr.Parser'); +goog.require('ol.expr.TokenType'); diff --git a/test/spec/ol/expression.test.js b/test/spec/ol/expression.test.js deleted file mode 100644 index fb9735d1d2..0000000000 --- a/test/spec/ol/expression.test.js +++ /dev/null @@ -1,78 +0,0 @@ -goog.provide('ol.test.Expression'); - -describe('ol.Expression', function() { - - describe('constructor', function() { - it('creates an expression', function() { - var exp = new ol.Expression('foo'); - expect(exp).to.be.a(ol.Expression); - }); - }); - - describe('#evaluate()', function() { - - it('evaluates and returns the result', function() { - // test cases here with unique values only (lack of messages in expect) - var cases = [{ - source: '42', result: 42 - }, { - source: '10 + 10', result: 20 - }, { - source: '"a" + "b"', result: 'ab' - }, { - source: 'Math.floor(Math.PI)', result: 3 - }, { - source: 'ol', result: ol - }, { - source: 'this', result: goog.global - }]; - - var c, exp; - for (var i = 0, ii = cases.length; i < ii; ++i) { - c = cases[i]; - exp = new ol.Expression(c.source); - expect(exp.evaluate()).to.be(c.result); - } - }); - - it('accepts an optional this argument', function() { - function Thing() { - this.works = true; - }; - - var exp = new ol.Expression('this.works ? "yes" : "no"'); - expect(exp.evaluate(new Thing())).to.be('yes'); - expect(exp.evaluate({})).to.be('no'); - }); - - it('accepts an optional scope argument', function() { - var exp; - var scope = { - greeting: 'hello world', - punctuation: '!', - pick: function(array, index) { - return array[index]; - } - }; - - // access two members in the scope - exp = new ol.Expression('greeting + punctuation'); - expect(exp.evaluate({}, scope)).to.be('hello world!'); - - // call a function in the scope - exp = new ol.Expression( - 'pick([10, 42, "chicken"], 2) + Math.floor(Math.PI)'); - expect(exp.evaluate({}, scope)).to.be('chicken3'); - - }); - - it('throws on error', function() { - var exp = new ol.Expression('@*)$(&'); - expect(function() {exp.evaluate()}).to.throwException(); - }); - - }); - -}); - -goog.require('ol.Expression'); diff --git a/test/spec/ol/filter/extentfilter.test.js b/test/spec/ol/filter/extentfilter.test.js deleted file mode 100644 index 835d4411d8..0000000000 --- a/test/spec/ol/filter/extentfilter.test.js +++ /dev/null @@ -1,36 +0,0 @@ -goog.provide('ol.test.filter.Extent'); - - -describe('ol.filter.Extent', function() { - - var extent, filter; - - beforeEach(function() { - extent = [0, 45, 0, 90]; - filter = new ol.filter.Extent(extent); - }); - - describe('#getExtent()', function() { - - it('returns the configured extent', function() { - expect(filter.getExtent()).to.be(extent); - }); - - }); - - describe('#evaluate()', function() { - - it('returns true if a feature intersects, false if not', function() { - expect(filter.applies(new ol.Feature({g: new ol.geom.Point([44, 89])}))) - .to.be(true); - expect(filter.applies(new ol.Feature({g: new ol.geom.Point([46, 91])}))) - .to.be(false); - }); - - }); - -}); - -goog.require('ol.Feature'); -goog.require('ol.filter.Extent'); -goog.require('ol.geom.Point'); diff --git a/test/spec/ol/filter/geometryfilter.test.js b/test/spec/ol/filter/geometryfilter.test.js deleted file mode 100644 index ab64abc862..0000000000 --- a/test/spec/ol/filter/geometryfilter.test.js +++ /dev/null @@ -1,51 +0,0 @@ -goog.provide('ol.test.filter.Geometry'); - - -describe('ol.filter.Geometry', function() { - - describe('constructor', function() { - it('creates a new filter', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.POINT); - expect(filter).to.be.a(ol.filter.Geometry); - }); - }); - - describe('#getType()', function() { - - it('works for point', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.POINT); - expect(filter.getType()).to.be(ol.filter.GeometryType.POINT); - }); - - it('works for linestring', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.LINESTRING); - expect(filter.getType()).to.be(ol.filter.GeometryType.LINESTRING); - }); - - it('works for polygon', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.POLYGON); - expect(filter.getType()).to.be(ol.filter.GeometryType.POLYGON); - }); - - it('works for multi-point', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.MULTIPOINT); - expect(filter.getType()).to.be(ol.filter.GeometryType.MULTIPOINT); - }); - - it('works for multi-linestring', function() { - var filter = new ol.filter.Geometry( - ol.filter.GeometryType.MULTILINESTRING); - expect(filter.getType()).to.be(ol.filter.GeometryType.MULTILINESTRING); - }); - - it('works for multi-polygon', function() { - var filter = new ol.filter.Geometry(ol.filter.GeometryType.MULTIPOLYGON); - expect(filter.getType()).to.be(ol.filter.GeometryType.MULTIPOLYGON); - }); - - }); - -}); - -goog.require('ol.filter.Geometry'); -goog.require('ol.filter.GeometryType'); diff --git a/test/spec/ol/filter/logicalfilter.test.js b/test/spec/ol/filter/logicalfilter.test.js deleted file mode 100644 index 847d43af3f..0000000000 --- a/test/spec/ol/filter/logicalfilter.test.js +++ /dev/null @@ -1,144 +0,0 @@ -goog.provide('ol.test.filter.Logical'); - - -describe('ol.filter.Logical', function() { - - var OR = ol.filter.LogicalOperator.OR; - var AND = ol.filter.LogicalOperator.AND; - var NOT = ol.filter.LogicalOperator.NOT; - var include = new ol.filter.Filter(function() {return true;}); - var exclude = new ol.filter.Filter(function() {return false;}); - - var apple = new ol.Feature({}); - var orange = new ol.Feature({}); - var duck = new ol.Feature({}); - - var isApple = new ol.filter.Filter(function(feature) { - return feature === apple; - }); - var isOrange = new ol.filter.Filter(function(feature) { - return feature === orange; - }); - var isDuck = new ol.filter.Filter(function(feature) { - return feature === duck; - }); - - describe('constructor', function() { - it('creates a new filter', function() { - var filter = new ol.filter.Logical([include, exclude], OR); - expect(filter).to.be.a(ol.filter.Logical); - }); - }); - - describe('#operator', function() { - it('can be OR', function() { - var filter = new ol.filter.Logical([include, exclude], OR); - expect(filter.operator).to.be(OR); - }); - - it('can be AND', function() { - var filter = new ol.filter.Logical([include, exclude], AND); - expect(filter.operator).to.be(AND); - }); - - it('can be NOT', function() { - var filter = new ol.filter.Logical([include], NOT); - expect(filter.operator).to.be(NOT); - }); - }); - - describe('#applies', function() { - - it('works for OR', function() { - var isFruit = new ol.filter.Logical([isApple, isOrange], OR); - - expect(isApple.applies(apple)).to.be(true); - expect(isOrange.applies(apple)).to.be(false); - expect(isFruit.applies(apple)).to.be(true); - - expect(isApple.applies(duck)).to.be(false); - expect(isOrange.applies(duck)).to.be(false); - expect(isFruit.applies(duck)).to.be(false); - }); - - it('works for AND', function() { - expect(include.applies(apple)).to.be(true); - expect(isApple.applies(apple)).to.be(true); - expect(isDuck.applies(apple)).to.be(false); - - var pass = new ol.filter.Logical([include, isApple], AND); - expect(pass.applies(apple)).to.be(true); - - var fail = new ol.filter.Logical([isApple, isDuck], AND); - expect(fail.applies(apple)).to.be(false); - }); - - it('works for NOT', function() { - expect(isApple.applies(apple)).to.be(true); - expect(isDuck.applies(apple)).to.be(false); - expect(isDuck.applies(duck)).to.be(true); - expect(isDuck.applies(apple)).to.be(false); - - var notApple = new ol.filter.Logical([isApple], NOT); - expect(notApple.applies(apple)).to.be(false); - expect(notApple.applies(duck)).to.be(true); - - var notDuck = new ol.filter.Logical([isDuck], NOT); - expect(notDuck.applies(apple)).to.be(true); - expect(notDuck.applies(duck)).to.be(false); - }); - }); - -}); - -describe('ol.filter.and', function() { - it('creates the a logical AND filter', function() { - var a = new ol.filter.Filter(); - var b = new ol.filter.Filter(); - var c = new ol.filter.Filter(); - var and = ol.filter.and(a, b, c); - expect(and).to.be.a(ol.filter.Logical); - expect(and.operator).to.be(ol.filter.LogicalOperator.AND); - var filters = and.getFilters(); - expect(filters[0]).to.be(a); - expect(filters[1]).to.be(b); - expect(filters[2]).to.be(c); - }); -}); - -describe('ol.filter.not', function() { - it('creates the logical compliment of another filter', function() { - var include = new ol.filter.Filter(function() {return true;}); - var notInclude = ol.filter.not(include); - expect(notInclude).to.be.a(ol.filter.Logical); - expect(notInclude.applies()).to.be(false); - - var exclude = new ol.filter.Filter(function() {return false;}); - var notExclude = ol.filter.not(exclude); - expect(notExclude).to.be.a(ol.filter.Logical); - expect(notExclude.applies()).to.be(true); - }); -}); - -describe('ol.filter.or', function() { - it('creates the a logical OR filter', function() { - var a = new ol.filter.Filter(); - var b = new ol.filter.Filter(); - var c = new ol.filter.Filter(); - var and = ol.filter.or(a, b, c); - expect(and).to.be.a(ol.filter.Logical); - expect(and.operator).to.be(ol.filter.LogicalOperator.OR); - var filters = and.getFilters(); - expect(filters[0]).to.be(a); - expect(filters[1]).to.be(b); - expect(filters[2]).to.be(c); - }); -}); - -goog.require('ol.Feature'); -goog.require('ol.filter.Filter'); -goog.require('ol.filter.Logical'); -goog.require('ol.filter.LogicalOperator'); -goog.require('ol.filter.and'); -goog.require('ol.filter.not'); -goog.require('ol.filter.or'); diff --git a/test/spec/ol/layer/vectorlayer.test.js b/test/spec/ol/layer/vectorlayer.test.js index 243750fb35..1525617d6a 100644 --- a/test/spec/ol/layer/vectorlayer.test.js +++ b/test/spec/ol/layer/vectorlayer.test.js @@ -50,46 +50,46 @@ describe('ol.layer.Vector', function() { layer.addFeatures(features); }); - var geomFilter = new ol.filter.Geometry(ol.geom.GeometryType.LINESTRING); - var extentFilter = new ol.filter.Extent([16, 16.3, 48, 48.3]); + var geomFilter = ol.expr.parse('geometryType("linestring")'); + var extentFilter = ol.expr.parse('extent(16, 16.3, 48, 48.3)'); it('can filter by geometry type using its GeometryType index', function() { - sinon.spy(geomFilter, 'applies'); + sinon.spy(geomFilter, 'evaluate'); var lineStrings = layer.getFeatures(geomFilter); - expect(geomFilter.applies).to.not.be.called(); + expect(geomFilter.evaluate).to.not.be.called(); expect(lineStrings.length).to.eql(4); expect(lineStrings).to.contain(features[4]); }); it('can filter by extent using its RTree', function() { - sinon.spy(extentFilter, 'applies'); + sinon.spy(extentFilter, 'evaluate'); var subset = layer.getFeatures(extentFilter); - expect(extentFilter.applies).to.not.be.called(); + expect(extentFilter.evaluate).to.not.be.called(); expect(subset.length).to.eql(4); expect(subset).not.to.contain(features[7]); }); it('can filter by extent and geometry type using its index', function() { - var filter1 = new ol.filter.Logical([geomFilter, extentFilter], - ol.filter.LogicalOperator.AND); - var filter2 = new ol.filter.Logical([extentFilter, geomFilter], - ol.filter.LogicalOperator.AND); - sinon.spy(filter1, 'applies'); - sinon.spy(filter2, 'applies'); + var filter1 = new ol.expr.Logical( + ol.expr.LogicalOp.AND, geomFilter, extentFilter); + var filter2 = new ol.expr.Logical( + ol.expr.LogicalOp.AND, extentFilter, geomFilter); + sinon.spy(filter1, 'evaluate'); + sinon.spy(filter2, 'evaluate'); var subset1 = layer.getFeatures(filter1); var subset2 = layer.getFeatures(filter2); - expect(filter1.applies).to.not.be.called(); - expect(filter2.applies).to.not.be.called(); + expect(filter1.evaluate).to.not.be.called(); + expect(filter2.evaluate).to.not.be.called(); expect(subset1.length).to.eql(0); expect(subset2.length).to.eql(0); }); - it('can handle query using the filter\'s applies function', function() { - var filter = new ol.filter.Logical([geomFilter, extentFilter], - ol.filter.LogicalOperator.OR); - sinon.spy(filter, 'applies'); + it('can handle query using the filter\'s evaluate function', function() { + var filter = new ol.expr.Logical( + ol.expr.LogicalOp.OR, geomFilter, extentFilter); + sinon.spy(filter, 'evaluate'); var subset = layer.getFeatures(filter); - expect(filter.applies).to.be.called(); + expect(filter.evaluate).to.be.called(); expect(subset.length).to.eql(8); }); @@ -107,7 +107,7 @@ describe('ol.layer.Vector', function() { symbolizers: [ new ol.style.Line({ strokeWidth: 2, - strokeColor: new ol.Expression('colorProperty'), + strokeColor: ol.expr.parse('colorProperty'), opacity: 1 }) ] @@ -144,7 +144,7 @@ describe('ol.layer.Vector', function() { it('groups equal symbolizers also when defined on features', function() { var symbolizer = new ol.style.Line({ strokeWidth: 3, - strokeColor: new ol.Expression('colorProperty'), + strokeColor: ol.expr.parse('colorProperty'), opacity: 1 }); var anotherSymbolizer = new ol.style.Line({ @@ -177,13 +177,10 @@ describe('ol.layer.Vector', function() { }); goog.require('goog.dispose'); -goog.require('ol.Expression'); goog.require('ol.Feature'); -goog.require('ol.filter.Extent'); -goog.require('ol.filter.Geometry'); -goog.require('ol.filter.Logical'); -goog.require('ol.filter.LogicalOperator'); -goog.require('ol.geom.GeometryType'); +goog.require('ol.expr'); +goog.require('ol.expr.Logical'); +goog.require('ol.expr.LogicalOp'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.proj'); diff --git a/test/spec/ol/style/line.test.js b/test/spec/ol/style/line.test.js index eaf2471dfb..6022486638 100644 --- a/test/spec/ol/style/line.test.js +++ b/test/spec/ol/style/line.test.js @@ -42,8 +42,8 @@ describe('ol.style.Line', function() { it('accepts expressions', function() { var symbolizer = new ol.style.Line({ - opacity: new ol.Expression('value / 100'), - strokeWidth: ol.Expression('widthAttr') + opacity: ol.expr.parse('value / 100'), + strokeWidth: ol.expr.parse('widthAttr') }); expect(symbolizer).to.be.a(ol.style.Line); }); @@ -54,8 +54,8 @@ describe('ol.style.Line', function() { it('evaluates expressions with the given feature', function() { var symbolizer = new ol.style.Line({ - opacity: new ol.Expression('value / 100'), - strokeWidth: ol.Expression('widthAttr') + opacity: ol.expr.parse('value / 100'), + strokeWidth: ol.expr.parse('widthAttr') }); var feature = new ol.Feature({ @@ -73,7 +73,7 @@ describe('ol.style.Line', function() { }); -goog.require('ol.Expression'); goog.require('ol.Feature'); +goog.require('ol.expr'); goog.require('ol.style.Line'); goog.require('ol.style.LineLiteral'); diff --git a/test/spec/ol/style/polygon.test.js b/test/spec/ol/style/polygon.test.js index b56bb1b160..6ce43ae867 100644 --- a/test/spec/ol/style/polygon.test.js +++ b/test/spec/ol/style/polygon.test.js @@ -45,8 +45,8 @@ describe('ol.style.Polygon', function() { it('accepts expressions', function() { var symbolizer = new ol.style.Polygon({ - opacity: new ol.Expression('value / 100'), - fillColor: new ol.Expression('fillAttr') + opacity: ol.expr.parse('value / 100'), + fillColor: ol.expr.parse('fillAttr') }); expect(symbolizer).to.be.a(ol.style.Polygon); }); @@ -57,8 +57,8 @@ describe('ol.style.Polygon', function() { it('evaluates expressions with the given feature', function() { var symbolizer = new ol.style.Polygon({ - opacity: new ol.Expression('value / 100'), - fillColor: new ol.Expression('fillAttr') + opacity: ol.expr.parse('value / 100'), + fillColor: ol.expr.parse('fillAttr') }); var feature = new ol.Feature({ @@ -89,7 +89,7 @@ describe('ol.style.Polygon', function() { }); -goog.require('ol.Expression'); goog.require('ol.Feature'); +goog.require('ol.expr'); goog.require('ol.style.Polygon'); goog.require('ol.style.PolygonLiteral'); diff --git a/test/spec/ol/style/rule.test.js b/test/spec/ol/style/rule.test.js index d4649eda6c..c2a2e2d821 100644 --- a/test/spec/ol/style/rule.test.js +++ b/test/spec/ol/style/rule.test.js @@ -13,14 +13,14 @@ describe('ol.style.Rule', function() { it('returns false when the rule does not apply', function() { rule = new ol.style.Rule({ - filter: new ol.filter.Filter(function() { return false; }) + filter: new ol.expr.Literal(false) }); expect(rule.applies(feature)).to.be(false); }); it('returns true when the rule applies', function() { rule = new ol.style.Rule({ - filter: new ol.filter.Filter(function() { return true; }) + filter: new ol.expr.Literal(true) }); expect(rule.applies(feature)).to.be(true); }); @@ -29,5 +29,5 @@ describe('ol.style.Rule', function() { }); goog.require('ol.Feature'); -goog.require('ol.filter.Filter'); +goog.require('ol.expr.Literal'); goog.require('ol.style.Rule'); diff --git a/test/spec/ol/style/shape.test.js b/test/spec/ol/style/shape.test.js index 361ddc6727..1e629eef1b 100644 --- a/test/spec/ol/style/shape.test.js +++ b/test/spec/ol/style/shape.test.js @@ -51,8 +51,8 @@ describe('ol.style.Shape', function() { it('accepts expressions', function() { var symbolizer = new ol.style.Shape({ - size: new ol.Expression('sizeAttr'), - strokeColor: new ol.Expression('color') + size: ol.expr.parse('sizeAttr'), + strokeColor: ol.expr.parse('color') }); expect(symbolizer).to.be.a(ol.style.Shape); }); @@ -63,8 +63,8 @@ describe('ol.style.Shape', function() { it('evaluates expressions with the given feature', function() { var symbolizer = new ol.style.Shape({ - size: new ol.Expression('sizeAttr'), - opacity: new ol.Expression('opacityAttr'), + size: ol.expr.parse('sizeAttr'), + opacity: ol.expr.parse('opacityAttr'), fillColor: '#BADA55' }); @@ -99,8 +99,8 @@ describe('ol.style.Shape', function() { it('applies default type if none provided', function() { var symbolizer = new ol.style.Shape({ - size: new ol.Expression('sizeAttr'), - opacity: new ol.Expression('opacityAttr'), + size: ol.expr.parse('sizeAttr'), + opacity: ol.expr.parse('opacityAttr'), fillColor: '#BADA55' }); @@ -119,8 +119,8 @@ describe('ol.style.Shape', function() { }); -goog.require('ol.Expression'); goog.require('ol.Feature'); +goog.require('ol.expr'); goog.require('ol.style.Shape'); goog.require('ol.style.ShapeLiteral'); goog.require('ol.style.ShapeType'); diff --git a/test/spec/ol/style/style.test.js b/test/spec/ol/style/style.test.js index 685c09f3f3..6a35416548 100644 --- a/test/spec/ol/style/style.test.js +++ b/test/spec/ol/style/style.test.js @@ -9,9 +9,7 @@ describe('ol.style.Style', function() { var style = new ol.style.Style({ rules: [ new ol.style.Rule({ - filter: new ol.filter.Filter(function(feature) { - return feature.get('foo') == 'bar'; - }), + filter: 'foo == "bar"', symbolizers: [ new ol.style.Shape({ size: 4, @@ -66,7 +64,6 @@ goog.require('ol.Feature'); goog.require('ol.geom.LineString'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); -goog.require('ol.filter.Filter'); goog.require('ol.style.Rule'); goog.require('ol.style.Shape'); goog.require('ol.style.ShapeLiteral');