File Coverage

blib/lib/Geo/Coder/Postcodes.pm
Criterion Covered Total %
statement 31 104 29.8
branch 0 34 0.0
condition 2 11 18.1
subroutine 10 13 76.9
pod 4 4 100.0
total 47 166 28.3


line stmt bran cond sub pod time code
1             package Geo::Coder::Postcodes;
2              
3 4     4   346027 use strict;
  4         36  
  4         131  
4 4     4   18 use warnings;
  4         6  
  4         79  
5              
6 4     4   15 use Carp;
  4         5  
  4         170  
7 4     4   1635 use Encode;
  4         27786  
  4         213  
8 4     4   1796 use JSON;
  4         28352  
  4         20  
9 4     4   1665 use HTTP::Request;
  4         58691  
  4         139  
10 4     4   1890 use LWP::UserAgent;
  4         73596  
  4         131  
11 4     4   1553 use LWP::Protocol::https;
  4         283714  
  4         160  
12 4     4   29 use URI;
  4         6  
  4         3029  
13              
14             =head1 NAME
15              
16             Geo::Coder::Postcodes - Provides a geocoding functionality using https://postcodes.io.
17              
18             =head1 VERSION
19              
20             Version 0.05
21              
22             =cut
23              
24             our $VERSION = '0.05';
25              
26             =head1 SYNOPSIS
27              
28             use Geo::Coder::Postcodes;
29              
30             my $geocoder = Geo::Coder::Postcodes->new();
31             my $location = $geocoder->geocode(location => 'Margate');
32              
33             =head1 DESCRIPTION
34              
35             Geo::Coder::Postcodes provides an interface to postcodes.io,
36             a free geocode database covering the towns in the UK.
37              
38             =head1 METHODS
39              
40             =head2 new
41              
42             $geocoder = Geo::Coder::Postcodes->new();
43             my $ua = LWP::UserAgent->new();
44             $ua->env_proxy(1);
45             $geocoder = Geo::Coder::Postcodes->new(ua => $ua);
46              
47             =cut
48              
49             sub new {
50 1     1 1 75 my($class, %param) = @_;
51              
52 1   33     15 my $ua = delete $param{ua} || LWP::UserAgent->new(agent => __PACKAGE__ . "/$VERSION");
53             # if(!defined($param{'host'})) {
54             # $ua->ssl_opts(verify_hostname => 0); # Yuck
55             # }
56 1   50     2130 my $host = delete $param{host} || 'api.postcodes.io';
57              
58 1         9 return bless { ua => $ua, host => $host }, $class;
59             }
60              
61             =head2 geocode
62              
63             $location = $geocoder->geocode(location => $location);
64              
65             print 'Latitude: ', $location->{'latitude'}, "\n";
66             print 'Longitude: ', $location->{'logitude'}, "\n";
67              
68             =cut
69              
70             sub geocode {
71 0     0 1   my $self = shift;
72              
73 0 0         scalar(@_) > 0 or
74             Carp::croak('Usage: geocode(location => $location)');
75              
76 0           my %param;
77 0 0         if (@_ % 2 == 0) {
78 0           %param = @_;
79             } else {
80 0           $param{location} = shift;
81             }
82              
83 0           my $location = $param{location};
84 0 0         unless(defined($location)) {
85 0           Carp::croak('Usage: geocode(location => $location)');
86 0           return;
87             }
88              
89 0           my $county;
90 0 0         if($location =~ /,/) {
91 0 0         if($location =~ /^([\w\s\-]+?),([\w\s]+?),[\w\s]+?$/i) {
92             # Turn 'Ramsgate, Kent, UK' into 'Ramsgate'
93 0           $location = $1;
94 0           $county = $2;
95 0           $county =~ s/^\s//g;
96 0           $county =~ s/\s$//g;
97             } else {
98 0           Carp::croak('Postcodes.io only supports towns, not full addresses');
99 0           return;
100             }
101             }
102 0           $location =~ s/\s/+/g;
103              
104 0 0         if(Encode::is_utf8($location)) {
105 0           $location = Encode::encode_utf8($location);
106             }
107              
108 0           my $uri = URI->new("https://$self->{host}/places/");
109 0           my %query_parameters = ('q' => $location);
110 0           $uri->query_form(%query_parameters);
111 0           my $url = $uri->as_string();
112 0           $url =~ s/%2B/+/g;
113              
114 0           my $res = $self->{ua}->get($url);
115              
116 0 0         if($res->is_error) {
117 0           Carp::croak("postcodes.io API returned error: on $url " . $res->status_line());
118 0           return;
119             }
120              
121 0           my $json = JSON->new()->utf8();
122              
123             # TODO: wantarray
124 0           my $rc = $json->decode($res->content);
125 0           my @results = @{$rc->{result}};
  0            
126 0 0         if($county) {
127             # TODO: search through all results for the right one, e.g. Leeds in
128             # Kent or in West Yorkshire?
129 0           foreach my $result(@results) {
130             # if(defined($result->{'county_unitary'}) && ($result->{'county_unitary_type'} eq 'County')) {
131 0 0         if(my $unitary = $result->{'county_unitary'}) {
132             # $location =~ s/+/ /g;
133 0 0 0       if(($unitary =~ /$county/i) || ($unitary =~ /$location/i)) {
134 0           return $result;
135             }
136             }
137 0 0 0       if((my $region = $result->{'region'}) && ($county =~ /\s+(\w+)$/)) {
138 0 0         if($region =~ /$1/) {
139             # e.g. looked for South Yorkshire, got Yorkshire and the Humber
140 0           return $result;
141             }
142             }
143             }
144 0           return;
145             }
146 0           return $results[0];
147             }
148              
149             =head2 ua
150              
151             Accessor method to get and set UserAgent object used internally. You
152             can call I for example, to get the proxy information from
153             environment variables:
154              
155             $geocoder->ua()->env_proxy(1);
156              
157             You can also set your own User-Agent object:
158              
159             use LWP::UserAgent::Throttled;
160             $geocoder->ua(LWP::UserAgent::Throttled->new());
161              
162             =cut
163              
164             sub ua {
165 0     0 1   my $self = shift;
166 0 0         if (@_) {
167 0           $self->{ua} = shift;
168             }
169 0           $self->{ua};
170             }
171              
172             =head2 reverse_geocode
173              
174             $location = $geocoder->reverse_geocode(latlng => '37.778907,-122.39732');
175              
176             Similar to geocode except it expects a latitude/longitude parameter.
177              
178             =cut
179              
180             sub reverse_geocode {
181 0     0 1   my $self = shift;
182              
183 0 0         scalar(@_) > 0 or
184             Carp::croak('Usage: reverse_geocode(latlng => $latlng)');
185              
186 0           my %param;
187 0 0         if (@_ % 2 == 0) {
188 0           %param = @_;
189             } else {
190 0           $param{latlng} = shift;
191             }
192              
193 0           my $latlng = $param{latlng};
194 0 0         unless(defined($latlng)) {
195 0           Carp::croak('Usage: reverse_geocode(latlng => $latlng)');
196 0           return;
197             }
198              
199 0           my $uri = URI->new("https://$self->{host}/postcodes/");
200 0           my ($lat, $lon) = split(/,/, $param{latlng});
201 0           my %query_parameters = ('lat' => $lat, 'lon' => $lon, radius => '1000');
202 0           $uri->query_form(%query_parameters);
203 0           my $url = $uri->as_string;
204              
205 0           my $res = $self->{ua}->get($url);
206              
207 0 0         if ($res->is_error) {
208 0           Carp::croak("postcodes.io API returned error: on $url " . $res->status_line());
209 0           return;
210             }
211              
212 0           my $json = JSON->new->utf8;
213              
214 0           my $rc = $json->decode($res->content);
215 0           my @results = @{$rc->{result}};
  0            
216 0           return $results[0];
217             }
218              
219             =head1 BUGS
220              
221             Note that this most only works on towns and cities, some searches such as "Margate, Kent, UK"
222             may work, but you're best to search only for "Margate".
223              
224             =head1 AUTHOR
225              
226             Nigel Horne
227              
228             Based on L.
229              
230             This library is free software; you can redistribute it and/or modify
231             it under the same terms as Perl itself.
232              
233             Lots of thanks to the folks at postcodes.io.
234              
235             =head1 SEE ALSO
236              
237             L, L
238              
239             =head1 LICENSE AND COPYRIGHT
240              
241             Copyright 2017-2018 Nigel Horne.
242              
243             This program is released under the following licence: GPL2
244              
245             =cut
246              
247             1;