File Coverage

blib/lib/Math/Polygon/Tree.pm
Criterion Covered Total %
statement 246 252 97.6
branch 100 124 80.6
condition 129 158 81.6
subroutine 24 25 96.0
pod 11 11 100.0
total 510 570 89.4


line stmt bran cond sub pod time code
1             package Math::Polygon::Tree;
2             $Math::Polygon::Tree::VERSION = '0.08';
3             # ABSTRACT: fast check if point is inside polygon
4              
5             # $Id$
6              
7              
8 6     6   86853 use 5.010;
  6         15  
  6         182  
9 6     6   24 use strict;
  6         7  
  6         183  
10 6     6   22 use warnings;
  6         9  
  6         140  
11 6     6   23 use utf8;
  6         5  
  6         28  
12 6     6   118 use Carp;
  6         7  
  6         463  
13              
14 6     6   24 use base qw{ Exporter };
  6         7  
  6         503  
15              
16 6     6   26 use List::Util qw{ reduce first sum min max };
  6         7  
  6         600  
17 6     6   2829 use List::MoreUtils qw{ all any };
  6         49146  
  6         53  
18 6     6   5805 use POSIX qw/ floor ceil /;
  6         27450  
  6         40  
19              
20             # todo: remove gpc and use simple bbox clip
21             our $CLIPPER_CLASS;
22             BEGIN {
23 6     6   5212 my @clippers = qw/
24             Math::Geometry::Planar::GPC::PolygonXS
25             Math::Geometry::Planar::GPC::Polygon
26             /;
27              
28 6         1385 for my $class ( @clippers ) {
29 6 50       2076 eval "require $class" or next;
30 6         4078 $CLIPPER_CLASS = $class;
31 6         12 last;
32             }
33              
34 6 50       15280 croak "No clipper class available" if !$CLIPPER_CLASS;
35             }
36            
37              
38              
39             our @EXPORT_OK = qw{
40             polygon_bbox
41             polygon_centroid
42             polygon_contains_point
43             };
44              
45             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
46              
47              
48             # tree options
49             our $MAX_LEAF_POINTS = 16;
50             our $SLICE_FIELD = 0.0001;
51             our $SLICE_NUM_COEF = 2;
52             our $SLICE_NUM_SPEED_COEF = 1;
53              
54             # floating-point comparsion accuracy
55             our $POLYGON_BORDER_WIDTH = 1e-9;
56              
57              
58              
59             sub new {
60 8417     8417 1 11297 my ($class, @in_contours) = @_;
61 8417 100       15034 my %opt = ref $in_contours[-1] eq 'HASH' ? %{pop @in_contours} : ();
  153         318  
62 8417         12308 my $self = bless {}, $class;
63              
64             ## load and close polys, calc bbox
65 8417         6553 my @contours;
66 8417         11109 while ( @in_contours ) {
67 8975         7272 my $contour = shift @in_contours;
68              
69 8975 100       12999 if ( ref $contour ne 'ARRAY' ) {
70 2         7 unshift @in_contours, read_poly_file($contour);
71 2         7 next;
72             }
73              
74 8973         16137 my @points = @$contour;
75 8973 100 100     31037 push @points, $points[0] if !( $points[0]->[0] == $points[-1]->[0] && $points[0]->[1] == $points[-1]->[1] );
76              
77 8973         9061 push @contours, \@points;
78              
79 8973         10962 my $bbox = polygon_bbox(\@points);
80 8973         26463 $self->{bbox} = bbox_union($bbox, $self->{bbox});
81             }
82              
83 8417 50       11424 croak "No contours" if !@contours;
84              
85 8417         8022 my $nrpoints = sum map { scalar @$_ } @contours;
  8973         15951  
86              
87             # small polygon - no need to slice
88 8417 100       12257 if ( $nrpoints <= $MAX_LEAF_POINTS ) {
89 7185         7217 $self->{poly} = \@contours;
90              
91             # find some filled bboxes for rough checks
92 7185 100       10196 if ( $opt{prepare_rough} ) {
93 131         93 my ($x0, $y0, $x1, $y1) = @{ $self->{bbox} };
  131         160  
94 131         150 for my $c ( @contours ) {
95 137         189 for my $i ( 0 .. $#$c-1 ) {
96 894 100       1422 my ($h, $j, $k) = ( ($i>0 ? $i-1 : -2), ($i+1) % $#$c , ($i+2) % $#$c );
97              
98 894 100 100     2244 if (
      100        
      66        
      100        
      66        
99             $c->[$h]->[1] == $c->[$i]->[1]
100             && $c->[$k]->[1] == $c->[$j]->[1]
101             && ($c->[$i]->[1] == $y0 || $c->[$i]->[1] == $y1)
102             && ($c->[$j]->[1] == $y0 || $c->[$j]->[1] == $y1)
103             ) {
104             # left edge
105 79 100 66     180 if ( $c->[$i]->[0] == $x0 && $c->[$j]->[0] == $x0 ) {
106 46         44 my $x = min grep {$_ > $x0} map {$_->[0]} @$c;
  344         312  
  344         317  
107 46         37 push @{ $self->{filled_bboxes} }, [$x0, $y0, $x, $y1];
  46         84  
108             }
109             # right edge
110 79 100 66     177 if ( $c->[$i]->[0] == $x1 && $c->[$j]->[0] == $x1 ) {
111 33         31 my $x = max grep {$_ < $x1} map {$_->[0]} @$c;
  196         192  
  196         186  
112 33         28 push @{ $self->{filled_bboxes} }, [$x, $y0, $x1, $y1];
  33         52  
113             }
114             }
115              
116 894 100 100     2583 if (
      100        
      66        
      100        
      66        
117             $c->[$h]->[0] == $c->[$i]->[0]
118             && $c->[$k]->[0] == $c->[$j]->[0]
119             && ($c->[$i]->[0] == $x0 || $c->[$i]->[0] == $x1)
120             && ($c->[$j]->[0] == $x0 || $c->[$j]->[0] == $x1)
121             ) {
122             # lower edge
123 124 100 100     274 if ( $c->[$i]->[1] == $y0 && $c->[$j]->[1] == $y0 ) {
124 59         58 my $y = min grep {$_ > $y0} map {$_->[1]} @$c;
  388         399  
  388         367  
125 59         53 push @{ $self->{filled_bboxes} }, [$x0, $y0, $x1, $y];
  59         124  
126             }
127             # upper edge
128 124 100 100     316 if ( $c->[$i]->[1] == $y1 && $c->[$j]->[1] == $y1 ) {
129 46         44 my $y = max grep {$_ < $y1} map {$_->[1]} @$c;
  311         290  
  311         276  
130 46         40 push @{ $self->{filled_bboxes} }, [$x0, $y, $x1, $y1];
  46         96  
131             }
132             }
133             }
134             }
135             }
136              
137 7185         29343 return $self;
138             }
139              
140              
141             # calc number of pieces (need to tune!)
142 1232         1025 my ($xmin, $ymin, $xmax, $ymax) = @{$self->{bbox}};
  1232         2238  
143 1232         1909 my $xy_ratio = ($xmax-$xmin) / ($ymax-$ymin);
144 1232         3343 my $nparts = $SLICE_NUM_COEF * log( exp(1) * ($nrpoints/$MAX_LEAF_POINTS)**$SLICE_NUM_SPEED_COEF );
145              
146 1232         3334 my $x_parts = $self->{x_parts} = ceil( sqrt($nparts * $xy_ratio) );
147 1232         1994 my $y_parts = $self->{y_parts} = ceil( sqrt($nparts / $xy_ratio) );
148 1232         1503 my $x_size = $self->{x_size} = ($xmax-$xmin) / $x_parts;
149 1232         1575 my $y_size = $self->{y_size} = ($ymax-$ymin) / $y_parts;
150              
151              
152             # slice
153 1232         1452 my $subparts = $self->{subparts} = [];
154            
155 1232         3116 my $gpc_poly = $CLIPPER_CLASS->new_gpc();
156 1232         11742 $gpc_poly->add_polygon( $_, 0 ) for @contours;
157            
158 1232         2180 for my $j ( 0 .. $y_parts-1 ) {
159 2750         2906 for my $i ( 0 .. $x_parts ) {
160              
161 9416         11806 my $x0 = $xmin + ($i -$SLICE_FIELD)*$x_size;
162 9416         8199 my $y0 = $ymin + ($j -$SLICE_FIELD)*$y_size;
163 9416         9274 my $x1 = $xmin + ($i+1+$SLICE_FIELD)*$x_size;
164 9416         7814 my $y1 = $ymin + ($j+1+$SLICE_FIELD)*$y_size;
165              
166 9416         18916 my $gpc_slice = $CLIPPER_CLASS->new_gpc();
167 9416         62940 $gpc_slice->add_polygon([ [$x0,$y0], [$x0,$y1], [$x1,$y1], [$x1,$y0], [$x0,$y0] ], 0);
168              
169 9416         6214765 my @slice_parts = $gpc_poly->clip_to($gpc_slice, 'INTERSECT')->get_polygons();
170              
171             # empty part
172 9416 100       24632 if ( !@slice_parts ) {
173 906         1932 $subparts->[$i]->[$j] = 0;
174 906         2376 next;
175             }
176              
177             # filled part
178 8510 100 100     12947 if (
      100        
179 8019         29272 @slice_parts == 1 && @{$slice_parts[0]} == 4
180 5179 100 100 5179   25384 && all { ($_->[0]==$x0 || $_->[0]==$x1) && ($_->[1]==$y0 || $_->[1]==$y1) } @{$slice_parts[0]}
  4073   100     7975  
181             ) {
182 101         211 $subparts->[$i]->[$j] = 1;
183 101         405 next;
184             }
185              
186             # complex subpart
187 8409 100       24503 $subparts->[$i]->[$j] = Math::Polygon::Tree->new( @slice_parts, (%opt ? \%opt : ()) );
188             }
189             }
190              
191 1232         9646 return $self;
192             }
193              
194              
195              
196              
197             sub read_poly_file {
198 2     2 1 3 my ($file) = @_;
199              
200 2   33     11 my $need_to_open = !ref $file || ref $file eq 'SCALAR';
201             my $fh = $need_to_open
202 2 0       5 ? do { open my $in, '<', $file or croak "Couldn't open $file: $@"; $in }
  0 50       0  
  0         0  
203             : $file;
204              
205 2         2 my @contours;
206             my $pid;
207 0         0 my @cur_points;
208 2         11 while ( my $line = readline $fh ) {
209             # new contour
210 15064 100       23210 if ( $line =~ /^([\-\!]?) (\d+)/x ) {
211 2 50       7 $pid = $1 ? -$2 : $2;
212 2         6 next;
213             }
214              
215             # point
216 15062 100       27554 if ( $line =~ /^\s+([0-9.Ee+-]+)\s+([0-9.Ee+-]+)/ ) {
217 15052         28067 push @cur_points, [ $1+0, $2+0 ];
218 15052         25531 next;
219             }
220              
221             # !!! inner contour - skipping
222 10 50 66     38 if ( $line =~ /^END/ && $pid < 0 ) {
223 0         0 @cur_points = ();
224 0         0 next;
225             }
226              
227             # outer contour
228 10 100 100     49 if ( $line =~ /^END/ && @cur_points ) {
229 2         996 push @contours, [ @cur_points ];
230 2         241 @cur_points = ();
231 2         10 next;
232             }
233             }
234              
235 2 50       13 close $fh if $need_to_open;
236 2         6 return @contours;
237             }
238              
239              
240              
241             sub contains {
242 52     52 1 5661 my ($self, $point) = @_;
243              
244 52 50       99 croak "Point should be a reference" if ref $point ne 'ARRAY';
245              
246             # check bbox
247 52         51 my ($px, $py) = @$point;
248 52         37 my ($xmin, $ymin, $xmax, $ymax) = @{ $self->{bbox} };
  52         82  
249 52 50 100     330 return 0
      66        
      66        
250             if $px < $xmin-$POLYGON_BORDER_WIDTH
251             || $px > $xmax+$POLYGON_BORDER_WIDTH
252             || $py < $ymin-$POLYGON_BORDER_WIDTH
253             || $py > $ymax+$POLYGON_BORDER_WIDTH;
254              
255             # leaf
256 44 100       64 if ( exists $self->{poly} ) {
257 24     26   63 my $result = first {$_} map {polygon_contains_point($point, $_)} @{$self->{poly}};
  26         38  
  26         37  
  24         37  
258 24   100     94 return $result // 0;
259             }
260              
261             # branch
262 20         58 my $i = min( floor( ($px-$xmin) / $self->{x_size} ), $self->{x_parts}-1 );
263 20         51 my $j = min( floor( ($py-$ymin) / $self->{y_size} ), $self->{y_parts}-1 );
264              
265 20         40 my $subpart = $self->{subparts}->[$i]->[$j];
266 20 100       34 return $subpart if !ref $subpart;
267 19         35 return $subpart->contains($point);
268             }
269              
270              
271              
272             sub contains_points {
273 6     6 1 1983 my ($self, @points) = @_;
274              
275 6 50 33     25 my $iter_list = @points==1 && ref $points[0]->[0] ? $points[0] : \@points;
276              
277 6         5 my $result;
278 6         8 for my $point ( @$iter_list ) {
279 11         16 my $point_result = 0 + !!$self->contains($point);
280 11 100 100     25 return undef if defined $result && $point_result != $result;
281 10         13 $result = $point_result;
282             }
283              
284 5         7 return $result;
285             }
286              
287              
288              
289             sub contains_bbox_rough {
290 10     10 1 9 my ($self, @bbox) = @_;
291 10 100       19 my %opt = ref $bbox[-1] eq 'HASH' ? %{pop @bbox} : ();
  1         3  
292 10 50       13 my $bbox = ref $bbox[0] ? $bbox[0] : \@bbox;
293              
294 10 50       15 croak "Box should be 4 values array: xmin, ymin, xmax, ymax" if @$bbox != 4;
295              
296 10         10 my ($x0, $y0, $x1, $y1) = @$bbox;
297 10         8 my ($xmin, $ymin, $xmax, $ymax) = @{$self->{bbox}};
  10         13  
298              
299             # completely outside bbox
300 10 50 66     59 return 0 if $x1 < $xmin || $x0 > $xmax || $y1 < $ymin || $y0 > $ymax;
      66        
      66        
301              
302             # partly inside
303 9 100 33     54 return undef if !( $x0 >= $xmin && $x1 <= $xmax && $y0 >= $ymin && $y1 <= $ymax );
      33        
      66        
304              
305 8 100       15 if ( !$self->{subparts} ) {
306 6 50       4 for my $fbbox ( @{ $self->{filled_bboxes} || [] } ) {
  6         13  
307 8         8 my ($fx0, $fy0, $fx1, $fy1) = @$fbbox;
308 8 100 66     54 return 1 if $x0>=$fx0 && $y0>=$fy0 && $x1<=$fx1 && $y1<=$fy1;
      100        
      100        
309             }
310              
311 3 100       10 return undef if !$opt{inaccurate};
312              
313 1         3 my @points = ( [$x0,$y0], [$x0,$y1], [$x1,$y0], [$x1,$y1] );
314             my $result =
315 1     1   2 any { my $p = $_; all { polygon_contains_point($_, $p) } @points }
  1         6  
  4         8  
316 1         4 @{ $self->{poly} };
  1         10  
317 1         6 return 0 + $result;
318             }
319              
320             # lays in defferent subparts
321 2         10 my $i0 = min( floor( ($x0-$xmin) / $self->{x_size} ), $self->{x_parts}-1 );
322 2         5 my $i1 = min( floor( ($x1-$xmin) / $self->{x_size} ), $self->{x_parts}-1 );
323 2 50       5 return undef if $i0 != $i1;
324            
325 2         5 my $j0 = min( floor( ($y0-$ymin) / $self->{y_size} ), $self->{y_parts}-1 );
326 2         6 my $j1 = min( floor( ($y1-$ymin) / $self->{y_size} ), $self->{y_parts}-1 );
327 2 50       3 return undef if $j0 != $j1;
328              
329 2         4 my $subpart = $self->{subparts}->[$i0]->[$j0];
330 2 50       3 return $subpart if !ref $subpart;
331 2         9 return $subpart->contains_bbox_rough($bbox);
332             }
333              
334              
335              
336             sub contains_polygon_rough {
337 8     8 1 2046 my ($self, $poly, %opt) = @_;
338 8 50       18 croak "Polygon should be a reference to array of points" if ref $poly ne 'ARRAY';
339              
340 8 100       11 return $self->contains_bbox_rough( polygon_bbox($poly), (%opt ? \%opt : ()) );
341             }
342              
343              
344              
345             sub bbox {
346 0     0 1 0 return shift()->{bbox};
347             }
348              
349              
350              
351              
352             sub polygon_bbox {
353 8984     8984 1 7483 my ($contour) = @_;
354              
355 8984 100       10834 return bbox_union(@$contour) if @$contour <= 2;
356 8977     129929   30911 return reduce { bbox_union($a, $b) } @$contour;
  129929         140402  
357             }
358              
359              
360              
361             sub bbox_union {
362 138914     138914 1 104509 my ($bbox1, $bbox2) = @_;
363              
364 138914   66     157401 $bbox2 //= $bbox1;
365              
366 138914   100     838658 my @bbox = (
      100        
      100        
      100        
367             min( $bbox1->[0], $bbox2->[0] ),
368             min( $bbox1->[1], $bbox2->[1] ),
369             max( $bbox1->[2] // $bbox1->[0], $bbox2->[2] // $bbox2->[0] ),
370             max( $bbox1->[3] // $bbox1->[1], $bbox2->[3] // $bbox2->[1] ),
371             );
372              
373 138914         171968 return \@bbox;
374             }
375              
376              
377              
378             sub polygon_centroid {
379 5     5 1 2081 my (@poly) = @_;
380 5 50       12 my $contour = ref $poly[0]->[0] ? $poly[0] : \@poly;
381              
382 5 100       13 return $contour->[0] if @$contour < 2;
383              
384 4         5 my $sx = 0;
385 4         2 my $sy = 0;
386 4         4 my $sq = 0;
387              
388 4         4 my $p0 = $contour->[0];
389 4         8 for my $i ( 1 .. $#$contour-1 ) {
390 5         4 my $p = $contour->[$i];
391 5         6 my $p1 = $contour->[$i+1];
392              
393 5         12 my $tsq = ( ( $p->[0] - $p0->[0] ) * ( $p1->[1] - $p0->[1] )
394             - ( $p1->[0] - $p0->[0] ) * ( $p->[1] - $p0->[1] ) );
395 5 100       9 next if $tsq == 0;
396            
397 4         6 my $tx = ( $p0->[0] + $p->[0] + $p1->[0] ) / 3;
398 4         6 my $ty = ( $p0->[1] + $p->[1] + $p1->[1] ) / 3;
399              
400 4         5 $sx += $tx * $tsq;
401 4         3 $sy += $ty * $tsq;
402 4         8 $sq += $tsq;
403             }
404              
405 4 100       9 if ( $sq == 0 ) {
406 1         3 my $bbox = polygon_bbox($contour);
407 1         5 return [ ($bbox->[0]+$bbox->[2])/2, ($bbox->[1]+$bbox->[3])/2 ];
408             }
409              
410 3         8 return [$sx/$sq, $sy/$sq];
411             }
412              
413              
414              
415             sub polygon_contains_point {
416 30     30 1 32 my ($point, @poly) = @_;
417 30 50       56 my $contour = ref $poly[0]->[0] ? $poly[0] : \@poly;
418              
419 30         25 my ($x, $y) = @$point;
420 30         25 my ($px, $py) = @{ $contour->[0] };
  30         30  
421 30         24 my ($nx, $ny);
422              
423 30         20 my $inside = 0;
424              
425 30         45 for my $i ( 1 .. scalar @$contour ) {
426 120         60 ($nx, $ny) = @{ $contour->[ $i % scalar @$contour ] };
  120         146  
427              
428 120 100 100     234 return -1 if abs($y-$py) < $POLYGON_BORDER_WIDTH && abs($x-$px) < $POLYGON_BORDER_WIDTH;
429              
430 118 50 100     211 return -1
      66        
      66        
      66        
      33        
431             if abs($y-$py) < $POLYGON_BORDER_WIDTH && abs($py-$ny) < $POLYGON_BORDER_WIDTH
432             && ( $x >= $px || $x >= $nx )
433             && ( $x <= $px || $x <= $nx );
434              
435 115 100       154 next if abs($py-$ny) < $POLYGON_BORDER_WIDTH;
436 73 100 100     155 next if $y < $py && $y < $ny;
437 52 100 100     124 next if $y > $py && $y > $ny;
438 38 100 100     82 next if $x > $px && $x > $nx;
439              
440 24         41 my $xx = ($y-$py)*($nx-$px)/($ny-$py)+$px;
441 24 100       67 return -1 if abs($x-$xx) < $POLYGON_BORDER_WIDTH;
442              
443 15 50 66     35 next if $y <= $py && $y <= $ny;
444              
445 15 100 100     69 $inside = 1 - $inside
446             if abs($px-$nx)<$POLYGON_BORDER_WIDTH || $x < $xx;
447             }
448 106         108 continue { ($px, $py) = ($nx, $ny); }
449              
450 16         46 return $inside;
451             }
452              
453              
454             1;
455              
456             __END__