File Coverage

blib/lib/Astro/Coord/ECI.pm
Criterion Covered Total %
statement 761 900 84.5
branch 279 434 64.2
condition 70 134 52.2
subroutine 84 93 90.3
pod 41 41 100.0
total 1235 1602 77.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Astro::Coord::ECI - Manipulate geocentric coordinates
4              
5             =head1 SYNOPSIS
6              
7             use Astro::Coord::ECI;
8             use Astro::Coord::ECI::Sun;
9             use Astro::Coord::ECI::TLE;
10             use Astro::Coord::ECI::Utils qw{rad2deg};
11            
12             # 1600 Pennsylvania Avenue, in radians, radians, and KM
13             my ($lat, $lon, $elev) = (0.678911227503559,
14             -1.34456123391096, 0.01668);
15            
16             # Record the time
17             my $time = time ();
18            
19             # Set up observer's location
20             my $loc = Astro::Coord::ECI->geodetic ($lat, $lon, $elev);
21            
22             # Uncomment to turn off atmospheric refraction if desired.
23             # $loc->set( refraction => 0 );
24            
25             # Instantiate the Sun.
26             my $sun = Astro::Coord::ECI::Sun->universal ($time);
27            
28             # Figure out if the Sun is up at the observer's location.
29             my ($azimuth, $elevation, $range) = $loc->azel ($sun);
30             print "The Sun is ", rad2deg ($elevation),
31             " degrees above the horizon.\n";
32              
33             See the L documentation
34             for an example involving satellite pass prediction.
35              
36             =head1 DESCRIPTION
37              
38             This module was written to provide a base class for a system to predict
39             satellite visibility. Its main task is to convert the
40             Earth-Centered Inertial (ECI) coordinates generated by the NORAD models
41             into other coordinate systems (e.g. latitude, longitude, and altitude
42             above mean sea level), but a other functions have accreted to it.
43             In addition, a few support routines have been exposed for testing, or
44             whatever.
45              
46             All distances are in kilometers, and all angles are in radians
47             (including right ascension, which is usually measured in hours).
48              
49             Times are normal Perl times, whether used as universal or dynamical
50             time. Universal time is what is usually meant, unless otherwise stated.
51              
52             Known subclasses include B to predict the
53             position of the Moon, B to predict the
54             position of a star, or anything else that can be considered fixed on
55             the celestial sphere, B to predict the position
56             of the Sun, B to predict the position of a
57             satellite given the NORAD orbital parameters, and
58             B (a subclass of
59             Astro::Coord::ECI::TLE, and as of 0.099_01 moved to its own package) to
60             predict Iridium flares.
61              
62             B that in version 0.022_01 the velocity code got a substantial
63             rework, which is still in progress. I am attempting give useful
64             velocities where they are available, and no velocities at all where they
65             are not. Unfortunately I have yet to locate any worked problems, so the
66             velocity code is, in the most literal sense, untested.
67              
68             In general, locations specified in Earth-fixed coordinates are
69             considered to share the rotational velocity of the Earth, and locations
70             specified in inertial coordinates are considered to have undefined
71             velocities unless a velocity was specified explicitly or produced by the
72             object's model. This involved a change in the behavior of the eci()
73             method when velocity was not specified but location was. See the next
74             paragraph for my excuse.
75              
76             B This class and its subclasses should probably be
77             considered alpha code, meaning that the public interface may not be
78             completely stable. I will try not to change it, but given sufficient
79             reason I will do so. If I do so, I will draw attention to the change
80             in the documentation.
81              
82             =head2 Methods
83              
84             The following methods should be considered public.
85              
86             =over 4
87              
88             =cut
89              
90             package Astro::Coord::ECI;
91              
92 17     17   79741 use strict;
  17         64  
  17         482  
93 17     17   84 use warnings;
  17         28  
  17         842  
94              
95             our $VERSION = '0.129_01';
96              
97 17     17   9691 use Astro::Coord::ECI::Utils qw{ @CARP_NOT :mainstream };
  17         57  
  17         7753  
98 17     17   131 use Carp;
  17         30  
  17         940  
99 17     17   7431 use Clone ();
  17         41153  
  17         534  
100 17     17   120 use POSIX qw{floor strftime};
  17         33  
  17         103  
101              
102 17         1085 use constant NO_CASCADING_STATIONS =>
103 17     17   1353 q{Cascading 'station' attributes are not supported};
  17         38  
104 17     17   138 use constant TWO_DEGREES => deg2rad( 2 );
  17         34  
  17         87  
105 17     17   100 use constant CIVIL_TWILIGHT => deg2rad( -6 );
  17         43  
  17         56  
