PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Geo/GeoNames.pm

http://geo-geonames.googlecode.com/
Perl | 737 lines | 713 code | 16 blank | 8 comment | 18 complexity | a8a49a3dad59318a56e44d09ef95f541 MD5 | raw file
  1. # $Id: GeoNames.pm 45 2008-03-27 14:52:56Z per.henrik.johansen@gmail.com $
  2. package Geo::GeoNames;
  3. use Data::Dumper;
  4. use 5.008006;
  5. use strict;
  6. use warnings;
  7. use Carp;
  8. use XML::Simple;
  9. use LWP;
  10. use JSON;
  11. use vars qw($VERSION $DEBUG $GNURL $CACHE %valid_parameters %searches);
  12. our $VERSION = '0.07svn';
  13. $GNURL = 'http://ws.geonames.org';
  14. %searches = (
  15. children => 'children?',
  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. );
  34. # r = required
  35. # o = optional
  36. # rc = required - only one of the fields marked with rc is allowed. At least one must be present
  37. # om = optional, multible entries allowed
  38. # d = depreciated - will be removed in later versions
  39. %valid_parameters = (
  40. search => {
  41. q => 'rc',
  42. name => 'rc',
  43. name_equals => 'rc,',
  44. maxRows => 'o',
  45. startRow => 'o',
  46. country => 'om',
  47. continentCode => 'o',
  48. adminCode1 => 'o',
  49. adminCode2 => 'o',
  50. adminCode3 => 'o',
  51. fclass => 'omd',
  52. featureClass => 'om',
  53. featureCode => 'om',
  54. lang => 'o',
  55. type => 'o',
  56. style => 'o',
  57. isNameRequired => 'o',
  58. tag => 'o'
  59. },
  60. postalcode_search => {
  61. postalcode => 'rc',
  62. placename => 'rc',
  63. country => 'o',
  64. maxRows => 'o',
  65. style => 'o'
  66. },
  67. find_nearby_postalcodes => {
  68. lat => 'r',
  69. lng => 'r',
  70. radius => 'o',
  71. maxRows => 'o',
  72. style => 'o',
  73. country => 'o',
  74. },
  75. postalcode_country_info => {
  76. },
  77. find_nearby_placename => {
  78. lat => 'r',
  79. lng => 'r',
  80. radius => 'o',
  81. style => 'o',
  82. maxRows => 'o'
  83. },
  84. find_nearest_address => {
  85. lat => 'r',
  86. lng => 'r'
  87. },
  88. find_nearest_intersection => {
  89. lat => 'r',
  90. lng => 'r'
  91. },
  92. find_nearby_streets => {
  93. lat => 'r',
  94. lng => 'r'
  95. },
  96. find_nearby_wikipedia => {
  97. lang => 'o',
  98. lat => 'r',
  99. lng => 'r',
  100. radius => 'o',
  101. maxRows => 'o',
  102. country => 'o'
  103. },
  104. find_nearby_wikipedia_by_postalcode => {
  105. postalcode => 'r',
  106. country => 'r',
  107. radius => 'o',
  108. maxRows => 'o'
  109. },
  110. wikipedia_search => {
  111. q => 'r',
  112. lang => 'o',
  113. title => 'o',
  114. maxRows => 'o'
  115. },
  116. wikipedia_bounding_box => {
  117. south => 'r',
  118. north => 'r',
  119. east => 'r',
  120. west => 'r',
  121. lang => 'o',
  122. maxRows => 'o'
  123. },
  124. country_info => {
  125. country => 'o',
  126. lang => 'o'
  127. },
  128. country_code => {
  129. lat => 'r',
  130. lng => 'r',
  131. lang => 'o',
  132. radius => 'o'
  133. },
  134. find_nearby_weather => {
  135. lat => 'r',
  136. lng => 'r'
  137. },
  138. cities => {
  139. north => 'r',
  140. south => 'r',
  141. east => 'r',
  142. west => 'r',
  143. lang => 'o',
  144. maxRows => 'o'
  145. },
  146. earthquakes => {
  147. north => 'r',
  148. south => 'r',
  149. east => 'r',
  150. west => 'r',
  151. date => 'o',
  152. minMagnutide => 'o',
  153. maxRows => 'o'
  154. },
  155. children => {
  156. geonameId => 'r',
  157. style => 'o'
  158. }
  159. );
  160. sub new {
  161. my $class = shift;
  162. my $self = shift;
  163. my %hash = @_;
  164. (exists($hash{url})) ? $self->{url} = $hash{url} : $self->{url} = $GNURL;
  165. (exists($hash{debug})) ? $DEBUG = $hash{debug} : 0;
  166. (exists($hash{cache})) ? $CACHE = $hash{cache} : 0;
  167. $self->{_functions} = \%searches;
  168. bless $self, $class;
  169. return $self;
  170. }
  171. sub _build_request {
  172. my $self = shift;
  173. my $request = shift;
  174. my $hash = {@_};
  175. my $request_string = $GNURL . '/' . $searches{$request};
  176. # check to see that mandatory arguments are present
  177. my $conditional_mandatory_flag = 0;
  178. my $conditional_mandatory_required = 0;
  179. foreach my $arg (keys %{$valid_parameters{$request}}) {
  180. my $flags = $valid_parameters{$request}->{$arg};
  181. if($flags =~ /d/ && exists($hash->{$arg})) {
  182. carp("Argument $arg is depreciated.");
  183. }
  184. $flags =~ s/d//g;
  185. if($flags eq 'r' && !exists($hash->{$arg})) {
  186. carp("Mandatory argument $arg is missing!");
  187. }
  188. if($flags eq 'rc') {
  189. $conditional_mandatory_required = 1;
  190. if(exists($hash->{$arg})) {
  191. $conditional_mandatory_flag++;
  192. }
  193. }
  194. }
  195. if($conditional_mandatory_required == 1 && $conditional_mandatory_flag != 1) {
  196. carp("Invalid number of mandatory arguments (there can be only one)");
  197. }
  198. foreach my $key (keys(%$hash)) {
  199. carp("Invalid argument $key") if(!defined($valid_parameters{$request}->{$key}));
  200. $request_string .= $key . '=' . $hash->{$key} . '&';
  201. }
  202. chop($request_string); # loose the trailing &
  203. return $request_string;
  204. }
  205. sub _parse_xml_result {
  206. my $self = shift;
  207. my $geonamesresponse = shift;
  208. my @result;
  209. my $xmlsimple = XML::Simple->new();
  210. my $xml = $xmlsimple->XMLin($geonamesresponse, KeyAttr=>[], ForceArray => 1);
  211. my $i = 0;
  212. foreach my $element (keys %{$xml}) {
  213. if ($element eq 'status') {
  214. carp "ERROR: " . $xml->{$element}->[0]->{message};
  215. }
  216. next if (ref($xml->{$element}) ne "ARRAY");
  217. foreach my $list (@{$xml->{$element}}) {
  218. next if (ref($list) ne "HASH");
  219. foreach my $attribute (%{$list}) {
  220. next if !defined($list->{$attribute}->[0]);
  221. $result[$i]->{$attribute} = $list->{$attribute}->[0];
  222. }
  223. $i++;
  224. }
  225. }
  226. return \@result;
  227. }
  228. sub _parse_json_result {
  229. my $self = shift;
  230. my $geonamesresponse = shift;
  231. my @result;
  232. my $json = new JSON;
  233. my $data = $json->decode($geonamesresponse);
  234. #print STDERR Data::Dumper->Dump([$data]);
  235. my $i = 0;
  236. foreach my $hash (keys %{$data}) {
  237. if(ref($data->{$hash}) eq 'ARRAY') { # we have a list of objects
  238. foreach my $object (@{$data->{$hash}}) { # $object is a hash ref
  239. next if(ref($object) ne 'HASH');
  240. foreach my $attribute (keys %{$object}) {
  241. $result[$i]->{$attribute} = $object->{$attribute};
  242. }
  243. $i++;
  244. }
  245. } else { #we have only one
  246. my $attributes = $data->{$hash};
  247. foreach my $attribute (keys %{$attributes}) {
  248. $result[$i]->{$attribute} = $attributes->{$attribute};
  249. }
  250. $i++;
  251. }
  252. }
  253. return \@result;
  254. }
  255. sub _parse_text_result {
  256. my $self = shift;
  257. my $geonamesresponse = shift;
  258. my @result;
  259. $result[0]->{Result} = $geonamesresponse;
  260. return \@result;
  261. }
  262. sub _request {
  263. my $self = shift;
  264. my $request = shift;
  265. my $browser = LWP::UserAgent->new;
  266. $browser->env_proxy();
  267. my $response = $browser->get($request);
  268. carp "Can't get $request -- ", $response->status_line unless $response->is_success;
  269. return ($response->content, $response->header('Content-Type'));
  270. }
  271. sub _do_search {
  272. my $self = shift;
  273. my $searchtype = shift;
  274. my $request = $self->_build_request($searchtype, @_);
  275. my ($result, $mimetype) = $self->_request($request);
  276. # check mime-type to determine which parse method to use.
  277. # we accept text/xml, text/plain (how do see if it is JSON or not?)
  278. if($mimetype =~ /^text\/xml;/) {
  279. return($self->_parse_xml_result($result));
  280. }
  281. if($mimetype =~ /^application\/json;/) {
  282. # a JSON object always start with a left-brace {
  283. # according to http://json.org/
  284. if($result =~ /^\{/) {
  285. return($self->_parse_json_result($result));
  286. } else {
  287. return($self->_parse_text_result($result));
  288. }
  289. }
  290. carp "Invalid mime type";
  291. return undef;
  292. }
  293. sub geocode {
  294. my $self = shift;
  295. my $q = shift;
  296. return($self->search(q=> $q));
  297. }
  298. sub AUTOLOAD {
  299. my $self = shift;
  300. my $type = ref($self) || croak "$self is not an object";
  301. my $name = our $AUTOLOAD;
  302. $name =~ s/.*://;
  303. unless (exists $self->{_functions}->{$name}) {
  304. croak "No such method '$AUTOLOAD'";
  305. }
  306. return($self->_do_search($name, @_));
  307. }
  308. sub DESTROY {
  309. }
  310. 1;
  311. __END__
  312. =head1 NAME
  313. Geo::GeoNames - Perform geographical queries using GeoNames Web Services
  314. =head1 SYNOPSIS
  315. use Geo::GeoNames;
  316. use Data::Dumper;
  317. my $geo = new Geo::GeoNames();
  318. # make a query based on placename
  319. my $result = $geo->search(q => 'Fredrikstad', maxRows => 2);
  320. # print the first result
  321. print " Name: " . $result->[0]->{name};
  322. print " Longitude: " . $result->[0]->{lng};
  323. print " Lattitude: " . $result->[0]->{lat};
  324. # Dump the data structure into readable form
  325. # This also will show the attributes to each found location
  326. Data::Dumper->Dump()
  327. # Make a query based on postcode
  328. $result = $geo->postalcode_search(postalcode => "1630", maxRows => 3, style => "FULL");
  329. =head1 DESCRIPTION
  330. Provides a perl interface to the webservices found at
  331. http://ws.geonames.org. That is, given a given placename or
  332. postalcode, the module will look it up and return more information
  333. (longitude, lattitude, etc) for the given placename or postalcode.
  334. Wikipedia lookups are also supported.
  335. If more than one match is found, a list of locations will be returned.
  336. =head1 METHODS
  337. =over 4
  338. =item new
  339. $geo = Geo::GeoNames->new()
  340. $geo = Geo::GeoNames->new(url => $url)
  341. Constructor for Geo::GeoNames. It returns a reference to an Geo::GeoNames object.
  342. You may also pass the url of the webservices to use. The default value is
  343. http://ws.geonames.org and is the only url, to my knowledge, that provides
  344. the services needed by this module.
  345. =item geocode($placename)
  346. This function is just an easy access to search. It is the same as saying:
  347. $geo->search(q => $placename);
  348. =item search(arg => $arg)
  349. Searches for information about a placename. Valid names for B<arg> are as follows:
  350. q => $placename
  351. name => $placename
  352. name_equals => $placename
  353. maxRows => $maxrows
  354. startRow => $startrow
  355. country => $countrycode
  356. continentCode => $continentcode
  357. adminCode1 => $admin1
  358. adminCode2 => $admin2
  359. adminCode3 => $admin3
  360. fclass => $fclass
  361. featureClass => $fclass,
  362. featureCode => $code
  363. lang => $lang
  364. type => $type
  365. style => $style
  366. isNameRequired => $isnamerequired
  367. tag => $tag
  368. One, and only one, of B<q>, B<name>, or B<name_equals> must be supplied to
  369. this function.
  370. fclass is depreciated.
  371. For a thorough description of the arguments, see
  372. http://www.geonames.org/export/geonames-search.html
  373. =item find_nearby_placename(arg => $arg)
  374. Reverse lookup for closest placename to a given coordinate. Valid names for
  375. B<arg> are as follows:
  376. lat => $lat
  377. lng => $lng
  378. radius => $radius
  379. style => $style
  380. maxRows => $maxrows
  381. Both B<lat> and B<lng> must be supplied to
  382. this function.
  383. For a thorough descriptions of the arguments, see
  384. http://www.geonames.org/export
  385. =item find_nearest_address(arg => $arg)
  386. Reverse lookup for closest address to a given coordinate. Valid names for
  387. B<arg> are as follows:
  388. lat => $lat
  389. lng => $lng
  390. Both B<lat> and B<lng> must be supplied to
  391. this function.
  392. For a thorough descriptions of the arguments, see
  393. http://www.geonames.org/maps/reverse-geocoder.html
  394. US only.
  395. =item find_nearest_intersection(arg => $arg)
  396. Reverse lookup for closest intersection to a given coordinate. Valid names for
  397. B<arg> are as follows:
  398. lat => $lat
  399. lng => $lng
  400. Both B<lat> and B<lng> must be supplied to
  401. this function.
  402. For a thorough descriptions of the arguments, see
  403. http://www.geonames.org/maps/reverse-geocoder.html
  404. US only.
  405. =item find_nearby_streets(arg => $arg)
  406. Reverse lookup for closest streets to a given coordinate. Valid names for
  407. B<arg> are as follows:
  408. lat => $lat
  409. lng => $lng
  410. Both B<lat> and B<lng> must be supplied to
  411. this function.
  412. For a thorough descriptions of the arguments, see
  413. http://www.geonames.org/maps/reverse-geocoder.html
  414. US only.
  415. =item postalcode_search(arg => $arg)
  416. Searches for information about a postalcode. Valid names for B<arg> are as follows:
  417. postalcode => $postalcode
  418. placename => $placename
  419. country => $country
  420. maxRows => $maxrows
  421. style => $style
  422. One, and only one, of B<postalcode> or B<placename> must be supplied to
  423. this function.
  424. For a thorough description of the arguments, see
  425. http://www.geonames.org/export
  426. =item find_nearby_postalcodes(arg => $arg)
  427. Reverse lookup for postalcodes. Valid names for B<arg> are as follows:
  428. lat => $lat
  429. lng => $lng
  430. radius => $radius
  431. maxRows => $maxrows
  432. style => $style
  433. country => $country
  434. Both B<lat> and B<lng> must be supplied to
  435. this function.
  436. For a thorough description of the arguments, see
  437. http://www.geonames.org/export
  438. =item postalcode_country_info
  439. Returns a list of all postalcodes found on GeoNames. This function
  440. takes no arguments.
  441. =item country_info(arg => $arg)
  442. Returns country information. Valid names for B<arg> are as follows:
  443. country => $country
  444. lang => $lang
  445. For a thorough description of the arguments, see
  446. http://www.geonames.org/export
  447. =item find_nearby_wikipedia(arg => $arg)
  448. Reverse lookup for Wikipedia articles. Valid names for B<arg> are as follows:
  449. lat => $lat
  450. lng => $lng
  451. radius => $radius
  452. maxRows => $maxrows
  453. lang => $lang
  454. country => $country
  455. Both B<lat> and B<lng> must be supplied to
  456. this function.
  457. For a thorough description of the arguments, see
  458. http://www.geonames.org/export
  459. =item find_nearby_wikipediaby_postalcode(arg => $arg)
  460. Reverse lookup for Wikipedia articles. Valid names for B<arg> are as follows:
  461. postalcode => $postalcode
  462. country => $country
  463. radius => $radius
  464. maxRows => $maxrows
  465. Both B<postalcode> and B<country> must be supplied to
  466. this function.
  467. For a thorough description of the arguments, see
  468. http://www.geonames.org/export
  469. =item wikipedia_search(arg => $arg)
  470. Searches for Wikipedia articles. Valid names for B<arg> are as follows:
  471. q => $placename
  472. maxRows => $maxrows
  473. lang => $lang
  474. title => $title
  475. B<q> must be supplied to
  476. this function.
  477. For a thorough description of the arguments, see
  478. http://www.geonames.org/export
  479. =item wikipedia_bounding_box(arg => $arg)
  480. Searches for Wikipedia articles. Valid names for B<arg> are as follows:
  481. south => $south
  482. north => $north
  483. east => $east
  484. west => $west
  485. lang => $lang
  486. maxRows => $maxrows
  487. B<south>, B<north>, B<east>, and B<west> and must be supplied to
  488. this function.
  489. For a thorough description of the arguments, see
  490. http://www.geonames.org/export
  491. =item cities(arg => $arg)
  492. Returns a list of cities and placenames within the bounding box.
  493. Valid names for B<arg> are as follows:
  494. south => $south
  495. north => $north
  496. east => $east
  497. west => $west
  498. lang => $lang
  499. maxRows => $maxrows
  500. B<south>, B<north>, B<east>, and B<west> and must be supplied to
  501. this function.
  502. For a thorough description of the arguments, see
  503. http://www.geonames.org/export
  504. =item country_code(arg => $arg)
  505. Return the country code for a given point. Valid names for B<arg> are as follows:
  506. lat => $lat
  507. lng => $lng
  508. radius => $radius
  509. lang => $lang
  510. Both B<lat> and B<lng> must be supplied to
  511. this function.
  512. For a thorough description of the arguments, see
  513. http://www.geonames.org/export
  514. =item earthquakes(arg => $arg)
  515. Returns a list of cities and placenames within the bounding box.
  516. Valid names for B<arg> are as follows:
  517. south => $south
  518. north => $north
  519. east => $east
  520. west => $west
  521. date => $date
  522. minMagnitude => $minmagnitude
  523. maxRows => $maxrows
  524. B<south>, B<north>, B<east>, and B<west> and must be supplied to
  525. this function.
  526. For a thorough description of the arguments, see
  527. http://www.geonames.org/export
  528. =item find_nearby_weather(arg => $arg)
  529. Return the country code for a given point. Valid names for B<arg> are as follows:
  530. lat => $lat
  531. lng => $lng
  532. Both B<lat> and B<lng> must be supplied to
  533. this function.
  534. For a thorough description of the arguments, see
  535. http://www.geonames.org/export
  536. =back
  537. =head1 RETURNED DATASTRUCTURE
  538. The datastructure returned from methods in this module is an array of
  539. hashes. Each array element contains a hash which in turn contains the information
  540. about the placename/postalcode.
  541. For example, running the statement
  542. my $result = $geo->search(q => "Fredrikstad", maxRows => 3, style => "FULL");
  543. yields the result (after doing a Data::Dumper->Dump($result);):
  544. $VAR1 = {
  545. 'population' => {},
  546. 'lat' => '59.2166667',
  547. 'elevation' => {},
  548. 'countryCode' => 'NO',
  549. 'adminName1' => "\x{d8}stfold",
  550. 'fclName' => 'city, village,...',
  551. 'adminCode2' => {},
  552. 'lng' => '10.95',
  553. 'geonameId' => '3156529',
  554. 'timezone' => {
  555. 'dstOffset' => '2.0',
  556. 'content' => 'Europe/Oslo',
  557. 'gmtOffset' => '1.0'
  558. },
  559. 'fcode' => 'PPL',
  560. 'countryName' => 'Norway',
  561. 'name' => 'Fredrikstad',
  562. 'fcodeName' => 'populated place',
  563. 'alternateNames' => 'Frederikstad,Fredrikstad,Fredrikstad kommun',
  564. 'adminCode1' => '13',
  565. 'adminName2' => {},
  566. 'fcl' => 'P'
  567. };
  568. The elements in the hashes depends on which B<style> is passed to the method, but
  569. will always contain B<name>, B<lng>, and B<lat> except for postalcode_country_info(),
  570. find_nearest_address(), find_nearest_intersection(), and find_nearby_streets().
  571. =head1 BUGS
  572. Not a bug, but the GeoNames services expects placenames to be
  573. UTF-8 encoded, and all data recieved from the webservices are
  574. also UTF-8 encoded. So make sure that strings are encoded/decoded
  575. based on the correct encoding.
  576. Please report any bugs found or feature requests to
  577. http://code.google.com/p/geo-geonames/issues/list
  578. =head1 SEE ALSO
  579. http://www.geonames.org/export
  580. http://www.geonames.org/export/ws-overview.html
  581. =head1 SOURCE AVAILABILITY
  582. The source code for this module is available from SVN
  583. at http://code.google.com/p/geo-geonames
  584. =head1 AUTHOR
  585. Per Henrik Johansen, E<lt>per.henrik.johansen@gmail.comE<gt>
  586. =head1 COPYRIGHT AND LICENSE
  587. Copyright (C) 2007-2008 by Per Henrik Johansen
  588. This library is free software; you can redistribute it and/or modify
  589. it under the same terms as Perl itself, either Perl version 5.8.8 or,
  590. at your option, any later version of Perl 5 you may have available.
  591. =cut