File Coverage

blib/lib/Math/Polygon/Calc.pm
Criterion Covered Total %
statement 159 172 92.4
branch 62 90 68.8
condition 102 156 65.3
subroutine 18 21 85.7
pod 15 15 100.0
total 356 454 78.4


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