diff --git a/examples/cql-format.html b/examples/cql-format.html new file mode 100644 index 0000000000..ccecad7e42 --- /dev/null +++ b/examples/cql-format.html @@ -0,0 +1,51 @@ + + + + + OpenLayers CQL Example + + + + + + + +

CQL Filter Example

+
+ CQL, filter +
+

+ Demonstrate use the CQL filter. +

+
+
+

+ Enter text for a CQL filter to update the features displayed. +
+

+ + + + +
+ +

+ View the cql-filter.js source + to see how this is done. +

+
+ + + + diff --git a/examples/cql-format.js b/examples/cql-format.js new file mode 100644 index 0000000000..2119b1aaa2 --- /dev/null +++ b/examples/cql-format.js @@ -0,0 +1,61 @@ + +// use a CQL parser for easy filter creation +var format = new OpenLayers.Format.CQL(); + +// this rule will get a filter from the CQL text in the form +var rule = new OpenLayers.Rule({ + // We could also set a filter here. E.g. + // filter: format.read("STATE_ABBR >= 'B' AND STATE_ABBR <= 'O'"), + symbolizer: { + fillColor: "#ff0000", + strokeColor: "#ffcccc", + fillOpacity: "0.5" + } +}); + +var states = new OpenLayers.Layer.Vector("States", { + styleMap: new OpenLayers.StyleMap({ + "default": new OpenLayers.Style(null, {rules: [rule]}) + }) +}); + +var map = new OpenLayers.Map({ + div: "map", + layers: [ + new OpenLayers.Layer.WMS( + "OpenLayers WMS", + "http://maps.opengeo.org/geowebcache/service/wms", + {layers: "openstreetmap", format: "image/png"} + ), + states + ], + center: new OpenLayers.LonLat(-101, 39), + zoom: 3 +}); + +// called when features are fetched +function loadFeatures(data) { + var features = new OpenLayers.Format.GeoJSON().read(data); + states.addFeatures(features); +}; + +// update filter and redraw when form is submitted +var cql = document.getElementById("cql"); +var output = document.getElementById("output"); +function updateFilter() { + var filter; + try { + filter = format.read(cql.value); + } catch (err) { + output.value = err.message; + } + if (filter) { + output.value = ""; + rule.filter = filter; + states.redraw(); + } + return false; +} +updateFilter(); +var form = document.getElementById("cql_form"); +form.onsubmit = updateFilter; diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index a86b6c8704..3bcadac9bc 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -251,6 +251,7 @@ "OpenLayers/Format/WMSDescribeLayer.js", "OpenLayers/Format/WMSDescribeLayer/v1_1.js", "OpenLayers/Format/WKT.js", + "OpenLayers/Format/CQL.js", "OpenLayers/Format/OSM.js", "OpenLayers/Format/GPX.js", "OpenLayers/Format/Filter.js", diff --git a/lib/OpenLayers/Format/CQL.js b/lib/OpenLayers/Format/CQL.js new file mode 100644 index 0000000000..e030eb496b --- /dev/null +++ b/lib/OpenLayers/Format/CQL.js @@ -0,0 +1,438 @@ +/* Copyright (c) 2006-2011 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + +/** + * @requires OpenLayers/Format/WKT.js + */ + +/** + * Class: OpenLayers.Format.CQL + * Read CQL strings to get objects. Write + * objects to get CQL strings. Create a new parser with + * the constructor. + * + * Inherits from: + * - + */ +OpenLayers.Format.CQL = (function() { + + var tokens = [ + "PROPERTY", "COMPARISON", "VALUE", "LOGICAL" + ], + + patterns = { + PROPERTY: /^[_a-zA-Z]\w*/, + COMPARISON: /^(=|<>|<=|<|>=|>|LIKE)/i, + COMMA: /^,/, + LOGICAL: /^(AND|OR)/i, + VALUE: /^('\w+'|\d+(\.\d*)?|\.\d+)/, + LPAREN: /^\(/, + RPAREN: /^\)/, + SPATIAL: /^(BBOX|INTERSECTS|DWITHIN|WITHIN|CONTAINS)/i, + NOT: /^NOT/i, + BETWEEN: /^BETWEEN/i, + GEOMETRY: function(text) { + var type = /^(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)/.exec(text); + if (type) { + var len = text.length; + var idx = text.indexOf("(", type[0].length); + if (idx > -1) { + var depth = 1; + while (idx < len && depth > 0) { + idx++; + switch(text.charAt(idx)) { + case '(': + depth++; + break; + case ')': + depth--; + break; + default: + // in default case, do nothing + } + } + } + return [text.substr(0, idx+1)]; + } + }, + END: /^$/ + }, + + follows = { + LPAREN: ['GEOMETRY', 'SPATIAL', 'PROPERTY', 'VALUE', 'LPAREN'], + RPAREN: ['NOT', 'LOGICAL', 'END', 'RPAREN'], + PROPERTY: ['COMPARISON', 'BETWEEN', 'COMMA'], + BETWEEN: ['VALUE'], + COMPARISON: ['VALUE'], + COMMA: ['GEOMETRY', 'VALUE', 'PROPERTY'], + VALUE: ['LOGICAL', 'COMMA', 'RPAREN', 'END'], + SPATIAL: ['LPAREN'], + LOGICAL: ['NOT', 'VALUE', 'SPATIAL', 'PROPERTY', 'LPAREN'], + NOT: ['PROPERTY', 'LPAREN'], + GEOMETRY: ['COMMA', 'RPAREN'] + }, + + operators = { + '=': OpenLayers.Filter.Comparison.EQUAL_TO, + '<>': OpenLayers.Filter.Comparison.NOT_EQUAL_TO, + '<': OpenLayers.Filter.Comparison.LESS_THAN, + '<=': OpenLayers.Filter.Comparison.LESS_THAN_OR_EQUAL_TO, + '>': OpenLayers.Filter.Comparison.GREATER_THAN, + '>=': OpenLayers.Filter.Comparison.GREATER_THAN_OR_EQUAL_TO, + 'LIKE': OpenLayers.Filter.Comparison.LIKE, + 'BETWEEN': OpenLayers.Filter.Comparison.BETWEEN + }, + + operatorReverse = {}, + + logicals = { + 'AND': OpenLayers.Filter.Logical.AND, + 'OR': OpenLayers.Filter.Logical.OR + }, + + logicalReverse = {}, + + precedence = { + 'RPAREN': 3, + 'LOGICAL': 2, + 'COMPARISON': 1 + }; + + var i; + for (i in operators) { + if (operators.hasOwnProperty(i)) { + operatorReverse[operators[i]] = i; + } + } + + for (i in logicals) { + if (logicals.hasOwnProperty(i)) { + logicalReverse[logicals[i]] = i; + } + } + + function tryToken(text, pattern) { + if (pattern instanceof RegExp) { + return pattern.exec(text); + } else { + return pattern(text); + } + } + + function nextToken(text, tokens) { + var i, token, len = tokens.length; + for (i=0; i 0 && + (precedence[operatorStack[operatorStack.length - 1].type] <= p) + ) { + postfix.push(operatorStack.pop()); + } + + operatorStack.push(tok); + break; + case "SPATIAL": + case "NOT": + case "LPAREN": + operatorStack.push(tok); + break; + case "RPAREN": + while (operatorStack.length > 0 && + (operatorStack[operatorStack.length - 1].type != "LPAREN") + ) { + postfix.push(operatorStack.pop()); + } + operatorStack.pop(); // toss out the LPAREN + + if (operatorStack.length > 0 && + operatorStack[operatorStack.length-1].type == "SPATIAL") { + postfix.push(operatorStack.pop()); + } + case "COMMA": + case "END": + break; + default: + throw new Error("Unknown token type " + tok.type); + } + } + + while (operatorStack.length > 0) { + postfix.push(operatorStack.pop()); + } + + function buildTree() { + var tok = postfix.pop(); + switch (tok.type) { + case "LOGICAL": + var rhs = buildTree(), + lhs = buildTree(); + return new OpenLayers.Filter.Logical({ + filters: [lhs, rhs], + type: logicals[tok.text.toUpperCase()] + }); + case "NOT": + var operand = buildTree(); + return new OpenLayers.Filter.Logical({ + filters: [operand], + type: OpenLayers.Filter.Logical.NOT + }); + case "BETWEEN": + var min, max, property; + postfix.pop(); // unneeded AND token here + max = buildTree(); + min = buildTree(); + property = buildTree(); + return new OpenLayers.Filter.Comparison({ + property: property, + lowerBoundary: min, + upperBoundary: max, + type: OpenLayers.Filter.Comparison.BETWEEN + }); + case "COMPARISON": + var value = buildTree(), + property = buildTree(); + return new OpenLayers.Filter.Comparison({ + property: property, + value: value, + type: operators[tok.text.toUpperCase()] + }); + case "VALUE": + if ((/^'.*'$/).test(tok.text)) { + return tok.text.substr(1, tok.text.length - 2); + } else { + return Number(tok.text); + } + case "SPATIAL": + switch(tok.text.toUpperCase()) { + case "BBOX": + var maxy = buildTree(), + maxx = buildTree(), + miny = buildTree(), + minx = buildTree(), + prop = buildTree(); + + return new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.BBOX, + property: prop, + value: OpenLayers.Bounds.fromArray( + [minx, miny, maxx, maxy] + ) + }); + case "INTERSECTS": + var value = buildTree(), + property = buildTree(); + return new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.INTERSECTS, + property: property, + value: value + }); + case "WITHIN": + var value = buildTree(), + property = buildTree(); + return new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.WITHIN, + property: property, + value: value + }); + case "CONTAINS": + var value = buildTree(), + property = buildTree(); + return new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.CONTAINS, + property: property, + value: value + }); + case "DWITHIN": + var distance = buildTree(), + value = buildTree(), + property = buildTree(); + return new OpenLayers.Filter.Spatial({ + type: OpenLayers.Filter.Spatial.DWITHIN, + value: value, + property: property, + distance: Number(distance) + }); + } + case "GEOMETRY": + return OpenLayers.Geometry.fromWKT(tok.text); + default: + return tok.text; + } + } + + var result = buildTree(); + if (postfix.length > 0) { + var msg = "Remaining tokens after building AST: \n"; + for (var i = postfix.length - 1; i >= 0; i--) { + msg += postfix[i].type + ": " + postfix[i].text + "\n"; + } + throw new Error(msg); + } + + return result; + } + + return OpenLayers.Class(OpenLayers.Format, { + /** + * APIMethod: read + * Generate a filter from a CQL string. + + * Parameters: + * text - {String} The CQL text. + * + * Returns: + * {} A filter based on the CQL text. + */ + read: function(text) { + var result = buildAst(tokenize(text)); + if (this.keepData) { + this.data = result; + }; + return result; + }, + + /** + * APIMethod: write + * Convert a filter into a CQL string. + + * Parameters: + * filter - {} The filter. + * + * Returns: + * {String} A CQL string based on the filter. + */ + write: function(filter) { + if (filter instanceof OpenLayers.Geometry) { + return filter.toString(); + } + switch (filter.CLASS_NAME) { + case "OpenLayers.Filter.Spatial": + switch(filter.type) { + case OpenLayers.Filter.Spatial.BBOX: + return "BBOX(" + + filter.property + "," + + filter.value.toBBOX() + + ")"; + case OpenLayers.Filter.Spatial.DWITHIN: + return "DWITHIN(" + + filter.property + ", " + + this.write(filter.value) + ", " + + filter.distance + ")"; + case OpenLayers.Filter.Spatial.WITHIN: + return "WITHIN(" + + filter.property + ", " + + this.write(filter.value) + ")"; + case OpenLayers.Filter.Spatial.INTERSECTS: + return "INTERSECTS(" + + filter.property + ", " + + this.write(filter.value) + ")"; + case OpenLayers.Filter.Spatial.CONTAINS: + return "CONTAINS(" + + filter.property + ", " + + this.write(filter.value) + ")"; + default: + throw new Error("Unknown spatial filter type: " + filter.type); + } + case "OpenLayers.Filter.Logical": + if (filter.type == OpenLayers.Filter.Logical.NOT) { + // TODO: deal with precedence of logical operators to + // avoid extra parentheses (not urgent) + return "NOT (" + this.write(filter.filters[0]) + ")"; + } else { + var res = "("; + var first = true; + for (var i = 0; i < filter.filters.length; i++) { + if (first) { + first = false; + } else { + res += ") " + logicalReverse[filter.type] + " ("; + } + res += this.write(filter.filters[i]); + } + return res + ")"; + } + case "OpenLayers.Filter.Comparison": + if (filter.type == OpenLayers.Filter.Comparison.BETWEEN) { + return filter.property + " BETWEEN " + + this.write(filter.lowerBoundary) + " AND " + + this.write(filter.upperBoundary); + } else { + + return filter.property + + " " + operatorReverse[filter.type] + " " + + this.write(filter.value); + } + case undefined: + if (typeof filter === "string") { + return "'" + filter + "'"; + } else if (typeof filter === "number") { + return String(filter); + } + default: + throw new Error("Can't encode: " + filter.CLASS_NAME + " " + filter); + } + }, + + CLASS_NAME: "OpenLayers.Format.CQL" + + }); +})(); + diff --git a/tests/Format/CQL.html b/tests/Format/CQL.html new file mode 100644 index 0000000000..c813bfdfd7 --- /dev/null +++ b/tests/Format/CQL.html @@ -0,0 +1,287 @@ + + + + + + + + diff --git a/tests/list-tests.html b/tests/list-tests.html index a364f9ec7a..022b4bf43f 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -53,6 +53,7 @@
  • Format/Atom.html
  • Format/ArcXML.html
  • Format/ArcXML/Features.html
  • +
  • Format/CQL.html
  • Format/GeoJSON.html
  • Format/GeoRSS.html
  • Format/GML.html