=head1 NAME

Astro::Coord::ECI - Manipulate geocentric coordinates

=head1 SYNOPSIS

 use Astro::Coord::ECI;
 use Astro::Coord::ECI::Sun;
 use Astro::Coord::ECI::TLE;
 use Math::Trig;	# for the rad2deg function.
 # 1600 Pennsylvania Avenue, in radians, radians, and KM
 my ($lat, $lon, $elev) = (0.678911227503559,
     -1.34456123391096, 0.01668);
 # Record the time
 my $time = time ();
 # Set up observer's location
 my $loc = Astro::Coord::ECI->geodetic ($lat, $lon, $elev);
 # Instantiate the Sun.
 my $sun = Astro::Coord::ECI::Sun->universal ($time);
 # Figure out if the Sun is up at the observer's location.
 my ($azimuth, $elevation, $range) = $loc->azel ($sun);
 print "The Sun is ", rad2deg ($elevation),
     " degrees above the horizon.\n";
 # Instantiate a satellite. The parser returns a list.
 my ($tle) = Astro::Coord::ECI::TLE->parse (<<eod);
 Pretend this text contains NORAD orbital element data for
 a satellite. If it contains more than one set of data, you
 get more than one object back.
 eod
 # Figure out where the satellite is in the observer's sky.
 # Note that setting the time runs the model.
 my ($right_asc, $decl, $rng) =
     $loc->equatorial ($tle->universal ($time));
 # Find the satellite's ECI coordinates at the given time.
 # It is not necessary to run the model again, unless you
 # want coordinates for a different time.
 my ($X, $Y, $Z) = $tle->eci ();
 print <<eod;
 Satellite position
     equatorial coordinates:
         right ascension: @{[rad2deg ($right_asc)]} degr
         declination: @{[rad2deg ($decl)]} degr
         range: $rng km
     ECI coordinates:
         X: $X
         Y: $Y
         Z: $Z
 eod

=head1 DESCRIPTION

This module was written to provide a base class for a system to predict
satellite visibility. Its main task is to convert the
Earth-Centered Inertial (ECI) coordinates generated by the NORAD models
into other coordinate systems (e.g. latitude, longitude, and altitude
above mean sea level), but a other functions have accreted to it.
In addition, a few support routines have been exposed for testing, or
whatever.

All distances are in kilometers, and all angles are in radians
(including right ascension, which is usually measured in hours).

Times are normal Perl times except for the Julian Day routines, which
convert Perl time into things like Julian Day or days since Julian
2000.0 (i.e. the aforementioned support routines), and for a few
support routines which take or return dynamical times. Time is
specified and returned in universal time unless the documentation of
the method explicitly states otherwise. In practice, universal time is
approximated by normal Perl time.

Known subclasses include B<Astro::Coord::ECI::Moon> to predict the
position of the Moon, B<Astro::Coord::ECI::Star> to predict the
position of a star, or anything else that can be considered fixed on
the celestial sphere, B<Astro::Coord::ECI::Sun> to predict the position
of the Sun, and B<Astro::Coord::ECI::TLE> to predict the position of a
satellite given the NORAD orbital parameters.

B<Caveat user:> This class and its subclasses should probably be
considered alpha code, meaning that the public interface may not be
completely stable. I will try not to change it, but given sufficient
reason I will do so. If I do so, I will draw attention to the change
in the documentation.

=head2 Methods

The following methods should be considered public.

=over 4

=cut

use strict;
use warnings;

package Astro::Coord::ECI;

our $VERSION = '0.005';

use Astro::Coord::ECI::Utils qw{:all};
use Carp;
use Data::Dumper;
use POSIX qw{floor strftime};
use Storable qw{dclone};
use Time::Local;
use UNIVERSAL qw{can isa};

use constant MINUSONEDEGREE => deg2rad (-1);

my %mutator;	# Attribute mutators. We define these just after the
		# set() method, for convenience.
my %known_ellipsoid;	# Known reference ellipsoids. We define these
		# just before the reference_ellipsoid() method for
		# convenience.
my %static = (	# The geoid, etc. Geoid get set later.
    angularvelocity => 7.292114992e-5,	# Of surface of Earth, 1998. Meeus, p.83
    debug => 0,
    diameter => 0,
    refraction => 1,
    twilight => deg2rad (-6),
    );
my %savatr;	# Attribs saved across "normal" operations. Set at end.
my @kilatr =	# Attributes to purge when setting coordinates.
    qw{_need_purge eci ecliptic equatorial
	geocentric geodetic local_mean_time specified ecef}; #?


=item $coord = Astro::Coord::ECI->new ();

This method instantiates a coordinate object. Any arguments are passed
to the set() method once the object has been instantiated.

=cut

sub new {
my $class = shift;
my $self = bless {%static}, $class;
@_ and $self->set (@_);
$self->{debug} and do {
    local $Data::Dumper::Terse = 1;
    print "Debug - Instantiated ", Dumper ($self);
    };
$self;
}


=item $angle = $coord->angle ($coord2, $coord3);

This method calculates the angle between the $coord2 and $coord3
objects, as seen from $coord. The calculation uses the law of cosines,
and does not take atmospheric refraction into account. The return is
a number of radians between 0 and pi.

=cut

sub angle {
my $self = shift;
my $B = shift;
my $C = shift;
ref $B && UNIVERSAL::isa ($B, __PACKAGE__) &&
	ref $C && UNIVERSAL::isa ($C, __PACKAGE__) or
	croak <<eod;
Error - Both arguments must be @{[__PACKAGE__]} objects.
eod

my $a = distsq ([$B->ecef], [$C->ecef]);
my $b = distsq ([$self->ecef], [$C->ecef]);
my $c = distsq ([$self->ecef], [$B->ecef]);

acos (($b + $c - $a) / sqrt (4 * $b * $c));
}


=item ($azimuth, $elevation, $range) = $coord->azel ($coord2, $upper);

This method takes another coordinate object, and computes its azimuth,
elevation, and range in reference to the object doing the computing.
The return is azimuth in radians measured clockwise from North (always
positive), elevation above the horizon in radians (negative if
below), and range in kilometers.

If the optional 'upper' argument is true, the calculation will be of
the upper limb of the object, using the 'diameter' attribute of the
$coord2 object.

As a side effect, the time of the $coord object is set from the
$coord object.

If the L<refraction|/refraction> attribute of the $coord object is
true, the elevation will be corrected for atmospheric refraction using
the correct_for_refraction() method.

The basic algorithm comes from T. S. Kelso's "Computers and Satellites"
column in "Satellite Times", November/December 1995, titled "Orbital
Coordinate Systems, Part II" and available at
F<http://celestrak.com/columns/v02n02/>.

=for comment help syntax-highlighting editor '

=cut

sub azel {
my $self = shift;
$self->{debug} and do {
    local $Data::Dumper::Terse = 1;
    print "Debug azel - ", Dumper ($self, @_);
    };
my $trn2 = shift;
my $upper = shift;
my @obj = $trn2->eci (@_);
my $time = $trn2->universal;
my @base = $self->eci ($time);
my ($phi, $lamda, $h) = $self->geodetic;
my @delta;


#	Kelso algorithm from
#	http://celestrak.com/columns/v02n02/

for (my $i = 0; $i < 6; $i++) {
    $delta[$i] = $obj[$i] - $base[$i];
    }
my $theta = mod2pi (thetag ($time) + $lamda);
my $sinlat = sin ($phi);
my $sintheta = sin ($theta);
my $coslat = cos ($phi);
my $costheta = cos ($theta);
my $rterm = $costheta * $delta[0] + $sintheta * $delta[1];
my $ts = $sinlat * $rterm - $coslat * $delta[2];
my $te = - $sintheta * $delta[0] + $costheta * $delta[1];
my $tz = $coslat * $rterm + $sinlat * $delta[2];
my $azimuth = mod2pi (atan2 ($te, - $ts));
my $range = sqrt ($delta[0] * $delta[0] + $delta[1] * $delta[1] +
	$delta[2] * $delta[2]);
my $elevation = asin ($tz / $range);


#	End of Kelso algorithm.


#	Adjust for upper limb and refraction if needed.

$upper and
    $elevation += atan2 ($trn2->get ('diameter') / 2, $range);

$self->{refraction} and
    $elevation = $self->correct_for_refraction ($elevation);

($azimuth, $elevation, $range);
}


=item $coord2 = $coord->clone ();

This method does a deep clone of an object, producing a different
but identical object.

It's really just a wrapper for Storable::dclone.

=for comment help syntax-highlighting editor '

=cut

sub clone {
dclone shift;
}

=item $elevation = $coord->correct_for_refraction ($elevation);

This method corrects the given angular elevation for atmospheric
refraction. This is done only if the corrected elevation would be
non-negative. Sorry for the discontinuity thus introduced, but I did
not have a corresponding algorithm for refraction through magma.

