File Coverage

blib/lib/Geo/GeoNames.pm
Criterion Covered Total %
statement 120 142 84.5
branch 37 56 66.0
condition 20 32 62.5
subroutine 22 25 88.0
pod 7 7 100.0
total 206 262 78.6


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