File Coverage

blib/lib/Acme/Geo/Whitwell/Name.pm
Criterion Covered Total %
statement 102 102 100.0
branch 36 36 100.0
condition 20 20 100.0
subroutine 12 12 100.0
pod 2 2 100.0
total 172 172 100.0


line stmt bran cond sub pod time code
1             package Acme::Geo::Whitwell::Name;
2              
3 6     6   157831 use strict;
  6         14  
  6         221  
4 6     6   33 use warnings;
  6         11  
  6         173  
5 6     6   32 use Carp qw(croak);
  6         15  
  6         444  
6              
7 6     6   33 use Exporter;
  6         11  
  6         431  
8             @Acme::Geo::Whitwell::Name::ISA = qw(Exporter);
9             @Acme::Geo::Whitwell::Name::EXPORT_OK = qw(to_whitwell from_whitwell);
10              
11 6     6   90 use Scalar::Util qw(looks_like_number);
  6         11  
  6         8834  
12              
13             =head1 NAME
14              
15             Acme::Geo::Whitwell::Name - Steadman Whitwell's "rational geographic nomenclature"
16              
17             =head1 VERSION
18              
19             Version 0.04
20              
21             =cut
22              
23             our $VERSION = '0.04';
24              
25             =head1 SYNOPSIS
26              
27             use Acme::Geo::Whitwell::Name qw(to_whitwell from_whitwell);
28              
29             # Convert Sunnyvale, CA's lat and lon to a Whitwell name pair.
30             my @names = to_whitwell("37.37N", "122.03");
31              
32             # Same conversion, using signed latitude and longitude instead.
33             my @names = to_whitwell(37.37, -122.03);
34              
35             # Convert a Whitwell name to a latitude and longitude.
36             # (Washington DC's "rational" name to N/S lat and E/W long.)
37             my($lat_string, $lon_string) = from_whitwell("Feiro Nyvout");
38              
39             # If you want signed values, add signed => some true value.
40             my($lat, $long) = from_whitwell("Feiro Nyvout", signed=>1);
41              
42             =head1 DESCRIPTION
43              
44             This module implements Steadman Whitwell's "rational system of geographic
45             nomenclature", in which place names are generated by converting the latitude
46             and longitude of the location into a two-part name by means of a
47             transliteration scheme.
48              
49             Whitwell devised this scheme in an attempt to provide an alternative to
50             the proliferation of similarly-named towns in the early US. However, people
51             seemed to prefer creating many Springfields and Washingtons in preference to
52             using Whitwell's uniquely quirky names.
53              
54             =head2 THE SCHEME
55              
56             Two tables of number-to-letter(s) are used to translate latitudes and
57             longitudes of two-decimal precision, digit-by-digit, into
58             vaguely-pronounceable two-part names.
59              
60             1 2 3 4 5 6 7 8 9 0
61             latitude a e i o u y ee ei ie ou vowels
62             longitude b d f k l m n p r t consonants
63              
64             Transliteration is done by looking up the apropriate digit in the tables above,
65             switching rows until all the digits are consumed. If the coordinate is negative,
66             a special 'sign consonant' is inserted into the (partial) name after the first
67             vowel is added, and the transliteration continues by choosing again from the
68             vowel table, then continuing to alternate again.
69              
70             This is very orderly, but confusing to generate by hand (putting aside the
71             fact that no one in their right mind really wants to live in "Isilu Buban"
72             instead of Sydney, AU, or "Feiro Nyvout" instead of Washington, DC).
73              
74             The generated names are guaranteed to have alternating consonants and vowels,
75             and should be pronounceable (though most likely bizarre). I have not been able
76             to locate the original documentation of the scheme, so I am unable to determine
77             why some example names are built in "reverse": with the first letter for the
78             latitude selected from the longitude table, and vice versa for the longitude. I
79             can only guess that the alternate construction was deemed more pronounceable or
80             "interesting". Since this is the case, I generate both alternatives so you can
81             choose the one that seems "better". In the cases of places like McMurdo Base
82             ("Eeseepu Bymeem" or "Neeveil Amyny"), I'm not sure there I a "better".
83              
84             However, solely for the purposes of amusement, it can be interesting to find
85             out what a given location would have been called in the alternate universe
86             where Whitwell's scheme caught on.
87              
88             It would be lovely to use this module to change all the place names on
89             online maps, wouldn't it?
90              
91             =head2 SOURCES
92              
93             =over
94              
95             =item * I, William E.
96             Wilson, Indiana University Press, 1984, p. 154
97             =item * Search books.google.com for '"new harmony gazette" whitwell'
98             =item * http://www.kirchersociety.org/blog/2007/05/15/whitwells-system-for-a-rational-geographical-nomenclature/
99              
100             =back
101              
102             =cut
103              
104             # These tables define the letters that the numbers will be transliterated into.
105             # 0 1 2 3 4 5 6 7 8 9
106             my @vowels = qw(ou a e i o u y ee ei ie);
107             my @consonants = qw(t b d f k l m n p r);
108              
109             # Allows us to detect when to insert the "sign consonant" for negative
110             # lats and lons.
111             my %vowel;
112             @vowel{@vowels} = ();
113              
114             =head1 EXPORT
115              
116             =head1 FUNCTIONS
117              
118             =head2 to_whitwell($lat, $lon)
119              
120             Generates a properly-capitalized Whitwell name from a latitude-longitude pair.
121             Latitude and longitude are truncated to the two digits after the decimal point,
122             in keeping with Whitwell's original scheme. Zeroes are added after the decimal
123             point as necessary.
124              
125             North latitudes are positve, and south latitudes are negative. East longitudes
126             are positive, west longitudes are negative. Trailing E/W and N/S are converted
127             into the appropriate sign. If you supply both for some reason, trailing
128             sign indicators override signs.
129              
130             Returns both alternatives for the name (see L).
131              
132             =cut
133              
134             sub to_whitwell {
135 6     6 1 13287 my($lat, $lon) = @_;
136 6         12 return ( _vowel_build($lat) . ' ' . _consonant_build($lon),
137             _consonant_build($lat) . ' ' . _vowel_build($lon)
138             );
139             }
140              
141 23     23   11607 sub _vowel_build { _gen(shift, [\@vowels, \@consonants], 's') }
142 18     18   65 sub _consonant_build { _gen(shift, [\@consonants, \@vowels], 'v') }
143              
144             sub _gen {
145             # The coordinate, the letter lists, and the appropriate sign consonant.
146 41     41   73 my($coord, $lists, $neg) = @_;
147              
148             # Turn the floating-point number into a list of digits.
149             # Note that _two_decimal does NOT CARE about sign or sign indicators.
150 41         79 $coord = uc(my $orig_coord = $coord);
151 41         87 my @coord = grep {/(\d)/} (split //, _two_decimal($coord));
  206         507  
152              
153 41         75 my $word = '';
154 41         79 my $list = 0;
155 41         47 my $signed = 0;
156              
157 41         133 my ($is_negative) = ($coord =~ s/[SW]//g);
158 41         102 my ($is_positive) = ($coord =~ s/[NE]//g);
159              
160 41 100       166 croak
161             "Coordinate '$orig_coord' does not look like a proper coordinate"
162             if !looks_like_number($coord);
163              
164 40 100       94 $is_negative = ($coord < 0) unless $is_negative;
165              
166 40 100 100     150 my $conflicting = ($is_negative and $is_positive) ? 'conflicting ' : '';
167 40 100 100     271 croak "Multiple ${conflicting}sign indicators detected in '$orig_coord'"
      100        
168             if $conflicting or $is_negative > 1 or $is_positive > 1;
169              
170 36         61 foreach my $digit (@coord) {
171             # Convert the next digit into a letter from the proper table.
172 146         252 my $letter = $lists->[$list]->[$digit];
173             ### "$letter -> $digit"
174              
175             # Decide whether to insert a sign consonant.
176 146 100 100     556 if (exists $vowel{$letter} and $is_negative and not $signed) {
      100        
177             # If negative, we have a vowel, and we haven't inserted the sign
178             # consonant yet, insert it.
179 16         18 $letter .= $neg;
180             # Now signed.
181 16         27 $signed = 1;
182 16         19 $list = !$list;
183             }
184             # Add new letter(s) to word and continue;
185 146         153 $word .= $letter;
186 146         231 $list = !$list;
187             }
188 36         226 return ucfirst $word;
189             }
190              
191             sub _two_decimal {
192 78     78   29546 my ($coord) = @_;
193            
194             # Discard non-digits except for a decimal point.
195 78         296 $coord =~ s/[^\d\.]//g;
196              
197             # Drop leading zeros.
198 78         272 $coord =~ s/^0*//g;
199 78 100       202 $coord = 0 unless $coord;
200              
201 78 100       247 if (abs($coord) > 180) {
202 2         41 croak "$coord must be between -180 and +180\n";
203             }
204 76 100       225 unless ($coord =~ /\./) {
205             # add decimals
206 24         282 $coord .= ".";
207             }
208             # Add two more zeroes; we'll discard them if we don't need them.
209 76         109 $coord .= "00";
210 76         301 ($coord) = ($coord =~ /^(\d{0,3}\.\d\d)/);
211 76         241 return $coord;
212             }
213              
214             =head2 from_whitwell($whitwell_name, signed => $yes_or_no)
215              
216             Converts a Whitwell name back into a lat/lon pair, in trailing indicator
217             format. Results will be undefined if the string does not match the Whitwell
218             scheme; if the strings I Whitwell-compatible, but includes extra letters,
219             these will be assumed to be further digits after the decimal point.
220              
221             If you supply the 'signed' option with a true value, the returned values are
222             signed numbers rather than numbers with trailing sign indicators.
223              
224             =cut
225              
226             sub from_whitwell {
227 10     10 1 5873 my($name, %opts) = @_;
228 10         28 my ($lat_name, $lon_name) = split(/\s+/, $name);
229              
230 10         13 my ($value, $negative);
231 10         24 ($value, $negative) = _coord_for(lc($lat_name));
232 10 100       22 if ($negative) {
233 5 100       12 if ($opts{signed}) {
234 2         4 $value = -$value;
235             }
236             else {
237 3         13 $value .= "S";
238             }
239             }
240             else {
241 5 100       11 unless ($opts{signed}) {
242 3         22 $value .= "N";
243             }
244             }
245 10         14 my $lat = $value;
246              
247 10         22 ($value, $negative) = _coord_for(lc($lon_name));
248 10 100       22 if ($negative) {
249 4 100       9 if ($opts{signed}) {
250 2         4 $value = -$value;
251             }
252             else {
253 2         7 $value .= "W";
254             }
255             }
256             else {
257 6 100       11 unless ($opts{signed}) {
258 4         17 $value .= "E";
259             }
260             }
261 10         14 my $lon = $value;
262              
263 10         32 return ($lat, $lon);
264            
265             }
266              
267             sub _coord_for {
268 27     27   6285 my($original) = my($string) = @_;
269              
270             # Determine if the string starts in the vowel table or the consonant table.
271 27         47 my @tables = (\@consonants, \@vowels);
272 27         32 my $vowel_found;
273 27   100     105 my $current = ($string =~ /^[aeiouy]/) || 0;
274              
275             # Decompose and look up the character(s).
276 27         26 my $coord_string;
277 27         29 my $try_sign = 0;
278 27         32 my $is_negative = 0;
279 27         31 my $sign_checked = 0;
280              
281             PARSE:
282 27         49 while ($string) {
283             # If we need to look for the sign character,
284             # do so. Since we've allowed names to start in either table
285             # as seems to have been the historical precedent (yes, someone
286             # actually did use this at least once for a real placename),
287             # we check for both sign characters and record whether or not
288             # we found one.
289 115 100       199 if ($try_sign) {
290             # Don't try more than once.
291 35         37 $try_sign = 0;
292 35 100       83 if ($string =~ s/^[vs]//) {
293 11         12 $is_negative = 1;
294             # Return to the vowel table again.
295 11         10 $current = 1;
296 11         24 next PARSE;
297             }
298             # Note we've looked for the sign once, so we shouldn't look
299             # again. This wil trap badly-placed sign characters.
300 24         26 $sign_checked = 1;
301             }
302             # Longer entries occur at the end of the vowel table, so
303             # to avoid parsing 'ee' as 'e' and 'e', we try the longer
304             # strings first. However: complicating this process is the '0'
305             # entry, which is also a longer one, so it has to be checked first.
306 104         149 for my $i (0, reverse 1..9) {
307 658         815 my $char = $tables[$current]->[$i];
308 658 100       4834 if ($string =~ s/^$char//) {
309             # Found it. Tack the number onto the coordinate string,
310             # swap tables, and see if we need to check the sign.
311 102         172 $coord_string .= $i;
312 102   100     814 $try_sign = ($current == 1 and !$sign_checked);
313 102         108 $current = !$current;
314 102         290 next PARSE;
315             }
316             }
317             # The current table should have matched, so the input string is bad.
318 2         32 croak "Bad character or sequencing found in '$original' at '$string'";
319             }
320             # Insert the decimal point such that the resulting number is < 180.
321             # This allows "high-precision" Whitwell names (constructed in some
322             # manner other than via to_whitwell) to be converted back correctly.
323 25 100       49 if (length($coord_string) >= 3) {
324             # Need to insert a decimal point. The final value must be < 180,
325             # and we asssume at least two decimal places.
326              
327             # Let's try the easy case first, and insert a decimal point
328             # right before the last two digits. All names generated via
329             # to_whitwell() will work with this case. Since we know the
330             # coordinate string only has numbers in it, we can just divide
331             # by 100.
332 24         70 my $trial_value = $coord_string/100;
333              
334             # Manufactured by some other means. Move the decimal left one
335             # character at a time until the number is < 180. We never do this
336             # at all if our initial guess worked.
337 24         54 $trial_value /= 10 while $trial_value > 180;
338 24         32 $coord_string = $trial_value;
339             }
340             else {
341             # < 3, so can't be > 180. Just add decimals.
342 1         3 $coord_string .= ".00";
343             }
344 25         71 return ($coord_string, $is_negative);
345             }
346              
347             =head1 AUTHOR
348              
349             Joe McMahon, C<< >>
350              
351             =head1 BUGS
352              
353             Please report any bugs or feature requests to C
354             rt.cpan.org>, or through the web interface at
355             L. I
356             will be notified, and then you'll automatically be notified of progress on your
357             bug as I make changes.
358              
359             =head2 KNOWN BUGS
360              
361             =over
362              
363             =item * (0,0) isn't handled correctly; however, since there's nothing there
364             but water, this is not a practical limitation.
365              
366             =back
367              
368             =head1 SUPPORT
369              
370             You can find documentation for this module with the perldoc command.
371              
372             perldoc Acme::Geo::Whitwell::Name
373              
374              
375             You can also look for information at:
376              
377             =over 4
378              
379             =item * RT: CPAN's request tracker
380              
381             L
382              
383             =item * AnnoCPAN: Annotated CPAN documentation
384              
385             L
386              
387             =item * CPAN Ratings
388              
389             L
390              
391             =item * Search CPAN
392              
393             L
394              
395             =back
396              
397              
398             =head1 ACKNOWLEDGEMENTS
399              
400              
401             =head1 COPYRIGHT & LICENSE
402              
403             Copyright 2008 Joe McMahon, all rights reserved.
404              
405             This program is free software; you can redistribute it and/or modify it
406             under the same terms as Perl itself.
407              
408              
409             =cut
410              
411             1; # End of Acme::Geo::Whitwell::Name