This method can also be called as a class method. It is really only
exposed for testing purposes (hence the cumbersome name). The azel()
method calls it for you if the L<refraction|/refraction> attribute
is true.

The algorithm for atmospheric refraction comes from Thorfinn
Saemundsson's article in "Sky and Telescope", volume 72, page 70
(July 1986) as reported Jean Meeus' "Astronomical Algorithms",
2nd Edition, chapter 16, page 106, and includes the adjustment
suggested by Meeus.

=cut

sub correct_for_refraction {
my $self = shift;
my $elevation = shift;


#	We can exclude anything with an elevation <= -1 degree because
#	the maximum deflection is about 35 minutes of arc. This is
#	not portable to (e.g.) Venus.


if ($elevation > MINUSONEDEGREE) {


#	Thorsteinn Saemundsson's algorithm for refraction, as reported
#	in Meeus, page 106, equation 16.4, and adjusted per the
#	suggestion in Meeus' following paragraph. Thorsteinn's
#	formula is in terms of angles in degrees and produces
#	a correction in minutes of arc. Meeus reports the original
#	publication as Sky and Telescope, volume 72 page 70, July 1986.

#	In deference to Thorsteinn I will point out:
#	* The Icelanders do not use family names. The "Saemundsson"
#	  simply means his father's name was Saemund.
#	* I have transcribed the names into 7-bit characters.
#	  "Thorsteinn" actually does not begin with "Th" but with
#	  the letter "Thorn." Similarly, the "ae" in "Saemund" is
#	  supposed to be a ligature (i.e. squished into one letter).

    my $deg = rad2deg ($elevation);
    my $correction = 1.02 / tan (deg2rad ($deg + 10.3/($deg + 5.11))) +
	.0019279;
    $self->get ('debug') and print <<eod;
Debug correct_for_refraction
    input: $deg degrees of arc
    correction: $correction minutes of arc
eod

#	End of Thorsteinn's algorithm. To be consistent with the
#	documentation, we do not correct the elevation unless the
#	correction would place the body above the horizon.

    $correction = deg2rad ($correction / 60);
    $elevation += $correction if $correction + $elevation >= 0;
    }
$elevation;
}


=item $angle = $coord->dip ();

This method calculates the dip angle of the horizon due to the
altitude of the body, in radians. It will be negative for a location
above the surface of the reference ellipsoid, and positive for a
location below the surface.

The algorithm is simple enough to be the author's.

=for comment '

=cut

sub dip {
my $self = shift;
my ($psi, $lamda, $h) = $self->geodetic;
my ($psiprime, undef, $rho) = $self->geocentric;
my $angle = $h >= 0 ?
    - acos (($rho - $h) / $rho) :
    acos ($rho / ($rho - $h));
}


=item $coord = $coord->dynamical ($time);

This method sets the dynamical time represented by the object.

This method can also be called as a class method, in which case it
instantiates the desired object.

The algorithm for converting this to universal time comes from Jean
Meeus' "Astronomical Algorithms", 2nd Edition, Chapter 10, pages 78ff.

=item $time = $coord->dynamical ();

This method returns the dynamical time previously set, or the
universal time previously set, converted to dynamical. The algorithm

The algorithm comes from Jean Meeus' "Astronomical Algorithms", 2nd
Edition, Chapter 10, pages 78ff.

=cut

sub dynamical {
my $self = shift;
unless (@_) {
    ref $self or croak <<eod;
Error - The dynamical() method may not be called as a class method
        unless you specify arguments.
eod
    return ($self->{dynamical} ||= $self->{universal} +
	_dynamical_delta ($self->{universal} || croak <<eod));
Error - Object's time has not been set.
eod

=for comment ' help syntax-highlighting editor.

=cut

    }

if (@_ == 1) {
    $self = $self->new () unless ref $self;
    $self->universal ($_[0] - _dynamical_delta ($_[0]));
    $self->{dynamical} = $_[0];
    }
  else {
    croak <<eod;
Error - The dynamical() method must be called with either zero
        arguments (to retrieve the time) or one argument (to set the
        time).
eod
    }

$self;
}


#	_dynamical_delta ($universal_time)

#	calculates and returns the difference between the dynamical
#	time and the universal time at the given universal time.
#	Actually, we typically ignore whether we're passing dynamical
#	or universal time, since the relation changes so slowly.

sub _dynamical_delta {
my $year = (gmtime $_[0])[5] + 1900;
my $t = ($year - 2000) / 100;
my $correction = .37 * ($year - 2100);	# Meeus' correction to (10.2)
(25.3 * $t + 102) * $t + 102		# Meeus (10.2)
	+ $correction;			# Meeus' correction.
}

=item $coord = $coord->ecef($x, $y, $z, $xdot, $ydot, $zdot)

This method sets the coordinates represented by the object in terms
of L</Earth-Centered, Earth-fixed (ECEF) coordinates>, with x being
latitude 0 longitude 0, y being latitude 0 longitude 90 degrees east,
and z being latitude 90 degrees north. The velocities are optional, and
will default to the rotational velocity at the point being set. The
object itself is returned.

This method can also be called as a class method, in which case it
instantiates the desired object.

=item ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->ecef();

This method returns the object's L</Earth-Centered, Earth-fixed (ECEF)
coordinates>.

B<Caveat:> Velocities are also returned, but should not at this point
be taken seriously unless they were originally set by the same method
that is returning them, since I have not at this point got the velocity
transforms worked out.

=for comment help syntax-highlighting editor '

=cut

sub ecef {
my $self = shift;

$self = $self->_check_coord (ecef => \@_);

unless (@_) {
    return @{$self->{ecef} || croak <<eod};
Error - Object has not been initialized.
eod
    }

if (@_ == 3) {
    push @_, 0, 0, 0;
    }

if (@_ == 6) {
    foreach my $key (@kilatr) {delete $self->{$key}}
    $self->{ecef} = [@_];
    $self->{specified} = 'ecef';
    $self->{inertial} = 0;
    }
  else {
    croak <<eod;
Error - The ecef() method must be called with either zero arguments (to
        retrieve coordinates), three arguments (to set coordinates,
        with velocity defaulting to the rotational velocity of the
        earth), or six arguments.
eod
    }

$self;
}


=item $coord = $coord->eci ($x, $y, $z, $xdot, $ydot, $zdot, $time)

This method sets the coordinates represented by the object in terms
of L</Earth-Centered Inertial (ECI) coordinates>, time being universal
time, x being 0 L</Right Ascension> 0 L</Declination>, y being 6 hours
L</Right Ascension> 0 L</Declination>, and z being 90 degrees north
L</Declination>. The velocities are optional, and will default to zero.

The time argument is optional if the time represented by the object
has already been set (e.g. by the universal() or dynamical() methods).

The object itself is returned.

This method can also be called as a class method, in which case it
instantiates the desired object. In this case the time is not optional.

The algorithm for converting from ECI to geocentric coordinates and
back is based on the description of ECI coordinates in T. S. Kelso's
"Computers and Satellites" column in "Satellite Times",
September/October 1995, titled "Orbital Coordinate Systems, Part I"
and available at F<http://celestrak.com/columns/v02n01/>.

=item ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->eci($time);

This method returns the L</Earth-Centered Inertial (ECI) coordinates>
of the object at the given time. The time argument is actually
optional if the time represented by the object has already been set.

If you specify a time, the time represented by the object will be set
to that time. The net effect of specifying a time is equivalent to

 ($x, $y, $z, $xdot, $ydot, $zdot) = $coord->universal($time)->eci()

B<Caveat:> Velocities are also returned, but should not at this point
be taken seriously unless they were originally set by the same method
that is returning them, since I have not at this point got the velocity
transforms worked out.

=for comment help syntax-highlighting editor '

=cut

