mirror of https://github.com/tidwall/tile38.git
445 lines
10 KiB
Go
445 lines
10 KiB
Go
package rtree
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tidwall/geojson"
|
|
"github.com/tidwall/geojson/geometry"
|
|
"github.com/tidwall/tile38/internal/collection/item"
|
|
)
|
|
|
|
type tBox struct {
|
|
min [dims]float64
|
|
max [dims]float64
|
|
}
|
|
|
|
var boxes []*item.Item
|
|
var points []*item.Item
|
|
|
|
func init() {
|
|
seed := time.Now().UnixNano()
|
|
// seed = 1532132365683340889
|
|
println("seed:", seed)
|
|
rand.Seed(seed)
|
|
}
|
|
|
|
func boxMin(box *item.Item) []float64 {
|
|
return box.Obj().(*tBox).min[:]
|
|
}
|
|
func boxMax(box *item.Item) []float64 {
|
|
return box.Obj().(*tBox).max[:]
|
|
}
|
|
|
|
func randPoints(N int) []*item.Item {
|
|
boxes := make([]*item.Item, N)
|
|
for i := 0; i < N; i++ {
|
|
box := new(tBox)
|
|
box.min[0] = rand.Float64()*360 - 180
|
|
box.min[1] = rand.Float64()*180 - 90
|
|
for j := 2; j < dims; j++ {
|
|
box.min[j] = rand.Float64()
|
|
}
|
|
box.max = box.min
|
|
boxes[i] = item.New(fmt.Sprintf("%d", i), box, false)
|
|
}
|
|
return boxes
|
|
}
|
|
|
|
func randBoxes(N int) []*item.Item {
|
|
boxes := make([]*item.Item, N)
|
|
for i := 0; i < N; i++ {
|
|
box := new(tBox)
|
|
box.min[0] = rand.Float64()*360 - 180
|
|
box.min[1] = rand.Float64()*180 - 90
|
|
for j := 2; j < dims; j++ {
|
|
box.min[j] = rand.Float64() * 100
|
|
}
|
|
box.max[0] = box.min[0] + rand.Float64()
|
|
box.max[1] = box.min[1] + rand.Float64()
|
|
for j := 2; j < dims; j++ {
|
|
box.max[j] = box.min[j] + rand.Float64()
|
|
}
|
|
if box.max[0] > 180 || box.max[1] > 90 {
|
|
i--
|
|
}
|
|
boxes[i] = item.New(fmt.Sprintf("%d", i), box, false)
|
|
}
|
|
return boxes
|
|
}
|
|
|
|
func sortBoxes(boxes []*item.Item) {
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
iMin, iMax := boxMin(boxes[i]), boxMax(boxes[i])
|
|
jMin, jMax := boxMin(boxes[j]), boxMax(boxes[j])
|
|
for k := 0; k < len(iMin); k++ {
|
|
if iMin[k] < jMin[k] {
|
|
return true
|
|
}
|
|
if iMin[k] > jMin[k] {
|
|
return false
|
|
}
|
|
if iMax[k] < jMax[k] {
|
|
return true
|
|
}
|
|
if iMax[k] > jMax[k] {
|
|
return false
|
|
}
|
|
}
|
|
return i < j
|
|
})
|
|
}
|
|
|
|
func sortBoxesNearby(boxes []*item.Item, min, max []float64) {
|
|
sort.Slice(boxes, func(i, j int) bool {
|
|
return testBoxDist(boxMin(boxes[i]), boxMax(boxes[i]), min, max) <
|
|
testBoxDist(boxMin(boxes[j]), boxMax(boxes[j]), min, max)
|
|
})
|
|
}
|
|
|
|
func testBoxDist(amin, amax, bmin, bmax []float64) float64 {
|
|
var dist float64
|
|
for i := 0; i < len(amin); i++ {
|
|
var min, max float64
|
|
if amin[i] > bmin[i] {
|
|
min = amin[i]
|
|
} else {
|
|
min = bmin[i]
|
|
}
|
|
if amax[i] < bmax[i] {
|
|
max = amax[i]
|
|
} else {
|
|
max = bmax[i]
|
|
}
|
|
squared := min - max
|
|
if squared > 0 {
|
|
dist += squared * squared
|
|
}
|
|
}
|
|
return dist
|
|
}
|
|
|
|
func testBoxesVarious(t *testing.T, items []*item.Item, label string) {
|
|
N := len(boxes)
|
|
|
|
var tr BoxTree
|
|
|
|
// N := 10000
|
|
// boxes := randPoints(N)
|
|
|
|
/////////////////////////////////////////
|
|
// insert
|
|
/////////////////////////////////////////
|
|
for i := 0; i < N; i++ {
|
|
tr.Insert(boxMin(boxes[i]), boxMax(boxes[i]), boxes[i])
|
|
}
|
|
if tr.Count() != N {
|
|
t.Fatalf("expected %d, got %d", N, tr.Count())
|
|
}
|
|
// area := tr.TotalOverlapArea()
|
|
// fmt.Printf("overlap: %.0f, %.1f/item\n", area, area/float64(N))
|
|
|
|
// ioutil.WriteFile(label+".svg", []byte(rtreetools.SVG(&tr)), 0600)
|
|
|
|
/////////////////////////////////////////
|
|
// scan all items and count one-by-one
|
|
/////////////////////////////////////////
|
|
var count int
|
|
tr.Scan(func(min, max []float64, _ *item.Item) bool {
|
|
count++
|
|
return true
|
|
})
|
|
if count != N {
|
|
t.Fatalf("expected %d, got %d", N, count)
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// check every point for correctness
|
|
/////////////////////////////////////////
|
|
var tboxes1 []*item.Item
|
|
tr.Scan(func(min, max []float64, item *item.Item) bool {
|
|
tboxes1 = append(tboxes1, item)
|
|
return true
|
|
})
|
|
tboxes2 := make([]*item.Item, len(boxes))
|
|
copy(tboxes2, boxes)
|
|
sortBoxes(tboxes1)
|
|
sortBoxes(tboxes2)
|
|
for i := 0; i < len(tboxes1); i++ {
|
|
if tboxes1[i] != tboxes2[i] {
|
|
t.Fatalf("expected '%v', got '%v'", tboxes2[i], tboxes1[i])
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// search for each item one-by-one
|
|
/////////////////////////////////////////
|
|
for i := 0; i < N; i++ {
|
|
var found bool
|
|
tr.Search(boxMin(boxes[i]), boxMax(boxes[i]),
|
|
func(min, max []float64, v *item.Item) bool {
|
|
if v == boxes[i] {
|
|
found = true
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if !found {
|
|
t.Fatalf("did not find item %d", i)
|
|
}
|
|
}
|
|
|
|
centerMin, centerMax := []float64{-18, -9}, []float64{18, 9}
|
|
for j := 2; j < dims; j++ {
|
|
centerMin = append(centerMin, -10)
|
|
centerMax = append(centerMax, 10)
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// search for 10% of the items
|
|
/////////////////////////////////////////
|
|
for i := 0; i < N/5; i++ {
|
|
var count int
|
|
tr.Search(centerMin, centerMax,
|
|
func(min, max []float64, _ *item.Item) bool {
|
|
count++
|
|
return true
|
|
},
|
|
)
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// delete every other item
|
|
/////////////////////////////////////////
|
|
for i := 0; i < N/2; i++ {
|
|
j := i * 2
|
|
tr.Delete(boxMin(boxes[j]), boxMax(boxes[j]), boxes[j])
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// count all items. should be half of N
|
|
/////////////////////////////////////////
|
|
count = 0
|
|
tr.Scan(func(min, max []float64, _ *item.Item) bool {
|
|
count++
|
|
return true
|
|
})
|
|
if count != N/2 {
|
|
t.Fatalf("expected %d, got %d", N/2, count)
|
|
}
|
|
|
|
///////////////////////////////////////////////////
|
|
// reinsert every other item, but in random order
|
|
///////////////////////////////////////////////////
|
|
var ij []int
|
|
for i := 0; i < N/2; i++ {
|
|
j := i * 2
|
|
ij = append(ij, j)
|
|
}
|
|
rand.Shuffle(len(ij), func(i, j int) {
|
|
ij[i], ij[j] = ij[j], ij[i]
|
|
})
|
|
for i := 0; i < N/2; i++ {
|
|
j := ij[i]
|
|
tr.Insert(boxMin(boxes[j]), boxMax(boxes[j]), boxes[j])
|
|
}
|
|
|
|
//////////////////////////////////////////////////////
|
|
// replace each item with an item that is very close
|
|
//////////////////////////////////////////////////////
|
|
var nboxes = make([]*item.Item, N)
|
|
for i := 0; i < N; i++ {
|
|
box := boxes[i].Obj().(*tBox)
|
|
nbox := new(tBox)
|
|
for j := 0; j < len(box.min); j++ {
|
|
nbox.min[j] = box.min[j] + (rand.Float64() - 0.5)
|
|
if box.min == box.max {
|
|
nbox.max[j] = nbox.min[j]
|
|
} else {
|
|
nbox.max[j] = box.max[j] + (rand.Float64() - 0.5)
|
|
}
|
|
}
|
|
nboxes[i] = item.New(fmt.Sprintf("%d", i), nbox, false)
|
|
}
|
|
for i := 0; i < N; i++ {
|
|
tr.Insert(boxMin(nboxes[i]), boxMax(nboxes[i]), nboxes[i])
|
|
tr.Delete(boxMin(boxes[i]), boxMax(boxes[i]), boxes[i])
|
|
}
|
|
if tr.Count() != N {
|
|
t.Fatalf("expected %d, got %d", N, tr.Count())
|
|
}
|
|
// area = tr.TotalOverlapArea()
|
|
// fmt.Fprintf(wr, "overlap: %.0f, %.1f/item\n", area, area/float64(N))
|
|
|
|
/////////////////////////////////////////
|
|
// check every point for correctness
|
|
/////////////////////////////////////////
|
|
tboxes1 = nil
|
|
tr.Scan(func(min, max []float64, value *item.Item) bool {
|
|
tboxes1 = append(tboxes1, value)
|
|
return true
|
|
})
|
|
tboxes2 = make([]*item.Item, len(nboxes))
|
|
copy(tboxes2, nboxes)
|
|
sortBoxes(tboxes1)
|
|
sortBoxes(tboxes2)
|
|
for i := 0; i < len(tboxes1); i++ {
|
|
if tboxes1[i] != tboxes2[i] {
|
|
t.Fatalf("expected '%v', got '%v'", tboxes2[i], tboxes1[i])
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////
|
|
// search for 10% of the items
|
|
/////////////////////////////////////////
|
|
for i := 0; i < N/5; i++ {
|
|
var count int
|
|
tr.Search(centerMin, centerMax,
|
|
func(min, max []float64, value *item.Item) bool {
|
|
count++
|
|
return true
|
|
},
|
|
)
|
|
}
|
|
|
|
var boxes3 []*item.Item
|
|
tr.Nearby(centerMin, centerMax,
|
|
func(min, max []float64, value *item.Item) bool {
|
|
boxes3 = append(boxes3, value)
|
|
return true
|
|
},
|
|
)
|
|
if len(boxes3) != len(nboxes) {
|
|
t.Fatalf("expected %d, got %d", len(nboxes), len(boxes3))
|
|
}
|
|
if len(boxes3) != tr.Count() {
|
|
t.Fatalf("expected %d, got %d", tr.Count(), len(boxes3))
|
|
}
|
|
var ldist float64
|
|
for i, box := range boxes3 {
|
|
dist := testBoxDist(boxMin(box), boxMax(box), centerMin, centerMax)
|
|
if i > 0 && dist < ldist {
|
|
t.Fatalf("out of order")
|
|
}
|
|
ldist = dist
|
|
}
|
|
}
|
|
|
|
func TestRandomBoxes(t *testing.T) {
|
|
testBoxesVarious(t, randBoxes(10000), "boxes")
|
|
}
|
|
|
|
func TestRandomPoints(t *testing.T) {
|
|
testBoxesVarious(t, randPoints(10000), "points")
|
|
}
|
|
|
|
func (r *box) boxstr() string {
|
|
var b []byte
|
|
b = append(b, '[', '[')
|
|
for i := 0; i < len(r.min); i++ {
|
|
if i != 0 {
|
|
b = append(b, ' ')
|
|
}
|
|
b = strconv.AppendFloat(b, r.min[i], 'f', -1, 64)
|
|
}
|
|
b = append(b, ']', '[')
|
|
for i := 0; i < len(r.max); i++ {
|
|
if i != 0 {
|
|
b = append(b, ' ')
|
|
}
|
|
b = strconv.AppendFloat(b, r.max[i], 'f', -1, 64)
|
|
}
|
|
b = append(b, ']', ']')
|
|
return string(b)
|
|
}
|
|
|
|
func (r *box) print(height, indent int) {
|
|
fmt.Printf("%s%s", strings.Repeat(" ", indent), r.boxstr())
|
|
if height == 0 {
|
|
fmt.Printf("\t'%v'\n", r.data)
|
|
} else {
|
|
fmt.Printf("\n")
|
|
for i := 0; i < (*node)(r.data).count; i++ {
|
|
(*node)(r.data).boxes[i].print(height-1, indent+1)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (tr BoxTree) print() {
|
|
if tr.root.data == nil {
|
|
println("EMPTY TREE")
|
|
return
|
|
}
|
|
tr.root.print(tr.height+1, 0)
|
|
}
|
|
|
|
func TestZeroPoints(t *testing.T) {
|
|
N := 10000
|
|
var tr BoxTree
|
|
pt := make([]float64, dims)
|
|
for i := 0; i < N; i++ {
|
|
tr.Insert(pt, nil, nil)
|
|
}
|
|
}
|
|
|
|
func BenchmarkRandomInsert(b *testing.B) {
|
|
var tr BoxTree
|
|
boxes := randBoxes(b.N)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
tr.Insert(boxMin(boxes[i]), boxMax(boxes[i]), nil)
|
|
}
|
|
}
|
|
|
|
func (s *tBox) Spatial() geojson.Spatial {
|
|
return geojson.EmptySpatial{}
|
|
}
|
|
func (s *tBox) ForEach(iter func(geom geojson.Object) bool) bool {
|
|
return iter(s)
|
|
}
|
|
func (s *tBox) Empty() bool {
|
|
return true
|
|
}
|
|
func (s *tBox) Valid() bool {
|
|
return false
|
|
}
|
|
func (s *tBox) Rect() geometry.Rect {
|
|
return geometry.Rect{}
|
|
}
|
|
func (s *tBox) Center() geometry.Point {
|
|
return geometry.Point{}
|
|
}
|
|
func (s *tBox) AppendJSON(dst []byte) []byte {
|
|
return nil
|
|
}
|
|
func (s *tBox) String() string {
|
|
return ""
|
|
}
|
|
func (s *tBox) JSON() string {
|
|
return string(s.AppendJSON(nil))
|
|
}
|
|
func (s *tBox) MarshalJSON() ([]byte, error) {
|
|
return s.AppendJSON(nil), nil
|
|
}
|
|
func (s *tBox) Within(obj geojson.Object) bool {
|
|
return false
|
|
}
|
|
func (s *tBox) Contains(obj geojson.Object) bool {
|
|
return false
|
|
}
|
|
func (s *tBox) Intersects(obj geojson.Object) bool {
|
|
return false
|
|
}
|
|
func (s *tBox) NumPoints() int {
|
|
return 0
|
|
}
|
|
func (s *tBox) Distance(obj geojson.Object) float64 {
|
|
return 0
|
|
}
|