File Coverage

blib/lib/Algorithm/QuadTree/PP.pm
Criterion Covered Total %
statement 135 138 97.8
branch 54 62 87.1
condition 14 15 93.3
subroutine 19 19 100.0
pod n/a
total 222 234 94.8


line stmt bran cond sub pod time code
1             package Algorithm::QuadTree::PP;
2             $Algorithm::QuadTree::PP::VERSION = '0.9';
3 8     8   51 use strict;
  8         16  
  8         256  
4 8     8   32 use warnings;
  8         15  
  8         382  
5 8     8   40 use Exporter qw(import);
  8         13  
  8         273  
6              
7 8     8   47 use Scalar::Util qw(weaken);
  8         38  
  8         842  
8              
9             our @EXPORT = qw(
10             _AQT_init
11             _AQT_deinit
12             _AQT_addObject
13             _AQT_findObjects
14             _AQT_delete
15             _AQT_clear
16             );
17              
18 8     8   90 use constant UNIQUE_RESULTS => 1;
  8         31  
  8         837  
19              
20 8     8   49 use constant SHAPE_CIRCLE => 1;
  8         13  
  8         401  
21 8     8   57 use constant SHAPE_RECTANGLE => 2;
  8         13  
  8         13226  
22              
23             sub _buildShape
24             {
25 227     227   494 my (@coords) = @_;
26 227         498 pop @coords while @coords > 4;
27              
28 227 100       460 my $shape_type = @coords == 3 ? SHAPE_CIRCLE : SHAPE_RECTANGLE;
29              
30             # pre-calculate some of the circle characteristics
31 227 100       435 if ($shape_type == SHAPE_CIRCLE) {
32 17         42 my $contained_radius = $coords[2] / sqrt(2);
33              
34             # inner box for this circle - fully contained within the circle
35 17         52 unshift @coords, (
36             $coords[0] - $contained_radius,
37             $coords[1] - $contained_radius,
38             $coords[0] + $contained_radius,
39             $coords[1] + $contained_radius,
40             );
41              
42             # avoid squaring the radius on each iteration
43 17         72 $coords[7] = $coords[6] ** 2;
44             }
45              
46             # -1 is always shape type. We use array for speed.
47 227         365 push @coords, $shape_type;
48 227         545 return \@coords;
49             }
50              
51             sub _shapesOverlap
52             {
53 1114     1114   1613 my ($s1, $s2) = @_;
54 1114         1338 my $type = $s1->[-1];
55              
56             # same element
57 1114 100       1868 if ($type == $s2->[-1]) {
58 1040 100       1855 if ($type == SHAPE_CIRCLE) {
    50          
59 3         5 my $dist_x = $s1->[4] - $s2->[4];
60 3         10 my $dist_y = $s1->[5] - $s2->[5];
61 3         6 my $diagonal = $s1->[6] + $s2->[6];
62              
63 3         19 return $dist_x ** 2 + $dist_y ** 2
64             <= $diagonal ** 2;
65             }
66             elsif ($type == SHAPE_RECTANGLE) {
67 1037   100     5725 return $s1->[0] <= $s2->[2] &&
68             $s1->[2] >= $s2->[0] &&
69             $s1->[1] <= $s2->[3] &&
70             $s1->[3] >= $s2->[1];
71             }
72             }
73              
74             # different elements - circle first
75 74 50       98 ($s1, $s2) = ($s2, $s1)
76             unless $type == SHAPE_CIRCLE;
77              
78 74 100       155 my $cx = $s1->[4] < $s2->[0]
    100          
79             ? $s2->[0] - $s1->[4]
80             : $s1->[4] > $s2->[2]
81             ? $s2->[2] - $s1->[4]
82             : 0
83             ;
84              
85 74 100       159 my $cy = $s1->[5] < $s2->[1]
    100          
86             ? $s2->[1] - $s1->[5]
87             : $s1->[5] > $s2->[3]
88             ? $s2->[3] - $s1->[5]
89             : 0
90             ;
91              
92 74         218 return $cx ** 2 + $cy ** 2
93             <= $s1->[7];
94             }
95              
96             sub _shapeContained
97             {
98 1110     1110   1682 my ($inner_s, $s) = @_;
99              
100 1110   100     2933 return $s->[0] <= $inner_s->[0] &&
101             $s->[2] >= $inner_s->[2] &&
102             $s->[1] <= $inner_s->[1] &&
103             $s->[3] >= $inner_s->[3];
104             }
105              
106             # recursive method which adds levels to the quadtree
107             sub _addLevel
108             {
109 94     94   159 my ($self, $depth, $parent, @coords) = @_;
110 94         183 my $node = {
111             PARENT => $parent,
112             OBJECTS => [],
113             HAS_OBJECTS => 0,
114             AREA => _buildShape(@coords),
115             DEPTH => $depth,
116             };
117              
118 94 100       190 weaken $node->{PARENT} if $parent;
119              
120 94 100       149 if ($depth < $self->{DEPTH}) {
121 22         38 my ($xmin, $ymin, $xmax, $ymax) = @coords;
122 22         47 my $xmid = $xmin + ($xmax - $xmin) / 2;
123 22         38 my $ymid = $ymin + ($ymax - $ymin) / 2;
124 22         25 $depth += 1;
125              
126             # segment in the following order:
127             # top left, top right, bottom left, bottom right
128             $node->{CHILDREN} = [
129 22         68 _addLevel($self, $depth, $node, $xmin, $ymid, $xmid, $ymax),
130             _addLevel($self, $depth, $node, $xmid, $ymid, $xmax, $ymax),
131             _addLevel($self, $depth, $node, $xmin, $ymin, $xmid, $ymid),
132             _addLevel($self, $depth, $node, $xmid, $ymin, $xmax, $ymid),
133             ];
134             }
135              
136 94         247 return $node;
137             }
138              
139             # this private method executes $code on every leaf node of the tree
140             # which is within the circular shape
141             sub _loopOnNodes
142             {
143 134     134   228 my ($self, $finding, $shape) = @_;
144              
145 134         171 my @nodes;
146 134         270 my @loopargs = $self->{ROOT};
147 134         309 my @loopargs_contained;
148             my $fully_contained;
149 134         0 my $current;
150              
151 134         289 while ($current = shift @loopargs) {
152 1166 100 100     2935 next if $finding && !$current->{HAS_OBJECTS};
153              
154 1110         1800 $fully_contained = _shapeContained($current->{AREA}, $shape);
155 1110 100 100     2402 next if !$fully_contained && !_shapesOverlap($shape, $current->{AREA});
156              
157 456 100       816 if ($finding) {
158 285         468 push @nodes, $current;
159 285 100       683 next unless $current->{CHILDREN};
160              
161 151 100       272 if ($fully_contained) {
162 2         4 push @loopargs_contained, @{$current->{CHILDREN}};
  2         6  
163             }
164             else {
165 149         219 push @loopargs, @{$current->{CHILDREN}};
  149         425  
166             }
167             }
168             else {
169 171         231 $current->{HAS_OBJECTS} = 1;
170 171 100 66     456 if ($fully_contained || !$current->{CHILDREN}) {
171 62         176 push @nodes, $current;
172             }
173             else {
174 109         134 push @loopargs, @{$current->{CHILDREN}};
  109         257  
175             }
176             }
177             }
178              
179 134 100       251 if ($finding) {
180 77         196 while (my $current = shift @loopargs_contained) {
181 28 100       48 next if !$current->{HAS_OBJECTS};
182              
183 22         28 push @nodes, $current;
184 5         12 push @loopargs_contained, @{$current->{CHILDREN}}
185 22 100       71 if $current->{CHILDREN};
186             }
187             }
188              
189 134         387 return \@nodes;
190             }
191              
192             sub _clearHasObjects
193             {
194 2     2   4 my $node = shift;
195              
196 2 50       6 if ($node->{CHILDREN}) {
197 2         3 for my $child (@{$node->{CHILDREN}}) {
  2         6  
198 2 50       8 return if $child->{HAS_OBJECTS};
199             }
200             }
201              
202 0         0 $node->{HAS_OBJECTS} = 0;
203 0 0       0 if ($node->{PARENT}) {
204 0         0 _clearHasObjects($node->{PARENT});
205             }
206             }
207              
208             sub _AQT_init
209             {
210 6     6   12 my $obj = shift;
211              
212 6         17 $obj->{BACKREF} = {};
213             $obj->{ROOT} = _addLevel(
214             $obj,
215             1, #current depth
216             undef, # parent - none
217             $obj->{XMIN},
218             $obj->{YMIN},
219             $obj->{XMAX},
220             $obj->{YMAX},
221 6         30 );
222             }
223              
224             sub _AQT_deinit
225       6     {
226             # do nothing in PP implementation
227             }
228              
229             sub _AQT_addObject
230             {
231 57     57   114 my ($self, $object, @coords) = @_;
232 57         103 my $shape = _buildShape(@coords);
233              
234 57         117 my $nodes = _loopOnNodes($self, 0, $shape);
235 57         102 for my $node (@$nodes) {
236 62         77 push @{$node->{OBJECTS}}, $object;
  62         132  
237             }
238              
239 57 50       266 $self->{BACKREF}{$object} = $shape
240             unless @$nodes == 0;
241             }
242              
243             sub _AQT_findObjects
244             {
245 76     76   181 my ($self, @coords) = @_;
246 76         230 my $shape = _buildShape(@coords);
247              
248             # map returned nodes to an array containing all of
249             # their objects
250 76         140 my %hash;
251 76         132 foreach my $node (@{_loopOnNodes($self, 1, $shape)}) {
  76         194  
252 304         393 foreach my $object (@{$node->{OBJECTS}}) {
  304         634  
253 166         438 $hash{$object} = $object;
254             }
255             }
256              
257 76 100       304 if ($self->{CHECK}) {
258 6         11 my $backref = $self->{BACKREF};
259 6         15 foreach my $key (keys %hash) {
260             delete $hash{$key}
261 7 100       44 unless _shapesOverlap($shape, $backref->{$key});
262             }
263             }
264              
265 76         550 return [values %hash];
266             }
267              
268             sub _AQT_delete
269             {
270 1     1   3 my ($self, $object) = @_;
271              
272 1 50       5 return unless exists $self->{BACKREF}{$object};
273              
274 1         2 for my $node (@{_loopOnNodes($self, 1, $self->{BACKREF}{$object})}) {
  1         5  
275 3         32 @{$node->{OBJECTS}} = grep {$_ ne $object} @{$node->{OBJECTS}};
  3         9  
  2         6  
  3         16  
276 3 100       5 _clearHasObjects($node) if !@{$node->{OBJECTS}};
  3         12  
277             }
278              
279 1         6 delete $self->{BACKREF}{$object};
280             }
281              
282             sub _AQT_clear
283             {
284 3     3   7 my ($self) = @_;
285              
286 3         7 my @loopargs = $self->{ROOT};
287 3         9 while (my $current = shift @loopargs) {
288 31 100       45 next unless $current->{HAS_OBJECTS};
289              
290 30         29 @{$current->{OBJECTS}} = ();
  30         37  
291 30         32 $current->{HAS_OBJECTS} = 0;
292              
293 30 100       75 if ($current->{CHILDREN}) {
294 7         10 push @loopargs, @{$current->{CHILDREN}};
  7         17  
295             }
296             }
297              
298 3         5 %{$self->{BACKREF}} = ();
  3         23  
299             }
300              
301             1;
302