erlquad
erlquad is a straightforward Erlang implementation of
quadtrees: a 2D spatial index that
recursively subdivides a rectangular region into four quadrants, so that objects
can be looked up by location without scanning them all.
It supports both bounding-box outlines and precise coordinates for small enough objects, and exposes functions for fetching, folding and testing (with a boolean predicate) either a particular area of interest or the whole tree.
Documentation is available on HexDocs.
Installation
Add erlquad to your rebar.config dependencies:
{deps, [
{erlquad, "~> 1.2"}
]}.
Concepts
- A tree covers a fixed rectangle and is subdivided to a fixed depth, both
chosen when you call
new/5. The tree never grows deeper afterwards; instead, each node's bucket has unlimited capacity. - Objects can be any term.
erlquadnever inspects them directly — you give it functions that read whatever it needs (an outline, a predicate, a fold). - An object is placed by its outline, which is one of:
- precise coordinates,
{X, Y}; or - a bounding box,
{Left, Bottom, Right, Top}.
- precise coordinates,
- An object descends into a quadrant only while it fits entirely inside it; an object straddling a node's split lines is kept at that node.
- The
area_query*functions return a conservative superset of the objects in the queried area: no match is ever omitted, but objects sharing a node with the area may also be returned. Apply your own precise filtering if you need exact results.
Usage
Representing your objects
The object representation is up to you. This example uses two records, plus a few little functions that read them — an outline, a predicate, and a fold:
-record(big_square, {x :: number(), y :: number(), side :: number()}).
-record(tiny_circle, {x :: number(), y :: number()}).
Objects = [
#big_square{ x = 1000, y = 750, side = 10 },
#tiny_circle{ x = 3000, y = 1000 },
#tiny_circle{ x = 3000, y = 2000 }
],
%% An object's outline: a bounding box for the square, coordinates for a circle.
GetOutline = fun (#tiny_circle{ x = X, y = Y }) ->
{X, Y};
(#big_square{ x = X, y = Y, side = S }) ->
{X - S/2, Y - S/2, X + S/2, Y + S/2}
end,
IsSquare = fun (#tiny_circle{}) -> false;
(#big_square{}) -> true
end,
CountCircles = fun (#tiny_circle{}, N) -> N + 1;
(#big_square{}, N) -> N
end.
Building and populating a tree
Create an empty tree over a rectangle and a fixed depth, then add the objects:
Q0 = erlquad:new(0, 0, 4000, 3000, 3), % Left, Bottom, Right, Top, Depth
Q = erlquad:objects_add(Objects, GetOutline, Q0).
Fetching every object
erlquad:objects_all(Q).
% => [#big_square{...}, #tiny_circle{...}, #tiny_circle{...}]
Querying an area
Pass a Left, Bottom, Right, Top rectangle. Remember the result is a
conservative superset (see Concepts):
erlquad:area_query(0, 0, 1999, 2999, Q). % left half of the world
% => [#big_square{...}]
erlquad:area_query(2000, 0, 3999, 2999, Q). % right half of the world
% => [#tiny_circle{...}, #tiny_circle{...}]
Folding and testing
Instead of materialising a list, fold over the objects or short-circuit with a predicate — over the whole tree...
erlquad:objects_fold(CountCircles, 0, Q). % => 2
erlquad:objects_any(IsSquare, Q). % => true
...or over just an area:
erlquad:area_query_fold(CountCircles, 0, 2000, 0, 3999, 1499, Q). % => 1
erlquad:area_query_any(IsSquare, 2000, 1500, 3999, 2999, Q). % => false
Deep-list variants
Every fetch/query has a *_deep counterpart that returns a nested list
mirroring the tree instead of a flat one, skipping the cost of concatenating
intermediate results. Reach for these when a nested list is fine — for instance,
when you are going to fold over the result yourself:
erlquad:objects_deep_all(Q).
erlquad:area_query_deep(0, 0, 3999, 2999, Q).
% => nested lists, e.g. [[], [[#big_square{...}, [[], [], ...]], [[[], ...]]]]
API reference
Building and populating
| Function | Purpose |
|---|---|
new/5 | Create an empty tree over a rectangle, subdivided to a fixed depth. |
objects_add/3 | Add a list of objects, using an outline function to place each one. |
Querying the whole tree
| Function | Purpose |
|---|---|
objects_all/1 | Every object, as a flat list. |
objects_deep_all/1 | Every object, as a nested list mirroring the tree. |
objects_fold/3 | Fold a function over every object. |
objects_any/2 | Whether a predicate holds for any object. |
Querying an area
Each takes a Left, Bottom, Right, Top rectangle and returns a conservative
superset of the objects within it.
| Function | Purpose |
|---|---|
area_query/5 | Candidate objects in the area, as a flat list. |
area_query_deep/5 | Candidate objects in the area, as a nested list. |
area_query_fold/7 | Fold a function over the candidate objects in the area. |
area_query_any/6 | Whether a predicate holds for any candidate object in the area. |
License
erlquad is released under the MIT License. See the LICENSE file for
details.