├── .gitignore ├── README.md └── quadtree ├── BoundingBox.go ├── BoundingBox_test.go ├── QuadTree.go └── QuadTree_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *~ 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | A quad-tree is a geometrical data structure which offers fast axis aligned range queries 4 | (i.e. bounding-box queries) on sets of rectangles and points. 5 | 6 | goquadtree is a simple 2D implementation in golang to be used as a golang package. 7 | 8 | Depending on size and distribution of data a query may be in orders of 9 | magnitudes faster than a linear search. 10 | 11 | From the included benchmarks: 12 | 13 | BenchmarkRectsQuadtree 100 11309510 ns/op 14 | BenchmarkRectsLinear 5 407917200 ns/op 15 | BenchmarkPointsQuadtree 50000 51042 ns/op 16 | BenchmarkPointsLinear 5 407603000 ns/op 17 | 18 | ## Usage 19 | 20 | The included tests and benchmarks give an example on how to use the package. 21 | 22 | -------------------------------------------------------------------------------- /quadtree/BoundingBox.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Volker Poplawski 3 | */ 4 | 5 | 6 | package quadtree 7 | 8 | import "math" 9 | 10 | 11 | // Use NewBoundingBox() to construct a BoundingBox object 12 | type BoundingBox struct { 13 | MinX, MaxX, MinY, MaxY float64 14 | } 15 | 16 | 17 | func NewBoundingBox(xa, xb, ya, yb float64) BoundingBox { 18 | return BoundingBox{ math.Min(xa, xb), math.Max(xa, xb), math.Min(ya, yb), math.Max(ya, yb) } 19 | } 20 | 21 | 22 | // Make BoundingBox implement the BoundingBoxer interface 23 | func (b BoundingBox) BoundingBox() BoundingBox { 24 | return b 25 | } 26 | 27 | 28 | func (b BoundingBox) SizeX() float64 { 29 | return b.MaxX - b.MinX 30 | } 31 | 32 | 33 | func (b BoundingBox) SizeY() float64 { 34 | return b.MaxY - b.MinY 35 | } 36 | 37 | 38 | // Returns true if o intersects this 39 | func (b BoundingBox) Intersects(o BoundingBox) bool { 40 | return b.MinX < o.MaxX && b.MinY < o.MaxY && 41 | b.MaxX > o.MinX && b.MaxY > o.MinY 42 | } 43 | 44 | 45 | // Returns true if o is within this 46 | func (b BoundingBox) Contains(o BoundingBox) bool { 47 | return b.MinX <= o.MinX && b.MinY <= o.MinY && 48 | b.MaxX >= o.MaxX && b.MaxY >= o.MaxY 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /quadtree/BoundingBox_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Volker Poplawski 3 | */ 4 | 5 | package quadtree 6 | 7 | import "testing" 8 | import _ "math" 9 | 10 | func TestBoundingBox(t *testing.T) { 11 | a := NewBoundingBox( 0, 10, 0, 10 ) 12 | 13 | b := NewBoundingBox( 4, 6, 4, 6 ) // b completely within a 14 | 15 | if ! a.Intersects(b) || ! b.Intersects(a) { 16 | t.Errorf("%v does not intersect %v", a, b) 17 | } 18 | 19 | if ! a.Intersects(a) { 20 | t.Errorf("%v does not intersect itself", a) 21 | } 22 | 23 | if ! a.Contains(b) { 24 | t.Errorf("%v does not contain %v", a, b) 25 | } 26 | 27 | if ! a.Contains(a) { 28 | t.Errorf("%v does not contain itself", a) 29 | } 30 | 31 | if b.Contains(a) { 32 | t.Errorf("%v contains %v", b, a) 33 | } 34 | 35 | c := NewBoundingBox( 10, 20, 0, 10 ) 36 | 37 | if a.Intersects(c) { 38 | t.Errorf("%v does intersect %v", a, c) 39 | } 40 | if c.Intersects(a) { 41 | t.Errorf("%v does intersect %v", c, a) 42 | } 43 | 44 | if a.Contains(c) || c.Contains(a) { 45 | t.Errorf("%v contains %v (or vise versa)", a, c) 46 | } 47 | 48 | d := NewBoundingBox( -10, 0, 0, 10 ) 49 | 50 | if a.Intersects(d) { 51 | t.Errorf("%v does intersect %v", a, d) 52 | } 53 | if d.Intersects(a) { 54 | t.Errorf("%v does intersect %v", d, a) 55 | } 56 | 57 | e := NewBoundingBox( 9, 15, 9, 15 ) 58 | 59 | if ! a.Intersects(e) || ! e.Intersects(a) { 60 | t.Errorf("%v does not intersect %v", a, e) 61 | } 62 | 63 | f := NewBoundingBox( -10, 20, 4, 6 ) 64 | 65 | if ! a.Intersects(f) || ! f.Intersects(a) { 66 | t.Errorf("%v does not intersect %v", a, f) 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /quadtree/QuadTree.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Volker Poplawski 3 | */ 4 | 5 | // Package quadtree is a simple 2D implementation of the Quad-Tree data structure. 6 | package quadtree 7 | 8 | import _ "fmt" 9 | 10 | // Number of entries until a quad is split 11 | const MAX_ENTRIES_PER_TILE = 16 12 | 13 | // Maximum depth of quad-tree (not counting the root node) 14 | const MAX_LEVELS = 10 15 | 16 | // some constants for tile-indeces, for clarity 17 | const ( 18 | _TOPRIGHT = 0 19 | _TOPLEFT = 1 20 | _BOTTOMLEFT = 2 21 | _BOTTOMRIGHT = 3 22 | ) 23 | 24 | 25 | // QuadTree exspects its values to implement the BoundingBoxer interface. 26 | type BoundingBoxer interface { 27 | BoundingBox() BoundingBox 28 | } 29 | 30 | 31 | type QuadTree struct { 32 | root qtile 33 | } 34 | 35 | 36 | // Constructs an empty quad-tree 37 | // bbox specifies the extends of the coordinate system. 38 | func NewQuadTree(bbox BoundingBox) QuadTree { 39 | qt := QuadTree{ qtile{BoundingBox:bbox} } 40 | 41 | return qt 42 | } 43 | 44 | 45 | // quad-tile / node of the quad-tree 46 | type qtile struct { 47 | BoundingBox 48 | level int // level this tile is at. root is level 0 49 | contents []BoundingBoxer // values stored in this tile 50 | childs [4]*qtile // sub-tiles. none or four. 51 | } 52 | 53 | 54 | // Adds a value to the quad-tree by trickle down from the root node. 55 | func (qb *QuadTree) Add(v BoundingBoxer) { 56 | qb.root.add(v) 57 | } 58 | 59 | 60 | // Returns all values which intersect the query box 61 | func (qb *QuadTree) Query(bbox BoundingBox) (values []BoundingBoxer) { 62 | return qb.root.query(bbox, values) 63 | } 64 | 65 | 66 | func (tile *qtile) add(v BoundingBoxer) { 67 | // look for sub-tile directly below this tile to accomodate value. 68 | if i := tile.findChildIndex(v.BoundingBox()); i < 0 { 69 | // no suitable sub-tile for value found. 70 | // either this tile has no childs or 71 | // value does not fit in any subtile. 72 | // store value at this level. 73 | tile.contents = append(tile.contents, v) 74 | 75 | // tile is split if exceeds it max number of entries and 76 | // has not childs already and max tree depth for this sub-tree not reached. 77 | if len(tile.contents) > MAX_ENTRIES_PER_TILE && tile.childs[_TOPRIGHT] == nil && tile.level < MAX_LEVELS { 78 | tile.split() 79 | } 80 | } else { 81 | // suitable sub-tile for value found at index i. 82 | // recursivly add value. 83 | tile.childs[i].add(v) 84 | } 85 | } 86 | 87 | 88 | // return child index for BoundingBox 89 | // returns -1 if quad has no children or BoundingBox does not fit into any child 90 | func (tile *qtile) findChildIndex(bbox BoundingBox) int { 91 | if tile.childs[_TOPRIGHT] == nil { 92 | return -1 93 | } 94 | 95 | for i, child := range tile.childs { 96 | if child.Contains(bbox) { 97 | return i 98 | } 99 | } 100 | 101 | return -1 102 | } 103 | 104 | 105 | // create four child quads. 106 | // distribute contents of this tiles on newly created childs. 107 | func (tile *qtile) split() { 108 | mx := tile.MaxX/2.0 + tile.MinX/2.0 109 | my := tile.MaxY/2.0 + tile.MinY/2.0 110 | 111 | tile.childs[_TOPRIGHT] = &qtile{ BoundingBox:NewBoundingBox(mx, tile.MaxX, my, tile.MaxY), level:tile.level+1 } 112 | tile.childs[_TOPLEFT] = &qtile{ BoundingBox:NewBoundingBox(tile.MinX, mx, my, tile.MaxY), level:tile.level+1 } 113 | tile.childs[_BOTTOMLEFT] = &qtile{ BoundingBox:NewBoundingBox(tile.MinX, mx, tile.MinY, my), level:tile.level+1 } 114 | tile.childs[_BOTTOMRIGHT] = &qtile{ BoundingBox:NewBoundingBox(mx, tile.MaxX, tile.MinY, my), level:tile.level+1 } 115 | 116 | // copy values to temporary slice 117 | var contentsBak []BoundingBoxer 118 | contentsBak = append(contentsBak, tile.contents...) 119 | 120 | // clear values on this tile 121 | tile.contents = []BoundingBoxer{} 122 | 123 | // reinsert from temporary slice 124 | for _,v := range contentsBak { 125 | tile.add(v) 126 | } 127 | } 128 | 129 | 130 | func (tile *qtile) query(qbox BoundingBox, ret []BoundingBoxer) []BoundingBoxer { 131 | // end recursion if this tile does not intersect the query range 132 | if ! tile.Intersects(qbox) { 133 | return ret 134 | } 135 | 136 | // return candidates at this tile 137 | for _, v := range tile.contents { 138 | if qbox.Intersects(v.BoundingBox()) { 139 | ret = append(ret, v) 140 | } 141 | } 142 | 143 | // recurse into childs (if any) 144 | if tile.childs[_TOPRIGHT] != nil { 145 | for _, child := range tile.childs { 146 | ret = child.query(qbox, ret) 147 | } 148 | } 149 | 150 | return ret 151 | } 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /quadtree/QuadTree_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Volker Poplawski 3 | */ 4 | 5 | package quadtree 6 | 7 | import "testing" 8 | import "math" 9 | import "math/rand" 10 | import _ "fmt" 11 | 12 | 13 | 14 | // Generates n BoundingBoxes in the range of frame with average width and height avgSize 15 | func randomBoundingBoxes(n int, frame BoundingBox, avgSize float64) []BoundingBox { 16 | ret := make([]BoundingBox, n) 17 | 18 | for i := 0; i < len(ret); i++ { 19 | w := rand.NormFloat64() * avgSize 20 | h := rand.NormFloat64() * avgSize 21 | x := rand.Float64() * frame.SizeX() + frame.MinX 22 | y := rand.Float64() * frame.SizeY() + frame.MinY 23 | ret[i] = NewBoundingBox(x, math.Min(frame.MaxX, x+w), y, math.Min(frame.MaxY, y+h)) 24 | } 25 | 26 | return ret 27 | } 28 | 29 | 30 | // Returns all elements of data which intersect query 31 | func queryLinear(data []BoundingBox, query BoundingBox) (ret []BoundingBoxer) { 32 | for _, v := range data { 33 | if query.Intersects(v.BoundingBox()) { 34 | ret = append(ret, v) 35 | } 36 | } 37 | 38 | return ret 39 | } 40 | 41 | 42 | func compareBoundingBoxer(v1, v2 BoundingBoxer) bool { 43 | b1 := v1.BoundingBox() 44 | b2 := v2.BoundingBox() 45 | 46 | return b1.MinX == b2.MinX && b1.MaxX == b2.MaxX && 47 | b1.MinY == b2.MinY && b2.MaxY == b2.MaxY 48 | } 49 | 50 | 51 | func lookupResults(r1, r2 []BoundingBoxer) int { 52 | for i, v1 := range r1 { 53 | found := false 54 | 55 | for _, v2 := range r2 { 56 | if compareBoundingBoxer(v1, v2) { 57 | found = true 58 | break 59 | } 60 | } 61 | 62 | if ! found { 63 | return i 64 | } 65 | } 66 | 67 | return -1 68 | } 69 | 70 | // World-space extends from -1000..1000 in X and Y direction 71 | var world BoundingBox = NewBoundingBox(-1000.0, 1000.0, -1000.0, 1000.0) 72 | 73 | 74 | // Compary correctness of quad-tree results vs simple look-up on set of random rectangles 75 | func TestQuadTreeRects(t *testing.T) { 76 | var rects []BoundingBox = randomBoundingBoxes(100*1000, world, 5) 77 | qt := NewQuadTree(world) 78 | 79 | for _, v := range rects { 80 | qt.Add(v) 81 | } 82 | 83 | queries := randomBoundingBoxes(1000, world, 10) 84 | 85 | for _, q := range queries { 86 | r1 := queryLinear(rects, q) 87 | r2 := qt.Query(q) 88 | 89 | if len(r1) != len(r2) { 90 | t.Errorf("r1 and r2 differ: %v %v\n", r1, r2) 91 | } 92 | 93 | if i := lookupResults(r1, r2); i != -1 { 94 | t.Errorf("%v was not returned by QT\n", r1[i]) 95 | } 96 | 97 | if i := lookupResults(r2, r1); i != -1 { 98 | t.Errorf("%v was not returned by brute-force\n", r2[i]) 99 | } 100 | 101 | } 102 | } 103 | 104 | 105 | // Compary correctness of quad-tree results vs simple look-up on set of random points 106 | func TestQuadTreePoints(t *testing.T) { 107 | var points []BoundingBox = randomBoundingBoxes(100*1000, world, 0) 108 | qt := NewQuadTree(world) 109 | 110 | for _, v := range points { 111 | qt.Add(v) 112 | } 113 | 114 | queries := randomBoundingBoxes(1000, world, 10) 115 | 116 | for _, q := range queries { 117 | r1 := queryLinear(points, q) 118 | r2 := qt.Query(q) 119 | 120 | if len(r1) != len(r2) { 121 | t.Errorf("r1 and r2 differ: %v %v\n", r1, r2) 122 | } 123 | 124 | if i := lookupResults(r1, r2); i != -1 { 125 | t.Errorf("%v was not returned by QT\n", r1[i]) 126 | } 127 | 128 | if i := lookupResults(r2, r1); i != -1 { 129 | t.Errorf("%v was not returned by brute-force\n", r2[i]) 130 | } 131 | 132 | } 133 | } 134 | 135 | 136 | // Benchmark insertion into quad-tree 137 | func BenchmarkInsert(b *testing.B) { 138 | b.StopTimer() 139 | 140 | var values []BoundingBox = randomBoundingBoxes(b.N, world, 5) 141 | qt := NewQuadTree(world) 142 | 143 | b.StartTimer() 144 | 145 | for _, v := range values { 146 | qt.Add(v) 147 | } 148 | } 149 | 150 | 151 | // A set of 10 million randomly distributed rectangles of avg size 5 152 | var boxes10M []BoundingBox = randomBoundingBoxes(10*1000*1000, world, 5) 153 | 154 | // Benchmark quad-tree on set of rectangles 155 | func BenchmarkRectsQuadtree(b *testing.B) { 156 | b.StopTimer() 157 | rand.Seed(1) 158 | qt := NewQuadTree(world) 159 | 160 | for _, v := range boxes10M { 161 | qt.Add(v) 162 | } 163 | 164 | queries := randomBoundingBoxes(b.N, world, 10) 165 | 166 | b.StartTimer() 167 | for _, q := range queries { 168 | qt.Query(q) 169 | } 170 | } 171 | 172 | 173 | // Benchmark simple look up on set of rectangles 174 | func BenchmarkRectsLinear(b *testing.B) { 175 | b.StopTimer() 176 | rand.Seed(1) 177 | queries := randomBoundingBoxes(b.N, world, 10) 178 | 179 | b.StartTimer() 180 | for _, q := range queries { 181 | queryLinear(boxes10M, q) 182 | } 183 | } 184 | 185 | 186 | // A set of 10 million randomly distributed points 187 | var points10M []BoundingBox = randomBoundingBoxes(10*1000*1000, world, 0) 188 | 189 | // Benchmark quad-tree on set of points 190 | func BenchmarkPointsQuadtree(b *testing.B) { 191 | b.StopTimer() 192 | rand.Seed(1) 193 | qt := NewQuadTree(world) 194 | 195 | for _, v := range points10M { 196 | qt.Add(v) 197 | } 198 | 199 | queries := randomBoundingBoxes(b.N, world, 10) 200 | 201 | b.StartTimer() 202 | for _, q := range queries { 203 | qt.Query(q) 204 | } 205 | } 206 | 207 | 208 | // Benchmark simple look-up on set of points 209 | func BenchmarkPointsLinear(b *testing.B) { 210 | b.StopTimer() 211 | rand.Seed(1) 212 | queries := randomBoundingBoxes(b.N, world, 10) 213 | 214 | b.StartTimer() 215 | for _, q := range queries { 216 | queryLinear(points10M, q) 217 | } 218 | } 219 | --------------------------------------------------------------------------------