Skip to content

Commit a12ce3f

Browse files
authored
Merge pull request #47 from CiscoM31/geo
Improve geospacial query support
2 parents 3230bb1 + c493bc1 commit a12ce3f

File tree

5 files changed

+169
-74
lines changed

5 files changed

+169
-74
lines changed

expression_parser.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ const (
5858
ExpressionTokenDuration // duration = [ "duration" ] SQUOTE durationValue SQUOTE
5959
ExpressionTokenGuid // [25] A 128-bit GUID
6060
ExpressionTokenAssignement // The '=' assignement for function arguments.
61-
ExpressionTokenGeographyPolygon //
62-
ExpressionTokenGeometryPolygon //
61+
ExpressionTokenGeographyPolygon // A polygon with geodetic (ie spherical) coordinates. Parsed Token.Value is '<long> <lat>,<long> <lat>...'
62+
ExpressionTokenGeometryPolygon // A polygon with planar (ie cartesian) coordinates. Parsed Token.Value is '<long> <lat>,<long> <lat>...'
63+
ExpressionTokenGeographyPoint // A geodetic coordinate point. Parsed Token.Value is '<long> <lat>'
6364
expressionTokenLast
6465
)
6566

@@ -94,6 +95,7 @@ func (e ExpressionTokenType) String() string {
9495
"ExpressionTokenAssignement",
9596
"ExpressionTokenGeographyPolygon",
9697
"ExpressionTokenGeometryPolygon",
98+
"ExpressionTokenGeographyPoint",
9799
"expressionTokenLast",
98100
}[e]
99101
}
@@ -178,15 +180,11 @@ func NewExpressionTokenizer() *Tokenizer {
178180
// E.g. ABNF for 'geo.distance':
179181
// distanceMethodCallExpr = "geo.distance" OPEN BWS commonExpr BWS COMMA BWS commonExpr BWS CLOSE
180182
t.Add("(?i)^(?P<token>(geo.distance|geo.intersects|geo.length))[\\s(]", ExpressionTokenFunc)
181-
// geographyPolygon = geographyPrefix SQUOTE fullPolygonLiteral SQUOTE
182-
// fullPolygonLiteral = sridLiteral polygonLiteral
183-
// sridLiteral = "SRID" EQ 1*5DIGIT SEMI
184-
// polygonLiteral = "Polygon" polygonData
185-
// polygonData = OPEN ringLiteral *( COMMA ringLiteral ) CLOSE
186-
// Example: geography'SRID=0;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581))'
187-
t.Add(`^geography'SRID=[0-9]{1,5};Polygon\(\((-?[0-9]+\.[0-9]+\s+-?[0-9]+\.[0-9]+)(,\s-?[0-9]+\.[0-9]+\s+-?[0-9]+\.[0-9]+)*\)\)'`, ExpressionTokenGeographyPolygon)
188-
// geometryPolygon = geometryPrefix SQUOTE fullPolygonLiteral SQUOTE
189-
t.Add(`^geometry'SRID=[0-9]{1,5};Polygon\(\((-?[0-9]+\.[0-9]+\s+-?[0-9]+\.[0-9]+)(,\s-?[0-9]+\.[0-9]+\s+-?[0-9]+\.[0-9]+)*\)\)'`, ExpressionTokenGeometryPolygon)
183+
// Example: geography'POLYGON((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581))'
184+
t.Add(`(?i)^geography'(?:SRID=(\d{1,5});)?POLYGON\s*\(\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?(?:\s*,\s*-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)*?)\s*\)\)'`, ExpressionTokenGeographyPolygon)
185+
t.Add(`(?i)^geometry'(?:SRID=(\d{1,5});)?POLYGON\s*\(\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?(?:\s*,\s*-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)*?)\s*\)\)'`, ExpressionTokenGeometryPolygon)
186+
// Example: geography'POINT(-122.131577 47.678581)'
187+
t.Add(`(?i)^geography'POINT\s*\(\s*(?P<subtoken>-?\d+(\.\d+)?\s+-?\d+(\.\d+)?)\s*\)'`, ExpressionTokenGeographyPoint)
190188
// According to ODATA ABNF notation, functions must be followed by a open parenthesis with no space
191189
// between the function name and the open parenthesis.
192190
// However, we are leniently allowing space characters between the function and the open parenthesis.
@@ -315,7 +313,7 @@ func NewExpressionParser() *ExpressionParser {
315313
// Edm.Boolean geo.intersects(Edm.GeometryPoint,Edm.GeometryPolygon)
316314
// The geo.intersects function returns true if the specified point lies within the interior
317315
// or on the boundary of the specified polygon, otherwise it returns false.
318-
parser.DefineFunction("geo.intersects", []int{2}, false)
316+
parser.DefineFunction("geo.intersects", []int{2}, true)
319317
// The geo.length function has the following signatures:
320318
// Edm.Double geo.length(Edm.GeographyLineString)
321319
// Edm.Double geo.length(Edm.GeometryLineString)
@@ -329,7 +327,7 @@ func NewExpressionParser() *ExpressionParser {
329327
parser.DefineFunction("all", []int{2}, true)
330328
// Define 'case' as a function accepting 1-10 arguments. Each argument is a pair of expressions separated by a colon.
331329
// See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_case
332-
parser.DefineFunction("case", []int{1,2,3,4,5,6,7,8,9,10}, true)
330+
parser.DefineFunction("case", []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, true)
333331

334332
return parser
335333
}

expression_parser_fixture_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ var testCases = []struct {
174174
},
175175
{
176176
// matches documents where any of the geo coordinates in the locations field is within the given polygon.
177-
expression: "locations/any(loc: geo.intersects(loc, geography'SRID=0;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))'))",
177+
expression: "locations/any(loc: geo.intersects(loc, geography'Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))'))",
178178
infixTokens: []*Token{
179179
{Value: "locations", Type: ExpressionTokenLiteral},
180180
{Value: "/", Type: ExpressionTokenLambdaNav},
@@ -186,7 +186,7 @@ var testCases = []struct {
186186
{Value: "(", Type: ExpressionTokenOpenParen},
187187
{Value: "loc", Type: ExpressionTokenLiteral},
188188
{Value: ",", Type: ExpressionTokenComma},
189-
{Value: "geography'SRID=0;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))'", Type: ExpressionTokenGeographyPolygon},
189+
{Value: "-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581", Type: ExpressionTokenGeographyPolygon},
190190
{Value: ")", Type: ExpressionTokenCloseParen},
191191
{Value: ")", Type: ExpressionTokenCloseParen},
192192
},
@@ -198,7 +198,7 @@ var testCases = []struct {
198198
{Value: "loc", Depth: 2, Type: ExpressionTokenLiteral},
199199
{Value: "geo.intersects", Depth: 2, Type: ExpressionTokenFunc},
200200
{Value: "loc", Depth: 3, Type: ExpressionTokenLiteral},
201-
{Value: "geography'SRID=0;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))'", Depth: 3, Type: ExpressionTokenGeographyPolygon},
201+
{Value: "-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581", Depth: 3, Type: ExpressionTokenGeographyPolygon},
202202
},
203203
},
204204
{
@@ -209,11 +209,11 @@ var testCases = []struct {
209209
// polygonLiteral = "Polygon" polygonData
210210
// polygonData = OPEN ringLiteral *( COMMA ringLiteral ) CLOSE
211211
// positionLiteral = doubleValue SP doubleValue ; longitude, then latitude
212-
expression: "geo.intersects(location, geometry'SRID=123;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))')",
212+
expression: "geo.intersects(location, geometry'Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))')",
213213
tree: []expectedParseNode{
214214
{Value: "geo.intersects", Depth: 0, Type: ExpressionTokenFunc},
215215
{Value: "location", Depth: 1, Type: ExpressionTokenLiteral},
216-
{Value: "geometry'SRID=123;Polygon((-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581))'", Depth: 1, Type: ExpressionTokenGeometryPolygon},
216+
{Value: "-122.031577 47.578581, -122.031577 47.678581, -122.131577 47.678581, -122.031577 47.578581", Depth: 1, Type: ExpressionTokenGeometryPolygon},
217217
},
218218
},
219219
{

expression_parser_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,6 @@ func TestInvalidBooleanExpressionSyntax(t *testing.T) {
241241
// Geo functions
242242
"geo.distance(CurrentPosition,TargetPosition)",
243243
"geo.length(DirectRoute)",
244-
"geo.intersects(Position,TargetArea)",
245-
"GEO.INTERSECTS(Position,TargetArea)", // functions are case insensitive in ODATA 4.0.1
246244
"now()",
247245
"tolower(Name)",
248246
"concat(First,Last)",

filter_parser.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ func ParseFilterString(ctx context.Context, filter string) (*GoDataFilterQuery,
2121
if err != nil {
2222
return nil, err
2323
}
24-
if tree == nil || tree.Token == nil ||
25-
(len(tree.Children) == 0 && tree.Token.Type != ExpressionTokenBoolean) {
24+
if tree == nil || tree.Token == nil || !GlobalFilterParser.isBooleanExpression(tree.Token) {
2625
return nil, BadRequestError("Value must be a boolean expression")
2726
}
2827
return &GoDataFilterQuery{tree, filter}, nil

0 commit comments

Comments
 (0)