File Coverage

blib/lib/Geo/Coder/Postcodes.pm
Criterion Covered Total %
statement 31 106 29.2
branch 0 36 0.0
condition 2 11 18.1
subroutine 10 13 76.9
pod 4 4 100.0
total 47 170 27.6


line stmt bran cond sub pod time code
1             package Geo::Coder::Postcodes;
2              
3 4     4   479330 use strict;
  4         33  
  4         120  
4 4     4   21 use warnings;
  4         7  
  4         104  
5              
6 4     4   20 use Carp;
  4         7  
  4         226  
7 4     4   2216 use Encode;
  4         38653  
  4         288  
8 4     4   3046 use JSON;
  4         41300  
  4         24  
9 4     4   2352 use HTTP::Request;
  4         81592  
  4         139  
10 4     4   2770 use LWP::UserAgent;
  4         102647  
  4         151  
11 4     4   1892 use LWP::Protocol::https;
  4         393032  
  4         197  
12 4     4   40 use URI;
  4         9  
  4         4742  
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.06
21              
22             =cut
23              
24             our $VERSION = '0.06';
25              
26             =head1 SYNOPSIS
27              
28             use Geo::Coder::Postcodes;
29              
30             my $geo_coder = Geo::Coder::Postcodes->new();
31             my $location = $geo_coder->geocode(location => 'Margate');
32              
33             =head1 DESCRIPTION
34              
35             Geo::Coder::Postcodes provides an interface to postcodes.io,
36             a free Geo-Coder database covering the towns in the UK.
37              
38             =head1 METHODS
39              
40             =head2 new
41              
42             $geo_coder = Geo::Coder::Postcodes->new();
43             my $ua = LWP::UserAgent->new();
44             $ua->env_proxy(1);
45             $geo_coder = Geo::Coder::Postcodes->new(ua => $ua);
46              
47             =cut
48              
49             sub new {
50 1     1 1 102 my($class, %param) = @_;
51              
52 1   33     16 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     2939 my $host = delete $param{host} || 'api.postcodes.io';
57              
58 1         11 return bless { ua => $ua, host => $host }, $class;
59             }
60              
61             =head2 geocode
62              
63             $location = $geo_coder->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             $geo_coder->ua()->env_proxy(1);
156              
157             You can also set your own User-Agent object:
158              
159             use LWP::UserAgent::Throttled;
160             $geo_coder->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 = $geo_coder->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 0         if($rc->{'result'}) {
216 0           my @results = @{$rc->{'result'}};
  0            
217 0           return $results[0];
218             }
219 0           return;
220             }
221              
222             =head1 BUGS
223              
224             Note that this most only works on towns and cities, some searches such as "Margate, Kent, UK"
225             may work, but you're best to search only for "Margate".
226              
227             =head1 AUTHOR
228              
229             Nigel Horne
230              
231             Based on L.
232              
233             This library is free software; you can redistribute it and/or modify
234             it under the same terms as Perl itself.
235              
236             Lots of thanks to the folks at postcodes.io.
237              
238             =head1 SEE ALSO
239              
240             L, L
241              
242             =head1 LICENSE AND COPYRIGHT
243              
244             Copyright 2017-2019 Nigel Horne.
245              
246             This program is released under the following licence: GPL2
247              
248             =cut
249              
250             1;