File Coverage

blib/lib/Geo/Hash.pm
Criterion Covered Total %
statement 50 51 98.0
branch 9 12 75.0
condition 3 6 50.0
subroutine 11 11 100.0
pod 5 5 100.0
total 78 85 91.7


line stmt bran cond sub pod time code
1             package Geo::Hash;
2              
3 2     2   119641 use warnings;
  2         7  
  2         80  
4 2     2   14 use strict;
  2         4  
  2         80  
5 2     2   12 use Carp;
  2         8  
  2         2065  
6              
7             =head1 NAME
8              
9             Geo::Hash - Encode / decode geohash.org locations.
10              
11             =head1 VERSION
12              
13             This document describes Geo::Hash version 0.02
14              
15             =cut
16              
17             our $VERSION = '0.02';
18              
19             =head1 SYNOPSIS
20              
21             use Geo::Hash;
22              
23             my $gh = Geo::Hash->new;
24             my $hash = $gh->encode( $lat, $lon );
25             my ( $lat, $lon ) = $gh->decode( $hash );
26            
27             =head1 DESCRIPTION
28              
29             Geohash is a latitude/longitude geocode system invented by Gustavo
30             Niemeyer when writing the web service at geohash.org, and put into the
31             public domain.
32              
33             This module encodes and decodes geohash locations.
34              
35             See L and L
36             for more information.
37              
38             =head1 INTERFACE
39              
40             =head2 C<< new >>
41              
42             Create a new Geo::Hash object.
43              
44             my $gh = Geo::Hash->new;
45              
46             =cut
47              
48 7     7 1 4311 sub new { bless {}, shift }
49              
50             my @ENC = qw(
51             0 1 2 3 4 5 6 7 8 9 b c d e f g h j k m n p q r s t u v w x y z
52             );
53              
54             my %DEC = map { $ENC[$_] => $_ } 0 .. $#ENC;
55              
56             sub _mid {
57 998     998   1575 my ( $ar, $wh ) = @_;
58 998         2353 return ( $ar->[$wh][0] + $ar->[$wh][1] ) / 2;
59             }
60              
61             # The number of bits necessary to represent the specified number of
62             # decimal digits
63 12     12   34 sub _d2b { int( shift() * 3.32192809488736 + 1 ) }
64              
65             sub _bits_for_number {
66 14     14   16 my $n = shift;
67 14 100       161 return 0 unless $n =~ s/.*\.//;
68 12         27 return _d2b( length $n );
69             }
70              
71             =head2 C<< precision >>
72              
73             Infer a suitable precision (number of character in hash) for a given
74             lat, lon pair.
75              
76             my $prec = $gh->precision( $lat, $lon );
77              
78             =cut
79              
80             sub precision {
81 7     7 1 11 my ( $self, $lat, $lon ) = @_;
82 7         16 my $lab = _bits_for_number( $lat ) + 8;
83 7         13 my $lob = _bits_for_number( $lon ) + 9;
84 7 100       48 return int( ( ( $lab > $lob ? $lab : $lob ) + 1 ) / 2.5 );
85             }
86              
87             =head2 C<< encode >>
88              
89             Encode a lat, long pair into a geohash.
90              
91             my $hash = $gh->encode( $lat, $lon );
92              
93             You may optionally supply the length of the desired geohash:
94              
95             # Very precise
96             my $hash = $gh->encode( $lat, $lon, 10 );
97              
98             If the precision argument is omitted C will be used to
99             provide a default.
100              
101             =cut
102              
103             sub encode {
104 14 50 33 14 1 3778 croak "encode needs two or three arguments"
105             unless @_ >= 3 && @_ <= 4;
106 14         37 my ( $self, @pos ) = splice @_, 0, 3;
107 14   66     45 my $prec = shift || $self->precision( @pos );
108 14         42 my $int = [ [ 90, -90 ], [ 180, -180 ] ];
109 14         16 my $flip = 1;
110 14         23 my @enc = ();
111 14         30 while ( @enc < $prec ) {
112 97         114 my $bits = 0;
113 97         139 for ( 0 .. 4 ) {
114 485         683 my $mid = _mid( $int, $flip );
115 485 100       1056 my $bit = $pos[$flip] >= $mid ? 1 : 0;
116 485         551 $bits = ( ( $bits << 1 ) | $bit );
117 485         1734 $int->[$flip][$bit] = $mid;
118 485         671 $flip ^= 1;
119             }
120 97         262 push @enc, $ENC[$bits];
121             }
122 14         90 return join '', @enc;
123             }
124              
125             =head2 C<< decode_to_interval >>
126              
127             Like C but instead of returning a pair of coordinates returns
128             the interval for each coordinate. This gives some indication of how
129             precisely the original hash specified the location.
130              
131             The return value is a pair of array refs. Each referred to array
132             contains the upper and lower bounds for each coordinate.
133              
134             my ( $lat_range, $lon_range ) = $gh->decode_to_interval( $hash );
135             # $lat_range and $lon_range are references to two element arrays
136              
137             =cut
138              
139             sub decode_to_interval {
140 14 50   14 1 207 croak "Needs one argument"
141             unless @_ == 2;
142 14         23 my ( $self, $hash ) = @_;
143              
144 14         40 my $int = [ [ 90, -90 ], [ 180, -180 ] ];
145 14         205 my $flip = 1;
146              
147 14         51 for my $ch ( split //, $hash ) {
148 97 50       211 if ( defined( my $bits = $DEC{$ch} ) ) {
149 97         143 for ( 0 .. 4 ) {
150 485         889 $int->[$flip][ ( $bits & 16 ) >> 4 ]
151             = _mid( $int, $flip );
152 485         694 $flip ^= 1;
153 485         2297 $bits <<= 1;
154             }
155             }
156             else {
157 0         0 croak "Bad character '$ch' in hash '$hash'";
158             }
159             }
160              
161 14         56 return @$int;
162             }
163              
164             =head2 C<< decode >>
165              
166             Decode a geohash into a lat, long pair.
167              
168             my ( $lat, $lon ) = $gh->decode( $hash );
169              
170             =cut
171              
172             sub decode {
173 14     14 1 6992 my @int = shift->decode_to_interval( @_ );
174 14         30 return map { _mid( \@int, $_ ) } 0 .. 1;
  28         46  
175             }
176              
177             1;
178             __END__