Adding a parser for reading and writing CQL. This can be used to create filters to be used in rules when styling or it can be used to serialize filters when making requests to services that supprot CQL. p=dwinslow, r=me (closes #2522)
git-svn-id: http://svn.openlayers.org/trunk/openlayers@11065 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
438
lib/OpenLayers/Format/CQL.js
Normal file
438
lib/OpenLayers/Format/CQL.js
Normal file
@@ -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 <OpenLayers.Filter> objects. Write
|
||||
* <OpenLayers.Filter> objects to get CQL strings. Create a new parser with
|
||||
* the <OpenLayers.Format.CQL> constructor.
|
||||
*
|
||||
* Inherits from:
|
||||
* - <OpenLayers.Format>
|
||||
*/
|
||||
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<len; i++) {
|
||||
token = tokens[i];
|
||||
var pat = patterns[token];
|
||||
var matches = tryToken(text, pat);
|
||||
if (matches) {
|
||||
var match = matches[0];
|
||||
var remainder = text.substr(match.length).replace(/^\s*/, "");
|
||||
return {
|
||||
type: token,
|
||||
text: match,
|
||||
remainder: remainder
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var msg = "ERROR: In parsing: [" + text + "], expected one of: ";
|
||||
for (i=0; i<len; i++) {
|
||||
token = tokens[i];
|
||||
msg += "\n " + token + ": " + patterns[token];
|
||||
}
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
function tokenize(text) {
|
||||
var results = [];
|
||||
var token, expect = ["NOT", "GEOMETRY", "SPATIAL", "PROPERTY", "LPAREN"];
|
||||
|
||||
do {
|
||||
token = nextToken(text, expect);
|
||||
text = token.remainder;
|
||||
expect = follows[token.type];
|
||||
if (token.type != "END" && !expect) {
|
||||
throw new Error("No follows list for " + token.type);
|
||||
}
|
||||
results.push(token);
|
||||
} while (token.type != "END");
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function buildAst(tokens) {
|
||||
var operatorStack = [],
|
||||
postfix = [];
|
||||
|
||||
while (tokens.length) {
|
||||
var tok = tokens.shift();
|
||||
switch (tok.type) {
|
||||
case "PROPERTY":
|
||||
case "GEOMETRY":
|
||||
case "VALUE":
|
||||
postfix.push(tok);
|
||||
break;
|
||||
case "COMPARISON":
|
||||
case "BETWEEN":
|
||||
case "LOGICAL":
|
||||
var p = precedence[tok.type];
|
||||
|
||||
while (operatorStack.length > 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:
|
||||
* {<OpenLayers.Filter>} 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 - {<OpenLayers.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"
|
||||
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user