File Coverage

lib/Geo/Coder/Free.pm
Criterion Covered Total %
statement 143 169 84.6
branch 62 84 73.8
condition 13 18 72.2
subroutine 11 12 91.6
pod 3 3 100.0
total 232 286 81.1


line stmt bran cond sub pod time code
1             package Geo::Coder::Free;
2              
3 3     3   263137 use strict;
  3         20  
  3         70  
4 3     3   13 use warnings;
  3         3  
  3         65  
5              
6 3     3   673 use Geo::Coder::Free::DB::admin1;
  3         8  
  3         80  
7 3     3   811 use Geo::Coder::Free::DB::admin2;
  3         7  
  3         70  
8 3     3   654 use Geo::Coder::Free::DB::cities;
  3         6  
  3         63  
9 3     3   793 use Module::Info;
  3         13834  
  3         73  
10 3     3   18 use Carp;
  3         5  
  3         154  
11 3     3   754 use Error::Simple;
  3         9189  
  3         18  
12 3     3   125 use File::Spec;
  3         6  
  3         4042  
13              
14             =head1 NAME
15              
16             Geo::Coder::Free - Provides a geocoding functionality using free databases of towns
17              
18             =head1 VERSION
19              
20             Version 0.01
21              
22             =cut
23              
24             our $VERSION = '0.01';
25              
26             =head1 SYNOPSIS
27              
28             use Geo::Coder::Free;
29              
30             my $geocoder = Geo::Coder::Free->new();
31             my $location = $geocoder->geocode(location => 'Ramsgate, Kent, UK');
32              
33             =head1 DESCRIPTION
34              
35             Geo::Coder::Free provides an interface to free databases.
36              
37             =head1 METHODS
38              
39             =head2 new
40              
41             $geocoder = Geo::Coder::Free->new();
42              
43             =cut
44              
45             sub new {
46 3     3 1 703 my($proto, %param) = @_;
47 3   66     19 my $class = ref($proto) || $proto;
48              
49             # Geo::Coder::Free->new not Geo::Coder::Free::new
50 3 100       12 return unless($class);
51              
52             # Geo::Coder::Free::DB::init(directory => 'lib/Geo/Coder/Free/databases');
53              
54 2         15 my $directory = Module::Info->new_from_loaded(__PACKAGE__)->file();
55 2         217 $directory =~ s/\.pm$//;
56 2         72 Geo::Coder::Free::DB::init(directory => File::Spec->catfile($directory, 'databases'));
57              
58 2         11 return bless { }, $class;
59             }
60              
61             =head2 geocode
62              
63             $location = $geocoder->geocode(location => $location);
64              
65             print 'Latitude: ', $location->{'latt'}, "\n";
66             print 'Longitude: ', $location->{'longt'}, "\n";
67              
68             # TODO:
69             # @locations = $geocoder->geocode('Portland, USA');
70             # diag 'There are Portlands in ', join (', ', map { $_->{'state'} } @locations);
71            
72             =cut
73              
74             sub geocode {
75 15     15 1 17349 my $self = shift;
76              
77 15         37 my %param;
78 15 50       81 if (@_ % 2 == 0) {
79 0         0 %param = @_;
80             } else {
81 15         45 $param{location} = shift;
82             }
83              
84             my $location = $param{location}
85 15 50       57 or Carp::croak("Usage: geocode(location => \$location)");
86              
87 15         87 my $county;
88             my $state;
89 15         0 my $country;
90 15         0 my $country_code;
91 15         0 my $concatenated_codes;
92              
93 15 100       175 if($location =~ /^([\w\s\-]+)?,([\w\s]+),([\w\s]+)?$/) {
    100          
94             # Turn 'Ramsgate, Kent, UK' into 'Ramsgate'
95 8         25 $location = $1;
96 8         21 $county = $2;
97 8         31 $country = $3;
98 8         26 $location =~ s/\-/ /g;
99 8         38 $county =~ s/^\s//g;
100 8         27 $county =~ s/\s$//g;
101 8         33 $country =~ s/^\s//g;
102 8         25 $country =~ s/\s$//g;
103 8 100       33 if($location =~ /^St\.? (.+)/) {
104 1         4 $location = "Saint $1";
105             }
106 8 50 33     63 if(($country eq 'UK') || ($country eq 'United Kingdom')) {
107 0         0 $country = 'Great Britain';
108 0         0 $concatenated_codes = 'GB';
109             }
110             } elsif($location =~ /^([\w\s\-]+)?,([\w\s]+),([\w\s]+),\s*(Canada|United States|USA|US)?$/) {
111 5         17 $location = $1;
112 5         14 $county = $2;
113 5         13 $state = $3;
114 5         14 $country = $4;
115 5         24 $county =~ s/^\s//g;
116 5         17 $county =~ s/\s$//g;
117 5         29 $state =~ s/^\s//g;
118 5         15 $state =~ s/\s$//g;
119 5         14 $country =~ s/^\s//g;
120 5         13 $country =~ s/\s$//g;
121             } else {
122 2         8 Carp::croak(__PACKAGE__, ' only supports towns, not full addresses');
123 0         0 return;
124             }
125              
126 13 50       42 if($country) {
127 13 100       53 if(!defined($self->{'admin1'})) {
128 1 50       10 $self->{'admin1'} = Geo::Coder::Free::DB::admin1->new() or die "Can't open the admin1 database";
129             }
130 13 100       69 if(my $admin1 = $self->{'admin1'}->fetchrow_hashref(asciiname => $country)) {
131 5         483 $concatenated_codes = $admin1->{'concatenated_codes'};
132             } else {
133 8         1148 require Locale::Country;
134 8 100       35934 if($state) {
135 5 100       32 if($state =~ /^[A-Z]{2}$/) {
136 3         20 $concatenated_codes = uc(Locale::Country::country2code($country)) . ".$state";
137             } else {
138 2         11 $concatenated_codes = uc(Locale::Country::country2code($country));
139 2         100 $country_code = $concatenated_codes;
140 2 50       9 if($state) {
141 2         4 my @admin1s = @{$self->{'admin1'}->selectall_hashref(asciiname => $state)};
  2         11  
142 2         222 foreach my $admin1(@admin1s) {
143 4 100       37 if($admin1->{'concatenated_codes'} =~ /^$concatenated_codes\./i) {
144 2         5 $concatenated_codes = $admin1->{'concatenated_codes'};
145 2         9 last;
146             }
147             }
148             }
149             }
150             } else {
151 3         20 $concatenated_codes = uc(Locale::Country::country2code($country));
152             }
153             }
154             }
155 13 50       416 return unless(defined($concatenated_codes));
156              
157 13 100       1522 if(!defined($self->{'admin2'})) {
158 1 50       14 $self->{'admin2'} = Geo::Coder::Free::DB::admin2->new() or die "Can't open the admin1 database";
159             }
160 13         64 my @admin2s;
161             my $region;
162 13         0 my @regions;
163 13 100       70 if($county =~ /^[A-Z]{2}/) {
164             # Canadian province or US state
165 1         3 $region = $county;
166             } else {
167 12         25 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $county)};
  12         66  
