File Coverage

lib/Math/ConvexHull.pm
Criterion Covered Total %
statement 66 68 97.0
branch 9 12 75.0
condition 14 18 77.7
subroutine 8 8 100.0
pod 1 1 100.0
total 98 107 91.5


line stmt bran cond sub pod time code
1             package Math::ConvexHull;
2 2     2   51213 use 5.006;
  2         6  
  2         78  
3 2     2   10 use strict;
  2         4  
  2         45  
4 2     2   8 use warnings;
  2         4  
  2         65  
5            
6 2     2   8 use constant PI => 3.1415926535897932384626433832795;
  2         4  
  2         1515  
7            
8             require Exporter;
9            
10             our $VERSION = '1.04';
11            
12            
13             our @ISA = qw(Exporter);
14            
15             our @EXPORT_OK = qw(
16             convex_hull
17             );
18            
19             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
20            
21            
22             sub convex_hull {
23 6     6 1 12188 my $points = shift;
24            
25 6         20 my $start_index = _find_start_point($points); # O(n)
26            
27 6         16 my $angles_points = _calculate_angles($points, $start_index); # O(n)
28 6         18 my $start = splice(@$angles_points, $start_index, 1);
29            
30 31 50 66     109 @$angles_points =
31             sort {
32 6         31 $a->[0] <=> $b->[0] ||
33             $a->[1][0] <=> $b->[1][0] ||
34             $a->[1][1] <=> $b->[1][1]
35             }
36             @$angles_points; # O(n*log(n))
37            
38 6         42 unshift @$angles_points, $start;
39            
40             # remove duplicates (O(n))
41             # At the same time, drop the angle
42 6         20 my $prev = $angles_points->[0][1];
43 6         19 my @hull;
44 6         13 push @hull, $prev;
45 6         12 for my $r (@$angles_points) {
46 31         40 my $p = $r->[1];
47 31 100 100     820 push @hull, $p
      66        
      100        
48             if ( $p->[0]+1e-15 <= $prev->[0] || $p->[0]-1e-15 >= $prev->[0]
49             || $p->[1]+1e-15 <= $prev->[1] || $p->[1]-1e-15 >= $prev->[1]);
50 31         75 $prev = $p;
51             }
52            
53             # copy of the reference point as sentinel to stop loop
54 6         27 unshift @hull, $hull[0];
55            
56 6         10 my $n_in_hull = 2;
57             # O(n)
58 6         26 for (my $i = 3; $i < @hull; ++$i) {
59 18         46 while (
60             _ccw(
61             $hull[$n_in_hull-1],
62             $hull[$n_in_hull],
63             $hull[$i]
64             ) <= 0
65             ) {
66 6 50       15 if ($n_in_hull == 2) {
67 0         0 ($hull[$i], $hull[$n_in_hull]) = (@hull[$n_in_hull, $i]);
68 0         0 ++$i;
69             }
70             else {
71 6         14 --$n_in_hull;
72             }
73             }
74 18         23 ++$n_in_hull;
75 18         64 ($hull[$i], $hull[$n_in_hull]) = (@hull[$n_in_hull, $i]);
76             }
77            
78             # return points in hull
79 6         43 return [@hull[1..$n_in_hull]];
80             }
81            
82             sub _ccw {
83 24     24   31 my $p1 = shift;
84 24         26 my $p2 = shift;
85 24         25 my $p3 = shift;
86            
87             return(
88 24         118 ($p2->[0] - $p1->[0])*($p3->[1] - $p1->[1])
89             -
90             ($p2->[1] - $p1->[1])*($p3->[0] - $p1->[0])
91             );
92             }
93            
94             sub _calculate_angles {
95 6     6   9 my $points = shift;
96 6         28 my $start = shift;
97            
98 6         11 my $s_x = $points->[$start]->[0];
99 6         14 my $s_y = $points->[$start]->[1];
100            
101 6         10 my $angles = [];
102            
103 6         10 my $p_no = 0;
104 6         11 foreach my $p (@$points) {
105 31         32 my $angle;
106 31 100       65 if ($p_no == $start) {
107 6         10 $angle = 0;
108             }
109             else {
110 25         37 my $x_diff = $p->[0] - $s_x;
111 25         34 my $y_diff = $p->[1] - $s_y;
112            
113 25         113 $angle = atan2($y_diff, $x_diff);
114 25 50       59 $angle = PI-$angle if $angle < 0;
115             }
116            
117 31         69 push @$angles, [$angle, $p];
118 31         53 $p_no++;
119             }
120            
121 6         15 return $angles;
122             }
123            
124            
125            
126             # Returns the index of the starting point.
127             sub _find_start_point {
128 6     6   8 my $points = shift;
129            
130             # Looking for the lowest, then leftmost point.
131            
132 6         12 my $s_point = 0;
133            
134 6         26 for (my $i = 1; $i < @$points; $i++) {
135 25         26 my ($p0, $p1) = @{ $points->[$i] };
  25         53  
136 25         28 my ($sp0, $sp1) = @{ $points->[$s_point] };
  25         40  
137 25 100 66     132 if (
      66        
138             $p1 <= $sp1 and
139             $p1 < $sp1 ||
140             $p0 < $sp0
141             ) {
142 1         6 $s_point = $i;
143             }
144             }
145            
146 6         15 return $s_point;
147             }
148            
149            
150            
151             1;
152             __END__