File Coverage

blib/lib/Geo/GeoNames.pm
Criterion Covered Total %
statement 120 144 83.3
branch 37 58 63.7
condition 22 32 68.7
subroutine 22 25 88.0
pod 7 7 100.0
total 208 266 78.2


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