Merge pull request #464 from rshura/area-expression

Add area expressions.
This commit is contained in:
Josh Baker 2019-10-28 13:45:10 -07:00 committed by GitHub
commit df477bf3f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 430 additions and 6 deletions

View File

@ -0,0 +1,186 @@
package server
import (
"strings"
"github.com/tidwall/geojson"
)
type BinaryOp byte
const (
NOOP BinaryOp = iota
AND
OR
tokenAND = "and"
tokenOR = "or"
tokenNOT = "not"
tokenLParen = "("
tokenRParen = ")"
)
// areaExpression is (maybe negated) either an spatial object or operator + children (other expressions).
type areaExpression struct {
negate bool
obj geojson.Object
op BinaryOp
children children
}
type children []*areaExpression
// String representation, helpful in logging.
func (e *areaExpression) String() (res string) {
if e.obj != nil {
res = e.obj.String()
} else {
var chStrings []string
for _, c := range e.children {
chStrings = append(chStrings, c.String())
}
switch e.op {
case NOOP:
res = "empty operator"
case AND:
res = "(" + strings.Join(chStrings, " "+tokenAND+" ") + ")"
case OR:
res = "(" + strings.Join(chStrings, " "+tokenOR+" ") + ")"
default:
res = "unknown operator"
}
}
if e.negate {
res = tokenNOT + " " + res
}
return
}
// Return boolean value modulo negate field of the expression.
func (e *areaExpression) maybeNegate(val bool) bool {
if e.negate {
return !val
}
return val
}
// Methods for testing an areaExpression against the spatial object.
func (e *areaExpression) testObject(
o geojson.Object,
objObjTest func(o1, o2 geojson.Object) bool,
exprObjTest func(ae *areaExpression, ob geojson.Object) bool,
) bool {
if e.obj != nil {
return objObjTest(e.obj, o)
}
switch e.op {
case AND:
for _, c := range e.children {
if !exprObjTest(c, o) {
return false
}
}
return true
case OR:
for _, c := range e.children {
if exprObjTest(c, o) {
return true
}
}
return false
}
return false
}
func (e *areaExpression) rawIntersects(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Intersects, (*areaExpression).Intersects)
}
func (e *areaExpression) rawContains(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Contains, (*areaExpression).Contains)
}
func (e *areaExpression) rawWithin(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Within, (*areaExpression).Within)
}
func (e *areaExpression) Intersects(o geojson.Object) bool {
return e.maybeNegate(e.rawIntersects(o))
}
func (e *areaExpression) Contains(o geojson.Object) bool {
return e.maybeNegate(e.rawContains(o))
}
func (e *areaExpression) Within(o geojson.Object) bool {
return e.maybeNegate(e.rawWithin(o))
}
// Methods for testing an areaExpression against another areaExpression.
func (e *areaExpression) testExpression(
other *areaExpression,
exprObjTest func(ae *areaExpression, ob geojson.Object) bool,
rawExprExprTest func(ae1, ae2 *areaExpression) bool,
exprExprTest func(ae1, ae2 *areaExpression) bool,
) bool {
if other.negate {
oppositeExp := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children}
nonNegateOther := &areaExpression{obj: other.obj, op: other.op, children: other.children}
return exprExprTest(oppositeExp, nonNegateOther)
}
if other.obj != nil {
return exprObjTest(e, other.obj)
}
switch other.op {
case AND:
for _, c := range other.children {
if !rawExprExprTest(e, c) {
return false
}
}
return true
case OR:
for _, c := range other.children {
if rawExprExprTest(e, c) {
return true
}
}
return false
}
return false
}
func (e *areaExpression) rawIntersectsExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawIntersects,
(*areaExpression).rawIntersectsExpr,
(*areaExpression).IntersectsExpr)
}
func (e *areaExpression) rawWithinExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawWithin,
(*areaExpression).rawWithinExpr,
(*areaExpression).WithinExpr)
}
func (e *areaExpression) rawContainsExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawContains,
(*areaExpression).rawContainsExpr,
(*areaExpression).ContainsExpr)
}
func (e *areaExpression) IntersectsExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawIntersectsExpr(other))
}
func (e *areaExpression) WithinExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawWithinExpr(other))
}
func (e *areaExpression) ContainsExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawContainsExpr(other))
}