sub eci {
my $self = shift;

$self = $self->_check_coord (eci => \@_);

unless (@_) {
    return @{$self->{eci}} if $self->{eci};
    my $thetag = thetag ($self->universal);
    my @data = $self->ecef;
    $self->{debug} and print <<eod;
Debug eci - thetag = $thetag
    (x, y) = (@data[0, 1])
eod
    my $costh = cos ($thetag);
    my $sinth = sin ($thetag);
    @data[0, 1] = ($data[0] * $costh - $data[1] * $sinth,
	$data[0] * $sinth + $data[1] * $costh);
    @data[3, 4] = ($data[3] * $costh - $data[4] * $sinth,
	$data[3] * $sinth + $data[4] * $costh);
     $self->{debug} and print <<eod;
Debug eci - after rotation,
    (x, y) = (@data[0, 1])
eod
   $data[3] += $data[1] * $self->{angularvelocity};
    $data[4] -= $data[0] * $self->{angularvelocity};
    return @{$self->{eci} = [@data]};
    }

@_ == 3 and push @_, 0, 0, 0;

if (@_ == 6) {
    my $thetag = thetag ($self->universal);
    my @ecef = @_;
    $ecef[3] -= $ecef[1] * $self->{angularvelocity};
    $ecef[4] += $ecef[0] * $self->{angularvelocity};
    my $costh = cos (- $thetag);
    my $sinth = sin (- $thetag);
    @ecef[0, 1] = ($ecef[0] * $costh - $ecef[1] * $sinth,
	$ecef[0] * $sinth + $ecef[1] * $costh);
    @ecef[3, 4] = ($ecef[3] * $costh - $ecef[4] * $sinth,
	$ecef[3] * $sinth + $ecef[4] * $costh);
    $self->ecef (@ecef);
    $self->{eci} = [@_];
    $self->{specified} = 'eci';
    $self->{inertial} = 1;
    }
  else {
    croak <<eod;
Error - The eci() method must be called with either zero or one
        arguments (to retrieve coordinates), three or four arguments
        (to set coordinates, with velocity defaulting to zero), or
        six or seven arguments.
eod
    }
$self;
}


=item $coord = $coord->ecliptic ($latitude, $longitude, $range, $time);

This method sets the L</Ecliptic> coordinates represented by the object
in terms of L</Ecliptic latitude> and L<Ecliptic longitude> in radians,
and the range to the object in kilometers, time being universal time.
The object itself is returned.

The time argument is optional if the time represented by the object has
already been set (e.g. by the universal() or dynamical() methods).

This method can also be called as a class method, in which case it
instantiates the desired object. In this case the time is not optional.

The algorithm for converting from ecliptic latitude and longitude to
right ascension and declination comes from Jean Meeus'
"Astronomical Algorithms", 2nd Edition, Chapter 13, page 93.

=item ($latitude, $longitude, $range) = $coord->ecliptic ($time);

This method returns the ecliptic latitude and longitude of the
object at the given time. The time is optional if the time represented
by the object has already been set (e.g. by the universal() or
dynamical() methods).

=for comment help syntax-highlighting editor '

=cut

sub ecliptic {
my $self = shift;

$self = $self->_check_coord (ecliptic => \@_);

unless (@_) {
    return @{$self->{ecliptic}} if $self->{ecliptic};
    my ($alpha, $delta, $rho) = $self->equatorial ();

    my $epsilon = obliquity ($self->dynamical);
    my $sinalpha = sin ($alpha);
    my $cosdelta = cos ($delta);
    my $sindelta = sin ($delta);
    my $cosepsilon = cos ($epsilon);
    my $sinepsilon = sin ($epsilon);

    my $lamda = mod2pi (atan2 ($sinalpha * $cosepsilon +	# Meeus (13.1), pg 93.
	$sindelta / $cosdelta * $sinepsilon, cos ($alpha)));
    my $beta = asin ($sindelta * $cosepsilon -		# Meeus (13.2), pg 93.
	$cosdelta * $sinepsilon * $sinalpha);

    return @{$self->{ecliptic} = [$beta, $lamda, $rho]};
    }

if (@_ == 3) {
    ref $self or $self = $self->new ();
    my ($beta, $lamda, $rho) = @_;

    $lamda = mod2pi ($lamda);
    my $epsilon = obliquity ($self->dynamical);
    my $sinlamda = sin ($lamda);
    my $cosepsilon = cos ($epsilon);
    my $sinepsilon = sin ($epsilon);
    my $cosbeta = cos ($beta);
    my $sinbeta = sin ($beta);
    my $alpha = mod2pi (atan2 ($sinlamda * $cosepsilon -	# Meeus (13.3), pg 93
	$sinbeta / $cosbeta * $sinepsilon, cos ($lamda)));
    my $delta = asin ($sinbeta * $cosepsilon +		# Meeus (13.4), pg 93.
	$cosbeta * $sinepsilon * $sinlamda);
    $self->{debug} and print <<eod;
Debug ecliptic -
    beta = $beta (ecliptic latitude, radians)
         = @{[rad2deg ($beta)]} (ecliptic latitude, degrees)
    lamda = $lamda (ecliptic longitude, radians)
         = @{[rad2deg ($lamda)]} (ecliptic longitude, degrees)
    rho = $rho (range, kilometers)
    epsilon = $epsilon (obliquity of ecliptic, radians)
    alpha = $alpha (right ascension, radians)
    delta = $delta (declination, radians)
eod
    $self->equatorial ($alpha, $delta, $rho);
    $self->{ecliptic} = [@_];
    $self->{specified} = 'ecliptic';
    $self->{inertial} = 1;
    }
  else {
    croak <<eod;
Error - The ecliptic() method must be called with either zero or one
        arguments (to retrieve coordinates), or three or four arguments
        (to set coordinates). There is currently no six or seven
        argument version.
eod
    }
$self;
}


=item $coord->equatorial ($rightasc, $declin, $range, $time);

This method sets the L</Equatorial> coordinates represented by the
object in terms of L<Right Ascension> and L</Declination> in radians,
and the range to the object in kilometers, time being universal
time. The object itself is returned.

The time argument is optional if the time represented by the object
has already been set (e.g. by the universal() or dynamical() methods).

This method can also be called as a class method, in which case it
instantiates the desired object. In this case the time is not optional.

=item ($rightasc, $declin, $range) = $coord->equatorial ($time);

This method returns the L</Equatorial> coordinates of the object at the
given time. The time argument is optional if the time represented by
the object has already been set (e.g. by the universal() or
dynamical() methods).

=item ($rightasc, $declin, $range) = $coord->equatorial ($coord2);

This method returns the apparant equatorial coordinates of the object
represented by $coord2, as seen from the location represented by
$coord.

As a side effect, the time of the $coord object is set from the
$coord object.

If the L<refraction|/refraction> attribute of the $coord object is
true, the coordinates will be corrected for atmospheric refraction using
the correct_for_refraction() method.

The algorithm is lifted pretty much verbatim from the Calculate_RADec
subroutine of SGP4 Pascal Library Version 2.65 by T. S. Kelso,
available at L<http://celestrak.com/software/tskelso-sw.asp>. Dr.
Kelso credits the algorithm to "Methods of Orbit Determination" by
Pedro Ramon Escobal, pages 401 - 402.

=cut

sub equatorial {
my $self = shift;

my $body = shift if @_ && isa ($_[0], __PACKAGE__);

$self = $self->_check_coord (equatorial => \@_);

my $time = $self->universal unless $body;

unless (@_) {

    if ($body) {
	my ($azimuth, $elevation, $range) = $self->azel ($body, 0);
	my $time = $body->universal ();
	my ($phi, $theta) = $self->geodetic;
	$theta = mod2pi ($theta + thetag ($time));
	my $sin_theta = sin ($theta);
	my $cos_theta = cos ($theta);
	my $sin_phi = sin ($phi);
	my $cos_phi = cos ($phi);
	my $coselev = cos ($elevation);
	my $lxh = - cos ($azimuth) * $coselev;
	my $lyh = sin ($azimuth) * $coselev;
	my $lzh = sin ($elevation);
	my $sx = $sin_phi * $cos_theta;
	my $ex = - $sin_theta;
	my $zx = $cos_theta * $cos_phi;
	my $sy = $sin_phi * $sin_theta;
	my $ey = $cos_theta;
	my $zy = $sin_theta * $cos_phi;
	my $sz = - $cos_phi;
	my $ez = 0;
	my $zz = $sin_phi;
	my $lx = $sx * $lxh + $ex * $lyh + $zx * $lzh;
	my $ly = $sy * $lxh + $ey * $lyh + $zy * $lzh;
	my $lz = $sz * $lxh + $ez * $lyh + $zz * $lzh;
	my $declination = asin ($lz);
	my $cos_delta = sqrt (1 - $lz * $lz);
	my $sin_alpha = $ly / $cos_delta;
	my $cos_alpha = $lx / $cos_delta;
	my $right_ascension = mod2pi (atan2 ($sin_alpha,$cos_alpha));
	return ($right_ascension, $declination, $range);
	}

    return @{$self->{equatorial}} if $self->{equatorial};
    my ($x, $y, $z, $xdot, $ydot, $zdot) = $self->eci ();
    my $ra = mod2pi (atan2 ($y, $x));	# Right ascension is always positive.
    my $rsq = $x * $x + $y * $y;
    my $dec = atan2 ($z, sqrt ($rsq));
    my $range = sqrt ($rsq + $z * $z);
    return @{$self->{equatorial} = [$ra, $dec, $range]};
    }

if (@_ == 3) {
    $body && croak <<eod;
Error - You may not set the equatorial coordinates relative to an
        observer.
eod
    my ($ra, $dec, $range) = @_;
    my $z = $range * sin ($dec);
    my $r = $range * cos ($dec);
    my $x = $r * cos ($ra);
    my $y = $r * sin ($ra);
    $self->eci ($x, $y, $z, 0, 0, 0);
    $self->{equatorial} = [@_];
    $self->{specified} = 'equatorial';
    $self->{inertial} = 1;
    }
  else {
    croak <<eod;
Error - The equatorial() method must be called with either zero or one
        arguments (to retrieve coordinates), or three or four arguments
        (to set coordinates). There is currently no six or seven
        argument version.
eod
    }
$self;
}


