aka "Golang kjkrol QuadTree"
The library provides a Quadtree data structure
for efficient spatial indexing and 2D range queries.
Powered by gokg toolkit: the tree understands both bounded and cyclic planes.
Queries grazing a cyclic edge automatically wrap and
fragment their search regions, so neighbour lookups stay accurate even when the area
straddles the boundary.
A spatial tree used for searching in a two-dimensional space. Formally defined as:
Which reads as: if a node contains more than L elements, create four children and distribute the elements among them.
Assume the tree root represents the plane’s AABB (axis-aligned bounding box). Let’s introduce the subdivision rules for a node:
- The node’s AABB
$Q$ splits into four equal AABBs representing fragments of the parent, i.e.${Q_{NE}, Q_{NW}, Q_{SE}, Q_{SW}}$ . - Each element is moved to the appropriate newly created leaf so that it lies within that leaf’s AABB.
The illustrations below show example points placed on the plane and stored in a quadtree. As more points are added, the area is split into smaller fragments.
Where point density is higher, the area is subdivided more: for example, region SE contains one point—no further subdivision is needed. In region NW, subdivision occurs, followed by a further subdivision of NW.SE.
package main
import (
"fmt"
"github.com/kjkrol/gokg/pkg/geometry"
"github.com/kjkrol/gokq/pkg/qtree"
)
type point struct {
box geometry.BoundingBox[float64]
id string
}
func newPoint(id string, x, y float64) *point {
pos := geometry.NewVec(x, y)
return &point{
box: geometry.NewBoundingBoxAt(pos, 0, 0),
id: id,
}
}
func (p *point) Bound() geometry.BoundingBox[float64] { return p.box }
func (p *point) Id() string { return p.id }
func main() {
plane := geometry.NewCyclicBoundedPlane[float64](64, 64)
tree := qtree.NewQuadTree(plane)
defer tree.Close()
for _, pt := range []*point{
newPoint("edge", 63.0, 63.0),
newPoint("wrap-x", 1.0, 63.0),
newPoint("wrap-y", 63.0, 1.0),
} {
tree.Add(pt)
}
target := newPoint("target", 63.5, 63.5)
for _, neighbor := range tree.FindNeighbors(target, 2.0) {
fmt.Printf("%s -> %v\n", neighbor.Id(), neighbor.Bound())
}
}For more scenarios, explore the example-based tests in pkg/qtree, which double as runnable docs.
Imagine we have N points on a plane. We want to check whether points collide or are within a given distance d of each other. For every pair
To do this we must compare all ordered pairs, with repetition allowed. In mathematical terms: “we are looking for the number of length-2 variations on a set of N elements.” The number of variations with repetition is given by
where N is the number of available elements and k is the sequence length.
In summary: the naive approach would require checking every possible pair of objects, i.e.
It would be better to compare each pair only once—order does not matter (“collision A with B is the same as B with A”). So we want the number of combinations without repetition
In our case, we are looking for “the number of ways to choose 2 elements from a set of N different elements, without repetition and without considering order.”
We obtained a result half as large, but the algorithmic complexity is still on the order of
If we place all points from the plane into a quadtree, we can leverage the tree structure to greatly speed up searching.
For this case:
- the root check is positive
- we check its children; those with a positive result are outlined in orange
- we recursively continue checking the children of nodes that tested positive until we reach leaves
Search(Q, queryAabb):
if Q.aabb does not intersect queryAabb:
return ∅
if Q is a leaf:
return all points p ∈ Q.points such that p ∈ queryAabb
// internal node – check only children whose AABB intersects the query
result ← ∅
for child C in {Q_NE, Q_NW, Q_SE, Q_SW}:
if C.aabb intersects queryAabb:
result ← result ∪ Search(C, queryAabb)
return result
In short: we discard whole subtrees whose AABB does not intersect the query area; in leaf nodes we filter points with an AABB membership test. This cuts down the checks to the subtrees actually intersecting the search area.
Search complexity.
Let
The tree height is
The number of nodes visited in the entire operation is
Thus the time to traverse the whole tree is
Taking into account that we have to perform distance tests for each of the up to 4 points stored in every leaf:
Dropping constants (the 4s), the algorithmic complexity of searching the tree is:
This is a much better result than blindly comparing every pair of points, which remember has complexity