File Coverage

blib/lib/Geo/Coder/GooglePlaces/V3.pm
Criterion Covered Total %
statement 46 116 39.6
branch 5 44 11.3
condition 8 28 28.5
subroutine 10 17 58.8
pod 5 5 100.0
total 74 210 35.2


line stmt bran cond sub pod time code
1             package Geo::Coder::GooglePlaces::V3;
2              
3 5     5   32 use strict;
  5         8  
  5         122  
4 5     5   31 use warnings;
  5         8  
  5         101  
5              
6 5     5   34 use Carp;
  5         10  
  5         247  
7 5     5   1834 use Encode;
  5         52089  
  5         358  
8 5     5   2858 use JSON;
  5         48195  
  5         26  
9 5     5   2508 use HTTP::Request;
  5         75038  
  5         163  
10 5     5   3225 use LWP::UserAgent;
  5         127077  
  5         186  
11 5     5   44 use URI;
  5         13  
  5         5033  
12              
13             my @ALLOWED_FILTERS = qw/route locality administrative_area postal_code country/;
14              
15             =head1 NAME
16              
17             Geo::Coder::GooglePlaces::V3 - Google Places Geocoding API V3
18              
19             =head1 VERSION
20              
21             Version 0.04
22              
23             =cut
24              
25             our $VERSION = '0.04';
26              
27             =head1 SYNOPSIS
28              
29             use Geo::Coder::GooglePlaces;
30              
31             my $geocoder = Geo::Coder::GooglePlaces->new();
32             my $location = $geocoder->geocode(location => 'Hollywood and Highland, Los Angeles, CA');
33              
34             =head1 DESCRIPTION
35              
36             Geo::Coder::GooglePlaces::V3 provides a geocoding functionality using Google Places API V3.
37              
38             =head1 SUBROUTINES/METHODS
39              
40             =head2 new
41              
42             $geocoder = Geo::Coder::GooglePlaces->new();
43             $geocoder = Geo::Coder::GooglePlaces->new(language => 'ru');
44             $geocoder = Geo::Coder::GooglePlaces->new(gl => 'ca');
45             $geocoder = Geo::Coder::GooglePlaces->new(oe => 'latin1');
46              
47             To specify the language of Google's response add C parameter
48             with a two-letter value. Note that adding that parameter does not
49             guarantee that every request returns translated data.
50              
51             You can also set C parameter to set country code (e.g. I for Canada).
52              
53             You can ask for a character encoding other than utf-8 by setting the I
54             parameter, but this is not recommended.
55              
56             You can optionally use your Places Premier Client ID, by passing your client
57             code as the C parameter and your private key as the C parameter.
58             The URL signing for Premier Client IDs requires the I
59             and I modules. To test your client, set the environment
60             variables GMAP_CLIENT and GMAP_KEY before running v3_live.t
61              
62             GMAP_CLIENT=your_id GMAP_KEY='your_key' make test
63              
64             You can get a key from https://console.developers.google.com/apis/credentials.
65              
66             =cut
67              
68             sub new {
69 10     10 1 27 my($class, %param) = @_;
70              
71 10   33     77 my $ua = delete $param{ua} || LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION");
72 10   50     9484 my $host = delete $param{host} || 'maps.googleapis.com';
73              
74 10   33     36 my $language = delete $param{language} || delete $param{hl};
75 10   33     34 my $region = delete $param{region} || delete $param{gl};
76 10   50     29 my $oe = delete $param{oe} || 'utf8';
77 10   50     31 my $sensor = delete $param{sensor} || 0;
78 10   50     33 my $client = delete $param{client} || '';
79 10   50     30 my $key = delete $param{key} || '';
80 10         16 my $components = delete $param{components};
81              
82 10         83 bless {
83             ua => $ua, host => $host, language => $language,
84             region => $region, oe => $oe, sensor => $sensor,
85             client => $client, key => $key,
86             components => $components,
87             }, $class;
88             }
89              
90             =head2 geocode
91              
92             $location = $geocoder->geocode(location => $location);
93             @location = $geocoder->geocode(location => $location);
94              
95             Queries I<$location> to Google Places geocoding API and returns hash
96             reference returned back from API server.
97             When you call the method in
98             an array context, it returns all the candidates got back, while it
99             returns the 1st one in a scalar context.
100              
101             When you'd like to pass non-ASCII string as a location, you should
102             pass it as either UTF-8 bytes or Unicode flagged string.
103              
104             =cut
105              
106             sub geocode {
107 0     0 1 0 my $self = shift;
108              
109 0         0 my %param;
110 0 0       0 if (@_ % 2 == 0) {
111 0         0 %param = @_;
112             } else {
113 0         0 $param{location} = shift;
114             }
115              
116             my $location = $param{location}
117 0 0       0 or Carp::croak('Usage: geocode(location => $location)');
118              
119 0 0       0 if (Encode::is_utf8($location)) {
120 0         0 $location = Encode::encode_utf8($location);
121             }
122              
123 0 0       0 my $loc_param = $param{reverse} ? 'latlng' : 'query';
124              
125 0         0 my $uri = URI->new("https://$self->{host}/maps/api/place/textsearch/json");
126 0         0 my %query_parameters = ($loc_param => $location);
127 0 0       0 $query_parameters{language} = $self->{language} if defined $self->{language};
128 0 0       0 $query_parameters{region} = $self->{region} if defined $self->{region};
129 0         0 $query_parameters{oe} = $self->{oe};
130 0 0       0 $query_parameters{sensor} = $self->{sensor} ? 'true' : 'false';
131 0         0 my $components_params = $self->_get_components_query_params;
132 0 0       0 $query_parameters{components} = $components_params if defined $components_params;
133 0 0 0     0 $query_parameters{key} = $self->{key} if(defined($self->{key}) && (length $self->{key}));
134 0         0 $uri->query_form(%query_parameters);
135 0         0 my $url = $uri->as_string;
136              
137             # Process Places Premier account info
138 0 0 0     0 if ($self->{client} and $self->{key}) {
139 0         0 delete $query_parameters{key};
140 0         0 $query_parameters{client} = $self->{client};
141 0         0 $uri->query_form(%query_parameters);
142              
143 0         0 my $signature = $self->_make_signature($uri);
144             # signature must be last parameter in query string or you get 403's
145 0         0 $url = $uri->as_string;
146 0 0       0 $url .= '&signature='.$signature if $signature;
147             }
148              
149 0         0 my $res = $self->{ua}->get($url);
150              
151 0 0       0 if ($res->is_error) {
152 0         0 Carp::croak('Google Places API returned error: ', $res->status_line());
153             }
154              
155 0         0 my $json = JSON->new()->utf8();
156 0         0 my $data = $json->decode($res->decoded_content());
157              
158 0 0 0     0 unless ($data->{status} eq 'OK' || $data->{status} eq 'ZERO_RESULTS') {
159 0         0 Carp::croak(sprintf "Google Places API returned status '%s'", $data->{status});
160             }
161              
162 0 0       0 my @results = @{ $data->{results} || [] };
  0         0  
163 0 0       0 wantarray ? @results : $results[0];
164             }
165              
166             =head2 reverse_geocode
167              
168             $location = $geocoder->reverse_geocode(latlng => '37.778907,-122.39732');
169             @location = $geocoder->reverse_geocode(latlng => '37.778907,-122.39732');
170              
171             Similar to geocode except it expects a latitude/longitude parameter.
172              
173             =cut
174              
175             sub reverse_geocode {
176 0     0 1 0 my $self = shift;
177              
178 0         0 my %param;
179 0 0       0 if (@_ % 2 == 0) {
180 0         0 %param = @_;
181             } else {
182 0         0 $param{latlng} = shift;
183             }
184              
185             my $latlng = $param{latlng}
186 0 0       0 or Carp::croak('Usage: reverse_geocode(latlng => $latlng)');
187              
188 0         0 return $self->geocode(location => $latlng, reverse => 1);
189             }
190              
191             # methods below adapted from
192             # http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/urlsigner.pl
193             sub _decode_urlsafe_base64 {
194 0     0   0 my ($self, $content) = @_;
195              
196 0         0 $content =~ tr/-/\+/;
197 0         0 $content =~ tr/_/\//;
198              
199 0         0 return MIME::Base64::decode_base64($content);
200             }
201              
202             sub _encode_urlsafe{
203 0     0   0 my ($self, $content) = @_;
204 0         0 $content =~ tr/\+/\-/;
205 0         0 $content =~ tr/\//\_/;
206              
207 0         0 return $content;
208             }
209              
210             sub _make_signature {
211 0     0   0 my ($self, $uri) = @_;
212              
213 0         0 require Digest::HMAC_SHA1;
214 0         0 require MIME::Base64;
215              
216 0         0 my $key = $self->_decode_urlsafe_base64($self->{key});
217 0         0 my $to_sign = $uri->path_query;
218              
219 0         0 my $digest = Digest::HMAC_SHA1->new($key);
220 0         0 $digest->add($to_sign);
221 0         0 my $signature = $digest->b64digest;
222              
223 0         0 return $self->_encode_urlsafe($signature);
224             }
225              
226             # Google API wants the components formatted in the following way:
227             # :|:|....|:
228             sub _get_components_query_params {
229 7     7   31 my ($self, ) = @_;
230 7         14 my $components = $self->{components};
231              
232 7         8 my @validated_components;
233 7         25 foreach my $filter (sort keys %$components ) {
234 8 100       15 next unless grep {$_ eq $filter} @ALLOWED_FILTERS;
  40         69  
235 7         12 my $value = $components->{$filter};
236 7 50       15 if (!defined $value) {
237 0         0 Carp::croak("Value not specified for filter $filter");
238             }
239             # Google API expects the parameter to be passed as :
240 7         19 push @validated_components, "$filter:$value";
241             }
242 7 100       17 return unless @validated_components;
243 6         33 return join('|', @validated_components);
244             }
245              
246             =head2 ua
247              
248             Accessor method to get and set UserAgent object used internally. You
249             can call I for example, to get the proxy information from
250             environment variables:
251              
252             $coder->ua->env_proxy(1);
253              
254             You can also set your own User-Agent object:
255              
256             $coder->ua( LWP::UserAgent::Throttled->new() );
257              
258             =cut
259              
260             sub ua {
261 0     0 1   my $self = shift;
262 0 0         if (@_) {
263 0           $self->{ua} = shift;
264             }
265 0           $self->{ua};
266             }
267              
268             =head2 key
269              
270             Accessor method to get and set your Google API key.
271              
272             print $coder->key(), "\n";
273              
274             =cut
275              
276             sub key {
277 0     0 1   my $self = shift;
278 0 0         if (@_) {
279 0           $self->{key} = shift;
280             }
281 0           $self->{key};
282             }
283              
284             1;
285             __END__