File Coverage

blib/lib/Geo/Distance.pm
Criterion Covered Total %
statement 122 178 68.5
branch 20 48 41.6
condition 20 44 45.4
subroutine 13 14 92.8
pod 4 6 66.6
total 179 290 61.7


line stmt bran cond sub pod time code
1             package Geo::Distance;
2 4     4   824986 use 5.008001;
  4         34  
3 4     4   19 use strict;
  4         7  
  4         70  
4 4     4   16 use warnings;
  4         5  
  4         162  
5             our $VERSION = '0.25';
6              
7 4     4   1731 use GIS::Distance;
  4         9605  
  4         128  
8 4     4   1731 use GIS::Distance::Constants qw( :all );
  4         68042  
  4         525  
9 4     4   31 use Carp qw( croak );
  4         8  
  4         160  
10 4     4   20 use Const::Fast;
  4         9  
  4         25  
11              
12             const our %GEO_TO_GIS_FORMULA_MAP => (qw(
13             alt ALT
14             cos Cosine
15             gcd GreatCircle
16             hsin Haversine
17             mt MathTrig
18             null Null
19             polar Polar
20             tv Vincenty
21             ));
22              
23             const our @FORMULAS => (keys %GEO_TO_GIS_FORMULA_MAP);
24              
25             sub new {
26 4     4 0 365 my $class = shift;
27 4         12 my $self = bless {}, $class;
28 4         12 my %args = @_;
29              
30 4         21 $self->{formula} = 'hsin';
31 4         11 $self->{units} = {};
32 4 50       16 if(!$args{no_units}){
33 4         51 $self->reg_unit( $KILOMETER_RHO, 'kilometer' );
34 4         32 $self->reg_unit( 1000, 'meter', => 'kilometer' );
35 4         13 $self->reg_unit( 100, 'centimeter' => 'meter' );
36 4         12 $self->reg_unit( 10, 'millimeter' => 'centimeter' );
37              
38 4         12 $self->reg_unit( 'kilometre' => 'kilometer' );
39 4         12 $self->reg_unit( 'metre' => 'meter' );
40 4         9 $self->reg_unit( 'centimetre' => 'centimeter' );
41 4         10 $self->reg_unit( 'millimetre' => 'millimeter' );
42              
43 4         11 $self->reg_unit( 'mile' => 1609.344, 'meter' );
44 4         10 $self->reg_unit( 'nautical mile' => 1852, 'meter' );
45 4         10 $self->reg_unit( 'yard' => 0.9144, 'meter' );
46 4         9 $self->reg_unit( 3, 'foot' => 'yard' );
47 4         12 $self->reg_unit( 12, 'inch' => 'foot' );
48 4         16 $self->reg_unit( 'light second' => 299792458, 'meter' );
49              
50 4         10 $self->reg_unit( 'poppy seed' => 2.11, 'millimeter' );
51 4         9 $self->reg_unit( 'barleycorn' => 8.467, 'millimeter' );
52 4         11 $self->reg_unit( 'rod' => 5.0292, 'meter' );
53 4         10 $self->reg_unit( 'pole' => 'rod' );
54 4         10 $self->reg_unit( 'perch' => 'rod' );
55 4         10 $self->reg_unit( 'chain' => 20.1168, 'meter' );
56 4         9 $self->reg_unit( 'furlong' => 201.168, 'meter' );
57 4         10 $self->reg_unit( 'league' => 4.828032, 'kilometer' );
58 4         8 $self->reg_unit( 1.8288, 'fathom' => 'meter' );
59             }
60              
61 4         11 return $self;
62             }
63              
64             sub formula {
65 2     2 1 513 my $self = shift;
66              
67 2 50       6 return $self->{formula} if !$_[0];
68 2         5 my $formula = shift;
69              
70 2         6 my $gis_formula = $GEO_TO_GIS_FORMULA_MAP{ $formula };
71              
72 2 50       6 croak(
73             'Unknown formula (available formulas are ',
74             join(', ', sort @FORMULAS),
75             ')',
76             ) if !$gis_formula;
77              
78 2         5 $self->{formula} = $formula;
79 2         6 $self->{gis_formula} = $gis_formula;
80              
81 2         3 return $formula;
82             }
83              
84             sub distance {
85 39     39 1 113 my ($self, $unit, $lon1, $lat1, $lon2, $lat2) = @_;
86              
87 39         63 my $unit_rho = $self->{units}->{$unit};
88 39 50       62 croak('Unkown unit type "' . $unit . '"') if !$unit_rho;
89              
90 39         145 my $gis = GIS::Distance->new( $self->{gis_formula} );
91              
92             # Reverse lon/lat to lat/lon, the way GIS::Distance wants it.
93 39         82979 my $km = $gis->{code}->( $lat1, $lon1, $lat2, $lon2 );
94              
95 39         1299 return $km * ($unit_rho / $KILOMETER_RHO);
96             }
97              
98 4     4   4492 use Math::Trig qw( acos asin atan deg2rad great_circle_distance pi tan );
  4         47565  
  4         5469  
