tile38/controller/fence.go

319 lines
8.5 KiB
Go

package controller
import (
"fmt"
"math"
"strconv"
"time"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/controller/glob"
"github.com/tidwall/tile38/controller/server"
"github.com/tidwall/tile38/geojson"
)
// FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte {
overall := time.Now()
defer func() {
return
fmt.Printf(">> %v\n", time.Since(overall))
}()
msgs := fenceMatch(hookName, sw, fence, details)
if len(fence.accept) == 0 {
return msgs
}
nmsgs := make([][]byte, 0, len(msgs))
for _, msg := range msgs {
if fence.accept[gjson.GetBytes(msg, "command").String()] {
nmsgs = append(nmsgs, msg)
}
}
return nmsgs
}
func appendJSONTimeFormat(b []byte, t time.Time) []byte {
b = append(b, '"')
b = t.AppendFormat(b, "2006-01-02T15:04:05.999999999Z07:00")
b = append(b, '"')
return b
}
func jsonTimeFormat(t time.Time) string {
var b []byte
b = appendJSONTimeFormat(b, t)
return string(b)
}
func fenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, details *commandDetailsT) [][]byte {
if details.command == "drop" {
return [][]byte{[]byte(`{"command":"drop","hook":` + jsonString(hookName) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)}
}
if len(fence.glob) > 0 && !(len(fence.glob) == 1 && fence.glob[0] == '*') {
match, _ := glob.Match(fence.glob, details.id)
if !match {
return nil
}
}
if details.obj == nil || !details.obj.IsGeometry() {
return nil
}
if details.command == "fset" {
sw.mu.Lock()
nofields := sw.nofields
sw.mu.Unlock()
if nofields {
return nil
}
}
if details.command == "del" {
return [][]byte{[]byte(`{"command":"del","hook":` + jsonString(hookName) + `,"id":` + jsonString(details.id) + `,"time":` + jsonTimeFormat(details.timestamp) + `}`)}
}
var roamkeys, roamids []string
var roammeters []float64
var detect string = "outside"
if fence != nil {
if fence.roam.on {
if details.command == "set" {
roamkeys, roamids, roammeters = fenceMatchRoam(sw.c, fence, details.key, details.id, details.obj)
}
if len(roamids) == 0 || len(roamids) != len(roamkeys) {
return nil
}
detect = "roam"
} else {
// not using roaming
match1 := fenceMatchObject(fence, details.oldObj)
match2 := fenceMatchObject(fence, details.obj)
if match1 && match2 {
detect = "inside"
} else if match1 && !match2 {
detect = "exit"
} else if !match1 && match2 {
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) {
detect = "cross"
}
if temp {
fence.cmd = "within"
}
}
}
}
}
}
if details.fmap == nil {
return nil
}
if fence.detect != nil && !fence.detect[detect] {
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 := make([]byte, sw.wr.Len())
copy(res, sw.wr.Bytes())
sw.wr.Reset()
if len(res) > 0 && res[0] == ',' {
res = res[1:]
}
if sw.output == outputIDs {
res = []byte(`{"id":` + string(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
}
}
msgs := make([][]byte, 0, 4)
if fence.detect == nil || fence.detect[detect] {
if len(res) > 0 && res[0] == '{' {
res = makemsg(details.command, group, detect, hookName, details.key, details.timestamp, res[1:])
}
msgs = append(msgs, res)
}
switch detect {
case "enter":
if fence.detect == nil || fence.detect["inside"] {
msgs = append(msgs, makemsg(details.command, group, "inside", hookName, details.key, details.timestamp, res[1:]))
}
case "exit", "cross":
if fence.detect == nil || fence.detect["outside"] {
msgs = append(msgs, makemsg(details.command, group, "outside", hookName, details.key, details.timestamp, res[1:]))
}
case "roam":
if len(msgs) > 0 {
var nmsgs [][]byte
msg := msgs[0][:len(msgs[0])-1]
for i, id := range roamids {
nmsg := append([]byte(nil), msg...)
nmsg = append(nmsg, `,"nearby":{"key":`...)
nmsg = appendJSONString(nmsg, roamkeys[i])
nmsg = append(nmsg, `,"id":`...)
nmsg = appendJSONString(nmsg, id)
nmsg = append(nmsg, `,"meters":`...)
nmsg = append(nmsg, strconv.FormatFloat(roammeters[i], 'f', -1, 64)...)
if fence.roam.scan != "" {
nmsg = append(nmsg, `,"scan":[`...)
func() {
sw.c.mu.Lock()
defer sw.c.mu.Unlock()
col := sw.c.getCol(roamkeys[i])
if col != nil {
obj, _, ok := col.Get(id)
if ok {
nmsg = append(nmsg, `{"id":`+jsonString(id)+`,"self":true,"object":`+obj.JSON()+`}`...)
}
pattern := id + fence.roam.scan
iterator := func(oid string, o geojson.Object, fields []float64) bool {
if oid == id {
return true
}
if matched, _ := glob.Match(pattern, oid); matched {
nmsg = append(nmsg, `,{"id":`+jsonString(oid)+`,"object":`+o.JSON()+`}`...)
}
return true
}
g := glob.Parse(pattern, false)
if g.Limits[0] == "" && g.Limits[1] == "" {
col.Scan(0, false, iterator)
} else {
col.ScanRange(0, g.Limits[0], g.Limits[1], false, iterator)
}
}
}()
nmsg = append(nmsg, ']')
}
nmsg = append(nmsg, '}')
nmsg = append(nmsg, '}')
nmsgs = append(nmsgs, nmsg)
}
msgs = nmsgs
}
}
return msgs
}
func makemsg(command, group, detect, hookName string, key string, t time.Time, tail []byte) []byte {
var buf []byte
buf = append(append(buf, `{"command":"`...), command...)
buf = append(append(buf, `","group":"`...), group...)
buf = append(append(buf, `","detect":"`...), detect...)
buf = appendJSONString(append(buf, `","hook":`...), hookName)
buf = appendJSONString(append(buf, `,"key":`...), key)
buf = appendJSONTimeFormat(append(buf, `,"time":`...), t)
buf = append(append(buf, ','), tail...)
return buf
}
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)
}
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},
})
}
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
}