View File

@ -231,8 +231,9 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
var ok bool
var test string
var obj1, obj2, clipped geojson.Object
if vs, obj1, err = s.parseArea(vs, false); err != nil {
var clipped geojson.Object
var area1, area2 *areaExpression
if vs, area1, err = s.parseAreaExpression(vs, false); err != nil {
return
}
if vs, test, ok = tokenval(vs); !ok || test == "" {
@ -259,7 +260,11 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
doClip = true
}
}
if vs, obj2, err = s.parseArea(vs, doClip); err != nil {
if vs, area2, err = s.parseAreaExpression(vs, doClip); err != nil {
return
}
if doClip && (area1.obj == nil || area2.obj == nil) {
err = errInvalidArgument("clip")
return
}
if len(vs) != 0 {
@ -268,14 +273,14 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
var result int
if lTest == "within" {
if obj1.Within(obj2) {
if area1.WithinExpr(area2) {
result = 1
}
} else if lTest == "intersects" {
if obj1.Intersects(obj2) {
if area1.IntersectsExpr(area2) {
result = 1
if doClip {
clipped = clip.Clip(obj1, obj2)
clipped = clip.Clip(area1.obj, area2.obj)
}
}
}

View File

@ -688,3 +688,140 @@ func (c *Server) parseSearchScanBaseTokens(
tout = t
return
}
type parentStack []*areaExpression
func (ps *parentStack) isEmpty() bool {
return len(*ps) == 0
}
func (ps *parentStack) push(e *areaExpression) {
*ps = append(*ps, e)
}
func (ps *parentStack) pop() (e *areaExpression, empty bool) {
n := len(*ps)
if n == 0 {
return nil, true
}
x := (*ps)[n-1]
*ps = (*ps)[:n-1]
return x, false
}
func (s *Server) parseAreaExpression(vsin []string, doClip bool) (vsout []string, ae *areaExpression, err error) {
ps := &parentStack{}
vsout = vsin[:]
var negate, needObj bool
loop:
for {
nvs, wtok, ok := tokenval(vsout)
if !ok || len(wtok) == 0 {
break
}
switch strings.ToLower(wtok) {
case tokenLParen:
newExpr := &areaExpression{negate: negate, op: NOOP}
negate = false
needObj = false
if ae != nil {
ae.children = append(ae.children, newExpr)
}
ae = newExpr
ps.push(ae)
vsout = nvs
case tokenRParen:
if needObj {
err = errInvalidArgument(tokenRParen)
return
}
if parent, empty := ps.pop(); empty {
err = errInvalidArgument(tokenRParen)
return
} else {
ae = parent
}
vsout = nvs
case tokenNOT:
negate = !negate
needObj = true
vsout = nvs
case tokenAND:
if needObj {
err = errInvalidArgument(tokenAND)
return
}
needObj = true
if ae == nil {
err = errInvalidArgument(tokenAND)
return
} else if ae.obj == nil {
switch ae.op {
case OR:
numChildren := len(ae.children)
if numChildren < 2 {
err = errInvalidNumberOfArguments
return
} else {
ae.children = append(
ae.children[:numChildren-1],
&areaExpression{
op: AND,
children: []*areaExpression{ae.children[numChildren-1]}})
}
case NOOP:
ae.op = AND
}
} else {
ae = &areaExpression{op: AND, children: []*areaExpression{ae}}
}
vsout = nvs
case tokenOR:
if needObj {
err = errInvalidArgument(tokenOR)
return
}
needObj = true
if ae == nil {
err = errInvalidArgument(tokenOR)
return
} else if ae.obj == nil {
switch ae.op {
case AND:
if len(ae.children) < 2 {
err = errInvalidNumberOfArguments
return
} else {
ae = &areaExpression{op: OR, children: []*areaExpression{ae}}
}
case NOOP:
ae.op = OR
}
} else {
ae = &areaExpression{op: OR, children: []*areaExpression{ae}}
}
vsout = nvs
case "point", "circle", "object", "bounds", "hash", "quadkey", "tile", "get":
if parsedVs, parsedObj, areaErr := s.parseArea(vsout, doClip); areaErr != nil {
err = areaErr
return
} else {
newExpr := &areaExpression{negate: negate, obj: parsedObj, op: NOOP}
negate = false
needObj = false
if ae == nil {
ae = newExpr
} else {
ae.children = append(ae.children, newExpr)
}
vsout = parsedVs
}
default:
break loop
}
}
if !ps.isEmpty() || needObj || ae == nil || (ae.obj == nil && len(ae.children) == 0) {
err = errInvalidNumberOfArguments
}
return
}

View File

@ -8,6 +8,8 @@ func subTestTestCmd(t *testing.T, mc *mockServer) {
runStep(t, mc, "WITHIN", testcmd_WITHIN_test)
runStep(t, mc, "INTERSECTS", testcmd_INTERSECTS_test)
runStep(t, mc, "INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test)
runStep(t, mc, "ExpressionErrors", testcmd_expressionErrors_test)
runStep(t, mc, "Expressions", testcmd_expression_test)
}
func testcmd_WITHIN_test(mc *mockServer) error {
@ -115,3 +117,97 @@ func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error {
{"TEST", "OBJECT", poly101, "INTERSECTS", "CLIP", "BOUNDS", 37.73315644825698, -122.44054287672043, 37.73349585185455, -122.44008690118788}, {"0"},
})
}
func testcmd_expressionErrors_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "foo", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"},
{"SET", "mykey", "bar", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"},
{"SET", "mykey", "baz", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "(", "GET", "mykey", "bar"}, {
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", ")"}, {
"ERR invalid argument ')'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "OR", "GET", "mykey", "bar"}, {
"ERR invalid argument 'or'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "AND", "GET", "mykey", "bar"}, {
"ERR invalid argument 'and'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "AND", "GET", "mykey", "baz"}, {
"ERR invalid argument 'and'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "OR", "GET", "mykey", "baz"}, {
"ERR invalid argument 'or'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "OR", "GET", "mykey", "baz"}, {
"ERR invalid argument 'or'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "AND", "GET", "mykey", "baz"}, {
"ERR invalid argument 'and'"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR"}, {
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND"}, {
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT"}, {
"ERR wrong number of arguments for 'test' command"},
{"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT", "AND", "GET", "mykey", "baz"}, {
"ERR invalid argument 'and'"},
})
}
func testcmd_expression_test(mc *mockServer) error {
poly := `{
"type": "Polygon",
"coordinates": [
[
[-122.44126439094543,37.732906137107],
[-122.43980526924135,37.732906137107],
[-122.43980526924135,37.73421283683962],
[-122.44126439094543,37.73421283683962],
[-122.44126439094543,37.732906137107]
]
]
}`
poly8 := `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]],[[-122.44060993194579,37.73345766902749],[-122.44044363498686,37.73345766902749],[-122.44044363498686,37.73355524732416],[-122.44060993194579,37.73355524732416],[-122.44060993194579,37.73345766902749]],[[-122.44060724973677,37.7336888869566],[-122.4402102828026,37.7336888869566],[-122.4402102828026,37.7339752567853],[-122.44060724973677,37.7339752567853],[-122.44060724973677,37.7336888869566]]]}`
poly9 := `{"type": "Polygon","coordinates": [[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}`
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"},
{"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "OBJECT", poly}, {"0"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "NOT", "OBJECT", poly}, {"0"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"0"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3"}, {"1"},
{"TEST", "NOT", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3",
"OR", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly,
"OR", "GET", "mykey", "line3"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR",
"(", "OBJECT", poly8, "AND", "OBJECT", poly, ")"}, {"1"},
{"TEST", "OBJECT", poly9, "INTERSECTS",
"(", "GET", "mykey", "line3", "OR", "OBJECT", poly8, ")", "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "line3"}, {"0"},
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"},
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND",
"(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"},
{"TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3"}, {"1"},
})
}