File Coverage

blib/lib/Math/Polygon/Tree.pm
Criterion Covered Total %
statement 31 33 93.9
branch 2 4 50.0
condition n/a
subroutine 10 10 100.0
pod n/a
total 43 47 91.4


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