File Coverage

blib/lib/Geo/Lookup/ByTime.pm
Criterion Covered Total %
statement 124 129 96.1
branch 44 54 81.4
condition 20 36 55.5
subroutine 19 20 95.0
pod 6 6 100.0
total 213 245 86.9


line stmt bran cond sub pod time code
1             package Geo::Lookup::ByTime;
2              
3 5     5   234970 use warnings;
  5         13  
  5         187  
4 5     5   29 use strict;
  5         10  
  5         167  
5 5     5   27 use Carp;
  5         13  
  5         464  
6 5     5   29 use Scalar::Util qw(blessed);
  5         8  
  5         694  
7 5     5   32 use base qw(Exporter);
  5         9  
  5         876  
8              
9             our @EXPORT_OK = qw(hav_distance);
10              
11             our $VERSION = '0.10';
12              
13 5     5   32 use constant EARTH_RADIUS => 6_378_137.0;
  5         8  
  5         633  
14 5     5   29 use constant PI => 4 * atan2( 1, 1 );
  5         9  
  5         313  
15 5     5   26 use constant DEG_TO_RAD => PI / 180.0;
  5         10  
  5         328  
16 5     5   26 use constant RAD_TO_DEG => 180.0 / PI;
  5         10  
  5         31364  
17              
18             sub new {
19 3     3 1 554 my $class = shift;
20 3         14 my $self = {
21             points => [],
22             need_sort => 0
23             };
24              
25 3         9 bless( $self, $class );
26              
27 3 100       14 if ( @_ ) {
28 2         10 $self->add_points( @_ );
29             }
30              
31 3         13 return $self;
32             }
33              
34             sub add_points {
35 10     10 1 40 my $self = shift;
36              
37 10 100       53 $self->{need_sort}++ if @_;
38              
39 10         28 for my $pt ( @_ ) {
40 306 100 66     1867 if ( blessed( $pt )
    100 66        
    100 33        
    50          
41             && $pt->can( 'latitude' )
42             && $pt->can( 'longitude' )
43             && $pt->can( 'time' ) ) {
44 11         12 push @{ $self->{points} },
  11         35  
45             {
46             lat => $pt->latitude(),
47             lon => $pt->longitude(),
48             time => $pt->time(),
49             orig => $pt
50             };
51             }
52             elsif ( ref( $pt ) eq 'CODE' ) {
53 2         34 my @pts = ();
54 2         12 while ( my $ipt = $pt->() ) {
55 271         2414 push @pts, $ipt;
56 271 100       1649 if ( @pts >= 100 ) {
57             # Add points 100 at a time.
58 2         15 $self->add_points( @pts );
59 2         18 @pts = ();
60             }
61             }
62 2         19 $self->add_points( @pts );
63             }
64             elsif ( ref( $pt ) eq 'ARRAY' ) {
65 1         2 $self->add_points( @{$pt} );
  1         32  
66             }
67             elsif ( ref( $pt ) eq 'HASH' ) {
68 292 50 33     1749 croak(
      33        
69             "Point hashes must have the following keys: lat, lon, time\n"
70             )
71             unless exists( $pt->{lat} )
72             && exists( $pt->{lon} )
73             && exists( $pt->{time} );
74 292         6485 push @{ $self->{points} }, $pt;
  292         619  
75             }
76             else {
77 0 0       0 croak( "Don't know how to add "
78             . ( defined( $pt ) ? $pt : '(undef)' ) );
79             }
80             }
81              
82 10         53 return;
83             }
84              
85             sub get_points {
86 310     310 1 782 my $self = shift;
87              
88 310 100       1935 if ( $self->{need_sort} ) {
89 399         540 my $np = [
90 313 50 33     2124 sort { $a->{time} <=> $b->{time} }
91             grep {
92 4         13 defined( $_->{lat} )
93             && defined( $_->{lon} )
94             && defined( $_->{time} )
95 4         10 } @{ $self->{points} }
96             ];
97 4         10 $self->{points} = $np;
98 4         18 $self->{need_sort} = 0;
99             }
100              
101 310         884 return $self->{points};
102             }
103              
104             # Returns the index of the first point with time >= the supplied time
105             sub _search {
106 306     306   743 my $pts = shift;
107 306         416 my $time = shift;
108              
109 306         313 my $max = scalar( @{$pts} );
  306         873  
110 306         627 my ( $lo, $mid, $hi ) = ( 0, 0, $max - 1 );
111              
112             TRY:
113 306         1211 while ( $lo <= $hi ) {
114 2352         4066 $mid = int( ( $lo + $hi ) / 2 );
115 2352         48024 my $cmp = $pts->[$mid]->{time} <=> $time;
116 2352 100       6196 if ( $cmp < 0 ) {
    100          
117 1225         3021 $lo = $mid + 1;
118             }
119             elsif ( $cmp > 0 ) {
120 1123         3307 $hi = $mid - 1;
121             }
122             else {
123 4         8 last TRY;
124             }
125             }
126              
127 306   100     3817 while ( $mid < $max && $pts->[$mid]->{time} < $time ) {
128 112         852 $mid++;
129             }
130              
131 306 100       1157 return ( $mid < $max ) ? $mid : undef;
132             }
133              
134             sub _interp {
135 600     600   1306 my ( $lo, $mid, $hi, $val1, $val2 ) = @_;
136 600 50 33     3438 confess "$lo <= $mid <= $hi !"
137             unless $lo <= $mid && $mid <= $hi;
138 600         1555 my $scale = $hi - $lo;
139 600         862 my $posn = $mid - $lo;
140 600         2124 return ( $val1 * ( $scale - $posn ) + $val2 * $posn ) / $scale;
141             }
142              
143             sub nearest {
144 306     306 1 705383 my $self = shift;
145 306         1034 my $time = shift;
146 306         608 my $max_dist = shift;
147              
148 306         780 my $pts = $self->get_points();
149 306         693 my $pos = _search( $pts, $time );
150              
151 306 100       1036 return unless defined( $pos );
152              
153 305 100       863 if ( $pts->[$pos]->{time} == $time ) {
154             # Exact match - just return the point
155 4         18 my $pt = {
156             lat => $pts->[$pos]->{lat},
157             lon => $pts->[$pos]->{lon},
158             time => $time
159             };
160              
161             return
162             wantarray
163 4 50 66     36 ? ( $pt, $pts->[$pos]->{orig} || $pts->[$pos], 0 )
164             : $pt;
165             }
166              
167             # If we're at the first point we can't
168             # interpolate with anything.
169 301 100       1546 return if $pos == 0;
170              
171 300         960 my ( $p1, $p2 ) = @$pts[ $pos - 1, $pos ];
172              
173             # Linear interpolation between nearest points
174 300         1014 my $lat = _interp( $p1->{time}, $time, $p2->{time}, $p1->{lat},
175             $p2->{lat} );
176 300         901 my $lon = _interp( $p1->{time}, $time, $p2->{time}, $p1->{lon},
177             $p2->{lon} );
178              
179 300         3150 my $pt = {
180             lat => $lat,
181             lon => $lon,
182             time => $time
183             };
184              
185 300         688 my $best_dist = 0;
186 300         387 my $best = undef;
187              
188             # Compute nearest if we need to return it or check proximity
189 300 100 66     930 if ( wantarray || defined( $max_dist ) ) {
190 200         424 my $d1 = abs( $pt->{time} - $p1->{time} );
191 200         398 my $d2 = abs( $pt->{time} - $p2->{time} );
192              
193 200 100       701 $best = ( $d1 < $d2 ) ? $p1 : $p2;
194 200         471 $best_dist = hav_distance( $pt, $best );
195              
196             # Nearest point out of range?
197 200 100 100     1251 return if defined( $max_dist ) && $best_dist > $max_dist;
198             }
199              
200             # Return a synthetic point
201             return
202 236 100 33     1503 wantarray ? ( $pt, $best->{orig} || $best, $best_dist ) : $pt;
203             }
204              
205             sub _deg {
206 0     0   0 return map { $_ * RAD_TO_DEG } @_;
  0         0  
207             }
208              
209             sub _rad {
210 1200     1200   1747 return map { $_ * DEG_TO_RAD } @_;
  2400         5177  
211             }
212              
213             # From
214             # http://perldoc.perl.org/functions/sin.html
215             sub _asin {
216 600     600   3798 return atan2( $_[0], sqrt( 1 - $_[0] * $_[0] ) );
217             }
218              
219             # Not a method
220             sub hav_distance {
221 600     600 1 231540 my $dist = 0;
222 600         783 my ( $lat1, $lon1 );
223 600         1726 while ( my $pt = shift ) {
224 1200         2899 my ( $lat2, $lon2 ) = _rad( $pt->{lat}, $pt->{lon} );
225 1200 100       2874 if ( defined( $lat1 ) ) {
226 600         1425 my $sdlat = sin( ( $lat1 - $lat2 ) / 2.0 );
227 600         1163 my $sdlon = sin( ( $lon1 - $lon2 ) / 2.0 );
228 600         10260 my $res = sqrt( $sdlat * $sdlat
229             + cos( $lat1 ) * cos( $lat2 ) * $sdlon * $sdlon );
230 600 50       2029 if ( $res > 1.0 ) {
    50          
231 0         0 $res = 1.0;
232             }
233             elsif ( $res < -1.0 ) {
234 0         0 $res = -1.0;
235             }
236 600         1142 $dist += 2.0 * _asin( $res );
237             }
238 1200         6142 ( $lat1, $lon1 ) = ( $lat2, $lon2 );
239             }
240              
241 600         1457 return $dist * EARTH_RADIUS;
242             }
243              
244             sub time_range {
245 2     2 1 21 my $self = shift;
246 2         9 my $pts = $self->get_points();
247 2 50       4 return unless @{$pts};
  2         12  
248 2         33 return ( $pts->[0]->{time}, $pts->[-1]->{time} );
249             }
250              
251             1;
252             __END__