Data-SpatialHash-Shared
view release on metacpan or search on metacpan
Region query cost
The cost of a region query scales with the number of grid cells covering
the query region, not with the number of matching points. For
"query_radius", "each_in_radius", and each "query_radius_many" sub-query
that is roughly (2 * radius / cell_size) ** dims cells (similarly for
"query_aabb", which scans the cells spanning the box). The scan runs
while holding a read lock. An over-large radius relative to "cell_size"
therefore scans many empty cells, wasting time and stalling concurrent
writers that are waiting for the lock. Size "cell_size" on the order of
your typical query radius so each query touches only a handful of cells.
As a safety net, any region query that would scan more than
approximately 67 million cells (2**26) -- a "query_radius",
"query_aabb", "each_in_radius", or a "query_radius_many" sub-query whose
region spans that many cells, a "query_knn" that must walk that many
cells across its expanding shells, or a "each_pair_within" /
"each_colliding_pair" whose per-entry neighbourhood spans that many
cells -- croaks with a message containing the word "cells" rather than
scanning unbounded. If your use case genuinely requires regions that
large, increase "cell_size" so the same physical region maps to fewer
cells.
Spherical worlds
For points on or above a sphere (planets, globes), construct with a body
radius and use the geo helpers; a separate cube-sphere scheme gives
stable hierarchical cell ids for chunking and level-of-detail. Curvature
needs no special handling: with "sphere" set, geo coordinates are
converted to and from Cartesian in C, so proximity is exact
straight-line distance -- correct for surface and air entities alike --
not a great-circle approximation.
Geo proximity
my $s = Data::SpatialHash::Shared->new(undef, $max, 0, $cell, sphere => $R);
my $h = $s->insert_geo($lat, $lon, $alt, $value); # radians; alt above the surface
$s->move_geo($h, $lat, $lon, $alt);
my ($lat, $lon, $alt) = $s->position_geo($h);
my @vals = $s->query_geo_radius($lat, $lon, $alt, $dist); # $dist in world units
"sphere" is the body radius -- distinct from a per-entry interaction
radius -- and must be finite and greater than zero; it is stored in the
map and restored on reopen. "sphere" and "wrap" are mutually exclusive
(a sphere is not a flat torus); passing both croaks. Latitude and
longitude are in radians ("lat" in -pi/2 .. pi/2, "lon" in -pi .. pi);
"alt" is height above the sphere of radius $R, so an entity lies at
distance "$R + $alt" from the centre. Each geo method converts to
Cartesian and delegates to the ordinary 3D engine, so $dist in
"query_geo_radius" is a true straight-line distance and must be finite
and non-negative. At a pole, longitude is undefined and "position_geo"
reports it as 0. Calling a geo method on a map created without "sphere"
croaks. "insert_geo" returns a handle or "undef" if the pool is
exhausted (test with "defined" -- handle 0 is valid but false);
"move_geo" returns true, or false for a freed/invalid handle;
"position_geo" croaks on a freed or invalid handle.
Cube-sphere cells
A direction (or lat/lon) maps to a hierarchical cell id on a
cube-sphere: six cube faces, each an equal-angle grid subdivided to a
chosen level (level 0 = the whole face, level "L" = "2**L" cells per
face edge, up to level 24). The ids are stateless integers independent
of any stored data -- useful as chunk keys for streaming and
level-of-detail.
my $cell = $s->cube_cell($x, $y, $z, $level); # direction (need not be unit length)
my $cell = $s->cube_cell_geo($lat, $lon, $level);
my @adj = $s->cube_neighbors($cell); # 4 edge-adjacent cells, seam-aware
my $up = $s->cube_parent($cell); # coarser cell (undef at level 0)
my @kids = $s->cube_children($cell); # 4 finer cells (empty at level 24)
my $lvl = $s->cube_level($cell);
my ($x, $y, $z) = $s->cube_center($cell); # cell centre as a unit vector
my ($lat, $lon) = $s->cube_center_geo($cell);
A cell id packs "(level, face, i, j)" into an unsigned integer, so cells
at different levels are different ids. "cube_neighbors" returns the four
edge-adjacent cells, correct across face seams; diagonal/corner
neighbours are not included. These methods read no map state (any handle
provides them), and a malformed cell id croaks; a zero or non-finite
direction yields an arbitrary but valid cell. The grid is near-uniform
(equal-angle), not equal-area.
Introspection
$s->count; # live entry count
$s->max_entries; # capacity
$s->num_buckets; # bucket table size
$s->cell_size; # cell size in world units
my @w = $s->world; # wrap extents: (Wx,Wy) or (Wx,Wy,Wz); empty if not toroidal
my $R = $s->sphere; # body radius (sphere => $R), or 0 if not a sphere map
$s->stats; # diagnostic hashref (see STATS)
Lifecycle
$s->path; # backing file path, or undef for anon/memfd
$s->memfd; # memfd fd (-1 for file-backed/anon)
$s->sync; # msync mmap to backing store
$s->unlink; # remove backing file
Class->unlink($path); # class-method form
"sync" and "unlink" croak on OS failure.
Event Loop Integration
my $fd = $s->eventfd; # lazy-create eventfd, returns fd
$s->eventfd_set($fd); # attach an external eventfd
my $fd = $s->fileno; # current eventfd fd, or -1
$s->notify; # write 1 to eventfd (signal update)
my $n = $s->eventfd_consume; # read+reset eventfd counter
"eventfd_set" attaches an external eventfd, closing the
previously-attached fd (such as one created by "eventfd") unless it is
the same descriptor. "notify" returns false if no eventfd is attached,
true after writing the signal. "eventfd_consume" returns the counter as
an integer, or "undef" if no eventfd is attached or nothing is pending
(a spurious wakeup).
TUNING
Choose "cell_size" close to your typical query radius. A value too small
means many cells are scanned per radius query; a value too large packs
many entries into each cell and increases false-positive tests.
Choose "num_buckets" to be a power of two slightly above the expected
peak live count; any value you pass is rounded up to the next power of
two. The default (0 = auto) picks a power of two near "max_entries",
which is a safe starting point. After loading real data, inspect
( run in 0.498 second using v1.01-cache-2.11-cpan-9581c071862 )