File Coverage

blib/lib/Geo/Distance/Google.pm
Criterion Covered Total %
statement 30 133 22.5
branch 0 54 0.0
condition 0 43 0.0
subroutine 10 20 50.0
pod 9 9 100.0
total 49 259 18.9


line stmt bran cond sub pod time code
1             package Geo::Distance::Google;
2              
3 1     1   25691 use strict;
  1         3  
  1         50  
4 1     1   6 use warnings;
  1         2  
  1         54  
5              
6             our $VERSION = '0.01';
7              
8 1     1   6 use Carp;
  1         6  
  1         122  
9 1     1   2255 use Data::Dumper qw( Dumper );
  1         20853  
  1         100  
10 1     1   1066 use Encode;
  1         14400  
  1         107  
11 1     1   1248 use JSON;
  1         15973  
  1         8  
12 1     1   1213 use HTTP::Request;
  1         27412  
  1         36  
13 1     1   1286 use LWP::UserAgent;
  1         37828  
  1         48  
14 1     1   1501 use Params::Validate;
  1         15936  
  1         74  
15 1     1   10 use URI;
  1         2  
  1         2024  
16              
17             sub new {
18 0     0 1   my($class, %param) = @_;
19              
20 0   0       my $ua = delete $param{ua} || LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION");
21 0   0       my $host = delete $param{host} || 'maps.googleapis.com';
22              
23 0   0       my $language = delete $param{language} || delete $param{hl};
24 0   0       my $region = delete $param{region} || delete $param{gl};
25 0   0       my $oe = delete $param{oe} || 'utf8';
26 0   0       my $sensor = delete $param{sensor} || 0;
27 0   0       my $client = delete $param{client} || '';
28 0   0       my $key = delete $param{key} || '';
29 0   0       my $units = delete $param{units} || '';
30 0           my $mode = delete $param{mode};
31 0           my $avoid = delete $param{avoid};
32 0   0       my $https = delete $param{https} || 0;
33 0   0       my $debug = delete $param{debug} || 0;
34            
35 0           bless {
36             ua => $ua,
37             host => $host,
38             language => $language,
39             region => $region,
40             oe => $oe,
41             sensor => $sensor,
42             client => $client,
43             key => $key,
44             units => $units,
45             mode => $mode,
46             avoid => $avoid,
47             https => $https,
48             __debug__ => $debug
49             }, $class;
50             }
51              
52             sub debug_level {
53 0     0 1   my $self = shift;
54              
55 0 0         if ( @_ ) { $self->{__debug__} = shift; }
  0            
56              
57 0           return $self->{__debug__};
58             }
59              
60             sub ua {
61 0     0 1   my $self = shift;
62 0 0         if (@_) {
63 0           $self->{ua} = shift;
64             }
65 0           $self->{ua};
66             }
67              
68             sub raw_distance {
69 0     0 1   my $self = shift;
70              
71 0 0         $self->{__raw_response__} = shift if @_;
72              
73 0           return $self->{__raw_response__};
74             }
75              
76             sub distance {
77 0     0 1   my $self = shift;
78 0           my %p = validate @_, {
79             origins => 1,
80             destinations => 1,
81             mode => 0,
82             avoid => 0,
83             units => 0
84             };
85              
86 0           my $origins;
87             my $destinations;
88              
89             # both can be array refs or single items
90 0           foreach my $k ( qw( origins destinations ) ) {
91             # convert to google format
92              
93             # TODO: in future allow seperate lat & long in hash ref
94 0 0         if ( ref $p{$k} ne 'ARRAY' ) {
95 0           $p{$k} = [ $p{$k} ];
96             }
97             }
98              
99 0           $origins = join '|', @{ $p{origins} };
  0            
100 0           $destinations = join '|', @{ $p{destinations} };
  0            
101              
102              
103 0 0         $origins = Encode::is_utf8( $origins ) ? Encode::encode_utf8( $origins ) : $origins;
104 0 0         $destinations = Encode::is_utf8( $destinations ) ? Encode::encode_utf8( $destinations ) : $destinations;
105              
106 0 0         my $url = sprintf "%s://%s/maps/api/distancematrix/json",
107             ( $self->{https} ? 'https' : 'http' ), $self->{host};
108              
109 0           my $uri = URI->new($url);
110              
111             # build query
112 0 0         my %query_parameters = (
    0          
    0          
    0          
    0          
    0          
113             origins => $origins,
114             destinations => $destinations,
115             oe => $self->{oe},
116             sensor => ( $self->{sensor} ? 'true' : 'false' ),
117             # optional parameters
118             ( $self->{units} ? ( units => $self->{units} ) : () ),
119             ( $p{mode} ? ( mode => $p{mode} )
120             : defined $self->{mode} ? ( mode => $self->{mode} ) : ()),
121             # TODO: add support for avoid as list ref process too
122             ( $p{avoid} ? ( avoid => $p{avoid} )
123             : defined $self->{avoid} ? ( avoid => $self->{avoid} ) : () )
124             );
125              
126             # not sure about these
127 0 0         $query_parameters{language} = $self->{language} if defined $self->{language};
128 0 0         $query_parameters{region} = $self->{region} if defined $self->{region};
129              
130 0           $uri->query_form(%query_parameters);
131              
132             # setup request
133 0           $url = $uri->as_string;
134              
135             # Process Maps Premier account info
136 0 0 0       if ($self->{client} and $self->{key}) {
137 0           $query_parameters{client} = $self->{client};
138 0           $uri->query_form(%query_parameters);
139              
140 0           my $signature = $self->make_signature($uri);
141             # signature must be last parameter in query string or you get 403's
142 0           $url = $uri->as_string;
143 0 0         $url .= '&signature='.$signature if $signature;
144             }
145              
146 0           $self->debug( "Sending request: $url" );
147              
148 0           my $res = $self->{ua}->get($url);
149              
150 0 0         if ($res->is_error) {
151 0           Carp::croak("Google Maps API returned error: " . $res->status_line);
152             }
153              
154 0 0         if ( $res->headers->content_type !~ /json/ ) {
155 0           my $ct = $res->headers->content_type;
156 0           croak "Invalid content-type '$ct' returned from webserver";
157             }
158              
159 0           my $json = JSON->new->utf8;
160 0           my $data = $json->decode($res->content);
161              
162 0           $self->raw_distance( $data );
163              
164 0           $self->debug( "data: " . Dumper( $data ) );
165              
166 0 0 0       if ( ! defined $data->{status} || $data->{status} ne 'OK' ) {
167 0   0       croak "Google Maps API status error: " . ( $data->{status} || 'Invalid status' );
168             }
169              
170             # reprocess to make more friendly (IMO)
171 0           my $distance = [];
172              
173             # origins[0] correspond to rows[0]
174             # destinations[0] correspond to rows->[x]->elements[0]
175 0           for ( my $oid = 0; $oid < scalar( @{ $p{origins} } ); $oid++ ) {
  0            
176              
177             # verify origin information
178 0 0 0       next unless defined $data->{origin_addresses} &&
179             defined $data->{origin_addresses}->[$oid];
180              
181             # missing return data
182 0 0 0       next unless defined $data->{rows} && $data->{rows}->[$oid];
183              
184 0           $distance->[$oid]->{origin_address} = $data->{origin_addresses}->[$oid];
185              
186 0           my $elements = $data->{rows}->[$oid]->{elements};
187              
188             # loop through each destination address
189 0           foreach ( my $did = 0; $did < scalar( @{ $p{destinations} } ); $did++ ) {
  0            
190 0 0         next unless defined $elements->[$did];
191              
192             # reformat it to be a bit nicer for the consumer
193 0           $distance->[$oid]->{destinations}->[$did] = {
194             address => $data->{destination_addresses}->[$did],
195             distance => $elements->[$did]->{distance},
196             duration => $elements->[$did]->{duration},
197             status => $elements->[$did]->{status}
198             };
199             }
200              
201             }
202              
203 0           $self->debug( "distance: " . Dumper($distance) );
204              
205 0           return $distance;
206             }
207              
208             # methods below adapted from
209             # http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/urlsigner.pl
210             sub decode_urlsafe_base64 {
211 0     0 1   my ($self, $content) = @_;
212              
213 0           $content =~ tr/-/\+/;
214 0           $content =~ tr/_/\//;
215              
216 0           return MIME::Base64::decode_base64($content);
217             }
218              
219             sub encode_urlsafe{
220 0     0 1   my ($self, $content) = @_;
221 0           $content =~ tr/\+/\-/;
222 0           $content =~ tr/\//\_/;
223              
224 0           return $content;
225             }
226              
227             sub make_signature {
228 0     0 1   my ($self, $uri) = @_;
229              
230 0           require Digest::HMAC_SHA1;
231 0           require MIME::Base64;
232              
233 0           my $key = $self->decode_urlsafe_base64($self->{key});
234 0           my $to_sign = $uri->path_query;
235              
236 0           my $digest = Digest::HMAC_SHA1->new($key);
237 0           $digest->add($to_sign);
238 0           my $signature = $digest->b64digest;
239              
240 0           return $self->encode_urlsafe($signature);
241             }
242              
243             # search input hash ref, then self for defined parameter or
244             # return empty list
245             sub _get_multiple {
246 0     0     my $self = shift;
247 0   0       my $p = shift || return (); # params
248 0   0       my $key = shift || return (); # key
249              
250 0 0         return () unless ref $p eq 'HASH';
251              
252 0 0         return defined $p->{$key} ? ( $key => $p->{$key} )
    0          
253             : defined $self->{$key} ? ( $key => $self->{$key} ) : ();
254             }
255              
256             sub debug {
257 0     0 1   my $self = shift;
258 0           my $f = (caller(1))[3];
259              
260 0 0         return unless $self->debug_level;
261              
262 0           printf STDERR "%s [%s] %s\n", scalar( localtime ), $f, shift;
263             }
264              
265              
266             1;
267              
268             __END__