168 12         1378 foreach my $admin2(@admin2s) {
169 53 100       329 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
170 9         22 $region = $admin2->{'concatenated_codes'};
171 9 100       52 if($region =~ /^[A-Z]{2}\.([A-Z]{2})\./) {
172 3         10 my $rc = $1;
173 3 100       14 if($state =~ /^[A-Z]{2}$/) {
174 1 50       5 if($state eq $rc) {
175 1         3 $region = $rc;
176 1         4 last;
177             }
178             } else {
179 2         7 push @regions, $region;
180 2         4 push @regions, $rc;
181             }
182             } else {
183 6         17 push @regions, $region;
184             }
185             }
186             }
187 12 100 100     69 if($state && !defined($region)) {
188 2 50       13 if($state =~ /^[A-Z]{2}$/) {
189 2         5 $region = $state;
190             } else {
191 0         0 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $state)};
  0         0  
192 0         0 foreach my $admin2(@admin2s) {
193 0 0       0 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
194 0         0 $region = $admin2->{'concatenated_codes'};
195 0         0 last;
196             }
197             }
198             }
199             }
200             }
201              
202 13 100 100     79 if((scalar(@regions) == 0) && (!defined($region))) {
203             # e.g. Unitary authorities in the UK
204 3         6 @admin2s = @{$self->{'admin2'}->selectall_hashref(asciiname => $location)};
  3         16  
205 3 100 66     339 if(scalar(@admin2s) && defined($admin2s[0]->{'concatenated_codes'})) {
206 1         3 foreach my $admin2(@admin2s) {
207 1 50       26 if($admin2->{'concatenated_codes'} =~ $concatenated_codes) {
208 1         3 $region = $admin2->{'concatenated_codes'};
209 1         3 last;
210             }
211             }
212             } else {
213             # e.g. states in the US
214 2         5 my @admin1s = @{$self->{'admin1'}->selectall_hashref(asciiname => $county)};
  2         22  
215 2         215 foreach my $admin1(@admin1s) {
216 3 100       37 if($admin1->{'concatenated_codes'} =~ /^$concatenated_codes\./i) {
217 2         7 $region = $admin1->{'concatenated_codes'};
218 2         8 last;
219             }
220             }
221             }
222             }
223              
224 13 100       53 if(!defined($self->{'cities'})) {
225 1         18 $self->{'cities'} = Geo::Coder::Free::DB::cities->new();
226             }
227              
228 13         57 my $options = { City => lc($location) };
229 13 50       39 if($region) {
230 13 100       58 if($region =~ /^.+\.(.+)$/) {
231 9         22 $region = $1;
232             }
233 13         36 $options->{'Region'} = $region;
234 13 100       45 if($country_code) {
235 2         7 $options->{'Country'} = lc($country_code);
236             }
237             }
238              
239             # This case nonsense is because DBD::CSV changes the columns to lowercase, wherease DBD::SQLite does not
240 13 50       36 if(wantarray) {
241 0         0 my @rc = @{$self->{'cities'}->selectall_hashref($options)};
  0         0  
242 0         0 foreach my $city(@rc) {
243 0 0       0 if($city->{'Latitude'}) {
244 0         0 $city->{'latitude'} = delete $city->{'Latitude'};
245 0         0 $city->{'longitude'} = delete $city->{'Longitude'};
246             }
247             }
248 0         0 return @rc;
249             }
250 13         59 my $city = $self->{'cities'}->fetchrow_hashref($options);
251 13 100       1334 if(!defined($city)) {
252 4         494 foreach $region(@regions) {
253 5 100       289 if($region =~ /^.+\.(.+)$/) {
254 3         9 $region = $1;
255             }
256 5         15 $options->{'Region'} = $region;
257 5         23 $city = $self->{'cities'}->fetchrow_hashref($options);
258 5 100       449 last if(defined($city));
259             }
260             }
261              
262 13 50 66     1734 if(defined($city) && $city->{'Latitude'}) {
263 0         0 $city->{'latitude'} = delete $city->{'Latitude'};
264 0         0 $city->{'longitude'} = delete $city->{'Longitude'};
265             }
266 13         196 return $city;
267             # my $rc;
268             # if(wantarray && $rc->{'otherlocations'} && $rc->{'otherlocations'}->{'loc'} &&
269             # (ref($rc->{'otherlocations'}->{'loc'}) eq 'ARRAY')) {
270             # my @rc = @{$rc->{'otherlocations'}->{'loc'}};
271             # if(scalar(@rc)) {
272             # return @rc;
273             # }
274             # }
275             # return $rc;
276             # my @results = @{ $data || [] };
277             # wantarray ? @results : $results[0];
278             }
279              
280             =head2 reverse_geocode
281              
282             $location = $geocoder->reverse_geocode(latlng => '37.778907,-122.39732');
283              
284             Similar to geocode except it expects a latitude/longitude parameter.
285              
286             =cut
287              
288             sub reverse_geocode {
289 0     0 1   my $self = shift;
290              
291 0           my %param;
292 0 0         if (@_ % 2 == 0) {
293 0           %param = @_;
294             } else {
295 0           $param{latlng} = shift;
296             }
297              
298             my $latlng = $param{latlng}
299 0 0         or Carp::croak("Usage: reverse_geocode(latlng => \$latlng)");
300              
301 0           return $self->geocode(location => $latlng, reverse => 1);
302             };
303              
304             =head1 AUTHOR
305              
306             Nigel Horne
307              
308             This library is free software; you can redistribute it and/or modify
309             it under the same terms as Perl itself.
310              
311             =head1 BUGS
312              
313             CSV files take a long time to load. Convert to SQLite.
314              
315             Lots of lookups fail at the moment.
316              
317             =head1 SEE ALSO
318              
319             VWF, Maxmind and geonames.
320              
321             =head1 LICENSE AND COPYRIGHT
322              
323             Copyright 2017 Nigel Horne.
324              
325             This program is released under the following licence: GPL2
326              
327             =cut
328              
329             1;