File Coverage

blib/lib/Geo/Postcode/Location.pm
Criterion Covered Total %
statement 107 107 100.0
branch 25 42 59.5
condition 8 19 42.1
subroutine 19 19 100.0
pod 13 13 100.0
total 172 200 86.0


line stmt bran cond sub pod time code
1             package Geo::Postcode::Location;
2              
3 1     1   4 use strict;
  1         2  
  1         40  
4 1     1   4 use warnings;
  1         2  
  1         40  
5 1     1   4 use vars qw($VERSION $AUTOLOAD $datafile $tablename $dbh $broadosgrid $fineosgrid $units $pi);
  1         2  
  1         123  
6 1     1   4 use DBI;
  1         3  
  1         1340  
7              
8             $VERSION = '0.12';
9             $tablename = "postcodes";
10             $units = "km";
11             $pi = 3.14159;
12             $datafile = undef;
13             $dbh = undef;
14              
15             =head1 NAME
16              
17             Geo::Postcode::Location - helper class for Geo::Postcode that handles grid reference lookups
18              
19             =head1 SYNOPSIS
20              
21             $Geo::Postcode::Location::datafile = '/usr/local/lib/postcodes.db';
22             my ($x, $y) = Geo::Postcode->coordinates('EC1R 8BB');
23              
24             =head1 DESCRIPTION
25              
26             Geo::Postcode::Location holds the gridref-lookup functions of Geo::Postcode. It is separated here to minimise the footprint of the main module and to facilitate subclassing.
27              
28             It doesn't really have a useful direct interface, since it requires an object of Geo::Postcode (or a subclass) and is most easily reached through that object, but it does have a couple of configuration variables and there is method documentation here for anyone interested in subclassing it or changing the data source.
29              
30             =head1 GRIDREF DATA
31              
32             There are at least three ways to supply your own gridref data.
33              
34             =over
35              
36             =item * replace the data file
37              
38             If you can get your data into a SQLite file, all you have to do is set the either C or $ENV{POSTCODE_DATA} to the full path to your data file:
39              
40             $Geo::Postcode::Location::datafile = '/home/site/data/postcodes.db';
41             # or
42             PerlSetEnv POSTCODE_DATA /home/site/data/postcodes.db
43              
44             I've included (in ./useful) an idiot script that I use to turn .csv data into a SQLite file suitable for use with this module.
45              
46             =item * replace the database handle
47              
48             The query that we use to retrieve location information is very simple, and should work with any DBI database handle. If your application already makes available a suitable database handle, or you would like to create one externally and make sure it is reused, it should just work:
49              
50             $Geo::Postcode::Location::dbh = $my_dbh;
51             $Geo::Postcode::Location::tablename = 'postcodedata';
52             my ($x, $y) = Geo::Postcode->coordinates('EC1Y 8PQ');
53              
54             If running under mod_perl, you probably don't want to share the handle like that. You can achieve the same thing with instance methods and avoid side-effects, but you have to make the calls at the right time:
55              
56             my $postcode = Geo::Postcode->new('EC1Y 8PQ');
57             $postcode->location->dbh( $my_dbh );
58             $postcode->location->tablename( 'postcodedata' );
59             my ($x, $y) = $postcode->coordinates;
60              
61             =item * override the lookup mechanism in subclass
62              
63             The data-retrieval process is divided up to make this as simple as possible: see the method descriptions below for details. You should be able to replace the data source by overriding C or redo the whole lookup by replacing C.
64              
65             $Geo::Postcode->location_class('My::Location');
66              
67             package My::Location;
68             use base qw(Geo::Postcode::Location);
69             sub dbh { ... }
70              
71             =back
72              
73             =head1 METHODS
74              
75             =head2 new ()
76              
77             Constructs and returns a location object. Must be supplied with a postcode object of the class dictated by C.
78              
79             =cut
80              
81             sub new {
82 5     5 1 10 my ($class, $postcode) = @_;
83 5 50 33     16 return unless $postcode && ref $postcode eq $class->postcode_class;
84 5         16 my $self = bless { postcode => $postcode }, $class;
85 5         25 return $self;
86             }
87              
88             =head2 postcode_class ()
89              
90             Returns the full name of the postcode class we should be expecting.
91              
92             =cut
93              
94 5     5 1 19 sub postcode_class { 'Geo::Postcode' }
95              
96             =head2 postcode ()
97              
98             Returns the postcode object used to construct this object.
99              
100             =cut
101              
102 5     5 1 24 sub postcode { return shift->{postcode} }
103              
104             =head2 retrieve ()
105              
106             Retrieves location information for this postcode. This method is called during construction, retrieves all the necessary information in one go, so all the rest have to do is look up internal values.
107              
108             =cut
109              
110             sub retrieve {
111 42     42 1 43 my $self = shift;
112 42 100       97 return if $self->{retrieved};
113 5   50     10 my $table = $self->tablename || 'postcodes';
114 5         11 my $sth = $self->dbh->prepare("SELECT * from $table where postcode = ?");
115 5         14772 my $row;
116 5         17 my $codes = $self->postcode->analyse;
117 5         14 TRY: for (@$codes) {
118 15         1288 $sth->execute($_);
119 15 100       350 last TRY if $row = $sth->fetchrow_hashref;
120             }
121 5         22 $self->{$_} = $row->{$_} for $self->cols;
122 5         11 $self->{retrieved} = 1;
123 5         20 $sth->finish;
124 5 50       12 $self->dbh->disconnect if $self->disconnect_after_use;
125 5         82 return;
126             }
127              
128             =head2 disconnect_after_use ()
129              
130             If this returns a true value, then dbh->disconnect will be called after location information is retrieved.
131              
132             =cut
133              
134 5     5 1 17 sub disconnect_after_use { 0 }
135              
136             =head2 dbh ()
137              
138             Accepts, returns - and creates, if necessary - the DBI handle that will be used to retrieve location information.
139              
140             This is only separate to make it easy to override.
141              
142             =cut
143              
144             sub dbh {
145 5     5 1 7 my $self = shift;
146 5 50       11 return $self->{dbh} = $_[0] if @_;
147 5 50       10 return $self->{dbh} = $dbh if defined $dbh;
148 5 50       12 return $self->{dbh} if $self->{dbh};
149            
150 5         10 my $file = $self->datafile;
151 5 50 33     230 return unless $file && -e $file && -f $file;
      33        
152 5         259 eval 'require DBI;';
153 5 50       18 return warn "$@" if $@;
154 5         31 return $self->{dbh} = DBI->connect("dbi:SQLite:dbname=$file","","");
155             }
156              
157             =head2 datafile ( path_to_file )
158              
159             Accepts and returns the location of the SQLite file we expect to provide location data.
160              
161             If no file path is supplied, or found by checking C<$Geo::Postcode::Location::datafile> and C<$ENV{POSTCODE_DATA}>, then we will scan the path to locate the default data file that is installed with this module.
162              
163             =cut
164              
165             sub datafile {
166 5     5 1 4 my $self = shift;
167 5 50       12 return $self->{datafile} = $_[0] if @_;
168 5 50       8 return $self->{datafile} = $datafile if $datafile;
169 5 50       12 return $self->{datafile} = $ENV{POSTCODE_DATA} if $ENV{POSTCODE_DATA};
170 5         9 return $self->{datafile} = _find_file('postcodes.db');
171             }
172              
173             sub _find_file {
174 5     5   6 my $file = shift;
175 5         7 my @files = grep { -e $_ } map { "$_/Geo/Postcode/$file" } @INC;
  55         1181  
  55         96  
176 5         25 return $files[0];
177             }
178              
179             =head2 tablename ()
180              
181             Sets and gets the name of the database table that should be expected to hold postcode data.
182              
183             =cut
184              
185             sub tablename {
186 5     5 1 5 my $self = shift;
187 5 50       10 return $self->{tablename} = $_[0] if @_;
188 5   33     30 return $self->{tablename} ||= $tablename;
189             }
190              
191             =head2 cols ()
192              
193             Returns a list of the columns we should pull from the database row into the location object's internal hash (and also provide as instance methods). This isn't used in the SQL query (which just SELECTs *), so we don't mind if columns are missing.
194              
195             =cut
196              
197 41     41 1 124 sub cols { return qw(gridn gride latitude longitude town ward nhsarea) }
198              
199             =head2 AUTOLOAD ()
200              
201             Turns the columns defined by C into lookup methods. You can't set values this way: the whole module is strictly read-only.
202              
203             =cut
204              
205             sub AUTOLOAD {
206 36     36   40 my $self = shift;
207 36         41 my $m = $AUTOLOAD;
208 36         124 $m =~ s/.*://;
209 36 50       82 return if $m eq 'DESTROY';
210 36 50       49 $m = 'latitude' if $m eq 'lat';
211 36 50       57 $m = 'longitude' if $m eq 'long';
212 36         63 $self->retrieve;
213 36         55 my %cols = map {$_=>1} $self->cols;
  252         413  
214 36 50       97 return unless $cols{$m};
215 36   50     196 return $self->{$m} || '00';
216             }
217              
218             =head2 gridref ()
219              
220             Returns a proper concatenated grid reference for this postcode, in classic Ordnance Survey AA123456 form rather than the all-digits version we use internally.
221              
222             See http://www.ordnancesurvey.co.uk/oswebsite/freefun/nationalgrid/nghelp2.html or the more sober http://vancouver-webpages.com/peter/osgbfaq.txt
223              
224             for more about grid references.
225              
226             Unlike other grid methods here, this one will also strip redundant trailing zeros from the eastings and northings for the sake of readability.
227              
228             =cut
229              
230             $broadosgrid = [
231             ['S', 'T'],
232             ['N', 'O'],
233             ['H', 'J'],
234             ];
235              
236             $fineosgrid = [
237             ['V', 'W', 'X', 'Y', 'Z'],
238             ['Q', 'R', 'S', 'T', 'U'],
239             ['L', 'M', 'N', 'O', 'P'],
240             ['F', 'G', 'H', 'J', 'K'],
241             ['A', 'B', 'C', 'D', 'E'],
242             ];
243              
244             sub gridref {
245 2     2 1 4 my $self = shift;
246 2 50       6 return $self->{gridref} if $self->{gridref};
247 2         4 $self->retrieve;
248 2         10 my $n = $self->gridn;
249 2         9 my $e = $self->gride;
250 2         8 my $broadn = int($n / 500000 );
251 2         4 my $broade = int($e / 500000 );
252 2         6 $n %= 500000;
253 2         3 $e %= 500000;
254 2         4 my $finen = int($n / 100000 );
255 2         3 my $finee = int($e / 100000 );
256 2         3 $n %= 100000;
257 2         2 $e %= 100000;
258 2         10 $n =~ s/(0+)$//;
259 2         8 $e =~ s/(0+)$//;
260 2         8 $n .= '0' x (length($e) - length($n));
261 2         3 $e .= '0' x (length($n) - length($e));
262 2         20 return $self->{gridref} = $broadosgrid->[$broadn][$broade] . $fineosgrid->[$finen][$finee] . $e . $n;
263             }
264              
265             =head2 distance_from ()
266              
267             We prefer to use grid references to calculate distances, since they're laid out nicely on a flat plane and don't require us to remember our A-levels. This method just returns a single distance value.
268              
269             You can specify the units of distance by setting C<$Geo::Postcode::Location::units> or passing in a second parameter. Either way it must be one of 'miles', 'km' or 'm'. The default is 'km'.
270              
271             =cut
272              
273             sub distance_from {
274 4     4 1 7 my ($self, $postcode, $u) = @_;
275 4 50       13 return unless $postcode;
276 4         9 $self->retrieve;
277 4         17 my $dx = $self->gride - $postcode->gride;
278 4         19 my $dy = $self->gridn - $postcode->gridn;
279 4         18 my $distance = sqrt($dx**2 + $dy**2);
280 4   66     16 $u ||= $units;
281            
282             # longer coordinates mean greater precision (*10 per digit), which means
283             # smaller units. we therefore have to multiply out by a factor based
284             # on the length of the coordinates to get a kilometer distance.
285             # the multiplier is adjusted to return other units if required.
286            
287 4         14 my $multiplier = 10**(3 - length($self->gride));
288 4 100       12 $multiplier *= 0.6214 if $u eq 'miles';
289 4 100       8 $multiplier *= 1000 if $u eq 'm';
290 4         25 return int($distance * $multiplier);
291             }
292              
293             =head2 bearing_to ()
294              
295             Returns the angle from grid north, in degrees clockwise, of the line from this postcode to the postcode object supplied.
296              
297             =cut
298              
299             sub bearing_to {
300 2     2 1 3 my ($self, $postcode) = @_;
301 2         8 my $dx = $self->gride - $postcode->gride;
302 2         8 my $dy = $self->gridn - $postcode->gridn;
303 2         32 my $r = atan2($dy,$dx);
304 2         7 my $d = (90 + ($r/$pi * 180) + 360) % 360;
305 2         7 return $d;
306             }
307              
308             =head2 friendly_bearing_to ()
309              
310             Returns a readable approximation of the bearing from here to there, in a form like 'NW' or 'SSE'.
311              
312             =cut
313              
314             sub friendly_bearing_to {
315 1     1 1 1 my ($self, $postcode) = @_;
316 1         3 my $bearing = $self->bearing_to( $postcode );
317 1         6 my @nicely = qw(N NNW NW WNW W WSW SW SSW S SSE SE ESE E ENE NE NNE N);
318 1         4 my $i = int( ($bearing + 11.25)/22.5 );
319 1         6 return $nicely[$i];
320             }
321              
322             =head1 AUTHOR
323              
324             William Ross, wross@cpan.org
325              
326             =head1 COPYRIGHT
327              
328             Copyright 2004 William Ross, spanner ltd.
329              
330             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
331              
332             =cut
333              
334             1;
335              
336