File Coverage

blib/lib/Math/Polygon/Calc.pm
Criterion Covered Total %
statement 163 176 92.6
branch 64 92 69.5
condition 111 156 71.1
subroutine 20 23 86.9
pod 16 16 100.0
total 374 463 80.7


line stmt bran cond sub pod time code
1             # Copyrights 2004-2018 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution Math::Polygon. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Math::Polygon::Calc;
10 19     19   324980 use vars '$VERSION';
  19         70  
  19         902  
11             $VERSION = '1.10';
12              
13 19     19   94 use base 'Exporter';
  19         28  
  19         1861  
14              
15 19     19   97 use strict;
  19         37  
  19         314  
16 19     19   93 use warnings;
  19         29  
  19         1047  
17              
18             our @EXPORT = qw/
19             polygon_area
20             polygon_bbox
21             polygon_beautify
22             polygon_centroid
23             polygon_clockwise
24             polygon_contains_point
25             polygon_counter_clockwise
26             polygon_distance
27             polygon_equal
28             polygon_is_clockwise
29             polygon_is_closed
30             polygon_perimeter
31             polygon_same
32             polygon_start_minxy
33             polygon_string
34             polygon_format
35             /;
36              
37 19     19   116 use List::Util qw/min max/;
  19         34  
  19         1798  
38 19     19   120 use Carp qw/croak/;
  19         40  
  19         35067  
