File Coverage

blib/lib/Math/Polygon/Calc.pm
Criterion Covered Total %
statement 159 172 92.4
branch 62 90 68.8
condition 111 156 71.1
subroutine 18 21 85.7
pod 15 15 100.0
total 365 454 80.4


line stmt bran cond sub pod time code
1             # Copyrights 2004-2017 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   123426 use strict;
  19         49  
  19         547  
6 19     19   140 use warnings;
  19         44  
  19         748  
7              
8             package Math::Polygon::Calc;
9 19     19   109 use vars '$VERSION';
  19         53  
  19         1008  
10             $VERSION = '1.06';
11              
12 19     19   105 use base 'Exporter';
  19         38  
  19         2117  
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   126 use List::Util qw/min max/;
  19         44  
  19         2099  
33 19     19   120 use Carp qw/croak/;
  19         38  
  19         33931  
34              
35              
36 64     64 1 1563 sub polygon_string(@) { join ', ', map "[$_->[0],$_->[1]]", @_ }
37              
38              
39             sub polygon_bbox(@)
40             {
41 31     31 1 839 ( 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 20 { my $area = 0;
51 6         20 while(@_ >= 2)
52 31         57 { $area += $_[0][0]*$_[1][1] - $_[0][1]*$_[1][0];
53 31         56 shift;
54             }
55              
56 6         31 abs($area)/2;
57             }
58              
59              
60             sub polygon_is_clockwise(@)
61 7     7 1 14 { my $area = 0;
62              
63 7 50       20 polygon_is_closed(@_)
64             or croak "ERROR: polygon must be closed: begin==end";
65              
66 7         27 while(@_ >= 2)
67 31         54 { $area += $_[0][0]*$_[1][1] - $_[0][1]*$_[1][0];
68 31         62 shift;
69             }
70              
71 7         32 $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 6935 { return @_ if @_ <= 1;
100 29   33     158 my $ring = $_[0][0]==$_[-1][0] && $_[0][1]==$_[-1][1];
101 29 50       76 pop @_ if $ring;
102              
103 29         65 my ($xmin, $ymin) = polygon_bbox @_;
104              
105 29         57 my $rot = 0;
106 29         81 my $dmin_sq = ($_[0][0]-$xmin)**2 + ($_[0][1]-$ymin)**2;
107              
108 29         81 for(my $i=1; $i<@_; $i++)
109 81 100       228 { next if $_[$i][0] - $xmin > $dmin_sq;
110              
111 24         52 my $d_sq = ($_[$i][0]-$xmin)**2 + ($_[$i][1]-$ymin)**2;
112 24 100       75 if($d_sq < $dmin_sq)
113 8         24 { $dmin_sq = $d_sq;
114 8         26 $rot = $i;
115             }
116             }
117              
118 29 50       158 $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 7517 { my %opts = ref $_[0] eq 'HASH' ? %{ (shift) } : ();
  5         20  
125 15 50       45 return () unless @_;
126              
127 15 100       37 my $despike = exists $opts{remove_spikes} ? $opts{remove_spikes} : 0;
128             # my $interpol = exists $opts{remove_between} ? $opts{remove_between} : 0;
129              
130 15         40 my @res = @_;
131 15 100       44 return () if @res < 4; # closed triangle = 4 points
132 13         19 pop @res; # cyclic: last is first
133 13         30 my $unchanged= 0;
134              
135 13         42 while($unchanged < 2*@res)
136 152 50       307 { return () if @res < 3; # closed triangle = 4 points
137              
138 152         191 my $this = shift @res;
139 152         203 push @res, $this; # recycle
140 152         178 $unchanged++;
141              
142             # remove doubles
143 152         240 my ($x, $y) = @$this;
144 152   66     693 while(@res && $res[0][0]==$x && $res[0][1]==$y)
      100        
145 12         22 { $unchanged = 0;
146 12         50 shift @res;
147             }
148              
149             # remove spike
150 152 100 66     427 if($despike && @res >= 2)
151             { # any spike
152 47 100 100     111 if($res[1][0]==$x && $res[1][1]==$y)
153 4         7 { $unchanged = 0;
154 4         5 shift @res;
155             }
156              
157             # x-spike
158 47 0 66     123 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     130 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     735 if( @res >= 2
      100        
      100        
      100        
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         8 { $unchanged = 0;
180 4         8 shift @res;
181             }
182              
183 152 100 66     726 if( @res >= 2
      100        
      100        
      100        
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         5 { $unchanged = 0;
188 2         4 shift @res;
189             }
190              
191             # remove 2 out-of order between two which stay
192 152 100 66     696 if(@res >= 3
      100        
      100        
      33        
      66        
      66        
      100        
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         5 { $unchanged = 0;
197 2         5 splice @res, 0, 2;
198             }
199              
200 152 0 66     829 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       79 @res ? (@res, $res[0]) : ();
210             }
211              
212              
213             sub polygon_equal($$;$)
214 5     5 1 16 { my ($f,$s, $tolerance) = @_;
215 5 50       19 return 0 if @$f != @$s;
216 5         18 my @f = @$f;
217 5         14 my @s = @$s;
218              
219 5 100       15 if(defined $tolerance)
220 2         8 { while(@f)
221 8 50 33     52 { return 0 if abs($f[0][0]-$s[0][0]) > $tolerance
222             || abs($f[0][1]-$s[0][1]) > $tolerance;
223 8         19 shift @f; shift @s;
  8         24  
224             }
225 2         22 return 1;
226             }
227              
228 3         11 while(@f)
229 9 100 66     57 { return 0 if $f[0][0] != $s[0][0] || $f[0][1] != $s[0][1];
230 8         50 shift @f; shift @s;
  8         24  
231             }
232              
233 2         14 1;
234             }
235              
236              
237             sub polygon_same($$;$)
238 2 50   2 1 5 { return 0 if @{$_[0]} != @{$_[1]};
  2         7  
  2         8  
239 2         6 my @f = polygon_start_minxy @{ (shift) };
  2         15  
240 2         5 my @s = polygon_start_minxy @{ (shift) };
  2         10  
241 2         18 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 67 { my $point = shift;
267 19 50       51 return 0 if @_ < 3;
268              
269 19         45 my ($x, $y) = @$point;
270 19         26 my $inside = 0;
271              
272 19 50       43 polygon_is_closed(@_)
273             or croak "ERROR: polygon must be closed: begin==end";
274              
275 19         37 my ($px, $py) = @{ (shift) };
  19         38  
276              
277 19         48 while(@_)
278 98         131 { my ($nx, $ny) = @{ (shift) };
  98         151  
279              
280             # Extra check for exactly on the edge when the axes are
281             # horizontal or vertical.
282 98 50 100     279 return 1 if $y==$py && $py==$ny
      66        
      66        
      66        
      66        
283             && ($x >= $px || $x >= $nx)
284             && ($x <= $px || $x <= $nx);
285              
286 95 50 100     277 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     525 if( $py == $ny
      100        
      100        
      100        
      100        
      100        
291             || ($y <= $py && $y <= $ny)
292             || ($y > $py && $y > $ny)
293             || ($x > $px && $x > $nx)
294             )
295             {
296 73         119 ($px, $py) = ($nx, $ny);
297 73         155 next;
298             }
299              
300             # side wrt diagonal
301 17         45 my $xinters = ($y-$py)*($nx-$px)/($ny-$py)+$px;
302 17 100 100     67 $inside = !$inside
303             if $px==$nx || $x <= $xinters;
304              
305 17         47 ($px, $py) = ($nx, $ny);
306             }
307              
308 11         52 $inside;
309             }
310              
311              
312             sub polygon_centroid(@)
313             {
314 4 50   4 1 1455 polygon_is_closed(@_)
315             or croak "ERROR: polygon must be closed: begin==end";
316              
317 4         10 my ($cx, $cy, $a) = (0, 0, 0);
318 4         11 foreach my $i (0..@_-2)
319 14         29 { my $ap = $_[$i][0]*$_[$i+1][1] - $_[$i+1][0]*$_[$i][1];
320 14         28 $cx += ($_[$i][0]+$_[$i+1][0]) * $ap;
321 14         23 $cy += ($_[$i][1]+$_[$i+1][1]) * $ap;
322 14         27 $a += $ap;
323             }
324 4         7 my $c = 3*$a; # 6*$a/2;
325 4         15 [ $cx/$c, $cy/$c ];
326             }
327              
328              
329             sub polygon_is_closed(@)
330 30 50   30 1 81 { @_ or croak "ERROR: empty polygon is neither closed nor open";
331              
332 30         74 my ($first, $last) = @_[0,-1];
333 30 50       179 $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 60 { my $p = shift;
343              
344 20         47 my ($x, $y) = @$p;
345 20         32 my $minDist;
346              
347 20 100       57 @_ or return undef;
348              
349 19         33 my ($x1, $y1) = @{ (shift) };
  19         38  
350 19 100       49 unless(@_)
351 1         3 { my ($dx, $dy) = ($x1 - $x, $y1 - $y);
352 1         8 return sqrt($dx * $dx + $dy * $dy);
353             }
354              
355 18         49 while(@_)
356 72         103 { my ($x2, $y2) = @{ (shift) }; # closed poly!
  72         129  
357 72         118 my $A = $x - $x1;
358 72         105 my $B = $y - $y1;
359 72         107 my $C = $x2 - $x1;
360 72         103 my $D = $y2 - $y1;
361              
362             # closest point to the line segment
363 72         111 my $dot = $A * $C + $B * $D;
364 72         108 my $len_sq = $C * $C + $D * $D;
365 72 50       159 my $angle = $len_sq==0 ? -1 : $dot / $len_sq;
366            
367 72 100       190 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         110 my $dx = $x - $xx;
373 72         158 my $dy = $y - $yy;
374 72         121 my $dist = sqrt($dx * $dx + $dy * $dy);
375 72 100       151 $minDist = $dist unless defined $minDist;
376 72 100       162 $minDist = $dist if $dist < $minDist;
377              
378 72         186 ($x1, $y1) = ($x2, $y2);
379             }
380              
381 18         88 $minDist;
382             }
383