File Coverage

blib/lib/Tree/STR.pm
Criterion Covered Total %
statement 106 111 95.5
branch 27 32 84.3
condition 43 52 82.6
subroutine 14 15 93.3
pod 4 5 80.0
total 194 215 90.2


line stmt bran cond sub pod time code
1             package Tree::STR;
2            
3 1     1   174575 use 5.010;
  1         4  
4 1     1   7 use strict;
  1         2  
  1         68  
5 1     1   6 use warnings;
  1         2  
  1         54  
6            
7 1     1   584 use Tree::STR::Node;
  1         8  
  1         81  
8            
9 1     1   663 use POSIX qw /ceil/;
  1         8822  
  1         6  
10 1     1   2058 use List::Util qw /min max/;
  1         2  
  1         113  
11 1     1   714 use Ref::Util qw/is_blessed_ref is_arrayref/;
  1         3413  
  1         1859  
12            
13            
14             =head1 NAME
15            
16             Tree::STR - A Sort-Tile-Recursive tree index
17            
18             =head1 VERSION
19            
20             Version 0.02
21            
22             =cut
23            
24             our $VERSION = '0.06';
25            
26            
27             =head1 SYNOPSIS
28            
29             use Tree::STR;
30            
31             my $data = [[0,1,2,2,'item 1'], [10,20,100,200,'item 2']];
32             my $tree = Tree::STR->new($data);
33             my $intersects_point = $tree->query_point(50,50); # ['item 2']
34             my $intersects_poly = $tree->query_partly_within_rect(20,20,200,200); # ['item 2']
35            
36             # Or if you need the Tree:R interface you can pass an array ref after
37             # the bounding coords. It will be populated with the results.
38             my $intersects_poly = [];
39             $tree->query_partly_within_rect(20,20,200,200, $intersects_poly); # ['item 2']
40             ...
41            
42             =head1 DESCRIPTION
43            
44             Create a Sort-Tile-Recursive tree. This is a read-only R-Tree that
45             is more efficient to create than a standard R-Tree.
46            
47             The input data need to be an array of arrays, where each internal
48             array contains the bounding box coordinates as (xmin, ymin, xmax, ymax)
49             followed by the item to be stored.
50            
51             Alternately one can pass an array of objects that provide a bbox method
52             that returns an array of coordinates in the order (xmin, ymin, xmax, ymax).
53            
54             =head1 EXPORT
55            
56             None.
57            
58             =head1 METHODS
59            
60             =head2 new
61            
62             =cut
63            
64             sub new {
65 3     3 1 275218 my ($class, $data, $n) = @_;
66 3   50     25 my $self = bless { nrects => $n // 3}, $class;
67 3         11 $self->{root} = $self->_load_data ($data);
68 3         11 return $self;
69             }
70            
71             sub bbox {
72 0     0 0 0 my ($self) = @_;
73 0 0       0 return if !$self->{root};
74 0         0 $self->{root}->bbox;
75             }
76            
77             sub _load_data {
78 3     3   9 my ($self, $data) = @_;
79             # we need to work on the bbox centres
80 3 50 33     9 my @bboxed = map {is_blessed_ref $_ && $_->can('bbox') ? [$_->bbox, $_] : $_} @$data;
  84         238  
81 3         9 my @centred = map {[($_->[0] + $_->[2] / 2), ($_->[1] + $_->[3] / 2), $_]} @bboxed;
  84         244  
82 3         26 my @bbox = $self->_get_bbox_from_centred_recs(\@centred);
83 3         12 my $children = $self->_load_data_inner(\@centred);
84 3         15 return Tree::STR::Node->new (
85             bbox => \@bbox,
86             children => $children,
87             );
88             }
89            
90             sub _load_data_inner {
91 42     42   103 my ($self, $data, $sort_axis) = @_;
92            
93 42   100     104 $sort_axis //= 0;
94 42         142 my @sorted = sort {$a->[$sort_axis] <=> $b->[$sort_axis]} @$data;
  599         1131  
95            
96 42         84 my $nrects = $self->{nrects};
97 42         67 my $nitems = @$data;
98 42         136 my $n_per_box = ceil ($nitems / $nrects);
99 42         70 my @ranges;
100 42         67 my $i = 0;
101 42         92 while ($i < $nitems) {
102 123         354 push @ranges, [$i,$i+$n_per_box-1];
103 123         257 $i += $n_per_box;
104             }
105            
106             # switch axis for inner calls
107 42 100       89 $sort_axis = $sort_axis ? 0 : 1;
108            
109 42         61 my @nodes;
110 42         74 for my $range (@ranges) {
111 123         460 my @recs = @sorted[$range->[0]..min($#sorted, $range->[1])];
112            
113 123 50       275 next if !@recs;
114            
115 123         258 my @bbox = $self->_get_bbox_from_centred_recs(\@recs);
116 123 100       260 if (@recs > 1) {
117 39         108 push @nodes, Tree::STR::Node->new (
118             bbox => \@bbox,
119             children => $self->_load_data_inner(\@recs, $sort_axis),
120             );
121             }
122             else {
123 84         247 push @nodes, Tree::STR::Node->new (
124             bbox => \@bbox,
125             tip => $recs[0][2][-1], # nasty...
126             );
127             }
128             }
129 42         184 return \@nodes;
130             }
131            
132             sub _get_bbox_from_centred_recs {
133 126     126   225 my ($self, $recs) = @_;
134 126         203 state $bbox_idx = 2;
135 126         194 my ($x1, $y1, $x2, $y2) = @{$recs->[0][$bbox_idx]}[0 .. 3];
  126         365  
136 126 100       527 return ($x1, $y1, $x2, $y2)
137             if @$recs == 1;
138 42         94 foreach my $rec (@$recs) {
139 324         532 my $bbox = $rec->[$bbox_idx];
140 324         606 $x1 = min($x1, $bbox->[0]);
141 324         575 $y1 = min($y1, $bbox->[1]);
142 324         624 $x2 = max($x2, $bbox->[2]);
143 324         678 $y2 = max($y2, $bbox->[3]);
144             }
145 42         144 return $x1, $y1, $x2, $y2;
146             }
147            
148             =head2 query_point
149            
150             =cut
151            
152             sub query_point {
153 5     5 1 16777 my ($self, $x, $y, $aref) = @_;
154            
155 5         8 my @tips;
156 5         13 my @children = ($self->{root});
157             CHILD:
158 5         21 while (my $child = shift @children ) {
159 42         92 my $bbox = $child->bbox;
160             next CHILD
161 42 100 100     275 if $x < $bbox->[0] || $x > $bbox->[2] || $y < $bbox->[1] || $y > $bbox->[3];
      100        
      100        
162            
163 17 100       38 if ($child->is_tip_node) {
164             push @tips, $child->{tip}
165 4         16 }
166             else {
167             # add to search stack
168 13         39 push @children, @{$child->children};
  13         29  
169             }
170             }
171            
172 5 100       25 return \@tips if !$aref;
173            
174             # Tree::R compat
175 1         4 push @$aref, @tips;
176 1         4 return;
177             }
178            
179             =head2 query_partly_within_rect
180            
181             =cut
182            
183             # non-recursive algorithm
184             sub query_partly_within_rect {
185 4     4 1 2306 my ($self, $x1, $y1, $x2, $y2, $aref) = @_;
186            
187 4         8 my @tips;
188 4         11 my @children = ($self->{root});
189             CHILD:
190 4         17 while (my $child = shift @children ) {
191 53         107 my $bbox = $child->bbox;
192             # no overlap
193             next CHILD
194 53 100 100     321 if $x2 < $bbox->[0] || $x1 > $bbox->[2]
      100        
      100        
195             || $y2 < $bbox->[1] || $y1 > $bbox->[3];
196            
197 29 100 100     65 if ($child->is_tip_node) {
    50 66        
      33        
198             push @tips, $child->{tip}
199 12         50 }
200             elsif ( $x1 < $bbox->[0] && $x2 > $bbox->[2]
201             && $y1 < $bbox->[1] && $y2 > $bbox->[3]) {
202             # bbox completely within search box, grab all tips
203 0         0 push @tips, @{$child->tips};
  0         0  
204             }
205             else {
206             # add to search stack
207 17         27 push @children, @{$child->children};
  17         37  
208             }
209             }
210            
211 4 100       19 return \@tips if !$aref;
212            
213             # Tree::R compat
214 1         4 push @$aref, @tips;
215 1         6 return;
216             }
217            
218             =head2 query_completely_within_rect
219            
220             =cut
221            
222             sub query_completely_within_rect {
223 4     4 1 5602 my ($self, $x1, $y1, $x2, $y2, $aref) = @_;
224            
225 4         38 my @tips;
226 4         13 my @children = ($self->{root});
227            
228             CHILD:
229 4         17 while (my $child = shift @children ) {
230 110         299 my $bbox = $child->bbox;
231            
232             # next if no overlap
233             next CHILD
234 110 100 66     673 if $x2 < $bbox->[0] || $x1 > $bbox->[2]
      100        
      66        
235             || $y2 < $bbox->[1] || $y1 > $bbox->[3];
236            
237             # user box contains bbox, get all the subtending tips
238 74 100 66     344 if ( $x1 < $bbox->[0] && $x2 > $bbox->[2]
      100        
      100        
239             && $y1 < $bbox->[1] && $y2 > $bbox->[3]) {
240 16         53 push @tips, @{$child->tips};
  16         38  
241             }
242             else {
243             # add to search stack
244 58         81 push @children, @{$child->children};
  58         118  
245             }
246            
247             }
248            
249 4 100       20 return \@tips if !$aref;
250            
251             # Tree::R compat
252 1         3 push @$aref, @tips;
253 1         5 return;
254             }
255            
256             =head1 AUTHOR
257            
258             Shawn Laffan
259            
260             =head1 BUGS
261            
262             L
263            
264            
265             =head1 SEE ALSO
266            
267             L
268            
269             L
270            
271             Leutenegger, Scott T.; Edgington, Jeffrey M.; Lopez, Mario A. (1997).
272             "STR: A Simple and Efficient Algorithm for R-Tree Packing".
273             L
274            
275            
276             =head1 LICENSE AND COPYRIGHT
277            
278             This software is Copyright (c) 2026 by Shawn Laffan .
279            
280             This is free software, licensed under:
281            
282             The Artistic License 2.0 (GPL Compatible)
283            
284            
285             =cut
286            
287             1; # End of Tree::STR