File Coverage

blib/lib/Algorithm/SpatialIndex/Strategy/OctTree.pm
Criterion Covered Total %
statement 194 212 91.5
branch 64 78 82.0
condition 27 38 71.0
subroutine 22 23 95.6
pod 6 6 100.0
total 313 357 87.6


line stmt bran cond sub pod time code
1             package Algorithm::SpatialIndex::Strategy::OctTree;
2 7     7   12799 use 5.008001;
  7         23  
  7         290  
3 7     7   37 use strict;
  7         12  
  7         189  
4 7     7   34 use warnings;
  7         61  
  7         208  
5 7     7   37 use Carp qw(croak);
  7         10  
  7         433  
6              
7 7     7   1755 use parent 'Algorithm::SpatialIndex::Strategy::3D';
  7         643  
  7         35  
8              
9             # Note that the subnode indexes are as follows:
10             # (like octants, http://en.wikipedia.org/wiki/Octant)
11             # After wikipedia:
12             #
13             # 0) first octant (+, +, +)
14             # 1) top-back-right (−, +, +)
15             # 2) top-back-left (−, −, +)
16             # 3) top-front-left (+, −, +)
17             # 4) bottom-front-left (+, −, −)
18             # 5) bottom-back-left (−, −, −)
19             # 6) bottom-back-right (−, +, −)
20             # 7) bottom-front-right (+, +, −)
21              
22              
23             use constant {
24 7         1808 XI => 1, # item X coord index
25             YI => 2, # item Y coord index
26             ZI => 3, # item Z coord index
27              
28             XLOW => 0, # for access to node coords
29             YLOW => 1,
30             ZLOW => 2,
31             XUP => 3,
32             YUP => 4,
33             ZUP => 5,
34             XSPLIT => 6,
35             YSPLIT => 7,
36             ZSPLIT => 8,
37              
38             PPP_NODE => 0,
39             MPP_NODE => 1,
40             MMP_NODE => 2,
41             PMP_NODE => 3,
42             PMM_NODE => 4,
43             MMM_NODE => 5,
44             MPM_NODE => 6,
45             PPM_NODE => 7,
46 7     7   629 };
  7         18  
47              
48 7     7   40 use Exporter 'import';
  7         17  
  7         678  
49             our @EXPORT_OK = qw(
50             XI
51             YI
52             ZI
53              
54             XLOW
55             YLOW
56             ZLOW
57             XUP
58             YUP
59             ZUP
60             XSPLIT
61             YSPLIT
62             ZSPLIT
63              
64             PPP_NODE
65             MPP_NODE
66             MMP_NODE
67             PMP_NODE
68             PMM_NODE
69             MMM_NODE
70             MPM_NODE
71             PPM_NODE
72             );
73             our %EXPORT_TAGS = ('all' => \@EXPORT_OK);
74              
75             use Class::XSAccessor {
76 7         79 getters => [qw(
77             top_node_id
78             bucket_size
79             max_depth
80             total_width
81             )],
82 7     7   38 };
  7         12  
