diff --git a/controller/fence.go b/controller/fence.go index a13a4d3b..d12ba5ec 100644 --- a/controller/fence.go +++ b/controller/fence.go @@ -1,6 +1,7 @@ package controller import ( + "strconv" "strings" "github.com/tidwall/tile38/controller/server" @@ -33,46 +34,63 @@ func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai return nil } match = false + + var roamkeys, roamids []string + var roammeters []float64 detect := "outside" if fence != nil { - match1 := fenceMatchObject(fence, details.oldObj) - match2 := fenceMatchObject(fence, details.obj) - if match1 && match2 { - match = true - detect = "inside" - } else if match1 && !match2 { - match = true - detect = "exit" - } else if !match1 && match2 { - match = true - detect = "enter" - if details.command == "fset" { - detect = "inside" + if fence.roam.on { + if details.command == "set" { + // println("roam", fence.roam.key, fence.roam.id, strconv.FormatFloat(fence.roam.meters, 'f', -1, 64)) + roamkeys, roamids, roammeters = fenceMatchRoam(sw.c, fence, details.key, details.id, details.obj) } + if len(roamids) == 0 || len(roamids) != len(roamkeys) { + return nil + } + match = true + detect = "roam" } else { - if details.command != "fset" { - // Maybe the old object and new object create a line that crosses the fence. - // Must detect for that possibility. - if details.oldObj != nil { - ls := geojson.LineString{ - Coordinates: []geojson.Position{ - details.oldObj.CalculatedPoint(), - details.obj.CalculatedPoint(), - }, - } - temp := false - if fence.cmd == "within" { - // because we are testing if the line croses the area we need to use - // "intersects" instead of "within". - fence.cmd = "intersects" - temp = true - } - if fenceMatchObject(fence, ls) { - //match = true - detect = "cross" - } - if temp { - fence.cmd = "within" + + // not using roaming + match1 := fenceMatchObject(fence, details.oldObj) + match2 := fenceMatchObject(fence, details.obj) + if match1 && match2 { + match = true + detect = "inside" + } else if match1 && !match2 { + match = true + detect = "exit" + } else if !match1 && match2 { + match = true + detect = "enter" + if details.command == "fset" { + detect = "inside" + } + } else { + if details.command != "fset" { + // Maybe the old object and new object create a line that crosses the fence. + // Must detect for that possibility. + if details.oldObj != nil { + ls := geojson.LineString{ + Coordinates: []geojson.Position{ + details.oldObj.CalculatedPoint(), + details.obj.CalculatedPoint(), + }, + } + temp := false + if fence.cmd == "within" { + // because we are testing if the line croses the area we need to use + // "intersects" instead of "within". + fence.cmd = "intersects" + temp = true + } + if fenceMatchObject(fence, ls) { + //match = true + detect = "cross" + } + if temp { + fence.cmd = "within" + } } } } @@ -93,21 +111,22 @@ func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai sw.mu.Unlock() return nil } + res := sw.wr.String() resb := make([]byte, len(res)) copy(resb, res) - res = string(resb) sw.wr.Reset() + res = string(resb) if sw.output == outputIDs { res = `{"id":` + res + `}` } sw.mu.Unlock() - if strings.HasPrefix(res, ",") { res = res[1:] } jskey := jsonString(details.key) + ores := res msgs := make([]string, 0, 2) if fence.detect == nil || fence.detect[detect] { @@ -125,6 +144,15 @@ func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, detai if fence.detect == nil || fence.detect["outside"] { msgs = append(msgs, `{"command":"`+details.command+`","detect":"outside","hook":`+jshookName+`,"key":`+jskey+`,"time":`+jstime+`,`+ores[1:]) } + case "roam": + if len(msgs) > 0 { + var nmsgs []string + msg := msgs[0][:len(msgs[0])-1] + for i, id := range roamids { + nmsgs = append(nmsgs, msg+`,"nearby":{"key":`+jsonString(roamkeys[i])+`,"id":`+jsonString(id)+`,"meters":`+strconv.FormatFloat(roammeters[i], 'f', -1, 64)+`}}`) + } + msgs = nmsgs + } } return msgs } @@ -133,6 +161,10 @@ func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { if obj == nil { return false } + if fence.roam.on { + // we need to check this object against + return false + } if fence.cmd == "nearby" { return obj.Nearby(geojson.Position{X: fence.lon, Y: fence.lat, Z: 0}, fence.meters) } else if fence.cmd == "within" { @@ -154,3 +186,33 @@ func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool { } return false } + +func fenceMatchRoam(c *Controller, fence *liveFenceSwitches, tkey, tid string, obj geojson.Object) (keys, ids []string, meterss []float64) { + c.mu.RLock() + defer c.mu.RUnlock() + col := c.getCol(fence.roam.key) + if col == nil { + return + } + p := obj.CalculatedPoint() + col.Nearby(0, 0, p.Y, p.X, fence.roam.meters, + func(id string, obj geojson.Object, fields []float64) bool { + var match bool + if id == tid { + return true // skip self + } + if fence.roam.pattern { + match, _ = globMatch(fence.roam.id, id) + } else { + match = fence.roam.id == id + } + if match { + keys = append(keys, fence.roam.key) + ids = append(ids, id) + meterss = append(meterss, obj.CalculatedPoint().DistanceTo(p)) + } + return true + }, + ) + return +} diff --git a/controller/scanner.go b/controller/scanner.go index db57c119..54686478 100644 --- a/controller/scanner.go +++ b/controller/scanner.go @@ -29,6 +29,7 @@ const ( type scanWriter struct { mu sync.Mutex + c *Controller wr *bytes.Buffer msg *server.Message col *collection.Collection @@ -67,6 +68,7 @@ func (c *Controller) newScanWriter( case outputIDs, outputObjects, outputCount, outputBounds, outputPoints, outputHashes: } sw := &scanWriter{ + c: c, wr: wr, msg: msg, output: output, diff --git a/controller/search.go b/controller/search.go index 9917e21b..2690e982 100644 --- a/controller/search.go +++ b/controller/search.go @@ -20,6 +20,15 @@ type liveFenceSwitches struct { minLat, minLon float64 maxLat, maxLon float64 cmd string + roam roamSwitches +} + +type roamSwitches struct { + on bool + key string + id string + pattern bool + meters float64 } func (s liveFenceSwitches) Error() string { @@ -46,18 +55,23 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string) } } } + ltyp := strings.ToLower(typ) var found bool for _, t := range types { - if strings.ToLower(typ) == t { + if ltyp == t { found = true break } } + if !found && s.searchScanBaseTokens.fence && ltyp == "roam" && cmd == "nearby" { + // allow roaming for nearby fence searches. + found = true + } if !found { err = errInvalidArgument(typ) return } - switch strings.ToLower(typ) { + switch ltyp { case "point": var slat, slon, smeters string if vs, slat, ok = tokenval(vs); !ok || slat == "" { @@ -206,6 +220,26 @@ func (c *Controller) cmdSearchArgs(cmd string, vs []resp.Value, types []string) } else { s.o = o } + case "roam": + s.roam.on = true + if vs, s.roam.key, ok = tokenval(vs); !ok || s.roam.key == "" { + err = errInvalidNumberOfArguments + return + } + if vs, s.roam.id, ok = tokenval(vs); !ok || s.roam.id == "" { + err = errInvalidNumberOfArguments + return + } + s.roam.pattern = globIsGlob(s.roam.id) + var smeters string + if vs, smeters, ok = tokenval(vs); !ok || smeters == "" { + err = errInvalidNumberOfArguments + return + } + if s.roam.meters, err = strconv.ParseFloat(smeters, 64); err != nil { + err = errInvalidArgument(smeters) + return + } } if len(vs) != 0 { err = errInvalidNumberOfArguments