=item $coord = $coord->geocentric($psiprime, $lamda, $rho);

This method sets the L</Geocentric> coordinates represented by the
object in terms of L</Geocentric latitude> psiprime and L</Longitude>
lamda in radians, and distance from the center of the Earth rho in
kilometers.

This method can also be called as a class method, in which case it
instantiates the desired object.

B<This method should not be used with map coordinates> because map
latitude is L</Geodetic latitude>, measured in terms of the tangent of
the reference ellipsoid, whereas geocentric coordinates are,
essentially, spherical coordinates.

The algorithm for conversion between geocentric and ECEF is the
author's.

=item ($psiprime, $lamda, $rho) = $coord->geocentric();

This method returns the L</Geocentric latitude>, L</Longitude>, and
distance to the center of the Earth.

=for comment help syntax-highlighting editor '

=cut

sub geocentric {
my $self = shift;

$self = $self->_check_coord (geocentric => \@_);

unless (@_) {
    return @{$self->{geocentric} ||= do {
	my ($x, $y, $z, $xdot, $ydot, $zdot) = $self->ecef;
	my $rsq = $x * $x + $y * $y;
	my $rho = sqrt ($z * $z + $rsq);
	my $lamda = atan2 ($y, $x);
	my $psiprime = atan2 ($z, sqrt ($rsq));
	$self->get ('debug') and print <<eod;
Debug geocentric () - ecef -> geocentric
    inputs:
        x = $x
        y = $y
        z = $z
    outputs:
        psiprime = $psiprime
        lamda = $lamda
        rho = $rho
eod
	[$psiprime, $lamda, $rho];
	}};
    }

if (@_ == 3) {
    my ($psiprime, $lamda, $rho) = @_;
    my $z = $rho * sin ($psiprime);
    my $r = $rho * cos ($psiprime);
    my $x = $r * cos ($lamda);
    my $y = $r * sin ($lamda);
    $self->get ('debug') and print <<eod;
Debug geocentric () - geocentric -> ecef
    inputs:
        psiprime = $psiprime
        lamda = $lamda
        rho = $rho
    outputs:
        x = $x
        y = $y
        z = $z
eod
    $self->ecef ($x, $y, $z);
    $self->{geocentric} = [@_];
    $self->{specified} = 'geocentric';
    $self->{inertial} = 0;
    }
  else {
    croak <<eod;
Error - Method geocentric() must be called with either zero arguments
        (to retrieve coordinates) or three arguments (to set
        coordinates). There is currently no six argument version.
eod
    }
$self;
}


=item $coord = $coord->geodetic($psi, $lamda, $h, $ellipsoid);

This method sets the L</Geodetic> coordinates represented by the object
in terms of its L</Geodetic latitude> psi and L</Longitude> lamda in
radians, and its height h above mean sea level in kilometers.

The ellipsoid argument is the name of a L</Reference Ellipsoid> known
to the class, and is optional. If passed, it will set the ellipsoid
to be used for calculations with this object. If not passed, the
default ellipsoid is used.

This method can also be called as a class method, in which case it
instantiates the desired object.

The conversion from geodetic to geocentric comes from Jean Meeus'
"Astronomical Algorithms", 2nd Edition, Chapter 11, page 82.

B<This is the method that should be used with map coordinates.>

=item ($psi, $lamda, $h) = $coord->geodetic($ellipsoid);

This method returns the geodetic latitude, longitude, and height
above mean sea level.

The ellipsoid argument is the name of a L</Reference ellipsoid> known
to the class, and is optional. If not specified, the most-recently-set
ellipsoid will be used.

The conversion from geocentric to geodetic comes from Kazimierz
Borkowski's "Accurate Algorithms to Transform Geocentric to Geodetic
Coordinates", at F<http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm>.
This is best viewed with Internet Explorer because of its use of Microsoft's
Symbol font.

=for comment help syntax-highlighting editor '

=cut

sub geodetic {
my $self = shift;


#	Detect and acquire the optional ellipsoid name argument. We do
#	this before the check, since the check expects the extra
#	argument to be a time.

my $elps = (@_ == 1 || @_ == 4) ? pop @_ : undef;


$self = $self->_check_coord (geodetic => \@_, $elps ? $elps : ());


#	The following is just a sleazy way to get a consistent
#	error message if the ellipsoid name is unknown.

$elps && $self->reference_ellipsoid ($elps);


#	If we're fetching the geodetic coordinates
    
unless (@_) {


#	Return cached coordinates if they exist and we did not
#	override the default ellipsoid.

    return @{$self->{geodetic}} if $self->{geodetic} && !$elps;
    $self->{debug} and do {
	local $Data::Dumper::Terse = 1;
	print "Debug geodetic - explicit ellipsoid ", Dumper ($elps);
	};


#	Get a reference to the ellipsoid data to use.

    $elps = $elps ? $known_ellipsoid{$elps} : $self;
    $self->{debug} and do {
	local $Data::Dumper::Terse = 1;
	print "Debug geodetic - ellipsoid ", Dumper ($elps);
	};


#	Calculate geodetic coordinates.

    my ($phiprime, $lamda, $rho) = $self->geocentric;
    my $r = $rho * cos ($phiprime);
    my $b = $elps->{semimajor} * (1- $elps->{flattening});
    my $a = $elps->{semimajor};
    my $z = $rho * sin ($phiprime);
    $b = - $b if $z < 0;	# Per Borkowski, for southern hemisphere.


#	The following algorithm is due to Kazimierz Borkowski's
#	paper "Accurate Algorithms to Transform Geocentric to Geodetic
#	Coordinates", from
#	http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm

    my $bz = $b * $z;
    my $asq_bsq = $a * $a - $b * $b;
    my $ar = $a * $r;
    my $E = ($bz - $asq_bsq) / $ar;		# Borkowski (10)
    my $F = ($bz + $asq_bsq) / $ar;		# Borkowski (11)
    my $Q = ($E * $E - $F * $F) * 2;		# Borkowski (17)
    my $P = ($E * $F + 1) * 4 / 3;		# Borkowski (16)
    my $D = $P * $P * $P + $Q * $Q;		# Borkowski (15)
    my $v = $D >= 0 ? do {
	my $d = sqrt $D;
	my $onethird = 1 / 3;
	my $vp = ($d - $Q) ** $onethird -	# Borkowski (14a)
	    ($d + $Q) ** $onethird;
	$vp * $vp >= abs ($P) ? $vp :
	    - ($vp * $vp * $vp + 2 * $Q) /	# Borkowski (20)
		(3 * $P);
      } : do {
	my $p = - $P;
	sqrt (cos (acos ($Q /			# Borkowski (14b)
	    sqrt ($p * $p * $p)) / 3) * $p) * 2;
	};
    my $G = (sqrt ($E * $E + $v)		# Borkowski (13)
	    + $E) / 2;
    my $t = sqrt ($G * $G + ($F - $v * $G)	# Borkowski (12)
	    / (2 * $G - $E)) - $G;
    my $phi = atan2 (($a * (1 - $t * $t)) /	# Borkowski (18)
	(2 * $b * $t), 1);			# equivalent to atan (arg1)
    my $h = ($r - $a * $t) * cos ($phi) +	# Borkowski (19)
	($z - $b) * sin ($phi);


#	End of Borkowski's algorthm.

#	Cache the results of the calculation if they were done using
#	the default ellipsoid.

    $self->{geodetic} = [$phi, $lamda, $h] unless $elps;


#	Return the results in any event.

    $self->get ('debug') and print <<eod;
Debug geodetic: geocentric to geodetic
    inputs:
        phiprime = $phiprime
        lamda = $lamda
        rho = $rho
    intermediates:
        z = $z
        r = $r
        E = $E
        F = $F
        P = $P
        Q = $Q
        D = $D
        v = $v
        G = $G
        t = $t
    outputs:
        phi = atan2 (a * (1 - t * t), 2 * b * t)
            = atan2 (@{[$a * (1 - $t * $t)]}, @{[2 * $b * $t]})
            = $phi (radians)
        h = (r - a * t) * cos (phi) + (z - b) * sin (phi)
          = @{[$r - $a * $t]} * cos (phi) + @{[$z - $b]} * sin (phi)
          = $h (kilometers)
eod
    return ($phi, $lamda, $h);
    }


#	If we're setting the geodetic coordinates.

if (@_ == 3) {


#	Set the ellipsoid for the object if one was specified.

    $self->set (ellipsoid => $elps) if $elps;


#	Calculate the geocentric data.

    my ($phi, $lamda, $h) = @_;
    my $bovera = 1 - $self->{flattening};


#	The following algorithm appears on page 82 of the second
#	edition of Jean Meeus' "Astronomical Algorithms."

    my $u = atan2 ($bovera * tan ($phi), 1);
    my $rhosinlatprime = $bovera * sin ($u) +
	$h / $self->{semimajor} * sin ($phi);
    my $rhocoslatprime = cos ($u) +
	$h / $self->{semimajor} * cos ($phi);
    my $phiprime = atan2 ($rhosinlatprime, $rhocoslatprime);
    my $rho = $self->{semimajor} * ($rhocoslatprime ?
	$rhocoslatprime / cos ($phiprime) :
	$rhosinlatprime / sin ($phiprime));


#	End of Meeus' algorithm.

#	Set the geocentric data as the coordinates.

    $self->geocentric ($phiprime, $lamda, $rho);
 
 
 #	Cache the geodetic coordinates.
 
    $self->{geodetic} = [$phi, $lamda, $h];
    $self->{specified} = 'geodetic';
    $self->{inertial} = 0;
    }



#	Else if the number of coordinates is bogus, croak.

  else {
    croak <<eod;
Error - Method geodetic() must be called with either zero arguments
        (to retrieve coordinates) or three arguments (to set
        coordinates). There is currently no six argument version.
eod
    }



#	Return the object, wherever it came from.

$self;

}


