Data-SpatialHash-Shared
view release on metacpan or search on metacpan
query region; smaller cells reduce false positives at the cost of more
bucket lookups.
2D vs 3D
All methods that accept coordinates accept either (x, y) or (x, y, z).
When z is omitted it is treated as 0, so a 2D query matches the z=0 cell
layer (points within one "cell_size" of the z=0 plane), not an infinite
z-column, and z does not enter the distance for a 2D call. A handle
created with a 2D insert can be queried with either 2D or 3D calls.
Toroidal space
By default the grid is unbounded and Euclidean. Construct with "wrap =>
[$Wx, $Wy]" (2D) or "[$Wx, $Wy, $Wz]" (3D) to make space a seamless
torus: neighbour-cell expansion wraps around the grid edges and
"query_radius", "query_knn", "each_in_radius", "query_radius_many", and
the pair emitters all use the minimum-image (shortest wrapping) distance
dx = abs(ax - bx); dx = $Wx - dx if dx > $Wx / 2; # per axis
so an entry near 0 and one near $Wx are neighbours. Keep positions
within "[0, $W)" per axis for the metric to be meaningful. Each wrapped
extent must be a positive multiple of "cell_size" so the cells tile the
world exactly, otherwise the constructor croaks. "query_cell" resolves
its coordinate to a wrapped cell, but "query_aabb" uses the literal
(non-wrapping) box even in a wrapping world. The wrap configuration is
part of the mapped format and is restored on reopen; the "world"
accessor returns the extents.
METHODS
Constructors
my $s = Data::SpatialHash::Shared->new($path, $max, $buckets, $cell);
my $s = Data::SpatialHash::Shared->new(undef, $max, $buckets, $cell);
my $s = Data::SpatialHash::Shared->new_memfd($name, $max, $buckets, $cell);
my $s = Data::SpatialHash::Shared->new_from_fd($fd);
my $s = Data::SpatialHash::Shared->new($path, $max, $buckets, $cell, wrap => [$Wx, $Wy]);
$path is the backing file path; "undef" creates an anonymous mapping.
$max is the maximum number of entries. $buckets is the bucket count (0 =
auto). $cell is the cell size (float).
Pass "wrap => [$Wx, $Wy]" (or "[$Wx, $Wy, $Wz]") to make the world a
seamless torus of those extents (see "Toroidal space"); omit it for an
unbounded Euclidean space.
When reopening an existing backing file or memfd, the stored header
wins: the caller's $max, $buckets, $cell, "wrap", and "sphere" arguments
are ignored and the file's original values are used. They are still
validated for well-formedness first (for example a non-positive $cell
croaks), so pass plausible values even on a reopen; only the stored
geometry is authoritative.
"new_memfd" creates a Linux memfd (anonymous but transferable via
"memfd" file descriptor). "new_from_fd" reopens an existing memfd in
another process.
Mutators
my $h = $s->insert(x, y, value); # 2D insert -- returns handle or undef
my $h = $s->insert(x, y, z, value); # 3D insert
my $h = $s->insert(x, y, z, value, r); # 3D insert with an interaction radius
$s->set_radius($h, $r); # set/replace an entry's radius
$s->move($h, x, y); # relocate entry (2D)
$s->move($h, x, y, z); # relocate entry (3D)
$s->remove($h); # free entry slot
$s->set_value($h, $v); # update stored value
$s->clear; # remove all entries
my @ids = $s->insert_many([ [x,y,value], [x,y,value,r], ... ]); # bulk insert
my $n = $s->move_many([ [handle,x,y], [handle,x,y,z], ... ]); # bulk move
"insert" returns a handle (opaque integer) on success, or "undef" if
"max_entries" is exhausted. "move" and "remove" return true on success,
or false if the handle is invalid or already removed. "set_value"
instead croaks on an invalid or freed handle; it and "clear" return
nothing.
Each entry may carry an interaction radius (default 0; must be finite
and non-negative), used by "each_colliding_pair". Set it with the
5-argument "insert" or with "set_radius" (which croaks on an invalid or
freed handle); for a 2D entry with a radius, insert then call
"set_radius". "insert_many" and "move_many" apply a whole batch under a
single lock acquisition -- each row is an arrayref. "insert_many"
inserts 2D entries (rows "[x,y,value]" or "[x,y,value,radius]"; use
"insert" in a loop for 3D) and returns the list of handles, with "undef"
for any row that overflowed the pool, was malformed (not an arrayref of
length 3 or 4), or carried a negative or non-finite radius. "move_many"
takes "[handle,x,y]" or "[handle,x,y,z]" rows and returns the count
successfully moved; freed/invalid handles and malformed rows are
skipped.
Handles are entry slot indices starting at 0, and 0 is false in Perl.
The very first insert into a fresh hash returns handle 0. Always test
the result with "defined $h", never for truthiness:
my $h = $s->insert($x, $y, $v);
die "full" unless defined $h; # correct: handle 0 is valid
# WRONG: "unless $h" would treat the first handle (0) as failure
Accessors
$s->has($h); # true if handle is live
$s->value($h); # stored value
$s->get_radius($h); # stored interaction radius (0 if unset)
my ($x, $y, $z) = $s->position($h); # current position
"has" is the safe predicate for a possibly-freed handle; "value",
"position", and "get_radius" croak on an invalid or freed handle.
Queries
Most query methods return a list of stored values (not handles) for
matching entries; "query_radius_many" instead returns an arrayref of
id-list arrayrefs (see "Batched radius queries"). For "query_knn",
results are in nearest-first order.
my @ids = $s->query_radius(x, y, r); # 2D radius search
my @ids = $s->query_radius(x, y, z, r); # 3D radius search
my @ids = $s->query_aabb(x0, y0, x1, y1); # 2D axis-aligned box
my @ids = $s->query_aabb(x0, y0, z0, x1, y1, z1); # 3D box
my @ids = $s->query_cell(x, y); # single cell (2D)
my @ids = $s->query_cell(x, y, z); # single cell (3D)
my @ids = $s->query_knn(x, y, k); # k nearest (2D)
my @ids = $s->query_knn(x, y, z, k); # k nearest (3D)
$s->each_in_radius(x, y, r, sub { my ($v) = @_; ... }); # 2D cb
$s->each_in_radius(x, y, z, r, sub { my ($v) = @_; ... }); # 3D cb
my $lists = $s->query_radius_many([ [x,y,r], [x,y,z,r] ]); # N radius queries, one lock
( run in 1.811 second using v1.01-cache-2.11-cpan-84de2e75c66 )