diff --git a/internal/server/expression.go b/internal/server/expression.go new file mode 100644 index 00000000..0fd87471 --- /dev/null +++ b/internal/server/expression.go @@ -0,0 +1,192 @@ +package server + +import ( + "strings" + + "github.com/tidwall/geojson" +) + +type BinaryOp byte + +const ( + NOOP BinaryOp = iota + AND + OR +) + +// areaExpression is either an object or operator+children +type areaExpression struct { + negate bool + obj geojson.Object + op BinaryOp + children children +} + +type children []*areaExpression + +func (e *areaExpression) String() string { + if e.obj != nil { + return e.obj.String() + } + var chStrings []string + for _, c := range e.children { + chStrings = append(chStrings, c.String()) + } + switch e.op { + case NOOP: + return "empty operator" + case AND: + return "(" + strings.Join(chStrings, " AND ") + ")" + case OR: + return "(" + strings.Join(chStrings, " OR ") + ")" + } + return "unknown operator" +} + +// Return boolean value modulo negate field of the expression. +func (e *areaExpression) booleanize(val bool) bool { + if e.negate { + return !val + } + return val +} + +func (e *areaExpression) Intersects(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Intersects(o)) + } + switch e.op { + case AND: + for _, c := range e.children { + if !c.Intersects(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c := range e.children { + if c.Intersects(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +// object within an expression means anything of this expression contains object +func (e *areaExpression) Contains(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Contains(o)) + } + switch e.op { + case AND: + for _, c:= range e.children { + if !c.Contains(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range e.children { + if c.Contains(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) Within(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Within(o)) + } + switch e.op { + case AND: + for _, c:= range e.children { + if !c.Within(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range e.children { + if c.Within(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) IntersectsExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Intersects(oe.obj)) + } + switch oe.op { + case AND: + for _, c := range oe.children { + if !e.IntersectsExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c := range oe.children { + if e.IntersectsExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) + +} + +func (e *areaExpression) WithinExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Within(oe.obj)) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.WithinExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range oe.children { + if e.WithinExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) ContainsExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Contains(oe.obj)) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.ContainsExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range oe.children { + if e.ContainsExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} diff --git a/internal/server/test.go b/internal/server/test.go index 267f3c27..e276367c 100644 --- a/internal/server/test.go +++ b/internal/server/test.go @@ -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) } } } diff --git a/internal/server/token.go b/internal/server/token.go index 71a81044..5adcdc56 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -688,3 +688,146 @@ 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 bool +loop: + for { + nvs, wtok, ok := tokenval(vsout) + if !ok || len(wtok) == 0 { + break + } + switch strings.ToLower(wtok) { + case "(": + newExpr := &areaExpression{negate: negate, op: NOOP} + negate = false + if ae != nil { + ps.push(ae) + ae.children = append(ae.children, newExpr) + } + ae = newExpr + vsout = nvs + case ")": + if negate { + err = errInvalidArgument("NOT") + return + } + if parent, empty := ps.pop(); empty { + err = errInvalidArgument(")") + return + } else { + ae = parent + } + vsout = nvs + case "not": + negate = true + vsout = nvs + case "and": + if negate { + err = errInvalidArgument("NOT") + return + } + if ae == nil { + err = errInvalidArgument("AND") + 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 "or": + if negate { + err = errInvalidArgument("NOT") + return + } + if ae == nil { + err = errInvalidArgument("OR") + return + } else if ae.obj == nil { + switch ae.op { + case AND: + if len(ae.children) < 2 { + err = errInvalidNumberOfArguments + return + } else { + parent, empty := ps.pop() + if empty { + parent = ae + } + parent.children = append( + parent.children, + &areaExpression{op: OR}) + ps.push(parent) + } + 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 + if ae == nil { + ae = newExpr + } else { + ae.children = append(ae.children, newExpr) + } + vsout = parsedVs + } + default: + if negate { + err = errInvalidArgument("NOT") + return + } + break loop + } + } + if prevExpr, empty := ps.pop(); !empty { + ae = prevExpr + } + return +} diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index d6d65231..2b67d3ef 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -8,6 +8,7 @@ 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, "Expressions", testcmd_expression_test) } func testcmd_WITHIN_test(mc *mockServer) error { @@ -115,3 +116,51 @@ 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_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", "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", "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"}, + }) + +}