=item $value = $coord->get ($attrib);

This method returns the named attributes of the object. If called in
list context, you can give more than one attribute name, and it will
return all their values.

If called as a class method, it returns the current default values.

See L</Attributes> for a list of the attributes you can get.

=cut

sub get {
my $self = shift;
ref $self or $self = \%static;
my @rslt;
foreach my $name (@_) {
    exists $mutator{$name} or croak <<eod;
Error - Attribute '$name' does not exist.
eod
    push @rslt, $self->{$name};
    }
return wantarray ? @rslt : $rslt[0];
}


=item $coord = $coord->local_mean_time ($time);

This method sets the local mean time of the object. B<This is not
local standard time,> but the universal time plus the longitude
of the object expressed in seconds. Another definition is mean
solar time plus 12 hours (since the solar day begins at noon).
You will get an exception of some sort if the position of the
object has not been set.

=item $time = $coord->local_mean_time ()

This method returns the civil time of the object. It will raise
an exception if the time has not been set.

=cut

sub local_mean_time {
my $self = shift;

ref $self or croak <<eod;
Error - The local_mean_time() method may not be called as a class method.
eod

unless (@_) {
    $self->{universal} || croak <<eod;
Error - Object's time has not been set.
eod
    return ($self->{local_mean_time} ||=
	$self->universal + _local_mean_delta ($self));
     }

if (@_ == 1) {
    $self->{specified} or croak <<eod;
Error - Object's coordinates have not been set.
eod
    delete $self->{dynamical};
    $self->{local_mean_time} = $_[0];
    $self->{universal} = $_[0] - _local_mean_delta ($self);
    $self->{_need_purge} = 1;
    $self->can ('time_set') and $self->time_set ();
    }
  else {
    croak <<eod;
Error - The local_mean_time() method must be called with either zero
        arguments (to retrieve the time) or one argument (to set
        the time).
eod
    }

$self;
}



=item $value = $coord->mean_angular_velocity();

This method returns the mean angular velocity of the body in radians
per second. If the $coord object has a period() method, this method
just returns two pi divided by the period. Otherwise it returns the
contents of the angularvelocity attribute.

=cut

sub mean_angular_velocity {
my $self = shift;
return $self->can ('period') ?
	TWOPI / $self->period :
	$self->{angularvelocity};
}


=item ($time, $rise) = $coord->next_elevation ($body, $elev, $upper)

This method calculates the next time the given body passes above or
below the given elevation (in radians). The $elev argument may be
omitted (or passed as undef), and will default to 0. If the $upper
argument is true, the calculation will be based on the upper limb
of the body (as determined from its angulardiameter attribute); if
false, the calculation will be based on the center of the body. The
$upper argument defaults to true if the $elev argument is zero or
positive, and false if the $elev argument is negative.

The algorithm is successive approximation, and assumes that the
body will be at its highest at meridian passage. It also assumes
that if the body hasn't passed the given elevation in 183 days it
never will. In this case it returns undef in scalar context, or
an empty list in list context.

=for comment help syntax-highlighting editor '

=cut

use constant NEVER_PASS_ELEV => 183 * SECSPERDAY;

sub next_elevation {
my $self = shift;
ref $self or croak <<eod;
Error - The next_elevation() method may not be called as a class method.
eod

my $body = shift;
isa ($body, __PACKAGE__) or croak <<eod;
Error - The first argument to next_elevation() must be a subclass of
        @{[__PACKAGE__]}.
eod

my $angle = shift || 0;
my $upper = shift;
defined $upper or $upper = $angle >= 0;

my $begin = $self->universal;
my $original = $begin;
my $rise = ($self->azel ($body->universal ($begin), $upper))[1] < $angle || 0;

my ($end, $above) = $self->next_meridian ($body, $rise);

my $give_up = $body->NEVER_PASS_ELEV ();

while ((($self->azel($body))[1] < 0 || 0) == $rise) {
    return if $end - $original > $give_up;
    $begin = $end;
    ($end, $above) = $self->next_meridian ($body, $rise);
    }

while ($end - $begin > 1) {
    my $mid = floor (($begin + $end) / 2);
    my ($azm, $elev, $rng) = $self->universal ($mid)->
	azel ($body->universal ($mid), $upper);
    ($begin, $end) =
	($elev < $angle || 0) == $rise ? ($mid, $end) : ($begin, $mid);
    }

$self->universal ($end);	# Ensure consistent time.
$body->universal ($end);
wantarray ? ($end, $rise) : $end;
}

=item ($time, $above) = $coord->next_meridian ($body, $want)

This method calculates the next meridian passage of the given body
over (or under) the location specified by the $coord object. The
$body object must be a subclass of Astro::Coord::ECI.

The optional $want argument should be specified as true (i.e. 1) if you
want the next passage above the observer, or as false (i.e. 0) if you
want the next passage below the observer. If this argument is omitted
or undefined, you get whichever passage is next.

The start time of the search is the current time setting of the
$coord object.

The returns are the time of the meridian passage, and an indicator
which is true if the passage is above the observer (i.e. local noon
if the $body represents the sun), or false if below (i.e. local
midnight if the $body represents the sun). If called in scalar
context, you get the time only.

The current time of both $coord and $body object are left at the
returned time.

The algorithm is by successive approximation. It will croak if the
period of the $body is close to synchronous, and will probably not
work well for bodies in highly eccentric orbits. The calculation is
to the nearest second, and the time returned is the first even
second after the body crosses the meridian.

=cut

sub next_meridian {
my $self = shift;
ref $self or croak <<eod;
Error - The next_meridian() method may not be called as a class method.
eod

my $body = shift;
isa ($body, __PACKAGE__) or croak <<eod;
Error - The argument to next_meridian() must be a subclass of
        @{[__PACKAGE__]}.
eod

my $want = shift;
defined $want and $want = $want ? 1 : 0;

my $denom = $body->mean_angular_velocity -
    $self->mean_angular_velocity;
my $retro = $denom >= 0 ? 0 : 1;
($denom = abs ($denom)) < 1e-11 and croak <<eod;
Error - The next_meridian() method will not work for geosynchronous
        bodies.
eod

my $apparent = TWOPI / $denom;
my $begin = $self->universal;
my $delta = floor ($apparent / 16);
my $end = $begin + $delta;

my ($above, $opposite) =
    mod2pi (($body->universal($begin)->geocentric)[1]
	- ($self->universal($begin)->geocentric)[1]) >= PI ?
    (1 - $retro, PI) : ($retro, 0);

($begin, $end) = ($end, $end + $delta)
    while mod2pi (($body->universal($end)->geocentric)[1] -
	($self->universal($end)->geocentric)[1] + $opposite) < PI;

if (defined $want && $want != $above) {
    $above = $want;
    $opposite = $opposite ? 0 : PI;
    ($begin, $end) = ($end, $end + $delta)
	while mod2pi (($body->universal($end)->geocentric)[1] -
	    ($self->universal($end)->geocentric)[1] + $opposite) < PI;
    }

while ($end - $begin > 1) {
    my $mid = floor (($begin + $end) / 2);
    my $long = ($body->universal($mid)->geocentric)[1];
    my $merid = ($self->universal($mid)->geocentric)[1];
    ($begin, $end) =
	mod2pi ($long - $merid + $opposite) < PI ?
	($mid, $end) : ($begin, $mid);
    }

$body->universal ($end);
$self->universal ($end);
wantarray ? ($end, $above) : $end;
}


