mirror of https://github.com/tidwall/tile38.git
Immutable Object type
This commit is contained in:
parent
ba9a767988
commit
2c643996e7
2
go.mod
2
go.mod
|
@ -27,7 +27,7 @@ require (
|
|||
github.com/tidwall/redbench v0.1.0
|
||||
github.com/tidwall/redcon v1.4.4
|
||||
github.com/tidwall/resp v0.1.1
|
||||
github.com/tidwall/rtree v1.9.1
|
||||
github.com/tidwall/rtree v1.9.2
|
||||
github.com/tidwall/sjson v1.2.4
|
||||
github.com/xdg/scram v1.0.5
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da
|
||||
|
|
4
go.sum
4
go.sum
|
@ -383,8 +383,8 @@ github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYg
|
|||
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
|
||||
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
|
||||
github.com/tidwall/rtree v1.3.1/go.mod h1:S+JSsqPTI8LfWA4xHBo5eXzie8WJLVFeppAutSegl6M=
|
||||
github.com/tidwall/rtree v1.9.1 h1:UIPtvE09nLKZRnMNEwRZxu9jRAkzROAZDR+NPS/9IRs=
|
||||
github.com/tidwall/rtree v1.9.1/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
|
||||
github.com/tidwall/rtree v1.9.2 h1:6HiSU/bf4a7l2smEC+fEum/WloHMFCIQKWHjahm0Do8=
|
||||
github.com/tidwall/rtree v1.9.2/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
|
||||
github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
|
||||
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
|
||||
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
|
||||
"github.com/tidwall/btree"
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geo"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/rtree"
|
||||
"github.com/tidwall/tile38/internal/deadline"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
// yieldStep forces the iterator to yield goroutine every 256 steps.
|
||||
|
@ -21,20 +21,13 @@ type Cursor interface {
|
|||
Step(count uint64)
|
||||
}
|
||||
|
||||
type itemT struct {
|
||||
id string
|
||||
obj geojson.Object
|
||||
expires int64 // unix nano expiration
|
||||
fields field.List
|
||||
func byID(a, b *object.Object) bool {
|
||||
return a.ID() < b.ID()
|
||||
}
|
||||
|
||||
func byID(a, b *itemT) bool {
|
||||
return a.id < b.id
|
||||
}
|
||||
|
||||
func byValue(a, b *itemT) bool {
|
||||
value1 := a.obj.String()
|
||||
value2 := b.obj.String()
|
||||
func byValue(a, b *object.Object) bool {
|
||||
value1 := a.String()
|
||||
value2 := b.String()
|
||||
if value1 < value2 {
|
||||
return true
|
||||
}
|
||||
|
@ -45,30 +38,23 @@ func byValue(a, b *itemT) bool {
|
|||
return byID(a, b)
|
||||
}
|
||||
|
||||
func byExpires(a, b *itemT) bool {
|
||||
if a.expires < b.expires {
|
||||
func byExpires(a, b *object.Object) bool {
|
||||
if a.Expires() < b.Expires() {
|
||||
return true
|
||||
}
|
||||
if a.expires > b.expires {
|
||||
if a.Expires() > b.Expires() {
|
||||
return false
|
||||
}
|
||||
// the values match so we'll compare IDs, which are always unique.
|
||||
return byID(a, b)
|
||||
}
|
||||
|
||||
func (item *itemT) Rect() geometry.Rect {
|
||||
if item.obj != nil {
|
||||
return item.obj.Rect()
|
||||
}
|
||||
return geometry.Rect{}
|
||||
}
|
||||
|
||||
// Collection represents a collection of geojson objects.
|
||||
type Collection struct {
|
||||
items *btree.BTreeG[*itemT] // items sorted by id
|
||||
spatial *rtree.RTreeGN[float32, *itemT] // items geospatially indexed
|
||||
values *btree.BTreeG[*itemT] // items sorted by value+id
|
||||
expires *btree.BTreeG[*itemT] // items sorted by ex+id
|
||||
items *btree.BTreeG[*object.Object] // sorted by id
|
||||
spatial *rtree.RTreeGN[float32, *object.Object] // geospatially indexed
|
||||
values *btree.BTreeG[*object.Object] // sorted by value+id
|
||||
expires *btree.BTreeG[*object.Object] // sorted by ex+id
|
||||
weight int
|
||||
points int
|
||||
objects int // geometry count
|
||||
|
@ -83,7 +69,7 @@ func New() *Collection {
|
|||
items: btree.NewBTreeGOptions(byID, optsNoLock),
|
||||
values: btree.NewBTreeGOptions(byValue, optsNoLock),
|
||||
expires: btree.NewBTreeGOptions(byExpires, optsNoLock),
|
||||
spatial: &rtree.RTreeGN[float32, *itemT]{},
|
||||
spatial: &rtree.RTreeGN[float32, *object.Object]{},
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
@ -121,31 +107,14 @@ func (c *Collection) Bounds() (minX, minY, maxX, maxY float64) {
|
|||
right.Rect().Max.X, top.Rect().Max.Y
|
||||
}
|
||||
|
||||
func objIsSpatial(obj geojson.Object) bool {
|
||||
_, ok := obj.(geojson.Spatial)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Collection) objWeight(item *itemT) int {
|
||||
var weight int
|
||||
weight += len(item.id)
|
||||
if objIsSpatial(item.obj) {
|
||||
weight += item.obj.NumPoints() * 16
|
||||
} else {
|
||||
weight += len(item.obj.String())
|
||||
}
|
||||
weight += item.fields.Weight()
|
||||
return weight
|
||||
}
|
||||
|
||||
func (c *Collection) indexDelete(item *itemT) {
|
||||
if !item.obj.Empty() {
|
||||
func (c *Collection) indexDelete(item *object.Object) {
|
||||
if !item.Geo().Empty() {
|
||||
c.spatial.Delete(rtreeItem(item))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collection) indexInsert(item *itemT) {
|
||||
if !item.obj.Empty() {
|
||||
func (c *Collection) indexInsert(item *object.Object) {
|
||||
if !item.Geo().Empty() {
|
||||
c.spatial.Insert(rtreeItem(item))
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +145,7 @@ func rtreeValueUp(d float64) float32 {
|
|||
return f
|
||||
}
|
||||
|
||||
func rtreeItem(item *itemT) (min, max [2]float32, data *itemT) {
|
||||
func rtreeItem(item *object.Object) (min, max [2]float32, data *object.Object) {
|
||||
min, max = rtreeRect(item.Rect())
|
||||
return min, max, item
|
||||
}
|
||||
|
@ -193,105 +162,68 @@ func rtreeRect(rect geometry.Rect) (min, max [2]float32) {
|
|||
|
||||
// Set adds or replaces an object in the collection and returns the fields
|
||||
// array.
|
||||
func (c *Collection) Set(id string, obj geojson.Object, fields field.List, ex int64) (
|
||||
oldObject geojson.Object, oldFields, newFields field.List,
|
||||
) {
|
||||
newItem := &itemT{
|
||||
id: id,
|
||||
obj: obj,
|
||||
expires: ex,
|
||||
fields: fields,
|
||||
}
|
||||
|
||||
// add the new item to main btree and remove the old one if needed
|
||||
oldItem, ok := c.items.Set(newItem)
|
||||
if ok {
|
||||
// the old item was removed, now let's remove it from the rtree/btree.
|
||||
if objIsSpatial(oldItem.obj) {
|
||||
c.indexDelete(oldItem)
|
||||
func (c *Collection) Set(obj *object.Object) (prev *object.Object) {
|
||||
prev, _ = c.items.Set(obj)
|
||||
if prev != nil {
|
||||
if prev.IsSpatial() {
|
||||
c.indexDelete(prev)
|
||||
c.objects--
|
||||
} else {
|
||||
c.values.Delete(oldItem)
|
||||
c.values.Delete(prev)
|
||||
c.nobjects--
|
||||
}
|
||||
// delete old item from the expires queue
|
||||
if oldItem.expires != 0 {
|
||||
c.expires.Delete(oldItem)
|
||||
if prev.Expires() != 0 {
|
||||
c.expires.Delete(prev)
|
||||
}
|
||||
|
||||
// decrement the point count
|
||||
c.points -= oldItem.obj.NumPoints()
|
||||
|
||||
// decrement the weights
|
||||
c.weight -= c.objWeight(oldItem)
|
||||
c.points -= prev.Geo().NumPoints()
|
||||
c.weight -= prev.Weight()
|
||||
}
|
||||
|
||||
// insert the new item into the rtree or strings tree.
|
||||
if objIsSpatial(newItem.obj) {
|
||||
c.indexInsert(newItem)
|
||||
if obj.IsSpatial() {
|
||||
c.indexInsert(obj)
|
||||
c.objects++
|
||||
} else {
|
||||
c.values.Set(newItem)
|
||||
c.values.Set(obj)
|
||||
c.nobjects++
|
||||
}
|
||||
// insert item into expires queue.
|
||||
if newItem.expires != 0 {
|
||||
c.expires.Set(newItem)
|
||||
if obj.Expires() != 0 {
|
||||
c.expires.Set(obj)
|
||||
}
|
||||
|
||||
// increment the point count
|
||||
c.points += newItem.obj.NumPoints()
|
||||
|
||||
// add the new weights
|
||||
c.weight += c.objWeight(newItem)
|
||||
|
||||
if oldItem != nil {
|
||||
return oldItem.obj, oldItem.fields, newItem.fields
|
||||
}
|
||||
return nil, field.List{}, newItem.fields
|
||||
c.points += obj.Geo().NumPoints()
|
||||
c.weight += obj.Weight()
|
||||
return prev
|
||||
}
|
||||
|
||||
// Delete removes an object and returns it.
|
||||
// If the object does not exist then the 'ok' return value will be false.
|
||||
func (c *Collection) Delete(id string) (
|
||||
obj geojson.Object, fields field.List, ok bool,
|
||||
) {
|
||||
oldItem, ok := c.items.Delete(&itemT{id: id})
|
||||
if !ok {
|
||||
return nil, field.List{}, false
|
||||
func (c *Collection) Delete(id string) (prev *object.Object) {
|
||||
key := object.New(id, nil, 0, 0, field.List{})
|
||||
prev, _ = c.items.Delete(key)
|
||||
if prev == nil {
|
||||
return nil
|
||||
}
|
||||
if objIsSpatial(oldItem.obj) {
|
||||
if !oldItem.obj.Empty() {
|
||||
c.indexDelete(oldItem)
|
||||
if prev.IsSpatial() {
|
||||
if !prev.Geo().Empty() {
|
||||
c.indexDelete(prev)
|
||||
}
|
||||
c.objects--
|
||||
} else {
|
||||
c.values.Delete(oldItem)
|
||||
c.values.Delete(prev)
|
||||
c.nobjects--
|
||||
}
|
||||
// delete old item from expires queue
|
||||
if oldItem.expires != 0 {
|
||||
c.expires.Delete(oldItem)
|
||||
if prev.Expires() != 0 {
|
||||
c.expires.Delete(prev)
|
||||
}
|
||||
c.weight -= c.objWeight(oldItem)
|
||||
c.points -= oldItem.obj.NumPoints()
|
||||
|
||||
return oldItem.obj, oldItem.fields, true
|
||||
c.points -= prev.Geo().NumPoints()
|
||||
c.weight -= prev.Weight()
|
||||
return prev
|
||||
}
|
||||
|
||||
// Get returns an object.
|
||||
// If the object does not exist then the 'ok' return value will be false.
|
||||
func (c *Collection) Get(id string) (
|
||||
obj geojson.Object,
|
||||
fields field.List,
|
||||
ex int64,
|
||||
ok bool,
|
||||
) {
|
||||
item, ok := c.items.Get(&itemT{id: id})
|
||||
if !ok {
|
||||
return nil, field.List{}, 0, false
|
||||
}
|
||||
return item.obj, item.fields, item.expires, true
|
||||
func (c *Collection) Get(id string) *object.Object {
|
||||
key := object.New(id, nil, 0, 0, field.List{})
|
||||
obj, _ := c.items.Get(key)
|
||||
return obj
|
||||
}
|
||||
|
||||
// Scan iterates though the collection ids.
|
||||
|
@ -299,7 +231,7 @@ func (c *Collection) Scan(
|
|||
desc bool,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iterator func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iterator func(obj *object.Object) bool,
|
||||
) bool {
|
||||
var keepon = true
|
||||
var count uint64
|
||||
|
@ -308,13 +240,13 @@ func (c *Collection) Scan(
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
iter := func(item *itemT) bool {
|
||||
iter := func(obj *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
keepon = iterator(item.id, item.obj, item.fields)
|
||||
keepon = iterator(obj)
|
||||
return keepon
|
||||
}
|
||||
if desc {
|
||||
|
@ -331,7 +263,7 @@ func (c *Collection) ScanRange(
|
|||
desc bool,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iterator func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iterator func(o *object.Object) bool,
|
||||
) bool {
|
||||
var keepon = true
|
||||
var count uint64
|
||||
|
@ -340,29 +272,30 @@ func (c *Collection) ScanRange(
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
iter := func(item *itemT) bool {
|
||||
iter := func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if !desc {
|
||||
if item.id >= end {
|
||||
if o.ID() >= end {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if item.id <= end {
|
||||
if o.ID() <= end {
|
||||
return false
|
||||
}
|
||||
}
|
||||
keepon = iterator(item.id, item.obj, item.fields)
|
||||
keepon = iterator(o)
|
||||
return keepon
|
||||
}
|
||||
|
||||
pstart := object.New(start, nil, 0, 0, field.List{})
|
||||
if desc {
|
||||
c.items.Descend(&itemT{id: start}, iter)
|
||||
c.items.Descend(pstart, iter)
|
||||
} else {
|
||||
c.items.Ascend(&itemT{id: start}, iter)
|
||||
c.items.Ascend(pstart, iter)
|
||||
}
|
||||
return keepon
|
||||
}
|
||||
|
@ -372,7 +305,7 @@ func (c *Collection) SearchValues(
|
|||
desc bool,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iterator func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iterator func(o *object.Object) bool,
|
||||
) bool {
|
||||
var keepon = true
|
||||
var count uint64
|
||||
|
@ -381,13 +314,13 @@ func (c *Collection) SearchValues(
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
iter := func(item *itemT) bool {
|
||||
iter := func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
keepon = iterator(item.id, item.obj, item.fields)
|
||||
keepon = iterator(o)
|
||||
return keepon
|
||||
}
|
||||
if desc {
|
||||
|
@ -402,7 +335,7 @@ func (c *Collection) SearchValues(
|
|||
func (c *Collection) SearchValuesRange(start, end string, desc bool,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iterator func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iterator func(o *object.Object) bool,
|
||||
) bool {
|
||||
var keepon = true
|
||||
var count uint64
|
||||
|
@ -411,38 +344,39 @@ func (c *Collection) SearchValuesRange(start, end string, desc bool,
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
iter := func(item *itemT) bool {
|
||||
iter := func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
keepon = iterator(item.id, item.obj, item.fields)
|
||||
keepon = iterator(o)
|
||||
return keepon
|
||||
}
|
||||
pstart := &itemT{obj: String(start)}
|
||||
pend := &itemT{obj: String(end)}
|
||||
|
||||
pstart := object.New("", String(start), 0, 0, field.List{})
|
||||
pend := object.New("", String(end), 0, 0, field.List{})
|
||||
if desc {
|
||||
// descend range
|
||||
c.values.Descend(pstart, func(item *itemT) bool {
|
||||
c.values.Descend(pstart, func(item *object.Object) bool {
|
||||
return bGT(c.values, item, pend) && iter(item)
|
||||
})
|
||||
} else {
|
||||
c.values.Ascend(pstart, func(item *itemT) bool {
|
||||
c.values.Ascend(pstart, func(item *object.Object) bool {
|
||||
return bLT(c.values, item, pend) && iter(item)
|
||||
})
|
||||
}
|
||||
return keepon
|
||||
}
|
||||
|
||||
func bLT(tr *btree.BTreeG[*itemT], a, b *itemT) bool { return tr.Less(a, b) }
|
||||
func bGT(tr *btree.BTreeG[*itemT], a, b *itemT) bool { return tr.Less(b, a) }
|
||||
func bLT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(a, b) }
|
||||
func bGT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(b, a) }
|
||||
|
||||
// ScanGreaterOrEqual iterates though the collection starting with specified id.
|
||||
func (c *Collection) ScanGreaterOrEqual(id string, desc bool,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iterator func(id string, obj geojson.Object, fields field.List, ex int64) bool,
|
||||
iterator func(o *object.Object) bool,
|
||||
) bool {
|
||||
var keepon = true
|
||||
var count uint64
|
||||
|
@ -451,33 +385,34 @@ func (c *Collection) ScanGreaterOrEqual(id string, desc bool,
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
iter := func(item *itemT) bool {
|
||||
iter := func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
keepon = iterator(item.id, item.obj, item.fields, item.expires)
|
||||
keepon = iterator(o)
|
||||
return keepon
|
||||
}
|
||||
pstart := object.New(id, nil, 0, 0, field.List{})
|
||||
if desc {
|
||||
c.items.Descend(&itemT{id: id}, iter)
|
||||
c.items.Descend(pstart, iter)
|
||||
} else {
|
||||
c.items.Ascend(&itemT{id: id}, iter)
|
||||
c.items.Ascend(pstart, iter)
|
||||
}
|
||||
return keepon
|
||||
}
|
||||
|
||||
func (c *Collection) geoSearch(
|
||||
rect geometry.Rect,
|
||||
iter func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iter func(o *object.Object) bool,
|
||||
) bool {
|
||||
alive := true
|
||||
min, max := rtreeRect(rect)
|
||||
c.spatial.Search(
|
||||
min, max,
|
||||
func(_, _ [2]float32, item *itemT) bool {
|
||||
alive = iter(item.id, item.obj, item.fields)
|
||||
func(_, _ [2]float32, o *object.Object) bool {
|
||||
alive = iter(o)
|
||||
return alive
|
||||
},
|
||||
)
|
||||
|
@ -486,29 +421,25 @@ func (c *Collection) geoSearch(
|
|||
|
||||
func (c *Collection) geoSparse(
|
||||
obj geojson.Object, sparse uint8,
|
||||
iter func(id string, obj geojson.Object, fields field.List) (match, ok bool),
|
||||
iter func(o *object.Object) (match, ok bool),
|
||||
) bool {
|
||||
matches := make(map[string]bool)
|
||||
alive := true
|
||||
c.geoSparseInner(obj.Rect(), sparse,
|
||||
func(id string, o geojson.Object, fields field.List) (
|
||||
match, ok bool,
|
||||
) {
|
||||
ok = true
|
||||
if !matches[id] {
|
||||
match, ok = iter(id, o, fields)
|
||||
if match {
|
||||
matches[id] = true
|
||||
}
|
||||
c.geoSparseInner(obj.Rect(), sparse, func(o *object.Object) (match, ok bool) {
|
||||
ok = true
|
||||
if !matches[o.ID()] {
|
||||
match, ok = iter(o)
|
||||
if match {
|
||||
matches[o.ID()] = true
|
||||
}
|
||||
return match, ok
|
||||
},
|
||||
)
|
||||
}
|
||||
return match, ok
|
||||
})
|
||||
return alive
|
||||
}
|
||||
func (c *Collection) geoSparseInner(
|
||||
rect geometry.Rect, sparse uint8,
|
||||
iter func(id string, obj geojson.Object, fields field.List) (match, ok bool),
|
||||
iter func(o *object.Object) (match, ok bool),
|
||||
) bool {
|
||||
if sparse > 0 {
|
||||
w := rect.Max.X - rect.Min.X
|
||||
|
@ -539,16 +470,14 @@ func (c *Collection) geoSparseInner(
|
|||
return true
|
||||
}
|
||||
alive := true
|
||||
c.geoSearch(rect,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
match, ok := iter(id, obj, fields)
|
||||
if !ok {
|
||||
alive = false
|
||||
return false
|
||||
}
|
||||
return !match
|
||||
},
|
||||
)
|
||||
c.geoSearch(rect, func(o *object.Object) bool {
|
||||
match, ok := iter(o)
|
||||
if !ok {
|
||||
alive = false
|
||||
return false
|
||||
}
|
||||
return !match
|
||||
})
|
||||
return alive
|
||||
}
|
||||
|
||||
|
@ -559,7 +488,7 @@ func (c *Collection) Within(
|
|||
sparse uint8,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iter func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iter func(o *object.Object) bool,
|
||||
) bool {
|
||||
var count uint64
|
||||
var offset uint64
|
||||
|
@ -568,45 +497,39 @@ func (c *Collection) Within(
|
|||
cursor.Step(offset)
|
||||
}
|
||||
if sparse > 0 {
|
||||
return c.geoSparse(obj, sparse,
|
||||
func(id string, o geojson.Object, fields field.List) (
|
||||
match, ok bool,
|
||||
) {
|
||||
count++
|
||||
if count <= offset {
|
||||
return false, true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if match = o.Within(obj); match {
|
||||
ok = iter(id, o, fields)
|
||||
}
|
||||
return match, ok
|
||||
},
|
||||
)
|
||||
}
|
||||
return c.geoSearch(obj.Rect(),
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
return c.geoSparse(obj, sparse, func(o *object.Object) (match, ok bool) {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
return false, true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if o.Within(obj) {
|
||||
return iter(id, o, fields)
|
||||
if match = o.Geo().Within(obj); match {
|
||||
ok = iter(o)
|
||||
}
|
||||
return match, ok
|
||||
})
|
||||
}
|
||||
return c.geoSearch(obj.Rect(), func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
},
|
||||
)
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if o.Geo().Within(obj) {
|
||||
return iter(o)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Intersects returns all object that are intersect an object or bounding box.
|
||||
// Set obj to nil in order to use the bounding box.
|
||||
func (c *Collection) Intersects(
|
||||
obj geojson.Object,
|
||||
gobj geojson.Object,
|
||||
sparse uint8,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iter func(id string, obj geojson.Object, fields field.List) bool,
|
||||
iter func(o *object.Object) bool,
|
||||
) bool {
|
||||
var count uint64
|
||||
var offset uint64
|
||||
|
@ -615,34 +538,29 @@ func (c *Collection) Intersects(
|
|||
cursor.Step(offset)
|
||||
}
|
||||
if sparse > 0 {
|
||||
return c.geoSparse(obj, sparse,
|
||||
func(id string, o geojson.Object, fields field.List) (
|
||||
match, ok bool,
|
||||
) {
|
||||
count++
|
||||
if count <= offset {
|
||||
return false, true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if match = o.Intersects(obj); match {
|
||||
ok = iter(id, o, fields)
|
||||
}
|
||||
return match, ok
|
||||
},
|
||||
)
|
||||
}
|
||||
return c.geoSearch(obj.Rect(),
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
return c.geoSparse(gobj, sparse, func(o *object.Object) (match, ok bool) {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
return false, true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if o.Intersects(obj) {
|
||||
return iter(id, o, fields)
|
||||
if match = o.Geo().Intersects(gobj); match {
|
||||
ok = iter(o)
|
||||
}
|
||||
return match, ok
|
||||
})
|
||||
}
|
||||
return c.geoSearch(gobj.Rect(), func(o *object.Object) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
},
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
if o.Geo().Intersects(gobj) {
|
||||
return iter(o)
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -651,41 +569,8 @@ func (c *Collection) Nearby(
|
|||
target geojson.Object,
|
||||
cursor Cursor,
|
||||
deadline *deadline.Deadline,
|
||||
iter func(id string, obj geojson.Object, fields field.List, dist float64) bool,
|
||||
iter func(o *object.Object, dist float64) bool,
|
||||
) bool {
|
||||
// First look to see if there's at least one candidate in the circle's
|
||||
// outer rectangle. This is a fast-fail operation.
|
||||
if circle, ok := target.(*geojson.Circle); ok {
|
||||
meters := circle.Meters()
|
||||
if meters > 0 {
|
||||
center := circle.Center()
|
||||
minLat, minLon, maxLat, maxLon :=
|
||||
geo.RectFromCenter(center.Y, center.X, meters)
|
||||
var exists bool
|
||||
min, max := rtreeRect(geometry.Rect{
|
||||
Min: geometry.Point{
|
||||
X: minLon,
|
||||
Y: minLat,
|
||||
},
|
||||
Max: geometry.Point{
|
||||
X: maxLon,
|
||||
Y: maxLat,
|
||||
},
|
||||
})
|
||||
c.spatial.Search(
|
||||
min, max,
|
||||
func(_, _ [2]float32, item *itemT) bool {
|
||||
exists = true
|
||||
return false
|
||||
},
|
||||
)
|
||||
if !exists {
|
||||
// no candidates
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// do the kNN operation
|
||||
alive := true
|
||||
center := target.Center()
|
||||
var count uint64
|
||||
|
@ -694,22 +579,22 @@ func (c *Collection) Nearby(
|
|||
offset = cursor.Offset()
|
||||
cursor.Step(offset)
|
||||
}
|
||||
distFn := geodeticDistAlgo[*itemT]([2]float64{center.X, center.Y})
|
||||
distFn := geodeticDistAlgo([2]float64{center.X, center.Y})
|
||||
c.spatial.Nearby(
|
||||
func(min, max [2]float32, data *itemT, item bool) float32 {
|
||||
return float32(distFn(
|
||||
func(min, max [2]float32, data *object.Object, item bool) float64 {
|
||||
return distFn(
|
||||
[2]float64{float64(min[0]), float64(min[1])},
|
||||
[2]float64{float64(max[0]), float64(max[1])},
|
||||
data, item,
|
||||
))
|
||||
)
|
||||
},
|
||||
func(_, _ [2]float32, item *itemT, dist float32) bool {
|
||||
func(_, _ [2]float32, o *object.Object, dist float64) bool {
|
||||
count++
|
||||
if count <= offset {
|
||||
return true
|
||||
}
|
||||
nextStep(count, cursor, deadline)
|
||||
alive = iter(item.id, item.obj, item.fields, float64(dist))
|
||||
alive = iter(o, dist)
|
||||
return alive
|
||||
},
|
||||
)
|
||||
|
@ -727,8 +612,6 @@ func nextStep(step uint64, cursor Cursor, deadline *deadline.Deadline) {
|
|||
}
|
||||
|
||||
// ScanExpires returns a list of all objects that have expired.
|
||||
func (c *Collection) ScanExpires(iter func(id string, expires int64) bool) {
|
||||
c.expires.Scan(func(item *itemT) bool {
|
||||
return iter(item.id, item.expires)
|
||||
})
|
||||
func (c *Collection) ScanExpires(iter func(o *object.Object) bool) {
|
||||
c.expires.Scan(iter)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
func PO(x, y float64) *geojson.Point {
|
||||
|
@ -47,14 +48,14 @@ func TestCollectionNewCollection(t *testing.T) {
|
|||
id := strconv.FormatInt(int64(i), 10)
|
||||
obj := PO(rand.Float64()*360-180, rand.Float64()*180-90)
|
||||
objs[id] = obj
|
||||
c.Set(id, obj, field.List{}, 0)
|
||||
c.Set(object.New(id, obj, 0, 0, field.List{}))
|
||||
}
|
||||
count := 0
|
||||
bbox := geometry.Rect{
|
||||
Min: geometry.Point{X: -180, Y: -90},
|
||||
Max: geometry.Point{X: 180, Y: 90},
|
||||
}
|
||||
c.geoSearch(bbox, func(id string, obj geojson.Object, _ field.List) bool {
|
||||
c.geoSearch(bbox, func(o *object.Object) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
|
@ -80,44 +81,32 @@ func TestCollectionSet(t *testing.T) {
|
|||
t.Run("AddString", func(t *testing.T) {
|
||||
c := New()
|
||||
str1 := String("hello")
|
||||
oldObject, oldFields, newFields := c.Set("str", str1, field.List{}, 0)
|
||||
expect(t, oldObject == nil)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
old := c.Set(object.New("str", str1, 0, 0, field.List{}))
|
||||
expect(t, old == nil)
|
||||
})
|
||||
t.Run("UpdateString", func(t *testing.T) {
|
||||
c := New()
|
||||
str1 := String("hello")
|
||||
str2 := String("world")
|
||||
oldObject, oldFields, newFields := c.Set("str", str1, field.List{}, 0)
|
||||
expect(t, oldObject == nil)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
oldObject, oldFields, newFields = c.Set("str", str2, field.List{}, 0)
|
||||
expect(t, oldObject == str1)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
old := c.Set(object.New("str", str1, 0, 0, field.List{}))
|
||||
expect(t, old == nil)
|
||||
old = c.Set(object.New("str", str2, 0, 0, field.List{}))
|
||||
expect(t, old.Geo() == str1)
|
||||
})
|
||||
t.Run("AddPoint", func(t *testing.T) {
|
||||
c := New()
|
||||
point1 := PO(-112.1, 33.1)
|
||||
oldObject, oldFields, newFields := c.Set("point", point1, field.List{}, 0)
|
||||
expect(t, oldObject == nil)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
old := c.Set(object.New("point", point1, 0, 0, field.List{}))
|
||||
expect(t, old == nil)
|
||||
})
|
||||
t.Run("UpdatePoint", func(t *testing.T) {
|
||||
c := New()
|
||||
point1 := PO(-112.1, 33.1)
|
||||
point2 := PO(-112.2, 33.2)
|
||||
oldObject, oldFields, newFields := c.Set("point", point1, field.List{}, 0)
|
||||
expect(t, oldObject == nil)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
oldObject, oldFields, newFields = c.Set("point", point2, field.List{}, 0)
|
||||
expect(t, oldObject == point1)
|
||||
expect(t, oldFields.Len() == 0)
|
||||
expect(t, newFields.Len() == 0)
|
||||
old := c.Set(object.New("point", point1, 0, 0, field.List{}))
|
||||
expect(t, old == nil)
|
||||
old = c.Set(object.New("point", point2, 0, 0, field.List{}))
|
||||
expect(t, old.Geo().Center() == point1.Base())
|
||||
})
|
||||
t.Run("Fields", func(t *testing.T) {
|
||||
c := New()
|
||||
|
@ -126,11 +115,8 @@ func TestCollectionSet(t *testing.T) {
|
|||
fNames := []string{"a", "b", "c"}
|
||||
fValues := []string{"1", "2", "3"}
|
||||
fields1 := toFields(fNames, fValues)
|
||||
oldObj, oldFlds, newFlds := c.Set("str", str1, fields1, 0)
|
||||
|
||||
expect(t, oldObj == nil)
|
||||
expect(t, oldFlds.Len() == 0)
|
||||
expect(t, reflect.DeepEqual(newFlds, fields1))
|
||||
old := c.Set(object.New("str", str1, 0, 0, fields1))
|
||||
expect(t, old == nil)
|
||||
|
||||
str2 := String("hello")
|
||||
|
||||
|
@ -138,25 +124,23 @@ func TestCollectionSet(t *testing.T) {
|
|||
fValues = []string{"4", "5", "6"}
|
||||
fields2 := toFields(fNames, fValues)
|
||||
|
||||
oldObj, oldFlds, newFlds = c.Set("str", str2, fields2, 0)
|
||||
expect(t, oldObj == str1)
|
||||
expect(t, reflect.DeepEqual(oldFlds, fields1))
|
||||
expect(t, reflect.DeepEqual(newFlds, fields2))
|
||||
old = c.Set(object.New("str", str2, 0, 0, fields2))
|
||||
expect(t, old.Geo() == str1)
|
||||
expect(t, reflect.DeepEqual(old.Fields(), fields1))
|
||||
|
||||
fNames = []string{"a", "b", "c", "d", "e", "f"}
|
||||
fValues = []string{"7", "8", "9", "10", "11", "12"}
|
||||
fields3 := toFields(fNames, fValues)
|
||||
oldObj, oldFlds, newFlds = c.Set("str", str1, fields3, 0)
|
||||
expect(t, oldObj == str2)
|
||||
expect(t, reflect.DeepEqual(oldFlds, fields2))
|
||||
expect(t, reflect.DeepEqual(newFlds, fields3))
|
||||
old = c.Set(object.New("str", str1, 0, 0, fields3))
|
||||
expect(t, old.Geo() == str2)
|
||||
expect(t, reflect.DeepEqual(old.Fields(), fields2))
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
c.Set("1", String("1"), field.List{}, 0)
|
||||
c.Set("2", String("2"), field.List{}, 0)
|
||||
c.Set("3", PO(1, 2), field.List{}, 0)
|
||||
c.Set(object.New("1", String("1"), 0, 0, field.List{}))
|
||||
c.Set(object.New("2", String("2"), 0, 0, field.List{}))
|
||||
c.Set(object.New("3", PO(1, 2), 0, 0, field.List{}))
|
||||
|
||||
expect(t, c.Count() == 3)
|
||||
expect(t, c.StringCount() == 2)
|
||||
|
@ -164,78 +148,30 @@ func TestCollectionSet(t *testing.T) {
|
|||
expect(t, bounds(c) == geometry.Rect{
|
||||
Min: geometry.Point{X: 1, Y: 2},
|
||||
Max: geometry.Point{X: 1, Y: 2}})
|
||||
var v geojson.Object
|
||||
var ok bool
|
||||
// var flds []float64
|
||||
// var updated bool
|
||||
// var updateCount int
|
||||
var prev *object.Object
|
||||
|
||||
v, _, ok = c.Delete("2")
|
||||
expect(t, v.String() == "2")
|
||||
expect(t, ok)
|
||||
prev = c.Delete("2")
|
||||
expect(t, prev.Geo().String() == "2")
|
||||
expect(t, c.Count() == 2)
|
||||
expect(t, c.StringCount() == 1)
|
||||
expect(t, c.PointCount() == 1)
|
||||
|
||||
v, _, ok = c.Delete("1")
|
||||
expect(t, v.String() == "1")
|
||||
expect(t, ok)
|
||||
prev = c.Delete("1")
|
||||
expect(t, prev.Geo().String() == "1")
|
||||
expect(t, c.Count() == 1)
|
||||
expect(t, c.StringCount() == 0)
|
||||
expect(t, c.PointCount() == 1)
|
||||
|
||||
// expect(t, len(c.FieldMap()) == 0)
|
||||
|
||||
// _, flds, updated, ok = c.SetField("3", "hello", 123)
|
||||
// expect(t, ok)
|
||||
// expect(t, reflect.DeepEqual(flds, []float64{123}))
|
||||
// expect(t, updated)
|
||||
// expect(t, c.FieldMap()["hello"] == 0)
|
||||
|
||||
// _, flds, updated, ok = c.SetField("3", "hello", 1234)
|
||||
// expect(t, ok)
|
||||
// expect(t, reflect.DeepEqual(flds, []float64{1234}))
|
||||
// expect(t, updated)
|
||||
|
||||
// _, flds, updated, ok = c.SetField("3", "hello", 1234)
|
||||
// expect(t, ok)
|
||||
// expect(t, reflect.DeepEqual(flds, []float64{1234}))
|
||||
// expect(t, !updated)
|
||||
|
||||
// _, flds, updateCount, ok = c.SetFields("3",
|
||||
// []string{"planet", "world"}, []float64{55, 66})
|
||||
// expect(t, ok)
|
||||
// expect(t, reflect.DeepEqual(flds, []float64{1234, 55, 66}))
|
||||
// expect(t, updateCount == 2)
|
||||
// expect(t, c.FieldMap()["hello"] == 0)
|
||||
// expect(t, c.FieldMap()["planet"] == 1)
|
||||
// expect(t, c.FieldMap()["world"] == 2)
|
||||
|
||||
v, _, ok = c.Delete("3")
|
||||
expect(t, v.String() == `{"type":"Point","coordinates":[1,2]}`)
|
||||
expect(t, ok)
|
||||
prev = c.Delete("3")
|
||||
expect(t, prev.Geo().String() == `{"type":"Point","coordinates":[1,2]}`)
|
||||
expect(t, c.Count() == 0)
|
||||
expect(t, c.StringCount() == 0)
|
||||
expect(t, c.PointCount() == 0)
|
||||
v, _, ok = c.Delete("3")
|
||||
expect(t, v == nil)
|
||||
expect(t, !ok)
|
||||
prev = c.Delete("3")
|
||||
expect(t, prev == nil)
|
||||
expect(t, c.Count() == 0)
|
||||
expect(t, bounds(c) == geometry.Rect{})
|
||||
v, _, _, ok = c.Get("3")
|
||||
expect(t, v == nil)
|
||||
expect(t, !ok)
|
||||
// _, _, _, ok = c.SetField("3", "hello", 123)
|
||||
// expect(t, !ok)
|
||||
// _, _, _, ok = c.SetFields("3", []string{"hello"}, []float64{123})
|
||||
// expect(t, !ok)
|
||||
// expect(t, c.TotalWeight() == 0)
|
||||
// expect(t, c.FieldMap()["hello"] == 0)
|
||||
// expect(t, c.FieldMap()["planet"] == 1)
|
||||
// expect(t, c.FieldMap()["world"] == 2)
|
||||
// expect(t, reflect.DeepEqual(
|
||||
// c.FieldArr(), []string{"hello", "planet", "world"}),
|
||||
// )
|
||||
expect(t, c.Get("3") == nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -260,82 +196,82 @@ func TestCollectionScan(t *testing.T) {
|
|||
c := New()
|
||||
for _, i := range rand.Perm(N) {
|
||||
id := fmt.Sprintf("%04d", i)
|
||||
c.Set(id, String(id), makeFields(
|
||||
c.Set(object.New(id, String(id), 0, 0, makeFields(
|
||||
field.Make("ex", id),
|
||||
), 0)
|
||||
)))
|
||||
}
|
||||
var n int
|
||||
var prevID string
|
||||
c.Scan(false, nil, nil, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
c.Scan(false, nil, nil, func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id > prevID)
|
||||
expect(t, o.ID() > prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == c.Count())
|
||||
n = 0
|
||||
c.Scan(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
c.Scan(true, nil, nil, func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id < prevID)
|
||||
expect(t, o.ID() < prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == c.Count())
|
||||
|
||||
n = 0
|
||||
c.ScanRange("0060", "0070", false, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id > prevID)
|
||||
expect(t, o.ID() > prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == 10)
|
||||
|
||||
n = 0
|
||||
c.ScanRange("0070", "0060", true, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id < prevID)
|
||||
expect(t, o.ID() < prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == 10)
|
||||
|
||||
n = 0
|
||||
c.ScanGreaterOrEqual("0070", true, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List, ex int64) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id < prevID)
|
||||
expect(t, o.ID() < prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == 71)
|
||||
|
||||
n = 0
|
||||
c.ScanGreaterOrEqual("0070", false, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List, ex int64) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, id > prevID)
|
||||
expect(t, o.ID() > prevID)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 0))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
|
||||
n++
|
||||
prevID = id
|
||||
prevID = o.ID()
|
||||
return true
|
||||
})
|
||||
expect(t, n == c.Count()-70)
|
||||
|
@ -356,58 +292,59 @@ func TestCollectionSearch(t *testing.T) {
|
|||
for i, j := range rand.Perm(N) {
|
||||
id := fmt.Sprintf("%04d", j)
|
||||
ex := fmt.Sprintf("%04d", i)
|
||||
c.Set(id, String(ex),
|
||||
c.Set(object.New(id, String(ex),
|
||||
0, 0,
|
||||
makeFields(
|
||||
field.Make("i", ex),
|
||||
field.Make("j", id),
|
||||
), 0)
|
||||
)))
|
||||
}
|
||||
var n int
|
||||
var prevValue string
|
||||
c.SearchValues(false, nil, nil, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
c.SearchValues(false, nil, nil, func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, obj.String() > prevValue)
|
||||
expect(t, o.Geo().String() > prevValue)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 1))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
|
||||
n++
|
||||
prevValue = obj.String()
|
||||
prevValue = o.Geo().String()
|
||||
return true
|
||||
})
|
||||
expect(t, n == c.Count())
|
||||
n = 0
|
||||
c.SearchValues(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
c.SearchValues(true, nil, nil, func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, obj.String() < prevValue)
|
||||
expect(t, o.Geo().String() < prevValue)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 1))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
|
||||
n++
|
||||
prevValue = obj.String()
|
||||
prevValue = o.Geo().String()
|
||||
return true
|
||||
})
|
||||
expect(t, n == c.Count())
|
||||
|
||||
n = 0
|
||||
c.SearchValuesRange("0060", "0070", false, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, obj.String() > prevValue)
|
||||
expect(t, o.Geo().String() > prevValue)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 1))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
|
||||
n++
|
||||
prevValue = obj.String()
|
||||
prevValue = o.Geo().String()
|
||||
return true
|
||||
})
|
||||
expect(t, n == 10)
|
||||
|
||||
n = 0
|
||||
c.SearchValuesRange("0070", "0060", true, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
if n > 0 {
|
||||
expect(t, obj.String() < prevValue)
|
||||
expect(t, o.Geo().String() < prevValue)
|
||||
}
|
||||
expect(t, id == fieldValueAt(fields, 1))
|
||||
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
|
||||
n++
|
||||
prevValue = obj.String()
|
||||
prevValue = o.Geo().String()
|
||||
return true
|
||||
})
|
||||
expect(t, n == 10)
|
||||
|
@ -415,41 +352,37 @@ func TestCollectionSearch(t *testing.T) {
|
|||
|
||||
func TestCollectionWeight(t *testing.T) {
|
||||
c := New()
|
||||
c.Set("1", String("1"), field.List{}, 0)
|
||||
c.Set(object.New("1", String("1"), 0, 0, field.List{}))
|
||||
expect(t, c.TotalWeight() > 0)
|
||||
c.Delete("1")
|
||||
expect(t, c.TotalWeight() == 0)
|
||||
c.Set("1", String("1"),
|
||||
c.Set(object.New("1", String("1"), 0, 0,
|
||||
toFields(
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"1", "2", "3"},
|
||||
),
|
||||
0,
|
||||
)
|
||||
))
|
||||
expect(t, c.TotalWeight() > 0)
|
||||
c.Delete("1")
|
||||
expect(t, c.TotalWeight() == 0)
|
||||
c.Set("1", String("1"),
|
||||
c.Set(object.New("1", String("1"), 0, 0,
|
||||
toFields(
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"1", "2", "3"},
|
||||
),
|
||||
0,
|
||||
)
|
||||
c.Set("2", String("2"),
|
||||
))
|
||||
c.Set(object.New("2", String("2"), 0, 0,
|
||||
toFields(
|
||||
[]string{"d", "e", "f"},
|
||||
[]string{"4", "5", "6"},
|
||||
),
|
||||
0,
|
||||
)
|
||||
c.Set("1", String("1"),
|
||||
))
|
||||
c.Set(object.New("1", String("1"), 0, 0,
|
||||
toFields(
|
||||
[]string{"d", "e", "f"},
|
||||
[]string{"4", "5", "6"},
|
||||
),
|
||||
0,
|
||||
)
|
||||
))
|
||||
c.Delete("1")
|
||||
c.Delete("2")
|
||||
expect(t, c.TotalWeight() == 0)
|
||||
|
@ -484,77 +417,63 @@ func TestSpatialSearch(t *testing.T) {
|
|||
q4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q4"]`).Raw, nil)
|
||||
|
||||
c := New()
|
||||
c.Set("p1", p1, field.List{}, 0)
|
||||
c.Set("p2", p2, field.List{}, 0)
|
||||
c.Set("p3", p3, field.List{}, 0)
|
||||
c.Set("p4", p4, field.List{}, 0)
|
||||
c.Set("r1", r1, field.List{}, 0)
|
||||
c.Set("r2", r2, field.List{}, 0)
|
||||
c.Set("r3", r3, field.List{}, 0)
|
||||
c.Set(object.New("p1", p1, 0, 0, field.List{}))
|
||||
c.Set(object.New("p2", p2, 0, 0, field.List{}))
|
||||
c.Set(object.New("p3", p3, 0, 0, field.List{}))
|
||||
c.Set(object.New("p4", p4, 0, 0, field.List{}))
|
||||
c.Set(object.New("r1", r1, 0, 0, field.List{}))
|
||||
c.Set(object.New("r2", r2, 0, 0, field.List{}))
|
||||
c.Set(object.New("r3", r3, 0, 0, field.List{}))
|
||||
|
||||
var n int
|
||||
|
||||
n = 0
|
||||
c.Within(q1, 0, nil, nil,
|
||||
func(id string, obj geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(q1, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 3)
|
||||
|
||||
n = 0
|
||||
c.Within(q2, 0, nil, nil,
|
||||
func(id string, obj geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(q2, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 7)
|
||||
|
||||
n = 0
|
||||
c.Within(q3, 0, nil, nil,
|
||||
func(id string, obj geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(q3, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 4)
|
||||
|
||||
n = 0
|
||||
c.Intersects(q1, 0, nil, nil,
|
||||
func(_ string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Intersects(q1, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 4)
|
||||
|
||||
n = 0
|
||||
c.Intersects(q2, 0, nil, nil,
|
||||
func(_ string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Intersects(q2, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 7)
|
||||
|
||||
n = 0
|
||||
c.Intersects(q3, 0, nil, nil,
|
||||
func(_ string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 5)
|
||||
|
||||
n = 0
|
||||
c.Intersects(q3, 0, nil, nil,
|
||||
func(_ string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return n <= 1
|
||||
},
|
||||
)
|
||||
c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return n <= 1
|
||||
})
|
||||
expect(t, n == 2)
|
||||
|
||||
var items []geojson.Object
|
||||
|
@ -564,15 +483,13 @@ func TestSpatialSearch(t *testing.T) {
|
|||
|
||||
lastDist := float64(-1)
|
||||
distsMonotonic := true
|
||||
c.Nearby(q4, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List, dist float64) bool {
|
||||
if dist < lastDist {
|
||||
distsMonotonic = false
|
||||
}
|
||||
items = append(items, obj)
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Nearby(q4, nil, nil, func(o *object.Object, dist float64) bool {
|
||||
if dist < lastDist {
|
||||
distsMonotonic = false
|
||||
}
|
||||
items = append(items, o.Geo())
|
||||
return true
|
||||
})
|
||||
expect(t, len(items) == 7)
|
||||
expect(t, distsMonotonic)
|
||||
expect(t, reflect.DeepEqual(items, exitems))
|
||||
|
@ -590,72 +507,60 @@ func TestCollectionSparse(t *testing.T) {
|
|||
x := (r.Max.X-r.Min.X)*rand.Float64() + r.Min.X
|
||||
y := (r.Max.Y-r.Min.Y)*rand.Float64() + r.Min.Y
|
||||
point := PO(x, y)
|
||||
c.Set(fmt.Sprintf("%d", i), point, field.List{}, 0)
|
||||
c.Set(object.New(fmt.Sprintf("%d", i), point, 0, 0, field.List{}))
|
||||
}
|
||||
var n int
|
||||
n = 0
|
||||
c.Within(rect, 1, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(rect, 1, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 4)
|
||||
|
||||
n = 0
|
||||
c.Within(rect, 2, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(rect, 2, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 16)
|
||||
|
||||
n = 0
|
||||
c.Within(rect, 3, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Within(rect, 3, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 64)
|
||||
|
||||
n = 0
|
||||
c.Within(rect, 3, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List) bool {
|
||||
n++
|
||||
return n <= 30
|
||||
},
|
||||
)
|
||||
c.Within(rect, 3, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return n <= 30
|
||||
})
|
||||
expect(t, n == 31)
|
||||
|
||||
n = 0
|
||||
c.Intersects(rect, 3, nil, nil,
|
||||
func(id string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return true
|
||||
},
|
||||
)
|
||||
c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return true
|
||||
})
|
||||
expect(t, n == 64)
|
||||
|
||||
n = 0
|
||||
c.Intersects(rect, 3, nil, nil,
|
||||
func(id string, _ geojson.Object, _ field.List) bool {
|
||||
n++
|
||||
return n <= 30
|
||||
},
|
||||
)
|
||||
c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool {
|
||||
n++
|
||||
return n <= 30
|
||||
})
|
||||
expect(t, n == 31)
|
||||
|
||||
}
|
||||
|
||||
func testCollectionVerifyContents(t *testing.T, c *Collection, objs map[string]geojson.Object) {
|
||||
for id, o2 := range objs {
|
||||
o1, _, _, ok := c.Get(id)
|
||||
if !ok {
|
||||
o := c.Get(id)
|
||||
if o == nil {
|
||||
t.Fatalf("ok[%s] = false, expect true", id)
|
||||
}
|
||||
j1 := string(o1.AppendJSON(nil))
|
||||
j1 := string(o.Geo().AppendJSON(nil))
|
||||
j2 := string(o2.AppendJSON(nil))
|
||||
if j1 != j2 {
|
||||
t.Fatalf("j1 == %s, expect %s", j1, j2)
|
||||
|
@ -682,7 +587,7 @@ func TestManyCollections(t *testing.T) {
|
|||
col = New()
|
||||
colsM[key] = col
|
||||
}
|
||||
col.Set(id, obj, field.List{}, 0)
|
||||
col.Set(object.New(id, obj, 0, 0, field.List{}))
|
||||
k++
|
||||
}
|
||||
}
|
||||
|
@ -693,7 +598,7 @@ func TestManyCollections(t *testing.T) {
|
|||
Min: geometry.Point{X: -180, Y: 30},
|
||||
Max: geometry.Point{X: 34, Y: 100},
|
||||
}
|
||||
col.geoSearch(bbox, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
col.geoSearch(bbox, func(o *object.Object) bool {
|
||||
//println(id)
|
||||
return true
|
||||
})
|
||||
|
@ -736,7 +641,7 @@ func benchmarkInsert(t *testing.B, nFields int) {
|
|||
col := New()
|
||||
t.ResetTimer()
|
||||
for i := 0; i < t.N; i++ {
|
||||
col.Set(items[i].id, items[i].object, items[i].fields, 0)
|
||||
col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -760,12 +665,12 @@ func benchmarkReplace(t *testing.B, nFields int) {
|
|||
}
|
||||
col := New()
|
||||
for i := 0; i < t.N; i++ {
|
||||
col.Set(items[i].id, items[i].object, items[i].fields, 0)
|
||||
col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
t.ResetTimer()
|
||||
for _, i := range rand.Perm(t.N) {
|
||||
o, _, _ := col.Set(items[i].id, items[i].object, field.List{}, 0)
|
||||
if o != items[i].object {
|
||||
o := col.Set(object.New(items[i].id, items[i].object, 0, 0, field.List{}))
|
||||
if o.Geo() != items[i].object {
|
||||
t.Fatal("shoot!")
|
||||
}
|
||||
}
|
||||
|
@ -791,12 +696,12 @@ func benchmarkGet(t *testing.B, nFields int) {
|
|||
}
|
||||
col := New()
|
||||
for i := 0; i < t.N; i++ {
|
||||
col.Set(items[i].id, items[i].object, items[i].fields, 0)
|
||||
col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
t.ResetTimer()
|
||||
for _, i := range rand.Perm(t.N) {
|
||||
o, _, _, _ := col.Get(items[i].id)
|
||||
if o != items[i].object {
|
||||
o := col.Get(items[i].id)
|
||||
if o.Geo() != items[i].object {
|
||||
t.Fatal("shoot!")
|
||||
}
|
||||
}
|
||||
|
@ -822,12 +727,12 @@ func benchmarkRemove(t *testing.B, nFields int) {
|
|||
}
|
||||
col := New()
|
||||
for i := 0; i < t.N; i++ {
|
||||
col.Set(items[i].id, items[i].object, items[i].fields, 0)
|
||||
col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
t.ResetTimer()
|
||||
for _, i := range rand.Perm(t.N) {
|
||||
o, _, _ := col.Delete(items[i].id)
|
||||
if o != items[i].object {
|
||||
prev := col.Delete(items[i].id)
|
||||
if prev.Geo() != items[i].object {
|
||||
t.Fatal("shoot!")
|
||||
}
|
||||
}
|
||||
|
@ -853,12 +758,12 @@ func benchmarkScan(t *testing.B, nFields int) {
|
|||
}
|
||||
col := New()
|
||||
for i := 0; i < t.N; i++ {
|
||||
col.Set(items[i].id, items[i].object, items[i].fields, 0)
|
||||
col.Set(object.New(items[i].id, items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
t.ResetTimer()
|
||||
for i := 0; i < t.N; i++ {
|
||||
var scanIteration int
|
||||
col.Scan(true, nil, nil, func(id string, obj geojson.Object, fields field.List) bool {
|
||||
col.Scan(true, nil, nil, func(o *object.Object) bool {
|
||||
scanIteration++
|
||||
return scanIteration <= 500
|
||||
})
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
package collection
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"math"
|
||||
|
||||
func geodeticDistAlgo[T any](center [2]float64) (
|
||||
algo func(min, max [2]float64, data T, item bool) (dist float64),
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
func geodeticDistAlgo(center [2]float64) (
|
||||
algo func(min, max [2]float64, obj *object.Object, item bool) (dist float64),
|
||||
) {
|
||||
const earthRadius = 6371e3
|
||||
return func(min, max [2]float64, data T, item bool) (dist float64) {
|
||||
return func(min, max [2]float64, obj *object.Object, item bool) (dist float64) {
|
||||
if item {
|
||||
r := obj.Rect()
|
||||
min[0] = r.Min.X
|
||||
min[1] = r.Min.Y
|
||||
max[0] = r.Max.X
|
||||
max[1] = r.Max.Y
|
||||
}
|
||||
return earthRadius * pointRectDistGeodeticDeg(
|
||||
center[1], center[0],
|
||||
min[1], min[0],
|
||||
|
|
|
@ -151,6 +151,9 @@ func (fields List) Set(field Field) List {
|
|||
|
||||
func delfield(b []byte, s, e int) *byte {
|
||||
totallen := s + (len(b) - e)
|
||||
if totallen == 0 {
|
||||
return nil
|
||||
}
|
||||
var psz [10]byte
|
||||
pn := binary.PutUvarint(psz[:], uint64(totallen))
|
||||
plen := pn + totallen
|
||||
|
@ -347,3 +350,12 @@ func (fields List) Weight() int {
|
|||
x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 10, 10})))
|
||||
return x + n
|
||||
}
|
||||
|
||||
// Bytes returns the raw bytes (including the header)
|
||||
func (fields List) Bytes() []byte {
|
||||
if fields.p == nil {
|
||||
return nil
|
||||
}
|
||||
x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 10, 10})))
|
||||
return (*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 0, n + x})))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
)
|
||||
|
||||
type Object struct {
|
||||
id string
|
||||
geo geojson.Object
|
||||
created int64 // unix nano created
|
||||
expires int64 // unix nano expiration
|
||||
fields field.List
|
||||
}
|
||||
|
||||
func (o *Object) ID() string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
return o.id
|
||||
}
|
||||
|
||||
func (o *Object) Fields() field.List {
|
||||
if o == nil {
|
||||
return field.List{}
|
||||
}
|
||||
return o.fields
|
||||
}
|
||||
|
||||
func (o *Object) Created() int64 {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return o.created
|
||||
}
|
||||
|
||||
func (o *Object) Expires() int64 {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return o.expires
|
||||
}
|
||||
|
||||
func (o *Object) Rect() geometry.Rect {
|
||||
if o == nil || o.geo == nil {
|
||||
return geometry.Rect{}
|
||||
}
|
||||
return o.geo.Rect()
|
||||
}
|
||||
|
||||
func (o *Object) Geo() geojson.Object {
|
||||
if o == nil || o.geo == nil {
|
||||
return nil
|
||||
}
|
||||
return o.geo
|
||||
}
|
||||
|
||||
func (o *Object) String() string {
|
||||
if o == nil || o.geo == nil {
|
||||
return ""
|
||||
}
|
||||
return o.geo.String()
|
||||
}
|
||||
|
||||
func (o *Object) IsSpatial() bool {
|
||||
_, ok := o.geo.(geojson.Spatial)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (o *Object) Weight() int {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
var weight int
|
||||
weight += len(o.ID())
|
||||
if o.IsSpatial() {
|
||||
weight += o.Geo().NumPoints() * 16
|
||||
} else {
|
||||
weight += len(o.Geo().String())
|
||||
}
|
||||
weight += o.Fields().Weight()
|
||||
return weight
|
||||
}
|
||||
|
||||
func New(id string, geo geojson.Object, created, expires int64,
|
||||
fields field.List,
|
||||
) *Object {
|
||||
return &Object{
|
||||
id: id,
|
||||
geo: geo,
|
||||
created: created,
|
||||
expires: expires,
|
||||
fields: fields,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/assert"
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
)
|
||||
|
||||
func P(x, y float64) geojson.Object {
|
||||
return geojson.NewSimplePoint(geometry.Point{X: 10, Y: 20})
|
||||
}
|
||||
func TestObject(t *testing.T) {
|
||||
o := New("hello", P(10, 20), 0, 99, field.List{})
|
||||
assert.Assert(o.ID() == "hello")
|
||||
}
|
|
@ -226,8 +226,8 @@ func (s *Server) getQueueCandidates(d *commandDetails) []*Hook {
|
|||
return true
|
||||
})
|
||||
// look for candidates that might "cross" geofences
|
||||
if d.oldObj != nil && d.obj != nil && s.hookCross.Len() > 0 {
|
||||
r1, r2 := d.oldObj.Rect(), d.obj.Rect()
|
||||
if d.old != nil && d.obj != nil && s.hookCross.Len() > 0 {
|
||||
r1, r2 := d.old.Rect(), d.obj.Rect()
|
||||
s.hookCross.Search(
|
||||
[2]float64{
|
||||
math.Min(r1.Min.X, r2.Min.X),
|
||||
|
@ -246,8 +246,8 @@ func (s *Server) getQueueCandidates(d *commandDetails) []*Hook {
|
|||
})
|
||||
}
|
||||
// look for candidates that overlap the old object
|
||||
if d.oldObj != nil {
|
||||
r1 := d.oldObj.Rect()
|
||||
if d.old != nil {
|
||||
r1 := d.old.Rect()
|
||||
s.hookTree.Search(
|
||||
[2]float64{r1.Min.X, r1.Min.Y},
|
||||
[2]float64{r1.Max.X, r1.Max.Y},
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/tidwall/btree"
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/tile38/core"
|
||||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/log"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
const maxkeys = 8
|
||||
|
@ -97,10 +97,10 @@ func (s *Server) aofshrink() {
|
|||
var now = time.Now().UnixNano() // used for expiration
|
||||
var count = 0 // the object count
|
||||
col.ScanGreaterOrEqual(nextid, false, nil, nil,
|
||||
func(id string, obj geojson.Object, fields field.List, ex int64) bool {
|
||||
func(o *object.Object) bool {
|
||||
if count == maxids {
|
||||
// we reached the max number of ids for one batch
|
||||
nextid = id
|
||||
nextid = o.ID()
|
||||
idsdone = false
|
||||
return false
|
||||
}
|
||||
|
@ -108,8 +108,8 @@ func (s *Server) aofshrink() {
|
|||
values = values[:0]
|
||||
values = append(values, "set")
|
||||
values = append(values, keys[0])
|
||||
values = append(values, id)
|
||||
fields.Scan(func(f field.Field) bool {
|
||||
values = append(values, o.ID())
|
||||
o.Fields().Scan(func(f field.Field) bool {
|
||||
if !f.Value().IsZero() {
|
||||
values = append(values, "field")
|
||||
values = append(values, f.Name())
|
||||
|
@ -117,8 +117,8 @@ func (s *Server) aofshrink() {
|
|||
}
|
||||
return true
|
||||
})
|
||||
if ex != 0 {
|
||||
ttl := math.Floor(float64(ex-now)/float64(time.Second)*10) / 10
|
||||
if o.Expires() != 0 {
|
||||
ttl := math.Floor(float64(o.Expires()-now)/float64(time.Second)*10) / 10
|
||||
if ttl < 0.1 {
|
||||
// always leave a little bit of ttl.
|
||||
ttl = 0.1
|
||||
|
@ -126,12 +126,12 @@ func (s *Server) aofshrink() {
|
|||
values = append(values, "ex")
|
||||
values = append(values, strconv.FormatFloat(ttl, 'f', -1, 64))
|
||||
}
|
||||
if objIsSpatial(obj) {
|
||||
if objIsSpatial(o.Geo()) {
|
||||
values = append(values, "object")
|
||||
values = append(values, string(obj.AppendJSON(nil)))
|
||||
values = append(values, string(o.Geo().AppendJSON(nil)))
|
||||
} else {
|
||||
values = append(values, "string")
|
||||
values = append(values, obj.String())
|
||||
values = append(values, o.Geo().String())
|
||||
}
|
||||
|
||||
// append the values to the aof buffer
|
||||
|
|
|
@ -14,30 +14,9 @@ import (
|
|||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/glob"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
// type fvt struct {
|
||||
// field string
|
||||
// value float64
|
||||
// }
|
||||
|
||||
// func orderFields(fmap map[string]int, farr []string, fields []float64) []fvt {
|
||||
// var fv fvt
|
||||
// var idx int
|
||||
// fvs := make([]fvt, 0, len(fmap))
|
||||
// for _, field := range farr {
|
||||
// idx = fmap[field]
|
||||
// if idx < len(fields) {
|
||||
// fv.field = field
|
||||
// fv.value = fields[idx]
|
||||
// if fv.value != 0 {
|
||||
// fvs = append(fvs, fv)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return fvs
|
||||
// }
|
||||
|
||||
func (s *Server) cmdBounds(msg *Message) (resp.Value, error) {
|
||||
start := time.Now()
|
||||
vs := msg.Args[1:]
|
||||
|
@ -150,7 +129,8 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
}
|
||||
return NOMessage, errKeyNotFound
|
||||
}
|
||||
o, fields, _, ok := col.Get(id)
|
||||
o := col.Get(id)
|
||||
ok = o != nil
|
||||
if !ok {
|
||||
if msg.OutputType == RESP {
|
||||
return resp.NullValue(), nil
|
||||
|
@ -174,17 +154,17 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
case "object":
|
||||
if msg.OutputType == JSON {
|
||||
buf.WriteString(`,"object":`)
|
||||
buf.WriteString(string(o.AppendJSON(nil)))
|
||||
buf.WriteString(string(o.Geo().AppendJSON(nil)))
|
||||
} else {
|
||||
vals = append(vals, resp.StringValue(o.String()))
|
||||
vals = append(vals, resp.StringValue(o.Geo().String()))
|
||||
}
|
||||
case "point":
|
||||
if msg.OutputType == JSON {
|
||||
buf.WriteString(`,"point":`)
|
||||
buf.Write(appendJSONSimplePoint(nil, o))
|
||||
buf.Write(appendJSONSimplePoint(nil, o.Geo()))
|
||||
} else {
|
||||
point := o.Center()
|
||||
z := extractZCoordinate(o)
|
||||
point := o.Geo().Center()
|
||||
z := extractZCoordinate(o.Geo())
|
||||
if z != 0 {
|
||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||
resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)),
|
||||
|
@ -209,7 +189,7 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
if err != nil || precision < 1 || precision > 12 {
|
||||
return NOMessage, errInvalidArgument(sprecision)
|
||||
}
|
||||
center := o.Center()
|
||||
center := o.Geo().Center()
|
||||
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision))
|
||||
if msg.OutputType == JSON {
|
||||
buf.WriteString(`"` + p + `"`)
|
||||
|
@ -219,7 +199,7 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
case "bounds":
|
||||
if msg.OutputType == JSON {
|
||||
buf.WriteString(`,"bounds":`)
|
||||
buf.Write(appendJSONSimpleBounds(nil, o))
|
||||
buf.Write(appendJSONSimpleBounds(nil, o.Geo()))
|
||||
} else {
|
||||
bbox := o.Rect()
|
||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||
|
@ -239,14 +219,14 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
return NOMessage, errInvalidNumberOfArguments
|
||||
}
|
||||
if withfields {
|
||||
nfields := fields.Len()
|
||||
nfields := o.Fields().Len()
|
||||
if nfields > 0 {
|
||||
fvals := make([]resp.Value, 0, nfields*2)
|
||||
if msg.OutputType == JSON {
|
||||
buf.WriteString(`,"fields":{`)
|
||||
}
|
||||
var i int
|
||||
fields.Scan(func(f field.Field) bool {
|
||||
o.Fields().Scan(func(f field.Field) bool {
|
||||
if msg.OutputType == JSON {
|
||||
if i > 0 {
|
||||
buf.WriteString(`,`)
|
||||
|
@ -282,57 +262,64 @@ func (s *Server) cmdGet(msg *Message) (resp.Value, error) {
|
|||
return NOMessage, nil
|
||||
}
|
||||
|
||||
func (s *Server) cmdDel(msg *Message) (res resp.Value, d commandDetails, err error) {
|
||||
// DEL key id [ERRON404]
|
||||
func (s *Server) cmdDel(msg *Message) (resp.Value, commandDetails, error) {
|
||||
start := time.Now()
|
||||
vs := msg.Args[1:]
|
||||
var ok bool
|
||||
if vs, d.key, ok = tokenval(vs); !ok || d.key == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
if vs, d.id, ok = tokenval(vs); !ok || d.id == "" {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
|
||||
// >> Args
|
||||
|
||||
args := msg.Args
|
||||
if len(args) < 3 {
|
||||
return retwerr(errInvalidNumberOfArguments)
|
||||
}
|
||||
key := args[1]
|
||||
id := args[2]
|
||||
erron404 := false
|
||||
if len(vs) > 0 {
|
||||
_, arg, ok := tokenval(vs)
|
||||
if ok && strings.ToLower(arg) == "erron404" {
|
||||
for i := 3; i < len(args); i++ {
|
||||
switch strings.ToLower(args[i]) {
|
||||
case "erron404":
|
||||
erron404 = true
|
||||
vs = vs[1:]
|
||||
} else {
|
||||
err = errInvalidArgument(arg)
|
||||
return
|
||||
default:
|
||||
return retwerr(errInvalidArgument(args[i]))
|
||||
}
|
||||
}
|
||||
if len(vs) != 0 {
|
||||
err = errInvalidNumberOfArguments
|
||||
return
|
||||
}
|
||||
found := false
|
||||
col, _ := s.cols.Get(d.key)
|
||||
|
||||
// >> Operation
|
||||
|
||||
updated := false
|
||||
var old *object.Object
|
||||
col, _ := s.cols.Get(key)
|
||||
if col != nil {
|
||||
d.obj, d.fields, ok = col.Delete(d.id)
|
||||
if ok {
|
||||
old = col.Delete(id)
|
||||
if old != nil {
|
||||
if col.Count() == 0 {
|
||||
s.cols.Delete(d.key)
|
||||
s.cols.Delete(key)
|
||||
}
|
||||
found = true
|
||||
updated = true
|
||||
} else if erron404 {
|
||||
err = errIDNotFound
|
||||
return
|
||||
return retwerr(errIDNotFound)
|
||||
}
|
||||
} else if erron404 {
|
||||
err = errKeyNotFound
|
||||
return
|
||||
return retwerr(errKeyNotFound)
|
||||
}
|
||||
s.groupDisconnectObject(d.key, d.id)
|
||||
s.groupDisconnectObject(key, id)
|
||||
|
||||
// >> Response
|
||||
|
||||
var d commandDetails
|
||||
|
||||
d.command = "del"
|
||||
d.updated = found
|
||||
d.key = key
|
||||
d.obj = old
|
||||
d.updated = updated
|
||||
d.timestamp = time.Now()
|
||||
|
||||
var res resp.Value
|
||||
|
||||
switch msg.OutputType {
|
||||
case JSON:
|
||||
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}")
|
||||
res = resp.StringValue(`{"ok":true,"elapsed":"` +
|
||||
time.Since(start).String() + "\"}")
|
||||
case RESP:
|
||||
if d.updated {
|
||||
res = resp.IntegerValue(1)
|
||||
|
@ -340,7 +327,7 @@ func (s *Server) cmdDel(msg *Message) (res resp.Value, d commandDetails, err err
|
|||
res = resp.IntegerValue(0)
|
||||
}
|
||||
}
|
||||
return
|
||||
return res, d, nil
|
||||
}
|
||||
|
||||
func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err error) {
|
||||
|
@ -360,14 +347,14 @@ func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
return
|
||||
}
|
||||
now := time.Now()
|
||||
iter := func(id string, o geojson.Object, fields field.List) bool {
|
||||
if match, _ := glob.Match(d.pattern, id); match {
|
||||
iter := func(o *object.Object) bool {
|
||||
if match, _ := glob.Match(d.pattern, o.ID()); match {
|
||||
d.children = append(d.children, &commandDetails{
|
||||
command: "del",
|
||||
updated: true,
|
||||
timestamp: now,
|
||||
key: d.key,
|
||||
id: id,
|
||||
obj: o,
|
||||
})
|
||||
}
|
||||
return true
|
||||
|
@ -384,14 +371,15 @@ func (s *Server) cmdPdel(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
}
|
||||
var atLeastOneNotDeleted bool
|
||||
for i, dc := range d.children {
|
||||
dc.obj, dc.fields, ok = col.Delete(dc.id)
|
||||
if !ok {
|
||||
old := col.Delete(dc.obj.ID())
|
||||
if old == nil {
|
||||
d.children[i].command = "?"
|
||||
atLeastOneNotDeleted = true
|
||||
} else {
|
||||
dc.obj = old
|
||||
d.children[i] = dc
|
||||
}
|
||||
s.groupDisconnectObject(dc.key, dc.id)
|
||||
s.groupDisconnectObject(dc.key, dc.obj.ID())
|
||||
}
|
||||
if atLeastOneNotDeleted {
|
||||
var nchildren []*commandDetails
|
||||
|
@ -565,7 +553,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
var ex int64
|
||||
var xx bool
|
||||
var nx bool
|
||||
var obj geojson.Object
|
||||
var oobj geojson.Object
|
||||
|
||||
args := msg.Args
|
||||
if len(args) < 3 {
|
||||
|
@ -614,7 +602,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
}
|
||||
str := args[i+1]
|
||||
i += 1
|
||||
obj = collection.String(str)
|
||||
oobj = collection.String(str)
|
||||
case "point":
|
||||
if i+2 >= len(args) {
|
||||
return retwerr(errInvalidNumberOfArguments)
|
||||
|
@ -642,9 +630,9 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
return retwerr(errInvalidArgument(slon))
|
||||
}
|
||||
if !hasZ {
|
||||
obj = geojson.NewPoint(geometry.Point{X: x, Y: y})
|
||||
oobj = geojson.NewPoint(geometry.Point{X: x, Y: y})
|
||||
} else {
|
||||
obj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z)
|
||||
oobj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z)
|
||||
}
|
||||
case "bounds":
|
||||
if i+4 >= len(args) {
|
||||
|
@ -659,7 +647,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
}
|
||||
}
|
||||
i += 4
|
||||
obj = geojson.NewRect(geometry.Rect{
|
||||
oobj = geojson.NewRect(geometry.Rect{
|
||||
Min: geometry.Point{X: vals[1], Y: vals[0]},
|
||||
Max: geometry.Point{X: vals[3], Y: vals[2]},
|
||||
})
|
||||
|
@ -670,7 +658,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
shash := args[i+1]
|
||||
i += 1
|
||||
lat, lon := geohash.Decode(shash)
|
||||
obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
||||
oobj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
||||
case "object":
|
||||
if i+1 >= len(args) {
|
||||
return retwerr(errInvalidNumberOfArguments)
|
||||
|
@ -678,7 +666,7 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
json := args[i+1]
|
||||
i += 1
|
||||
var err error
|
||||
obj, err = geojson.Parse(json, &s.geomParseOpts)
|
||||
oobj, err = geojson.Parse(json, &s.geomParseOpts)
|
||||
if err != nil {
|
||||
return retwerr(err)
|
||||
}
|
||||
|
@ -702,7 +690,10 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
|
||||
var ofields field.List
|
||||
if !nada {
|
||||
_, ofields, _, ok = col.Get(id)
|
||||
o := col.Get(id)
|
||||
if o != nil {
|
||||
ofields = o.Fields()
|
||||
}
|
||||
if xx || nx {
|
||||
if (nx && ok) || (xx && !ok) {
|
||||
nada = true
|
||||
|
@ -730,18 +721,16 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
ofields = ofields.Set(f)
|
||||
}
|
||||
|
||||
oldObj, oldFields, newFields := col.Set(id, obj, ofields, ex)
|
||||
obj := object.New(id, oobj, 0, ex, ofields)
|
||||
old := col.Set(obj)
|
||||
|
||||
// >> Response
|
||||
|
||||
var d commandDetails
|
||||
d.command = "set"
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.obj = obj
|
||||
d.oldObj = oldObj
|
||||
d.oldFields = oldFields
|
||||
d.fields = newFields
|
||||
d.old = old
|
||||
d.updated = true // perhaps we should do a diff on the previous object?
|
||||
d.timestamp = time.Now()
|
||||
|
||||
|
@ -811,12 +800,14 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
if !ok {
|
||||
return retwerr(errKeyNotFound)
|
||||
}
|
||||
obj, ofields, ex, ok := col.Get(id)
|
||||
o := col.Get(id)
|
||||
ok = o != nil
|
||||
if !(ok || xx) {
|
||||
return retwerr(errIDNotFound)
|
||||
}
|
||||
|
||||
if ok {
|
||||
ofields := o.Fields()
|
||||
for _, f := range fields {
|
||||
prev := ofields.Get(f.Name())
|
||||
if !prev.Value().Equals(f.Value()) {
|
||||
|
@ -824,11 +815,11 @@ func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) {
|
|||
updateCount++
|
||||
}
|
||||
}
|
||||
col.Set(id, obj, ofields, ex)
|
||||
d.obj = obj
|
||||
obj := object.New(id, o.Geo(), 0, o.Expires(), ofields)
|
||||
col.Set(obj)
|
||||
d.command = "fset"
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.obj = obj
|
||||
d.timestamp = time.Now()
|
||||
d.updated = updateCount > 0
|
||||
}
|
||||
|
@ -861,21 +852,22 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
|
|||
return retwerr(errInvalidArgument(svalue))
|
||||
}
|
||||
var ok bool
|
||||
var obj *object.Object
|
||||
col, _ := s.cols.Get(key)
|
||||
if col != nil {
|
||||
// replace the expiration by getting the old objec
|
||||
ex := time.Now().Add(time.Duration(float64(time.Second) * value)).UnixNano()
|
||||
var obj geojson.Object
|
||||
var fields field.List
|
||||
obj, fields, _, ok = col.Get(id)
|
||||
o := col.Get(id)
|
||||
ok = o != nil
|
||||
if ok {
|
||||
col.Set(id, obj, fields, ex)
|
||||
obj = object.New(id, o.Geo(), 0, ex, o.Fields())
|
||||
col.Set(obj)
|
||||
}
|
||||
}
|
||||
var d commandDetails
|
||||
if ok {
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.obj = obj
|
||||
d.command = "expire"
|
||||
d.updated = true
|
||||
d.timestamp = time.Now()
|
||||
|
@ -909,41 +901,35 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
|
|||
return retwerr(errInvalidNumberOfArguments)
|
||||
}
|
||||
key, id := args[1], args[2]
|
||||
var cleared bool
|
||||
var ok bool
|
||||
col, _ := s.cols.Get(key)
|
||||
if col != nil {
|
||||
var ex int64
|
||||
_, _, ex, ok = col.Get(id)
|
||||
if ok && ex != 0 {
|
||||
var obj geojson.Object
|
||||
var fields field.List
|
||||
obj, fields, _, ok = col.Get(id)
|
||||
if ok {
|
||||
col.Set(id, obj, fields, 0)
|
||||
}
|
||||
if ok {
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
if col == nil {
|
||||
if msg.OutputType == RESP {
|
||||
return resp.IntegerValue(0), commandDetails{}, nil
|
||||
}
|
||||
if col == nil {
|
||||
return retwerr(errKeyNotFound)
|
||||
return retwerr(errKeyNotFound)
|
||||
}
|
||||
o := col.Get(id)
|
||||
if o == nil {
|
||||
if msg.OutputType == RESP {
|
||||
return resp.IntegerValue(0), commandDetails{}, nil
|
||||
}
|
||||
return retwerr(errIDNotFound)
|
||||
}
|
||||
|
||||
var obj *object.Object
|
||||
var cleared bool
|
||||
if o.Expires() != 0 {
|
||||
obj = object.New(id, o.Geo(), 0, 0, o.Fields())
|
||||
col.Set(obj)
|
||||
cleared = true
|
||||
}
|
||||
|
||||
var res resp.Value
|
||||
|
||||
var d commandDetails
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.command = "persist"
|
||||
d.key = key
|
||||
d.obj = obj
|
||||
d.updated = cleared
|
||||
d.timestamp = time.Now()
|
||||
|
||||
|
@ -973,15 +959,15 @@ func (s *Server) cmdTTL(msg *Message) (resp.Value, error) {
|
|||
var ok2 bool
|
||||
col, _ := s.cols.Get(key)
|
||||
if col != nil {
|
||||
var ex int64
|
||||
_, _, ex, ok = col.Get(id)
|
||||
o := col.Get(id)
|
||||
ok = o != nil
|
||||
if ok {
|
||||
if ex != 0 {
|
||||
if o.Expires() != 0 {
|
||||
now := start.UnixNano()
|
||||
if now > ex {
|
||||
if now > o.Expires() {
|
||||
ok2 = false
|
||||
} else {
|
||||
v = float64(ex-now) / float64(time.Second)
|
||||
v = float64(o.Expires()-now) / float64(time.Second)
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/log"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
const bgExpireDelay = time.Second / 10
|
||||
|
@ -31,11 +32,11 @@ func (s *Server) backgroundExpireObjects(now time.Time) {
|
|||
nano := now.UnixNano()
|
||||
var msgs []*Message
|
||||
s.cols.Scan(func(key string, col *collection.Collection) bool {
|
||||
col.ScanExpires(func(id string, expires int64) bool {
|
||||
if nano < expires {
|
||||
col.ScanExpires(func(o *object.Object) bool {
|
||||
if nano < o.Expires() {
|
||||
return false
|
||||
}
|
||||
msgs = append(msgs, &Message{Args: []string{"del", key, id}})
|
||||
msgs = append(msgs, &Message{Args: []string{"del", key, o.ID()}})
|
||||
return true
|
||||
})
|
||||
return true
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/glob"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
// FenceMatch executes a fence match returns back json messages for fence detection.
|
||||
|
@ -81,10 +82,13 @@ func fenceMatch(
|
|||
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
|
||||
}
|
||||
}
|
||||
if !multiGlobMatch(fence.globs, details.id) {
|
||||
if details.obj == nil {
|
||||
return nil
|
||||
}
|
||||
if details.obj == nil || !objIsSpatial(details.obj) {
|
||||
if !multiGlobMatch(fence.globs, details.obj.ID()) {
|
||||
return nil
|
||||
}
|
||||
if !objIsSpatial(details.obj.Geo()) {
|
||||
return nil
|
||||
}
|
||||
if details.command == "fset" {
|
||||
|
@ -97,7 +101,7 @@ func fenceMatch(
|
|||
return []string{
|
||||
`{"command":"del"` + hookJSONString(hookName, metas) +
|
||||
`,"key":` + jsonString(details.key) +
|
||||
`,"id":` + jsonString(details.id) +
|
||||
`,"id":` + jsonString(details.obj.ID()) +
|
||||
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +111,7 @@ func fenceMatch(
|
|||
if fence.roam.on {
|
||||
if details.command == "set" {
|
||||
roamNearbys, roamFaraways =
|
||||
fenceMatchRoam(sw.s, fence, details.id,
|
||||
details.oldObj, details.obj)
|
||||
fenceMatchRoam(sw.s, fence, details.obj, details.old)
|
||||
if len(roamNearbys) == 0 && len(roamFaraways) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -117,14 +120,14 @@ func fenceMatch(
|
|||
} else {
|
||||
var nocross bool
|
||||
// not using roaming
|
||||
match1 := fenceMatchObject(fence, details.oldObj)
|
||||
match1 := fenceMatchObject(fence, details.old)
|
||||
if match1 {
|
||||
match1, _, _ = sw.testObject(details.id, details.oldObj, details.oldFields)
|
||||
match1, _, _ = sw.testObject(details.old)
|
||||
nocross = !match1
|
||||
}
|
||||
match2 := fenceMatchObject(fence, details.obj)
|
||||
if match2 {
|
||||
match2, _, _ = sw.testObject(details.id, details.obj, details.fields)
|
||||
match2, _, _ = sw.testObject(details.obj)
|
||||
nocross = !match2
|
||||
}
|
||||
if match1 && match2 {
|
||||
|
@ -140,11 +143,11 @@ func fenceMatch(
|
|||
if details.command != "fset" {
|
||||
// Maybe the old object and new object create a line that crosses the fence.
|
||||
// Must detect for that possibility.
|
||||
if !nocross && details.oldObj != nil {
|
||||
if !nocross && details.old != nil {
|
||||
ls := geojson.NewLineString(geometry.NewLine(
|
||||
[]geometry.Point{
|
||||
details.oldObj.Center(),
|
||||
details.obj.Center(),
|
||||
details.old.Geo().Center(),
|
||||
details.obj.Geo().Center(),
|
||||
}, nil))
|
||||
temp := false
|
||||
if fence.cmd == "within" {
|
||||
|
@ -153,7 +156,8 @@ func fenceMatch(
|
|||
fence.cmd = "intersects"
|
||||
temp = true
|
||||
}
|
||||
if fenceMatchObject(fence, ls) {
|
||||
lso := object.New("", ls, 0, 0, field.List{})
|
||||
if fenceMatchObject(fence, lso) {
|
||||
detect = "cross"
|
||||
}
|
||||
if temp {
|
||||
|
@ -185,18 +189,15 @@ func fenceMatch(
|
|||
}
|
||||
var distance float64
|
||||
if fence.distance && fence.obj != nil {
|
||||
distance = details.obj.Distance(fence.obj)
|
||||
distance = details.obj.Geo().Distance(fence.obj)
|
||||
}
|
||||
// TODO: fields
|
||||
// sw.fmap = details.fmap
|
||||
|
||||
sw.fullFields = true
|
||||
sw.msg.OutputType = JSON
|
||||
sw.writeObject(ScanWriterParams{
|
||||
id: details.id,
|
||||
o: details.obj,
|
||||
fields: details.fields,
|
||||
obj: details.obj,
|
||||
noTest: true,
|
||||
distance: distance,
|
||||
dist: distance,
|
||||
distOutput: fence.distance,
|
||||
})
|
||||
|
||||
|
@ -215,14 +216,14 @@ func fenceMatch(
|
|||
|
||||
var group string
|
||||
if detect == "enter" {
|
||||
group = sw.s.groupConnect(hookName, details.key, details.id)
|
||||
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
|
||||
} else if detect == "cross" {
|
||||
sw.s.groupDisconnect(hookName, details.key, details.id)
|
||||
group = sw.s.groupConnect(hookName, details.key, details.id)
|
||||
sw.s.groupDisconnect(hookName, details.key, details.obj.ID())
|
||||
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
|
||||
} else {
|
||||
group = sw.s.groupGet(hookName, details.key, details.id)
|
||||
group = sw.s.groupGet(hookName, details.key, details.obj.ID())
|
||||
if group == "" {
|
||||
group = sw.s.groupConnect(hookName, details.key, details.id)
|
||||
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
|
||||
}
|
||||
}
|
||||
var msgs []string
|
||||
|
@ -287,26 +288,24 @@ func extendRoamMessage(
|
|||
nmsg = append(nmsg, `,"scan":[`...)
|
||||
col, _ := sw.s.cols.Get(fence.roam.key)
|
||||
if col != nil {
|
||||
obj, _, _, ok := col.Get(match.id)
|
||||
if ok {
|
||||
o := col.Get(match.id)
|
||||
if o != nil {
|
||||
nmsg = append(nmsg, `{"id":`...)
|
||||
nmsg = appendJSONString(nmsg, match.id)
|
||||
nmsg = append(nmsg, `,"self":true,"object":`...)
|
||||
nmsg = obj.AppendJSON(nmsg)
|
||||
nmsg = o.Geo().AppendJSON(nmsg)
|
||||
nmsg = append(nmsg, '}')
|
||||
}
|
||||
pattern := match.id + fence.roam.scan
|
||||
iterator := func(
|
||||
oid string, o geojson.Object, fields field.List,
|
||||
) bool {
|
||||
if oid == match.id {
|
||||
iterator := func(o *object.Object) bool {
|
||||
if o.ID() == match.id {
|
||||
return true
|
||||
}
|
||||
if matched, _ := glob.Match(pattern, oid); matched {
|
||||
if matched, _ := glob.Match(pattern, o.ID()); matched {
|
||||
nmsg = append(nmsg, `,{"id":`...)
|
||||
nmsg = appendJSONString(nmsg, oid)
|
||||
nmsg = appendJSONString(nmsg, o.ID())
|
||||
nmsg = append(nmsg, `,"object":`...)
|
||||
nmsg = o.AppendJSON(nmsg)
|
||||
nmsg = o.Geo().AppendJSON(nmsg)
|
||||
nmsg = append(nmsg, '}')
|
||||
}
|
||||
return true
|
||||
|
@ -345,8 +344,8 @@ func makemsg(
|
|||
return string(buf)
|
||||
}
|
||||
|
||||
func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {
|
||||
if obj == nil {
|
||||
func fenceMatchObject(fence *liveFenceSwitches, o *object.Object) bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
if fence.roam.on {
|
||||
|
@ -356,18 +355,18 @@ func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {
|
|||
switch fence.cmd {
|
||||
case "nearby":
|
||||
// nearby is an INTERSECT on a Circle
|
||||
return obj.Intersects(fence.obj)
|
||||
return o.Geo().Intersects(fence.obj)
|
||||
case "within":
|
||||
return obj.Within(fence.obj)
|
||||
return o.Geo().Within(fence.obj)
|
||||
case "intersects":
|
||||
return obj.Intersects(fence.obj)
|
||||
return o.Geo().Intersects(fence.obj)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fenceMatchNearbys(
|
||||
s *Server, fence *liveFenceSwitches,
|
||||
id string, obj geojson.Object,
|
||||
obj *object.Object,
|
||||
) (nearbys []roamMatch) {
|
||||
if obj == nil {
|
||||
return nil
|
||||
|
@ -376,49 +375,49 @@ func fenceMatchNearbys(
|
|||
if col == nil {
|
||||
return nil
|
||||
}
|
||||
center := obj.Center()
|
||||
center := obj.Geo().Center()
|
||||
minLat, minLon, maxLat, maxLon :=
|
||||
geo.RectFromCenter(center.Y, center.X, fence.roam.meters)
|
||||
rect := geometry.Rect{
|
||||
Min: geometry.Point{X: minLon, Y: minLat},
|
||||
Max: geometry.Point{X: maxLon, Y: maxLat},
|
||||
}
|
||||
col.Intersects(geojson.NewRect(rect), 0, nil, nil, func(
|
||||
id2 string, obj2 geojson.Object, fields field.List,
|
||||
) bool {
|
||||
var idMatch bool
|
||||
if id2 == id {
|
||||
return true // skip self
|
||||
}
|
||||
meters := obj.Distance(obj2)
|
||||
if meters > fence.roam.meters {
|
||||
return true // skip outside radius
|
||||
}
|
||||
if fence.roam.pattern {
|
||||
idMatch, _ = glob.Match(fence.roam.id, id2)
|
||||
} else {
|
||||
idMatch = fence.roam.id == id2
|
||||
}
|
||||
if !idMatch {
|
||||
return true // skip non-id match
|
||||
}
|
||||
match := roamMatch{
|
||||
id: id2,
|
||||
obj: obj2,
|
||||
meters: obj.Distance(obj2),
|
||||
}
|
||||
nearbys = append(nearbys, match)
|
||||
return true
|
||||
})
|
||||
col.Intersects(geojson.NewRect(rect), 0, nil, nil,
|
||||
func(o *object.Object) bool {
|
||||
var idMatch bool
|
||||
if o.ID() == obj.ID() {
|
||||
return true // skip self
|
||||
}
|
||||
meters := o.Geo().Distance(o.Geo())
|
||||
if meters > fence.roam.meters {
|
||||
return true // skip outside radius
|
||||
}
|
||||
if fence.roam.pattern {
|
||||
idMatch, _ = glob.Match(fence.roam.id, o.ID())
|
||||
} else {
|
||||
idMatch = fence.roam.id == o.ID()
|
||||
}
|
||||
if !idMatch {
|
||||
return true // skip non-id match
|
||||
}
|
||||
match := roamMatch{
|
||||
id: o.ID(),
|
||||
obj: o.Geo(),
|
||||
meters: obj.Geo().Distance(o.Geo()),
|
||||
}
|
||||
nearbys = append(nearbys, match)
|
||||
return true
|
||||
},
|
||||
)
|
||||
return nearbys
|
||||
}
|
||||
|
||||
func fenceMatchRoam(
|
||||
s *Server, fence *liveFenceSwitches,
|
||||
id string, old, obj geojson.Object,
|
||||
obj, old *object.Object,
|
||||
) (nearbys, faraways []roamMatch) {
|
||||
oldNearbys := fenceMatchNearbys(s, fence, id, old)
|
||||
newNearbys := fenceMatchNearbys(s, fence, id, obj)
|
||||
oldNearbys := fenceMatchNearbys(s, fence, old)
|
||||
newNearbys := fenceMatchNearbys(s, fence, obj)
|
||||
// Go through all matching objects in new-nearbys and old-nearbys.
|
||||
for i := 0; i < len(oldNearbys); i++ {
|
||||
var match bool
|
||||
|
@ -444,7 +443,7 @@ func fenceMatchRoam(
|
|||
faraways, nearbys = oldNearbys, newNearbys
|
||||
// ensure the faraways distances are to the new object
|
||||
for i := 0; i < len(faraways); i++ {
|
||||
faraways[i].meters = faraways[i].obj.Distance(obj)
|
||||
faraways[i].meters = faraways[i].obj.Distance(obj.Geo())
|
||||
}
|
||||
sortRoamMatches(faraways)
|
||||
sortRoamMatches(nearbys)
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/tidwall/resp"
|
||||
"github.com/tidwall/sjson"
|
||||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
func appendJSONString(b []byte, s string) []byte {
|
||||
|
@ -191,8 +193,8 @@ func (s *Server) cmdJget(msg *Message) (resp.Value, error) {
|
|||
}
|
||||
return NOMessage, errKeyNotFound
|
||||
}
|
||||
o, _, _, ok := col.Get(id)
|
||||
if !ok {
|
||||
o := col.Get(id)
|
||||
if o == nil {
|
||||
if msg.OutputType == RESP {
|
||||
return resp.NullValue(), nil
|
||||
}
|
||||
|
@ -200,9 +202,9 @@ func (s *Server) cmdJget(msg *Message) (resp.Value, error) {
|
|||
}
|
||||
var res gjson.Result
|
||||
if doget {
|
||||
res = gjson.Get(o.String(), path)
|
||||
res = gjson.Get(o.Geo().String(), path)
|
||||
} else {
|
||||
res = gjson.Parse(o.String())
|
||||
res = gjson.Parse(o.Geo().String())
|
||||
}
|
||||
var val string
|
||||
if raw {
|
||||
|
@ -270,10 +272,12 @@ func (s *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
}
|
||||
var json string
|
||||
var geoobj bool
|
||||
o, fields, _, ok := col.Get(id)
|
||||
if ok {
|
||||
geoobj = objIsSpatial(o)
|
||||
json = o.String()
|
||||
var fields field.List
|
||||
o := col.Get(id)
|
||||
if o != nil {
|
||||
geoobj = objIsSpatial(o.Geo())
|
||||
json = o.Geo().String()
|
||||
fields = o.Fields()
|
||||
}
|
||||
if raw {
|
||||
// set as raw block
|
||||
|
@ -295,14 +299,15 @@ func (s *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
if createcol {
|
||||
s.cols.Set(key, col)
|
||||
}
|
||||
var oobj geojson.Object = collection.String(json)
|
||||
obj := object.New(id, oobj, 0, 0, fields)
|
||||
col.Set(obj)
|
||||
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.obj = collection.String(json)
|
||||
d.obj = obj
|
||||
d.timestamp = time.Now()
|
||||
d.updated = true
|
||||
|
||||
col.Set(d.id, d.obj, fields, 0)
|
||||
switch msg.OutputType {
|
||||
case JSON:
|
||||
var buf bytes.Buffer
|
||||
|
@ -335,10 +340,12 @@ func (s *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
|
||||
var json string
|
||||
var geoobj bool
|
||||
o, fields, _, ok := col.Get(id)
|
||||
if ok {
|
||||
geoobj = objIsSpatial(o)
|
||||
json = o.String()
|
||||
var fields field.List
|
||||
o := col.Get(id)
|
||||
if o != nil {
|
||||
geoobj = objIsSpatial(o.Geo())
|
||||
json = o.Geo().String()
|
||||
fields = o.Fields()
|
||||
}
|
||||
njson, err := sjson.Delete(json, path)
|
||||
if err != nil {
|
||||
|
@ -361,12 +368,14 @@ func (s *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err er
|
|||
return s.cmdSET(&nmsg)
|
||||
}
|
||||
|
||||
var oobj geojson.Object = collection.String(json)
|
||||
obj := object.New(id, oobj, 0, 0, fields)
|
||||
col.Set(obj)
|
||||
|
||||
d.key = key
|
||||
d.id = id
|
||||
d.obj = collection.String(json)
|
||||
d.obj = obj
|
||||
d.timestamp = time.Now()
|
||||
d.updated = true
|
||||
col.Set(d.id, d.obj, fields, 0)
|
||||
switch msg.OutputType {
|
||||
case JSON:
|
||||
var buf bytes.Buffer
|
||||
|
|
|
@ -5,9 +5,8 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/resp"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
func (s *Server) cmdScanArgs(vs []string) (
|
||||
|
@ -70,11 +69,9 @@ func (s *Server) cmdScan(msg *Message) (res resp.Value, err error) {
|
|||
if limits[0] == "" && limits[1] == "" {
|
||||
sw.col.Scan(args.desc, sw,
|
||||
msg.Deadline,
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
obj: o,
|
||||
})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
|
@ -86,11 +83,9 @@ func (s *Server) cmdScan(msg *Message) (res resp.Value, err error) {
|
|||
} else {
|
||||
sw.col.ScanRange(limits[0], limits[1], args.desc, sw,
|
||||
msg.Deadline,
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
obj: o,
|
||||
})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/glob"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
const limitItems = 100
|
||||
|
@ -60,10 +61,8 @@ type scanWriter struct {
|
|||
}
|
||||
|
||||
type ScanWriterParams struct {
|
||||
id string
|
||||
o geojson.Object
|
||||
fields field.List
|
||||
distance float64
|
||||
obj *object.Object
|
||||
dist float64
|
||||
distOutput bool // query or fence requested distance output
|
||||
noTest bool
|
||||
ignoreGlobMatch bool
|
||||
|
@ -201,34 +200,37 @@ func extractZCoordinate(o geojson.Object) float64 {
|
|||
}
|
||||
}
|
||||
|
||||
func getFieldValue(o geojson.Object, fields field.List, name string) field.Value {
|
||||
func getFieldValue(o *object.Object, name string) field.Value {
|
||||
if name == "z" {
|
||||
return field.ValueOf(strconv.FormatFloat(extractZCoordinate(o), 'f', -1, 64))
|
||||
z := extractZCoordinate(o.Geo())
|
||||
return field.ValueOf(strconv.FormatFloat(z, 'f', -1, 64))
|
||||
}
|
||||
f := fields.Get(name)
|
||||
return f.Value()
|
||||
return o.Fields().Get(name).Value()
|
||||
}
|
||||
|
||||
func (sw *scanWriter) fieldMatch(o geojson.Object, fields field.List) (bool, error) {
|
||||
func (sw *scanWriter) fieldMatch(o *object.Object) (bool, error) {
|
||||
for _, where := range sw.wheres {
|
||||
if !where.match(getFieldValue(o, fields, where.name)) {
|
||||
if !where.match(getFieldValue(o, where.name)) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
for _, wherein := range sw.whereins {
|
||||
if !wherein.match(getFieldValue(o, fields, wherein.name)) {
|
||||
if !wherein.match(getFieldValue(o, wherein.name)) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if len(sw.whereevals) > 0 {
|
||||
fieldsWithNames := make(map[string]field.Value)
|
||||
fieldsWithNames["z"] = field.ValueOf(strconv.FormatFloat(extractZCoordinate(o), 'f', -1, 64))
|
||||
fields.Scan(func(f field.Field) bool {
|
||||
fieldsWithNames[f.Name()] = f.Value()
|
||||
fieldNames := make(map[string]field.Value)
|
||||
if objIsSpatial(o.Geo()) {
|
||||
z := extractZCoordinate(o.Geo())
|
||||
fieldNames["z"] = field.ValueOf(strconv.FormatFloat(z, 'f', -1, 64))
|
||||
}
|
||||
o.Fields().Scan(func(f field.Field) bool {
|
||||
fieldNames[f.Name()] = f.Value()
|
||||
return true
|
||||
})
|
||||
for _, whereval := range sw.whereevals {
|
||||
match, err := whereval.match(fieldsWithNames)
|
||||
match, err := whereval.match(fieldNames)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -240,7 +242,7 @@ func (sw *scanWriter) fieldMatch(o geojson.Object, fields field.List) (bool, err
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (sw *scanWriter) globMatch(id string, o geojson.Object) (ok, keepGoing bool) {
|
||||
func (sw *scanWriter) globMatch(o *object.Object) (ok, keepGoing bool) {
|
||||
if sw.globEverything {
|
||||
return true, true
|
||||
}
|
||||
|
@ -248,7 +250,7 @@ func (sw *scanWriter) globMatch(id string, o geojson.Object) (ok, keepGoing bool
|
|||
if sw.matchValues {
|
||||
val = o.String()
|
||||
} else {
|
||||
val = id
|
||||
val = o.ID()
|
||||
}
|
||||
for _, pattern := range sw.globs {
|
||||
ok, _ := glob.Match(pattern, val)
|
||||
|
@ -270,13 +272,13 @@ func (sw *scanWriter) Step(n uint64) {
|
|||
|
||||
// ok is whether the object passes the test and should be written
|
||||
// keepGoing is whether there could be more objects to test
|
||||
func (sw *scanWriter) testObject(id string, o geojson.Object, fields field.List,
|
||||
func (sw *scanWriter) testObject(o *object.Object,
|
||||
) (ok, keepGoing bool, err error) {
|
||||
match, kg := sw.globMatch(id, o)
|
||||
match, kg := sw.globMatch(o)
|
||||
if !match {
|
||||
return false, kg, nil
|
||||
}
|
||||
ok, err = sw.fieldMatch(o, fields)
|
||||
ok, err = sw.fieldMatch(o)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
@ -288,7 +290,7 @@ func (sw *scanWriter) pushObject(opts ScanWriterParams) (keepGoing bool, err err
|
|||
if !opts.noTest {
|
||||
var ok bool
|
||||
var err error
|
||||
ok, keepGoing, err = sw.testObject(opts.id, opts.o, opts.fields)
|
||||
ok, keepGoing, err = sw.testObject(opts.obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -301,10 +303,17 @@ func (sw *scanWriter) pushObject(opts ScanWriterParams) (keepGoing bool, err err
|
|||
return sw.count < sw.limit, nil
|
||||
}
|
||||
if opts.clip != nil {
|
||||
opts.o = clip.Clip(opts.o, opts.clip, &sw.s.geomIndexOpts)
|
||||
// create a newly clipped object
|
||||
opts.obj = object.New(
|
||||
opts.obj.ID(),
|
||||
clip.Clip(opts.obj.Geo(), opts.clip, &sw.s.geomIndexOpts),
|
||||
0, opts.obj.Expires(),
|
||||
opts.obj.Fields(),
|
||||
)
|
||||
}
|
||||
|
||||
if !sw.fullFields {
|
||||
opts.fields.Scan(func(f field.Field) bool {
|
||||
opts.obj.Fields().Scan(func(f field.Field) bool {
|
||||
sw.fkeys.Insert(f.Name())
|
||||
return true
|
||||
})
|
||||
|
@ -339,10 +348,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
}
|
||||
fieldsOutput := sw.hasFieldsOutput()
|
||||
if fieldsOutput && sw.fullFields {
|
||||
if opts.fields.Len() > 0 {
|
||||
if opts.obj.Fields().Len() > 0 {
|
||||
jsfields = `,"fields":{`
|
||||
var i int
|
||||
opts.fields.Scan(func(f field.Field) bool {
|
||||
opts.obj.Fields().Scan(func(f field.Field) bool {
|
||||
if !f.Value().IsZero() {
|
||||
if i > 0 {
|
||||
jsfields += `,`
|
||||
|
@ -361,7 +370,7 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
if i > 0 {
|
||||
jsfields += `,`
|
||||
}
|
||||
f := opts.fields.Get(name)
|
||||
f := opts.obj.Fields().Get(name)
|
||||
jsfields += f.Value().JSON()
|
||||
i++
|
||||
return true
|
||||
|
@ -369,29 +378,29 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
jsfields += `]`
|
||||
}
|
||||
if sw.output == outputIDs {
|
||||
if opts.distOutput || opts.distance > 0 {
|
||||
wr.WriteString(`{"id":` + jsonString(opts.id) +
|
||||
`,"distance":` + strconv.FormatFloat(opts.distance, 'f', -1, 64) + "}")
|
||||
if opts.distOutput || opts.dist > 0 {
|
||||
wr.WriteString(`{"id":` + jsonString(opts.obj.ID()) +
|
||||
`,"distance":` + strconv.FormatFloat(opts.dist, 'f', -1, 64) + "}")
|
||||
} else {
|
||||
wr.WriteString(jsonString(opts.id))
|
||||
wr.WriteString(jsonString(opts.obj.ID()))
|
||||
}
|
||||
} else {
|
||||
wr.WriteString(`{"id":` + jsonString(opts.id))
|
||||
wr.WriteString(`{"id":` + jsonString(opts.obj.ID()))
|
||||
switch sw.output {
|
||||
case outputObjects:
|
||||
wr.WriteString(`,"object":` + string(opts.o.AppendJSON(nil)))
|
||||
wr.WriteString(`,"object":` + string(opts.obj.Geo().AppendJSON(nil)))
|
||||
case outputPoints:
|
||||
wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.o)))
|
||||
wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.obj.Geo())))
|
||||
case outputHashes:
|
||||
center := opts.o.Center()
|
||||
center := opts.obj.Geo().Center()
|
||||
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
|
||||
wr.WriteString(`,"hash":"` + p + `"`)
|
||||
case outputBounds:
|
||||
wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.o)))
|
||||
wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.obj.Geo())))
|
||||
}
|
||||
wr.WriteString(jsfields)
|
||||
if opts.distOutput || opts.distance > 0 {
|
||||
wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.distance, 'f', -1, 64))
|
||||
if opts.distOutput || opts.dist > 0 {
|
||||
wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.dist, 'f', -1, 64))
|
||||
}
|
||||
|
||||
wr.WriteString(`}`)
|
||||
|
@ -399,10 +408,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
sw.wr.Write(wr.Bytes())
|
||||
case RESP:
|
||||
vals := make([]resp.Value, 1, 3)
|
||||
vals[0] = resp.StringValue(opts.id)
|
||||
vals[0] = resp.StringValue(opts.obj.ID())
|
||||
if sw.output == outputIDs {
|
||||
if opts.distOutput || opts.distance > 0 {
|
||||
vals = append(vals, resp.FloatValue(opts.distance))
|
||||
if opts.distOutput || opts.dist > 0 {
|
||||
vals = append(vals, resp.FloatValue(opts.dist))
|
||||
sw.values = append(sw.values, resp.ArrayValue(vals))
|
||||
} else {
|
||||
sw.values = append(sw.values, vals[0])
|
||||
|
@ -410,10 +419,10 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
} else {
|
||||
switch sw.output {
|
||||
case outputObjects:
|
||||
vals = append(vals, resp.StringValue(opts.o.String()))
|
||||
vals = append(vals, resp.StringValue(opts.obj.String()))
|
||||
case outputPoints:
|
||||
point := opts.o.Center()
|
||||
z := extractZCoordinate(opts.o)
|
||||
point := opts.obj.Geo().Center()
|
||||
z := extractZCoordinate(opts.obj.Geo())
|
||||
if z != 0 {
|
||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||
resp.FloatValue(point.Y),
|
||||
|
@ -427,11 +436,11 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
}))
|
||||
}
|
||||
case outputHashes:
|
||||
center := opts.o.Center()
|
||||
center := opts.obj.Geo().Center()
|
||||
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
|
||||
vals = append(vals, resp.StringValue(p))
|
||||
case outputBounds:
|
||||
bbox := opts.o.Rect()
|
||||
bbox := opts.obj.Rect()
|
||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||
resp.ArrayValue([]resp.Value{
|
||||
resp.FloatValue(bbox.Min.Y),
|
||||
|
@ -444,23 +453,22 @@ func (sw *scanWriter) writeFilled(opts ScanWriterParams) {
|
|||
}))
|
||||
}
|
||||
if sw.hasFieldsOutput() {
|
||||
if opts.fields.Len() > 0 {
|
||||
var fvals []resp.Value
|
||||
var i int
|
||||
opts.fields.Scan(func(f field.Field) bool {
|
||||
if !f.Value().IsZero() {
|
||||
fvals = append(fvals, resp.StringValue(f.Name()), resp.StringValue(f.Value().Data()))
|
||||
i++
|
||||
}
|
||||
return true
|
||||
})
|
||||
var fvals []resp.Value
|
||||
var i int
|
||||
opts.obj.Fields().Scan(func(f field.Field) bool {
|
||||
if !f.Value().IsZero() {
|
||||
fvals = append(fvals, resp.StringValue(f.Name()), resp.StringValue(f.Value().Data()))
|
||||
i++
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(fvals) > 0 {
|
||||
vals = append(vals, resp.ArrayValue(fvals))
|
||||
}
|
||||
}
|
||||
if opts.distOutput || opts.distance > 0 {
|
||||
vals = append(vals, resp.FloatValue(opts.distance))
|
||||
if opts.distOutput || opts.dist > 0 {
|
||||
vals = append(vals, resp.FloatValue(opts.dist))
|
||||
}
|
||||
|
||||
sw.values = append(sw.values, resp.ArrayValue(vals))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/tidwall/geojson"
|
||||
"github.com/tidwall/geojson/geometry"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
type testPointItem struct {
|
||||
|
@ -47,7 +48,7 @@ func BenchmarkFieldMatch(t *testing.B) {
|
|||
for i := 0; i < t.N; i++ {
|
||||
// one call is super fast, measurements are not reliable, let's do 100
|
||||
for ix := 0; ix < 100; ix++ {
|
||||
sw.fieldMatch(items[i].object, items[i].fields)
|
||||
sw.fieldMatch(object.New("", items[i].object, 0, 0, items[i].fields))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ import (
|
|||
"github.com/tidwall/tile38/internal/bing"
|
||||
"github.com/tidwall/tile38/internal/buffer"
|
||||
"github.com/tidwall/tile38/internal/clip"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/glob"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
const defaultCircleSteps = 64
|
||||
|
@ -376,11 +376,12 @@ func (s *Server) cmdSearchArgs(
|
|||
err = errKeyNotFound
|
||||
return
|
||||
}
|
||||
lfs.obj, _, _, ok = col.Get(id)
|
||||
if !ok {
|
||||
o := col.Get(id)
|
||||
if o == nil {
|
||||
err = errIDNotFound
|
||||
return
|
||||
}
|
||||
lfs.obj = o.Geo()
|
||||
case "roam":
|
||||
lfs.roam.on = true
|
||||
if vs, lfs.roam.key, ok = tokenval(vs); !ok || lfs.roam.key == "" {
|
||||
|
@ -499,12 +500,10 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) {
|
|||
}
|
||||
var ierr error
|
||||
if sw.col != nil {
|
||||
iterStep := func(id string, o geojson.Object, fields field.List, meters float64) bool {
|
||||
iterStep := func(o *object.Object, dist float64) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
distance: meters,
|
||||
obj: o,
|
||||
dist: dist,
|
||||
distOutput: sargs.distance,
|
||||
ignoreGlobMatch: true,
|
||||
skipTesting: true,
|
||||
|
@ -523,16 +522,16 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) {
|
|||
errors.New("cannot use SPARSE without a point distance")
|
||||
}
|
||||
// An intersects operation is required for SPARSE
|
||||
iter := func(id string, o geojson.Object, fields field.List) bool {
|
||||
var meters float64
|
||||
iter := func(o *object.Object) bool {
|
||||
var dist float64
|
||||
if sargs.distance {
|
||||
meters = o.Distance(sargs.obj)
|
||||
dist = o.Geo().Distance(sargs.obj)
|
||||
}
|
||||
return iterStep(id, o, fields, meters)
|
||||
return iterStep(o, dist)
|
||||
}
|
||||
sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline, iter)
|
||||
} else {
|
||||
iter := func(id string, o geojson.Object, fields field.List, dist float64) bool {
|
||||
iter := func(o *object.Object, dist float64) bool {
|
||||
if maxDist > 0 && dist > maxDist {
|
||||
return false
|
||||
}
|
||||
|
@ -540,7 +539,7 @@ func (s *Server) cmdNearby(msg *Message) (res resp.Value, err error) {
|
|||
if sargs.distance {
|
||||
meters = dist
|
||||
}
|
||||
return iterStep(id, o, fields, meters)
|
||||
return iterStep(o, meters)
|
||||
}
|
||||
sw.col.Nearby(sargs.obj, sw, msg.Deadline, iter)
|
||||
}
|
||||
|
@ -599,41 +598,31 @@ func (s *Server) cmdWITHINorINTERSECTS(cmd string, msg *Message) (res resp.Value
|
|||
var ierr error
|
||||
if sw.col != nil {
|
||||
if cmd == "within" {
|
||||
sw.col.Within(sargs.obj, sargs.sparse, sw, msg.Deadline, func(
|
||||
id string, o geojson.Object, fields field.List,
|
||||
) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
return false
|
||||
}
|
||||
return keepGoing
|
||||
})
|
||||
sw.col.Within(sargs.obj, sargs.sparse, sw, msg.Deadline,
|
||||
func(o *object.Object) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{obj: o})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
return false
|
||||
}
|
||||
return keepGoing
|
||||
},
|
||||
)
|
||||
} else if cmd == "intersects" {
|
||||
sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline, func(
|
||||
id string,
|
||||
o geojson.Object,
|
||||
fields field.List,
|
||||
) bool {
|
||||
params := ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
}
|
||||
if sargs.clip {
|
||||
params.clip = sargs.obj
|
||||
}
|
||||
keepGoing, err := sw.pushObject(params)
|
||||
if err != nil {
|
||||
ierr = err
|
||||
return false
|
||||
}
|
||||
return keepGoing
|
||||
})
|
||||
sw.col.Intersects(sargs.obj, sargs.sparse, sw, msg.Deadline,
|
||||
func(o *object.Object) bool {
|
||||
params := ScanWriterParams{obj: o}
|
||||
if sargs.clip {
|
||||
params.clip = sargs.obj
|
||||
}
|
||||
keepGoing, err := sw.pushObject(params)
|
||||
if err != nil {
|
||||
ierr = err
|
||||
return false
|
||||
}
|
||||
return keepGoing
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
if ierr != nil {
|
||||
|
@ -732,11 +721,9 @@ func (s *Server) cmdSearch(msg *Message) (res resp.Value, err error) {
|
|||
limits := multiGlobParse(sw.globs, sargs.desc)
|
||||
if limits[0] == "" && limits[1] == "" {
|
||||
sw.col.SearchValues(sargs.desc, sw, msg.Deadline,
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
obj: o,
|
||||
})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
|
@ -750,11 +737,9 @@ func (s *Server) cmdSearch(msg *Message) (res resp.Value, err error) {
|
|||
// globSingle is only for ID matches, not values.
|
||||
sw.col.SearchValuesRange(limits[0], limits[1], sargs.desc, sw,
|
||||
msg.Deadline,
|
||||
func(id string, o geojson.Object, fields field.List) bool {
|
||||
func(o *object.Object) bool {
|
||||
keepGoing, err := sw.pushObject(ScanWriterParams{
|
||||
id: id,
|
||||
o: o,
|
||||
fields: fields,
|
||||
obj: o,
|
||||
})
|
||||
if err != nil {
|
||||
ierr = err
|
||||
|
|
|
@ -36,8 +36,8 @@ import (
|
|||
"github.com/tidwall/tile38/internal/collection"
|
||||
"github.com/tidwall/tile38/internal/deadline"
|
||||
"github.com/tidwall/tile38/internal/endpoint"
|
||||
"github.com/tidwall/tile38/internal/field"
|
||||
"github.com/tidwall/tile38/internal/log"
|
||||
"github.com/tidwall/tile38/internal/object"
|
||||
)
|
||||
|
||||
var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'")
|
||||
|
@ -55,14 +55,11 @@ const (
|
|||
// for geofence formulas.
|
||||
type commandDetails struct {
|
||||
command string // client command, like "SET" or "DEL"
|
||||
key, id string // collection key and object id of object
|
||||
key string // collection key
|
||||
newKey string // new key, for RENAME command
|
||||
|
||||
obj geojson.Object // new object
|
||||
fields field.List // array of field values
|
||||
|
||||
oldObj geojson.Object // previous object, if any
|
||||
oldFields field.List // previous object field values
|
||||
obj *object.Object // target object
|
||||
old *object.Object // previous object, if any
|
||||
|
||||
updated bool // object was updated
|
||||
timestamp time.Time // timestamp when the update occured
|
||||
|
|
|
@ -278,11 +278,12 @@ func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Ob
|
|||
err = errKeyNotFound
|
||||
return
|
||||
}
|
||||
o, _, _, ok = col.Get(id)
|
||||
if !ok {
|
||||
obj := col.Get(id)
|
||||
if obj == nil {
|
||||
err = errIDNotFound
|
||||
return
|
||||
}
|
||||
o = obj.Geo()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ func keys_KNN_basic_test(mc *mockServer) error {
|
|||
{"NEARBY", "mykey", "LIMIT", 10, "POINTS", "POINT", 20, 20}, {
|
||||
"[0 [[2 [19 19]] [3 [12 19]] [5 [33 21]] [1 [5 5]] [4 [-5 5]] [6 [52 13]]]]"},
|
||||
{"NEARBY", "mykey", "LIMIT", 10, "IDS", "POINT", 20, 20, 4000000}, {"[0 [2 3 5 1 4 6]]"},
|
||||
{"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "IDS", "POINT", 20, 20, 1500000}, {"[0 [[2 152808.671875] [3 895945.125] [5 1448929.625]]]"},
|
||||
{"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "IDS", "POINT", 20, 20, 1500000}, {"[0 [[2 152808.67164037024] [3 895945.1409106688] [5 1448929.5916252395]]]"},
|
||||
{"NEARBY", "mykey", "LIMIT", 10, "DISTANCE", "POINT", 52, 13, 100}, {`[0 [[6 {"type":"Point","coordinates":[13,52]} 0]]]`},
|
||||
{"NEARBY", "mykey", "LIMIT", 10, "POINT", 52.1, 13.1, 100000}, {`[0 [[6 {"type":"Point","coordinates":[13,52]}]]]`},
|
||||
{"OUTPUT", "json"}, {func(res string) bool { return gjson.Get(res, "ok").Bool() }},
|
||||
|
|
Loading…
Reference in New Issue