erlquad

Hex.pmCIErlang Versions

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

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

FunctionPurpose
new/5Create an empty tree over a rectangle, subdivided to a fixed depth.
objects_add/3Add a list of objects, using an outline function to place each one.

Querying the whole tree

FunctionPurpose
objects_all/1Every object, as a flat list.
objects_deep_all/1Every object, as a nested list mirroring the tree.
objects_fold/3Fold a function over every object.
objects_any/2Whether 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.

FunctionPurpose
area_query/5Candidate objects in the area, as a flat list.
area_query_deep/5Candidate objects in the area, as a nested list.
area_query_fold/7Fold a function over the candidate objects in the area.
area_query_any/6Whether a predicate holds for any candidate object in the area.

License

erlquad is released under the MIT License. See the LICENSE file for details.