106              
107             my %mutator; # Attribute mutators. We define these just after the
108             # set() method, for convenience.
109             my %known_ellipsoid; # Known reference ellipsoids. We define these
110             # just before the reference_ellipsoid() method for
111             # convenience.
112             my %static = ( # The geoid, etc. Geoid gets set later.
113             # almanac_horizon => set when instantiated, so that the following
114             # _almanac_horizon => gets set automatically by the mutator.
115             angularvelocity => 7.292114992e-5, # Of surface of Earth, 1998. Meeus, p.83
116             debug => 0,
117             diameter => 0,
118             edge_of_earths_shadow => 1,
119             horizon => deg2rad (20),
120             refraction => 1,
121             twilight => CIVIL_TWILIGHT,
122             );
123             my %savatr; # Attribs saved across "normal" operations. Set at end.
124             my @kilatr = # Attributes to purge when setting coordinates.
125             qw{_need_purge _ECI_cache inertial local_mean_time specified}; #?
126              
127             =item $coord = Astro::Coord::ECI->new ();
128              
129             This method instantiates a coordinate object. Any arguments are passed
130             to the set() method once the object has been instantiated.
131              
132             =cut
133              
134             sub new {
135 172     172 1 13657 my ( $class, @args ) = @_;
136 172   33     1710 my $self = bless { %static }, ref $class || $class;
137 172         696 $self->{inertial} = $self->__initial_inertial();
138             ref $static{sun}
139 172 50       812 and $self->set( sun => $static{sun}->clone() );
140 172 100       741 @args and $self->set( @args );
141             exists $self->{almanac_horizon}
142 172 50       686 or $self->set( almanac_horizon => 0 );
143 172 50       409 $self->{debug} and do {
144 0         0 require Data::Dumper;
145 0         0 local $Data::Dumper::Terse = 1;
146 0         0 print "Debug - Instantiated ", Data::Dumper::Dumper ($self);
147             };
148 172         736 return $self;
149             }
150              
151             =item $angle = $coord->angle ($coord2, $coord3);
152              
153             This method calculates the angle between the $coord2 and $coord3
154             objects, as seen from $coord. The calculation uses the law of
155             haversines, and does not take atmospheric refraction into account. The
156             return is a number of radians between 0 and pi.
157              
158             The algorithm comes from "Ask Dr. Math" on the Math Forum,
159             C, which
160             attributes it to the Geographic Information Systems FAQ,
161             L, which in turn
162             attributes it to R. W. Sinnott, "Virtues of the Haversine," Sky and
163             Telescope, volume 68, number 2, 1984, page 159.
164              
165             Unfortunately, as of early 2021 the National Council of Teachers of
166             Mathematics restricted the Dr. Math content to their members, but an
167             annotated and expanded version of the article on haversines is available
168             at
169             L.
170             If you want the original article, you can feed the URL
171             C to the Wayback
172             Machine.
173              
174             Prior to version 0.011_03 the law of cosines was used, but this produced
175             errors on extremely small angles. The haversine law was selected based
176             on Jean Meeus, "Astronomical Algorithms", 2nd edition, chapter 17
177             "Angular Separation".
178              
179             This method ignores the C attribute.
180              
181             =cut
182              
183             sub angle {
184 1205     1205 1 1800 my $self = shift;
185 1205         1667 my $B = shift;
186 1205         1669 my $C = shift;
187 1205 50 33     3793 (ref $B && $B->represents (__PACKAGE__)
      33        
      33        
188             && ref $C && $C->represents (__PACKAGE__))
189             or croak <
190 0         0 Error - Both arguments must represent @{[__PACKAGE__]} objects.
191             eod
192             my ($ra1, $dec1) = $self->{inertial} ?
193 1205 100       3669 $B->equatorial ($self) : $self->_angle_non_inertial ($B);
194             my ($ra2, $dec2) = $self->{inertial} ?
195 1205 100       2936 $C->equatorial ($self) : $self->_angle_non_inertial ($C);
196 1205         2338 my $sindec = sin (($dec2 - $dec1)/2);
197 1205         1758 my $sinra = sin (($ra2 - $ra1)/2);
198 1205         2372 my $a = $sindec * $sindec +
199             cos ($dec1) * cos ($dec2) * $sinra * $sinra;
200 1205         5436 return 2 * atan2 (sqrt ($a), sqrt (1 - $a));
201              
202             }
203              
204             sub _angle_non_inertial {
205 2408     2408   3448 my $self = shift;
206 2408         3118 my $other = shift;
207 2408         4198 my ($x1, $y1, $z1) = $self->ecef ();
208 2408         4676 my ($x2, $y2, $z2) = $other->ecef ();
209 2408         3930 my $X = $x2 - $x1;
210 2408         3517 my $Y = $y2 - $y1;
211 2408         3214 my $Z = $z2 - $z1;
212 2408         4379 my $lon = atan2 ($Y, $X);
213 2408         6387 my $lat = mod2pi (atan2 ($Z, sqrt ($X * $X + $Y * $Y)));
214 2408         5246 return ($lon, $lat);
215             }
216              
217             =item $which = $coord->attribute ($name);
218              
219             This method returns the name of the class that implements the named
220             attribute, or undef if the attribute name is not valid.
221              
222             =cut
223              
224 0 0   0 1 0 sub attribute {return exists $mutator{$_[1]} ? __PACKAGE__ : undef}
225              
226             =item ($azimuth, $elevation, $range) = $coord->azel( $coord2 );
227              
228             This method takes another coordinate object, and computes its azimuth,
229             elevation, and range in reference to the object doing the computing.
230             The return is azimuth in radians measured clockwise from North (always
231             positive), elevation above the horizon in radians (negative if
232             below), and range in kilometers.
233              
234             As a side effect, the time of the $coord object may be set from the
235             $coord2 object.
236              
237             Atmospheric refraction is taken into account using the
238             C method if the C<$coord> object's
239             C attribute is true. The invocant's
240             C method is the one used; that is, if
241             C<$coord> and C<$coord2> have different C
242             methods, the C<$coord> object's method is used.
243              
244             B that the C attribute defaults to true.
245             For cases where both invocant and argument are ground-based objects, you
246             should probably set the invocant's C false
247             before invoking this method.
248              
249             This method is implemented in terms of azel_offset(). See that method's
250             documentation for further details.
251              
252             =item ( $azimuth, $elevation, $range ) = $coord->azel();
253              
254             This method computes the azimuth, elevation, and range if the C<$coord>
255             object as seen from the position stored in the C<$coord> object's
256             C attribute. An exception will be thrown if the C
257             attribute is not set.
258              
259             =cut
260              
261             sub azel { ## no critic (RequireArgUnpacking)
262 16905 50   16905 1 33090 @_ > 2
263             and croak q{Too many arguments};
264 16905         33277 @_ = _expand_args_default_station( @_ );
265 16905 50       31031 $_[2] = $_[2] ? 1 : 0;
266 16905         38950 goto &azel_offset;
267             }
268              
269             =item ($azimuth, $elevation, $range) = $coord->azel_offset($coord2, $offset);
270              
271             This method takes another coordinate object, and computes its azimuth,
272             elevation, and range in reference to the object doing the computing.
273             The return is azimuth in radians measured clockwise from North (always
274             positive), elevation above the horizon in radians (negative if
275             below), and range in kilometers.
276              
277             If the optional C<$offset> argument is provided, the elevation is offset
278             upward by the given fraction of the radius of the C<$coord2> object.
279             Thus, an C<$offset> of C<1> specifies the upper limb of the object, C<0>
280             specifies the center of the object, and C<-1> specifies the lower limb.
281             This offset is applied before atmospheric refraction.
282              
283             As a side effect, the time of the $coord object may be set from the
284             $coord2 object.
285              
286             By default, atmospheric refraction is taken into account in the
287             calculation of elevation, using the C method.
288             This better represents the observed position in the sky when the object
289             is above the horizon, but produces a gap in the data when the object
290             passes below the horizon, since I have no refraction equations for rock.
291             See the C documentation for details.
292              
293             The invocant's C method is the one used; that
294             is, if C<$coord> and C<$coord2> have different
295             C methods, the C<$coord> object's method is
296             used.
297              
298             If you want to ignore atmospheric refraction (and not have a gap in your
299             data), set the C attribute of the $coord object
300             to any value Perl considers false (e.g. 0).
301              
302             The algorithm for position is the author's, but he confesses to having
303             to refer to T. S. Kelso's "Computers and Satellites"
304             column in "Satellite Times", November/December 1995, titled "Orbital
305             Coordinate Systems, Part II" and available at
306             F to get the signs right.
307              
308             If velocities are available for both bodies involved in the calculation,
309             they will be returned after the position (i.e. you get a six-element
310             array back instead of a three-element array). The velocity of a point
311             specified in Earth-fixed coordinates (e.g. geodetic latitude, longitude,
312             and altitude) is assumed to be the rotational velocity of the Earth at
313             that point. The returned velocities are azimuthal velocity in radians
314             per second (B radians of azimuth, which get smaller as you go
315             farther away from the Equator), elevational velocity in radians per
316             second, and velocity in recession in kilometers per second, in that
317             order.
318              
319             If velocities are available for both bodies B the C<$coord2> object
320             has its C attribute set, the returned array will contain
321             seven elements, with the seventh being the Doppler shift.
322              
323             The algorithm for recessional velocity comes from John A. Magliacane's
324             C program, available at
325             L. The transverse velocity
326             computations are the author's, but use the same basic approach: vector
327             dot product of the velocity vector with a unit vector in the appropriate
328             direction, the latter generated by the appropriate (I hope!) vector
329             cross products.
330              
331             Velocity conversions to C appear to me to be mostly sane. See
332             L, below, for details.
333              
334             If velocities are available I you have provided a non-zero value
335             for the C attribute, you will get the Doppler shift as the
336             seventh element of the returned array. The I about velocity in
337             recession apply to the Doppler shift as well. The frequency is from the
338             C<$coord2> object. Getting the frequency from the C<$coord> object used
339             to be supported as a fallback, but now results in an exception.
340              
341             =item ( $azimuth, $elevation, $range ) = $coord->azel_offset( $offset );
342              
343             This method computes the azimuth, elevation, and range if the C<$coord>
344             object as seen from the location stored in the C<$coord> object's
345             C attribute. The functionality is as above, except for the fact
346             that in the above version the station is the invocant, whereas in this
347             version the orbiting body is the invocant.
348              
349             This version also returns velocities if they are available for both
350             bodies, and Doppler shift if in addition the C attribute of
351             the invocant is set.
352              
353             =cut
354              
355             sub azel_offset {
356 21993     21993 1 41855 my ( $self, $trn2, $offset ) = _expand_args_default_station( @_ );
357 21993 50       47977 $self->{debug} and do {
358 0         0 require Data::Dumper;
359 0         0 local $Data::Dumper::Terse = 1;
360 0         0 print "Debug azel_offset - ", Data::Dumper::Dumper ($self, $trn2, $offset);
361             };
362              
363             # _local_cartesian() returns NEU coordinates. Converting these to
364             # spherical gets Azimuth (clockwise from North), Elevation, and
365             # Range.
366              
367 21993         47661 my ( $azimuth, $elevation, $range, @velocity ) =
368             _convert_cartesian_to_spherical(
369             $self->_local_cartesian( $trn2 )
370             );
371              
372             # If the velocity and frequency are defined, we provide the Doppler
373             # shift as well.
374              
375 21993 100       48370 if ( defined $velocity[2] ) {
376 16896         42379 my $freq = $trn2->get( 'frequency' );
377 16896 100       35035 if ( not defined $freq ) {
378 16895         28161 $freq = $self->get( 'frequency' );
379 16895 50       34025 defined $freq
380             and croak 'Calculation of Doppler shift based ',
381             'on the frequency attribute of the observing ',
382             'station is not allowed';
383             }
384 16896 100       29043 if ( defined $freq ) {
385 1         4 $velocity[3] = - $freq * $velocity[2] / SPEED_OF_LIGHT;
386             }
387             }
388              
389             # Adjust for upper limb and refraction if needed.
390              
391             $offset
392 21993 100       42659 and $elevation += atan2(
393             $trn2->get( 'diameter' ) * $offset / 2,
394             $range,
395             );
396              
397             $self->{refraction} and
398 21993 50       56373 $elevation = $self->correct_for_refraction( $elevation );
399              
400 21993         76259 return ( $azimuth, $elevation, $range, @velocity );
401             }
402              
403             =item $coord2 = $coord->clone ();
404              
405             This method does a deep clone of an object, producing a different
406             but identical object.
407              
408             At the moment, it's really just a wrapper for Clone::clone.
409              
410             =cut
411              
412             sub clone {
413 3     3 1 14 my ( $self ) = @_;
414 3         85 return Clone::clone( $self );
415             }
416              
417             =item $elevation = $coord->correct_for_refraction ($elevation);
418              
419             This method corrects the given angular elevation for atmospheric
420             refraction. This is done only if the elevation has a reasonable chance
421             of being visible; that is, if the elevation before correction is not
422             more than two degrees below either the 'horizon' attribute or zero,
423             whichever is lesser. Sorry for the discontinuity thus introduced, but I
424             did not wish to carry the refraction calculation too low because I am
425             uncertain about the behavior of the algorithm, and I do not have a
426             corresponding algorithm for refraction through magma.
427              
428             This method can also be called as a class method, in which case the
429             correction is applied only if the uncorrected elevation is greater than
430             minus two degrees. It is really only exposed for testing purposes (hence
431             the cumbersome name). The azel() method calls it for you if the
432             C attribute is true.
433              
434             The algorithm for atmospheric refraction comes from Thorfinn
435             Saemundsson's article in "Sky and Telescope", volume 72, page 70
436             (July 1986) as reported Jean Meeus' "Astronomical Algorithms",
437             2nd Edition, chapter 16, page 106, and includes the adjustment
438             suggested by Meeus.
439              
440             =cut
441              
442             sub correct_for_refraction {
443 21077     21077 1 31147 my $self = shift;
444 21077         26109 my $elevation = shift;
445              
446             # If called as a static method, our effective horizon is zero. If
447             # called as a normal method, our effective horizon is the lesser of
448             # the 'horizon' setting or zero.
449              
450 21077 100       49140 my $horizon = ref $self ? min( 0, $self->get( 'horizon' ) ) : 0;
451              
452             # We exclude anything with an elevation <= 2 degrees below our
453             # effective horizon; this is presumed to be not visible, since the
454             # maximum deflection is about 35 minutes of arc. This is not
455             # portable to (e.g.) Venus.
456              
457 21077 100       51022 if ( $elevation > $horizon - TWO_DEGREES ) {
458              
459             # Thorsteinn Saemundsson's algorithm for refraction, as reported
460             # in Meeus, page 106, equation 16.4, and adjusted per the
461             # suggestion in Meeus' following paragraph. Thorsteinn's
462             # formula is in terms of angles in degrees and produces
463             # a correction in minutes of arc. Meeus reports the original
464             # publication as Sky and Telescope, volume 72 page 70, July 1986.
465              
466             # In deference to Thorsteinn I will point out:
467             # * The Icelanders do not use family names. The "Saemundsson"
468             # simply means his father's name was Saemund.
469             # * I have transcribed the names into 7-bit characters.
470             # "Thorsteinn" actually does not begin with "Th" but with
471             # the letter "Thorn." Similarly, the "ae" in "Saemund" is
472             # supposed to be a ligature (i.e. squished into one letter).
473              
474 5961         12883 my $deg = rad2deg ($elevation);
475 5961         15521 my $correction = 1.02 / tan (deg2rad ($deg + 10.3/($deg + 5.11))) +
476             .0019279;
477 5961 50       12195 $self->get ('debug') and print <
478             Debug correct_for_refraction
479             input: $deg degrees of arc
480             correction: $correction minutes of arc
481             eod
482              
483             # End of Thorsteinn's algorithm.
484              
485 5961         13852 $correction = deg2rad ($correction / 60);
486 5961         9147 $elevation += $correction;
487             }
488 21077         32944 return $elevation;
489             }
490              
491             =item $angle = $coord->dip ();
492              
493             This method calculates the dip angle of the horizon due to the
494             altitude of the body, in radians. It will be negative for a location
495             above the surface of the reference ellipsoid, and positive for a
496             location below the surface.
497              
498             The algorithm is simple enough to be the author's.
499              
500             =cut
501              
502             sub dip {
503 917     917 1 1470 my $self = shift;
504 917         1871 my (undef, undef, $h) = $self->geodetic ();
505 917         1842 my (undef, undef, $rho) = $self->geocentric ();
506 917 50       3312 return $h >= 0 ?
507             - acos (($rho - $h) / $rho) :
508             acos ($rho / ($rho - $h));
509             }
510              
511             =item $coord = $coord->dynamical ($time);
512              
513             This method sets the dynamical time represented by the object.
514              
515             This method can also be called as a class method, in which case it
516             instantiates the desired object.
517              
518             The algorithm for converting this to universal time comes from Jean
519             Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 10, pages 78ff.
520              
521             =item $time = $coord->dynamical ();
522              
523             This method returns the dynamical time previously set, or the
524             universal time previously set, converted to dynamical.
525              
526             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
527             Edition, Chapter 10, pages 78ff.
528              
529             =cut
530              
531             sub dynamical {
532 58444     58444 1 96819 my ($self, @args) = @_;
533 58444 100       107920 unless (@args) {
534 58315 50       111393 ref $self or croak <
535             Error - The dynamical() method may not be called as a class method
536             unless you specify arguments.
537             eod
538             return ($self->{dynamical} ||= $self->{universal} +
539 58315   33     181976 dynamical_delta ($self->{universal} || croak <
      66        
540             Error - Universal time of object has not been set.
541             eod
542             }
543              
544 129 50       276 if (@args == 1) {
545 129         263 my $time = shift @args;
546 129 100       305 $self = $self->new () unless ref $self;
547 129         267 $self->{_no_set}++; # Supress running the model if any
548 129         336 $self->universal ($time - dynamical_delta ($time));
549 129         236 $self->{dynamical} = $time;
550 129         221 --$self->{_no_set}; # Undo supression of model
551 129         242 $self->_call_time_set (); # Run the model if any
552             } else {
553 0         0 croak <
554             Error - The dynamical() method must be called with either zero
555             arguments (to retrieve the time) or one argument (to set the
556             time).
557             eod
558             }
559              
560 129         397 return $self;
561             }
562              
563             =item $coord = $coord->ecef($x, $y, $z, $xdot, $ydot, $zdot)
564              
565             This method sets the coordinates represented by the object in terms of
566             L in kilometers, with
567             the x axis being latitude 0 longitude 0, the y axis being latitude 0
568             longitude 90 degrees east, and the z axis being latitude 90 degrees
569             north. The velocities in kilometers per second are optional, and will
570             default to zero. ECEF velocities are considered to be relative to the
571             surface of the Earth; when converting to ECI, the rotational velocity of
572             the Earth will be added in.
573              
574             B that prior to version 0.022_01, the documentation said that the
575             default velocity was the rotational velocity of the Earth. This was not
576             correct B The functionality of the code itself in
577             this respect did not change.
578              
579             The object itself is returned.
580              
581             This method can also be called as a class method, in which case it
582             instantiates the desired object.
583              
584             =item ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->ecef();
585              
586             This method returns the object's L
587             coordinates>.
588              
589             If the original coordinate setting was in an inertial system (e.g. eci,
590             equatorial, or ecliptic) B the absolute difference between the
591             current value of 'equinox_dynamical' and the current dynamical() setting
592             is greater than the value of $Astro::Coord::ECI::EQUINOX_TOLERANCE, the
593             coordinates will be precessed to the current dynamical time before
594             conversion. Yes, this should be done any time the equinox is not the
595             current time, but for satellite prediction precession by a year or
596             so does not seem to make much difference in practice. The default value
597             of $Astro::Coord:ECI::EQUINOX_TOLERANCE is 365 days. B that if
598             this behavior or the default value of $EQUINOX_TOLERANCE begins to look
599             like a bug, it will be changed, and noted in the documentation.
600              
601             Some velocity conversions involving C appear to me to be mostly
602             sane. See L, below, for
603             details.
604              
605             =cut
606              
607             sub ecef {
608 57640     57640 1 93103 my ($self, @args) = @_;
609              
610 57640         106818 $self = $self->_check_coord (ecef => \@args);
611              
612 57640 100       118156 unless (@args) {
613 55526   50     109993 my $cache = ($self->{_ECI_cache} ||= {});
614 55526 100       112683 return @{$cache->{fixed}{ecef}} if $cache->{fixed}{ecef};
  25605         60046  
615 29921 50       72691 return $self->_convert_eci_to_ecef () if $self->{inertial};
616 0         0 croak <
617             Error - Object has not been initialized.
618             eod
619             }
620              
621 2114 50       6610 @args == 3 and push @args, 0, 0, 0;
622              
623 2114 50       3900 if (@args == 6) {
624 2114         3531 foreach my $key (@kilatr) {delete $self->{$key}}
  10570         21693  
625             ## $self->{_ECI_cache}{fixed}{ecef} = \@args;
626 2114         6344 $self->{_ECI_cache} = {fixed => {ecef => \@args}};
627 2114         4097 $self->{specified} = 'ecef';
628 2114         3740 $self->{inertial} = 0;
629             } else {
630 0         0 croak <
631             Error - The ecef() method must be called with either zero arguments (to
632             retrieve coordinates), three arguments (to set coordinates,
633             with velocity defaulting to zero) or six arguments.
634             eod
635             }
636              
637 2114         3577 return $self;
638             }
639              
640             =item $coord = $coord->eci ($x, $y, $z, $xdot, $ydot, $zdot, $time)
641              
642             This method sets the coordinates represented by the object in terms of
643             L in kilometers, time being
644             universal time, the x axis being 0 hours L 0 degrees
645             L, y being 6 hours L 0 degrees
646             L, and z being 90 degrees north L. The
647             velocities in kilometers per second are optional. If omitted, the object
648             will be considered not to have a velocity. B
649             with version 0.022_01.> Before that, the velocity defaulted to 0.
650              
651             The time argument is optional if the time represented by the object
652             has already been set (e.g. by the universal() or dynamical() methods).
653              
654             The object itself is returned.
655              
656             This method can also be called as a class method, in which case it
657             instantiates the desired object. In this case the time is not optional.
658              
659             The algorithm for converting from ECI to geocentric coordinates and
660             back is based on the description of ECI coordinates in T. S. Kelso's
661             "Computers and Satellites" column in "Satellite Times",
662             September/October 1995, titled "Orbital Coordinate Systems, Part I"
663             and available at F.
664              
665             =item ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->eci($time);
666              
667             This method returns the L
668             of the object at the given time. The time argument is actually
669             optional if the time represented by the object has already been set.
670              
671             If you specify a time, the time represented by the object will be set
672             to that time. The net effect of specifying a time is equivalent to
673              
674             ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->universal($time)->eci()
675              
676             If the original coordinate setting was in a non-inertial system (e.g.
677             ECEF or geodetic), the equinox_dynamical attribute will be set to the
678             object's dynamical time.
679              
680             Velocities will be returned if they were originally provided. B
681             a change introduced in version 0.022_01.> Prior to that version,
682             velocities were always returned.
683              
684             Some velocity conversions involving C appear to me to be mostly
685             sane. See L, below, for
686             details.
687              
688             =cut
689              
690             sub eci {
691 66903     66903 1 137537 my ($self, @args) = @_;
692              
693 66903         137423 $self = $self->_check_coord (eci => \@args);
694              
695 66903 100       132004 unless (@args) {
696 32740   50     63793 my $cache = ($self->{_ECI_cache} ||= {});
697 32740 100       63879 return @{$cache->{inertial}{eci}} if $cache->{inertial}{eci};
  32733         97298  
698 7 50       42 return $self->_convert_ecef_to_eci () if $self->{specified};
699 0         0 croak <
700             Error - Object has not been initialized.
701             eod
702              
703             }
704              
705             ## @args == 3 and push @args, 0, 0, 0;
706              
707 34163 50 66     93724 if (@args == 3 || @args == 6) {
708 34163         56609 foreach my $key (@kilatr) {delete $self->{$key}}
  170815         296091  
709 34163         91677 $self->{_ECI_cache} = {inertial => {eci => \@args}};
710 34163         62740 $self->{specified} = 'eci';
711 34163         52148 $self->{inertial} = 1;
712             } else {
713 0         0 croak <
714             Error - The eci() method must be called with either zero or one
715             arguments (to retrieve coordinates), three or four arguments
716             (to set coordinates, with velocity defaulting to zero), or
717             six or seven arguments.
718             eod
719             }
720 34163         58227 return $self;
721             }
722              
723             =item $coord = $coord->ecliptic_cartesian( $X, $Y, $Z, $time );
724              
725             This method sets the Cartesian L coordinates represented by
726             the object in terms of C (kilometers toward the vernal equinox), C
727             (90 degrees along the ecliptic from C), and C (toward ecliptic
728             North). The time is universal time. The object itself is returned.
729              
730             The time argument is optional if the time represented by the object has
731             already been set (e.g. by the universal() or dynamical() methods).
732              
733             This method can also be called as a class method, in which case it
734             instantiates the desired object. In this case the time is not optional.
735              
736             B you can pass in velocities (before the C<$time>) but they are
737             unsupported, meaning that I can not at this point say whether they will
738             be transformed sanely, much less correctly. B.
739              
740             =item ( $X, $Y, $Z) = $coord->ecliptic_cartesian( $time );
741              
742             This method returns the Cartesian ecliptic coordinates of the object at
743             the given time. The time is optional if the time represented by the
744             object has already been set (e.g. by the universal() or dynamical()
745             methods).
746              
747             B velocities are unsupported by this method. That means you may
748             (or may not, depending on the coordinates originally set) get them back,
749             but I have not looked into whether they are actually correct.
750              
751             =cut
752              
753             sub ecliptic_cartesian {
754 14420     14420 1 33227 my ( $self, @args ) = @_;
755              
756 14420         26207 $self = $self->_check_coord( ecliptic_cartesian => \@args );
757              
758 14420 100       25492 if ( @args ) {
759 13058 50       25582 @args % 3
760             and croak <<'EOD';
761             Error - The ecliptic_cartesian() method must be called with either zero
762             or one arguments (to retrieve coordinates), or three or four
763             arguments (to set coordinates). There is currently no six or
764             seven argument version.
765             EOD
766 13058         24568 my $epsilon = $self->obliquity();
767 13058         19936 my $sin_epsilon = sin $epsilon;
768 13058         18403 my $cos_epsilon = cos $epsilon;
769 13058         16770 my @eci;
770 13058         29587 for ( my $inx = 0; $inx < @args; $inx += 3 ) {
771 13058         21806 push @eci, $args[ $inx ];
772 13058         26767 push @eci, - $args[ $inx + 2 ] * $sin_epsilon +
773             $args[ $inx + 1 ] * $cos_epsilon;
774 13058         33036 push @eci, $args[ $inx + 2 ] * $cos_epsilon +
775             $args[ $inx + 1 ] * $sin_epsilon;
776             }
777 13058         31482 $self->eci( @eci );
778 13058         23373 $self->{_ECI_cache}{inertial}{ecliptic_cartesian} = \@args;
779 13058         19956 $self->{inertial} = 1;
780 13058         18286 $self->{specified} = 'ecliptic_cartesian';
781 13058         25190 return $self;
782             } else {
783             $self->{_ECI_cache}{inertial}{ecliptic_cartesian}
784             and return @{
785 1362 100       2633 $self->{_ECI_cache}{inertial}{ecliptic_cartesian}
786 237         669 };
787 1125         2163 my @eci = $self->eci();
788 1125         2211 my $epsilon = $self->obliquity();
789 1125         1714 my $sin_epsilon = sin $epsilon;
790 1125         1660 my $cos_epsilon = cos $epsilon;
791 1125         1425 my @ecliptic_cartesian;
792 1125         2430 for ( my $inx = 0; $inx < @eci; $inx += 3 ) {
793 1125         2267 push @ecliptic_cartesian, $eci[ $inx ];
794 1125         2294 push @ecliptic_cartesian, $eci[ $inx + 2 ] * $sin_epsilon +
795             $eci[ $inx + 1 ] * $cos_epsilon;
796 1125         2882 push @ecliptic_cartesian, $eci[ $inx + 2 ] * $cos_epsilon -
797             $eci[ $inx + 1 ] * $sin_epsilon;
798             }
799             return @{
800 1125         1570 $self->{_ECI_cache}{inertial}{ecliptic_cartesian} =
801             \@ecliptic_cartesian
802 1125         4441 };
803             }
804              
805             }
806              
807             =item $coord = $coord->ecliptic ($latitude, $longitude, $range, $time);
808              
809             This method sets the L coordinates represented by the object
810             in terms of L and L in radians,
811             and the range to the object in kilometers, time being universal time.
812             The object itself is returned.
813              
814             The time argument is optional if the time represented by the object has
815             already been set (e.g. by the universal() or dynamical() methods).
816              
817             The latitude should be a number between -PI/2 and PI/2 radians
818             inclusive. The longitude should be a number between -2*PI and 2*PI
819             radians inclusive. The increased range (one would expect -PI to PI) is
820             because in some astronomical usages latitudes outside the range + or -
821             180 degrees are employed. A warning will be generated if either of these
822             range checks fails.
823              
824             This method can also be called as a class method, in which case it
825             instantiates the desired object. In this case the time is not optional.
826              
827             The algorithm for converting from ecliptic latitude and longitude to
828             right ascension and declination comes from Jean Meeus'
829             "Astronomical Algorithms", 2nd Edition, Chapter 13, page 93.
830              
831             =item ($latitude, $longitude, $range) = $coord->ecliptic ($time);
832              
833             This method returns the ecliptic latitude and longitude of the
834             object at the given time. The time is optional if the time represented
835             by the object has already been set (e.g. by the universal() or
836             dynamical() methods).
837              
838             B velocities are not returned by this method.
839              
840             =cut
841              
842             sub ecliptic {
843 14436     14436 1 29368 my ($self, @args) = @_;
844              
845 14436         28662 $self = $self->_check_coord( ecliptic => \@args );
846              
847 14436 100       26088 if ( @args ) {
848              
849 13058 50       24950 @args % 3
850             and croak 'Arguments are in threes, plus an optional time';
851 13058         27772 $self->{_ECI_cache}{inertial}{ecliptic} = \@args;
852 13058         28150 $self->ecliptic_cartesian(
853             _convert_spherical_x_to_cartesian( @args ) );
854 13058         21734 $self->{specified} = 'ecliptic';
855 13058         17262 $self->{inertial} = 1;
856 13058         25960 return $self;
857              
858             } else {
859              
860 1378         1782 return @{ $self->{_ECI_cache}{inertial}{ecliptic} ||= [
861 1378   100     4354 _convert_cartesian_to_spherical_x(
862             $self->ecliptic_cartesian() )
863             ]
864             };
865              
866             }
867             }
868              
869             =item $longitude = $coord->ecliptic_longitude();
870              
871             This method returns the ecliptic longitude of the body at its current
872             time setting, in radians. It is really just a convenience method, since
873             it is equivalent to C<< ( $coord->ecliptic() )[1] >>, and in fact that
874             is how it is implemented.
875              
876             =cut
877              
878             sub ecliptic_longitude {
879 0     0 1 0 my ( $self ) = @_;
880 0         0 return ( $self->ecliptic() )[1];
881             }
882              
883             =item $seconds = $self->equation_of_time( $time );
884              
885             This method returns the equation of time at the given B
886             time. If the time is C, the invocant's dynamical time is used.
887              
888             The algorithm is from W. S. Smart's "Text-Book on Spherical Astronomy",
889             as reported in Jean Meeus' "Astronomical Algorithms", 2nd Edition,
890             Chapter 28, page 185.
891              
892             =cut
893              
894             sub equation_of_time {
895 1     1 1 343 my ( $self, $time ) = @_;
896              
897 1 50       6 if ( looks_like_number( $self ) ) {
898 0         0 ( $self, $time ) = ( __PACKAGE__, $self );
899 0         0 __subroutine_deprecation();
900             ## Carp::cluck( 'Subroutine call to equation_of_time() is deprecated' );
901             }
902 1 50       4 defined $time
903             or $time = $self->dynamical();
904              
905 1         3 my $epsilon = $self->obliquity( $time );
906 1         4 my $y = tan($epsilon / 2);
907 1         3 $y *= $y;
908              
909             # The following algorithm is from Meeus, chapter 25, page, 163 ff.
910              
911 1         13 my $T = jcent2000( $time ); # Meeus (25.1)
912 1         5 my $L0 = mod2pi( deg2rad( (.0003032 * $T + 36000.76983 ) * $T
913             + 280.46646 ) ); # Meeus (25.2)
914 1         4 my $M = mod2pi( deg2rad( ( ( -.0001537 ) * $T + 35999.05029 )
915             * $T + 357.52911 ) ); # Meeus (25.3)
916 1         4 my $e = ( -0.0000001267 * $T - 0.000042037 ) * $T
917             + 0.016708634; # Meeus (25.4)
918              
919 1         11 my $E = $y * sin( 2 * $L0 ) - 2 * $e * sin( $M ) +
920             4 * $e * $y * sin( $M ) * cos( 2 * $L0 ) -
921             $y * $y * .5 * sin( 4 * $L0 ) -
922             1.25 * $e * $e * sin( 2 * $M ); # Meeus (28.3)
923              
924 1         4 return $E * SECSPERDAY / TWOPI; # The formula gives radians.
925             }
926              
927             =item $coord->equatorial ($rightasc, $declin, $range, $time);
928              
929             This method sets the L coordinates represented by the
930             object (relative to the center of the Earth) in terms of
931             L and L in radians, and the range to the
932             object in kilometers, time being universal time. The object itself is
933             returned.
934              
935             If C is called in this way, the C attribute will
936             be ignored, for historical reasons.
937              
938             The right ascension should be a number between 0 and 2*PI radians
939             inclusive. The declination should be a number between -PI/2 and PI/2
940             radians inclusive. A warning will be generated if either of these range
941             checks fails.
942              
943             The time argument is optional if the time represented by the object
944             has already been set (e.g. by the universal() or dynamical() methods).
945              
946             This method can also be called as a class method, in which case it
947             instantiates the desired object. In this case the time is not optional.
948              
949             You may optionally pass velocity information in addition to position
950             information. If you do this, the velocity in right ascension (in radians
951             per second), declination (ditto), and range (in kilometers per second in
952             recession) are passed after the position arguments, and before the $time
953             argument if any.
954              
955             =item ( $rightasc, $declin, $range ) = $coord->equatorial( $time );
956              
957             This method returns the L coordinates of the object at the
958             relative to the center of the Earth. The C attribute is
959             ignored. The time argument is optional if the time represented by the
960             object has already been set (e.g. by the universal() or dynamical()
961             methods).
962              
963             If velocities are available from the object (i.e. if it is an instance
964             of Astro::Coord::ECI::TLE) the return will contain velocity in right
965             ascension, declination, and range, the first two being in radians per
966             second and the third in kilometers per second in recession.
967              
968             B these velocities are believed by me to be sane B they are
969             derived from ECI coordinates. This method does not support setting
970             velocities. See L, below,
971             for details.
972              
973             =item ($rightasc, $declin, $range) = $coord->equatorial( $coord2 );
974              
975             This method is retained (for the moment) for historical reasons, but
976             C is preferred.
977              
978             This method returns the apparent equatorial coordinates of the object
979             represented by $coord2, as seen from the location represented by
980             $coord.
981              
982             As a side effect, the time of the $coord object may be set from the
983             $coord2 object.
984              
985             If the C attribute of the $coord object is
986             true, the coordinates will be corrected for atmospheric refraction using
987             the correct_for_refraction() method.
988              
989             If velocities are available from both objects (i.e. if both objects are
990             Astro::Coord::ECI::TLE objects) the return will contain velocity in
991             right ascension, declination, and range, the first two being in radians
992             per second and the third in kilometers per second in recession.
993              
994             B these velocities are believed by me B to be correct.
995              
996             =cut
997              
998             sub equatorial {
999 3388     3388 1 6391 my ( $self, @args ) = @_;
1000              
1001 3388         4216 my $body;
1002             @args
1003 3388 100 100     8813 and embodies( $args[0], __PACKAGE__ )
1004             and $body = shift @args;
1005              
1006 3388         7213 $self = $self->_check_coord( equatorial => \@args );
1007 3388         4681 my $time;
1008 3388 100       7679 $body or $time = $self->universal;
1009              
1010 3388 100       6161 if ( @args ) {
    100          
1011 2253 50       4342 @args % 3
1012             and croak 'Arguments must be in threes, with an optional time';
1013 2253 50       4075 $body
1014             and croak 'You may not set the equatorial coordinates ',
1015             'relative to an observer';
1016              
1017             ## my ($ra, $dec, $range, @eqvel) = @args;
1018 2253         4029 $args[0] = _check_right_ascension( 'right ascension' => $args[0] );
1019 2253         4146 $args[1] = _check_latitude( declination => $args[1] );
1020 2253         4077 foreach my $key (@kilatr) {delete $self->{$key}}
  11265         19302  
1021 2253         5463 $self->{_ECI_cache}{inertial}{equatorial} = \@args;
1022 2253         4638 $self->eci(
1023             _convert_spherical_to_cartesian( @args ) );
1024 2253         3666 $self->{specified} = 'equatorial';
1025 2253         2971 $self->{inertial} = 1;
1026 2253         5000 return $self;
1027              
1028             } elsif ( $body ) {
1029              
1030 3         12 return $self->_equatorial_reduced( $body );
1031              
1032             } else {
1033              
1034 1132         1449 return @{ $self->{_ECI_cache}{inertial}{equatorial} ||= [
1035 1132   50     3305 _convert_cartesian_to_spherical( $self->eci() )
1036             ] };
1037              
1038             }
1039             }
1040              
1041             =item ($ra, $decl, $rng) = $coord->equatorial_apparent( $sta );
1042              
1043             This method returns the apparent equatorial coordinates of the C<$coord>
1044             object as seen from the object specified by the C<$sta> argument, or by
1045             the object in the C attribute of the C<$coord> object if no
1046             argument is specified.
1047              
1048             This method will return velocities if available, but I have no idea
1049             whether they are correct.
1050              
1051             =cut
1052              
1053             sub equatorial_apparent {
1054 1     1 1 6 my ( $self, $station ) = @_;
1055 1 50 33     5 ( $station ||= $self->get( 'station' ) )
1056             or croak 'Station attribute is required';
1057 1         4 return $station->_equatorial_reduced( $self );
1058             }
1059              
1060             =item my ($rasc, $decl, $range, $v_rasc, $v_decl, $v_r) = $coord->equatorial_unreduced($body);
1061              
1062             This method computes the unreduced equatorial position of the second ECI
1063             object as seen from the first. If the second argument is undefined,
1064             computes the equatorial position of the first object as seen from the
1065             center of the Earth. Unlike the equatorial() method itself, this method
1066             is an accessor only. This method would probably not be exposed except
1067             for the anticipation of the usefulness of $range and $v_r in satellite
1068             conjunction computations, and the desirability of not taking the time to
1069             make the two atan2() calls that are unneeded in this application.
1070              
1071             The 'unreduced' in the name of the method is intended to refer to the
1072             fact that the $rasc and $decl are not the right ascension and
1073             declination themselves, but the arguments to atan2() needed to compute
1074             them, and $v_rasc and $v_decl are in km/sec, rather than being divided
1075             by the range to get radians per second.
1076              
1077             This method ignores the C<'station'> attribute.
1078              
1079             The returned data are:
1080              
1081             $rasc is an array reference, which can be converted to the right
1082             ascension in radians by mod2pi(atan2($rasc->[0], $rasc->[1])).
1083              
1084             $decl is an array reference, which can be converted to the declination
1085             in radians by atan2($decl->[0], $decl->[1]).
1086              
1087             $range is the range in kilometers.
1088              
1089             $v_rasc is the velocity in the right ascensional direction in kilometers
1090             per second. It can be converted to radians per second by dividing by
1091             $range.
1092              
1093             $v_decl is the velocity in the declinational direction in kilometers per
1094             second. It can be converted to radians per second by dividing by $range.
1095              
1096             $v_r is the velocity in recession in kilometers per second. Negative
1097             values indicate that the objects are approaching.
1098              
1099             The velocities are only returned if they are available from the input
1100             objects.
1101              
1102             B these velocities are believed by me B to be correct.
1103              
1104             =cut
1105              
1106             sub equatorial_unreduced {
1107              
1108             # Unpack the two objects.
1109              
1110 4     4 1 10 my ($self, $body) = @_;
1111              
1112             # Compute the relative positions if there are in fact two objects;
1113             # otherwise just get the absolute position.
1114              
1115 4         6 my @pos;
1116 4 50       10 if ($body) {
1117 4         9 @pos = $body->eci();
1118 4         11 my @base = $self->eci($body->universal());
1119 4 50       16 my $limit = @pos < @base ? @pos : @base;
1120 4         14 foreach my $inx (0 .. $limit - 1) {
1121 21         382 $pos[$inx] -= $base[$inx];
1122             }
1123 4         14 splice @pos, $limit;
1124             } else {
1125             $self->{_ECI_cache}{inertial}{equatorial_unreduced} and return
1126 0 0       0 @{$self->{_ECI_cache}{inertial}{equatorial_unreduced}};
  0         0  
1127 0         0 @pos = $self->eci();
1128             }
1129              
1130             # Rotate the coordinate system so that the second body lies in the
1131             # X-Z plane. The matrix is
1132             # +- -+
1133             # | cos rasc -sin rasc 0 |
1134             # | sin rasc cos rasc 0 |
1135             # | 0 0 1 |
1136             # +- -+
1137             # where rasc is -atan2(y,x). This means sin rasc = -y/h and
1138             # cos rasc = x/h where h = sqrt(x*x + y*y). You postmultiply
1139             # the matrix by the vector to get the new vector.
1140              
1141 4         13 my $hypot_rasc = sqrt($pos[0] * $pos[0] + $pos[1] * $pos[1]);
1142              
1143             # Now we need to rotate the coordinates in the new X-Z plane until
1144             # the second body lies along the X axis. The matrix is
1145             # +- -+
1146             # | cos decl 0 -sin decl |
1147             # | 0 1 0 |
1148             # | sin decl 0 cos decl |
1149             # +- -+
1150             # where decl is -atan2(z,x') (in the new coordinate system), or
1151             # -atan2(z,h) in the old coordinate system. This means that sin decl
1152             # = z/h' and cos decl = x/h' where h' = sqrt(h*h + z*z). Again you
1153             # postmultiply the matrix by the vector to get the result.
1154              
1155 4         10 my $hypot_decl = sqrt($hypot_rasc * $hypot_rasc + $pos[2] * $pos[2]);
1156              
1157             # But at this point we have the equatorial coordinates themselves in
1158             # terms of the original coordinates and the various intermediate
1159             # quantities needed to compute the matrix. So we only need the
1160             # matrix if we need to deal with the velocities. For this, the
1161             # velocity rotation matrix is
1162             # +- -+ +- -+
1163             # | cos decl 0 -sin decl | | cos rasc -sin rasc 0 |
1164             # | 0 1 0 | x | sin rasc cos rasc 0 | =
1165             # | sin decl 0 cos decl | | 0 0 1 |
1166             # +- -+ +- -+
1167             #
1168             # +- -+
1169             # | cos decl cos rasc -cos decl sin rasc -sin decl |
1170             # | sin rasc cos rasc 0 |
1171             # | sin decl cos rasc -sin decl sin rasc cos decl |
1172             # +- +
1173              
1174 4         23 my @rslt = ([$pos[1], $pos[0]], [$pos[2], $hypot_rasc], $hypot_decl);
1175 4 100       16 if (@pos >= 6) {
1176 3         14 my $cos_rasc = $pos[0]/$hypot_rasc;
1177 3         10 my $sin_rasc = -$pos[1]/$hypot_rasc;
1178 3         6 my $cos_decl = $hypot_rasc/$hypot_decl;
1179 3         7 my $sin_decl = -$pos[2]/$hypot_decl;
1180 3         15 push @rslt,
1181             $sin_rasc * $pos[3] + $cos_rasc * $pos[4],
1182             $sin_decl * $cos_rasc * $pos[3]
1183             - $sin_decl * $sin_rasc * $pos[4] + $cos_decl * $pos[5],
1184             # Computationally the following is the top row of the
1185             # matrix, but it is swapped to the bottom in the output for
1186             # consistency of returned data sequence.
1187             $cos_decl * $cos_rasc * $pos[3]
1188             - $cos_decl * $sin_rasc * $pos[4] - $sin_decl * $pos[5];
1189             }
1190             $body
1191 4 50       19 or $self->{_ECI_cache}{inertial}{equatorial_unreduced} = \@rslt;
1192 4         25 return @rslt;
1193              
1194             }
1195              
1196             sub _equatorial_reduced {
1197 4     4   15 my ( $self, $body ) = @_;
1198 4         13 my @rslt = $self->equatorial_unreduced( $body );
1199 4         23 $rslt[0] = mod2pi( atan2( $rslt[0][0], $rslt[0][1] ) );
1200 4         13 $rslt[1] = atan2( $rslt[1][0], $rslt[1][1] );
1201 4 100       14 if (@rslt >= 6) {
1202 3         6 $rslt[3] /= $rslt[2];
1203 3         6 $rslt[4] /= $rslt[2];
1204             }
1205 4         18 return @rslt;
1206             }
1207              
1208             =item $coord = $coord->equinox_dynamical ($value);
1209              
1210             This method sets the value of the equinox_dynamical attribute, and
1211             returns the modified object. If called without an argument, it returns
1212             the current value of the equinox_dynamical attribute.
1213              
1214             Yes, it is equivalent to $coord->set (equinox_dynamical => $value) and
1215             $coord->get ('equinox_dynamical'). But there seems to be a significant
1216             performance penalty in the $self->SUPER::set () needed to get this
1217             attribute set from a subclass. It is possible that more methods like
1218             this will be added, but I do not plan to eliminate the set() interface.
1219              
1220             =cut
1221              
1222             sub equinox_dynamical {
1223 31897 50   31897 1 59902 if (@_ > 1) {
1224 31897         51219 $_[0]{equinox_dynamical} = $_[1];
1225 31897         53676 return $_[0];
1226             } else {
1227 0         0 return $_[0]{equinox_dynamical};
1228             }
1229             }
1230              
1231             =item $coord = $coord->geocentric($psiprime, $lambda, $rho);
1232              
1233             This method sets the L coordinates represented by the
1234             object in terms of L psiprime and L
1235             lambda in radians, and distance from the center of the Earth rho in
1236             kilometers.
1237              
1238             The latitude should be a number between -PI/2 and PI/2 radians
1239             inclusive. The longitude should be a number between -2*PI and 2*PI
1240             radians inclusive. The increased range (one would expect -PI to PI) is
1241             because in some astronomical usages latitudes outside the range + or -
1242             180 degrees are employed. A warning will be generated if either of these
1243             range checks fails.
1244              
1245             This method can also be called as a class method, in which case it
1246             instantiates the desired object.
1247              
1248             B because map
1249             latitude is L, measured in terms of the tangent of
1250             the reference ellipsoid, whereas geocentric coordinates are,
1251             essentially, spherical coordinates.
1252              
1253             The algorithm for conversion between geocentric and ECEF is the
1254             author's.
1255              
1256             =item ($psiprime, $lambda, $rho) = $coord->geocentric();
1257              
1258             This method returns the L, L, and
1259             distance to the center of the Earth.
1260              
1261             =cut
1262              
1263             sub geocentric {
1264 16664     16664 1 30132 my ($self, @args) = @_;
1265              
1266 16664         32030 $self = $self->_check_coord (geocentric => \@args);
1267              
1268 16664 100       33776 unless (@args) {
1269              
1270 14557 100       31502 if ( ! $self->{_ECI_cache}{fixed}{geocentric} ) {
1271              
1272             ## my ($x, $y, $z, $xdot, $ydot, $zdot) = $self->ecef;
1273 6700         12522 my ($x, $y, $z) = $self->ecef;
1274 6700         13222 my $rsq = $x * $x + $y * $y;
1275 6700         11776 my $rho = sqrt ($z * $z + $rsq);
1276 6700         14195 my $lambda = atan2 ($y, $x);
1277 6700         11236 my $psiprime = atan2 ($z, sqrt ($rsq));
1278 6700 50       18336 $self->get ('debug') and print <
1279             Debug geocentric () - ecef -> geocentric
1280             inputs:
1281             x = $x
1282             y = $y
1283             z = $z
1284             outputs:
1285             psiprime = $psiprime
1286             lambda = $lambda
1287             rho = $rho
1288             eod
1289             $self->{_ECI_cache}{fixed}{geocentric} =
1290 6700         18349 [ $psiprime, $lambda, $rho ];
1291             }
1292              
1293 14557         19738 return @{ $self->{_ECI_cache}{fixed}{geocentric} };
  14557         39521  
1294             }
1295              
1296 2107 50       4080 if (@args == 3) {
1297 2107         3962 my ($psiprime, $lambda, $rho) = @args;
1298 2107         3818 $psiprime = _check_latitude(latitude => $psiprime);
1299 2107         3857 $lambda = _check_longitude(longitude => $lambda);
1300 2107         3806 my $z = $rho * sin ($psiprime);
1301 2107         3415 my $r = $rho * cos ($psiprime);
1302 2107         3396 my $x = $r * cos ($lambda);
1303 2107         3466 my $y = $r * sin ($lambda);
1304 2107 50       4594 $self->get ('debug') and print <
1305             Debug geocentric () - geocentric -> ecef
1306             inputs:
1307             psiprime = $psiprime
1308             lambda = $lambda
1309             rho = $rho
1310             outputs:
1311             x = $x
1312             y = $y
1313             z = $z
1314             eod
1315 2107         6424 $self->ecef ($x, $y, $z);
1316 2107         3906 $self->{_ECI_cache}{fixed}{geocentric} = \@args;
1317 2107         3436 $self->{specified} = 'geocentric';
1318 2107         3736 $self->{inertial} = 0;
1319             } else {
1320 0         0 croak <
1321             Error - Method geocentric() must be called with either zero arguments
1322             (to retrieve coordinates) or three arguments (to set
1323             coordinates). There is currently no six argument version.
1324             eod
1325             }
1326 2107         3232 return $self;
1327             }
1328              
1329             =item $coord = $coord->geodetic($psi, $lambda, $h, $ellipsoid);
1330              
1331             This method sets the L coordinates represented by the object
1332             in terms of its L psi and L lambda in
1333             radians, and its height h above mean sea level in kilometers.
1334              
1335             The latitude should be a number between -PI/2 and PI/2 radians
1336             inclusive. The longitude should be a number between -2*PI and 2*PI
1337             radians inclusive. The increased range (one would expect -PI to PI) is
1338             because in some astronomical usages latitudes outside the range + or -
1339             180 degrees are employed. A warning will be generated if either of these
1340             range checks fails.
1341              
1342             The ellipsoid argument is the name of a L known
1343             to the class, and is optional. If passed, it will set the ellipsoid
1344             to be used for calculations with this object. If not passed, the
1345             default ellipsoid is used.
1346              
1347             This method can also be called as a class method, in which case it
1348             instantiates the desired object.
1349              
1350             The conversion from geodetic to geocentric comes from Jean Meeus'
1351             "Astronomical Algorithms", 2nd Edition, Chapter 11, page 82.
1352              
1353             B
1354              
1355             =item ($psi, $lambda, $h) = $coord->geodetic($ellipsoid);
1356              
1357             This method returns the geodetic latitude, longitude, and height
1358             above mean sea level.
1359              
1360             The ellipsoid argument is the name of a L known
1361             to the class, and is optional. If not specified, the most-recently-set
1362             ellipsoid will be used.
1363              
1364             The conversion from geocentric to geodetic comes from Kazimierz
1365             Borkowski's "Accurate Algorithms to Transform Geocentric to Geodetic
1366             Coordinates", at F.
1367             This is best viewed with Internet Explorer because of its use of Microsoft's
1368             Symbol font.
1369              
1370             =cut
1371              
1372             sub geodetic {
1373 27116     27116 1 1018078 my ($self, @args) = @_;
1374              
1375             # Detect and acquire the optional ellipsoid name argument. We do
1376             # this before the check, since the check expects the extra
1377             # argument to be a time.
1378              
1379 27116 50 33     95394 my $elps = (@args == 1 || @args == 4) ? pop @args : undef;
1380              
1381 27116 50       68840 $self = $self->_check_coord (geodetic => \@args, $elps ? $elps : ());
1382              
1383             # The following is just a sleazy way to get a consistent
1384             # error message if the ellipsoid name is unknown.
1385              
1386 27116 50       53965 $elps && $self->reference_ellipsoid ($elps);
1387              
1388             # If we're fetching the geodetic coordinates
1389            
1390 27116 100       51379 unless (@args) {
1391              
1392             # Return cached coordinates if they exist and we did not
1393             # override the default ellipsoid.
1394              
1395 23171         68042 return @{$self->{_ECI_cache}{fixed}{geodetic}}
1396 25011 100 66     87485 if $self->{_ECI_cache}{fixed}{geodetic} && !$elps;
1397 1840 50       3491 $self->{debug} and do {
1398 0         0 require Data::Dumper;
1399 0         0 local $Data::Dumper::Terse = 1;
1400 0         0 print "Debug geodetic - explicit ellipsoid ",
1401             Data::Dumper::Dumper( $elps );
1402             };
1403              
1404             # Get a reference to the ellipsoid data to use.
1405              
1406 1840 50       3229 $elps = $elps ? $known_ellipsoid{$elps} : $self;
1407 1840 50       3261 $self->{debug} and do {
1408 0         0 require Data::Dumper;
1409 0         0 local $Data::Dumper::Terse = 1;
1410 0         0 print "Debug geodetic - ellipsoid ", Data::Dumper::Dumper( $elps );
1411             };
1412              
1413             # Calculate geodetic coordinates.
1414              
1415 1840         3374 my ($phiprime, $lambda, $rho) = $self->geocentric;
1416 1840         3573 my $r = $rho * cos ($phiprime);
1417 1840         3219 my $b = $elps->{semimajor} * (1- $elps->{flattening});
1418 1840         2545 my $a = $elps->{semimajor};
1419 1840         3053 my $z = $rho * sin ($phiprime);
1420             # The $b is _not_ a magic variable, since we are not inside
1421             # any of the specialized blocks that make $b magic. Perl::Critic
1422             # is simply confused.
1423 1840 100       3696 $b = - $b ## no critic (RequireLocalizedPunctuationVars)
1424             if $z < 0; # Per Borkowski, for southern hemisphere.
1425              
1426             # The following algorithm is due to Kazimierz Borkowski's
1427             # paper "Accurate Algorithms to Transform Geocentric to Geodetic
1428             # Coordinates", from
1429             # http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm
1430              
1431 1840         2490 my $bz = $b * $z;
1432 1840         2856 my $asq_bsq = $a * $a - $b * $b;
1433 1840         2614 my $ar = $a * $r;
1434 1840         2829 my $E = ($bz - $asq_bsq) / $ar; # Borkowski (10)
1435 1840         2676 my $F = ($bz + $asq_bsq) / $ar; # Borkowski (11)
1436 1840         2805 my $Q = ($E * $E - $F * $F) * 2; # Borkowski (17)
1437 1840         2871 my $P = ($E * $F + 1) * 4 / 3; # Borkowski (16)
1438 1840         3191 my $D = $P * $P * $P + $Q * $Q; # Borkowski (15)
1439             my $v = $D >= 0 ? do {
1440 1840         3182 my $d = sqrt $D;
1441 1840         2597 my $onethird = 1 / 3;
1442 1840         5458 my $vp = ($d - $Q) ** $onethird - # Borkowski (14a)
1443             ($d + $Q) ** $onethird;
1444 1840 50       5294 $vp * $vp >= abs ($P) ? $vp :
1445             - ($vp * $vp * $vp + 2 * $Q) / # Borkowski (20)
1446             (3 * $P);
1447 1840 50       3307 } : do {
1448 0         0 my $p = - $P;
1449 0         0 sqrt (cos (acos ($Q / # Borkowski (14b)
1450             sqrt ($p * $p * $p)) / 3) * $p) * 2;
1451             };
1452 1840         3372 my $G = (sqrt ($E * $E + $v) # Borkowski (13)
1453             + $E) / 2;
1454 1840         3553 my $t = sqrt ($G * $G + ($F - $v * $G) # Borkowski (12)
1455             / (2 * $G - $E)) - $G;
1456              
1457             # Borkowski (18)
1458             # equivalent to atan (arg1)
1459 1840         2496 my $phi = do {
1460 1840         2830 my $Y = $a * ( 1 - $t * $t );
1461 1840         2604 my $X = 2 * $b * $t;
1462 1840 100       3462 ( $Y, $X ) = ( -$Y, -$X ) if $X < 0;
1463 1840         3364 atan2 $Y, $X;
1464             };
1465              
1466 1840         3582 my $h = ($r - $a * $t) * cos ($phi) + # Borkowski (19)
1467             ($z - $b) * sin ($phi);
1468              
1469             # End of Borkowski's algorthm.
1470              
1471             # Cache the results of the calculation if they were done using
1472             # the default ellipsoid.
1473              
1474 1840 50       3650 $self->{_ECI_cache}{fixed}{geodetic} = [$phi, $lambda, $h] unless $elps;
1475              
1476             # Return the results in any event.
1477              
1478 1840 50       4605 $self->get ('debug') and print <
1479             Debug geodetic: geocentric to geodetic
1480             inputs:
1481             phiprime = $phiprime
1482             lambda = $lambda
1483             rho = $rho
1484             intermediates:
1485             z = $z
1486             r = $r
1487             E = $E
1488             F = $F
1489             P = $P
1490             Q = $Q
1491             D = $D
1492             v = $v
1493             G = $G
1494             t = $t
1495             outputs:
1496             phi = atan2 (a * (1 - t * t), 2 * b * t)
1497 0         0 = atan2 (@{[$a * (1 - $t * $t)]}, @{[2 * $b * $t]})
  0         0  
1498             = $phi (radians)
1499             h = (r - a * t) * cos (phi) + (z - b) * sin (phi)
1500 0         0 = @{[$r - $a * $t]} * cos (phi) + @{[$z - $b]} * sin (phi)
  0         0  
1501             = $h (kilometers)
1502             eod
1503 1840         5266 return ($phi, $lambda, $h);
1504             }
1505              
1506             # If we're setting the geodetic coordinates.
1507              
1508 2105 50       3922 if (@args == 3) {
1509              
1510             # Set the ellipsoid for the object if one was specified.
1511              
1512 2105 50       3953 $self->set (ellipsoid => $elps) if $elps;
1513              
1514             # Calculate the geocentric data.
1515              
1516 2105         4319 my ($phi, $lambda, $h) = @args;
1517 2105         4019 $phi = _check_latitude(latitude => $phi);
1518 2105         4325 $lambda = _check_longitude(longitude => $lambda);
1519 2105         3893 my $bovera = 1 - $self->{flattening};
1520              
1521             # The following algorithm appears on page 82 of the second
1522             # edition of Jean Meeus' "Astronomical Algorithms."
1523              
1524 2105         4972 my $u = atan2 ($bovera * tan ($phi), 1);
1525             my $rhosinlatprime = $bovera * sin ($u) +
1526 2105         5393 $h / $self->{semimajor} * sin ($phi);
1527             my $rhocoslatprime = cos ($u) +
1528 2105         4123 $h / $self->{semimajor} * cos ($phi);
1529 2105         3374 my $phiprime = atan2 ($rhosinlatprime, $rhocoslatprime);
1530 2105 50       4867 my $rho = $self->{semimajor} * ($rhocoslatprime ?
1531             $rhocoslatprime / cos ($phiprime) :
1532             $rhosinlatprime / sin ($phiprime));
1533              
1534             # End of Meeus' algorithm.
1535              
1536             # Set the geocentric data as the coordinates.
1537              
1538 2105         5744 $self->geocentric ($phiprime, $lambda, $rho);
1539            
1540            
1541             # Cache the geodetic coordinates.
1542            
1543 2105         5127 $self->{_ECI_cache}{fixed}{geodetic} = [$phi, $lambda, $h];
1544 2105         3327 $self->{specified} = 'geodetic';
1545 2105         3394 $self->{inertial} = 0;
1546              
1547             # Else if the number of coordinates is bogus, croak.
1548              
1549             } else {
1550 0         0 croak <
1551             Error - Method geodetic() must be called with either zero arguments
1552             (to retrieve coordinates) or three arguments (to set
1553             coordinates). There is currently no six argument version.
1554             eod
1555             }
1556              
1557             # Return the object, wherever it came from.
1558              
1559 2105         6197 return $self;
1560              
1561             }
1562              
1563             =item $value = $coord->get ($attrib);
1564              
1565             This method returns the named attributes of the object. If called in
1566             list context, you can give more than one attribute name, and it will
1567             return all their values.
1568              
1569             If called as a class method, it returns the current default values.
1570              
1571             See L for a list of the attributes you can get.
1572              
1573             =cut
1574              
1575             { # Begin local symbol block.
1576              
1577             my %accessor = (
1578             sun => sub {
1579             ## my ( $self, $name ) = @_;
1580             my ( $self ) = @_; # Name unused
1581             $self->{sun}
1582             and return $self->{sun};
1583             my $sun_class = $self->SUN_CLASS();
1584             ( my $path = "$sun_class.pm" ) =~ s< :: >smxg;
1585             require $path;
1586             return( $self->{sun} ||= $sun_class->new() );
1587             },
1588             );
1589              
1590             sub get {
1591 99518     99518 1 183931 my ($self, @args) = @_;
1592 99518 100       180758 ref $self or $self = \%static;
1593 99518         121028 my @rslt;
1594 99518         152143 foreach my $name (@args) {
1595 99518 50       183454 exists $mutator{$name}
1596             or croak " Error - Attribute '$name' does not exist";
1597 99518 100       161891 if ($accessor{$name}) {
1598 1263         2425 push @rslt, $accessor{$name}->($self, $name);
1599             } else {
1600 98255         183919 push @rslt, $self->{$name};
1601             }
1602             }
1603 99518 100       275897 return wantarray ? @rslt : $rslt[0];
1604             }
1605              
1606             } # End local symbol block
1607              
1608             =item @coords = $coord->enu();
1609              
1610             This method reports the coordinates of C<$coord> in the East-North-Up
1611             coordinate system, as seen from the position stored in the C
1612             attribute of C<$coord>.
1613              
1614             Velocity conversions to C appear to me to be mostly sane. See
1615             L, below, for details.
1616              
1617             =cut
1618              
1619             sub enu { # East, North, Up
1620 3     3 1 7 my ( $self ) = @_;
1621 3         11 my @vector = $self->neu();
1622 3         8 @vector[ 0, 1 ] = @vector[ 1, 0 ]; # Swap North and East
1623 3 50       9 @vector > 3 # If we have velocity,
1624             and @vector[ 3, 4 ] = @vector[ 4, 3 ]; # Ditto
1625 3         11 return @vector;
1626             }
1627              
1628             =item @coords = $coord->neu();
1629              
1630             This method reports the coordinates of C<$coord2> in the North-East-Up
1631             coordinate system, as seen from the position stored in the C
1632             attribute of C<$coord>. This is a B coordinate system.
1633              
1634             Velocity conversions to C appear to me to be mostly sane. See
1635             L, below, for details.
1636              
1637             =cut
1638              
1639             sub neu { # North, East, Up
1640 6     6 1 10 my ( $self ) = @_;
1641 6 50       10 my $station = $self->get( 'station' )
1642             or croak 'Station attribute required';
1643 6         15 return $station->_local_cartesian( $self );
1644             }
1645              
1646             =item @coords = $coord->heliocentric_ecliptic_cartesian()
1647              
1648             This method returns the Heliocentric ecliptic Cartesian position of the
1649             object. You can optionally pass time as an argument; this is equivalent
1650             to
1651              
1652             @coords = $coord->universal( $time )
1653             ->heliocentric_ecliptic_cartesian();
1654              
1655             At this time this object is not able to derive Heliocentric ecliptic
1656             Cartesian coordinates from other coordinates (say, ECI).
1657              
1658             =item $coord->heliocentric_ecliptic_cartesian( $X, $Y, $Z )
1659              
1660             This method sets the object's position in Heliocentric ecliptic
1661             Cartesian coordinates. Velocities may not be set at the moment, though
1662             this is planned. You may also pass an optional time as the last
1663             argument.
1664              
1665             The invocant is returned.
1666              
1667             =cut
1668              
1669             sub heliocentric_ecliptic_cartesian {
1670 0     0 1 0 my ( $self, @args ) = @_;
1671              
1672 0         0 $self = $self->_check_coord( heliocentric_ecliptic_cartesian => \@args );
1673              
1674 0 0       0 if ( @args == 3 ) {
    0          
1675             $self->{_ECI_cache} = {
1676 0         0 inertial => {
1677             heliocentric_ecliptic_cartesian => \@args,
1678             },
1679             };
1680 0 0       0 my $sun = $self->get( 'sun' )
1681             or croak q{Attribute 'sun' not set};
1682 0         0 my ( $X_sun, $Y_sun, $Z_sun ) = $sun->universal(
1683             $self->universal() )->ecliptic_cartesian();
1684 0         0 my ( $X, $Y, $Z ) = ( $args[0] + $X_sun, $args[1] + $Y_sun,
1685             $args[2] + $Z_sun );
1686 0         0 $self->ecliptic_cartesian( $X, $Y, $Z );
1687 0         0 $self->{specified} = 'heliocentric_ecliptic_cartesian';
1688 0         0 $self->{inertial} = 1;
1689             } elsif ( @args ) {
1690 0         0 croak 'heliocentric_ecliptic_cartesian() wants 0, 1, 3 or 4 arguments';
1691             } else {
1692             $self->{_ECI_cache}{inertial}{heliocentric_ecliptic_cartesian}
1693             and return @{
1694 0 0       0 $self->{_ECI_cache}{inertial}{heliocentric_ecliptic_cartesian}
1695 0         0 };
1696 0 0       0 my $sun = $self->get( 'sun' )
1697             or croak q{Attribute 'sun' not set};
1698 0         0 my ( $X_self, $Y_self, $Z_self ) = $self->ecliptic_cartesian();
1699 0         0 my ( $X_sun, $Y_sun, $Z_sun ) = $sun->universal(
1700             $self->universal() )->ecliptic_cartesian();
1701              
1702 0         0 my @hec = ( $X_self - $X_sun, $Y_self - $Y_sun, $Z_self - $Z_sun );
1703              
1704 0         0 $self->set( equinox_dynamical => $self->dynamical() );
1705 0         0 $self->{_ECI_cache}{inertial}{heliocentric_ecliptic_cartesian} = \@hec;
1706 0         0 return @hec;
1707             }
1708 0         0 return $self;
1709             }
1710              
1711             =item @coords = $coord->heliocentric_ecliptic();
1712              
1713             This method returns the Heliocentric ecliptic coordinates of the object.
1714              
1715             =cut
1716              
1717             sub heliocentric_ecliptic {
1718 0     0 1 0 my ( $self, @args ) = @_;
1719              
1720 0         0 $self = $self->_check_coord( heliocentric_ecliptic => \@args );
1721              
1722 0 0       0 if ( @args ) {
1723 0 0       0 @args % 3
1724             and croak 'Arguments must be in threes, plus an optional time';
1725 0         0 $self->{_ECI_cache}{inertial}{heliocentric_ecliptic} = \@args;
1726 0         0 $self->heliocentric_ecliptic_cartesian(
1727             _convert_spherical_x_to_cartesian( @args ) );
1728 0         0 $self->{specified} = 'heliocentric_ecliptic';
1729 0         0 return $self;
1730             } else {
1731             return @{
1732 0         0 $self->{_ECI_cache}{inertial}{heliocentric_ecliptic} ||= [
1733 0   0     0 _convert_cartesian_to_spherical_x(
1734             $self->heliocentric_ecliptic_cartesian() ) ] }
1735             }
1736 0         0 return $self;
1737             }
1738              
1739             # ( $temp, $X, $Y, $Z, $Xprime, $Yprime, $Zprime ) =
1740             # $self->_local_cartesian( $trn2 );
1741             # This method computes local Cartesian coordinates of $trn2 as seen from
1742             # $self. The first return is intermediate results useful for the azimuth
1743             # and elevation. The subsequent results are X, Y, and Z coordinates (and
1744             # velocities if available), in the North, East, Up coordinate system.
1745             # This is a left-handed coordinate system, but so is the azel() system,
1746             # which it serves.
1747              
1748             sub _local_cartesian {
1749 21999     21999   34611 my ( $self, $trn2 ) = @_;
1750 21999 50       38114 $self->{debug} and do {
1751 0         0 require Data::Dumper;
1752 0         0 local $Data::Dumper::Terse = 1;
1753 0         0 print "Debug local_cartesian - ", Data::Dumper::Dumper( $self, $trn2 );
1754             };
1755              
1756 21999         37401 my $time = $trn2->universal();
1757 21999         50674 $self->universal( $time );
1758              
1759             # Pick up the ECEF of the body and the station
1760              
1761 21999         41448 my @tgt = $trn2->ecef();
1762 21999         42032 my @obs = $self->ecef();
1763              
1764             # Translate the ECEF coordinates to the station.
1765              
1766 21999         71532 my $limit = int( min( scalar @tgt, scalar @obs ) / 3 ) * 3 - 1;
1767 21999         44801 foreach my $inx ( 0 .. $limit ) {
1768 116703         165064 $tgt[$inx] -= $obs[$inx];
1769             }
1770 21999         37756 splice @tgt, $limit + 1;
1771              
1772             # Pick up the latitude and longitude.
1773              
1774 21999         45510 my ( $lat, $lon ) = $self->geodetic();
1775 21999         38184 my $sinlat = sin $lat;
1776 21999         31810 my $coslat = cos $lat;
1777 21999         28397 my $sinlon = sin $lon;
1778 21999         27058 my $coslon = cos $lon;
1779              
1780             # Rotate X->Y to longitude of station,
1781             # then Z->X to latitude of station
1782              
1783             # NOTE to Flat Earthers: For the next two statements to produce a
1784             # position in a local Cartesian system with the X-Y plane coincident
1785             # with the horizon, the Earth must be the oblate spheroid specified
1786             # by the currently-set reference elllipsoid.
1787              
1788 21999         44904 @tgt[ 0, 1 ] = (
1789             $tgt[0] * $coslon + $tgt[1] * $sinlon,
1790             - $tgt[0] * $sinlon + $tgt[1] * $coslon,
1791             );
1792 21999         37782 @tgt[ 0, 2 ] = (
1793             $tgt[0] * $sinlat - $tgt[2] * $coslat,
1794             $tgt[0] * $coslat + $tgt[2] * $sinlat,
1795             );
1796              
1797 21999         32273 $tgt[0] = - $tgt[0]; # Convert Southing to Northing
1798              
1799 21999 100       43474 if ( @tgt > 5 ) {
1800 16902         32976 @tgt[ 3, 4 ] = (
1801             $tgt[3] * $coslon + $tgt[4] * $sinlon,
1802             - $tgt[3] * $sinlon + $tgt[4] * $coslon,
1803             );
1804 16902         29678 @tgt[ 3, 5 ] = (
1805             $tgt[3] * $sinlat - $tgt[5] * $coslat,
1806             $tgt[3] * $coslat + $tgt[5] * $sinlat,
1807             );
1808              
1809 16902         21827 $tgt[3] = - $tgt[3]; # Convert South velocity to North
1810             }
1811              
1812 21999         73534 return @tgt;
1813             }
1814              
1815             =item $coord = $coord->local_mean_time ($time);
1816              
1817             This method sets the local mean time of the object. B
1818             local standard time,> but the universal time plus the longitude
1819             of the object expressed in seconds. Another definition is mean
1820             solar time plus 12 hours (since the solar day begins at noon).
1821             You will get an exception of some sort if the position of the
1822             object has not been set, or if the object represents inertial
1823             coordinates, or on any subclass whose position depends on the time.
1824              
1825             =item $time = $coord->local_mean_time ()
1826              
1827             This method returns the local mean time of the object. It will raise
1828             an exception if the time has not been set.
1829              
1830             Note that this returns the actual local time, not the GMT equivalent.
1831             This means that in formatting for output, you call
1832              
1833             strftime $format, gmtime $coord->local_mean_time ();
1834              
1835             =cut
1836              
1837             sub local_mean_time {
1838 2     2 1 5 my ($self, @args) = @_;
1839              
1840 2 50       7 ref $self or croak <
1841             Error - The local_mean_time() method may not be called as a class method.
1842             eod
1843              
1844 2 100       5 unless (@args) {
1845 1 50       15 $self->{universal} || croak <
1846             Error - Object's time has not been set.
1847             eod
1848             $self->{local_mean_time} = $self->universal () +
1849             _local_mean_delta ($self)
1850 1 50       7 unless defined $self->{local_mean_time};
1851 1         9 return $self->{local_mean_time};
1852             }
1853              
1854 1 50       5 if (@args == 1) {
1855 1         3 my $time = shift @args;
1856 1 50       4 $self->{specified} or croak <
1857             Error - Object's coordinates have not been set.
1858             eod
1859 1 50       3 $self->{inertial} and croak <
1860             Error - You can not set the local time of an object that represents
1861             a position in an inertial coordinate system, because this
1862             causes the earth-fixed position to change, invalidating the
1863             local time.
1864             $self->can ('time_set') and croak <
1865 0         0 Error - You can not set the local time on an @{[ref $self]}
1866             object, because when you do the time_set() method will just
1867             move the object, invalidating the local time.
1868             eod
1869 1         4 $self->universal($time - _local_mean_delta($self));
1870 1         3 $self->{local_mean_time} = $time;
1871             } else {
1872 0         0 croak <
1873             Error - The local_mean_time() method must be called with either zero
1874             arguments (to retrieve the time) or one argument (to set
1875             the time).
1876             eod
1877             }
1878              
1879 1         4 return $self;
1880             }
1881              
1882             =item $time = $coord->local_time ();
1883              
1884             This method returns the local time (defined as solar time plus 12 hours)
1885             of the given object. There is no way to set the local time.
1886              
1887             This is really just a convenience routine - the calculation is simply
1888             the local mean time plus the equation of time at the given time.
1889              
1890             Note that this returns the actual local time, not the GMT equivalent.
1891             This means that in formatting for output, you call
1892              
1893             strftime $format, gmtime $coord->local_time ();
1894              
1895             =cut
1896              
1897             sub local_time {
1898 0     0 1 0 my $self = shift;
1899 0         0 return $self->local_mean_time() + $self->equation_of_time();
1900             }
1901              
1902             =item ( $maidenhead_loc, $height ) = $coord->maidenhead( $precision );
1903              
1904             This method returns the location of the object in the Maidenhead Locator
1905             System. Height above the reference ellipsoid is not part of the system,
1906             but is returned anyway, in kilometers. The $precision is optional, and
1907             is an integer greater than 0.
1908              
1909             The default precision is 4, but this can be modified by setting
1910             C<$Astro::Coord::ECI::DEFAULT_MAIDENHEAD_PRECISION> to the desired
1911             value. For example, if you want the default precision to be 3 (which it
1912             probably should have been in the first place), you can use
1913              
1914             no warnings qw{ once };
1915             local $Astro::Coord::ECI::DEFAULT_MAIDENHEAD_PRECISION = 3;
1916              
1917             Note that precisions greater than 4 are not defined by the standard.
1918             This method extends the system by alternating letters (base 24) with
1919             digits (base 10), but this is unsupported since the results will change,
1920             possibly without notice, if the standard is extended in a manner
1921             incompatible with this implementation.
1922              
1923             Conversion of latitudes and longitudes to Maidenhead Grid is subject to
1924             truncation error, perhaps more so since latitude and longitude are
1925             specified in radians. An attempt has been made to minimize this by using
1926             Perl's stringification of numbers to ensure that something that looks
1927             like C<42> is not handled as C<41.999999999385>. This probably amounts
1928             to shifting some grid squares very slightly to the north-west, but in
1929             practice it seems to give better results for points on the boundaries of
1930             the grid squares.
1931              
1932             =item $coord->maidenhead( $maidenhead_loc, $height );
1933              
1934             This method sets the geodetic location in the Maidenhead Locator System.
1935             Height above the reference ellipsoid is not part of the system, but is
1936             accepted anyway, in kilometers, defaulting to 0.
1937              
1938             The actual location set is the center of the specified grid square.
1939              
1940             Locations longer than 8 characters are not defined by the standard. This
1941             method extends precision by assuming alternate letters (base 24) and
1942             digits (base 10), but this will change, possibly without notice, if the
1943             standard is extended in a manner incompatible with this implementation.
1944              
1945             The object itself is returned, to allow call chaining.
1946              
1947             =cut
1948             {
1949              
1950             our $DEFAULT_MAIDENHEAD_PRECISION = 4;
1951              
1952             my $alpha = 'abcdefghijklmnopqrstuvwxyz';
1953              
1954             sub maidenhead {
1955 2085     2085 1 579284 my ( $self, @args ) = @_;
1956 2085 100 66     16596 if ( @args > 1 || defined $args[0] && $args[0] =~ m/ [^0-9] /smx ) {
      66        
1957 1042         2384 my ( $loc, $alt ) = @args;
1958 1042 50       2337 defined $alt or $alt = 0;
1959              
1960 1042         1830 my $precision = length $loc;
1961 1042 50       2575 $precision % 2
1962             and croak "Invalid Maidenhead locator; length must be even";
1963 1042         2099 $precision /= 2;
1964              
1965 1042         1472 my $lat = 0.5;
1966 1042         1621 my $lon = 0.5;
1967 1042         6988 my @chars = split qr{}smx, $loc;
1968 1042         2899 foreach my $base ( reverse _maidenhead_bases( $precision ) ) {
1969 3125         5147 foreach ( $lat, $lon ) {
1970 6250         9519 my $chr = pop @chars;
1971 6250 100       10308 if ( $base > 10 ) {
1972 4166 50       9756 ( my $inx = index $alpha, lc $chr ) < 0
1973             and croak 'Invalid Maidenhead locator; ',
1974             "'$chr' is not a letter";
1975 4166 100       7941 my $limit = @chars > 1 ? $base - 1 : $base;
1976 4166 50       7741 $inx > $limit
1977             and croak 'Invalid Maidenhead locator; ',
1978             "'$chr' is greater than '",
1979             substr( $alpha, $limit, 1 ), "'";
1980 4166         6889 $_ += $inx;
1981 4166         7672 $_ /= $base;
1982             } else {
1983 2084 50       4647 $chr =~ m/ [^0-9] /smx
1984             and croak 'Invalid Maidenhead locator; ',
1985             "'$chr' is not a digit";
1986 2084         3606 $_ += $chr;
1987 2084         3626 $_ /= $base;
1988             }
1989             }
1990             }
1991              
1992             $self->geodetic(
1993 1042         3622 deg2rad( $lat * 180 - 90 ),
1994             deg2rad( $lon * 360 - 180 ),
1995             $alt
1996             );
1997 1042         5772 return $self;
1998              
1999             } else {
2000 1043         2507 my ( $precision ) = @args;
2001 1043 50 33     3665 defined $precision
2002             and $precision > 0
2003             or $precision = $DEFAULT_MAIDENHEAD_PRECISION;
2004              
2005 1043         2132 my @bases = reverse _maidenhead_bases( $precision );
2006              
2007             # In order to minimize truncation errors we multiply
2008             # everything out, and then work right-to-left using the mod
2009             # operator. We stringify to take advantage of Perl's smarts
2010             # about when something is 42 versus 41.999999999583.
2011            
2012 1043         1668 my $multiplier = 1;
2013 1043         1847 foreach ( @bases ) {
2014 3126         4668 $multiplier *= $_;
2015             }
2016              
2017 1043         1959 my ( $lat, $lon, $alt ) = $self->geodetic();
2018 1043         2431 $lat = ( $lat + PIOVER2 ) * $multiplier / PI;
2019 1043         1462 $lon = ( $lon + PI ) * $multiplier / TWOPI;
2020 1043         10058 $lat = floor( "$lat" );
2021 1043         5864 $lon = floor( "$lon" );
2022              
2023 1043         1972 my @rslt;
2024 1043         1858 foreach my $base ( @bases ) {
2025 3126         5037 foreach ( $lat, $lon ) {
2026 6252         8978 my $inx = $_ % $base;
2027 6252 100       13127 push @rslt, $base > 10 ?
2028             substr( $alpha, $inx, 1 ) : $inx;
2029 6252         14094 $_ = floor( $_ / $base );
2030             }
2031             }
2032              
2033             @rslt
2034 1043 50       2864 and $rslt[-1] = uc $rslt[-1];
2035 1043 50       2254 @rslt > 1
2036             and $rslt[-2] = uc $rslt[-2];
2037              
2038             return (
2039 1043         6801 join( '', reverse @rslt ),
2040             $alt,
2041             );
2042             }
2043             }
2044              
2045             sub _maidenhead_bases {
2046 2085     2085   3930 my ( $precision ) = @_;
2047 2085         3108 my @bases;
2048 2085         4891 foreach my $inx ( 1 .. $precision ) {
2049 6251 100       13439 push @bases, $inx % 2 ? 24 : 10;
2050             }
2051 2085         3306 $bases[0] = 18;
2052 2085         4835 return @bases;
2053             }
2054             }
2055              
2056             =item $value = $coord->mean_angular_velocity();
2057              
2058             This method returns the mean angular velocity of the body in radians
2059             per second. If the $coord object has a period() method, this method
2060             just returns two pi divided by the period. Otherwise it returns the
2061             contents of the angularvelocity attribute.
2062              
2063             =cut
2064              
2065             sub mean_angular_velocity {
2066 528     528 1 871 my $self = shift;
2067             return $self->can ('period') ?
2068             TWOPI / $self->period :
2069 528 100       2541 $self->{angularvelocity};
2070             }
2071              
2072             =item $time = $coord->next_azimuth( $body, $azimuth );
2073              
2074             This method returns the next time the given C<$body> passes the given
2075             C<$azimuth> as seen from the given C<$coord>, calculated to the nearest
2076             second. The start time is the current time setting of the C<$body>
2077             object.
2078              
2079             =item $time = $coord->next_azimuth( $azimuth );
2080              
2081             This method returns the next time the C<$coord> object passes the given
2082             C<$azimuth> as seen from the location in the C<$coord> object's
2083             C attribute, calculated to the nearest second. The start time
2084             is the current time setting of the C<$coord> object.
2085              
2086             =cut
2087              
2088             sub next_azimuth {
2089 0     0 1 0 my ( $self, $body, $azimuth ) = _expand_args_default_station( @_ );
2090 0 0       0 ref $self or croak <<'EOD';
2091             Error - The next_azimuth() method may not be called as a class method.
2092             EOD
2093              
2094 0 0       0 $body->represents(__PACKAGE__) or croak <<"EOD";
2095             Error - The argument to next_azimuth() must be a subclass of
2096 0         0 @{[__PACKAGE__]}.
2097             EOD
2098              
2099 0         0 my $want = shift;
2100 0 0       0 defined $want and $want = $want ? 1 : 0;
    0          
2101              
2102 0         0 my $denom = $body->mean_angular_velocity -
2103             $self->mean_angular_velocity;
2104             ## my $retro = $denom >= 0 ? 0 : 1;
2105 0 0       0 ($denom = abs ($denom)) < 1e-11 and croak <
2106             Error - The next_azimuth() method will not work for geosynchronous
2107             bodies.
2108             eod
2109              
2110 0         0 my $apparent = TWOPI / $denom;
2111 0         0 my $begin = $self->universal;
2112 0         0 my $delta = floor( $apparent / 8 );
2113 0         0 my $end = $begin + $delta;
2114              
2115 0         0 my $begin_angle = mod2pi(
2116             ( $self->azel( $body->universal( $begin ) ) )[0] - $azimuth );
2117 0         0 my $end_angle = mod2pi(
2118             ( $self->azel( $body->universal( $end ) ) )[0] - $azimuth );
2119 0   0     0 while ( $begin_angle < PI || $end_angle >= PI ) {
2120 0         0 $begin_angle = $end_angle;
2121 0         0 $begin = $end;
2122 0         0 $end = $end + $delta;
2123 0         0 $end_angle = mod2pi(
2124             ( $self->azel( $body->universal( $end ) ) )[0] - $azimuth );
2125             }
2126              
2127 0         0 while ($end - $begin > 1) {
2128 0         0 my $mid = floor (($begin + $end) / 2);
2129 0         0 my $mid_angle = mod2pi(
2130             ( $self->azel( $body->universal( $mid ) ) )[0] - $azimuth );
2131 0 0       0 ( $begin, $end ) = ( $mid_angle >= PI ) ?
2132             ( $mid, $end ) : ( $begin, $mid );
2133             }
2134              
2135 0         0 $body->universal ($end);
2136 0         0 $self->universal ($end);
2137 0         0 return $end;
2138             }
2139              
2140             =item ($time, $rise) = $coord->next_elevation ($body, $elev, $upper)
2141              
2142             This method calculates the next time the given body passes above or
2143             below the given elevation (in radians) as seen from C<$coord>. The
2144             C<$elev> argument may be omitted (or passed as undef), and will default
2145             to 0. If the C<$upper> argument is true, the calculation will be based
2146             on the upper limb of the body (as determined from its C
2147             attribute); if false, the calculation will be based on the center of the
2148             body. The C<$upper> argument defaults to true if the C<$elev> argument
2149             is zero or positive, and false if the C<$elev> argument is negative.
2150              
2151             The algorithm is successive approximation, and assumes that the
2152             body will be at its highest at meridian passage. It also assumes
2153             that if the body hasn't passed the given elevation in 183 days it
2154             never will. In this case it returns undef in scalar context, or
2155             an empty list in list context.
2156              
2157             =item ($time, $rise) = $coord->next_elevation( $elev, $upper );
2158              
2159             This method calculates the next time the C<$coord> object passes above
2160             or below the given elevation (in radians) as seen from the position
2161             found in the C<$coord> object's C attribute. The C<$elev>
2162             argument may be omitted (or passed as undef), and will default to 0. If
2163             the C<$upper> argument is true, the calculation will be based on the
2164             upper limb of the body (as determined from its C
2165             attribute); if false, the calculation will be based on the center of the
2166             body. The C<$upper> argument defaults to true if the C<$elev> argument
2167             is zero or positive, and false if the C<$elev> argument is negative.
2168              
2169             The algorithm is successive approximation, and assumes that the
2170             body will be at its highest at meridian passage. It also assumes
2171             that if the body hasn't passed the given elevation in 183 days it
2172             never will. In this case it returns undef in scalar context, or
2173             an empty list in list context.
2174              
2175             =cut
2176              
2177 17     17   166 use constant NEVER_PASS_ELEV => 183 * SECSPERDAY;
  17         49  
  17         16929  
2178              
2179             sub next_elevation {
2180 241     241 1 3498 my ( $self, $body, $angle, $upper ) = _expand_args_default_station( @_ );
2181 241 50       699 ref $self or croak <<'EOD';
2182             Error - The next_elevation() method may not be called as a class method.
2183             EOD
2184              
2185 241 50       696 $body->represents(__PACKAGE__) or croak <<"EOD";
2186             Error - The first argument to next_elevation() must be a subclass of
2187 0         0 @{[__PACKAGE__]}.
2188             EOD
2189              
2190 241   100     697 $angle ||= 0;
2191 241 100       677 defined $upper or $upper = $angle >= 0;
2192 241 100       689 $upper = $upper ? 1 : 0;
2193              
2194 241         539 my $begin = $self->universal;
2195 241         572 my $original = $begin;
2196 241         717 my ( undef, $elev ) = $self->azel_offset(
2197             $body->universal( $begin ), $upper );
2198 241   100     875 my $rise = ( $elev < $angle ) || 0;
2199              
2200 241         847 my ($end, $above) = $self->next_meridian ($body, $rise);
2201              
2202 241         780 my $give_up = $body->NEVER_PASS_ELEV ();
2203              
2204 241         386 while ( 1 ) {
2205 241         686 my ( undef, $elev ) = $self->azel_offset( $body, $upper );
2206 241 50 100     1296 ( ( $elev < $angle ) || 0 ) == $rise
2207             or last;
2208 0 0       0 return if $end - $original > $give_up;
2209 0         0 $begin = $end;
2210 0         0 ($end, $above) = $self->next_meridian ($body, $rise);
2211             }
2212              
2213 241         849 while ($end - $begin > 1) {
2214 3689         8962 my $mid = floor (($begin + $end) / 2);
2215 3689         7942 my ( undef, $elev ) = $self->universal( $mid )->
2216             azel_offset( $body->universal( $mid ), $upper );
2217 3689 100 100     16981 ( $begin, $end ) =
2218             ($elev < $angle || 0) == $rise ? ($mid, $end) : ($begin, $mid);
2219             }
2220              
2221 241         856 $self->universal ($end); # Ensure consistent time.
2222 241         742 $body->universal ($end);
2223 241 100       1616 return wantarray ? ($end, $rise) : $end;
2224             }
2225              
2226             =item ( $time, $above ) = $coord->next_meridian( $body, $want )
2227              
2228             This method calculates the next meridian passage of the given C<$body>
2229             over (or under) the location specified by the C<$coord> object. The
2230             C<$body> object must be a subclass of Astro::Coord::ECI.
2231              
2232             The optional C<$want> argument should be specified as true (e.g. 1) if
2233             you want the next passage above the observer, or as false (e.g. 0) if
2234             you want the next passage below the observer. If this argument is
2235             omitted or undefined, you get whichever passage is next.
2236              
2237             The start time of the search is the current time setting of the
2238             C<$coord> object.
2239              
2240             The returns are the time of the meridian passage, and an indicator which
2241             is true if the passage is above the observer (i.e. local noon if the
2242             C<$body> represents the Sun), or false if below (i.e. local midnight if
2243             the C<$body> represents the Sun). If called in scalar context, you get
2244             the time only.
2245              
2246             The current time of both C<$coord> and C<$body> objects are left at the
2247             returned time.
2248              
2249             The algorithm is by successive approximation. It will croak if the
2250             period of the C<$body> is close to synchronous, and will probably not
2251             work well for bodies in highly eccentric orbits. The calculation is to
2252             the nearest second, and the time returned is the first even second after
2253             the body crosses the meridian.
2254              
2255             =item ( $time, $above ) = $coord->next_meridian( $want )
2256              
2257             This method calculates the next meridian passage of the C<$coord> object
2258             over (or under) the location specified by the C<$coord> object's
2259             C attribute.
2260              
2261             The optional C<$want> argument should be specified as true (e.g. 1) if
2262             you want the next passage above the observer, or as false (e.g. 0) if
2263             you want the next passage below the observer. If this argument is
2264             omitted or undefined, you get whichever passage is next.
2265              
2266             The start time of the search is the current time setting of the
2267             C<$coord> object.
2268              
2269             The returns are the time of the meridian passage, and an indicator which
2270             is true if the passage is above the observer (i.e. local noon if the
2271             C<$coord> object represents the Sun), or false if below (i.e. local
2272             midnight if the C<$coord> object represents the Sun). If called in
2273             scalar context, you get the time only.
2274              
2275             The current time of both C<$coord> and its C are left at the
2276             returned time.
2277              
2278             The algorithm is by successive approximation. It will croak if the
2279             period of the C<$body> is close to synchronous, and will probably not
2280             work well for bodies in highly eccentric orbits. The calculation is to
2281             the nearest second, and the time returned is the first even second after
2282             the body crosses the meridian.
2283              
2284             =cut
2285              
2286             sub next_meridian {
2287 264     264 1 3942 my ( $self, $body, $want ) = _expand_args_default_station( @_ );
2288 264 50       868 ref $self or croak <<'EOD';
2289             Error - The next_meridian() method may not be called as a class method.
2290             EOD
2291              
2292 264 50       862 $body->represents(__PACKAGE__) or croak <<"EOD";
2293             Error - The argument to next_meridian() must be a subclass of
2294 0         0 @{[__PACKAGE__]}.
2295             EOD
2296              
2297 264 100       1023 defined $want and $want = $want ? 1 : 0;
    100          
2298              
2299 264         1089 my $denom = $body->mean_angular_velocity -
2300             $self->mean_angular_velocity;
2301 264 50       817 my $retro = $denom >= 0 ? 0 : 1;
2302 264 50       848 ($denom = abs ($denom)) < 1e-11 and croak <<'EOD';
2303             Error - The next_meridian() method will not work for geosynchronous
2304             bodies.
2305             EOD
2306              
2307 264         553 my $apparent = TWOPI / $denom;
2308 264         589 my $begin = $self->universal;
2309 264         1054 my $delta = floor ($apparent / 16);
2310 264         597 my $end = $begin + $delta;
2311              
2312 264 100       692 my ($above, $opposite) =
2313             mod2pi (($body->universal($begin)->geocentric)[1]
2314             - ($self->universal($begin)->geocentric)[1]) >= PI ?
2315             (1 - $retro, PI) : ($retro, 0);
2316              
2317 264         1014 ($begin, $end) = ($end, $end + $delta)
2318             while mod2pi (($body->universal($end)->geocentric)[1] -
2319             ($self->universal($end)->geocentric)[1] + $opposite) < PI;
2320              
2321 264 100 100     1361 if (defined $want && $want != $above) {
2322 108         222 $above = $want;
2323 108 100       327 $opposite = $opposite ? 0 : PI;
2324 108         314 ($begin, $end) = ($end, $end + $delta)
2325             while mod2pi (($body->universal($end)->geocentric)[1] -
2326             ($self->universal($end)->geocentric)[1] + $opposite) < PI;
2327             }
2328              
2329 264         895 while ($end - $begin > 1) {
2330 3302         7802 my $mid = floor (($begin + $end) / 2);
2331 3302         6585 my $long = ($body->universal($mid)->geocentric)[1];
2332 3302         7601 my $merid = ($self->universal($mid)->geocentric)[1];
2333 3302 100       9816 ($begin, $end) =
2334             mod2pi ($long - $merid + $opposite) < PI ?
2335             ($mid, $end) : ($begin, $mid);
2336             }
2337              
2338 264         905 $body->universal ($end);
2339 264         920 $self->universal ($end);
2340 264 100       1197 return wantarray ? ($end, $above) : $end;
2341             }
2342              
2343             =item ( $delta_psi, $delta_epsilon ) = $self->nutation( $time )
2344              
2345             This method calculates the nutation in longitude (delta psi) and
2346             obliquity (delta epsilon) for the given B time. If the time
2347             is unspecified or specified as C, the current B time
2348             of the object is used.
2349              
2350             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
2351             Edition, Chapter 22, pages 143ff. Meeus states that it is good to
2352             0.5 seconds of arc.
2353              
2354             =cut
2355              
2356             sub nutation {
2357 15985     15985 1 27150 my ( $self, $time ) = @_;
2358 15985 100       28724 defined $time
2359             or $time = $self->dynamical();
2360              
2361 15985         28919 my $T = jcent2000( $time ); # Meeus (22.1)
2362              
2363 15985         40464 my $omega = mod2pi( deg2rad( ( ( $T / 450000 + .0020708 ) * $T -
2364             1934.136261 ) * $T + 125.04452 ) );
2365              
2366 15985         34934 my $L = mod2pi( deg2rad( 36000.7698 * $T + 280.4665 ) );
2367 15985         34706 my $Lprime = mod2pi( deg2rad( 481267.8813 * $T + 218.3165 ) );
2368              
2369 15985         51141 my $delta_psi = deg2rad( ( -17.20 * sin( $omega )
2370             - 1.32 * sin( 2 * $L )
2371             - 0.23 * sin( 2 * $Lprime )
2372             + 0.21 * sin( 2 * $omega ) ) / 3600 );
2373 15985         48769 my $delta_epsilon = deg2rad( ( 9.20 * cos( $omega )
2374             + 0.57 * cos( 2 * $L )
2375             + 0.10 * cos( 2 * $Lprime )
2376             - 0.09 * cos( 2 * $omega ) ) / 3600 );
2377              
2378 15985         33292 return ( $delta_psi, $delta_epsilon );
2379             }
2380              
2381             =item $epsilon = $self->obliquity( $time )
2382              
2383             This method calculates the obliquity of the ecliptic in radians at the
2384             given B time. If the time is unspecified or specified as
2385             C, the current B time of the object is used.
2386              
2387             The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
2388             Edition, Chapter 22, pages 143ff. The conversion from universal to
2389             dynamical time comes from chapter 10, equation 10.2 on page 78.
2390              
2391             =cut
2392              
2393 17     17   347 use constant E0BASE => (21.446 / 60 + 26) / 60 + 23;
  17         38  
  17         17069  
2394              
2395             sub obliquity {
2396 14185     14185 1 24884 my ( $self, $time ) = @_;
2397              
2398 14185 50       38802 if ( looks_like_number( $self ) ) {
2399 0         0 ( $self, $time ) = ( __PACKAGE__, $self );
2400 0         0 __subroutine_deprecation();
2401             ## Carp::cluck( 'Subroutine call to obliquity() is deprecated' );
2402             }
2403 14185 100       32543 defined $time
2404             or $time = $self->dynamical();
2405              
2406 14185         34547 my $T = jcent2000 ($time); # Meeus (22.1)
2407              
2408 14185         30763 my ( undef, $delta_epsilon ) = $self->nutation( $time );
2409              
2410 14185         33862 my $epsilon0 = deg2rad( ( ( 0.001813 * $T - 0.00059 ) * $T - 46.8150 )
2411             * $T / 3600 + E0BASE);
2412 14185         25210 return $epsilon0 + $delta_epsilon;
2413             }
2414              
2415             =item $coord = $coord->precess ($time);
2416              
2417             This method is a convenience wrapper for precess_dynamical(). The
2418             functionality is the same except that B
2419             universal time.>
2420              
2421             =cut
2422              
2423             sub precess {
2424 2 50   2 1 26 $_[1]
2425             and $_[1] += dynamical_delta( $_[1] );
2426 2         8 goto &precess_dynamical;
2427             }
2428              
2429             =item $coord = $coord->precess_dynamical ($time);
2430              
2431             This method precesses the coordinates of the object to the given
2432             equinox, B The starting equinox is the
2433             value of the 'equinox_dynamical' attribute, or the current time setting
2434             if the 'equinox_dynamical' attribute is any false value (i.e. undef, 0,
2435             or ''). A warning will be issued if the value of 'equinox_dynamical' is
2436             undef (which is the default setting) and the object represents inertial
2437             coordinates. As of version 0.013_02, B
2438             unaffected by this operation.>
2439              
2440             As a side effect, the value of the 'equinox_dynamical' attribute will be
2441             set to the dynamical time corresponding to the argument.
2442              
2443             As of version 0.061_01, this does nothing to non-inertial
2444             objects -- that is, those whose position was set in Earth-fixed
2445             coordinates.
2446              
2447             If the object's 'station' attribute is set, the station is also
2448             precessed.
2449              
2450             The object itself is returned.
2451              
2452             The algorithm comes from Jean Meeus, "Astronomical Algorithms", 2nd
2453             Edition, Chapter 21, pages 134ff (a.k.a. "the rigorous method").
2454              
2455             =cut
2456              
2457             sub precess_dynamical {
2458 1127     1127 1 1926 my ( $self, $end ) = @_;
2459              
2460 1127 50       2085 $end
2461             or croak "No equinox time specified";
2462              
2463             # Non-inertial coordinate systems are not referred to the equinox,
2464             # and so do not get precessed.
2465 1127 50       2308 $self->get( 'inertial' )
2466             or return $self;
2467              
2468 1127 50       1908 defined ( my $start = $self->get( 'equinox_dynamical' ) )
2469             or carp "Warning - Precess called with equinox_dynamical ",
2470             "attribute undefined";
2471 1127   33     2023 $start ||= $self->dynamical ();
2472              
2473 1127         1489 my $sta;
2474 1127 100 100     1773 if ( $sta = $self->get( 'station' ) and $sta->get( 'inertial' )
2475             ) {
2476 1 50       4 $sta->get( 'station' )
2477             and croak NO_CASCADING_STATIONS;
2478 1         4 $sta->universal( $self->universal() );
2479 1         19 $sta->precess_dynamical( $end );
2480             }
2481              
2482 1127 100       2452 $start == $end
2483             and return $self;
2484              
2485 1125         2349 my ($alpha0, $delta0, $rho0) = $self->equatorial ();
2486              
2487 1125         2645 my $T = jcent2000($start);
2488 1125         1866 my $t = ($end - $start) / (36525 * SECSPERDAY);
2489              
2490             # The following four assignments correspond to Meeus' (21.2).
2491 1125         1788 my $zterm = (- 0.000139 * $T + 1.39656) * $T + 2306.2181;
2492 1125         2783 my $zeta = deg2rad((((0.017998 * $t + (- 0.000344 * $T + 0.30188)) *
2493             $t + $zterm) * $t) / 3600);
2494 1125         2656 my $z = deg2rad((((0.018203 * $t + (0.000066 * $T + 1.09468)) * $t +
2495             $zterm) * $t) / 3600);
2496 1125         3222 my $theta = deg2rad(((( - 0.041833 * $t - (0.000217 * $T + 0.42665))
2497             * $t + (- 0.000217 * $T - 0.85330) * $T + 2004.3109) * $t) /
2498             3600);
2499              
2500             # The following assignments correspond to Meeus' (21.4).
2501 1125         2049 my $sindelta0 = sin ($delta0);
2502 1125         1674 my $cosdelta0 = cos ($delta0);
2503 1125         1455 my $sintheta = sin ($theta);
2504 1125         1496 my $costheta = cos ($theta);
2505 1125         2140 my $cosdelta0cosalpha0 = cos ($alpha0 + $zeta) * $cosdelta0;
2506 1125         1715 my $A = $cosdelta0 * sin ($alpha0 + $zeta);
2507 1125         1524 my $B = $costheta * $cosdelta0cosalpha0 - $sintheta * $sindelta0;
2508 1125         1660 my $C = $sintheta * $cosdelta0cosalpha0 + $costheta * $sindelta0;
2509              
2510 1125         2518 my $alpha = mod2pi(atan2 ($A , $B) + $z);
2511 1125         2375 my $delta = asin($C);
2512              
2513 1125         2752 $self->equatorial ($alpha, $delta, $rho0);
2514 1125         2935 $self->set (equinox_dynamical => $end);
2515              
2516 1125         2623 return $self;
2517             }
2518              
2519             =item Astro::Coord::ECI->reference_ellipsoid($semi, $flat, $name);
2520              
2521             This class method can be used to define or redefine reference
2522             ellipsoids.
2523              
2524             Nothing bad will happen if you call this as an object method, but
2525             it still just creates a reference ellipsoid definition -- the object
2526             is unaffected.
2527              
2528             It is not an error to redefine an existing ellipsoid.
2529              
2530             =item ($semi, $flat, $name) = Astro::Coord::ECI->reference_ellipsoid($name)
2531              
2532             This class method returns the definition of the named reference
2533             ellipsoid. It croaks if there is no such ellipsoid.
2534              
2535             You can also call this as an object method, but the functionality is
2536             the same.
2537              
2538             The following reference ellipsoids are known to the class initially:
2539              
2540             CLARKE-1866 - 1866.
2541             semimajor => 6378.2064 km, flattening => 1/294.98.
2542             Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2543              
2544             GRS67 - Geodetic Reference System 1967.
2545             semimajor => 6378.160 km, flattening => 1/298.247.
2546             Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2547              
2548             GRS80 - Geodetic Reference System 1980.
2549             semimajor => 6378.137 km, flattening => 1/298.25722210088
2550             (flattening per U.S. Department of Commerce 1989).
2551             Source: http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2552              
2553             IAU68 - International Astronomical Union, 1968.
2554             semimajor => 6378.160 km, flattening => 1/298.25.
2555             Source: http://maic.jmu.edu/sic/standards/ellipsoid.htm
2556              
2557             IAU76 - International Astronomical Union, 1976.
2558             semimajor => 6378.14 km, flattening => 1 / 298.257.
2559             Source: Jean Meeus, "Astronomical Algorithms", 2nd Edition
2560              
2561             NAD83 - North American Datum, 1983.
2562             semimajor => 6378.137 km, flattening => 1/298.257.
2563             Source: http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2564             (NAD83 uses the GRS80 ellipsoid)
2565              
2566             sphere - Just in case you were wondering how much difference it
2567             makes (a max of 11 minutes 32.73 seconds of arc, per Jean
2568             Meeus).
2569             semimajor => 6378.137 km (from GRS80), flattening => 0.
2570              
2571             WGS72 - World Geodetic System 1972.
2572             semimajor => 6378.135 km, flattening=> 1/298.26.
2573             Source: http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2574              
2575             WGS84 - World Geodetic System 1984.
2576             semimajor => 6378.137 km, flattening => 1/1/298.257223563.
2577             Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2578              
2579             Reference ellipsoid names are case-sensitive.
2580              
2581             The default model is WGS84.
2582              
2583             =cut
2584              
2585             # Wish I had:
2586             # Maling, D.H., 1989, Measurements from Maps: Principles and methods of cartometry, Pergamon Press, Oxford, England.
2587             # Maling, D.H., 1993, Coordinate Systems and Map Projections, Pergamon Press, Oxford, England.
2588              
2589             # http://www.gsi.go.jp/PCGIAP/95wg/wg3/geodinf.htm has a partial list of who uses
2590             # what in the Asia/Pacific.
2591              
2592             %known_ellipsoid = ( # Reference Ellipsoids
2593             'CLARKE-1866' => { # http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2594             semimajor => 6378.2064,
2595             flattening => 1/294.9786982,
2596             },
2597             GRS67 => { # http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2598             semimajor => 6378.160,
2599             flattening => 1/298.247167427,
2600             },
2601             GRS80 => { # http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2602             semimajor => 6378.137, # km
2603             flattening => 1/298.25722210088, # U.S. Dept of Commerce 1989 (else 1/298.26)
2604             },
2605             IAU68 => { # http://maic.jmu.edu/sic/standards/ellipsoid.htm
2606             semimajor => 6378.160,
2607             flattening => 1/298.25,
2608             },
2609             IAU76 => { # Meeus, p. 82.
2610             semimajor => 6378.14,
2611             flattening => 1/298.257,
2612             },
2613             NAD83 => { # http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2614             semimajor => 6378.137, # km
2615             flattening => 1/298.257,
2616             },
2617             sphere => { # Defined by me for grins, with semimajor from GRS80.
2618             semimajor => 6378.137, # km, from GRS80
2619             flattening => 0, # It's a sphere!
2620             },
2621             WGS72 => { # http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
2622             semimajor => 6378.135, # km
2623             flattening => 1/298.26,
2624             },
2625             WGS84 => { # http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
2626             semimajor => 6378.137,
2627             flattening => 1/298.257223563,
2628             },
2629             );
2630             foreach my $name (keys %known_ellipsoid) {
2631             $known_ellipsoid{$name}{name} = $name;
2632             }
2633              
2634             sub reference_ellipsoid {
2635 0     0 1 0 my ( undef, @args ) = @_; # Invocant unused
2636 0 0       0 my $name = pop @args or croak <
2637             Error - You must specify the name of a reference ellipsoid.
2638             eod
2639 0 0       0 if (@args == 0) {
    0          
2640 0 0       0 $known_ellipsoid{$name} or croak <
2641             Error - Reference ellipsoid $name is unknown to this software. Known
2642             ellipsoids are:
2643 0         0 @{[join ', ', sort keys %known_ellipsoid]}.
2644             eod
2645             } elsif (@args == 2) {
2646 0         0 $known_ellipsoid{$name} = {
2647             semimajor => $args[0],
2648             flattening => $args[1],
2649             name => $name,
2650             };
2651             } else {
2652 0         0 croak <
2653             Error - You must call the reference_ellipsoid class method with either one
2654             argument (to fetch the definition of a known ellipsoid) or three
2655             arguments (to define a new ellipsoid or redefine an old one).
2656             eod
2657             }
2658 0         0 return @{$known_ellipsoid{$name}}{qw{semimajor flattening name}}
  0         0  
2659             }
2660              
2661             =item $coord->represents ($class);
2662              
2663             This method returns true if the $coord object represents the given class.
2664             It is pretty much like isa (), but if called on a container class (i.e.
2665             Astro::Coord::ECI::TLE::Set), it returns true based on the class of
2666             the members of the set, and dies if the set has no members.
2667              
2668             The $class argument is optional. If not specified (or undef), it is
2669             pretty much like ref $coord || $coord (i.e. it returns the class
2670             name), but with the delegation behavior described in the previous
2671             paragraph if the $coord object is a container.
2672              
2673             There. This took many more words to explain than it did to implement.
2674              
2675             =cut
2676              
2677             sub represents {
2678 85018 100 66 85018 1 236450 return defined ($_[1]) ?
    100          
2679             ## $_[0]->represents()->isa($_[1]) :
2680             __classisa($_[0]->represents(), $_[1]) ? 1 : 0 :
2681             (ref $_[0] || $_[0]);
2682             }
2683              
2684             =item $coord->set (name => value ...);
2685              
2686             This method sets various attributes of the object. If called as a class
2687             method, it changes the defaults.
2688              
2689             For reasons that seemed good at the time, this method returns the
2690             object it was called on (i.e. $coord in the above example).
2691              
2692             See L for a list of the attributes you can set.
2693              
2694             =cut
2695              
2696 17     17   298 use constant SET_ACTION_NONE => 0; # Do nothing.
  17         67  
  17         1159  
2697 17     17   143 use constant SET_ACTION_RESET => 1; # Reset the coordinates based on the initial setting.
  17         43  
  17         14133  
2698              
2699             sub set {
2700 1618     1618 1 3824 my ($self, @args) = @_;
2701 1618         2245 my $original = $self;
2702 1618 100       3212 ref $self
2703             or $self = \%static;
2704 1618 50       3258 @args % 2
2705             and croak 'Error - The set() method requires an even ',
2706             'number of arguments.';
2707 1618         2284 my $action = 0;
2708 1618         3014 while (@args) {
2709 1647         2526 my $name = shift @args;
2710 1647 50       3637 exists $mutator{$name}
2711             or croak "Error - Attribute '$name' does not exist.";
2712 1647 50       4018 CODE_REF eq ref $mutator{$name}
2713             or croak "Error - Attribute '$name' is read-only";
2714 1647         4020 $action |= $mutator{$name}->($self, $name, shift @args);
2715             }
2716              
2717             $self->{_need_purge} = 1
2718 1617 100 66     7837 if ref $self && $self->{specified} && $action & SET_ACTION_RESET;
      100        
2719              
2720 1617         2823 return $original;
2721             }
2722              
2723             # The following are the mutators for the attributes. All are
2724             # passed three arguments: a reference to the hash to be set,
2725             # the hash key to be set, and the value. They must return the
2726             # bitwise 'or' of the desired action masks, defined above.
2727              
2728             %mutator = (
2729             almanac_horizon => \&_set_almanac_horizon,
2730             angularvelocity => \&_set_value,
2731             debug => \&_set_value,
2732             diameter => \&_set_value,
2733             edge_of_earths_shadow => \&_set_value,
2734             ellipsoid => \&_set_reference_ellipsoid,
2735             equinox_dynamical => \&_set_value, # CAVEAT: _convert_eci_to_ecef
2736             # accesses this directly for
2737             # speed.
2738             flattening => \&_set_custom_ellipsoid,
2739             frequency => \&_set_value,
2740             horizon => \&_set_elevation,
2741             id => \&_set_id,
2742             inertial => undef,
2743             name => \&_set_value,
2744             refraction => \&_set_value,
2745             specified => undef,
2746             semimajor => \&_set_custom_ellipsoid,
2747             station => \&_set_station,
2748             sun => \&_set_sun,
2749             twilight => \&_set_elevation,
2750             );
2751              
2752             {
2753             # TODO this will all be nicer if I had state variables.
2754              
2755             my %special = (
2756             ## horizon => sub { return $_[0]->get( 'horizon' ); },
2757             height => sub { return $_[0]->dip(); },
2758             );
2759              
2760             sub _set_almanac_horizon {
2761 172     172   481 my ( $hash, $attr, $value ) = @_;
2762 172 50       405 defined $value
2763             or $value = 0; # Default
2764 172 50 33     1384 if ( $special{$value} ) {
    50 33        
2765 0         0 $hash->{"_$attr"} = $special{$value};
2766             } elsif ( looks_like_number( $value )
2767             && $value >= - PIOVER2 # Not -PIOVER2 to avoid warning under 5.10.1.
2768             && $value <= PIOVER2
2769             ) {
2770 172     5   1041 $hash->{"_$attr"} = sub { return $_[0]->get( $attr ) };
  5         14  
2771             } else {
2772 0         0 croak "'$value' is an invalid value for '$attr'";
2773             }
2774 172         589 $hash->{$attr} = $value;
2775 172         523 return SET_ACTION_NONE;
2776             }
2777             }
2778              
2779             # Get the actual value of the almanac horizon, from whatever
2780             # source.
2781              
2782             sub __get_almanac_horizon {
2783 5     5   20 goto $_[0]{_almanac_horizon};
2784             }
2785              
2786             # If you set semimajor or flattening, the ellipsoid name becomes
2787             # undefined. Also clear any cached geodetic coordinates.
2788              
2789             sub _set_custom_ellipsoid {
2790 0     0   0 $_[0]->{ellipsoid} = undef;
2791 0         0 $_[0]->{$_[1]} = $_[2];
2792 0         0 return SET_ACTION_RESET;
2793             }
2794              
2795             {
2796             my %dflt = (
2797             twilight => CIVIL_TWILIGHT,
2798             );
2799              
2800             # Any attribute specified as angle above or below the horizon.
2801             sub _set_elevation {
2802 1     1   4 my ( $self, $name, $value ) = @_;
2803             defined $value
2804 1 50 0     4 or $value = ( $dflt{$name} || 0 ); # Default
2805 1 50 33     336 looks_like_number( $value )
      33        
2806             and $value >= - PIOVER2 # Not -PIOVER2 to avoid warning under 5.10.1.
2807             and $value <= PIOVER2
2808             or croak "'$value' is an invalid value for '$name'";
2809 0         0 $self->{$name} = $value;
2810 0         0 return SET_ACTION_NONE;
2811             }
2812             }
2813              
2814             sub _set_station {
2815 12     12   34 my ( $hash, $attr, $value ) = @_;
2816 12 50       41 if ( defined $value ) {
2817 12 50       39 embodies( $value, 'Astro::Coord::ECI' )
2818             or croak "Attribute $attr must be undef or an ",
2819             'Astro::Coord::ECI object';
2820 12 50       44 $value->get( 'station' )
2821             and croak NO_CASCADING_STATIONS;
2822             }
2823 12         32 $hash->{$attr} = $value;
2824 12         42 return SET_ACTION_RESET;
2825             }
2826              
2827 17     17   259 use constant SUN_CLASS => 'Astro::Coord::ECI::Sun';
  17         35  
  17         56896  
2828              
2829             sub _set_sun {
2830 2     2   6 my ( $self, $name, $value ) = @_;
2831 2 50       18 embodies( $value, $self->SUN_CLASS() )
2832             or croak sprintf 'The sun attribute must be a %s',
2833             $self->SUN_CLASS();
2834 2 50       18 ref $value
2835             or $value = $value->new();
2836 2         6 $self->{$name} = $value;
2837 2         12 return SET_ACTION_NONE;
2838             }
2839              
2840             # Unfortunately, the TLE subclass may need objects reblessed if
2841             # the ID changes. So much for factoring. Sigh.
2842              
2843             sub _set_id {
2844 139     139   321 $_[0]{$_[1]} = $_[2];
2845 139 100       763 $_[0]->rebless () if $_[0]->can ('rebless');
2846 139         423 return SET_ACTION_NONE;
2847             }
2848              
2849             # If this is a reference ellipsoid name, check it, and if it's
2850             # OK set semimajor and flattening also. Also clear any cached
2851             # geodetic coordinates.
2852              
2853             sub _set_reference_ellipsoid {
2854 31     31   73 my ($self, $name, $value) = @_;
2855 31 50       85 defined $value or croak <
2856             Error - Can not set reference ellipsoid to undefined.
2857             eod
2858 31 50       115 exists $known_ellipsoid{$value} or croak <
2859             Error - Reference ellipsoid '$value' is unknown.
2860             eod
2861              
2862 31         90 $self->{semimajor} = $known_ellipsoid{$value}{semimajor};
2863 31         76 $self->{flattening} = $known_ellipsoid{$value}{flattening};
2864 31         59 $self->{$name} = $value;
2865 31         122 return SET_ACTION_RESET;
2866             }
2867              
2868             # If this is a vanilla setting, just do it.
2869              
2870             sub _set_value {
2871 1290     1290   2638 $_[0]->{$_[1]} = $_[2];
2872 1290         3360 return SET_ACTION_NONE;
2873             }
2874              
2875             =item $coord->universal ($time)
2876              
2877             This method sets the time represented by the object, in universal time
2878             (a.k.a. CUT, a.k.a. Zulu, a.k.a. Greenwich).
2879              
2880             This method can also be called as a class method, in which case it
2881             instantiates the desired object.
2882              
2883             =item $time = $coord->universal ();
2884              
2885             This method returns the universal time previously set.
2886              
2887             =cut
2888              
2889             sub universal {
2890 140693     140693 1 233750 my ( $self, $time, @args ) = @_;
2891              
2892             @args
2893 140693 50       244252 and croak <<'EOD';
2894             Error - The universal() method must be called with either zero
2895             arguments (to retrieve the time) or one argument (to set the
2896             time).
2897             EOD
2898              
2899 140693 100       237051 if ( defined $time ) {
2900 65243         117805 return $self->__universal( $time, 1 );
2901             } else {
2902 75450 50       138708 ref $self or croak <<'EOD';
2903             Error - The universal() method may not be called as a class method
2904             unless you specify arguments.
2905             EOD
2906             defined $self->{universal}
2907 75450 50       137523 or croak <<'EOD';
2908             Error - Object's time has not been set.
2909             EOD
2910 75450         198677 return $self->{universal};
2911             }
2912             }
2913              
2914             # Set universal time without running model
2915              
2916             sub __universal {
2917 84084     84084   139762 my ( $self, $time, $run_model ) = @_;
2918 84084 50       149982 defined $time
2919             or confess 'Programming error - __universal() requires a defined time';
2920 84084 100       158142 $self = $self->new () unless ref $self;
2921             defined $self->{universal}
2922             and $time == $self->{universal}
2923 84084 100 100     319341 and return $self;
2924 58330         126772 $self->__clear_time();
2925 58330         92714 $self->{universal} = $time;
2926 58330 100       155994 $run_model
2927             and $self->_call_time_set();
2928 58316         133177 return $self;
2929             }
2930              
2931             sub __clear_time {
2932 58344     58344   86864 my ( $self ) = @_;
2933 58344         90123 delete $self->{universal};
2934 58344         78906 delete $self->{local_mean_time};
2935 58344         77535 delete $self->{dynamical};
2936 58344 100       107489 if ($self->{specified}) {
2937 58230 100       103216 if ($self->{inertial}) {
2938 31846         76795 delete $self->{_ECI_cache}{fixed};
2939             } else {
2940 26384         42204 delete $self->{_ECI_cache}{inertial};
2941             }
2942             }
2943 58344         84393 return;
2944             }
2945              
2946             sub __event_name {
2947 30     30   86 my ( $self, $event, $tplt ) = @_;
2948 30 50       89 ARRAY_REF eq ref $tplt
2949             or confess 'Programming error - $tplt must be array ref';
2950 30         105 return __sprintf( $tplt->[$event], $self->__object_name() );
2951             }
2952              
2953             sub __horizon_name {
2954 10     10   28 my ( $self, $event, $tplt ) = @_;
2955 10   33     53 return $self->__event_name(
2956             $event,
2957             $tplt || $self->__horizon_name_tplt(),
2958             );
2959             }
2960              
2961             sub __horizon_name_tplt {
2962 0     0   0 return [ '%s sets', '%s rises' ];
2963             }
2964              
2965             sub __transit_name {
2966 10     10   30 my ( $self, $event, $tplt ) = @_;
2967 10   33     82 return $self->__event_name(
2968             $event,
2969             $tplt || $self->__transit_name_tplt(),
2970             );
2971             }
2972              
2973             sub __transit_name_tplt {
2974 6     6   35 return [ undef, '%s transits meridian' ];
2975             }
2976              
2977             sub __object_name {
2978 50     50   101 my ( $self ) = @_;
2979             return $self->get( 'name' ) || $self->get( 'id' ) || (
2980 50   33     117 $self->{_name} ||= $self->__object_name_from_class_name()
2981             );
2982             }
2983              
2984             sub __object_name_from_class_name {
2985 5     5   15 my ( $self ) = @_;
2986 5   33     34 ( my $name = ref $self || $self ) =~ s/ (?: :: | _ ) XS \z //smx;
2987 5         45 $name =~ s/ .* :: //smx;
2988 5         17 return $name;
2989             }
2990              
2991             sub __object_is_self_named {
2992 20     20   42 my ( $self ) = @_;
2993 20   66     85 $self->{_name_re} ||= do {
2994 5         26 my $re = $self->__object_name_from_class_name();
2995 5         90 qr< \A \Q$re\E \z >smxi;
2996             };
2997 20         70 return $self->__object_name() =~ $self->{_name_re};
2998             }
2999              
3000             #######################################################################
3001             #
3002             # Internal
3003             #
3004              
3005             # $coord->_call_time_set ()
3006              
3007             # This method calls the time_set method if it exists and if we are
3008             # not already in it. It is a way to avoid endless recursion if the
3009             # time_set method should happen to set the time.
3010              
3011             sub _call_time_set {
3012 57916     57916   82595 my $self = shift;
3013 57916 100       172780 $self->can ('time_set') or return;
3014 31492         45575 my $exception;
3015 31492 100       70813 unless ($self->{_no_set}++) {
3016 31374 100       46973 eval {$self->time_set (); 1;}
  31374         79711  
  31360         65914  
3017             or $exception = $@;
3018             }
3019 31492 100       69919 --$self->{_no_set} or delete $self->{_no_set};
3020 31492 100       58417 if ($exception) {
3021 14         42 $self->__clear_time();
3022             # Re-raise the exception now that we have cleaned up.
3023 14         56 die $exception; ## no critic (RequireCarping)
3024             }
3025 31478         46467 return;
3026             }
3027              
3028             # $coord->_check_coord (method => \@_)
3029              
3030             # This is designed to be called "up front" for any of the methods
3031             # that retrieve or set coordinates, to be sure the object is in
3032             # a consistent state.
3033             # * If $self is not a reference, it creates a new object if there
3034             # are arguments, or croaks if there are none.
3035             # * If the number arguments passed (after removing self and the
3036             # method name) is one more than a multiple of three, the last
3037             # argument is removed, and used to set the universal time of
3038             # the object.
3039             # * If there are arguments, all coordinate caches are cleared;
3040             # otherwise the coordinates are reset if needed.
3041             # The object itself is returned.
3042              
3043             sub _check_coord {
3044 200567     200567   330026 my ($self, $method, $args, @extra) = @_;
3045              
3046 200567 100       369006 unless (ref $self) {
3047 14 50       31 @$args or croak <
3048             Error - The $method() method may not be called as a class method
3049             unless you specify arguments.
3050             eod
3051 14         30 $self = $self->new ();
3052             }
3053              
3054 200567 50       358596 $self->{debug} and do {
3055 0         0 require Data::Dumper;
3056 0         0 local $Data::Dumper::Terse = 1;
3057 0         0 print "Debug $method (", Data::Dumper::Dumper (@$args, @extra), ")\n";
3058             };
3059              
3060             {
3061 200567         251184 my $inx = 0;
  200567         247627  
3062 200567         345313 foreach (@$args) {
3063 263135 50       453263 defined $_
3064             and next;
3065 0         0 require Data::Dumper;
3066 0         0 local $Data::Dumper::Terse = 1;
3067 0 0       0 croak <
3068 0         0 Error - @{[ (caller (1))[3] ]} argument $inx is undefined.
3069             Arguments are (@{[ join ', ',
3070 0         0 map {my $x = Data::Dumper::Dumper $_; chomp $x; $x} @$args ]})
  0         0  
  0         0  
  0         0  
3071             eod
3072 0         0 $inx++;
3073             }
3074             }
3075              
3076 200567 100       375571 $self->universal (pop @$args) if @$args % 3 == 1;
3077              
3078 200567 100       352181 if ($self->{specified}) {
3079 198107 100       381270 if (@$args) {
    100          
3080             # Cached coordinate deletion moved to ecef(), so it's only done once.
3081             } elsif ($self->{_need_purge}) {
3082 4         8 delete $self->{_need_purge};
3083 4         8 my $spec = $self->{specified};
3084 4         42 my $data = [$self->$spec ()];
3085 4         9 foreach my $key (@kilatr) {delete $self->{$key}}
  20         37  
3086 4         12 $self->$spec (@$data);
3087             }
3088             }
3089              
3090 200567         338302 return $self;
3091             }
3092              
3093             # _check_latitude($arg_name => $value)
3094             #
3095             # This subroutine range-checks the given latitude value,
3096             # generating a warning if it is outside the range -PIOVER2 <=
3097             # $value <= PIOVER2. The $arg_name is used in the exception, if
3098             # any. The value is normalized to the range -PI to PI, and
3099             # returned. Not that a value outside the validation range makes
3100             # any sense.
3101              
3102             sub _check_latitude {
3103 6465 50 33 6465   28143 (-&PIOVER2 <= $_[1] && $_[1] <= &PIOVER2)
3104             or carp (ucfirst $_[0],
3105             ' must be in radians, between -PI/2 and PI/2');
3106 6465         15576 return mod2pi($_[1] + PI) - PI;
3107             }
3108              
3109             # $value = _check_longitude($arg_name => $value)
3110             #
3111             # This subroutine range-checks the given longitude value,
3112             # generating a warning if it is outside the range -TWOPI <= $value
3113             # <= TWOPI. The $arg_name is used in the exception, if any. The
3114             # value is normalized to the range -PI to PI, and returned.
3115              
3116             sub _check_longitude {
3117 4212 50 33 4212   15904 (-&TWOPI <= $_[1] && $_[1] <= &TWOPI)
3118             or carp (ucfirst $_[0],
3119             ' must be in radians, between -2*PI and 2*PI');
3120 4212         8917 return mod2pi($_[1] + PI) - PI;
3121             }
3122              
3123             # _check_right_ascension($arg_name => $value)
3124             #
3125             # This subroutine range-checks the given right ascension value,
3126             # generating a warning if it is outside the range 0 <= $value <=
3127             # TWOPI. The $arg_name is used in the exception, if any. The value
3128             # is normalized to the range 0 to TWOPI, and returned.
3129              
3130             sub _check_right_ascension {
3131 2253 50 33 2253   9009 (0 <= $_[1] && $_[1] <= &TWOPI)
3132             or carp (ucfirst $_[0],
3133             ' must be in radians, between 0 and 2*PI');
3134 2253         4791 return mod2pi($_[1]);
3135             }
3136              
3137             # @cartesian = _convert_spherical_to_cartesian( @spherical )
3138             #
3139             # This subroutine converts spherical coordinates to Cartesian
3140             # coordinates of the same handedness.
3141             #
3142             # The first three arguments are Phi (in the X-Y plane, e.g. azimuth or
3143             # longitude, in radians), Theta (perpendicular to the X-Y plane, e.g.
3144             # elevation or latitude, in radians) and Rho (range). Subsequent
3145             # triplets of arguments are optional, and can represent derivitaves of
3146             # position (velocity, acceleration ... ) at that point.
3147             #
3148             # The return is the corresponding X, Y, and Z coordinates. If more than
3149             # three triplets of arguments are specified, they will be converted to
3150             # spherical coordinates as though measured at that point, and returned
3151             # in the same order. That is, if you supplied Phi, Theta, and Rho
3152             # velocities, you will get back X, Y, and Z velocities. velocities, you
3153             # will get back Phi, Theta, and Rho velocities, in that order.
3154              
3155             sub _convert_spherical_to_cartesian {
3156 15312     15312   28647 my @sph_data = @_;
3157             @sph_data
3158 15312 50 33     50772 and not @sph_data % 3
3159             or confess( 'Programming error - Want 3 or 6 arguments' );
3160              
3161             # The first triplet is position. We extract it into its own array,
3162             # then compute the corresponding Cartesian coordinates using the
3163             # definition of the coordinate system.
3164              
3165 15312         29275 my @sph_pos = splice @sph_data, 0, 3;
3166 15312         25755 my $range = $sph_pos[2];
3167 15312         23636 my $sin_theta = sin $sph_pos[1];
3168 15312         21515 my $cos_theta = cos $sph_pos[1];
3169 15312         24239 my $sin_phi = sin $sph_pos[0];
3170 15312         21779 my $cos_phi = cos $sph_pos[0];
3171 15312         20657 my $diag = $range * $cos_theta;
3172              
3173 15312         19999 my $X = $diag * $cos_phi;
3174 15312         20035 my $Y = $diag * $sin_phi;
3175 15312         20079 my $Z = $range * $sin_theta;
3176              
3177             # Accumulate results.
3178              
3179 15312         24263 my @rslt = ( $X, $Y, $Z );
3180              
3181             # If we have velocity (accelelation, etc) components
3182              
3183 15312 100       29338 if ( @sph_data ) {
3184              
3185             # We compute unit vectors in the Cartesian coordinate system.
3186              
3187             # x = cos Theta cos Phi r - sin Theta cos Phi theta - sin Phi phi
3188             # y = cos Theta sin Phi r - sin Theta sin Phi theta - cos Phi phi
3189             # z = sin Theta r + cos Theta theta
3190              
3191 1         4 my $X_hat = [ - $sin_phi, - $sin_theta * $cos_phi,
3192             $cos_theta * $cos_phi ];
3193 1         3 my $Y_hat = [ $cos_phi, - $sin_theta * $sin_phi,
3194             $cos_theta * $sin_phi ];
3195 1         2 my $Z_hat = [ 0, $cos_theta, $sin_theta ];
3196              
3197 1         3 while ( @sph_data ) {
3198              
3199             # Each triplet is then converted by projecting the Cartesian
3200             # vector onto the appropriate unit vector. Azimuth and
3201             # elevation are also converted to length by multiplying by
3202             # the range. NOTE that this is the small-angle
3203             # approximation, but should be OK since we assume we're
3204             # dealing with derivatives of position.
3205              
3206 1         3 my @sph_info = splice @sph_data, 0, 3;
3207 1         2 $sph_info[0] *= $range;
3208 1         2 $sph_info[1] *= $range;
3209 1         4 push @rslt, vector_dot_product( $X_hat, \@sph_info );
3210 1         3 push @rslt, vector_dot_product( $Y_hat, \@sph_info );
3211 1         3 push @rslt, vector_dot_product( $Z_hat, \@sph_info );
3212             }
3213              
3214             }
3215              
3216 15312         53558 return @rslt;
3217             }
3218              
3219             # @cartesian = _convert_spherical_x_to_cartesian( @spherical )
3220             #
3221             # This subroutine is a convenience wrapper for
3222             # _convert_spherical_to_cartesian(). The only difference is that the
3223             # arguments are Theta (perpendicular to the X-Y plane), Phi (in the X-Y
3224             # plane) and Rho, rather than Phi, Theta, Rho. This subroutine is my
3225             # penance for not having all the methods that involve spherical
3226             # coordinates return their arguments in the same order.
3227              
3228             sub _convert_spherical_x_to_cartesian {
3229 13058     13058   24290 my @args = @_;
3230 13058         28286 for ( my $inx = 0; $inx < @args; $inx += 3 ) {
3231 13058         39522 @args[ $inx, $inx + 1 ] = @args[ $inx + 1, $inx ];
3232             }
3233 13058         25692 return _convert_spherical_to_cartesian( @args );
3234             }
3235              
3236             # @spherical = _convert_cartesian_to_spherical( @cartesian )
3237             #
3238             # This subroutine converts three-dimensional Cartesian coordinates to
3239             # spherical coordinates of the same handedness.
3240             #
3241             # The first three arguments are the X, Y, and Z coordinates, and are
3242             # required. Subsequent triplets af arguments are optional, and can
3243             # represent anything (velocity, acceleration ... ) at that point.
3244             #
3245             # The return is the spherical coordinates Phi (in the X-Y plane, e.g.
3246             # azimuth or longitude, in radians), Theta (perpendicular to the X-Y
3247             # plane, e.g. elevation or latitude, in radians), and Rho (range). If
3248             # more than three triplets of arguments are specified, they will be
3249             # converted to spherical coordinates as though measured at that point,
3250             # and returned in the same order. That is, if you supplied X, Y, and Z
3251             # velocities, you will get back Phi, Theta, and Rho velocities, in that
3252             # order.
3253              
3254             sub _convert_cartesian_to_spherical {
3255 24487     24487   49225 my @cart_data = @_;
3256             @cart_data
3257 24487 50 33     80874 and not @cart_data % 3
3258             or confess( 'Programming error - Want 3 or 6 arguments' );
3259              
3260             # The first triplet is position. We extract it into its own array,
3261             # then compute the corresponding spherical coordinates using the
3262             # definition of the coordinate system.
3263              
3264 24487         47601 my @cart_pos = splice @cart_data, 0, 3;
3265 24487         61998 my $range = vector_magnitude( \@cart_pos );
3266 24487 100 66     101063 my $azimuth = ( $cart_pos[0] || $cart_pos[1] ) ?
3267             mod2pi( atan2 $cart_pos[1], $cart_pos[0] ) :
3268             0;
3269 24487 100       69154 my $elevation = $range ? asin( $cart_pos[2] / $range ) : 0;
3270              
3271             # Accumulate results.
3272              
3273 24487         46439 my @rslt = ( $azimuth, $elevation, $range );
3274              
3275             # If we have velocity (accelelation, etc) components
3276              
3277 24487 100       46855 if ( @cart_data ) {
3278              
3279             # We compute unit vectors in the spherical coordinate system.
3280             #
3281             # The "Relationships among Unit Vectors" at
3282             # http://plaza.obu.edu/corneliusk/mp/rauv.pdf (and retained in
3283             # the ref/ directory) gives the transformation both ways. With
3284             # x, y, and z being the Cartesian unit vectors, Theta and Phi
3285             # being the elevation (in the range 0 to pi, 0 being along the +
3286             # Z axis) and azimuth (X toward Y, i.e. right-handed), and r,
3287             # theta, and phi being the corresponding unit vectors:
3288             #
3289             # r = sin Theta cos Phi x + sin Theta sin Phi y + cos Theta z
3290             # theta = cos Theta cos Phi x + cos Theta sin Phi y - sin Theta z
3291             # phi = - sin Phi x + cos phi y
3292             #
3293             # and
3294             #
3295             # x = sin Theta cos Phi r + cos Theta cos Phi theta - sin Phi phi
3296             # y = sin Theta sin Phi r + cos Theta sin Phi theta - cos Phi phi
3297             # z = cos Theta r - sin Theta theta
3298             #
3299             # It looks to me like I get the Theta convention I'm using by
3300             # replacing sin Theta with cos Theta and cos Theta by sin Theta
3301             # (because Dr. Cornelius takes 0 as the positive Z axis whereas
3302             # I take zero as the X-Y plane) and changing the sign of theta
3303             # (since Dr. Cornelius' Theta increases in the negative Z
3304             # direction, whereas mine increases in the positive Z
3305             # direction).
3306             #
3307             # The document was found at http://plaza.obu.edu/corneliusk/
3308             # which is the page for Dr. Kevin Cornelius' Mathematical
3309             # Physics (PHYS 4053) course at Ouachita Baptist University in
3310             # Arkadelphia AR.
3311              
3312 16898         26515 my $diag = sqrt( $cart_pos[0] * $cart_pos[0] +
3313             $cart_pos[1] * $cart_pos[1] );
3314 16898         24313 my ( $sin_theta, $cos_theta, $sin_phi, $cos_phi );
3315 16898 50       28733 if ( $range > 0 ) {
3316 16898         25395 $sin_theta = $cart_pos[2] / $range;
3317 16898         22493 $cos_theta = $diag / $range;
3318 16898 50       27275 if ( $diag > 0 ) {
3319 16898         20569 $sin_phi = $cart_pos[1] / $diag;
3320 16898         22539 $cos_phi = $cart_pos[0] / $diag;
3321             } else {
3322 0         0 $sin_phi = 0;
3323 0         0 $cos_phi = 1;
3324             }
3325              
3326             # phi = - sin Phi x + cos phi y
3327             # theta = - sin Theta cos Phi x - sin Theta sin Phi y + cos Theta z
3328             # r = cos Theta cos Phi x + cos Theta sin Phi y + sin Theta z
3329              
3330 16898         39409 my $az_hat = [ - $sin_phi, $cos_phi, 0 ];
3331 16898         35563 my $el_hat = [ - $sin_theta * $cos_phi, - $sin_theta * $sin_phi,
3332             $cos_theta ];
3333 16898         44137 my $rng_hat = [ $cos_theta * $cos_phi, $cos_theta * $sin_phi,
3334             $sin_theta ];
3335              
3336 16898         34494 while ( @cart_data ) {
3337              
3338             # Each triplet is then converted by projecting the
3339             # Cartesian vector onto the appropriate unit vector.
3340             # Azimuth and elevation are also converted to radians by
3341             # dividing by the range. NOTE that this is the
3342             # small-angle approximation, but since we assume we're
3343             # dealing with derivitaves, it's OK.
3344              
3345 16898         28120 my @cart_info = splice @cart_data, 0, 3;
3346 16898         35636 push @rslt, vector_dot_product( $az_hat, \@cart_info ) / $range;
3347 16898         34084 push @rslt, vector_dot_product( $el_hat, \@cart_info ) / $range;
3348 16898         32654 push @rslt, vector_dot_product( $rng_hat, \@cart_info );
3349             }
3350             } else {
3351             # $sin_theta = $sin_phi = 0;
3352             # $cos_theta = $cos_phi = 1;
3353             # We used to do the above and then drop through and do the
3354             # velocity calculation if needed. But in this case the
3355             # velocity calulation blows up. So for the moment we are
3356             # just going to punt.
3357             }
3358              
3359             }
3360              
3361 24487         72461 return @rslt;
3362              
3363             }
3364              
3365             # @spherical = _convert_cartesian_to_spherical_x( @cartesian )
3366             #
3367             # This subroutine is a convenience wrapper for
3368             # _convert_cartesian_to_spherical(). The only difference is that the
3369             # return is Theta (perpendicular to the X-Y plane), Phi (in the X-Y
3370             # plane) and Rho, rather than Phi, Theta, Rho. This subroutine is my
3371             # penance for not having all the methods that involve spherical
3372             # coordinates return their arguments in the same order.
3373              
3374             sub _convert_cartesian_to_spherical_x {
3375 1362     1362   2958 my @sph_data = _convert_cartesian_to_spherical( @_ );
3376 1362         3285 for ( my $inx = 0; $inx < @sph_data; $inx += 3 ) {
3377 1362         3868 @sph_data[ $inx, $inx + 1 ] = @sph_data[ $inx + 1, $inx ];
3378             }
3379 1362         7266 return @sph_data;
3380             }
3381              
3382             # @eci = $self->_convert_ecef_to_eci ()
3383              
3384             # This subroutine converts the object's ECEF setting to ECI, and
3385             # both caches and returns the result.
3386              
3387             sub _convert_ecef_to_eci {
3388 7     7   14 my $self = shift;
3389 7         14 my $thetag = thetag ($self->universal);
3390 7         22 my @data = $self->ecef ();
3391 7 50       19 $self->{debug} and print <
3392             Debug eci - thetag = $thetag
3393             (x, y) = (@data[0, 1])
3394             eod
3395 7         19 my $costh = cos ($thetag);
3396 7         12 my $sinth = sin ($thetag);
3397 7         21 @data[0, 1] = ($data[0] * $costh - $data[1] * $sinth,
3398             $data[0] * $sinth + $data[1] * $costh);
3399 7 50       18 $self->{debug} and print <
3400             Debug eci - after rotation,
3401             (x, y) = (@data[0, 1])
3402             eod
3403 7 50       38 if ( @data > 5 ) {
3404              
3405 7         32 @data[3, 4] = ($data[3] * $costh - $data[4] * $sinth,
3406             $data[3] * $sinth + $data[4] * $costh);
3407 7         12 $data[3] -= $data[1] * $self->{angularvelocity};
3408 7         16 $data[4] += $data[0] * $self->{angularvelocity};
3409             }
3410              
3411             # A bunch of digging says this was added by Subversion commit 697,
3412             # which was first released with Astro::Coord::ECI version 0.014. The
3413             # comment says "Handle equinox in conversion between eci and ecef.
3414             # Correctly, I hope." I'm leaving it in for now, but ...
3415 7         18 $self->set (equinox_dynamical => $self->dynamical);
3416 7         9 return @{$self->{_ECI_cache}{inertial}{eci} = \@data};
  7         55  
3417             }
3418              
3419             # This subroutine converts the object's ECI setting to ECEF, and
3420             # both caches and returns the result.
3421              
3422             our $EQUINOX_TOLERANCE = 365 * SECSPERDAY;
3423              
3424             sub _convert_eci_to_ecef {
3425 29921     29921   41246 my $self = shift;
3426 29921         47272 my $thetag = thetag ($self->universal);
3427              
3428 29921         62075 my $dyn = $self->dynamical;
3429             ## my $equi = $self->get ('equinox_dynamical') || do {
3430             ## $self->set (equinox_dynamical => $dyn); $dyn};
3431 29921   66     65425 my $equi = $self->{equinox_dynamical} ||= $dyn;
3432 29921 50       70401 if (abs ($equi - $dyn) > $EQUINOX_TOLERANCE) {
3433 0         0 $self->precess_dynamical ($dyn);
3434             }
3435              
3436 29921         55211 my @ecef = $self->eci ();
3437 29921         62184 my $costh = cos (- $thetag);
3438 29921         47268 my $sinth = sin (- $thetag);
3439 29921         68589 @ecef[0, 1] = ($ecef[0] * $costh - $ecef[1] * $sinth,
3440             $ecef[0] * $sinth + $ecef[1] * $costh);
3441              
3442 29921 100       58881 if ( @ecef > 3 ) {
3443 18309         32047 @ecef[ 3, 4 ] = ( $ecef[3] * $costh - $ecef[4] * $sinth,
3444             $ecef[3] * $sinth + $ecef[4] * $costh );
3445 18309         30003 $ecef[3] += $ecef[1] * $self->{angularvelocity};
3446 18309         24696 $ecef[4] -= $ecef[0] * $self->{angularvelocity};
3447             }
3448              
3449 29921         60280 $self->{_ECI_cache}{fixed}{ecef} = \@ecef;
3450              
3451             defined wantarray
3452 29921 50       58176 or return;
3453 29921         80929 return @ecef;
3454             }
3455              
3456             # my @args = _expand_args_default_station( @_ )
3457             #
3458             # This subroutine handles the placing of the contents of the
3459             # 'station' attribute into the argument list of methods that,
3460             # prior to the introduction of the 'station' attribute, formerly
3461             # took two Astro::Coord::ECI objects and computed the position of
3462             # the second as seen from the first.
3463              
3464             sub _expand_args_default_station {
3465 39403     39403   69113 my @args = @_;
3466 39403 100       83229 if ( ! embodies( $args[1], 'Astro::Coord::ECI' ) ) {
3467 17         57 unshift @args, $args[0]->get( 'station' );
3468 17 50       55 embodies( $args[0], 'Astro::Coord::ECI' )
3469             or croak 'Station not set';
3470             defined $args[1]->{universal}
3471 17 50       64 and $args[0]->universal( $args[1]->universal() );
3472             }
3473 39403         92422 return @args;
3474             }
3475              
3476             # $value = $self->__initial_inertial
3477             #
3478             # Return the initial setting of the inertial attribute. At this
3479             # level we are assumed not inertial until we acquire a position.
3480             # This is not part of the public interface, but may be used by
3481             # subclasses to set an initial value for this read-only attribute.
3482             # Setting the coordinates explicitly will still set the {inertial}
3483             # attribute appropriately.
3484              
3485 59     59   155 sub __initial_inertial{ return };
3486              
3487             # $value = _local_mean_delta ($coord)
3488              
3489             # Calculate the delta from universal to civil time for the object.
3490             # An exception is raised if the coordinates of the object have not
3491             # been set.
3492              
3493             sub _local_mean_delta {
3494 2     2   7 return ($_[0]->geodetic ())[1] * SECSPERDAY / TWOPI;
3495             }
3496              
3497             =begin comment
3498              
3499             # $string = _rad2dms ($angle)
3500              
3501             # Convert radians to degrees, minutes, and seconds of arc.
3502             # Used for debugging.
3503              
3504             sub _rad2dms {
3505             my $angle = rad2deg (shift);
3506             my $deg = floor ($angle);
3507             $angle = ($angle - $deg) * 60;
3508             my $min = floor ($angle);
3509             $angle = ($angle - $min) * 60;
3510             return "$deg degrees $min minutes $angle seconds of arc";
3511             }
3512              
3513             =end comment
3514              
3515             =cut
3516              
3517             #######################################################################
3518             #
3519             # Package initialization
3520             #
3521              
3522             __PACKAGE__->set (ellipsoid => 'WGS84');
3523              
3524             %savatr = map {$_ => 1} (keys %static, qw{dynamical id name universal});
3525             # Note that local_mean_time does not get preserved, because it
3526             # changes if the coordinates change.
3527              
3528             1;
3529              
3530             __END__