=item $coord = $coord->precess ($time);

This method precesses the equatorial coordinates of the object to the
given universal time. The starting time is assumed to be the time
setting of the object when the call is made. The equatorial coordinates
of the object are set to the results of the calculation, and the
universal time of the object is set to the value of the $time argument.
The object itself is returned.

B<NOTE> that side effects of setting the time (i.e. in subclasses which
define the time_set() method) B<will> take place as a result of calling
this method.

The algorithm comes from Jean Meeus, "Astronomical Algorithms", 2nd
Edition, Chapter 21, pages 134ff (a.k.a. "the rigorous method").

=cut

sub precess {
my $self = shift;
my $time = shift;

my $start = $self->dynamical;
my $end = $time + _dynamical_delta ($time);

my ($alpha0, $delta0, $rho0) = $self->equatorial ();

my $T = jcent2000 ($start);
my $t = ($end - $start) / (36525 * SECSPERDAY);

#	The following four assignments correspond to Meeus' (21.2).
my $zterm = (- 0.000139 * $T + 1.39656) * $T + 2306.2181;
my $zeta = deg2rad ((((0.017998 * $t + (- 0.000344 * $T + 0.30188)) *
	$t + $zterm) * $t) / 3600);
my $z = deg2rad ((((0.018203 * $t + (0.000066 * $T + 1.09468)) * $t +
	$zterm) * $t) / 3600);
my $theta = deg2rad (((( - 0.041833 * $t - (0.000217 * $T + 0.42665))
	* $t + (- 0.000217 * $T - 0.85330) * $T + 2004.3109) * $t) /
	3600);

#	The following assignments correspond to Meeus' (21.4).
my $sindelta0 = sin ($delta0);
my $cosdelta0 = cos ($delta0);
my $sintheta = sin ($theta);
my $costheta = cos ($theta);
my $cosdelta0cosalpha0 = cos ($alpha0 + $zeta) * $cosdelta0;
my $A = $cosdelta0 * sin ($alpha0 + $zeta);
my $B = $costheta * $cosdelta0cosalpha0 - $sintheta * $sindelta0;
my $C = $sintheta * $cosdelta0cosalpha0 + $costheta * $sindelta0;

my $alpha = atan2 ($A , $B) + $z;
my $delta = asin ($C);

$self->equatorial ($alpha, $delta, $rho0, $time);
$self->{dynamical} = $end;
$self;
}


=item Astro::Coord::ECI->reference_ellipsoid($semi, $flat, $name);

This class method can be used to define or redefine reference
ellipsoids.

Nothing bad will happen if you call this as an object method, but
it still just creates a reference ellipsoid definition -- the object
is unaffected.

It is not an error to redefine an existing ellipsoid.

=item ($semi, $flat, $name) = Astro::Coord::ECI->reference_ellipsoid($name)

This class method returns the definition of the named reference
ellipsoid. It croaks if there is no such ellipsoid.

You can also call this as an object method, but the functionality is
the same.

The following reference ellipsoids are known to the class initially:

 CLARKE-1866 - 1866.
   semimajor => 6378.2064 km, flattening => 1/294.98.
   Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html

 GRS67 - Geodetic Reference System 1967.
   semimajor => 6378.160 km, flattening => 1/298.247.
   Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html

 GRS80 - Geodetic Reference System 1980.
   semimajor => 6378.137 km, flattening => 1/298.25722210088
     (flattening per U.S. Department of Commerce 1989).
   Source: http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm

 IAU68 - International Astronomical Union, 1968.
   semimajor => 6378.160 km, flattening => 1/298.25.
   Source: http://maic.jmu.edu/sic/standards/ellipsoid.htm

 IAU76 - International Astronomical Union, 1976.
   semimajor => 6378.14 km, flattening => 1 / 298.257.
   Source: Jean Meeus, "Astronomical Algorithms", 2nd Edition

 NAD83 - North American Datum, 1983.
    semimajor => 6378.137 km, flattening => 1/298.257.
    Source:  http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
    (NAD83 uses the GRS80 ellipsoid)

 sphere - Just in case you were wondering how much difference it
   makes (a max of 11 minutes 32.73 seconds of arc, per Jean
   Meeus).
   semimajor => 6378.137 km (from GRS80), flattening => 0.

 WGS72 - World Geodetic System 1972.
   semimajor => 6378.135 km, flattening=> 1/298.26.
   Source: http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm

 WGS84 - World Geodetic System 1984.
   semimajor => 6378.137 km, flattening => 1/1/298.257223563.
   Source: http://www.colorado.edu/geography/gcraft/notes/datum/elist.html

Reference ellipsoid names are case-sensitive.

The default model is WGS84.

=cut

# help for editor that does not understand POD "

# Wish I had:
# Maling, D.H., 1989, Measurements from Maps: Principles and methods of cartometry, Pergamon Press, Oxford, England.
# Maling, D.H., 1993, Coordinate Systems and Map Projections, Pergamon Press, Oxford, England. 

# http://www.gsi.go.jp/PCGIAP/95wg/wg3/geodinf.htm has a partial list of who uses
#	what in the Asia/Pacific.

%known_ellipsoid = (	# Reference Ellipsoids
    'CLARKE-1866' => {	# http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
	semimajor => 6378.2064,
	flattening => 1/294.9786982,
	},
    GRS67 => {		# http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
	semimajor => 6378.160,
	flattening => 1/298.247167427,
	},
    GRS80 => {		# http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
	semimajor => 6378.137,		# km
	flattening => 1/298.25722210088, # U.S. Dept of Commerce 1989 (else 1/298.26)
	},
    IAU68 => {		# http://maic.jmu.edu/sic/standards/ellipsoid.htm
	semimajor => 6378.160,
	flattening => 1/298.25,
	},
    IAU76 => {		# Meeus, p. 82.
	semimajor => 6378.14,
	flattening => 1/298.257,
	},
    NAD83 => {		# http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
	semimajor => 6378.137,		# km
	flattening => 1/298.257,
	},
    sphere => {		# Defined by me for grins, with semimajor from GRS80.
	semimajor => 6378.137,		# km, from GRS80
	flattening => 0,		# It's a sphere!
	},
    WGS72 => {		# http://biology.usgs.gov/fgdc.metadata/version2/spref/horiz/geodet/faq.htm
	semimajor => 6378.135,		# km
	flattening => 1/298.26,
	},
    WGS84 => {		# http://www.colorado.edu/geography/gcraft/notes/datum/elist.html
	semimajor => 6378.137,
	flattening => 1/298.257223563,
	},
    );
foreach my $name (keys %known_ellipsoid) {
    $known_ellipsoid{$name}{name} = $name;
    }

sub reference_ellipsoid {
my $self = shift;
my $name = pop @_ or croak <<eod;
Error - You must specify the name of a reference ellipsoid.
eod
if (@_ == 0) {
    $known_ellipsoid{$name} or croak <<eod;
Error - Reference ellipsoid $name is unknown to this software. Known
        ellipsoids are:
        @{[join ', ', sort keys %known_ellipsoid]}.
eod
    }
  elsif (@_ == 2) {
    $known_ellipsoid{$name} = {
	semimajor => $_[0],
	flattening => $_[1],
	name => $name,
	};
    }
  else {
    croak <<eod;
Error - You must call the reference_ellipsoid class method with either one
        argument (to fetch the definition of a known ellipsoid) or three
        arguments (to define a new ellipsoid or redefine an old one).
eod
    }
@{$known_ellipsoid{$name}}{qw{semimajor flattening name}}
}


=item $coord->set (name => value ...);

This method sets various attributes of the object. If called as a class
method, it changes the defaults.

For reasons that seemed good at the time, this method returns the
object it was called on (i.e. $coord in the above example).

See L</Attributes> for a list of the attributes you can set.

=cut

use constant SET_ACTION_NONE => 0;	# Do nothing.
use constant SET_ACTION_RESET => 1;	# Reset the coordinates based on the initial setting.

sub set {
my $self = shift;
ref $self or $self = \%static;
@_ %2 and croak <<eod;
Error - The set() method requires an even number of arguments.
eod
my $action;
while (@_) {
    my $name = shift;
    exists $mutator{$name} or croak <<eod;
Error - Attribute '$name' does not exist.
eod
    ref $mutator{$name} eq 'CODE' or croak <<eod;
Error - Attribute '$name' is read-only.
eod
    $action |= $mutator{$name}->($self, $name, shift);
    }

$self->{_need_purge} = 1
    if ref $self && $self->{specified} && $action & SET_ACTION_RESET;

$self;
}

