tile38/controller/fence.go
Josh Baker 6c52f3f3f1 added command filter for geofences
It's now possible to mask the fence notifications based on the
command. For example, if we only want "set" and "del" commands.

NEARBY fleet FENCE COMMANDS set,del POINT 33 -115 10000

Suggested by @amorskoy, closes #99
2016-12-15 10:00:08 -07:00

255 lines
6.8 KiB
Go

package controller
import (
"math"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/controller/glob"
"github.com/tidwall/tile38/controller/server"
"github.com/tidwall/tile38/geojson"
)
var tmfmt = "2006-01-02T15:04:05.999999999Z07:00"
// FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) []string {
msgs := fenceMatch(hookName, sw, fence, details)
if len(fence.accept) == 0 {
return msgs
}
nmsgs := make([]string, 0, len(msgs))
for _, msg := range msgs {
if fence.accept[gjson.Get(msg, "command").String()] {
nmsgs = append(nmsgs, msg)
}
}
return nmsgs
}
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) []string {
jshookName := jsonString(hookName)
jstime := jsonString(details.timestamp.Format(tmfmt))
pattern := fence.glob
if details.command == "drop" {
return []string{`{"command":"drop","hook":` + jshookName + `,"time":` + jstime + `}`}
}
match := true
if pattern != "" && pattern != "*" {
match, _ = glob.Match(pattern, details.id)
}
if !match {
return nil
}
sw.mu.Lock()
nofields := sw.nofields
sw.mu.Unlock()
if details.obj == nil || !details.obj.IsGeometry() || (details.command == "fset" && nofields) {
return nil
}
match = false
var roamkeys, roamids []string
var roammeters []float64
detect := "outside"
if fence != nil {
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 {
// 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"
}
}
}
}
}
}
if details.command == "del" {
return []string{`{"command":"del","hook":` + jshookName + `,"id":` + jsonString(details.id) + `,"time":` + jstime + `}`}
}
if details.fmap == nil {
return nil
}
sw.mu.Lock()
sw.fmap = details.fmap
sw.fullFields = true
sw.msg.OutputType = server.JSON
sw.writeObject(details.id, details.obj, details.fields, true)
if sw.wr.Len() == 0 {
sw.mu.Unlock()
return nil
}
res := sw.wr.String()
resb := make([]byte, len(res))
copy(resb, res)
sw.wr.Reset()
res = string(resb)
if strings.HasPrefix(res, ",") {
res = res[1:]
}
if sw.output == outputIDs {
res = `{"id":` + res + `}`
}
sw.mu.Unlock()
if fence.groups == nil {
fence.groups = make(map[string]string)
}
groupkey := details.key + ":" + details.id
var group string
var ok bool
if detect == "enter" {
group = bsonID()
fence.groups[groupkey] = group
} else if detect == "cross" {
group = bsonID()
delete(fence.groups, groupkey)
} else {
group, ok = fence.groups[groupkey]
if !ok {
group = bsonID()
fence.groups[groupkey] = group
}
}
jskey := jsonString(details.key)
ores := res
msgs := make([]string, 0, 4)
if fence.detect == nil || fence.detect[detect] {
if strings.HasPrefix(ores, "{") {
res = `{"command":"` + details.command + `","group":"` + group + `","detect":"` + detect + `","hook":` + jshookName + `,"key":` + jskey + `,"time":` + jstime + `,` + ores[1:]
}
msgs = append(msgs, res)
}
switch detect {
case "enter":
if fence.detect == nil || fence.detect["inside"] {
msgs = append(msgs, `{"command":"`+details.command+`","group":"`+group+`","detect":"inside","hook":`+jshookName+`,"key":`+jskey+`,"time":`+jstime+`,`+ores[1:])
}
case "exit", "cross":
if fence.detect == nil || fence.detect["outside"] {
msgs = append(msgs, `{"command":"`+details.command+`","group":"`+group+`","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
}
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" {
if fence.o != nil {
return obj.Within(fence.o)
}
return obj.WithinBBox(geojson.BBox{
Min: geojson.Position{X: fence.minLon, Y: fence.minLat, Z: 0},
Max: geojson.Position{X: fence.maxLon, Y: fence.maxLat, Z: 0},
})
} else if fence.cmd == "intersects" {
if fence.o != nil {
return obj.Intersects(fence.o)
}
return obj.IntersectsBBox(geojson.BBox{
Min: geojson.Position{X: fence.minLon, Y: fence.minLat, Z: 0},
Max: geojson.Position{X: fence.maxLon, Y: fence.maxLat, Z: 0},
})
}
return false
}
func fenceMatchRoam(c *Controller, fence *liveFenceSwitches, tkey, tid string, obj geojson.Object) (keys, ids []string, meterss []float64) {
col := c.getCol(fence.roam.key)
if col == nil {
return
}
p := obj.CalculatedPoint()
col.Nearby(0, 0, p.Y, p.X, fence.roam.meters, math.Inf(-1), math.Inf(+1),
func(id string, obj geojson.Object, fields []float64) bool {
var match bool
if id == tid {
return true // skip self
}
if fence.roam.pattern {
match, _ = glob.Match(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
}