File Coverage

blib/lib/Algorithm/SpatialIndex/Strategy/QuadTree.pm
Criterion Covered Total %
statement 161 176 91.4
branch 39 52 75.0
condition 21 26 80.7
subroutine 22 23 95.6
pod 6 6 100.0
total 249 283 87.9


line stmt bran cond sub pod time code
1             package Algorithm::SpatialIndex::Strategy::QuadTree;
2 7     7   14173 use 5.008001;
  7         26  
  7         261  
3 7     7   37 use strict;
  7         14  
  7         236  
4 7     7   38 use warnings;
  7         148  
  7         200  
5 7     7   41 use Carp qw(croak);
  7         11  
  7         426  
6              
7 7     7   2092 use parent 'Algorithm::SpatialIndex::Strategy::2D';
  7         1308  
  7         34  
8              
9             # Note that the subnode indexes are as follows:
10             # (like quadrants in planar geometry)
11             #
12             # /---\
13             # |1|0|
14             # |-+-|
15             # |2+3|
16             # \---/
17             #
18              
19             use constant {
20 7         1178 XI => 1, # item X coord index
21             YI => 2, # item Y coord index
22              
23             XLOW => 0, # for access to node coords
24             YLOW => 1,
25             XUP => 2,
26             YUP => 3,
27             XSPLIT => 4,
28             YSPLIT => 5,
29              
30             UPPER_RIGHT_NODE => 0,
31             UPPER_LEFT_NODE => 1,
32             LOWER_LEFT_NODE => 2,
33             LOWER_RIGHT_NODE => 3,
34 7     7   595 };
  7         12  
35              
36 7     7   40 use Exporter 'import';
  7         12  
  7         703  
37             our @EXPORT_OK = qw(
38             XI
39             YI
40              
41             XLOW
42             YLOW
43             XUP
44             YUP
45             XSPLIT
46             YSPLIT
47              
48             UPPER_RIGHT_NODE
49             UPPER_LEFT_NODE
50             LOWER_LEFT_NODE
51             LOWER_RIGHT_NODE
52             );
53             our %EXPORT_TAGS = ('all' => \@EXPORT_OK);
54              
55             use Class::XSAccessor {
56 7         80 getters => [qw(
57             top_node_id
58             bucket_size
59             max_depth
60             total_width
61             )],
62 7     7   36 };
  7         13  