39              
40              
41 66     66 1 1504 sub polygon_string(@) { join ', ', map "[$_->[0],$_->[1]]", @_ }
42              
43              
44             sub polygon_bbox(@)
45             {
46 31     31 1 690 ( min( map $_->[0], @_ )
47             , min( map $_->[1], @_ )
48             , max( map $_->[0], @_ )
49             , max( map $_->[1], @_ )
50             );
51             }
52              
53              
54             sub polygon_area(@)
55 6     6 1 73 { my $area = 0;
56 6         17 while(@_ >= 2)
57 31         41 { $area += $_[0][0]*$_[1][1] - $_[0][1]*$_[1][0];
58 31         50 shift;
59             }
60              
61 6         23 abs($area)/2;
62             }
63              
64              
65             sub polygon_is_clockwise(@)
66 7     7 1 13 { my $area = 0;
67              
68 7 50       15 polygon_is_closed(@_)
69             or croak "ERROR: polygon must be closed: begin==end";
70              
71 7         19 while(@_ >= 2)
72 31         43 { $area += $_[0][0]*$_[1][1] - $_[0][1]*$_[1][0];
73 31         49 shift;
74             }
75              
76 7         21 $area < 0;
77             }
78              
79              
80             sub polygon_clockwise(@)
81 0 0   0 1 0 { polygon_is_clockwise(@_) ? @_ : reverse @_;
82             }
83              
84              
85             sub polygon_counter_clockwise(@)
86 0 0   0 1 0 { polygon_is_clockwise(@_) ? reverse(@_) : @_;
87             }
88              
89              
90              
91             sub polygon_perimeter(@)
92 0     0 1 0 { my $l = 0;
93              
94 0         0 while(@_ >= 2)
95 0         0 { $l += sqrt(($_[0][0]-$_[1][0])**2 + ($_[0][1]-$_[1][1])**2);
96 0         0 shift;
97             }
98              
99 0         0 $l;
100             }
101              
102              
103             sub polygon_start_minxy(@)
104 29 50   29 1 4316 { return @_ if @_ <= 1;
105 29   33     83 my $ring = $_[0][0]==$_[-1][0] && $_[0][1]==$_[-1][1];
106 29 50       45 pop @_ if $ring;
107              
108 29         38 my ($xmin, $ymin) = polygon_bbox @_;
109              
110 29         34 my $rot = 0;
111 29         48 my $dmin_sq = ($_[0][0]-$xmin)**2 + ($_[0][1]-$ymin)**2;
112              
113 29         49 for(my $i=1; $i<@_; $i++)
114 81 100       130 { next if $_[$i][0] - $xmin > $dmin_sq;
115              
116 24         29 my $d_sq = ($_[$i][0]-$xmin)**2 + ($_[$i][1]-$ymin)**2;
117 24 100       42 if($d_sq < $dmin_sq)
118 8         10 { $dmin_sq = $d_sq;
119 8         12 $rot = $i;
120             }
121             }
122              
123 29 50       89 $rot==0 ? (@_, ($ring ? $_[0] : ()))
    50          
    100          
124             : (@_[$rot..$#_], @_[0..$rot-1], ($ring ? $_[$rot] : ()));
125             }
126              
127              
128             sub polygon_beautify(@)
129 15 100   15 1 5166 { my %opts = ref $_[0] eq 'HASH' ? %{ (shift) } : ();
  5         16  
130 15 50       32 return () unless @_;
131              
132 15 100       24 my $despike = exists $opts{remove_spikes} ? $opts{remove_spikes} : 0;
133              
134 15         22 my @res = @_;
135 15 100       30 return () if @res < 4; # closed triangle = 4 points
136 13         15 pop @res; # cyclic: last is first
137 13         14 my $unchanged= 0;
138              
139 13         26 while($unchanged < 2*@res)
140 152 50       180 { return () if @res < 3; # closed triangle = 4 points
141              
142 152         135 my $this = shift @res;
143 152         129 push @res, $this; # recycle
144 152         112 $unchanged++;
145              
146             # remove doubles
147 152         156 my ($x, $y) = @$this;
148 152   66     397 while(@res && $res[0][0]==$x && $res[0][1]==$y)
      100        
149 12         11 { $unchanged = 0;
150 12         31 shift @res;
151             }
152              
153             # remove spike
154 152 100 66     242 if($despike && @res >= 2)
155             { # any spike
156 47 100 100     71 if($res[1][0]==$x && $res[1][1]==$y)
157 4         5 { $unchanged = 0;
158 4         4 shift @res;
159             }
160              
161             # x-spike
162 47 0 66     69 if($y==$res[0][1] && $y==$res[1][1]
      0        
      33        
163             && ( ($res[0][0] < $x && $x < $res[1][0])
164             || ($res[0][0] > $x && $x > $res[1][0])))
165 0         0 { $unchanged = 0;
166 0         0 shift @res;
167             }
168              
169             # y-spike
170 47 50 100     73 if( $x==$res[0][0] && $x==$res[1][0]
      33        
      66        
171             && ( ($res[0][1] < $y && $y < $res[1][1])
172             || ($res[0][1] > $y && $y > $res[1][1])))
173 0         0 { $unchanged = 0;
174 0         0 shift @res;
175             }
176             }
177              
178             # remove intermediate
179 152 100 66     450 if( @res >= 2
      100        
      100        
      100        
180             && $res[0][0]==$x && $res[1][0]==$x
181             && ( ($y < $res[0][1] && $res[0][1] < $res[1][1])
182             || ($y > $res[0][1] && $res[0][1] > $res[1][1])))
183 4         5 { $unchanged = 0;
184 4         4 shift @res;
185             }
186              
187 152 100 66     402 if( @res >= 2
      100        
      100        
      100        
188             && $res[0][1]==$y && $res[1][1]==$y
189             && ( ($x < $res[0][0] && $res[0][0] < $res[1][0])
190             || ($x > $res[0][0] && $res[0][0] > $res[1][0])))
191 2         3 { $unchanged = 0;
192 2         2 shift @res;
193             }
194              
195             # remove 2 out-of order between two which stay
196 152 100 66     376 if(@res >= 3
      100        
      100        
      33        
      66        
      66        
      100        
197             && $x==$res[0][0] && $x==$res[1][0] && $x==$res[2][0]
198             && ($y < $res[0][1] && $y < $res[1][1]
199             && $res[0][1] < $res[2][1] && $res[1][1] < $res[2][1]))
200 2         2 { $unchanged = 0;
201 2         3 splice @res, 0, 2;
202             }
203              
204 152 0 66     473 if(@res >= 3
      100        
      66        
      0        
      0        
      0        
      33        
205             && $y==$res[0][1] && $y==$res[1][1] && $y==$res[2][1]
206             && ($x < $res[0][0] && $x < $res[1][0]
207             && $res[0][0] < $res[2][0] && $res[1][0] < $res[2][0]))
208 0         0 { $unchanged = 0;
209 0         0 splice @res, 0, 2;
210             }
211             }
212              
213 13 50       58 @res ? (@res, $res[0]) : ();
214             }
215              
216              
217             sub polygon_equal($$;$)
218 5     5 1 7 { my ($f,$s, $tolerance) = @_;
219 5 50       10 return 0 if @$f != @$s;
220 5         7 my @f = @$f;
221 5         7 my @s = @$s;
222              
223 5 100       8 if(defined $tolerance)
224 2         11 { while(@f)
225 8 50 33     27 { return 0 if abs($f[0][0]-$s[0][0]) > $tolerance
226             || abs($f[0][1]-$s[0][1]) > $tolerance;
227 8         9 shift @f; shift @s;
  8         10  
228             }
229 2         9 return 1;
230             }
231              
232 3         5 while(@f)
233 9 100 66     34 { return 0 if $f[0][0] != $s[0][0] || $f[0][1] != $s[0][1];
234 8         10 shift @f; shift @s;
  8         11  
235             }
236              
237 2         7 1;
238             }
239              
240              
241             sub polygon_same($$;$)
242 2 50   2 1 2 { return 0 if @{$_[0]} != @{$_[1]};
  2         19  
  2         14  
243 2         4 my @f = polygon_start_minxy @{ (shift) };
  2         5  
244 2         3 my @s = polygon_start_minxy @{ (shift) };
  2         4  
245 2         5 polygon_equal \@f, \@s, @_;
246             }
247              
248              
249             # Algorithms can be found at
250             # http://www.eecs.umich.edu/courses/eecs380/HANDOUTS/PROJ2/InsidePoly.html
251             # p1 = polygon[0];
252             # for (i=1;i<=N;i++) {
253             # p2 = polygon[i % N];
254             # if (p.y > MIN(p1.y,p2.y)) {
255             # if (p.y <= MAX(p1.y,p2.y)) {
256             # if (p.x <= MAX(p1.x,p2.x)) {
257             # if (p1.y != p2.y) {
258             # xinters = (p.y-p1.y)*(p2.x-p1.x)/(p2.y-p1.y)+p1.x;
259             # if (p1.x == p2.x || p.x <= xinters)
260             # counter++;
261             # }
262             # }
263             # }
264             # }
265             # p1 = p2;
266             # }
267             # inside = counter % 2;
268              
269             sub polygon_contains_point($@)
270 19     19 1 97 { my $point = shift;
271 19 50       40 return 0 if @_ < 3;
272              
273 19         34 my ($x, $y) = @$point;
274 19         24 my $inside = 0;
275              
276 19 50       30 polygon_is_closed(@_)
277             or croak "ERROR: polygon must be closed: begin==end";
278              
279 19         27 my ($px, $py) = @{ (shift) };
  19         28  
280              
281 19         35 while(@_)
282 98         98 { my ($nx, $ny) = @{ (shift) };
  98         116  
283              
284             # Extra check for exactly on the edge when the axes are
285             # horizontal or vertical.
286 98 50 100     186 return 1 if $y==$py && $py==$ny
      66        
      66        
      66        
      66        
287             && ($x >= $px || $x >= $nx)
288             && ($x <= $px || $x <= $nx);
289              
290 95 50 100     178 return 1 if $x==$px && $px==$nx
      66        
      66        
      66        
      33        
291             && ($y >= $py || $y >= $ny)
292             && ($y <= $py || $y <= $ny);
293              
294 90 100 100     321 if( $py == $ny
      100        
      100        
      100        
      100        
      100        
295             || ($y <= $py && $y <= $ny)
296             || ($y > $py && $y > $ny)
297             || ($x > $px && $x > $nx)
298             )
299             {
300 73         97 ($px, $py) = ($nx, $ny);
301 73         114 next;
302             }
303              
304             # side wrt diagonal
305 17         37 my $xinters = ($y-$py)*($nx-$px)/($ny-$py)+$px;
306 17 100 100     43 $inside = !$inside
307             if $px==$nx || $x <= $xinters;
308              
309 17         32 ($px, $py) = ($nx, $ny);
310             }
311              
312 11         57 $inside;
313             }
314              
315              
316             sub polygon_centroid(@)
317             {
318 4 50   4 1 956 polygon_is_closed(@_)
319             or croak "ERROR: polygon must be closed: begin==end";
320              
321 4         8 my ($cx, $cy, $a) = (0, 0, 0);
322 4         11 foreach my $i (0..@_-2)
323 14         23 { my $ap = $_[$i][0]*$_[$i+1][1] - $_[$i+1][0]*$_[$i][1];
324 14         18 $cx += ($_[$i][0]+$_[$i+1][0]) * $ap;
325 14         17 $cy += ($_[$i][1]+$_[$i+1][1]) * $ap;
326 14         17 $a += $ap;
327             }
328 4         5 my $c = 3*$a; # 6*$a/2;
329 4         9 [ $cx/$c, $cy/$c ];
330             }
331              
332              
333             sub polygon_is_closed(@)
334 30 50   30 1 56 { @_ or croak "ERROR: empty polygon is neither closed nor open";
335              
336 30         52 my ($first, $last) = @_[0,-1];
337 30 50       126 $first->[0]==$last->[0] && $first->[1]==$last->[1];
338             }
339              
340              
341             # Contributed by Andreas Koenig for 1.05
342             # http://stackoverflow.com/questions/10983872/distance-from-a-point-to-a-polygon#10984080
343             # with correction from
344             # http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
345             sub polygon_distance($%)
346 20     20 1 94 { my $p = shift;
347              
348 20         29 my ($x, $y) = @$p;
349 20         19 my $minDist;
350              
351 20 100       43 @_ or return undef;
352              
353 19         21 my ($x1, $y1) = @{ (shift) };
  19         25  
354 19 100       32 unless(@_)
355 1         2 { my ($dx, $dy) = ($x1 - $x, $y1 - $y);
356 1         5 return sqrt($dx * $dx + $dy * $dy);
357             }
358              
359 18         23 while(@_)
360 72         72 { my ($x2, $y2) = @{ (shift) }; # closed poly!
  72         87  
361 72         80 my $A = $x - $x1;
362 72         74 my $B = $y - $y1;
363 72         72 my $C = $x2 - $x1;
364 72         68 my $D = $y2 - $y1;
365              
366             # closest point to the line segment
367 72         82 my $dot = $A * $C + $B * $D;
368 72         78 my $len_sq = $C * $C + $D * $D;
369 72 50       105 my $angle = $len_sq==0 ? -1 : $dot / $len_sq;
370            
371 72 100       148 my ($xx, $yy)
    100          
372             = $angle < 0 ? ($x1, $y1) # perpendicular line crosses off segment
373             : $angle > 1 ? ($x2, $y2)
374             : ($x1 + $angle * $C, $y1 + $angle * $D);
375              
376 72         73 my $dx = $x - $xx;
377 72         72 my $dy = $y - $yy;
378 72         81 my $dist = sqrt($dx * $dx + $dy * $dy);
379 72 100       101 $minDist = $dist unless defined $minDist;
380 72 100       98 $minDist = $dist if $dist < $minDist;
381              
382 72         119 ($x1, $y1) = ($x2, $y2);
383             }
384              
385 18         50 $minDist;
386             }
387              
388              
389              
390             sub polygon_format($@)
391 8     8 1 11 { my $format = shift;
392             my $call = ref $format eq 'CODE' ? $format
393 8 100   8   22 : sub { sprintf $format, $_[0] };
  8         34  
394              
395 8         18 map [ $call->($_->[0]), $call->($_->[1]) ], @_;
396             }
397              
398             1;