File Coverage

blib/lib/Math/Polygon/Calc.pm
Criterion Covered Total %
statement 169 189 89.4
branch 73 106 68.8
condition 113 162 69.7
subroutine 21 23 91.3
pod 16 16 100.0
total 392 496 79.0


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