Add area expressions.
Add parser. Hook up to test command. Add tests for expressions in test command.
This commit is contained in:
parent
37d64f0466
commit
d0ca579f5c
192
internal/server/expression.go
Normal file
192
internal/server/expression.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"},
|
||||
})
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user