mirror of https://github.com/emirpasic/gods
add quadtree
parent
79df803e55
commit
f0513c834e
@ -0,0 +1,289 @@
|
||||
|
||||
package quadtree
|
||||
|
||||
type Quadtree struct {
|
||||
Bounds Bounds
|
||||
MaxObjects int
|
||||
MaxLevels int
|
||||
Level int
|
||||
Objects []Bounds
|
||||
Nodes []Quadtree
|
||||
Total int
|
||||
}
|
||||
|
||||
type Bounds struct {
|
||||
X float64
|
||||
Y float64
|
||||
Width float64
|
||||
Height float64
|
||||
}
|
||||
|
||||
func (b *Bounds) IsPoint() bool {
|
||||
|
||||
if b.Width == 0 && b.Height == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (b *Bounds) Intersects(a Bounds) bool {
|
||||
|
||||
aMaxX := a.X + a.Width
|
||||
aMaxY := a.Y + a.Height
|
||||
bMaxX := b.X + b.Width
|
||||
bMaxY := b.Y + b.Height
|
||||
|
||||
if aMaxX < b.X {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.X > bMaxX {
|
||||
return false
|
||||
}
|
||||
|
||||
if aMaxY < b.Y {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.Y > bMaxY {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) TotalNodes() int {
|
||||
|
||||
total := 0
|
||||
|
||||
if len(qt.Nodes) > 0 {
|
||||
for i := 0; i < len(qt.Nodes); i++ {
|
||||
total += 1
|
||||
total += qt.Nodes[i].TotalNodes()
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) split() {
|
||||
|
||||
if len(qt.Nodes) == 4 {
|
||||
return
|
||||
}
|
||||
|
||||
nextLevel := qt.Level + 1
|
||||
subWidth := qt.Bounds.Width / 2
|
||||
subHeight := qt.Bounds.Height / 2
|
||||
x := qt.Bounds.X
|
||||
y := qt.Bounds.Y
|
||||
|
||||
qt.Nodes = append(qt.Nodes, Quadtree{
|
||||
Bounds: Bounds{
|
||||
X: x + subWidth,
|
||||
Y: y,
|
||||
Width: subWidth,
|
||||
Height: subHeight,
|
||||
},
|
||||
MaxObjects: qt.MaxObjects,
|
||||
MaxLevels: qt.MaxLevels,
|
||||
Level: nextLevel,
|
||||
Objects: make([]Bounds, 0),
|
||||
Nodes: make([]Quadtree, 0, 4),
|
||||
})
|
||||
|
||||
qt.Nodes = append(qt.Nodes, Quadtree{
|
||||
Bounds: Bounds{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: subWidth,
|
||||
Height: subHeight,
|
||||
},
|
||||
MaxObjects: qt.MaxObjects,
|
||||
MaxLevels: qt.MaxLevels,
|
||||
Level: nextLevel,
|
||||
Objects: make([]Bounds, 0),
|
||||
Nodes: make([]Quadtree, 0, 4),
|
||||
})
|
||||
|
||||
qt.Nodes = append(qt.Nodes, Quadtree{
|
||||
Bounds: Bounds{
|
||||
X: x,
|
||||
Y: y + subHeight,
|
||||
Width: subWidth,
|
||||
Height: subHeight,
|
||||
},
|
||||
MaxObjects: qt.MaxObjects,
|
||||
MaxLevels: qt.MaxLevels,
|
||||
Level: nextLevel,
|
||||
Objects: make([]Bounds, 0),
|
||||
Nodes: make([]Quadtree, 0, 4),
|
||||
})
|
||||
|
||||
qt.Nodes = append(qt.Nodes, Quadtree{
|
||||
Bounds: Bounds{
|
||||
X: x + subWidth,
|
||||
Y: y + subHeight,
|
||||
Width: subWidth,
|
||||
Height: subHeight,
|
||||
},
|
||||
MaxObjects: qt.MaxObjects,
|
||||
MaxLevels: qt.MaxLevels,
|
||||
Level: nextLevel,
|
||||
Objects: make([]Bounds, 0),
|
||||
Nodes: make([]Quadtree, 0, 4),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) getIndex(pRect Bounds) int {
|
||||
|
||||
index := -1
|
||||
|
||||
verticalMidpoint := qt.Bounds.X + (qt.Bounds.Width / 2)
|
||||
horizontalMidpoint := qt.Bounds.Y + (qt.Bounds.Height / 2)
|
||||
topQuadrant := (pRect.Y < horizontalMidpoint) && (pRect.Y+pRect.Height < horizontalMidpoint)
|
||||
bottomQuadrant := (pRect.Y > horizontalMidpoint)
|
||||
if (pRect.X < verticalMidpoint) && (pRect.X+pRect.Width < verticalMidpoint) {
|
||||
|
||||
if topQuadrant {
|
||||
index = 1
|
||||
} else if bottomQuadrant {
|
||||
index = 2
|
||||
}
|
||||
|
||||
} else if pRect.X > verticalMidpoint {
|
||||
|
||||
if topQuadrant {
|
||||
index = 0
|
||||
} else if bottomQuadrant {
|
||||
index = 3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return index
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) Insert(pRect Bounds) {
|
||||
|
||||
qt.Total++
|
||||
|
||||
i := 0
|
||||
var index int
|
||||
|
||||
if len(qt.Nodes) > 0 == true {
|
||||
|
||||
index = qt.getIndex(pRect)
|
||||
|
||||
if index != -1 {
|
||||
qt.Nodes[index].Insert(pRect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
qt.Objects = append(qt.Objects, pRect)
|
||||
|
||||
if (len(qt.Objects) > qt.MaxObjects) && (qt.Level < qt.MaxLevels) {
|
||||
|
||||
if len(qt.Nodes) > 0 == false {
|
||||
qt.split()
|
||||
}
|
||||
|
||||
for i < len(qt.Objects) {
|
||||
|
||||
index = qt.getIndex(qt.Objects[i])
|
||||
|
||||
if index != -1 {
|
||||
|
||||
splice := qt.Objects[i]
|
||||
qt.Objects = append(qt.Objects[:i], qt.Objects[i+1:]...)
|
||||
|
||||
qt.Nodes[index].Insert(splice)
|
||||
|
||||
} else {
|
||||
|
||||
i++
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) Retrieve(pRect Bounds) []Bounds {
|
||||
|
||||
index := qt.getIndex(pRect)
|
||||
|
||||
returnObjects := qt.Objects
|
||||
|
||||
if len(qt.Nodes) > 0 {
|
||||
if index != -1 {
|
||||
|
||||
returnObjects = append(returnObjects, qt.Nodes[index].Retrieve(pRect)...)
|
||||
|
||||
} else {
|
||||
|
||||
for i := 0; i < len(qt.Nodes); i++ {
|
||||
returnObjects = append(returnObjects, qt.Nodes[i].Retrieve(pRect)...)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return returnObjects
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) RetrievePoints(find Bounds) []Bounds {
|
||||
|
||||
var foundPoints []Bounds
|
||||
potentials := qt.Retrieve(find)
|
||||
for o := 0; o < len(potentials); o++ {
|
||||
|
||||
xyMatch := potentials[o].X == float64(find.X) && potentials[o].Y == float64(find.Y)
|
||||
if xyMatch && potentials[o].IsPoint() {
|
||||
foundPoints = append(foundPoints, find)
|
||||
}
|
||||
}
|
||||
|
||||
return foundPoints
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) RetrieveIntersections(find Bounds) []Bounds {
|
||||
|
||||
var foundIntersections []Bounds
|
||||
|
||||
potentials := qt.Retrieve(find)
|
||||
for o := 0; o < len(potentials); o++ {
|
||||
if potentials[o].Intersects(find) {
|
||||
foundIntersections = append(foundIntersections, potentials[o])
|
||||
}
|
||||
}
|
||||
|
||||
return foundIntersections
|
||||
|
||||
}
|
||||
|
||||
func (qt *Quadtree) Clear() {
|
||||
|
||||
qt.Objects = []Bounds{}
|
||||
|
||||
if len(qt.Nodes)-1 > 0 {
|
||||
for i := 0; i < len(qt.Nodes); i++ {
|
||||
qt.Nodes[i].Clear()
|
||||
}
|
||||
}
|
||||
|
||||
qt.Nodes = []Quadtree{}
|
||||
qt.Total = 0
|
||||
|
||||
}
|
@ -0,0 +1,394 @@
|
||||
package quadtree
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestQuadtreeCreation(t *testing.T) {
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
if qt.Bounds.Width != 640 && qt.Bounds.Height != 480 {
|
||||
t.Errorf("Quadtree was not created correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
qt.split()
|
||||
if len(qt.Nodes) != 4 {
|
||||
t.Error("Quadtree did not split correctly, expected 4 nodes got", len(qt.Nodes))
|
||||
}
|
||||
|
||||
qt.split()
|
||||
if len(qt.Nodes) != 4 {
|
||||
t.Error("Quadtree should not split itself more than once", len(qt.Nodes))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTotalSubnodes(t *testing.T) {
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
qt.split()
|
||||
for i := 0; i < len(qt.Nodes); i++ {
|
||||
qt.Nodes[i].split()
|
||||
}
|
||||
|
||||
total := qt.TotalNodes()
|
||||
if total != 20 {
|
||||
t.Error("Quadtree did not split correctly, expected 20 nodes got", total)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestQuadtreeInsert(t *testing.T) {
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
|
||||
grid := 10.0
|
||||
gridh := qt.Bounds.Width / grid
|
||||
gridv := qt.Bounds.Height / grid
|
||||
var randomObject Bounds
|
||||
numObjects := 1000
|
||||
|
||||
for i := 0; i < numObjects; i++ {
|
||||
|
||||
x := randMinMax(0, gridh) * grid
|
||||
y := randMinMax(0, gridv) * grid
|
||||
|
||||
randomObject = Bounds{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: randMinMax(1, 4) * grid,
|
||||
Height: randMinMax(1, 4) * grid,
|
||||
}
|
||||
|
||||
index := qt.getIndex(randomObject)
|
||||
if index < -1 || index > 3 {
|
||||
t.Errorf("The index should be -1 or between 0 and 3, got %d \n", index)
|
||||
}
|
||||
|
||||
qt.Insert(randomObject)
|
||||
|
||||
}
|
||||
|
||||
if qt.Total != numObjects {
|
||||
t.Errorf("Error: Should have totalled %d, got %d \n", numObjects, qt.Total)
|
||||
} else {
|
||||
t.Logf("Success: Total objects in the Quadtree is %d (as expected) \n", qt.Total)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCorrectQuad(t *testing.T) {
|
||||
|
||||
qt := setupQuadtree(0, 0, 100, 100)
|
||||
|
||||
var index int
|
||||
pass := true
|
||||
|
||||
topRight := Bounds{
|
||||
X: 99,
|
||||
Y: 99,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
qt.Insert(topRight)
|
||||
index = qt.getIndex(topRight)
|
||||
if index == 0 {
|
||||
t.Errorf("The index should be 0, got %d \n", index)
|
||||
pass = false
|
||||
}
|
||||
|
||||
topLeft := Bounds{
|
||||
X: 99,
|
||||
Y: 1,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
qt.Insert(topLeft)
|
||||
index = qt.getIndex(topLeft)
|
||||
if index == 1 {
|
||||
t.Errorf("The index should be 1, got %d \n", index)
|
||||
pass = false
|
||||
}
|
||||
|
||||
bottomLeft := Bounds{
|
||||
X: 1,
|
||||
Y: 1,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
qt.Insert(bottomLeft)
|
||||
index = qt.getIndex(bottomLeft)
|
||||
if index == 2 {
|
||||
t.Errorf("The index should be 2, got %d \n", index)
|
||||
pass = false
|
||||
}
|
||||
|
||||
bottomRight := Bounds{
|
||||
X: 1,
|
||||
Y: 51,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
qt.Insert(bottomRight)
|
||||
index = qt.getIndex(bottomRight)
|
||||
if index == 3 {
|
||||
t.Errorf("The index should be 3, got %d \n", index)
|
||||
pass = false
|
||||
}
|
||||
|
||||
if pass == true {
|
||||
t.Log("Success: The points were inserted into the correct quadrants")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestQuadtreeRetrieval(t *testing.T) {
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
|
||||
var randomObject Bounds
|
||||
numObjects := 100
|
||||
|
||||
for i := 0; i < numObjects; i++ {
|
||||
|
||||
randomObject = Bounds{
|
||||
X: float64(i),
|
||||
Y: float64(i),
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
|
||||
qt.Insert(randomObject)
|
||||
|
||||
}
|
||||
|
||||
for j := 0; j < numObjects; j++ {
|
||||
|
||||
Cursor := Bounds{
|
||||
X: float64(j),
|
||||
Y: float64(j),
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
|
||||
objects := qt.Retrieve(Cursor)
|
||||
|
||||
found := false
|
||||
|
||||
if len(objects) >= numObjects {
|
||||
t.Error("Objects should not be equal to or bigger than the number of retrieved objects")
|
||||
}
|
||||
|
||||
for o := 0; o < len(objects); o++ {
|
||||
if objects[o].X == float64(j) && objects[o].Y == float64(j) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found != true {
|
||||
t.Error("Error finding the correct point")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestQuadtreeRandomPointRetrieval(t *testing.T) {
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
|
||||
numObjects := 1000
|
||||
|
||||
for i := 1; i < numObjects+1; i++ {
|
||||
|
||||
randomObject := Bounds{
|
||||
X: float64(i),
|
||||
Y: float64(i),
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
|
||||
qt.Insert(randomObject)
|
||||
|
||||
}
|
||||
|
||||
failure := false
|
||||
iterations := 20
|
||||
for j := 1; j < iterations+1; j++ {
|
||||
|
||||
Cursor := Bounds{
|
||||
X: float64(j),
|
||||
Y: float64(j),
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
}
|
||||
|
||||
point := qt.RetrievePoints(Cursor)
|
||||
|
||||
for k := 0; k < len(point); k++ {
|
||||
if point[k].X == 0 {
|
||||
failure = true
|
||||
}
|
||||
if point[k].Y == 0 {
|
||||
failure = true
|
||||
}
|
||||
if failure {
|
||||
t.Error("Point was incorrectly retrieved", point)
|
||||
}
|
||||
if point[k].IsPoint() == false {
|
||||
t.Error("Point should have width and height of 0")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if failure == false {
|
||||
t.Logf("Success: All the points were retrieved correctly", iterations, numObjects)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIntersectionRetrieval(t *testing.T) {
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
qt.Insert(Bounds{
|
||||
X: 1,
|
||||
Y: 1,
|
||||
Width: 10,
|
||||
Height: 10,
|
||||
})
|
||||
qt.Insert(Bounds{
|
||||
X: 5,
|
||||
Y: 5,
|
||||
Width: 10,
|
||||
Height: 10,
|
||||
})
|
||||
qt.Insert(Bounds{
|
||||
X: 10,
|
||||
Y: 10,
|
||||
Width: 10,
|
||||
Height: 10,
|
||||
})
|
||||
qt.Insert(Bounds{
|
||||
X: 15,
|
||||
Y: 15,
|
||||
Width: 10,
|
||||
Height: 10,
|
||||
})
|
||||
inter := qt.RetrieveIntersections(Bounds{
|
||||
X: 5,
|
||||
Y: 5,
|
||||
Width: 2.5,
|
||||
Height: 2.5,
|
||||
})
|
||||
if len(inter) != 2 {
|
||||
t.Error("Should have two intersections")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuadtreeClear(t *testing.T) {
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano()) // Seed Random properly
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
|
||||
grid := 10.0
|
||||
gridh := qt.Bounds.Width / grid
|
||||
gridv := qt.Bounds.Height / grid
|
||||
var randomObject Bounds
|
||||
numObjects := 1000
|
||||
|
||||
for i := 0; i < numObjects; i++ {
|
||||
|
||||
x := randMinMax(0, gridh) * grid
|
||||
y := randMinMax(0, gridv) * grid
|
||||
|
||||
randomObject = Bounds{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: randMinMax(1, 4) * grid,
|
||||
Height: randMinMax(1, 4) * grid,
|
||||
}
|
||||
|
||||
index := qt.getIndex(randomObject)
|
||||
if index < -1 || index > 3 {
|
||||
t.Errorf("The index should be -1 or between 0 and 3, got %d \n", index)
|
||||
}
|
||||
|
||||
qt.Insert(randomObject)
|
||||
|
||||
}
|
||||
|
||||
qt.Clear()
|
||||
|
||||
if qt.Total != 0 {
|
||||
t.Errorf("Error: The Quadtree should be cleared")
|
||||
} else {
|
||||
t.Logf("Success: The Quadtree was cleared correctly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func BenchmarkInsertOneThousand(b *testing.B) {
|
||||
|
||||
qt := setupQuadtree(0, 0, 640, 480)
|
||||
|
||||
grid := 10.0
|
||||
gridh := qt.Bounds.Width / grid
|
||||
gridv := qt.Bounds.Height / grid
|
||||
var randomObject Bounds
|
||||
numObjects := 1000
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
for i := 0; i < numObjects; i++ {
|
||||
|
||||
x := randMinMax(0, gridh) * grid
|
||||
y := randMinMax(0, gridv) * grid
|
||||
|
||||
randomObject = Bounds{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: randMinMax(1, 4) * grid,
|
||||
Height: randMinMax(1, 4) * grid,
|
||||
}
|
||||
|
||||
qt.Insert(randomObject)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func setupQuadtree(x float64, y float64, width float64, height float64) *Quadtree {
|
||||
|
||||
return &Quadtree{
|
||||
Bounds: Bounds{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: width,
|
||||
Height: height,
|
||||
},
|
||||
MaxObjects: 4,
|
||||
MaxLevels: 8,
|
||||
Level: 0,
|
||||
Objects: make([]Bounds, 0),
|
||||
Nodes: make([]Quadtree, 0),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func randMinMax(min float64, max float64) float64 {
|
||||
val := min + (rand.Float64() * (max - min))
|
||||
return val
|
||||
}
|
Loading…
Reference in New Issue