File Coverage

blib/lib/Geo/Ellipsoid.pm
Criterion Covered Total %
statement 322 357 90.2
branch 83 134 61.9
condition 4 6 66.6
subroutine 39 39 100.0
pod 26 26 100.0
total 474 562 84.3


line stmt bran cond sub pod time code
1             # Geo::Ellipsoid
2             #
3             # This package implements an Ellipsoid class to perform latitude
4             # and longitude calculations on the surface of an ellipsoid.
5             #
6             # This is a Perl conversion of existing Fortran code (see
7             # ACKNOWLEDGEMENTS) and the author of this class makes no
8             # claims of originality. Nor can he even vouch for the
9             # results of the calculations, although they do seem to
10             # work for him and have been tested against other methods.
11              
12             package Geo::Ellipsoid;
13              
14 23     23   1680184 use warnings;
  23         246  
  23         760  
15 23     23   125 use strict;
  23         53  
  23         459  
16 23     23   475 use 5.006_00;
  23         99  
17              
18 23     23   153 use Scalar::Util 'looks_like_number';
  23         53  
  23         1403  
19 23     23   13227 use Math::Trig 1.23;
  23         372834  
  23         3203  
20 23     23   207 use Carp;
  23         46  
  23         111403  
21              
22             =pod
23              
24             =head1 NAME
25              
26             Geo::Ellipsoid - longitude and latitude calculations using an ellipsoid model
27              
28             =head1 VERSION
29              
30             Version 1.15.
31              
32             =cut
33              
34             our $VERSION = '1.15';
35             our $DEBUG = 0;
36              
37             =pod
38              
39             =head1 SYNOPSIS
40              
41             use Geo::Ellipsoid;
42              
43             my $geo = Geo::Ellipsoid->new(ellipsoid => 'NAD27',
44             angle_unit => 'degrees');
45              
46             my @origin = ( 37.619002, -122.374843 ); # SFO
47             my @dest = ( 33.942536, -118.408074 ); # LAX
48              
49             # range and bearing from one location to another
50              
51             my ( $range, $bearing ) = $geo->to( @origin, @dest );
52             $range = $geo->range( @origin, @dest );
53             $bearing = $geo->bearing( @origin, @dest );
54              
55             # destination given start point, range, and bearing
56              
57             my ( $lat, $lon ) = $geo->at( @origin, 2000, 45.0 );
58              
59             # approximate displacement given one location and a destination
60              
61             my ( $x, $y ) = $geo->displacement( @origin, @dest );
62              
63             # approximate location given one location and displacement
64              
65             my @pos = $geo->location( $lat, $lon, $x, $y );
66              
67             =head1 ABSTRACT
68              
69             Accurate latitude/longitude calculations.
70              
71             =head1 DESCRIPTION
72              
73             Geo::Ellipsoid performs geometrical calculations on the surface of
74             an ellipsoid. An ellipsoid is a three-dimension object formed from
75             the rotation of an ellipse about one of its axes. The approximate
76             shape of the earth is an ellipsoid, so Geo::Ellipsoid can accurately
77             calculate distance and bearing between two widely-separated locations
78             on the earth's surface.
79              
80             The shape of an ellipsoid is defined by the lengths of its
81             semi-major and semi-minor axes. The shape may also be specified by
82             the flattening ratio C as:
83              
84             f = ( semi-major - semi-minor ) / semi-major
85              
86             which, since f is a small number, is normally given as the reciprocal
87             of the flattening C<1/f>.
88              
89             The shape of the earth has been surveyed and estimated differently
90             at different times over the years. The two most common sets of values
91             used to describe the size and shape of the earth in the United States
92             are 'NAD27', dating from 1927, and 'WGS84', from 1984. United States
93             Geological Survey topographical maps, for example, use one or the
94             other of these values, and commonly-available Global Positioning
95             System (GPS) units can be set to use one or the other.
96             See L<"DEFINED ELLIPSOIDS"> below for the ellipsoid survey values
97             that may be selected for use by Geo::Ellipsoid.
98              
99             =cut
100              
101             # class data and constants
102             our $degrees_per_radian = 180/pi;
103             our $eps = 1.0e-23;
104             our $max_loop_count = 20;
105             our $twopi = 2 * pi;
106             our $halfpi = pi/2;
107             our %defaults = (
108             ellipsoid => 'WGS84',
109             angle_unit => 'radians',
110             distance_unit => 'meter',
111             longitude_symmetric => 0,
112             latitude_symmetric => 1, # allows use of _normalize_output
113             bearing_symmetric => 0,
114             );
115             our %distance = (
116             'foot' => 0.3048,
117             'kilometer' => 1_000,
118             'meter' => 1.0,
119             'mile' => 1_609.344,
120             'nm' => 1_852,
121             );
122              
123             # set of ellipsoids that can be used.
124             # values are
125             # 1) a = semi-major (equatorial) radius of Ellipsoid
126             # 2) 1/f = reciprocal of flattening (f), the ratio of the semi-minor
127             # (polar) radius to the semi-major (equatorial) axis, or
128             # polar radius = equatorial radius * ( 1 - f )
129              
130             our %ellipsoids = (
131             'AIRY' => [ 6377563.396, 299.3249646 ],
132             'AIRY-MODIFIED' => [ 6377340.189, 299.3249646 ],
133             'AUSTRALIAN' => [ 6378160.0, 298.25 ],
134             'BESSEL-1841' => [ 6377397.155, 299.1528128 ],
135             'CLARKE-1880' => [ 6378249.145, 293.465 ],
136             'EVEREST-1830' => [ 6377276.345, 300.8017 ],
137             'EVEREST-MODIFIED' => [ 6377304.063, 300.8017 ],
138             'FISHER-1960' => [ 6378166.0, 298.3 ],
139             'FISHER-1968' => [ 6378150.0, 298.3 ],
140             'GRS80' => [ 6378137.0, 298.25722210088 ],
141             'HOUGH-1956' => [ 6378270.0, 297.0 ],
142             'HAYFORD' => [ 6378388.0, 297.0 ],
143             'IAU76' => [ 6378140.0, 298.257 ],
144             'KRASSOVSKY-1938' => [ 6378245.0, 298.3 ],
145             'NAD27' => [ 6378206.4, 294.9786982138 ],
146             'NWL-9D' => [ 6378145.0, 298.25 ],
147             'SOUTHAMERICAN-1969' => [ 6378160.0, 298.25 ],
148             'SOVIET-1985' => [ 6378136.0, 298.257 ],
149             'WGS72' => [ 6378135.0, 298.26 ],
150             'WGS84' => [ 6378137.0, 298.257223563 ],
151             );
152              
153             =pod
154              
155             =head1 CONSTRUCTOR
156              
157             =over
158              
159             =item new
160              
161             The new() constructor may be called with a hash list to set the value of the
162             ellipsoid to be used, the value of the units to be used for angles and
163             distances, and whether or not the output range of longitudes and bearing
164             angles should be symmetric around zero or always greater than zero.
165             The initial default constructor is equivalent to the following:
166              
167             my $geo = Geo::Ellipsoid->new(
168             ellipsoid => 'WGS84',
169             angle_unit => 'radians' ,
170             distance_unit => 'meter',
171             longitude_symmetric => 0,
172             bearing_symmetric => 0,
173             );
174              
175             The constructor arguments may be of any case and, with the exception of
176             the ellipsoid value, abbreviated to their first three characters.
177             Thus, ( UNI => 'DEG', DIS => 'FEE', Lon => 1, ell => 'NAD27', bEA => 0 )
178             is valid.
179              
180             =cut
181              
182             sub new
183             {
184 231     231 1 183878 my( $class, %args ) = @_;
185 231         1152 my $self = {%defaults};
186 231 50       699 print "new: @_\n" if $DEBUG;
187 231         780 while (my ($key, $val) = each %args) {
188 148 100       770 if( $key =~ /^ell/i ) {
    100          
    100          
    100          
    50          
189 105         457 $self->{ellipsoid} = uc $val;
190             }elsif( $key =~ /^(uni|ang)/i ) {
191 21         94 $self->{angle_unit} = $val;
192             }elsif( $key =~ /^dis/i ) {
193 12         45 $self->{distance_unit} = $val;
194             }elsif( $key =~ /^lon/i ) {
195 6         29 $self->{longitude_symmetric} = $val;
196             }elsif( $key =~ /^bea/i ) {
197 4         34 $self->{bearing_symmetric} = $val;
198             }else{
199 0         0 carp("Unknown argument: $key => $val");
200             }
201             }
202 231         470 bless $self, $class;
203 231         922 $self->set_ellipsoid($self->{ellipsoid});
204 231         671 $self->set_units($self->{angle_unit});
205 231         558 $self->set_distance_unit($self->{distance_unit});
206 231         626 $self->set_longitude_symmetric($self->{longitude_symmetric});
207 231         658 $self->set_bearing_symmetric($self->{bearing_symmetric});
208 231 50       461 print
209             "Ellipsoid(angle_unit=>$self->{angle_unit},distance_unit=>" .
210             "$self->{distance_unit},ellipsoid=>$self->{ellipsoid}," .
211             "longitude_symmetric=>$self->{longitude_symmetric},bearing_symmetric=>$self->{bearing_symmetric})\n" if $DEBUG;
212 231         705 return $self;
213             }
214              
215             =pod
216              
217             =back
218              
219             =head1 CLASS METHODS
220              
221             =over
222              
223             =item get_ellipsoids
224              
225             Returns a list with the names all known ellipsoids.
226              
227             =back
228              
229             =cut
230              
231             sub get_ellipsoids {
232 1     1 1 695 sort keys %ellipsoids;
233             }
234              
235             =pod
236              
237             =head1 INSTANCE METHODS
238              
239             =head2 Setters
240              
241             =over
242              
243             =item set_angle_unit
244              
245             =item set_units
246              
247             Set the angle unit used by the Geo::Ellipsoid object. The unit may
248             also be set in the constructor of the object. The allowable values are
249             'degrees' or 'radians'. The default is 'radians'. The unit value is
250             not case sensitive and may be abbreviated to 3 letters. The unit of
251             angle apply to both input and output latitude, longitude, and bearing
252             values.
253              
254             $geo->set_angle_unit('degrees');
255              
256             =cut
257              
258             sub set_angle_unit
259             {
260 280     280 1 1114 my $self = shift;
261 280         390 my $unit = shift;
262 280 100       1150 if( $unit =~ /deg/i ) {
    50          
263 62         107 $unit = 'degrees';
264             }elsif( $unit =~ /rad/i ) {
265 218         356 $unit = 'radians';
266             }else{
267 0 0       0 croak("Invalid unit specifier '$unit' - please use either " .
268             "degrees or radians (the default)") unless $unit =~ /rad/i;
269             }
270 280         498 $self->{angle_unit} = $unit;
271             }
272              
273             *set_units = \&set_angle_unit;
274              
275             =pod
276              
277             =item set_distance_unit
278              
279             Set the distance unit used by the Geo::Ellipsoid object. The unit of distance
280             may also be set in the constructor of the object. The recognized values are
281             'meter', 'kilometer', 'mile', 'nm' (nautical mile), or 'foot'. The default is
282             'meter'. The value is not case sensitive and may be abbreviated to 3 letters.
283              
284             $geo->set_distance_unit('kilometer');
285              
286             For any other unit of distance not recognized by this method, pass a numerical
287             argument representing the length of the distance unit in meters. For example,
288             to use units of furlongs, call
289              
290             $geo->set_distance_unit(201.168);
291              
292             The distance conversion factors used by this module are as follows:
293              
294             Unit Units per meter
295             -------- ---------------
296             foot 0.3048
297             kilometer 1000.0
298             mile 1609.344
299             nm 1852.0
300              
301             =cut
302              
303             sub set_distance_unit
304             {
305 239     239 1 350 my $self = shift;
306 239         685 my $unit = shift;
307 239 50       469 print "distance unit = $unit\n" if $DEBUG;
308              
309 239         367 my $conversion = 0;
310              
311 239 50       413 if( defined $unit ) {
312              
313 239         369 my( $key, $val );
314 239         748 while( ($key,$val) = each %distance ) {
315 816         1734 my $re = substr($key,0,3);
316 816 50       1397 print "trying ($key,$re,$val)\n" if $DEBUG;
317 816 100       6680 if( $unit =~ /^$re/i ) {
318 239         483 $self->{distance_unit} = $unit;
319 239         351 $conversion = $val;
320              
321             # finish iterating to reset 'each' function call
322 239         939 while( each %distance ) {}
323 239         479 last;
324             }
325             }
326              
327 239 50       660 if( $conversion == 0 ) {
328 0 0       0 if( looks_like_number($unit) ) {
329 0         0 $conversion = $unit;
330             }else{
331 0         0 carp("Unknown argument to set_distance_unit: $unit\nAssuming meters");
332 0         0 $conversion = 1.0;
333             }
334             }
335             }else{
336 0         0 carp("Missing or undefined argument to set_distance_unit: ".
337             "$unit\nAssuming meters");
338 0         0 $conversion = 1.0;
339             }
340 239         512 $self->{conversion} = $conversion;
341             }
342              
343             =pod
344              
345             =item set_ellipsoid
346              
347             Set the ellipsoid to be used by the Geo::Ellipsoid object. See
348             L<"DEFINED ELLIPSOIDS"> below for the allowable values. The value
349             may also be set by the constructor. The default value is 'WGS84'.
350              
351             $geo->set_ellipsoid('NAD27');
352              
353             =cut
354              
355             sub set_ellipsoid
356             {
357 314     314 1 660 my $self = shift;
358 314   33     875 my $ellipsoid = uc shift || $defaults{ellipsoid};
359 314 50       623 print " set ellipsoid to $ellipsoid\n" if $DEBUG;
360 314 50       733 unless( exists $ellipsoids{$ellipsoid} ) {
361 0         0 croak("Ellipsoid $ellipsoid does not exist - please use " .
362             "set_custom_ellipsoid to use an ellipsoid not in valid set");
363             }
364 314         553 $self->{ellipsoid} = $ellipsoid;
365 314         456 my( $major, $recip ) = @{$ellipsoids{$ellipsoid}};
  314         832  
366 314         615 $self->{equatorial} = $major;
367 314 100       725 if( $recip == 0 ) {
368 1         188 carp("Infinite flattening specified by ellipsoid -- assuming a sphere");
369 1         123 $self->{polar} = $self->{equatorial};
370 1         4 $self->{flattening} = 0;
371 1         4 $self->{eccentricity} = 0;
372             }else{
373 313         935 $self->{flattening} = ( 1.0 / $ellipsoids{$ellipsoid}[1]);
374 313         714 $self->{polar} = $self->{equatorial} * ( 1.0 - $self->{flattening} );
375             $self->{eccentricity} = sqrt( 2.0 * $self->{flattening} -
376 313         781 ( $self->{flattening} * $self->{flattening} ) );
377             }
378             }
379              
380             =pod
381              
382             =item set_custom_ellipsoid
383              
384             Sets the ellipsoid parameters to the specified semi-major axis (given in
385             meters) and reciprocal flattening. A zero value for the reciprocal flattening
386             will result in a sphere for the ellipsoid, and a warning message will be
387             issued.
388              
389             $geo->set_custom_ellipsoid( 'sphere', 6378137, 0 );
390              
391             =cut
392              
393             sub set_custom_ellipsoid
394             {
395 3     3 1 19 my $self = shift;
396 3         9 my( $name, $major, $recip ) = @_;
397 3         7 $name = uc $name;
398 3 50       10 $recip = 0 unless defined $recip;
399 3 50       9 if( $major ) {
400 3         11 $ellipsoids{$name} = [ $major, $recip ];
401             }else{
402 0         0 croak("set_custom_ellipsoid called without semi-major radius parameter");
403             }
404 3         8 $self->set_ellipsoid($name);
405             }
406              
407             =pod
408              
409             =item set_longitude_symmetric
410              
411             If called with no argument or a true argument, sets the range of output values
412             for longitude to be symmetric around zero, i.e., in the range [-pi,+pi)
413             radians, [-180,180) degrees etc. depending on the angle unit.
414              
415             If called with a false or undefined argument, sets the output angle range to be
416             non-negative, i.e., [0,2*pi) radians, [0, 360) degrees etc. depending on the
417             angle unit.
418              
419             $geo->set_longitude_symmetric(1);
420              
421             =cut
422              
423             sub set_longitude_symmetric
424             {
425 235     235 1 454 my( $self, $sym ) = @_;
426             # see if argument passed
427 235 50       529 if( $#_ > 0 ) {
428             # yes -- use value passed
429 235         396 $self->{longitude_symmetric} = $sym;
430             }else{
431             # no -- set to true
432 0         0 $self->{longitude_symmetric} = 1;
433             }
434             }
435              
436             =pod
437              
438             =item set_bearing_symmetric
439              
440             If called with no argument or a true argument, sets the range of output values
441             for bearing to be symmetric around zero, i.e., in the range [-pi,+pi) radians,
442             [-180,180) degrees etc. depending on the angle unit.
443              
444             If called with a false or undefined argument, sets the output angle range to be
445             non-negative, i.e., [0,2*pi) radians, [0,360) degrees etc. depending on the
446             angle unit.
447              
448             $geo->set_bearing_symmetric(1);
449              
450             =cut
451              
452             sub set_bearing_symmetric
453             {
454 235     235 1 403 my( $self, $sym ) = @_;
455             # see if argument passed
456 235 50       457 if( $#_ > 0 ) {
457             # yes -- use value passed
458 235         363 $self->{bearing_symmetric} = $sym;
459             }else{
460             # no -- set to true
461 0         0 $self->{bearing_symmetric} = 1;
462             }
463             }
464              
465             =pod
466              
467             =item set_defaults
468              
469             Sets the defaults for the new() constructor method. Call with key, value pairs
470             similar to new.
471              
472             Geo::Ellipsoid->set_defaults(
473             ellipsoid => 'GRS80',
474             angle_unit => 'degrees',
475             distance_unit => 'kilometer',
476             longitude_symmetric => 1,
477             bearing_symmetric => 0
478             );
479              
480             Keys and string values (except for the ellipsoid identifier) may be shortened
481             to their first three letters and are case-insensitive:
482              
483             Geo::Ellipsoid->set_defaults(
484             uni => 'deg',
485             ell => 'GRS80',
486             dis => 'kil',
487             lon => 1,
488             bea => 0
489             );
490              
491             =cut
492              
493             sub set_defaults
494             {
495 21     21 1 57109 my $self = shift;
496 21         70 my %args = @_;
497 21         81 while (my ($key, $val) = each %args) {
498 45 100       210 if( $key =~ /^ell/i ) {
    100          
    100          
    100          
    50          
499 21         86 $defaults{ellipsoid} = uc $val;
500             }elsif( $key =~ /^(uni|ang)/i ) {
501 21         76 $defaults{angle_unit} = $val;
502             }elsif( $key =~ /^dis/i ) {
503 1         3 $defaults{distance_unit} = $val;
504             }elsif( $key =~ /^lon/i ) {
505 1         5 $defaults{longitude_symmetric} = $val;
506             }elsif( $key =~ /^bea/i ) {
507 1         4 $defaults{bearing_symmetric} = $val;
508             }else{
509 0         0 croak("Geo::Ellipsoid::set_defaults called with invalid key: $key");
510             }
511             }
512 21 50       90 print "Defaults set to ($defaults{ellipsoid},$defaults{angle_unit}\n"
513             if $DEBUG;
514             }
515              
516             =pod
517              
518             =back
519              
520             =head2 Getters
521              
522             =over
523              
524             =item get_ellipsoid
525              
526             Returns the name of the ellipsoid.
527              
528             =cut
529              
530             sub get_ellipsoid {
531 40     40 1 21067 my $self = shift;
532 40         136 return $self -> {ellipsoid};
533             }
534              
535             =pod
536              
537             =item get_equatorial_radius
538              
539             Returns the equatorial radius in meters.
540              
541             =cut
542              
543             sub get_equatorial_radius {
544 20     20 1 63 my $self = shift;
545 20         37 return $self -> {equatorial};
546             }
547              
548             =pod
549              
550             =item get_polar_radius
551              
552             Returns the polar radius in meters.
553              
554             =cut
555              
556             sub get_polar_radius {
557 20     20 1 62 my $self = shift;
558 20         35 return $self -> {polar};
559             }
560              
561             =pod
562              
563             =item get_geocentric_radius ANGLE
564              
565             Returns the geocentric radius in meters, given a geocentric latitude. The
566             geocentric latitude is the angle between the equatorial plane and the radius
567             from centre to the point on the surface.
568              
569             =cut
570              
571             sub get_geocentric_radius {
572 10     10 1 3414 my $self = shift;
573 10         16 my $angle = shift;
574 10         15 my $angle_unit = $self->{angle_unit};
575 10 50       32 $angle /= $degrees_per_radian if $angle_unit eq 'degrees';
576              
577 10         18 my $a = $self -> {equatorial};
578 10         14 my $b = $self -> {polar};
579              
580 10         22 my $sa = sin $angle;
581 10         37 my $ca = cos $angle;
582              
583 10         29 return $a * $b / _hypot($a * $sa, $b * $ca);
584             }
585              
586             =pod
587              
588             =item get_flattening
589              
590             Returns the flattening.
591              
592             =cut
593              
594             sub get_flattening {
595 20     20 1 66 my $self = shift;
596 20         39 return $self -> {flattening};
597             }
598              
599             =pod
600              
601             =item get_eccentricity
602              
603             Returns the eccentricity.
604              
605             =cut
606              
607             sub get_eccentricity {
608 20     20 1 62 my $self = shift;
609 20         39 return $self -> {eccentricity};
610             }
611              
612             =pod
613              
614             =item get_longitude_symmetric
615              
616             Returns true if the longitude is symmetric around zero, and false otherwise.
617              
618             =cut
619              
620             sub get_longitude_symmetric {
621 6     6 1 19 my $self = shift;
622 6         24 return $self -> {longitude_symmetric};
623             }
624              
625             =pod
626              
627             =item get_bearing_symmetric
628              
629             Returns true if the bearing is symmetric around zero, and false otherwise.
630              
631             =cut
632              
633             sub get_bearing_symmetric {
634 6     6 1 17 my $self = shift;
635 6         33 return $self -> {bearing_symmetric};
636             }
637              
638             =pod
639              
640             =item get_angle_unit
641              
642             Returns the angle unit, i.e., the unit for latitude, longitude, and bearing.
643              
644             =cut
645              
646             sub get_angle_unit {
647 32     32 1 16526 my $self = shift;
648 32         102 return $self -> {angle_unit};
649             }
650              
651             =pod
652              
653             =item get_distance_unit
654              
655             Returns the distance unit.
656              
657             =cut
658              
659             sub get_distance_unit {
660 14     14 1 47 my $self = shift;
661 14         58 return $self -> {distance_unit};
662             }
663              
664             =pod
665              
666             =back
667              
668             =head2 Calculations
669              
670             =over
671              
672             =item scales
673              
674             Returns a list consisting of the distance unit per angle of latitude
675             and longitude (degrees or radians) at the specified latitude.
676             These values may be used for fast approximations of distance
677             calculations in the vicinity of some location.
678              
679             ( $lat_scale, $lon_scale ) = $geo->scales($lat0);
680             $x = $lon_scale * ($lon - $lon0);
681             $y = $lat_scale * ($lat - $lat0);
682              
683             =cut
684              
685             sub scales
686             {
687 90     90 1 53287 my $self = shift;
688 90         174 my $units = $self->{angle_unit};
689 90         124 my $lat = $_[0];
690 90 50       199 if( defined $lat ) {
691 90 50       280 $lat /= $degrees_per_radian if( $units eq 'degrees' );
692             }else{
693 0         0 carp("scales() method requires latitude argument; assuming 0");
694 0         0 $lat = 0;
695             }
696              
697 90         137 my $aa = $self->{equatorial};
698 90         127 my $bb = $self->{polar};
699 90         225 my $d1 = $aa * cos($lat);
700 90         152 my $d2 = $bb * sin($lat);
701 90         158 my $d3 = $d1*$d1 + $d2*$d2;
702 90         122 my $d4 = sqrt($d3);
703 90         109 my $n1 = $aa * $bb;
704 90         175 my $latscl = ( $n1 * $n1 ) / ( $d3 * $d4 * $self->{conversion} );
705 90         141 my $lonscl = ( $aa * $d1 ) / ( $d4 * $self->{conversion} );
706              
707 90 50       171 if( $DEBUG ) {
708 0         0 print "lat=$lat, aa=$aa, bb=$bb\nd1=$d1, d2=$d2, d3=$d3, d4=$d4\n";
709 0         0 print "latscl=$latscl, lonscl=$lonscl\n";
710             }
711              
712 90 50       184 if( $self->{angle_unit} eq 'degrees' ) {
713 90         118 $latscl /= $degrees_per_radian;
714 90         119 $lonscl /= $degrees_per_radian;
715             }
716 90         327 return ( $latscl, $lonscl );
717             }
718              
719             =pod
720              
721             =item range
722              
723             Returns the range in distance units between two specified locations given
724             as latitude, longitude pairs.
725              
726             my $dist = $geo->range( $lat1, $lon1, $lat2, $lon2 );
727             my $dist = $geo->range( @origin, @destination );
728              
729             =cut
730              
731             sub range
732             {
733 1220     1220 1 375726 my $self = shift;
734 1220         2448 my @args = _normalize_input($self->{angle_unit},@_);
735 1220         9671 my($range,$bearing) = $self->_inverse(@args);
736 1220 50       2236 print "inverse(@_[1..4]) returns($range,$bearing)\n" if $DEBUG;
737 1220         3030 return $range;
738             }
739              
740             =pod
741              
742             =item bearing
743              
744             Returns the bearing in degrees or radians from the first location to
745             the second. Zero bearing is true north.
746              
747             my $bearing = $geo->bearing( $lat1, $lon1, $lat2, $lon2 );
748              
749             =cut
750              
751             sub bearing
752             {
753 2432     2432 1 803983 my $self = shift;
754 2432         4671 my $units = $self->{angle_unit};
755 2432         5401 my @args = _normalize_input($units,@_);
756 2432         8059 my($range,$bearing) = $self->_inverse(@args);
757 2432 50       4506 print "inverse(@args) returns($range,$bearing)\n" if $DEBUG;
758 2432         3377 my $t = $bearing;
759 2432         5659 $self->_normalize_output('bearing_symmetric',$bearing);
760 2432 50       6970 print "_normalize_output($t) returns($bearing)\n" if $DEBUG;
761 2432         6448 return $bearing;
762             }
763              
764             =pod
765              
766             =item at
767              
768             Returns the list (latitude,longitude) in degrees or radians that is a
769             specified range and bearing from a given location.
770              
771             my( $lat2, $lon2 ) = $geo->at( $lat1, $lon1, $range, $bearing );
772              
773             =cut
774              
775             sub at
776             {
777 2408     2408 1 968163 my $self = shift;
778 2408         4690 my $units = $self->{angle_unit};
779 2408         6119 my( $lat, $lon, $az ) = _normalize_input($units,@_[0,1,3]);
780 2408         6593 my $r = $_[2];
781 2408 50       5292 print "at($lat,$lon,$r,$az)\n" if $DEBUG;
782 2408         5138 my( $lat2, $lon2 ) = $self->_forward($lat,$lon,$r,$az);
783 2408 50       4883 print "_forward returns ($lat2,$lon2)\n" if $DEBUG;
784 2408         5947 $self->_normalize_output('longitude_symmetric',$lon2);
785 2408         7684 $self->_normalize_output('latitude_symmetric',$lat2);
786 2408         9570 return ( $lat2, $lon2 );
787             }
788              
789             =pod
790              
791             =item to
792              
793             In list context, returns (range, bearing) between two specified locations.
794             In scalar context, returns just the range.
795              
796             my( $dist, $theta ) = $geo->to( $lat1, $lon1, $lat2, $lon2 );
797             my $dist = $geo->to( $lat1, $lon1, $lat2, $lon2 );
798              
799             =cut
800              
801             sub to
802             {
803 252     252 1 148746 my $self = shift;
804 252         465 my $units = $self->{angle_unit};
805 252         555 my @args = _normalize_input($units,@_);
806 252 50       2134 print "to($units,@args)\n" if $DEBUG;
807 252         547 my($range,$bearing) = $self->_inverse(@args);
808 252 50       595 print "to: inverse(@args) returns($range,$bearing)\n" if $DEBUG;
809             #$bearing *= $degrees_per_radian if $units eq 'degrees';
810 252         697 $self->_normalize_output('bearing_symmetric',$bearing);
811 252 50       2163 if( wantarray() ) {
812 252         1099 return ( $range, $bearing );
813             }else{
814 0         0 return $range;
815             }
816             }
817              
818             =pod
819              
820             =item displacement
821              
822             Returns the (x,y) displacement in distance units between the two specified
823             locations.
824              
825             my( $x, $y ) = $geo->displacement( $lat1, $lon1, $lat2, $lon2 );
826              
827             NOTE: The x and y displacements are only approximations and only valid
828             between two locations that are fairly near to each other. Beyond 10 kilometers
829             or more, the concept of X and Y on a curved surface loses its meaning.
830              
831             =cut
832              
833             sub displacement
834             {
835 100     100 1 59880 my $self = shift;
836 100 50       250 print "displacement(",join(',',@_),"\n" if $DEBUG;
837 100         281 my @args = _normalize_input($self->{angle_unit},@_);
838 100 50       799 print "call _inverse(@args)\n" if $DEBUG;
839 100         227 my( $range, $bearing ) = $self->_inverse(@args);
840 100 50       175 print "disp: _inverse(@args) returns ($range,$bearing)\n" if $DEBUG;
841 100         167 my $x = $range * sin($bearing);
842 100         162 my $y = $range * cos($bearing);
843 100         348 return ($x,$y);
844             }
845              
846             =pod
847              
848             =item location
849              
850             Returns the list (latitude,longitude) of a location at a given (x,y)
851             displacement from a given location.
852              
853             my @loc = $geo->location( $lat, $lon, $x, $y );
854              
855             =cut
856              
857             sub location
858             {
859 200     200 1 119820 my $self = shift;
860 200         374 my $units = $self->{angle_unit};
861 200         351 my($lat,$lon,$x,$y) = @_;
862 200         383 my $range = sqrt( $x*$x+ $y*$y );
863 200         442 my $bearing = atan2($x,$y);
864 200 50       516 $bearing *= $degrees_per_radian if $units eq 'degrees';
865 200 50       406 print "location($lat,$lon,$x,$y,$range,$bearing)\n" if $DEBUG;
866 200         418 return $self->at($lat,$lon,$range,$bearing);
867             }
868              
869             ########################################################################
870             #
871             # internal functions
872             #
873             # inverse
874             #
875             # Calculate the displacement from origin to destination.
876             # The input to this subroutine is
877             # ( latitude-1, longitude-1, latitude-2, longitude-2 ) in radians.
878             #
879             # Return the results as the list (range,bearing) with range in the
880             # current specified distance unit and bearing in radians.
881              
882             sub _inverse()
883             {
884 3996     3996   5814 my $self = shift;
885 3996         6872 my( $lat1, $lon1, $lat2, $lon2 ) = (@_);
886 3996 50       8013 print "_inverse($lat1,$lon1,$lat2,$lon2)\n" if $DEBUG;
887              
888 3996         6158 my $a = $self->{equatorial};
889 3996         6084 my $f = $self->{flattening};
890              
891 3996         5670 my $r = 1.0 - $f;
892 3996         8890 my $tu1 = $r * sin($lat1) / cos($lat1);
893 3996         6997 my $tu2 = $r * sin($lat2) / cos($lat2);
894 3996         6690 my $cu1 = 1.0 / ( sqrt(($tu1*$tu1) + 1.0) );
895 3996         5313 my $su1 = $cu1 * $tu1;
896 3996         5778 my $cu2 = 1.0 / ( sqrt( ($tu2*$tu2) + 1.0 ));
897 3996         5496 my $s = $cu1 * $cu2;
898 3996         5496 my $baz = $s * $tu2;
899 3996         5351 my $faz = $baz * $tu1;
900 3996         5730 my $dlon = $lon2 - $lon1;
901              
902 3996 50       6379 if( $DEBUG ) {
903 0         0 printf "lat1=%.8f, lon1=%.8f\n", $lat1, $lon1;
904 0         0 printf "lat2=%.8f, lon2=%.8f\n", $lat2, $lon2;
905 0         0 printf "r=%.8f, tu1=%.8f, tu2=%.8f\n", $r, $tu1, $tu2;
906 0         0 printf "faz=%.8f, dlon=%.8f\n", $faz, $dlon;
907             }
908              
909 3996         5157 my $x = $dlon;
910 3996         5024 my $cnt = 0;
911 3996 50       6453 print "enter loop:\n" if $DEBUG;
912 3996         5970 my( $c2a, $c, $cx, $cy, $cz, $d, $del, $e, $sx, $sy, $y );
913 3996   100     5075 do {
914 22431 50       35574 printf " x=%.8f\n", $x if $DEBUG;
915 22431         29932 $sx = sin($x);
916 22431         29404 $cx = cos($x);
917 22431         26944 $tu1 = $cu2*$sx;
918 22431         29241 $tu2 = $baz - ($su1*$cu2*$cx);
919              
920 22431 50       33840 printf " sx=%.8f, cx=%.8f, tu1=%.8f, tu2=%.8f\n",
921             $sx, $cx, $tu1, $tu2 if $DEBUG;
922              
923 22431         30472 $sy = sqrt( $tu1*$tu1 + $tu2*$tu2 );
924 22431         28442 $cy = $s*$cx + $faz;
925 22431         30785 $y = atan2($sy,$cy);
926 22431         26269 my $sa;
927 22431 100       34312 if( $sy == 0.0 ) {
928 72         100 $sa = 1.0;
929             }else{
930 22359         28993 $sa = ($s*$sx) / $sy;
931             }
932              
933 22431 50       34265 printf " sy=%.8f, cy=%.8f, y=%.8f, sa=%.8f\n", $sy, $cy, $y, $sa
934             if $DEBUG;
935              
936 22431         28200 $c2a = 1.0 - ($sa*$sa);
937 22431         26949 $cz = $faz + $faz;
938 22431 100       35408 if( $c2a > 0.0 ) {
939 21687         28632 $cz = ((-$cz)/$c2a) + $cy;
940             }
941 22431         29091 $e = ( 2.0 * $cz * $cz ) - 1.0;
942 22431         32194 $c = ( ((( (-3.0 * $c2a) + 4.0)*$f) + 4.0) * $c2a * $f )/16.0;
943 22431         29251 $d = $x;
944 22431         31631 $x = ( ($e * $cy * $c + $cz) * $sy * $c + $y) * $sa;
945 22431         30493 $x = ( 1.0 - $c ) * $x * $f + $dlon;
946 22431         26817 $del = $d - $x;
947              
948 22431 50       70606 if( $DEBUG ) {
949 0         0 printf " c2a=%.8f, cz=%.8f\n", $c2a, $cz;
950 0         0 printf " e=%.8f, d=%.8f\n", $e, $d;
951 0         0 printf " (d-x)=%.8g\n", $del;
952             }
953              
954             }while( (abs($del) > $eps) && ( ++$cnt <= $max_loop_count ) );
955              
956 3996         5998 $faz = atan2($tu1,$tu2);
957 3996         7135 $baz = atan2($cu1*$sx,($baz*$cx - $su1*$cu2)) + pi;
958 3996         6413 $x = sqrt( ((1.0/($r*$r)) -1.0 ) * $c2a+1.0 ) + 1.0;
959 3996         5406 $x = ($x-2.0)/$x;
960 3996         5089 $c = 1.0 - $x;
961 3996         5843 $c = (($x*$x)/4.0 + 1.0)/$c;
962 3996         5938 $d = ((0.375*$x*$x) - 1.0)*$x;
963 3996         5011 $x = $e*$cy;
964              
965 3996 50       6775 if( $DEBUG ) {
966 0         0 printf "e=%.8f, cy=%.8f, x=%.8f\n", $e, $cy, $x;
967 0         0 printf "sy=%.8f, c=%.8f, d=%.8f\n", $sy, $c, $d;
968 0         0 printf "cz=%.8f, a=%.8f, r=%.8f\n", $cz, $a, $r;
969             }
970              
971 3996         5452 $s = 1.0 - $e - $e;
972 3996         8152 $s = (((((((( $sy * $sy * 4.0 ) - 3.0) * $s * $cz * $d/6.0) - $x) *
973             $d /4.0) + $cz) * $sy * $d) + $y ) * $c * $a * $r;
974              
975 3996 50       6399 printf "s=%.8f\n", $s if $DEBUG;
976              
977             # return result
978 3996         8832 my @disp = ( ($s/$self->{conversion}), $faz );
979 3996 50       6538 print "disp = (@disp)\n" if $DEBUG;
980 3996         10334 return @disp;
981             }
982              
983             # _forward
984             #
985             # Calculate the location (latitue,longitude) of a point
986             # given a starting point and a displacement from that
987             # point as (range,bearing)
988             #
989             sub _forward
990             {
991 2400     2400   3390 my $self = shift;
992 2400         4265 my( $lat1, $lon1, $range, $bearing ) = @_;
993              
994 2400 50       4130 if( $DEBUG ) {
995 0         0 printf "_forward(lat1=%.8f,lon1=%.8f,range=%.8f,bearing=%.8f)\n",
996             $lat1, $lon1, $range, $bearing;
997             }
998              
999 2400         3117 my $eps = 0.5e-13;
1000              
1001 2400         3573 my $a = $self->{equatorial};
1002 2400         3596 my $f = $self->{flattening};
1003 2400         3525 my $r = 1.0 - $f;
1004              
1005 2400         6301 my $tu = $r * sin($lat1) / cos($lat1);
1006 2400         3242 my $faz = $bearing;
1007 2400         3692 my $s = $self->{conversion} * $range;
1008 2400         3847 my $sf = sin($faz);
1009 2400         3467 my $cf = cos($faz);
1010              
1011 2400         3082 my $baz = 0.0;
1012 2400 50       5788 $baz = 2.0 * atan2($tu,$cf) if( $cf != 0.0 );
1013              
1014 2400         4292 my $cu = 1.0 / sqrt(1.0 + $tu*$tu);
1015 2400         3286 my $su = $tu * $cu;
1016 2400         3108 my $sa = $cu * $sf;
1017 2400         3342 my $c2a = 1.0 - ($sa*$sa);
1018 2400         4208 my $x = 1.0 + sqrt( (((1.0/($r*$r)) - 1.0 )*$c2a) +1.0);
1019 2400         3533 $x = ($x-2.0)/$x;
1020 2400         3156 my $c = 1.0 - $x;
1021 2400         3785 $c = ((($x*$x)/4.0) + 1.0)/$c;
1022 2400         3744 my $d = $x * ((0.375*$x*$x)-1.0);
1023 2400         3668 $tu = (($s/$r)/$a)/$c;
1024 2400         3188 my $y = $tu;
1025              
1026 2400 50       4242 if( $DEBUG ) {
1027 0         0 printf "r=%.8f, tu=%.8f, faz=%.8f\n", $r, $tu, $faz;
1028 0         0 printf "baz=%.8f, sf=%.8f, cf=%.8f\n", $baz, $sf, $cf;
1029 0         0 printf "cu=%.8f, su=%.8f, sa=%.8f\n", $cu, $su, $sa;
1030 0         0 printf "x=%.8f, c=%.8f, y=%.8f\n", $x, $c, $y;
1031             }
1032              
1033 2400         3505 my( $cy, $cz, $e, $sy );
1034 2400         3025 do {
1035 8676         10763 $sy = sin($y);
1036 8676         10891 $cy = cos($y);
1037 8676         11672 $cz = cos($baz+$y);
1038 8676         12220 $e = (2.0*$cz*$cz)-1.0;
1039 8676         10792 $c = $y;
1040 8676         10510 $x = $e * $cy;
1041 8676         10737 $y = (2.0 * $e) - 1.0;
1042 8676         20840 $y = ((((((((($sy*$sy*4.0)-3.0)*$y*$cz*$d)/6.0)+$x)*$d)/4.0)-$cz)*$sy*$d) +
1043             $tu;
1044             } while( abs($y-$c) > $eps );
1045              
1046 2400         3549 $baz = ($cu*$cy*$cf) - ($su*$sy);
1047 2400         3571 $c = $r*sqrt(($sa*$sa) + ($baz*$baz));
1048 2400         3303 $d = $su*$cy + $cu*$sy*$cf;
1049 2400         3849 my $lat2 = atan2($d,$c);
1050 2400         3386 $c = $cu*$cy - $su*$sy*$cf;
1051 2400         3650 $x = atan2($sy*$sf,$c);
1052 2400         3989 $c = (((((-3.0*$c2a)+4.0)*$f)+4.0)*$c2a*$f)/16.0;
1053 2400         3849 $d = (((($e*$cy*$c) + $cz)*$sy*$c)+$y)*$sa;
1054 2400         3524 my $lon2 = $lon1 + $x - (1.0-$c)*$d*$f;
1055             #$baz = atan2($sa,$baz) + pi;
1056              
1057             # return result
1058 2400         5835 return ($lat2,$lon2);
1059              
1060             }
1061              
1062             # _normalize_input
1063             #
1064             # Normalize a set of input angle values by converting to radians if given
1065             # in degrees. We don't add/subtract multiples of two pi, because the
1066             # trigonometric functions do this more accuractely.
1067             #
1068             sub _normalize_input
1069             {
1070 6412     6412   9749 my $units = shift;
1071 6412         12284 my @args = @_;
1072             return map {
1073 6412 100       12838 $units eq 'degrees' ? deg2rad($_) : $_
  23240         90130  
1074             } @args;
1075             }
1076              
1077             # _normalize_output
1078             #
1079             # Normalize a set of output angle values by converting to
1080             # degrees if needed and by converting to the range [-pi,+pi) or
1081             # [0,2pi) as needed.
1082             #
1083             sub _normalize_output
1084             {
1085 7500     7500   9925 my $self = shift;
1086 7500         10478 my $elem = shift; # '(bearing|latitude|longitude)_symmetric'
1087             # adjust remaining input values by reference
1088 7500         13366 for ( @_ ) {
1089 7500 100       14259 if( $self->{$elem} ) {
1090             # normalize to range [-pi,pi)
1091 4824         9399 while( $_ < -(pi) ) { $_ += $twopi }
  0         0  
1092 4824         8771 while( $_ >= pi ) { $_ -= $twopi }
  98         168  
1093             }else{
1094             # normalize to range [0,2*pi)
1095 2676         5498 while( $_ < 0 ) { $_ += $twopi }
  1193         2429  
1096 2676         5205 while( $_ >= $twopi ) { $_ -= $twopi }
  0         0  
1097             }
1098 7500 100       16716 $_ = rad2deg($_) if $self->{angle_unit} eq 'degrees';
1099             }
1100             }
1101              
1102             # _max
1103             #
1104             # Return the maximum of the two input arguments.
1105              
1106             sub _max {
1107 10 100   10   37 $_[0] > $_[1] ? $_[0] : $_[1];
1108             }
1109              
1110             # _min
1111             #
1112             # Return the minimum of the two input arguments.
1113              
1114             sub _min {
1115 10 100   10   25 $_[0] < $_[1] ? $_[0] : $_[1];
1116             }
1117              
1118             # _hypot
1119             #
1120             # Returns the length of the hypotenuse of a right-angle triangle given the
1121             # length of the two catheti (the two other sides). The result is computed in a
1122             # way that avoids problems that occur when squaring very large or very small
1123             # numbers.
1124              
1125             sub _hypot {
1126 10     10   18 my $x = abs($_[0]);
1127 10         13 my $y = abs($_[1]);
1128 10         31 my $z = _max($x, $y);
1129 10         21 my $r = _min($x, $y) / $z;
1130 10         51 return $z * sqrt(1 + $r * $r);
1131             }
1132              
1133             =pod
1134              
1135             =back
1136              
1137             =head1 DEFINED ELLIPSOIDS
1138              
1139             The following ellipsoids are defined in Geo::Ellipsoid, with the
1140             semi-major axis in meters and the reciprocal flattening as shown.
1141             The default ellipsoid is WGS84.
1142              
1143             Ellipsoid Semi-Major Axis (m.) 1/Flattening
1144             --------- ------------------- ---------------
1145             AIRY 6377563.396 299.3249646
1146             AIRY-MODIFIED 6377340.189 299.3249646
1147             AUSTRALIAN 6378160.0 298.25
1148             BESSEL-1841 6377397.155 299.1528128
1149             CLARKE-1880 6378249.145 293.465
1150             EVEREST-1830 6377276.345 290.8017
1151             EVEREST-MODIFIED 6377304.063 290.8017
1152             FISHER-1960 6378166.0 298.3
1153             FISHER-1968 6378150.0 298.3
1154             GRS80 6378137.0 298.25722210088
1155             HOUGH-1956 6378270.0 297.0
1156             HAYFORD 6378388.0 297.0
1157             IAU76 6378140.0 298.257
1158             KRASSOVSKY-1938 6378245.0 298.3
1159             NAD27 6378206.4 294.9786982138
1160             NWL-9D 6378145.0 298.25
1161             SOUTHAMERICAN-1969 6378160.0 298.25
1162             SOVIET-1985 6378136.0 298.257
1163             WGS72 6378135.0 298.26
1164             WGS84 6378137.0 298.257223563
1165              
1166             =head1 LIMITATIONS
1167              
1168             The methods should not be used on points which are too near the poles
1169             (above or below 89 degrees), and should not be used on points which
1170             are antipodal, i.e., exactly on opposite sides of the ellipsoid. The
1171             methods will not return valid results in these cases.
1172              
1173             =head1 ACKNOWLEDGEMENTS
1174              
1175             The conversion algorithms used here are Perl translations of Fortran
1176             routines written by LCDR S NGS Rockville MD that implement
1177             S Modified Rainsford's method with Helmert's elliptical
1178             terms as published in "Direct and Inverse Solutions of Ellipsoid on
1179             the Ellipsoid with Application of Nested Equations", S
1180             Survey Review, April 1975.
1181              
1182             The Fortran source code files inverse.for and forward.for
1183             may be obtained from
1184              
1185             ftp://ftp.ngs.noaa.gov/pub/pcsoft/for_inv.3d/source/
1186              
1187             =head1 AUTHOR
1188              
1189             Peter John Acklam, C<< >> (current maintainer)
1190              
1191             Jim Gibson, C<< >> (original author)
1192              
1193             =head1 BUGS
1194              
1195             See LIMITATIONS, above.
1196              
1197             There are currently no known bugs.
1198              
1199             Please report any bugs or feature requests via
1200             L.
1201              
1202             Old bug reports and feature requests can be found at
1203             L.
1204              
1205             =head1 SUPPORT
1206              
1207             You can find documentation for this module with the perldoc command.
1208              
1209             perldoc Geo::Ellipsoid
1210              
1211             You can also look for information at:
1212              
1213             =over 4
1214              
1215             =item *
1216              
1217             GitHub
1218              
1219             GitHub is a code hosting platform for version control and collaboration.
1220              
1221             L
1222              
1223             =item *
1224              
1225             MetaCPAN
1226              
1227             A modern, open-source CPAN search engine, useful to view POD in HTML format.
1228              
1229             L
1230              
1231             =item *
1232              
1233             CPANTS
1234              
1235             The CPANTS is a website that analyzes the Kwalitee (code metrics) of a
1236             distribution.
1237              
1238             L
1239              
1240             =item *
1241              
1242             CPAN Testers Reports
1243              
1244             The CPAN Testers is a network of smoke testers who run automated tests on
1245             uploaded CPAN distributions.
1246              
1247             L
1248              
1249             =item *
1250              
1251             CPAN Testers Matrix
1252              
1253             The CPAN Testers Matrix displays smoke test results for this distribution for
1254             various combinations of Perl version and operating systems.
1255              
1256             L
1257              
1258             =back
1259              
1260             =head1 COPYRIGHT & LICENSE
1261              
1262             Copyright 2016-2021 Peter John Acklam, current maintainer.
1263              
1264             Copyright 2005-2008 Jim Gibson, all rights reserved.
1265              
1266             This program is free software; you can redistribute it and/or modify it
1267             under the same terms as Perl itself.
1268              
1269             =head1 SEE ALSO
1270              
1271             Geo::Distance, Geo::Ellipsoids
1272              
1273             =cut
1274              
1275             1; # End of Geo::Ellipsoid