package rbang import ( "fmt" "math/rand" "os" "sort" "strconv" "strings" "testing" "time" "github.com/tidwall/geoindex" "github.com/tidwall/lotsa" ) func TestBenchInsert2D(t *testing.T) { testBenchInsert(t, 100000, 2) } func testBenchInsert(t *testing.T, N, D int) { rand.Seed(time.Now().UnixNano()) points := make([]float64, N*D) for i := 0; i < N; i++ { for j := 0; j < D; j++ { points[i*D+j] = rand.Float64()*100 - 50 } } var tr RTree lotsa.Output = os.Stdout fmt.Printf("Insert(%dD): ", D) lotsa.Ops(N, 1, func(i, _ int) { point := [2]float64{points[i*D+0], points[i*D+1]} tr.Insert(point, point, i) }) fmt.Printf("Search(%dD): ", D) var count int lotsa.Ops(N, 1, func(i, _ int) { point := [2]float64{points[i*D+0], points[i*D+1]} tr.Search(point, point, func(min, max [2]float64, value interface{}) bool { count++ return true }, ) }) if count != N { t.Fatalf("expected %d, got %d", N, count) } fmt.Printf("Delete(%dD): ", D) lotsa.Ops(N, 1, func(i, _ int) { point := [2]float64{points[i*D+0], points[i*D+1]} tr.Delete(point, point, i) }) if tr.Len() != 0 { t.Fatalf("expected %d, got %d", N, tr.Len()) } } type tItem2 struct { point [2]float64 } func (item *tItem2) Point() (x, y float64) { return item.point[0], item.point[1] } func (item *tItem2) Rect() (minX, minY, maxX, maxY float64) { return item.point[0], item.point[1], item.point[0], item.point[1] } const dims = 2 type tBox struct { min [dims]float64 max [dims]float64 } var boxes []tBox var points []tBox func init() { seed := time.Now().UnixNano() seed = 1532132365683340889 println("seed:", seed) rand.Seed(seed) } func randPoints(N int) []tBox { boxes := make([]tBox, N) for i := 0; i < N; i++ { boxes[i].min[0] = rand.Float64()*360 - 180 boxes[i].min[1] = rand.Float64()*180 - 90 for j := 2; j < dims; j++ { boxes[i].min[j] = rand.Float64() } boxes[i].max = boxes[i].min } return boxes } func randBoxes(N int) []tBox { boxes := make([]tBox, N) for i := 0; i < N; i++ { boxes[i].min[0] = rand.Float64()*360 - 180 boxes[i].min[1] = rand.Float64()*180 - 90 for j := 2; j < dims; j++ { boxes[i].min[j] = rand.Float64() * 100 } boxes[i].max[0] = boxes[i].min[0] + rand.Float64() boxes[i].max[1] = boxes[i].min[1] + rand.Float64() for j := 2; j < dims; j++ { boxes[i].max[j] = boxes[i].min[j] + rand.Float64() } if boxes[i].max[0] > 180 || boxes[i].max[1] > 90 { i-- } } return boxes } func sortBoxes(boxes []tBox) { sort.Slice(boxes, func(i, j int) bool { for k := 0; k < len(boxes[i].min); k++ { if boxes[i].min[k] < boxes[j].min[k] { return true } if boxes[i].min[k] > boxes[j].min[k] { return false } if boxes[i].max[k] < boxes[j].max[k] { return true } if boxes[i].max[k] > boxes[j].max[k] { return false } } return i < j }) } func sortBoxesNearby(boxes []tBox, min, max [2]float64) { sort.Slice(boxes, func(i, j int) bool { return testBoxDist(boxes[i].min, boxes[i].max, min, max) < testBoxDist(boxes[j].min, boxes[j].max, min, max) }) } func testBoxDist(amin, amax, bmin, bmax [2]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, boxes []tBox, label string) { N := len(boxes) var tr RTree // N := 10000 // boxes := randPoints(N) ///////////////////////////////////////// // insert ///////////////////////////////////////// for i := 0; i < N; i++ { tr.Insert(boxes[i].min, boxes[i].max, boxes[i]) } if tr.Len() != N { t.Fatalf("expected %d, got %d", N, tr.Len()) } // 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 [2]float64, value interface{}) bool { count++ return true }) if count != N { t.Fatalf("expected %d, got %d", N, count) } ///////////////////////////////////////// // check every point for correctness ///////////////////////////////////////// var tboxes1 []tBox tr.Scan(func(min, max [2]float64, value interface{}) bool { tboxes1 = append(tboxes1, value.(tBox)) return true }) tboxes2 := make([]tBox, 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(boxes[i].min, boxes[i].max, func(min, max [2]float64, value interface{}) bool { if value == boxes[i] { found = true return false } return true }) if !found { t.Fatalf("did not find item %d", i) } } centerMin, centerMax := [2]float64{-18, -9}, [2]float64{18, 9} ///////////////////////////////////////// // search for 10% of the items ///////////////////////////////////////// for i := 0; i < N/5; i++ { var count int tr.Search(centerMin, centerMax, func(min, max [2]float64, value interface{}) bool { count++ return true }, ) } ///////////////////////////////////////// // delete every other item ///////////////////////////////////////// for i := 0; i < N/2; i++ { j := i * 2 tr.Delete(boxes[j].min, boxes[j].max, boxes[j]) } ///////////////////////////////////////// // count all items. should be half of N ///////////////////////////////////////// count = 0 tr.Scan(func(min, max [2]float64, value interface{}) 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(boxes[j].min, boxes[j].max, boxes[j]) } ////////////////////////////////////////////////////// // replace each item with an item that is very close ////////////////////////////////////////////////////// var nboxes = make([]tBox, N) for i := 0; i < N; i++ { for j := 0; j < len(boxes[i].min); j++ { nboxes[i].min[j] = boxes[i].min[j] + (rand.Float64() - 0.5) if boxes[i].min == boxes[i].max { nboxes[i].max[j] = nboxes[i].min[j] } else { nboxes[i].max[j] = boxes[i].max[j] + (rand.Float64() - 0.5) } } } for i := 0; i < N; i++ { tr.Insert(nboxes[i].min, nboxes[i].max, nboxes[i]) tr.Delete(boxes[i].min, boxes[i].max, boxes[i]) } if tr.Len() != N { t.Fatalf("expected %d, got %d", N, tr.Len()) } // 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 [2]float64, value interface{}) bool { tboxes1 = append(tboxes1, value.(tBox)) return true }) tboxes2 = make([]tBox, 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 [2]float64, value interface{}) bool { count++ return true }, ) } var boxes3 []tBox geoindex.Wrap(&tr).Nearby( geoindex.SimpleBoxAlgo(centerMin, centerMax), func(min, max [2]float64, value interface{}, dist float64) bool { boxes3 = append(boxes3, value.(tBox)) return true }, ) if len(boxes3) != len(nboxes) { t.Fatalf("expected %d, got %d", len(nboxes), len(boxes3)) } if len(boxes3) != tr.Len() { t.Fatalf("expected %d, got %d", tr.Len(), len(boxes3)) } var ldist float64 for i, box := range boxes3 { dist := testBoxDist(box.min, box.max, 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 *rect) 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 *rect) 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 < r.data.(*node).count; i++ { r.data.(*node).boxes[i].print(height-1, indent+1) } } } func (tr RTree) 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 RTree var pt [2]float64 for i := 0; i < N; i++ { tr.Insert(pt, pt, i) } } func BenchmarkRandomInsert(b *testing.B) { var tr RTree boxes := randBoxes(b.N) b.ResetTimer() for i := 0; i < b.N; i++ { tr.Insert(boxes[i].min, boxes[i].max, i) } }