83              
84 1     1 1 14 sub coord_types { qw(double double double double double double double double double) } # 9 doubles
85              
86 1     1 1 3 sub init {}
87              
88             sub init_storage {
89 1     1 1 3 my $self = shift;
90 1         8 my $index = $self->index;
91 1         8 my $storage = $self->storage;
92              
93             # stored bucket_size for persistent indexes
94 1         3 $self->{bucket_size} = $storage->get_option('bucket_size');
95 1         4 $self->{max_depth} = $storage->get_option('max_depth');
96             # or use configured one
97 1 50       9 $self->{bucket_size} = $index->bucket_size if not defined $self->bucket_size;
98 1 50       8 $self->{max_depth} = $index->max_depth if not defined $self->max_depth;
99              
100 1         4 $self->{top_node_id} = $storage->get_option('top_node_id');
101 1 50       6 if (not defined $self->top_node_id) {
102             # create a new top node and its bucket
103 1         22 my $node = Algorithm::SpatialIndex::Node->new(
104             coords => [
105             $index->limit_x_low, $index->limit_y_low, $index->limit_z_low,
106             $index->limit_x_up, $index->limit_y_up, $index->limit_z_up,
107             undef, undef, undef,
108             ],
109             subnode_ids => [],
110             );
111 1         5 $self->{top_node_id} = $storage->store_node($node);
112 1         6 $self->_make_bucket_for_node($node, $storage);
113             }
114              
115 1         8 $self->{total_width} = $index->limit_x_up - $index->limit_x_low;
116             }
117              
118             sub insert {
119 240     240 1 332 my ($self, $id, $x, $y, $z) = @_;
120 240         332 my $storage = $self->{storage}; # hash access due to hot path
121 240         664 my $top_node = $storage->fetch_node($self->{top_node_id}); # hash access due to hot path
122 240         541 return $self->_insert($id, $x, $y, $z, $top_node, $storage);
123             }
124              
125 7         13807 SCOPE: {
126 7     7   4798 no warnings 'recursion';
  7         26  
127             sub _insert {
128 691     691   1100 my ($self, $id, $x, $y, $z, $node, $storage) = @_;
129 691         966 my $nxyz = $node->coords;
130 691         838 my $subnodes = $node->subnode_ids;
131              
132             # If we have a bucket, we are the last level of nodes
133 691         1852 SCOPE: {
134 691         713 my $bucket = $storage->fetch_bucket($node->id);
135 691 100       1475 if (defined $bucket) {
136 265         411 my $items = $bucket->items;
137 265 100 33     733 if (@$items < $self->{bucket_size}) {
    50          
138             # sufficient space in bucket. Insert and return
139 240         238 push @{$items}, [$id, $x, $y, $z];
  240         655  
140 240         662 $storage->store_bucket($bucket);
141 240         1042 return();
142             }
143             # check whether we've reached the maximum depth of the tree
144             # and ignore bucket size if necessary
145             # ( total width / local width ) = 2^( depth )
146             elsif ($nxyz->[XUP] - $nxyz->[XLOW] <= 0.
147             or log($self->total_width / ($nxyz->[XUP]-$nxyz->[XLOW])) / log(2) >= $self->max_depth)
148             {
149             # bucket at the maximum depth. Insert and return
150 0         0 push @{$items}, [$id, $x, $y];
  0         0  
151 0         0 $storage->store_bucket($bucket);
152 0         0 return();
153             }
154             else {
155             # bucket full, need to add new layer of nodes and split the bucket
156 25         60 $self->_split_node($node, $bucket);
157             # refresh data that will have changed:
158 25         86 $node = $storage->fetch_node($node->id); # has updated subnode ids
159 25         107 $subnodes = $node->subnode_ids;
160             # Now we just continue with the normal subnode checking below:
161             }
162             }
163             } # end scope
164              
165 451         453 my $subnode_index;
166 451 100       787 if ($x <= $nxyz->[XSPLIT]) {
167 203 100       316 if ($y <= $nxyz->[YSPLIT]) {
168 75 100       112 if ($z <= $nxyz->[ZSPLIT]) { $subnode_index = MMM_NODE }
  33         42  
169 42         55 else { $subnode_index = MMP_NODE }
170             }
171             else { # $y > ysplit
172 128 100       191 if ($z <= $nxyz->[ZSPLIT]) { $subnode_index = MPM_NODE }
  60         89  
173 68         89 else { $subnode_index = MPP_NODE }
174             }
175             }
176             else { # $x > xsplit
177 248 100       423 if ($y <= $nxyz->[YSPLIT]) {
178 120 100       183 if ($z <= $nxyz->[ZSPLIT]) { $subnode_index = PMM_NODE }
  60         80  
179 60         198 else { $subnode_index = PMP_NODE }
180             }
181             else { # $y > ysplit
182 128 100       209 if ($z <= $nxyz->[ZSPLIT]) { $subnode_index = PPM_NODE }
  60         83  
183 68         94 else { $subnode_index = PPP_NODE }
184             }
185             }
186              
187 451 50       724 if (not defined $subnodes->[$subnode_index]) {
188 0         0 die("Cannot find subnode $subnode_index if node id=".$node->id);
189             }
190             else {
191 451         1174 my $subnode = $storage->fetch_node($subnodes->[$subnode_index]);
192 451 50       927 die("Need node '" .$subnodes->[$subnode_index] . '", but it is not in storage!')
193             if not defined $subnode;
194 451         968 return $self->_insert($id, $x, $y, $z, $subnode, $storage);
195             }
196             }
197             } # end SCOPE
198              
199             sub _node_split_coords {
200             # args: $self, $node, $bucket, $coords
201 26     26   33 my $c = $_[3];
202             return(
203 26         110 ($c->[XLOW]+$c->[XUP])/2,
204             ($c->[YLOW]+$c->[YUP])/2,
205             ($c->[ZLOW]+$c->[ZUP])/2,
206             );
207             }
208              
209              
210             # Splits the given node into four new nodes of equal
211             # size and assigns the items
212             sub _split_node {
213 25     25   33 my $self = shift;
214 25         25 my $parent_node = shift;
215 25         30 my $bucket = shift; # just for speed, can be taken from parent_node
216              
217 25         41 my $storage = $self->storage;
218 25         40 my $parent_node_id = $parent_node->id;
219 25 50       45 $bucket = $storage->fetch_bucket($parent_node_id) if not defined $bucket;
220              
221 25         41 my $coords = $parent_node->coords;
222 25         53 my ($splitx, $splity, $splitz) = $self->_node_split_coords($parent_node, $bucket, $coords);
223 25         68 @$coords[XSPLIT, YSPLIT, ZSPLIT] = ($splitx, $splity, $splitz); # stored below
224 25         39 my @child_nodes;
225              
226             # PPP_NODE
227 25         198 push @child_nodes, Algorithm::SpatialIndex::Node->new(
228             coords => [$splitx, $splity, $splitz,
229             $coords->[XUP], $coords->[YUP], $coords->[ZUP],
230             undef, undef, undef],
231             subnode_ids => [],
232             );
233             # MPP_NODE
234 25         198 push @child_nodes, Algorithm::SpatialIndex::Node->new(
235             coords => [$coords->[XLOW], $splity, $splitz,
236             $splitx, $coords->[YUP], $coords->[ZUP],
237             undef, undef, undef],
238             subnode_ids => [],
239             );
240             # MMP_NODE
241 25         186 push @child_nodes, Algorithm::SpatialIndex::Node->new(
242             coords => [$coords->[XLOW], $coords->[YLOW], $splitz,
243             $splitx, $splity, $coords->[ZUP],
244             undef, undef, undef],
245             subnode_ids => [],
246             );
247             # PMP_NODE
248 25         182 push @child_nodes, Algorithm::SpatialIndex::Node->new(
249             coords => [$splitx, $coords->[YLOW], $splitz,
250             $coords->[XUP], $splity, $coords->[ZUP],
251             undef, undef, undef],
252             subnode_ids => [],
253             );
254             # PMM_NODE
255 25         201 push @child_nodes, Algorithm::SpatialIndex::Node->new(
256             coords => [$splitx, $coords->[YLOW], $coords->[ZLOW],
257             $coords->[XUP], $splity, $splitz,
258             undef, undef, undef],
259             subnode_ids => [],
260             );
261             # MMM_NODE
262 25         186 push @child_nodes, Algorithm::SpatialIndex::Node->new(
263             coords => [$coords->[XLOW], $coords->[YLOW], $coords->[ZLOW],
264             $splitx, $splity, $splitz,
265             undef, undef, undef],
266             subnode_ids => [],
267             );
268             # MPM_NODE
269 25         171 push @child_nodes, Algorithm::SpatialIndex::Node->new(
270             coords => [$coords->[XLOW], $splity, $coords->[ZLOW],
271             $splitx, $coords->[YUP], $splitz,
272             undef, undef, undef],
273             subnode_ids => [],
274             );
275             # PPM_NODE
276 25         196 push @child_nodes, Algorithm::SpatialIndex::Node->new(
277             coords => [$splitx, $splity, $coords->[ZLOW],
278             $coords->[XUP], $coords->[YUP], $splitz,
279             undef, undef, undef],
280             subnode_ids => [],
281             );
282              
283             # save nodes
284 25         48 my $snode_ids = $parent_node->subnode_ids;
285 25         41 foreach my $cnode (@child_nodes) {
286 200         208 push @{$snode_ids}, $storage->store_node($cnode);
  200         569  
287             }
288 25         81 $storage->store_node($parent_node);
289              
290             # split bucket
291 25         70 my $items = $bucket->items;
292 25         170 my @child_items = ( map [], @child_nodes );
293 25         52 foreach my $item (@$items) {
294 125 100       228 if ($item->[XI] <= $splitx) {
295 85 100       122 if ($item->[YI] <= $splity) {
296 69 100       113 if ($item->[ZI] <= $splitz) { push @{$child_items[MMM_NODE]}, $item }
  39         39  
  39         83  
297 30         32 else { push @{$child_items[MMP_NODE]}, $item }
  30         70  
298             }
299             else { # $item->[YI] > ysplit
300 16 100       25 if ($item->[ZI] <= $splitz) { push @{$child_items[MPM_NODE]}, $item }
  12         13  
  12         27  
301 4         5 else { push @{$child_items[MPP_NODE]}, $item }
  4         9  
302             }
303             }
304             else { # $item->[XI] > xsplit
305 40 100       72 if ($item->[YI] <= $splity) {
306 24 100       40 if ($item->[ZI] <= $splitz) { push @{$child_items[PMM_NODE]}, $item }
  12         11  
  12         37  
307 12         11 else { push @{$child_items[PMP_NODE]}, $item }
  12         27  
308             }
309             else { # $item->[YI] > ysplit
310 16 100       31 if ($item->[ZI] <= $splitz) { push @{$child_items[PPM_NODE]}, $item }
  12         14  
  12         44  
311 4         4 else { push @{$child_items[PPP_NODE]}, $item }
  4         13  
312             }
313             }
314             }
315            
316             # generate buckets
317 25         60 foreach my $subnode_idx (0..$#child_nodes) {
318 200         464 $self->_make_bucket_for_node(
319             $child_nodes[$subnode_idx],
320             $storage,
321             $child_items[$subnode_idx]
322             );
323             }
324              
325             # remove the parent node's bucket
326 25         83 $storage->delete_bucket($bucket);
327             }
328              
329             sub _make_bucket_for_node {
330 201     201   229 my $self = shift;
331 201         209 my $node_id = shift;
332 201   33     382 my $storage = shift || $self->storage;
333 201   100     358 my $items = shift || [];
334 201 50       477 $node_id = $node_id->id if ref $node_id;
335              
336 201         1056 my $b = $storage->bucket_class->new(
337             node_id => $node_id,
338             items => $items,
339             );
340 201         572 $storage->store_bucket($b);
341             }
342              
343              
344             sub find_node_for {
345 10     10 1 4392 my ($self, $x, $y, $z) = @_;
346 10         28 my $storage = $self->storage;
347 10         43 my $topnode = $storage->fetch_node($self->top_node_id);
348 10         23 my $coords = $topnode->coords;
349              
350             # boundary check
351 10 50 100     138 if ($x < $coords->[XLOW]
      66        
      66        
      33        
      33        
352             or $x > $coords->[XUP]
353             or $y < $coords->[YLOW]
354             or $y > $coords->[YUP]
355             or $z < $coords->[ZLOW]
356             or $z > $coords->[ZUP])
357             {
358 3         18 return undef;
359             }
360              
361 7         26 return $self->_find_node_for($x, $y, $z, $storage, $topnode);
362             }
363              
364             # TODO: This is almost trivial to rewrite in non-recursive form
365 7         7166 SCOPE: {
366 7     7   54 no warnings 'recursion';
  7         21  
367             sub _find_node_for {
368 25     25   68 my ($self, $x, $y, $z, $storage, $node) = @_;
369              
370 25         59 my $snode_ids = $node->subnode_ids;
371 25 100       85 return $node if not @$snode_ids;
372              
373             # find the right sub node
374 18         23 my ($xsplit, $ysplit, $zsplit) = @{$node->coords}[XSPLIT, YSPLIT, ZSPLIT];
  18         60  
375 18         23 my $subnode_id;
376 18 100       39 if ($x <= $xsplit) {
377 10 100       22 if ($y <= $ysplit) {
378 7 100       16 if ($z <= $zsplit) { $subnode_id = $snode_ids->[MMM_NODE] }
  1         4  
379 6         14 else { $subnode_id = $snode_ids->[MMP_NODE] }
380             }
381             else { # $y > ysplit
382 3 50       8 if ($z <= $zsplit) { $subnode_id = $snode_ids->[MPM_NODE] }
  0         0  
383 3         7 else { $subnode_id = $snode_ids->[MPP_NODE] }
384             }
385             }
386             else { # $x > xsplit
387 8 100       20 if ($y <= $ysplit) {
388 5 100       12 if ($z <= $zsplit) { $subnode_id = $snode_ids->[PMM_NODE] }
  4         9  
389 1         4 else { $subnode_id = $snode_ids->[PMP_NODE] }
390             }
391             else { # $y > ysplit
392 3 100       8 if ($z <= $zsplit) { $subnode_id = $snode_ids->[PPM_NODE] }
  1         4  
393 2         5 else { $subnode_id = $snode_ids->[PPP_NODE] }
394             }
395             }
396              
397 18         58 my $snode = $storage->fetch_node($subnode_id);
398 18         66 return $self->_find_node_for($x, $y, $z, $storage, $snode);
399             }
400             } # end SCOPE
401              
402              
403             sub find_nodes_for {
404 6     6 1 1953 my ($self, $x1, $y1, $z1, $x2, $y2, $z2) = @_;
405              
406             # normalize coords
407 6 50       21 my ($xl, $xu) = $x1 < $x2 ? ($x1, $x2) : ($x2, $x1);
408 6 50       19 my ($yl, $yu) = $y1 < $y2 ? ($y1, $y2) : ($y2, $y1);
409 6 100       20 my ($zl, $zu) = $z1 < $z2 ? ($z1, $z2) : ($z2, $z1);
410              
411 6         19 my $storage = $self->storage;
412 6         26 my $topnode = $storage->fetch_node($self->top_node_id);
413 6         16 my $coords = $topnode->coords;
414              
415 6         12 my $rv = [];
416 6         17 _find_nodes_for($self, $xl, $yl, $zl, $xu, $yu, $zu, $storage, $topnode, $rv);
417 6         152 return @$rv;
418             }
419              
420             sub _find_nodes_for {
421 806     806   1602 my ($self, $xl, $yl, $zl, $xu, $yu, $zu, $storage, $node, $rv) = @_;
422            
423 806         1421 my $coords = $node->coords;
424              
425             # boundary check
426 806 100 100     9821 if ( $xu < $coords->[XLOW]
      100        
      66        
      100        
      100        
427             or $xl > $coords->[XUP]
428             or $yu < $coords->[YLOW]
429             or $yl > $coords->[YUP]
430             or $zu < $coords->[ZLOW]
431             or $zl > $coords->[ZUP])
432             {
433 62         161 return;
434             }
435              
436 744         1227 my $snode_ids = $node->subnode_ids;
437 744 100       1516 if (not @$snode_ids) {
438             # leaf
439 644         963 push @$rv, $node;
440 644         1753 return;
441             }
442              
443             # not a leaf
444 100         187 foreach my $id (@$snode_ids) {
445 800         2434 $self->_find_nodes_for(
446             $xl, $yl, $zl, $xu, $yu, $zu, $storage,
447             $storage->fetch_node($id),
448             $rv
449             );
450             }
451             }
452              
453             # Returns the leaves for the given node
454             sub _get_all_leaf_nodes {
455 0     0     my $self = shift;
456 0           my $node = shift;
457 0           my $storage = $self->storage;
458              
459 0           my @leaves;
460 0           my @nodes = ($node);
461 0           while (@nodes) {
462 0           $node = shift @nodes;
463 0           my $snode_ids = $node->subnode_ids;
464 0 0         if (@$snode_ids) {
465 0           push @nodes, map $storage->fetch_node($_), @$snode_ids;
466             }
467             else {
468 0           push @leaves, $node;
469             }
470             }
471              
472 0           return @leaves;
473             }
474              
475             1;
476             __END__