#	The following are the mutators for the attributes. All are
#	passed three arguments: a reference to the hash to be set,
#	the hash key to be set, and the value. They must return the
#	bitwise 'or' of the desired action masks, defined above.

%mutator = (
    angularvelocity => \&_set_value,
    debug => \&_set_value,
    diameter => \&_set_value,
    ellipsoid => \&_set_reference_ellipsoid,
    flattening => \&_set_custom_ellipsoid,
    id => \&_set_value,
    inertial => undef,
    name => \&_set_value,
    refraction => \&_set_value,
    semimajor => \&_set_custom_ellipsoid,
    twilight => \&_set_value,
    );

#	If you set semimajor or flattening, the ellipsoid name becomes
#	undefined. Also clear any cached geodetic coordinates.

sub _set_custom_ellipsoid {
$_[0]->{ellipsoid} = undef;
$_[0]->{$_[1]} = $_[2];
SET_ACTION_RESET;
}

#	If this is a reference ellipsoid name, check it, and if it's
#	OK set semimajor and flattening also. Also clear any cached
#	geodetic coordinates.

sub _set_reference_ellipsoid {
defined $_[2] or croak <<eod;
Error - Can not set reference ellipsoid to undefined.
eod
exists $known_ellipsoid{$_[2]} or croak <<eod;
Error - Reference ellipsoid '$_[2]' is unknown.
eod

$_[0]->{semimajor} = $known_ellipsoid{$_[2]}{semimajor};
$_[0]->{flattening} = $known_ellipsoid{$_[2]}{flattening};
$_[0]->{$_[1]} = $_[2];
SET_ACTION_RESET;
}

#	If this is a vanilla setting, just do it.

sub _set_value {
$_[0]->{$_[1]} = $_[2];
SET_ACTION_NONE;
}


=item $coord->universal ($time)

This method sets the time represented by the object, in universal time
(a.k.a. CUT, a.k.a. Zulu, a.k.a. Greenwich).

This method can also be called as a class method, in which case it
instantiates the desired object.

=item $time = $coord->universal ();

This method returns the universal time previously set.

=cut

sub universal {
my $self = shift;
unless (@_) {
    ref $self or croak <<eod;
Error - The universal() method may not be called as a class method
        unless you specify arguments.
eod
    return $self->{universal} || croak <<eod;
Error - Object's time has not been set.
eod
# help syntax-highlighting editor '
    }

if (@_ == 1) {
    $self = $self->new () unless ref $self;
    delete $self->{local_mean_time};
    delete $self->{dynamical};
    $self->{universal} = shift;
    $self->{_need_purge} = 1 if $self->{specified};
    $self->can ('time_set') && !$self->{_no_set} and do {
	$self->{_no_set} = 1;
	$self->time_set ();
	delete $self->{_no_set};
	};
    }
  else {
    croak <<eod;
Error - The universal() method must be called with either zero
        arguments (to retrieve the time) or one argument (to set the
        time).
eod
    }
$self;
}


#######################################################################
#
#	Internal
#


#	$coord->_check_coord (method => \@_)

#	This is designed to be called "up front" for any of the methods
#	that retrieve or set coordinates, to be sure the object is in
#	a consistent state.
#	* If $self is not a reference, it creates a new object if there
#	  are arguments, or croaks if there are none.
#	* If the number arguments passed (after removing self and the
#	  method name) is one more than a multiple of three, the last
#	  argument is removed, and used to set the universal time of
#	  the object.
#	* If there are arguments, all coordinate caches are cleared;
#	  otherwise the coordinates are reset if needed.
#	The object itself is returned.

sub _check_coord {
my $self = shift;
my $method = shift;
my $args = shift;

unless (ref $self) {
    @$args or croak <<eod;
Error - The $method() method may not be called as a class method
        unless you specify arguments.
eod
    $self = $self->new ();
    }

$self->{debug} and do {
    local $Data::Dumper::Terse = 1;
    print "Debug $method (", Dumper (@$args, @_), ")\n";
    };

$self->universal (pop @$args) if @$args % 3 == 1;

if ($self->{specified}) {
    if (@$args) {
#	Cached coordinate deletion moved to ecef(), so it's only done once.
	}
      elsif ($self->{_need_purge}) {
	my $spec = $self->{specified};
	my $data = $self->{$spec};
	foreach my $key (@kilatr) {delete $self->{$key}}
	$self->$spec (@$data);
	delete $self->{_need_purge};
	}
    }

$self;
}


#	$value = _local_mean_delta ($coord)

#	Calculate the delta from universal to civil time for the object.
#	An exception is raised if the coordinates of the object have not
#	been set.

sub _local_mean_delta {
return ($_[0]->geodetic ())[1] * SECSPERDAY / TWOPI;
}


#	$string = _rad2dms ($angle)

#	Convert radians to degrees, minutes, and seconds of arc.
#	Used for debugging.

sub _rad2dms {
my $angle = rad2deg (shift);
my $deg = floor ($angle);
$angle = ($angle - $deg) * 60;
my $min = floor ($angle);
$angle = ($angle - $min) * 60;
"$deg degrees $min minutes $angle seconds of arc";
}


#######################################################################
#
#	Package initialization
#

__PACKAGE__->set (ellipsoid => 'WGS84');

%savatr = map {$_ => 1} (keys %static, qw{dynamical id name universal});
#	Note that local_mean_time does not get preserved, because it
#	changes if the coordinates change.

1;

__END__

=back

=head2 Attributes

This class has the following attributes:

=over

=item angularvelocity (radians per second)

This attribute represents the angular velocity of the Earth' surface in
radians per second. The initial value is 7.292114992e-5, which
according to Jean Meeus is the value for 1996.5. He cites the
International Earth Rotation Service's Annual Report for 1996
(Published at the Observatoire de Paris, 1997).

Subclasses may place appropriate values here, or provide a period()
method.

=item debug (numeric)

This attribute turns on debugging output. The only supported value of
this attribute is 0. That is to say, the author makes no guarantees of
what will happen if you set it to some other value, nor does he
guarantee that this behaviour will not change from release to release.

The default is 0.

=item diameter (numeric, kilometers)

This attribute exists to support classes/instances which represent
astronomical bodies. It represents the diameter of the body, and is
used by the azel() method when computing the upper limb of the body.
It has nothing to do with the semimajor attribute, which always refers
to the Earth, and is used to calculate the latitude and longitude of
the body.

The default is 0.

=item ellipsoid (string)

This attribute represents the name of the reference ellipsoid to use.
It must be set to one of the known ellipsoids. If you set this,
flattening and semimajor will be set also. See the documentation to
the known_ellipsoid() method for the initially-valid names, and how
to add more.

The default is 'WGS84'.

=item flattening (numeric)

This attribute represents the flattening factor of the reference
ellipsoid. If you set the ellipsoid attribute, this attribute will be
set to the flattening factor for the named ellipsoid. If you set this
attribute, the ellipsoid attribute will become undefined.

The default is appropriate to the default ellipsoid.

=item id (string)

This is an informational attribute, and its setting (or lack thereof)
does not affect the functioning of the class. Certain subclasses will
set this when they are instantiated. See the subclass documentation
for details.

=item inertial (boolean, read-only)

This attribute returns true (in the Perl sense) if the object was
most-recently set to inertial coordinates (i.e. eci, ecliptic, or
equatorial) and false otherwise. If coordinates have not been set,
it is undefined (and therefore false).

=item name (string)

This is an informational attribute, and its setting (or lack thereof)
does not affect the functioning of the class. Certain subclasses will
set this when they are instantiated. See the subclass documentation
for details.

=item refraction (boolean)

Setting this attribute to a true value includes refraction in the
calculation of the azel() method. If set to a false value, atmospheric
refraction is ignored.

The default is true (well, 1 actually).

=item semimajor (numeric, kilometers)

This attribute represents the semimajor axis of the reference
ellipsoid. If you set the ellipsoid attribute, this attribute will be
set to the semimajor axis for the named ellipsoid. If you set this
attribute, the ellipsoid attribute will become undefined.

For subclasses representing bodies other than the Earth, this attribute
will be set appropriately.

The default is appropriate to the default ellipsoid.

=item twilight (numeric, radians)

This attribute represents the elevation of the center of the Sun's disk
at the beginning and end of twilight. It should probably be an
attribute of the Sun subclass, since it is only used by the almanac ()
method of that subclass, but it's here so you can blindly set it when
computing almanac data.

Some of the usual values are:

 civil twilight: -6 degrees
 nautical twilight: -12 degrees
 astronomical twilight: -18 degrees

The default is -6 degrees (or, actually, the equivalent in radians).

=back

=head1 TERMINOLOGY AND CONVENTIONS

