File Coverage

blib/lib/Geo/Coder/Postcodes.pm
Criterion Covered Total %
statement 36 112 32.1
branch 5 42 11.9
condition 2 11 18.1
subroutine 10 13 76.9
pod 4 4 100.0
total 57 182 31.3


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