File Coverage

blib/lib/Geo/Coder/Postcodes.pm
Criterion Covered Total %
statement 33 108 30.5
branch 2 38 5.2
condition 2 11 18.1
subroutine 10 13 76.9
pod 4 4 100.0
total 51 174 29.3


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