Fixed conf bug
[geohash-tool.git] / Hashing.pm
1 #!/usr/bin/perl -w
2 #
3 # $Id: Hashing.pm 257 2008-06-25 02:02:19Z dan $
4 #
5
6 package Geo::Hashing;
7
8 use strict;
9 use warnings;
10 use Carp;
11 use Digest::MD5 qw/md5_hex/;
12
13 our $VERSION = '0.06';
14
15 =head1 NAME
16
17 Geo::Hashing - Perl library to calculate Geohashing points
18
19 =head1 SYNOPSIS
20
21 use Geo::Hashing;
22 my $g = new Geo::Hashing(lat => 37, lon => -122, date => "2008-05-24");
23 printf "Today's location is at %.6f, %.6f.\n", $g->lat, $g->lon;
24
25 =head1 DESCRIPTION
26
27 This module allows calculating the locaiton of Geohashes as described
28 in http://wiki.xkcd.com/geohashing/Main_Page.
29
30 =head1 METHODS
31
32 =cut
33
34 =head2 new
35
36 Create a new Geo::Hashing object.
37
38 =cut
39
40 sub new {
41 my $class = shift;
42 my %p = @_;
43
44 my $self = {_lat => 0, _lon => 0, _dlat => 0, _dlon => 0, _debug => 0};
45 bless $self, $class;
46
47 $self->{_date} = sprintf("%04d-%02d-%02d", (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3]);
48
49 {
50 no strict 'subs';
51 foreach (qw/debug source lat lon date use_30w_rule djia/) {
52 if (exists $p{$_}) {
53 $self->_log("Setting $_ to $p{$_}");
54 $self->$_($p{$_});
55 }
56 }
57 }
58
59 unless ($p{source}) {
60 $self->source('peeron');
61 }
62
63 $self->_log("Using", $self->source, "as the DJIA source");
64
65 return $self;
66 }
67
68 =head2 lat
69
70 Set or get the points latitude. When settings, only the integer portion is
71 considered. Set to undef to just get the offset.
72
73 =cut
74
75 sub lat {
76 my $self = shift;
77 my $lat = shift;
78
79 if (defined $lat) {
80 if ($lat =~ /^-?\d+(?:\.\d+)?$/ and $lat > -180 and $lat < 180) {
81 $self->{_lat} = $lat ne "-0" ? int($lat) : "-0";
82 $self->_update();
83 } else {
84 croak "Invalid latitude ($lat)!";
85 }
86 }
87
88 return undef unless defined $self->{_dlat} and defined $self->{_dlon};
89
90 return $self->{_lat} eq "-0" || $self->{_lat} < 0 ?
91 $self->{_lat} - $self->{_dlat} :
92 $self->{_lat} + $self->{_dlat};
93 }
94
95 =head2 lon
96
97 Set or get the points longitude. When settings, only the integer portion is
98 considered. Set to undef to just get the offset.
99
100 =cut
101
102 sub lon {
103 my $self = shift;
104 my $lon = shift;
105
106 if (defined $lon) {
107 if ($lon =~ /^-?\d+(?:\.\d+)?$/ and $lon > -180 and $lon < 180) {
108 $self->{_lon} = $lon ne "-0" ? int($lon) : "-0";
109 $self->_update();
110 } else {
111 croak "Invalid longitude ($lon)!";
112 }
113 }
114
115 return undef unless defined $self->{_dlat} and defined $self->{_dlon};
116
117 return $self->{_lon} eq "-0" || $self->{_lon} < 0 ?
118 $self->{_lon} - $self->{_dlon} :
119 $self->{_lon} + $self->{_dlon};
120 }
121
122 =head2 date
123
124 Set or get the date used for the calculation. Note that this is the actual
125 date of the meetup in question, even when the 30w rule is in effect.
126
127 =cut
128
129 sub date {
130 my $self = shift;
131 my $date = shift;
132
133 if (defined $date) {
134 if ($date =~ /^\d\d\d\d-\d\d-\d\d$/) {
135 $self->{_date} = $date;
136 $self->djia(undef);
137 $self->_update();
138 } else {
139 croak "Invalid date ($date)!";
140 }
141 }
142
143 return $self->{_date};
144 }
145
146 =head2 djia
147
148 Set or get the Dow Jones Industrial Average used for the calculation. If not
149 set, it will be automatically retrieved depending on the value of
150 $self->source. If the data cannot be retrieved, undef will be returned.
151
152 =cut
153
154 sub djia {
155 my $self = shift;
156 my $djia = shift;
157
158 if ($djia) {
159 if ($djia =~ /^\d+(?:\.\d+)?$/) {
160 $self->{_djia} = $djia;
161 } else {
162 croak "Invalid DJIA ($djia)!";
163 }
164 } elsif ($self->source) {
165 my $date = $self->date;
166 # if ($self->use_30w_rule) {
167 if ($self->{_lat} > -30) {
168 require Time::Local;
169 my ($y, $m, $d) = split /-/, $self->date;
170 my $time = Time::Local::timelocal(0, 0, 0, $d, $m-1, $y);
171 ($d, $m, $y) = (localtime($time - 24*60*60))[3,4,5];
172 $m++; $y += 1900;
173 $date = sprintf("%04d-%02d-%02d", $y, $m, $d);
174 }
175 $self->_log("Requesting", $self->source, "->DJIA($date)");
176 $self->{_djia} = $self->_get_djia($date);
177 } else {
178 $self->_log("No source set, can't automatically get DJIA");
179 return undef;
180 }
181 $self->{_djia} =~ s/ *$//g;
182 return $self->{_djia};
183 }
184
185 =head2 source
186
187 Set the source of the DJIA opening data. Will load Geo::Hashing::Source::Name
188 and call get_djia($date). See Geo::Hashing::Source::Random for a sample.
189
190 =cut
191
192 sub source {
193 my $self = shift;
194 my $source = shift;
195
196 if (defined $source) {
197 $self->_log("Loading source Geo::Hashing::Source::\u$source");
198 eval " require Geo::Hashing::Source::\u$source";
199
200 if ($@) {
201 croak "Failed to load Geo::Hashing::Source::\u$source: $@";
202 }
203
204 $self->{_source} = $source;
205 $self->_update();
206 }
207
208 if ($self->{_source}) {
209 return "Geo::Hashing::Source::" . ucfirst $self->{_source};
210 } else {
211 return undef;
212 }
213 }
214
215 =head2 use_30w_rule
216
217 Set or get the 30w flag. Will be set automatically if lon is set and is
218 greater than -30.
219
220 =cut
221
222 sub use_30w_rule {
223
224 my $self = shift;
225 my $w30 = shift;
226 if (defined $w30) {
227 $self->{_30w} = $w30 ? 1 : 0;
228 $self->_update();
229 } elsif (defined $self->lon) {
230 if ($self->lon > -30) {
231 if (not $self->date) {
232 $self->{_30w} = 1;
233 } else {
234 my ($y, $m, $d) = split /-/, $self->date;
235 if ($y > 2008) {
236 $self->{_30w} = 1;
237 } elsif ($y == 2008 and $m > 5) {
238 $self->{_30w} = 1;
239 } elsif ($y == 2008 and $m == 5 and $d >= 27) {
240 $self->{_30w} = 1;
241 } else {
242 $self->{_30w} = 1;
243 }
244 }
245 } else {
246 $self->{_30w} = 0;
247 }
248 }
249 return $self->{_30w};
250 }
251
252 =head2 debug
253
254 Enable or disable diagnostic logging
255
256 =cut
257
258 sub debug {
259 my $self = shift;
260 my $debug = shift;
261
262 if (defined $debug) {
263 $self->{_debug} = $debug ? 1 : 0;
264 $self->_log("Debug", $self->{_debug} ? "enabled" : "disabled");
265 }
266
267 return $self->{_debug};
268 }
269
270 # private methods
271 # _update - given all the information in the object, calculate the day's
272 # offsets
273 sub _update {
274 my $self = shift;
275
276 my $djia = $self->djia;
277 unless (defined $djia) {
278 $self->_log("Failed to get DJIA");
279 $self->{_dlat} = $self->{_dlon} = undef;
280 return undef;
281 }
282
283 $self->_log("DJIA for", $self->date, "is $djia");
284
285 my $md5 = md5_hex($self->date . "-" . $djia);
286 $self->_log(" - md5(". $self->date ."-$djia)");
287 $self->_log(" - md5 = $md5");
288
289 my ($md5lat, $md5lon) = (substr($md5, 0, 16), substr($md5, 16, 16));
290 $self->_log(" - = $md5lat, $md5lon");
291 ($self->{_dlat}, $self->{_dlon}) = (0, 0);
292
293 while (length $md5lat) {
294 $self->{_dlat} += hex substr($md5lat, -1, 1, "");
295 $self->{_dlon} += hex substr($md5lon, -1, 1, "");
296 $self->{_dlat} /= 16;
297 $self->{_dlon} /= 16;
298 }
299
300 $self->_log(" dlat, dlon => $self->{_dlat}, $self->{_dlon}");
301 }
302
303 # _log - print out a timestampped log entry
304 sub _log {
305 my $self = shift;
306
307 return unless $self->debug;
308
309 print scalar localtime, " - @_\n";
310 }
311
312 # _get_djia - call get_djia on from the current source
313 sub _get_djia {
314 my $self = shift;
315
316 $self->_log("getting DJIA from", $self->source);
317 return $self->source->get_djia(@_);
318 }
319
320 =head1 SEE ALSO
321
322 Original comic: http://www.xkcd.com/426/
323
324 Wiki: http://wiki.xkcd.com/geohashing/Main_Page
325
326 IRC: irc://irc.xkcd.com/geohashing
327
328 =head1 AUTHOR
329
330 Dan Boger, E<lt>zigdon@gmail.comE<gt>
331
332 =head1 COPYRIGHT AND LICENSE
333
334 Copyright (C) 2008 by Dan Boger
335
336 This library is free software; you can redistribute it and/or modify
337 it under the same terms as Perl itself, either Perl version 5.10.0 or,
338 at your option, any later version of Perl 5 you may have available.
339
340
341 =cut
342
343 1;
344