File Coverage

blib/lib/Geo/GeoNames.pm
Criterion Covered Total %
statement 121 145 83.4
branch 37 58 63.7
condition 22 32 68.7
subroutine 22 25 88.0
pod 7 7 100.0
total 209 267 78.2


line stmt bran cond sub pod time code
1             package Geo::GeoNames;
2 5     5   118419 use utf8;
  5         119  
  5         32  
3 5     5   238 use v5.10;
  5         20  
4 5     5   27 use strict;
  5         9  
  5         113  
5 5     5   33 use warnings;
  5         10  
  5         173  
6              
7 5     5   26 use Carp;
  5         8  
  5         369  
8 5     5   2818 use Mojo::UserAgent;
  5         1829537  
  5         43  
9 5     5   208 use Scalar::Util qw/blessed/;
  5         10  
  5         278  
10              
11             =encoding utf8
12              
13             =head1 NAME
14              
15             Geo::GeoNames - Perform geographical queries using GeoNames Web Services
16              
17             =head1 VERSION
18              
19             Version 1.14
20              
21             =cut
22              
23             our $VERSION = '1.14';
24              
25 5     5   26 use vars qw($DEBUG $CACHE);
  5         11  
  5         4990  
26              
27             our %searches = (
28             cities => 'cities?',
29             country_code => 'countrycode?type=xml&',
30             country_info => 'countryInfo?',
31             earthquakes => 'earthquakesJSON?',
32             find_nearby_placename => 'findNearbyPlaceName?',
33             find_nearby_postalcodes => 'findNearbyPostalCodes?',
34             find_nearby_streets => 'findNearbyStreets?',
35             find_nearby_weather => 'findNearByWeatherXML?',
36             find_nearby_wikipedia => 'findNearbyWikipedia?',
37             find_nearby_wikipedia_by_postalcode => 'findNearbyWikipedia?',
38             find_nearest_address => 'findNearestAddress?',
39             find_nearest_intersection => 'findNearestIntersection?',
40             postalcode_country_info => 'postalCodeCountryInfo?',
41             postalcode_search => 'postalCodeSearch?',
42             search => 'search?',
43             wikipedia_bounding_box => 'wikipediaBoundingBox?',
44             wikipedia_search => 'wikipediaSearch?',
45             get => 'get?',
46             hierarchy => 'hierarchy?',
47             children => 'children?',
48             );
49              
50             # r = required
51             # o = optional
52             # rc = required - only one of the fields marked with rc is allowed. At least one must be present
53             # om = optional, multiple entries allowed
54             # d = deprecated - will be removed in later versions
55             our %valid_parameters = (
56             search => {
57             'q' => 'rc',
58             name => 'rc',
59             name_equals => 'rc',
60             maxRows => 'o',
61             startRow => 'o',
62             country => 'om',
63             continentCode => 'o',
64             adminCode1 => 'o',
65             adminCode2 => 'o',
66             adminCode3 => 'o',
67             fclass => 'omd',
68             featureClass => 'om',
69             featureCode => 'om',
70             lang => 'o',
71             type => 'o',
72             style => 'o',
73             isNameRequired => 'o',
74             tag => 'o',
75             username => 'r',
76             name_startsWith => 'o',
77             countryBias => 'o',
78             cities => 'om',
79             operator => 'o',
80             searchlang => 'o',
81             charset => 'o',
82             fuzzy => 'o',
83             north => 'o',
84             west => 'o',
85             east => 'o',
86             south => 'o',
87             orderby => 'o',
88             },
89             postalcode_search => {
90             postalcode => 'rc',
91             placename => 'rc',
92             country => 'o',
93             maxRows => 'o',
94             style => 'o',
95             username => 'r',
96             },
97             find_nearby_postalcodes => {
98             lat => 'r',
99             lng => 'r',
100             radius => 'o',
101             maxRows => 'o',
102             style => 'o',
103             country => 'o',
104             username => 'r',
105             },
106             postalcode_country_info => {
107             username => 'r',
108             },
109             find_nearby_placename => {
110             lat => 'r',
111             lng => 'r',
112             radius => 'o',
113             style => 'o',
114             maxRows => 'o',
115             lang => 'o',
116             cities => 'o',
117             username => 'r',
118             },
119             find_nearest_address => {
120             lat => 'r',
121             lng => 'r',
122             username => 'r',
123             },
124             find_nearest_intersection => {
125             lat => 'r',
126             lng => 'r',
127             username => 'r',
128             },
129             find_nearby_streets => {
130             lat => 'r',
131             lng => 'r',
132             username => 'r',
133             },
134             find_nearby_wikipedia => {
135             lang => 'o',
136             lat => 'r',
137             lng => 'r',
138             radius => 'o',
139             maxRows => 'o',
140             country => 'o',
141             username => 'r',
142             },
143             find_nearby_wikipedia_by_postalcode => {
144             postalcode => 'r',
145             country => 'r',
146             radius => 'o',
147             maxRows => 'o',
148             username => 'r',
149             },
150             wikipedia_search => {
151             'q' => 'r',
152             lang => 'o',
153             title => 'o',
154             maxRows => 'o',
155             username => 'r',
156             },
157             wikipedia_bounding_box => {
158             south => 'r',
159             north => 'r',
160             east => 'r',
161             west => 'r',
162             lang => 'o',
163             maxRows => 'o',
164             username => 'r',
165             },
166             country_info => {
167             country => 'o',
168             lang => 'o',
169             username => 'r',
170             },
171             country_code => {
172             lat => 'r',
173             lng => 'r',
174             lang => 'o',
175             radius => 'o',
176             username => 'r',
177             },
178             find_nearby_weather => {
179             lat => 'r',
180             lng => 'r',
181             username => 'r',
182             },
183             cities => {
184             north => 'r',
185             south => 'r',
186             east => 'r',
187             west => 'r',
188             lang => 'o',
189             maxRows => 'o',
190             username => 'r',
191             },
192             earthquakes => {
193             north => 'r',
194             south => 'r',
195             east => 'r',
196             west => 'r',
197             date => 'o',
198             minMagnutide => 'o',
199             maxRows => 'o',
200             username => 'r',
201             },
202             get => {
203             geonameId => 'r',
204             lang => 'o',
205             style => 'o',
206             username => 'r',
207             },
208             hierarchy => {
209             geonameId => 'r',
210             username => 'r',
211             style => 'o',
212             },
213             children => {
214             geonameId => 'r',
215             username => 'r',
216             style => 'o',
217             },
218             );
219              
220             sub new {
221 9     9 1 8872 my( $class, %hash ) = @_;
222              
223 9         28 my $self = bless { _functions => \%searches }, $class;
224              
225 9 100       301 croak <<"HERE" unless length $hash{username};
226             You must specify a GeoNames username to use Geo::GeoNames.
227             See http://www.geonames.org/export/web-services.html
228             HERE
229              
230 7         20 $self->username( $hash{username} );
231 7   66     28 $self->url( $hash{url} // $self->default_url );
232              
233             croak 'Illegal ua object, needs either a Mojo::UserAgent or an LWP::UserAgent derived object'
234 7 100 100     404 if exists $hash{ua} && !(ref $hash{ua} && blessed($hash{ua}) && ( $hash{ua}->isa('Mojo::UserAgent') || $hash{ua}->isa('LWP::UserAgent') ) );
      100        
235 4   66     21 $self->ua($hash{ua} || $self->default_ua );
236              
237 4 50       12 (exists($hash{debug})) ? $DEBUG = $hash{debug} : 0;
238 4 50       10 (exists($hash{cache})) ? $CACHE = $hash{cache} : 0;
239 4         8 $self->{_functions} = \%searches;
240              
241 4         12 return $self;
242             }
243              
244             sub username {
245 8     8 1 15 my( $self, $username ) = @_;
246              
247 8 100       29 $self->{username} = $username if @_ == 2;
248              
249 8         57 $self->{username};
250             }
251              
252             =head2 ua
253              
254             Accessor method to get and set UserAgent object used internally. You
255             can call I for example, to get the proxy information from
256             environment variables:
257              
258             $geo_coder->ua()->env_proxy(1);
259              
260             You can also set your own User-Agent object:
261              
262             use LWP::UserAgent::Throttled;
263             $geo_coder->ua(LWP::UserAgent::Throttled->new());
264              
265             =cut
266              
267             sub ua {
268 4     4 1 6 my $self = shift;
269 4 50       11 if (@_) {
270 4         16 $self->{ua} = shift;
271             }
272 4         12 $self->{ua};
273             }
274              
275             sub default_ua {
276 3     3 1 23 my $ua = Mojo::UserAgent->new;
277 3     0   39 $ua->on( error => sub { carp "Can't get request" } );
  0         0  
278 3         39 $ua;
279             }
280 6     6 1 24 sub default_url { 'http://api.geonames.org' }
281              
282             sub url {
283 8     8 1 13 my( $self, $url ) = @_;
284              
285 8 100       21 $self->{url} = $url if @_ == 2;
286              
287 8         13 $self->{url};
288             }
289              
290             sub _build_request_url {
291 1     1   4 my( $self, $request, @args ) = @_;
292 1         10 my $hash = { @args, username => $self->username };
293 1         3 my $request_url = $self->url . '/' . $searches{$request};
294              
295             # check to see that mandatory arguments are present
296 1         2 my $conditional_mandatory_flag = 0;
297 1         1 my $conditional_mandatory_required = 0;
298 1         2 foreach my $arg (keys %{$valid_parameters{$request}}) {
  1         5  
299 31         38 my $flags = $valid_parameters{$request}->{$arg};
300 31 50 66     45 if($flags =~ /d/ && exists($hash->{$arg})) {
301 0         0 carp("Argument $arg is deprecated.");
302             }
303 31         35 $flags =~ s/d//g;
304 31 50 66     51 if($flags eq 'r' && !exists($hash->{$arg})) {
305 0         0 carp("Mandatory argument $arg is missing!");
306             }
307 31 50 100     74 if($flags !~ /m/ && exists($hash->{$arg}) && ref($hash->{$arg})) {
      66        
308 0         0 carp("Argument $arg cannot have multiple values.");
309             }
310 31 100       52 if($flags eq 'rc') {
311 3         4 $conditional_mandatory_required = 1;
312 3 100       6 if(exists($hash->{$arg})) {
313 1         1 $conditional_mandatory_flag++;
314             }
315             }
316             }
317              
318 1 50 33     6 if($conditional_mandatory_required == 1 && $conditional_mandatory_flag != 1) {
319 0         0 carp("Invalid number of mandatory arguments (there can be only one)");
320             }
321 1         8 foreach my $key (sort keys(%$hash)) {
322 2 50       5 carp("Invalid argument $key") if(!defined($valid_parameters{$request}->{$key}));
323 2 50       5 my @vals = ref($hash->{$key}) ? @{$hash->{$key}} : $hash->{$key};
  0         0  
324 5     5   66 no warnings 'uninitialized';
  5         13  
  5         4314  
325 2         3 $request_url .= join('', map { "$key=$_&" } sort @vals );
  2         8  
326             }
327              
328 1         2 chop($request_url); # lose the trailing &
329 1         3 return $request_url;
330             }
331              
332             sub _parse_xml_result {
333 3     3   1822 require XML::Simple;
334 3         15265 my( $self, $geonamesresponse, $single_result ) = @_;
335 3         6 my @result;
336 3         18 my $xmlsimple = XML::Simple->new;
337 3         167 my $xml = $xmlsimple->XMLin( $geonamesresponse, KeyAttr => [], ForceArray => 1 );
338              
339 3 100       118685 if ($xml->{'status'}) {
340 1         10 carp "GeoNames error: " . $xml->{'status'}->[0]->{message};
341 1         16 return [];
342             }
343              
344 2 100       22 $xml = { geoname => [ $xml ], totalResultsCount => '1' } if $single_result;
345              
346 2         3 my $i = 0;
347 2         3 foreach my $element (keys %{$xml}) {
  2         7  
348 5 100       13 next if (ref($xml->{$element}) ne "ARRAY");
349 3         3 foreach my $list (@{$xml->{$element}}) {
  3         7  
350 3 100       7 next if (ref($list) ne "HASH");
351 2         3 foreach my $attribute (%{$list}) {
  2         10  
352 96 100       198 next if !defined($list->{$attribute}->[0]);
353 48 100       46 $result[$i]->{$attribute} = (scalar @{$list->{$attribute}} == 1 ? $list->{$attribute}->[0] : $list->{$attribute});
  48         121  
354             }
355 2         5 $i++;
356             }
357             }
358 2         34 return \@result;
359             }
360              
361             sub _parse_json_result {
362 1     1   642 require JSON;
363 1         6939 my( $self, $geonamesresponse ) = @_;
364 1         2 my @result;
365 1         287 return JSON->new->utf8->decode($geonamesresponse);
366             }
367              
368             sub _parse_text_result {
369 0     0   0 my( $self, $geonamesresponse ) = @_;
370 0         0 my @result;
371 0         0 $result[0]->{Result} = $geonamesresponse;
372 0         0 return \@result;
373             }
374              
375             sub _request {
376 1     1   2 my( $self, $request_url ) = @_;
377              
378 1         3 my $res = $self->{ua}->get( $request_url );
379 1 50       119518 return $res->can('res') ? $res->res : $res;
380             }
381              
382             sub _do_search {
383 1     1   3 my( $self, $searchtype, @args ) = @_;
384              
385 1         7 my $request_url = $self->_build_request_url( $searchtype, @args );
386 1         2 my $response = $self->_request( $request_url );
387              
388             # check mime-type to determine which parse method to use.
389             # we accept text/xml, text/plain (how do see if it is JSON or not?)
390 1   50     9 my $mime_type = $response->headers->content_type || '';
391              
392 1         21 my $body = '';
393 1 50       16 if ($response->can('body')) {
394 1         7 $body = $response->body;
395             }
396             else {
397 0         0 $body = $response->content;
398             }
399              
400 1 50       28 if($mime_type =~ m(\Atext/xml;?) ) {
401 1         7 return $self->_parse_xml_result( $body, $searchtype eq 'get' );
402             }
403 0 0       0 if($mime_type =~ m(\Aapplication/json;?) ) {
404             # a JSON object always start with a left-brace {
405             # according to http://json.org/
406 0 0       0 if( $body =~ m/\A\{/ ) {
407 0 0       0 if ($response->can('json')) {
408 0         0 return $response->json;
409             }
410             else {
411 0         0 return $self->_parse_json_result( $body );
412             }
413             }
414             else {
415 0         0 return $self->_parse_text_result( $body );
416             }
417             }
418              
419 0 0       0 if($mime_type eq 'text/plain') {
420 0         0 carp 'Invalid mime type [text/plain]. ', $response->content();
421             } else {
422 0         0 carp "Invalid mime type [$mime_type]. Maybe you aren't connected.";
423             }
424              
425 0         0 return [];
426             }
427              
428             sub geocode {
429 0     0 1 0 my( $self, $q ) = @_;
430 0         0 $self->search( 'q' => $q );
431             }
432              
433             sub AUTOLOAD {
434 1     1   8 my $self = shift;
435 1   33     10 my $type = ref($self) || croak "$self is not an object";
436 1         2 my $name = our $AUTOLOAD;
437 1         6 $name =~ s/.*://;
438              
439 1 50       3 unless (exists $self->{_functions}->{$name}) {
440 0         0 croak "No such method '$AUTOLOAD'";
441             }
442              
443 1         4 return($self->_do_search($name, @_));
444             }
445              
446 9     9   3190 sub DESTROY { 1 }
447              
448             1;
449              
450             __END__