Partly because this module straddles the divide between geography and
astronomy, the establishment of terminology and conventions was a
thorny and in the end somewhat arbitrary process. Because of this,
documentation of salient terms and conventions seemed to be in order.

=head2 Altitude

This term refers to the distance of a location above mean sea level.

Altitude input to and output from this module is in kilometers.

Maps use "elevation" for this quantity, and measure it in meters. But
we're using L</Elevation> for something different, and I needed
consistent units.

=head2 Azimuth

This term refers to distance around the horizon measured clockwise from
North.

Azimuth output from this module is in radians.

Astronomical sources tend to measure from the South, but I chose the
geodetic standard, which seems to be usual in satellite tracking
software.

=head2 Declination

Declination is the angle a point makes with the plane of the equator
projected onto the sky. North declination is positive, south
declination is negative.

Declination input to and output from this module is in radians.

=head2 Earth-Centered, Earth-fixed (ECEF) coordinates

This is a Cartesian coodinate system whose origin is the center of the
reference ellipsoid. The X axis passes through 0 degrees L</Latitude>
and 0 degrees L</Longitude>. The Y axis passes through 90 degrees east
L</Latitude> and 0 degrees L</Longitude>, and the Z axis passes through
90 degrees north L</Latitude> (a.k.a the North Pole).

All three axes are input to and output from this module in kilometers.

Also known as L</XYZ coordinates>, e.g. at
L<http://www.ngs.noaa.gov/cgi-bin/xyz_getxyz.prl>

=head2 Earth-Centered Inertial (ECI) coordinates

This is the Cartesian coordinate system in which NORAD's models predict
the position of orbiting bodies. The X axis passes through 0 hours
L</Right Ascension> and 0 degrees L</Declination>. The Y axis passes
through 6 hours L</Right Ascension> and 0 degrees L</Declination>. The
Z axis passes through +90 degrees L</Declination> (a.k.a. the North
Pole).

All three axes are input to and output from this module in kilometers.

=head2 Ecliptic

The Ecliptic is the plane of the Earth's orbit, projected onto the sky.
Ecliptic coordinates are a spherical coordinate system referred to
the ecliptic and expressed in terms of L</Ecliptic latitude> and
L</Ecliptic longitude>.

=head3 Ecliptic latitude

Ecliptic longitude is the angular distance of a point above the plane
of the Earth's orbit.

Ecliptic latitude is input to and output from this module in radians.

=head3 Ecliptic longitude

Ecliptic longitude is the angular distance of a point east of the point
where the plane of the Earth's orbit intersects the plane of the
equator. This point is also known as the vernal equinox and the
first point of Ares.

Ecliptic longitude is input to and output from this module in radians.

=head2 Elevation

This term refers to an angular distance above the horizon.

Elevation output from this module is in radians.

This is the prevailing meaning of the term in satellite tracking.
Astronomers use "altitude" for this quantity, and call the
corresponding coordinate system "altazimuth." But we're using
L</Altitude> for something different.

=head2 Equatorial

Equatorial coordinates are a spherical coordinate system referred to
the plane of the equator projected onto the sky. Equatorial coordinates
are specified in L</Right Ascension> and L</Declination>.

=head2 Geocentric

When referring to a coordinate system, this term means that the
coordinate system assumes the Earth is spherical.

=head3 Geocentric latitude

Geocentric latitude is the angle that the ray from the center of the
Earth to the location makes with the plane of the equator. North
latitude is positive, south latitude is negative.

Geocentric latitude is input to and output from this module in radians.

=head2 Geodetic

When referring to a coordinate system, this term means that the
coordinate system assumes the Earth is an ellipsoid of revolution
(or an oblate spheroid if you prefer). A number of standard
L</Reference Ellipsoids> have been adopted over the years.

=head3 Geodetic latitude

Geodetic latitude is the latitude found on maps. North latitude is
positive, south latitude is negative.

Geodetic latitude is input to and output from this module in radians.

Technically speaking, Geodetic latitude is the complement of the angle
the plane of the horizon makes with the plane of the equator. In
this software, the plane of the horizon is determined from a
L</Reference Ellipsoid>.

=head2 Latitude

See either L</Ecliptic latitude>, L</Geocentric latitude> or
L</Geodetic latitude>. When used without qualification, L</Geodetic
latitude> is meant.

=head2 Longitude

When unqualified, this term refers to the angular distance East or West
of the standard meridian. East longitude is positive, West longitude is
negative.

Longitude is input to or output from this module in radians.

For L</Ecliptic longitude>, see that entry.

Jean Meeus reports in "Astronomical Algorithms" that the International
Astronomical Union has waffled on the sign convention. I have taken
the geographic convention.

=head2 Obliquity (of the Ecliptic)

This term refers to the angle the plane of the equator makes with the
plane of the Earth's orbit.

Obliquity is output from this module in radians.

=head2 Reference Ellipsoid

This term refers to a specific ellipsoid adopted as a model of the
shape of the Earth. It is defined in terms of the equatorial radius
in kilometers, and a dimensionless flattening factor which is used
to derive the polar radius, and defined such that the flattening
factor of a sphere is zero.

See the documentation on the reference_ellipsoid() method for a list
of reference ellipsoids known to this class.

=head2 Right Ascension

This term refers to the angular distance of a point east of the
intersection of the plane of the Earth's orbit with the plane of
the equator.

Right Ascension is input to and output from this module in radians.

In astronomical literature it is usual to report right ascension
in hours, minutes, and seconds, with 60 seconds in a minute, 60
minutes in an hour, and 24 hours in a circle.

=head2 XYZ coordinates

See L</Earth-Centered, Earth-fixed (ECEF) coordinates>.

=head1 ACKNOWLEDGEMENTS

The author wishes to acknowledge the following individuals and
organizations.

Kazimierz Borkowski, whose "Accurate Algorithms to Transform
Geocentric to Geodetic Coordinates", at
L<http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm>,
was used for transforming geocentric to geodetic coordinates.

Dominik Brodowski (L<http://www.brodo.de/>), whose SGP C-lib
(available at L<http://www.brodo.de/space/sgp/>) provided a
reference implementation that I could easily run, and pick
apart to help get B<Astro::Coord::ECI::TLE> working. Dominik based
his work on Dr. Kelso's Pascal implementation.

Felix R. Hoots and Ronald L. Roehric, the authors of "SPACETRACK
REPORT NO. 3 - Models for Propagation of NORAD Element Sets,"
which provided the basis for the Astro::Coord::ECI::TLE module.

T. S. Kelso, who compiled this report and made it available at
L<http://celestrak.com/>, whose "Computers and Satellites" columns
in "Satellite Times" magazine were invaluable to an understanding
and implementation of satellite tracking software, whose support,
encouragement, patience, and willingness to provide answers on arcane
topics were a Godsend, who kindly granted permission to use his
azimuth/elevation algorithm, and whose Pascal implementation of the
NORAD tracking algorithms indirectly provided a reference
implementation for me to use during development.

Jean Meeus, whose book "Astronomical Algorithms" (second edition)
formed the basis for this module and the B<Astro::Coord::ECI::Sun>,
B<Astro::Coord::ECI::Moon>, and B<Astro::Coord::ECI::Star> modules,
and without whom it is impossible to get far in computational
astronomy. Any algorithm not explicitly credited above is probably
due to him.

Dr. Meeus' publisher, Willmann-Bell Inc (L<http://www.willbell.com/>),
which kindly and patiently answered my intellectual-property questions.

=head1 BUGS

Functionality involving velocities is B<untested>, and is quite likely
to be wrong.

Bugs can be reported to the author by mail, or through
L<http://rt.cpan.org/>.

=head1 SEE ALSO

The B<Astro> package by Chris Phillips. This contains three function-based
modules: B<Astro::Coord>, which provides various astronomical coordinate
conversions, plus the calculation of various ephemeris variables;
B<Astro::Time> contains time and unit conversions, and B<Astro::Misc>
contains various calculations unrelated to position and time.

The B<Astro-Coords> package by Tim Jenness. This contains various modules
to do astronomical calculations, and includes coordinate conversion and
calculation of planetary orbits based on orbital elements. Requires
SLALIB from L<http://www.starlink.rl.ac.uk/Software/software_store.htm>.

=head1 AUTHOR

Thomas R. Wyant, III (F<wyant at cpan dot org>)

=head1 COPYRIGHT

Copyright 2005, 2006 by Thomas R. Wyant, III
(F<wyant at cpan dot org>). All rights reserved.

This module is free software; you can use it, redistribute it and/or
modify it under the same terms as Perl itself. Please see
L<http://perldoc.perl.org/index-licence.html> for the current licenses.

This software is provided without any warranty of any kind, express or
implied. The author will not be liable for any damages of any sort
relating in any way to this software.

=cut
