File Coverage

blib/lib/Geo/Coordinates/UTM.pm
Criterion Covered Total %
statement 180 182 98.9
branch 96 118 81.3
condition 69 102 67.6
subroutine 18 19 94.7
pod 9 9 100.0
total 372 430 86.5


line stmt bran cond sub pod time code
1 3     3   69556 use strict;
  3         6  
  3         123  
2 3     3   16 use warnings;
  3         6  
  3         122  
3              
4             package Geo::Coordinates::UTM;
5              
6 3     3   17 use Carp;
  3         9  
  3         367  
7              
8 3     3   16 use base 'Exporter';
  3         6  
  3         477  
9              
10             our @EXPORT = qw( latlon_to_utm latlon_to_utm_force_zone
11             utm_to_latlon utm_to_mgrs
12             latlon_to_mgrs mgrs_to_utm mgrs_to_latlon
13             ellipsoid_info ellipsoid_names );
14              
15             our $VERSION = '0.11';
16              
17 3     3   2974 use Math::Trig;
  3         85207  
  3         1552  
18             my $deg2rad = pi / 180;
19             my $rad2deg = 180 / pi;
20              
21             # remove all markup from an ellipsoid name, to increase the chance
22             # that a match is found.
23             sub _cleanup_name($)
24 355     355   934 { my $copy = lc(shift);
25 355         802 for($copy)
26 355         789 { s/\([^)]+\)//g; # remove text between parantheses
27 355         2568 s/[\s-]//g; # no blanks or dashes
28             }
29 355         19168 $copy;
30             }
31              
32             # Ellipsoid array (name,equatorial radius,square of eccentricity)
33             # Same data also as hash with key eq name (in variations)
34              
35             my (@Ellipsoid, %Ellipsoid);
36              
37              
38             BEGIN { # Initialize this before other modules get a chance
39 3     3   86 @Ellipsoid =
40             ( [ "Airy", 6377563, 0.00667054]
41             , [ "Australian National", 6378160, 0.006694542]
42             , [ "Bessel 1841", 6377397, 0.006674372]
43             , [ "Bessel 1841 Nambia", 6377484, 0.006674372]
44             , [ "Clarke 1866", 6378206, 0.006768658]
45             , [ "Clarke 1880", 6378249, 0.006803511]
46             , [ "Everest 1830 India", 6377276, 0.006637847]
47             , [ "Fischer 1960 Mercury", 6378166, 0.006693422]
48             , [ "Fischer 1968", 6378150, 0.006693422]
49             , [ "GRS 1967", 6378160, 0.006694605]
50             , [ "GRS 1980", 6378137, 0.00669438]
51             , [ "Helmert 1906", 6378200, 0.006693422]
52             , [ "Hough", 6378270, 0.00672267]
53             , [ "International", 6378388, 0.00672267]
54             , [ "Krassovsky", 6378245, 0.006693422]
55             , [ "Modified Airy", 6377340, 0.00667054]
56             , [ "Modified Everest", 6377304, 0.006637847]
57             , [ "Modified Fischer 1960", 6378155, 0.006693422]
58             , [ "South American 1969", 6378160, 0.006694542]
59             , [ "WGS 60", 6378165, 0.006693422]
60             , [ "WGS 66", 6378145, 0.006694542]
61             , [ "WGS-72", 6378135, 0.006694318]
62             , [ "WGS-84", 6378137, 0.00669438 ]
63             , [ "Everest 1830 Malaysia", 6377299, 0.006637847]
64             , [ "Everest 1956 India", 6377301, 0.006637847]
65             , [ "Everest 1964 Malaysia and Singapore", 6377304, 0.006637847]
66             , [ "Everest 1969 Malaysia", 6377296, 0.006637847]
67             , [ "Everest Pakistan", 6377296, 0.006637534]
68             , [ "Indonesian 1974", 6378160, 0.006694609]
69             , [ "Arc 1950", 6378249.145,0.006803481]
70             , [ "NAD 27",6378206.4,0.006768658]
71             , [ "NAD 83",6378137,0.006694384]
72             );
73              
74             # calc ecc as
75             # a = semi major axis
76             # b = semi minor axis
77             # e^2 = (a^2-b^2)/a^2
78             # For clarke 1880 (Arc1950) a=6378249.145 b=6356514.966398753
79             # e^2 (40682062155693.23 - 40405282518051.34) / 40682062155693.23
80             # e^2 = 0.0068034810178165
81            
82              
83 3         12 foreach my $el (@Ellipsoid)
84 96         133 { my ($name, $eqrad, $eccsq) = @$el;
85 96         195 $Ellipsoid{$name} = $el;
86 96         144 $Ellipsoid{_cleanup_name $name} = $el;
87             }
88             }
89              
90             sub _valid_utm_zone($)
91 1117     1117   2686 { my $char = shift;
92 1117         4781 index("CDEFGHJKLMNPQRSTUVWX", $char) >= 0;
93             }
94              
95             # Returns all pre-defined ellipsoid names, sorted alphabetically
96             sub ellipsoid_names()
97 0     0 1 0 { map { $_->[0] } @Ellipsoid;
  0         0  
98             }
99              
100             # Returns "official" name, equator radius and square eccentricity
101             # The specified name can be numeric (for compatibility reasons) or
102             # a more-or-less exact name
103             # Examples: my($name, $r, $sqecc) = ellipsoid_info 'wgs84';
104             # my($name, $r, $sqecc) = ellipsoid_info 'WGS 84';
105             # my($name, $r, $sqecc) = ellipsoid_info 'WGS-84';
106             # my($name, $r, $sqecc) = ellipsoid_info 'WGS-84 (new specs)';
107             # my($name, $r, $sqecc) = ellipsoid_info 22;
108              
109             sub ellipsoid_info($)
110 3077     3077 1 5182 { my $id = shift;
111              
112 3077 100 66     19992 my $el = $id !~ m/\D/
113             ? $Ellipsoid[$id-1] # old system counted from 1
114             : $Ellipsoid{$id} || $Ellipsoid{_cleanup_name $id};
115              
116 3077 50       18334 defined $el ? @$el : ();
117             }
118              
119             # Expects Ellipsoid Number or name, Latitude, Longitude
120             # (Latitude and Longitude in decimal degrees)
121             # Returns UTM Zone, UTM Easting, UTM Northing
122              
123             sub latlon_to_utm($$$)
124 1039     1039 1 1915784 { my ($ellips, $latitude, $longitude) = @_;
125              
126 1039 50 33     16523 croak "Longitude value ($longitude) invalid."
127             if $longitude < -180 || $longitude > 180;
128              
129 1039         4238 my $long2 = $longitude - int(($longitude + 180)/360) * 360;
130 1039         3176 my $zone = _latlon_zone_number($latitude, $long2);
131              
132 1039         2785 _latlon_to_utm($ellips, $zone, $latitude, $long2);
133             }
134              
135             sub latlon_to_utm_force_zone($$$$)
136 999     999 1 1033071 { my ($ellips, $zone, $latitude, $longitude) = @_;
137              
138 999 50 33     17690 croak "Longitude value ($longitude) invalid."
139             if $longitude < -180 || $longitude > 180;
140              
141 999         4163 my $long2 = $longitude - int(($longitude + 180)/360) * 360;
142              
143 999         6464 my ($zone_number) = $zone =~ /^(\d+)[CDEFGHJKLMNPQRSTUVWX]?$/i;
144              
145 999 50 33     8568 croak "Zone value ($zone) invalid."
146             unless defined($zone_number) && $zone_number <= 60;
147              
148 999         16342 _latlon_to_utm($ellips, $zone_number, $latitude, $long2);
149             }
150              
151             sub _latlon_zone_number
152 1039     1039   2107 { my ($latitude, $long2) = @_;
153              
154 1039         2525 my $zone = int( ($long2 + 180)/6) + 1;
155 1039 100 100     5347 if($latitude >= 56.0 && $latitude < 64.0 && $long2 >= 3.0 && $long2 < 12.0)
      100        
      100        
156 1         3 { $zone = 32;
157             }
158 1039 100 66     31771 if($latitude >= 72.0 && $latitude < 84.0) {
159 82 100 100     5657 $zone = ($long2 >= 0.0 && $long2 < 9.0) ? 31
    100 100        
    100 100        
    100 100        
160             : ($long2 >= 9.0 && $long2 < 21.0) ? 33
161             : ($long2 >= 21.0 && $long2 < 33.0) ? 35
162             : ($long2 >= 33.0 && $long2 < 42.0) ? 37
163             : $zone;
164             }
165 1039         2653 return $zone;
166             }
167              
168             sub _latlon_to_utm
169 2038     2038   3999 { my ($ellips, $zone, $latitude, $long2) = @_;
170              
171 2038 50       4623 my ($name, $radius, $eccentricity) = ellipsoid_info $ellips
172             or croak "Ellipsoid value ($ellips) invalid.";
173              
174 2038         5192 my $lat_radian = $deg2rad * $latitude;
175 2038         4023 my $long_radian = $deg2rad * $long2;
176              
177 2038         2897 my $k0 = 0.9996; # scale
178              
179 2038         4738 my $longorigin = ($zone - 1)*6 - 180 + 3;
180 2038         3190 my $longoriginradian = $deg2rad * $longorigin;
181 2038         3356 my $eccentprime = $eccentricity/(1-$eccentricity);
182            
183 2038         7397 my $N = $radius / sqrt(1-$eccentricity * sin($lat_radian)*sin($lat_radian));
184 2038         8509 my $T = tan($lat_radian) * tan($lat_radian);
185 2038         94731 my $C = $eccentprime * cos($lat_radian)*cos($lat_radian);
186 2038         4026 my $A = cos($lat_radian) * ($long_radian - $longoriginradian);
187 2038         14580 my $M = $radius
188             * ( ( 1 - $eccentricity/4 - 3 * $eccentricity * $eccentricity/64
189             - 5 * $eccentricity * $eccentricity * $eccentricity/256
190             ) * $lat_radian
191             - ( 3 * $eccentricity/8 + 3 * $eccentricity * $eccentricity/32
192             + 45 * $eccentricity * $eccentricity * $eccentricity/1024
193             ) * sin(2 * $lat_radian)
194             + ( 15 * $eccentricity * $eccentricity/256 +
195             45 * $eccentricity * $eccentricity * $eccentricity/1024
196             ) * sin(4 * $lat_radian)
197             - ( 35 * $eccentricity * $eccentricity * $eccentricity/3072
198             ) * sin(6 * $lat_radian)
199             );
200              
201 2038         9969 my $utm_easting = $k0*$N*($A+(1-$T+$C)*$A*$A*$A/6
202             + (5-18*$T+$T*$T+72*$C-58*$eccentprime)*$A*$A*$A*$A*$A/120)
203             + 500000.0;
204              
205 2038         6736 my $utm_northing= $k0 * ( $M + $N*tan($lat_radian) * ( $A*$A/2+(5-$T+9*$C+4*$C*$C)*$A*$A*$A*$A/24 + (61-58*$T+$T*$T+600*$C-330*$eccentprime) * $A*$A*$A*$A*$A*$A/720));
206              
207 2038 100       39626 $utm_northing += 10000000.0 if $latitude < 0;
208              
209 2038 50 66     75546 my $utm_letter
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 66        
    100 33        
210             = ( 84 >= $latitude && $latitude >= 72) ? 'X'
211             : ( 72 > $latitude && $latitude >= 64) ? 'W'
212             : ( 64 > $latitude && $latitude >= 56) ? 'V'
213             : ( 56 > $latitude && $latitude >= 48) ? 'U'
214             : ( 48 > $latitude && $latitude >= 40) ? 'T'
215             : ( 40 > $latitude && $latitude >= 32) ? 'S'
216             : ( 32 > $latitude && $latitude >= 24) ? 'R'
217             : ( 24 > $latitude && $latitude >= 16) ? 'Q'
218             : ( 16 > $latitude && $latitude >= 8) ? 'P'
219             : ( 8 > $latitude && $latitude >= 0) ? 'N'
220             : ( 0 > $latitude && $latitude >= -8) ? 'M'
221             : ( -8 > $latitude && $latitude >= -16) ? 'L'
222             : (-16 > $latitude && $latitude >= -24) ? 'K'
223             : (-24 > $latitude && $latitude >= -32) ? 'J'
224             : (-32 > $latitude && $latitude >= -40) ? 'H'
225             : (-40 > $latitude && $latitude >= -48) ? 'G'
226             : (-48 > $latitude && $latitude >= -56) ? 'F'
227             : (-56 > $latitude && $latitude >= -64) ? 'E'
228             : (-64 > $latitude && $latitude >= -72) ? 'D'
229             : (-72 > $latitude && $latitude >= -80) ? 'C'
230             : croak "Latitude ($latitude) out of UTM range.";
231              
232 2038         4265 $zone .= $utm_letter;
233              
234 2038         14167 ($zone, $utm_easting, $utm_northing);
235             }
236              
237             # Expects Ellipsoid Number or name, UTM zone, UTM Easting, UTM Northing
238             # Returns Latitude, Longitude
239             # (Latitude and Longitude in decimal degrees, UTM Zone e.g. 23S)
240              
241             sub utm_to_latlon($$$$)
242 1039     1039 1 1797061 { my ($ellips, $zone, $easting, $northing) = @_;
243              
244 1039 50       2878 my ($name, $radius, $eccentricity) = ellipsoid_info $ellips
245             or croak "Ellipsoid value ($ellips) invalid.";
246            
247 1039         2532 my $zone_number = $zone;
248 1039         2064 my $zone_letter = chop $zone_number;
249              
250 1039 50       3659 croak "UTM zone ($zone_letter) invalid."
251             unless _valid_utm_zone $zone_letter;
252              
253 1039         1902 my $k0 = 0.9996;
254 1039         1971 my $x = $easting - 500000; # Remove Longitude offset
255 1039         4065 my $y = $northing;
256              
257             # Set hemisphere (1=Northern, 0=Southern)
258 1039         2272 my $hemisphere = $zone_letter ge 'N';
259 1039 100       2557 $y -= 10000000.0 unless $hemisphere; # Remove Southern Offset
260              
261 1039         3270 my $longorigin = ($zone_number - 1)*6 - 180 + 3;
262 1039         2097 my $eccPrimeSquared = ($eccentricity)/(1-$eccentricity);
263 1039         1592 my $M = $y/$k0;
264 1039         3470 my $mu = $M/($radius*(1-$eccentricity/4-3*$eccentricity*$eccentricity/64-5*$eccentricity*$eccentricity*$eccentricity/256));
265              
266 1039         3117 my $e1 = (1-sqrt(1-$eccentricity))/(1+sqrt(1-$eccentricity));
267 1039         9301 my $phi1rad = $mu+(3*$e1/2-27*$e1*$e1*$e1/32)*sin(2*$mu)+(21*$e1*$e1/16-55*$e1*$e1*$e1*$e1/32)*sin(4*$mu)+(151*$e1*$e1*$e1/96)*sin(6*$mu);
268 1039         1553 my $phi1 = $phi1rad*$rad2deg;
269 1039         2726 my $N1 = $radius/sqrt(1-$eccentricity*sin($phi1rad)*sin($phi1rad));
270 1039         3515 my $T1 = tan($phi1rad)*tan($phi1rad);
271 1039         29814 my $C1 = $eccentricity*cos($phi1rad)*cos($phi1rad);
272 1039         18561 my $R1 = $radius * (1-$eccentricity)
273             / ((1-$eccentricity*sin($phi1rad)*sin($phi1rad))**1.5);
274 1039         1834 my $D = $x/($N1*$k0);
275              
276 1039         3732 my $Latitude = $phi1rad-($N1*tan($phi1rad)/$R1)*($D*$D/2-(5+3*$T1+10*$C1-4*$C1*$C1-9*$eccPrimeSquared)*$D*$D*$D*$D/24+(61+90*$T1+298*$C1+45*$T1*$T1-252*$eccPrimeSquared-3*$C1*$C1)*$D*$D*$D*$D*$D*$D/720);
277 1039         22598 $Latitude = $Latitude * $rad2deg;
278              
279 1039         5872 my $Longitude = ($D-(1+2*$T1+$C1)*$D*$D*$D/6+(5-2*$C1+28*$T1-3*$C1*$C1+8*$eccPrimeSquared+24*$T1*$T1)*$D*$D*$D*$D*$D/120)/cos($phi1rad);
280 1039         1574 $Longitude = $longorigin + $Longitude * $rad2deg;
281              
282 1039         5351 ($Latitude, $Longitude);
283             }
284              
285             sub utm_to_mgrs($$$)
286 39     39 1 49 { my ($zone,$easting,$northing) = @_;
287 39         43 my $zone_number = $zone;
288 39         52 my $zone_letter = chop $zone_number;
289              
290 39 50       64 croak "UTM zone ($zone_letter) invalid."
291             unless _valid_utm_zone $zone_letter;
292              
293 39         84 my $northing_zones="ABCDEFGHJKLMNPQRSTUV";
294 39         104 my $rnd_north=sprintf("%d",$northing);
295 39         53 my $north_split=length($rnd_north)-5;
296 39 50       71 $north_split=0 if $north_split < 0;
297 39         65 my $mgrs_north=substr($rnd_north,(length($rnd_north)-5));
298 39         182 $rnd_north -=2000000 while ($rnd_north >= 2000000);
299 39 50       70 $rnd_north+=2000000 if $rnd_north < 0;
300 39         53 my $num_north=int($rnd_north/100000);
301 39 100       87 $num_north+=5 if not ($zone_number % 2);
302 39         150 $num_north-=20 until $num_north <20;
303 39         55 my $lett_north=substr($northing_zones,$num_north,1);
304              
305 39         73 my $rnd_east=sprintf("%d",$easting);
306 39         47 my $east_split=length($rnd_east)-5;
307 39 50       67 $east_split=0 if $east_split < 0;
308 39         54 my $mgrs_east=substr($rnd_east,(length($rnd_east)-5));
309 39         90 my $num_east=substr($rnd_east,0,(length($rnd_east)-5));
310 39 50       61 $num_east=0 if not $num_east;
311 39         42 my $mgrs_zone=$zone_number;
312 39         338 $mgrs_zone-=3 until $mgrs_zone < 4;
313             # zones are 6deg wide, mgrs letters are 18deg = 8 per zone
314             # calculate which zone required
315 39 50       92 my $easting_zones
    100          
    100          
316             = ( $mgrs_zone == 1) ? 'ABCDEFGH'
317             : ( $mgrs_zone == 2) ? 'JKLMNPQR'
318             : ( $mgrs_zone == 3) ? 'STUVWXYZ'
319             : croak "Could not calculate MGRS zone.";
320 39         48 $num_east--;
321 39 50       92 my $lett_east=substr($easting_zones,$num_east,1) or croak "Could not detect Easting Zone for MGRS coordinate";
322              
323 39         67 my $MGRS="$zone$lett_east$lett_north$mgrs_east$mgrs_north";
324 39         158 ($MGRS);
325             }
326              
327             sub latlon_to_mgrs($$$)
328 39     39 1 22505 { my ($ellips, $latitude, $longitude) = @_;
329 39         79 my ($zone,$x_coord,$y_coord)=latlon_to_utm($ellips, $latitude, $longitude);
330 39         78 my $mgrs_string=utm_to_mgrs($zone,$x_coord,$y_coord);
331 39         93 ($mgrs_string);
332             }
333              
334              
335             sub mgrs_to_utm($)
336 39     39 1 50 { my ($mgrs_string) = @_;
337 39         66 my $zone = substr($mgrs_string,0,2);
338 39 100       208 $mgrs_string = "0$mgrs_string" if !($zone =~ /^\d+$/);
339 39         76 $zone = substr($mgrs_string,0,3);
340 39         44 my $zone_number = $zone;
341 39         62 my $zone_letter = chop $zone_number;
342              
343 39 50       59 croak "UTM zone ($zone_letter) invalid."
344             unless _valid_utm_zone $zone_letter;
345              
346 39         70 my $first_letter = substr($mgrs_string,3,1);
347 39 50       122 croak "MGRS zone ($first_letter) invalid."
348             unless $first_letter =~ /[ABCDEFGHJKLMNPQRSTUVWXYZ]/;
349              
350 39         53 my $second_letter = substr($mgrs_string,4,1);
351 39 50       97 croak "MGRS zone ($second_letter) invalid."
352             unless $second_letter =~ /[ABCDEFGHJKLMNPQRSTUV]/;
353              
354 39         60 my $coords=substr($mgrs_string,5);
355 39         52 my $coord_len=length($coords);
356 39 50 33     362 croak "MGRS coords ($coords) invalid."
      33        
357             unless ((($coord_len > 0) and ($coord_len <= 10)) and !($coord_len % 2));
358            
359 39         57 $coord_len=int($coord_len/2);
360 39         53 my $x_coord=substr($coords,0,$coord_len);
361 39         46 my $y_coord=substr($coords,$coord_len);
362 39         75 $x_coord*=10 ** (5 - $coord_len);
363 39         48 $y_coord*=10 ** (5 - $coord_len);
364              
365 39 50       241 my $east_pos
    100          
    100          
366             = ( $first_letter =~ /[ABCDEFGH]/) ? index('ABCDEFGH',$first_letter)
367             : ( $first_letter =~ /[JKLMNPQR]/) ? index('JKLMNPQR',$first_letter)
368             : ( $first_letter =~ /[STUVWXYZ]/) ? index('STUVWXYZ',$first_letter)
369             : croak "Could not calculate MGRS Easting zone.";
370 39 50       84 croak "MGRS Letter $first_letter invalid." if $east_pos < 0;
371 39         35 $east_pos++;
372 39         39 $east_pos*=100000;
373 39         44 $x_coord+=$east_pos;
374              
375 39         40 my $northing_zones="ABCDEFGHJKLMNPQRSTUV";
376 39         51 my $north_pos=index($northing_zones,$second_letter);
377 39 50       67 croak "MGRS Letter $second_letter invalid." if $north_pos < 0;
378 39         42 $north_pos++;
379 39 100       87 $north_pos-=5 if not ($zone_number % 2);
380 39         81 $north_pos+=20 until $north_pos > 0;
381 39 100       83 if ($zone_letter =~ /[NPQRSTUVWX]/) {
382             # Northern hemisphere
383 21         34 my $tmpNorth=index('NPQRSTUVWX',$zone_letter);
384 21         21 $tmpNorth++;
385 21         91 $tmpNorth*=8;
386 21         23 $tmpNorth*=10/9;
387 21         46 $tmpNorth=int((($tmpNorth-$north_pos)/20)+0.5)*20;
388 21         22 $north_pos+=$tmpNorth;
389 21         22 $north_pos*=100000;
390 21         18 $north_pos-=100000;
391 21         28 $y_coord+=$north_pos;
392             }
393             else {
394             #Southern Hemisphere
395 18         27 my $tmpNorth=index('CDEFGHJKLM',$zone_letter);
396 18         19 $tmpNorth*=8;
397 18         18 $tmpNorth*=10/9;
398 18         36 $tmpNorth=int((($tmpNorth-$north_pos)/20)+0.5)*20;
399 18         19 $north_pos+=$tmpNorth;
400 18         25 $north_pos*=100000;
401 18         19 $north_pos-=100000;
402 18 100       42 $north_pos+=2000000 if $zone_letter ne "C";
403 18         21 $y_coord+=$north_pos;
404             }
405              
406 39         133 ($zone,$x_coord,$y_coord);
407             }
408              
409             sub mgrs_to_latlon($$)
410 39     39 1 23834 { my ($ellips, $mgrs_string) = @_;
411 39         70 my ($zone,$x_coord,$y_coord)=mgrs_to_utm($mgrs_string);
412 39         84 my ($latitude,$longitude)=utm_to_latlon($ellips,$zone,$x_coord,$y_coord);
413 39         155 ($latitude,$longitude);
414             }
415              
416             1;
417              
418             __END__