63              
64 1     1 1 12 sub coord_types { qw(double double double double double double) }
65              
66             sub init {
67 2     2 1 7 my $self = shift;
68             }
69              
70             sub init_storage {
71 2     2 1 4 my $self = shift;
72 2         16 my $index = $self->index;
73 2         13 my $storage = $self->storage;
74              
75             # stored bucket_size/max_depth for persistent indexes
76 2         9 $self->{bucket_size} = $storage->get_option('bucket_size');
77 2         8 $self->{max_depth} = $storage->get_option('max_depth');
78             # or use configured ones
79 2 50       16 $self->{bucket_size} = $index->bucket_size if not defined $self->bucket_size;
80 2 50       15 $self->{max_depth} = $index->max_depth if not defined $self->max_depth;
81              
82 2         7 $self->{top_node_id} = $storage->get_option('top_node_id');
83 2 50       10 if (not defined $self->top_node_id) {
84             # create a new top node and its bucket
85 2         34 my $node = Algorithm::SpatialIndex::Node->new(
86             coords => [
87             $index->limit_x_low, $index->limit_y_low,
88             $index->limit_x_up, $index->limit_y_up,
89             undef, undef,
90             ],
91             subnode_ids => [],
92             );
93 2         9 $self->{top_node_id} = $storage->store_node($node);
94 2         9 $self->_make_bucket_for_node($node, $storage);
95             }
96              
97 2         21 $self->{total_width} = $index->limit_x_up - $index->limit_x_low;
98             }
99              
100             sub insert {
101 143     143 1 220 my ($self, $id, $x, $y) = @_;
102 143         188 my $storage = $self->{storage}; # hash access due to hot path
103 143         392 my $top_node = $storage->fetch_node($self->{top_node_id}); # hash access due to hot path
104 143         297 return $self->_insert($id, $x, $y, $top_node, $storage);
105             }
106              
107 7         10089 SCOPE: {
108 7     7   4993 no warnings 'recursion';
  7         17  
109             sub _insert {
110 454     454   615 my ($self, $id, $x, $y, $node, $storage) = @_;
111 454         617 my $nxy = $node->coords;
112 454         584 my $subnodes = $node->subnode_ids;
113              
114             # If we have a bucket, we are the last level of nodes
115 454         1331 SCOPE: {
116 454         474 my $bucket = $storage->fetch_bucket($node->id);
117 454 100       915 if (defined $bucket) {
118 162 100 66     408 if ($bucket->nitems < $self->{bucket_size}) {
    100          
119             # sufficient space in bucket. Insert and return
120 135         439 $bucket->add_items([$id, $x, $y]);
121 135         338 $storage->store_bucket($bucket);
122 135         575 return();
123             }
124             # check whether we've reached the maximum depth of the tree
125             # and ignore bucket size if necessary
126             # ( total width / local width ) = 2^( depth )
127             elsif ($nxy->[XUP] - $nxy->[XLOW] <= 0.
128             or log($self->total_width / ($nxy->[XUP]-$nxy->[XLOW])) / log(2) >= $self->max_depth)
129             {
130             # bucket at the maximum depth. Insert and return
131 8         27 $bucket->add_items([$id, $x, $y]);
132 8         54 $storage->store_bucket($bucket);
133 8         34 return();
134             }
135             else {
136             # bucket full, need to add new layer of nodes and split the bucket
137 19         49 $self->_split_node($node, $bucket);
138             # refresh data that will have changed:
139 19         60 $node = $storage->fetch_node($node->id); # has updated subnode ids
140 19         83 $subnodes = $node->subnode_ids;
141             # Now we just continue with the normal subnode checking below:
142             }
143             }
144             } # end scope
145              
146 311         327 my $subnode_index;
147 311 100       543 if ($x <= $nxy->[XSPLIT]) {
148 124 100       194 if ($y <= $nxy->[YSPLIT]) { $subnode_index = LOWER_LEFT_NODE }
  60         76  
149 64         89 else { $subnode_index = UPPER_LEFT_NODE }
150             }
151             else {
152 187 100       318 if ($y <= $nxy->[YSPLIT]) { $subnode_index = LOWER_RIGHT_NODE }
  90         106  
153 97         125 else { $subnode_index = UPPER_RIGHT_NODE }
154             }
155              
156 311 50       537 if (not defined $subnodes->[$subnode_index]) {
157 0         0 die("Cannot find subnode $subnode_index if node id=".$node->id);
158             }
159             else {
160 311         757 my $subnode = $storage->fetch_node($subnodes->[$subnode_index]);
161 311 50       596 die("Need node '" .$subnodes->[$subnode_index] . '", but it is not in storage!')
162             if not defined $subnode;
163 311         596 return $self->_insert($id, $x, $y, $subnode, $storage);
164             }
165             }
166             } # end SCOPE
167              
168             sub _node_split_coords {
169             # args: $self, $node, $bucket, $coords
170 20     20   30 my $c = $_[3];
171 20         64 return( ($c->[0]+$c->[2])/2, ($c->[1]+$c->[3])/2 );
172             }
173              
174              
175             # Splits the given node into four new nodes of equal
176             # size and assigns the items
177             sub _split_node {
178 19     19   23 my $self = shift;
179 19         37 my $parent_node = shift;
180 19         22 my $bucket = shift; # just for speed, can be taken from parent_node
181              
182 19         44 my $storage = $self->storage;
183 19         30 my $parent_node_id = $parent_node->id;
184 19 50       37 $bucket = $storage->fetch_bucket($parent_node_id) if not defined $bucket;
185              
186 19         27 my $coords = $parent_node->coords;
187 19         39 my ($splitx, $splity) = $self->_node_split_coords($parent_node, $bucket, $coords);
188 19         58 @$coords[XSPLIT, YSPLIT] = ($splitx, $splity); # stored below
189 19         27 my @child_nodes;
190              
191             # UPPER_RIGHT_NODE => 0
192 19         122 push @child_nodes, Algorithm::SpatialIndex::Node->new(
193             coords => [$splitx, $splity, $coords->[XUP], $coords->[YUP], undef, undef],
194             subnode_ids => [],
195             );
196             # UPPER_LEFT_NODE => 1
197 19         136 push @child_nodes, Algorithm::SpatialIndex::Node->new(
198             coords => [$coords->[XLOW], $splity, $splitx, $coords->[YUP], undef, undef],
199             subnode_ids => [],
200             );
201             # LOWER_LEFT_NODE => 2
202 19         140 push @child_nodes, Algorithm::SpatialIndex::Node->new(
203             coords => [$coords->[XLOW], $coords->[YLOW], $splitx, $splity, undef, undef],
204             subnode_ids => [],
205             );
206             # LOWER_RIGHT_NODE => 3
207 19         114 push @child_nodes, Algorithm::SpatialIndex::Node->new(
208             coords => [$splitx, $coords->[YLOW], $coords->[XUP], $splity, undef, undef],
209             subnode_ids => [],
210             );
211              
212             # save nodes
213 19         41 my $snode_ids = $parent_node->subnode_ids;
214 19         30 foreach my $cnode (@child_nodes) {
215 76         88 push @{$snode_ids}, $storage->store_node($cnode);
  76         220  
216             }
217 19         57 $storage->store_node($parent_node);
218              
219             # split bucket
220 19         36 my $items = $bucket->items;
221 19         58 my @child_items = ([], [], [], []);
222 19         34 foreach my $item (@$items) {
223 89 100       167 if ($item->[XI] <= $splitx) {
224 86 100       161 if ($item->[YI] <= $splity) { push @{$child_items[LOWER_LEFT_NODE]}, $item }
  54         54  
  54         120  
225 32         35 else { push @{$child_items[UPPER_LEFT_NODE]}, $item }
  32         74  
226             }
227             else {
228 3 50       9 if ($item->[YI] <= $splity) { push @{$child_items[LOWER_RIGHT_NODE]}, $item }
  3         11  
  3         9  
229 0         0 else { push @{$child_items[UPPER_RIGHT_NODE]}, $item }
  0         0  
230             }
231             }
232            
233             # generate buckets
234 19         35 foreach my $subnode_idx (0..3) {
235 76         177 $self->_make_bucket_for_node(
236             $child_nodes[$subnode_idx],
237             $storage,
238             $child_items[$subnode_idx]
239             );
240             }
241              
242             # remove the parent node's bucket
243 19         56 $storage->delete_bucket($bucket);
244             }
245              
246             sub _make_bucket_for_node {
247 78     78   102 my $self = shift;
248 78         85 my $node_id = shift;
249 78   33     147 my $storage = shift || $self->storage;
250 78   100     156 my $items = shift || [];
251 78 50       211 $node_id = $node_id->id if ref $node_id;
252              
253 78         424 my $b = $storage->bucket_class->new(
254             node_id => $node_id,
255             items => $items,
256             );
257 78         208 $storage->store_bucket($b);
258             }
259              
260              
261             sub find_node_for {
262 10     10 1 6282 my ($self, $x, $y) = @_;
263 10         27 my $storage = $self->storage;
264 10         44 my $topnode = $storage->fetch_node($self->top_node_id);
265 10         29 my $coords = $topnode->coords;
266              
267             # boundary check
268 10 50 100     118 if ($x < $coords->[XLOW]
      66        
      66        
269             or $x > $coords->[XUP]
270             or $y < $coords->[YLOW]
271             or $y > $coords->[YUP]) {
272 3         14 return undef;
273             }
274              
275 7         22 return $self->_find_node_for($x, $y, $storage, $topnode);
276             }
277              
278             # TODO: This is almost trivial to rewrite in non-recursive form
279 7         5094 SCOPE: {
280 7     7   42 no warnings 'recursion';
  7         13  
281             sub _find_node_for {
282 27     27   146 my ($self, $x, $y, $storage, $node) = @_;
283              
284 27         57 my $snode_ids = $node->subnode_ids;
285 27 100       87 return $node if not @$snode_ids;
286              
287             # find the right sub node
288 20         25 my ($splitx, $splity) = @{$node->coords}[XSPLIT, YSPLIT];
  20         53  
289 20         22 my $subnode_id;
290 20 100       41 if ($x <= $splitx) {
291 11 100       24 if ($y <= $splity) { $subnode_id = $snode_ids->[LOWER_LEFT_NODE] }
  7         24  
292 4         7 else { $subnode_id = $snode_ids->[UPPER_LEFT_NODE] }
293             }
294             else {
295 9 100       22 if ($y <= $splity) { $subnode_id = $snode_ids->[LOWER_RIGHT_NODE] }
  5         10  
296 4         7 else { $subnode_id = $snode_ids->[UPPER_RIGHT_NODE] }
297             }
298              
299 20         52 my $snode = $storage->fetch_node($subnode_id);
300 20         51 return $self->_find_node_for($x, $y, $storage, $snode);
301             }
302             } # end SCOPE
303              
304              
305             sub find_nodes_for {
306 6     6 1 7990 my ($self, $x1, $y1, $x2, $y2) = @_;
307              
308             # normalize coords
309 6 50       22 my ($xl, $xu) = $x1 < $x2 ? ($x1, $x2) : ($x2, $x1);
310 6 50       17 my ($yl, $yu) = $y1 < $y2 ? ($y1, $y2) : ($y2, $y1);
311              
312 6         19 my $storage = $self->storage;
313 6         28 my $topnode = $storage->fetch_node($self->top_node_id);
314 6         18 my $coords = $topnode->coords;
315              
316 6         10 my $rv = [];
317 6         17 _find_nodes_for($self, $xl, $yl, $xu, $yu, $storage, $topnode, $rv);
318 6         76 return @$rv;
319             }
320              
321             sub _find_nodes_for {
322 338     338   505 my ($self, $xl, $yl, $xu, $yu, $storage, $node, $rv) = @_;
323            
324 338         481 my $coords = $node->coords;
325              
326             # boundary check
327 338 100 100     2881 if ( $xu < $coords->[XLOW]
      100        
      100        
328             or $xl > $coords->[XUP]
329             or $yu < $coords->[YLOW]
330             or $yl > $coords->[YUP])
331             {
332 21         53 return;
333             }
334              
335 317         707 my $snode_ids = $node->subnode_ids;
336 317 100       583 if (not @$snode_ids) {
337             # leaf
338 234         299 push @$rv, $node;
339 234         525 return;
340             }
341              
342             # not a leaf
343 83         114 foreach my $id (@$snode_ids) {
344 332         1466 $self->_find_nodes_for(
345             $xl, $yl, $xu, $yu, $storage,
346             $storage->fetch_node($id),
347             $rv
348             );
349             }
350             }
351              
352             # Returns the leaves for the given node
353             sub _get_all_leaf_nodes {
354 0     0     my $self = shift;
355 0           my $node = shift;
356 0           my $storage = $self->storage;
357              
358 0           my @leaves;
359 0           my @nodes = ($node);
360 0           while (@nodes) {
361 0           $node = shift @nodes;
362 0           my $snode_ids = $node->subnode_ids;
363 0 0         if (@$snode_ids) {
364 0           push @nodes, map $storage->fetch_node($_), @$snode_ids;
365             }
366             else {
367 0           push @leaves, $node;
368             }
369             }
370              
371 0           return @leaves;
372             }
373              
374             1;
375             __END__