99              
100             sub old_distance {
101 0     0 0 0 my($self,$unit,$lon1,$lat1,$lon2,$lat2) = @_;
102 0 0       0 croak('Unkown unit type "'.$unit.'"') unless($unit = $self->{units}->{$unit});
103              
104 0 0       0 return 0 if $self->{formula} eq 'null';
105 0 0       0 return 0 if $self->{formula} eq 'alt';
106              
107 0 0       0 if($self->{formula} eq 'mt'){
108 0         0 return great_circle_distance(
109             deg2rad($lon1),
110             deg2rad(90 - $lat1),
111             deg2rad($lon2),
112             deg2rad(90 - $lat2),
113             $unit
114             );
115             }
116              
117 0         0 $lon1 = deg2rad($lon1); $lat1 = deg2rad($lat1);
  0         0  
118 0         0 $lon2 = deg2rad($lon2); $lat2 = deg2rad($lat2);
  0         0  
119 0         0 my $c;
120 0 0       0 if($self->{formula} eq 'cos'){
    0          
    0          
    0          
    0          
121 0         0 my $a = sin($lat1) * sin($lat2);
122 0         0 my $b = cos($lat1) * cos($lat2) * cos($lon2 - $lon1);
123 0         0 $c = acos($a + $b);
124             }
125             elsif($self->{formula} eq 'hsin'){
126 0         0 my $dlon = $lon2 - $lon1;
127 0         0 my $dlat = $lat2 - $lat1;
128 0         0 my $a = (sin($dlat/2)) ** 2 + cos($lat1) * cos($lat2) * (sin($dlon/2)) ** 2;
129 0         0 $c = 2 * atan2(sqrt($a), sqrt(abs(1-$a)));
130             }
131             elsif($self->{formula} eq 'polar'){
132 0         0 my $a = pi/2 - $lat1;
133 0         0 my $b = pi/2 - $lat2;
134 0         0 $c = sqrt( $a ** 2 + $b ** 2 - 2 * $a * $b * cos($lon2 - $lon1) );
135             }
136             elsif($self->{formula} eq 'gcd'){
137 0         0 $c = 2*asin( sqrt(
138             ( sin(($lat1-$lat2)/2) )**2 +
139             cos($lat1) * cos($lat2) *
140             ( sin(($lon1-$lon2)/2) )**2
141             ) );
142              
143             # Eric Samuelson recommended this formula.
144             # http://forums.devshed.com/t54655/sc3d021a264676b9b440ea7cbe1f775a1.html
145             # http://williams.best.vwh.net/avform.htm
146             # It seems to produce the same results at the hsin formula, so...
147              
148             #my $dlon = $lon2 - $lon1;
149             #my $dlat = $lat2 - $lat1;
150             #my $a = (sin($dlat / 2)) ** 2
151             # + cos($lat1) * cos($lat2) * (sin($dlon / 2)) ** 2;
152             #$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
153             }
154             elsif($self->{formula} eq 'tv'){
155 0         0 my($a,$b,$f) = (6378137,6356752.3142,1/298.257223563);
156 0         0 my $l = $lon2 - $lon1;
157 0         0 my $u1 = atan((1-$f) * tan($lat1));
158 0         0 my $u2 = atan((1-$f) * tan($lat2));
159 0         0 my $sin_u1 = sin($u1); my $cos_u1 = cos($u1);
  0         0  
160 0         0 my $sin_u2 = sin($u2); my $cos_u2 = cos($u2);
  0         0  
161 0         0 my $lambda = $l;
162 0         0 my $lambda_pi = 2 * pi;
163 0         0 my $iter_limit = 20;
164 0         0 my($cos_sq_alpha,$sin_sigma,$cos2sigma_m,$cos_sigma,$sigma);
165 0   0     0 while( abs($lambda-$lambda_pi) > 1e-12 && --$iter_limit>0 ){
166 0         0 my $sin_lambda = sin($lambda); my $cos_lambda = cos($lambda);
  0         0  
167 0         0 $sin_sigma = sqrt(($cos_u2*$sin_lambda) * ($cos_u2*$sin_lambda) +
168             ($cos_u1*$sin_u2-$sin_u1*$cos_u2*$cos_lambda) * ($cos_u1*$sin_u2-$sin_u1*$cos_u2*$cos_lambda));
169 0         0 $cos_sigma = $sin_u1*$sin_u2 + $cos_u1*$cos_u2*$cos_lambda;
170 0         0 $sigma = atan2($sin_sigma, $cos_sigma);
171 0         0 my $alpha = asin($cos_u1 * $cos_u2 * $sin_lambda / $sin_sigma);
172 0         0 $cos_sq_alpha = cos($alpha) * cos($alpha);
173 0         0 $cos2sigma_m = $cos_sigma - 2*$sin_u1*$sin_u2/$cos_sq_alpha;
174 0         0 my $cc = $f/16*$cos_sq_alpha*(4+$f*(4-3*$cos_sq_alpha));
175 0         0 $lambda_pi = $lambda;
176 0         0 $lambda = $l + (1-$cc) * $f * sin($alpha) *
177             ($sigma + $cc*$sin_sigma*($cos2sigma_m+$cc*$cos_sigma*(-1+2*$cos2sigma_m*$cos2sigma_m)));
178             }
179 0 0       0 undef if( $iter_limit==0 );
180 0         0 my $usq = $cos_sq_alpha*($a*$a-$b*$b)/($b*$b);
181 0         0 my $aa = 1 + $usq/16384*(4096+$usq*(-768+$usq*(320-175*$usq)));
182 0         0 my $bb = $usq/1024 * (256+$usq*(-128+$usq*(74-47*$usq)));
183 0         0 my $delta_sigma = $bb*$sin_sigma*($cos2sigma_m+$bb/4*($cos_sigma*(-1+2*$cos2sigma_m*$cos2sigma_m)-
184             $bb/6*$cos2sigma_m*(-3+4*$sin_sigma*$sin_sigma)*(-3+4*$cos2sigma_m*$cos2sigma_m)));
185 0         0 $c = ( $b*$aa*($sigma-$delta_sigma) ) / $self->{units}->{meter};
186             }
187             else{
188 0         0 croak('Unkown distance formula "'.$self->{formula}.'"');
189             }
190              
191 0         0 return $unit * $c;
192             }
193              
194             sub closest {
195 4     4 1 9202 my $self = shift;
196 4         33 my %args = @_;
197              
198             # Set defaults and prepare.
199 4   33     16 my $dbh = $args{dbh} || croak('You must supply a database handle');
200 4 50       28 $dbh->isa('DBI::db') || croak('The dbh must be a DBI database handle');
201 4   33     21 my $table = $args{table} || croak('You must supply a table name');
202 4   33     12 my $lon = $args{lon} || croak('You must supply a longitude');
203 4   33     7 my $lat = $args{lat} || croak('You must supply a latitude');
204 4   33     9 my $distance = $args{distance} || croak('You must supply a distance');
205 4   33     10 my $unit = $args{unit} || croak('You must specify a unit type');
206 4   33     16 my $unit_size = $self->{units}->{$unit} || croak('This unit type is not known');
207 4         22 my $degrees = $distance / ( $DEG_RATIO * $unit_size );
208 4   50     19 my $lon_field = $args{lon_field} || 'lon';
209 4   50     15 my $lat_field = $args{lat_field} || 'lat';
210 4   50     14 my $fields = $args{fields} || [];
211              
212 4         12 unshift @$fields, $lon_field, $lat_field;
213 4         12 $fields = join( ',', @$fields );
214 4   100     16 my $count = $args{count} || 0;
215 4   66     15 my $sort = $args{sort} || ( $count ? 1 : 0 );
216 4         13 my $where = qq{$lon_field >= ? AND $lat_field >= ? AND $lon_field <= ? AND $lat_field <= ?};
217 4 50       11 $where .= ( $args{where} ? " AND ($args{where})" : '' );
218              
219             my @bind = (
220             $lon-$degrees, $lat-$degrees,
221             $lon+$degrees, $lat+$degrees,
222 4 50       25 ( $args{bind} ? @{$args{bind}} : () )
  0         0  
223             );
224              
225             # Retrieve locations.
226 4         59 my $sth = $dbh->prepare(qq{
227             SELECT $fields
228             FROM $table
229             WHERE $where
230             });
231 4         628 $sth->execute( @bind );
232 4         12 my $locations = [];
233 4         175 while(my $location = $sth->fetchrow_hashref){
234 35         354 push @$locations, $location;
235             }
236              
237             # Calculate distances.
238 4         11 my $closest = [];
239 4         8 foreach my $location (@$locations){
240             $location->{distance} = $self->distance(
241             $unit, $lon, $lat,
242             $location->{$lon_field},
243 35         86 $location->{$lat_field}
244             );
245 35 100       77 if( $location->{distance} <= $distance ){
246 32         55 push @$closest, $location;
247             }
248             }
249 4         9 $locations = $closest;
250              
251             # Sort.
252 4 100       10 if( $sort ){
253 1         9 @$locations = sort { $a->{distance} <=> $b->{distance} } @$locations;
  17         23  
254             }
255              
256             # Split for count.
257 4 100 66     19 if( $count and $count < @$locations ){
258 1         5 splice @$locations, $count;
259             }
260              
261 4         103 return $locations;
262             }
263              
264             sub reg_unit {
265 92     92 1 103 my $self = shift;
266 92         100 my $units = $self->{units};
267 92         111 my($count1,$key1,$count2,$key2);
268 92         97 $count1 = shift;
269 92 100 66     272 if($count1=~/[^\.0-9]/ or !@_){ $key1=$count1; $count1=1; }
  64         74  
  64         66  
270 28         42 else{ $key1 = shift; }
271 92 100       119 if(!@_){
272 4         12 $units->{$key1} = $count1;
273             }else{
274 88         96 $count2 = shift;
275 88 100 66     259 if($count2=~/[^\.0-9]/ or !@_){ $key2=$count2; $count2=1; }
  48         58  
  48         53  
276 40         61 else{ $key2 = shift; }
277 88 50       145 ($key1,$key2) = ($key2,$key1) if( defined $units->{$key1} );
278 88         213 $units->{$key1} = ($units->{$key2}*$count1) / $count2;
279             }
280             }
281              
282             1;
283             __END__