/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2014 European Southern Observatory
 *           (C) 2000-2002 European Southern Observatory
 *           (C) 2002-2006 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <math.h>
#include <string.h>
#include <ctype.h>

#include "muse_astro.h"

#include "muse_pfits.h"

/*----------------------------------------------------------------------------*
 *                              Defines                                       *
 *----------------------------------------------------------------------------*/

#define MUSE_ASTRO_AIRMASS_APPROX 2 /* airmass approximation to use: *
                                     * 0   Young & Irvine (1967)     *
                                     * 1   Young (1994)              *
                                     * 2   Hardie (1962)             */

/* Constants used by the radial-velocity correction (for precession etc.) */
static const double kT0 = 2415020.; /* t0: Julian date of Dec 31st, 1899, 12 UT */
static const double kEpoch1900 = 1900.; /* epoch B1900 */
static const double kJulianCentury = 36525.; /* Julian century [days] */
static const double kTropYear = 365.242198781; /* tropical year of B1900 [days] */
static const double kBesselOffset = 0.31352; /* offset for Besselian epoch [days] */
static const double kAU = 149597870.7; /* 1 au = 149 597 870.7 km (IAU 2012) */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_astro        Astronomical tools
 *
 * Functions in this group are related to handling of astronomical properties.
 * They are partly based on pilastroutils/giastroutils, and girvcorrection taken
 * from the VIMOS and GIRAFFE pipelines (written by Carlo Izzo and Ralf Palsa,
 * (C) 2000-2004 and 2002-2006 European Southern Observatory).
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the zenith distance for an observation.
  @param   aHourAngle   Hour angle in radians.
  @param   aDelta       Declination in radians.
  @param   aLatitude    Latitude of the observatory in radians.
  @return  cos(z) on success or 0. on error.

  The function computes the cosine of the zenith distance for an observation
  taken at an angle aHourAngle from the meridian, which can take values in the
  range extending from @f$-\pi@f$ to @f$\pi@f$, and the declination aDelta with
  possible values between @f$-0.5\pi@f$ and @f$0.5\pi@f$. The latitude
  aLatitude of the observing site may take values in the range @f$0@f$ to
  @f$2\pi@f$.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_get_zenith_distance(double aHourAngle, double aDelta,
                               double aLatitude)
{
  double p0 = sin(aLatitude) * sin(aDelta),
         p1 = cos(aLatitude) * cos(aDelta),
         z = p0 + cos(aHourAngle) * p1;
  return fabs(z) < FLT_EPSILON ? 0. : z;
}

#if MUSE_ASTRO_AIRMASS_APPROX == 0
/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the zenith distance for an observation.
  @param   aSecZ   Secans of the zenith distance.
  @return  The function returns the airmass.

  The function uses the approximation given by Young and Irvine (Young A. T.,
  Irvine W. M., 1967, Astron. J. 72, 945) to compute the airmass for a given
  sec(z) aSecZ. This approximation takes into account atmosphere refraction and
  curvature, but is in principle only valid at sea level.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_get_airmass_youngirvine(double aSecZ)
{
  return aSecZ * (1. - 0.0012 * (pow(aSecZ, 2) - 1.));
}
#endif /* MUSE_ASTRO_AIRMASS_APPROX == 0 */

#if MUSE_ASTRO_AIRMASS_APPROX == 1
/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the zenith distance for an observation.
  @param   aCosZt   The cosine of the true zenith distance.
  @return  The function returns the airmass.

  The function uses the approximation given by Young (Young A. T., 1994 ApOpt,
  33, 1108) to compute the relative optical air mass as a function of true,
  rather than refracted, zenith angle which is given in terms of its cosine
  aCosZt.
  It is supposedy more accurate than Young & Irvine (1967) but restrictions are
  not known.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_get_airmass_young(double aCosZt)
{
  return (1.002432 * aCosZt*aCosZt + 0.148386 * aCosZt + 0.0096467)
         / (aCosZt*aCosZt*aCosZt + 0.149864 * aCosZt*aCosZt + 0.0102963 * aCosZt
            + 0.000303978);
}
#endif /* MUSE_ASTRO_AIRMASS_APPROX == 1 */

#if MUSE_ASTRO_AIRMASS_APPROX == 2
/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the zenith distance for an observation.
  @param   aSecZ   The secans of the zenith distance.
  @return  The function returns the airmass.

  The function uses the approximation given by Hardie (Hardie 1962, In:
  "Astronomical Techniques", ed. Hiltner, p. 184) to compute the airmass as a
  function of zenith angle which is given in terms of its secons aSecZ.
  It is supposedy more accurate than Young & Irvine (1967) and usable for zenith
  angles below 85 degrees.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_get_airmass_hardie(double aSecZ)
{
  double secm1 = aSecZ - 1;
  return aSecZ - 0.0018167 * secm1 - 0.002875 * secm1*secm1
         - 0.0008083 * secm1*secm1*secm1;
}
#endif /* MUSE_ASTRO_AIRMASS_APPROX == 2 */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute the effective airmass of an observation.
  @param   aRA         right ascension in degrees
  @param   aDEC        declination in degrees
  @param   aLST        local sidereal time in seconds elapsed since siderial
                       midnight
  @param   aExptime    integration time in seconds
  @param   aLatitude   latitude of the observatory site in degrees
  @return  The computed average airmass or -1 on error.

  The function calculates the average airmass for the line of sight given by the
  right ascension aRA and the declination aDEC. The latitude aLatitude in
  degrees of the observatory site and the local siderial time aLST at
  observation start has to be given, as well as the duration of the observation,
  i.e. the exposure time aExptime. If aExptime is zero then only one value of
  airmass is computed, instead of weighting beginning, middle, and end of
  exposure according to Stetson (Stetson P., 1987, PASP 99, 191).

  This function uses the approximation given by Hardie (Hardie 1962, In:
  "Astronomical Techniques", ed. Hiltner, p. 184) to compute the airmass as a
  function of zenith distance.

  @note This function can be recompiled to use the formula of Young and Irvine
        (Young A. T., Irvine W. M., 1967, Astron. J. 72, 945) instead of the
        less widely used Hardie (1962). Then the range of trustworthy airmass
        outputs is only between 1 and 4.
  @note This function can be recompiled to use the formula of Young
        (Young A. T., 1994 ApOpt, 33, 1108).
  @note http://en.wikipedia.org/w/index.php?title=Airmass&oldid=358226579#Interpolative_formulas
        has an interesting collection of other approximations.

  @error{set CPL_ERROR_ILLEGAL_INPUT\, return -1,
         aRA\, aDEC\, aLST\, or aLatitude values are not in a sensible range}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return -1, aExptime is negative}
  @error{output warning\, set CPL_ERROR_ILLEGAL_OUTPUT\, return -1,
         the computed zenith distance is invalid}
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_compute_airmass(double aRA, double aDEC, double aLST,
                           double aExptime, double aLatitude)
{
  cpl_ensure(aRA >= 0. && aRA < 360. && aDEC >= -90. && aDEC <= 90. &&
             aLST >= 0. && aLST < 86400. && aLatitude >= -90. && aLatitude <= 90.,
             CPL_ERROR_ILLEGAL_INPUT, -1.);
  cpl_ensure(aExptime >= 0., CPL_ERROR_ILLEGAL_INPUT, -1.);

  /* Compute hour angle of the observation in degrees. */
  double HA = aLST * 15./3600. - aRA;
  /* Range adjustments. Angle between line of sight and the meridian is needed. */
  if (HA < -180.) {
    HA += 360.;
  }
  if (HA > 180.) {
    HA -= 360.;
  }

  /* Convert angles from degrees to radians. */
  double delta = aDEC * CPL_MATH_RAD_DEG,
         latitude = aLatitude * CPL_MATH_RAD_DEG,
         hourangle = HA * CPL_MATH_RAD_DEG;

  /* Calculate airmass of the observation using the approximation given    *
   * by Young (1994). For non-zero exposure times these airmass values are *
   * averaged using the weights given by Stetson.                          */
  double cosz = muse_astro_get_zenith_distance(hourangle, delta, latitude);
#if MUSE_ASTRO_AIRMASS_APPROX == 2
  double z = acos(cosz) * CPL_MATH_DEG_RAD;
  const double zlimit = 80.;
  if (z > zlimit) {
    cpl_msg_warning(__func__, "Zenith angle %f > %f!", z, zlimit);
  }
#endif
  if (cosz == 0. || fabs(1. / cosz) < FLT_EPSILON ||
      acos(cosz) > CPL_MATH_PI_2) {
    cpl_msg_error(__func__, "Airmass computation unsuccessful. Object is below "
                  "the horizon at start (z = %f).", acos(cosz) * CPL_MATH_DEG_RAD);
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_OUTPUT);
    return -1.;
  }

  double airmass = 0.;
#if MUSE_ASTRO_AIRMASS_APPROX == 0
  airmass = muse_astro_get_airmass_youngirvine(1. / cosz);
#endif
#if MUSE_ASTRO_AIRMASS_APPROX == 1
  airmass = muse_astro_get_airmass_young(cosz);
#endif
#if MUSE_ASTRO_AIRMASS_APPROX == 2
  airmass = muse_astro_get_airmass_hardie(1. / cosz);
#endif
#if MUSE_ASTRO_AIRMASS_APPROX > 2
#error set MUSE_ASTRO_AIRMASS_APPROX to 0, 1, 2
#endif

  /* if the exposure time is larger than zero, weight in airmass   *
   * at mid and end exposure according to Stetson's weights (which *
   * are also used in IRAF/astcalc for the eairmass function       */
  if (aExptime > 0.) {
    const double weights[] = {1./6., 2./3., 1./6.};
    const int nweights = sizeof(weights) / sizeof(double);

    double timeStep = aExptime / (nweights - 1) * 15./3600. * CPL_MATH_RAD_DEG;
    airmass *= weights[0];

    int i;
    for (i = 1; i < nweights; i++) {
      cosz = muse_astro_get_zenith_distance(hourangle + i * timeStep, delta, latitude);
#if MUSE_ASTRO_AIRMASS_APPROX == 2
      z = acos(cosz) * CPL_MATH_DEG_RAD;
      if (z > zlimit) {
        cpl_msg_warning(__func__, "Zenith angle %f > %f!", z, zlimit);
      }
#endif
      if (cosz == 0. || fabs(1. / cosz) < FLT_EPSILON ||
          acos(cosz) > CPL_MATH_PI_2) {
        cpl_msg_error(__func__, "Airmass computation unsuccessful at timeStep. "
                      "Object is below the horizon at %s exposure (z=%f).",
                      i == 1 ? "mid" : "end", acos(cosz) * CPL_MATH_DEG_RAD);
        cpl_error_set(__func__, CPL_ERROR_ILLEGAL_OUTPUT);
        return -1.;
      }

#if MUSE_ASTRO_AIRMASS_APPROX == 0
      airmass += weights[i] * muse_astro_get_airmass_youngirvine(1. / cosz);
#endif
#if MUSE_ASTRO_AIRMASS_APPROX == 1
      airmass += weights[i] * muse_astro_get_airmass_young(cosz);
#endif
#if MUSE_ASTRO_AIRMASS_APPROX == 2
      airmass += weights[i] * muse_astro_get_airmass_hardie(1. / cosz);
#endif
    } /* for i (weights) */
  } /* if aExptime */

#if MUSE_ASTRO_AIRMASS_APPROX == 0
  /* Accuracy limit for airmass approximation of Young & Irvine */
  const double airmasslimit = 4.;
  if (airmass > airmasslimit) {
    cpl_msg_warning(__func__, "Airmass larger than %f", airmasslimit);
  }
#endif

  return airmass;
} /* muse_astro_compute_airmass() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Derive the effective airmass of an observation from information in
           a FITS header.
  @param   aHeader   the FITS header to use
  @return  The computed average airmass or -1 on error.

  The function queries the necessary header keywords from the input propertylist
  and calls muse_astro_compute_airmass() to do the actual computation.
  It compares the result with the ESO.TEL.AIRM.START and ESO.TEL.AIRM.END
  keywords if they are found in the input header and gives a warning if the
  output is not in between those values (with a fuzzyness of 0.005).

  @error{set CPL_ERROR_ILLEGAL_INPUT\, return -1, aHeader is NULL}
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_airmass(cpl_propertylist *aHeader)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, -1.);

  cpl_errorstate prestate = cpl_errorstate_get();
  double airm1 = muse_pfits_get_airmass_start(aHeader),
         airm2 = muse_pfits_get_airmass_end(aHeader);
  cpl_errorstate_set(prestate); /* we don't really care if these are missing */

  /* Try to read all necessary FITS headers.  Reset them to something invalid *
   * if they cannot be read, so that we get a real error message from         *
   * muse_astro_compute_airmass(), just for GEOLAT we don't care, because     *
   * muse_pfits knows a good default.                                         */
  prestate = cpl_errorstate_get();
  double ra = muse_pfits_get_ra(aHeader);
  if (!cpl_errorstate_is_equal(prestate)) {
    ra = -1000.;
  }
  prestate = cpl_errorstate_get();
  double dec = muse_pfits_get_dec(aHeader);
  if (!cpl_errorstate_is_equal(prestate)) {
    dec = -1000.;
  }
  prestate = cpl_errorstate_get();
  double lst = muse_pfits_get_lst(aHeader);
  if (!cpl_errorstate_is_equal(prestate)) {
    lst = -1000.;
  }
  prestate = cpl_errorstate_get();
  double exptime = muse_pfits_get_exptime(aHeader);
  if (!cpl_errorstate_is_equal(prestate)) {
    exptime = -1.;
  }

  double airmass = muse_astro_compute_airmass(ra, dec, lst, exptime,
                                              muse_pfits_get_geolat(aHeader));

  /* airmass computation failed, use simple average */
  if (airmass < 0. && airm1 != 0. && airm2 != 0.) {
    /* comparison doesn't make sense in this case, warn and return immediately */
    double average = (airm1 + airm2) / 2.;
    cpl_msg_warning(__func__, "airmass computation unsuccessful (%s), using simple "
                    "average of start and end values (%f)", cpl_error_get_message(),
                    average);
    return average;
  }

  /* compare computed and header values */
  cpl_msg_debug(__func__, "airmass=%f (header %f, %f)", airmass, airm1, airm2);
  if (airm1 != 0. && airm2 != 0.) {
    cpl_boolean check = airmass > fmin(airm1, airm2) - 0.005
                      && airmass < fmax(airm1, airm2) + 0.005;
    if (!check) {
      cpl_msg_warning(__func__, "Computed airmass %.3f is NOT in the range "
                      "recorded in the FITS header (%f, %f)", airmass,
                      airm1, airm2);
    }
  }

  return airmass;
} /* muse_astro_airmass() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Derive the position angle of an observation from information in
           a FITS header.
  @param   aHeader   the FITS header to use
  @return  The computed position angle or 0 on error.

  The function queries the header keyword INS.DROT.POSANG and INS.DROT.MODE.
  If INS.DROT.MODE is found to be "SKY", the sign found in POSANG is inverted,
  for "STAT" it is not. For all other cases, it's not clear what should happen,
  so output a warning and leave the original sign untouched.

  @error{set CPL_ERROR_NULL_INPUT\, return 0., aHeader is NULL}
  @error{propagate error from muse_pfits_get_drot_posang(),
         INS.DROT.POSANG could not be found in the input aHeader}
  @error{propagate error from muse_pfits_get_drot_mode(),
         INS.DROT.MODE could not be found in the input aHeader}
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_posangle(const cpl_propertylist *aHeader)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, 0.);
  double posang = muse_pfits_get_drot_posang(aHeader);
  const char *mode = muse_pfits_get_drot_mode(aHeader);
  if (mode && !strncmp(mode, "SKY", 4)) {
    posang *= -1.;
  } else if (mode && strncmp(mode, "STAT", 5)) { /* not STAT, which we ignore */
    cpl_msg_warning(__func__, "Derotator mode is neither SKY nor STAT! "
                    "Effective position angle may be wrong!");
  } else if (!mode) { /* MODE missing */
    cpl_msg_warning(__func__, "Derotator mode is not given! Effective position "
                    "angle may be wrong!");
  }
  return posang;
} /* muse_astro_posangle() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Properly average parallactic angle values of one exposure.
  @param   aHeader   the FITS header to use
  @return  The average parallactic angle or 0 on error.

  The function queries the header keywords ESO.TEL.PARANG.START and
  ESO.TEL.PARANG.END, and if a large flip occurs between them (i.e. from ~-180
  to ~+180) it properly averages them to a value with an absolute of ~180 not
  around zero.

  @error{set CPL_ERROR_NULL_INPUT\, return 0., aHeader is NULL}
  @error{output warning\, propagate error from muse_pfits_get_parang_start(),
         ESO.TEL.PARANG.START could not be found in the input aHeader}
  @error{output warning\, propagate error from muse_pfits_get_parang_end(),
         ESO.TEL.PARANG.END could not be found in the input aHeader}
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_parangle(const cpl_propertylist *aHeader)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, 0.);
  cpl_errorstate es = cpl_errorstate_get();
  double p1 = muse_pfits_get_parang_start(aHeader),
         p2 = muse_pfits_get_parang_end(aHeader);
  if (!cpl_errorstate_is_equal(es)) {
    cpl_msg_warning(__func__, "One or both TEL.PARANG keywords are missing!");
  }

  double parang = (p1 + p2) / 2.;
  if (fabs(p1 - p2) < 90.) {
    /* are not going through the meridian with a 360 deg flip in   *
     * PARANG, so it should be safe to just average the two values */
    return parang;
  }
  /* Flip in PARANG while observing this object, special (pretty          *
   * convoluted) handling needed: Both absolute values should be close to *
   * 180., so compute the absolute distance from 180, the average of the  *
   * distance to 180 with the respective sign, and subtract the average   *
   * of those 180. The final sign is taken from the value whose distance  *
   * to the from respectively signed 180 is larger.                       */
  double d1 = copysign(180. - fabs(p1), p1),
         d2 = copysign(180. - fabs(p2), p2);
  parang = 180. - fabs((d1 + d2) / 2.);
  if (fabs(d1) > fabs(d2)) {
    parang = copysign(parang, p1);
  } else {
    parang = copysign(parang, p2);
  } /* else */
  return parang;
} /* muse_astro_parangle() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute angular distance in the sky between two positions.
  @param   aRA1    RA [deg] of first position
  @param   aDEC1   DEC [deg] of first position
  @param   aRA2    RA [deg] of second position
  @param   aDEC2   DEC [deg] of second position
  @return  The computed angular distance in degrees.

  This function just implements the Vincenty formula from
  http://en.wikipedia.org/w/index.php?title=Great-circle_distance&oldid=599795109#Computational_formulas
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_angular_distance(double aRA1, double aDEC1, double aRA2, double aDEC2)
{
  /* first convert all inputs to radians */
  double ra1 = aRA1 * CPL_MATH_RAD_DEG,
         dec1 = aDEC1 * CPL_MATH_RAD_DEG,
         ra2 = aRA2 * CPL_MATH_RAD_DEG,
         dec2 = aDEC2 * CPL_MATH_RAD_DEG;
  /* compute formula components */
  double dra = ra2 - ra1,
         cosdra = cos(dra),
         sindec1 = sin(dec1),
         cosdec1 = cos(dec1),
         sindec2 = sin(dec2),
         cosdec2 = cos(dec2);
  /* compute the two terms in the nominator */
  double t1 = cosdec2 * sin(dra),
         t2 = cosdec1 * sindec2 - sindec1 * cosdec2 * cosdra,
         dsigma = atan2(sqrt(t1*t1 + t2*t2),
                        sindec1 * sindec2 + cosdec1 * cosdec2 * cosdra);
  return dsigma * CPL_MATH_DEG_RAD;
} /* muse_astro_angular_distance() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute air wavelength for a given vacuum wavelength.
  @param   aVac   wavelength in vacuum [Angstrom]
  @return  The computed wavelength in air [Angstrom]

  According to http://www.sdss3.org/dr10/spectro/spectro_basics.php#vacuum the
  formula used comes from Morton 1991 ApJS 77, 119, and represents the IAU
  standard conversion relation.

  http://www.iau.org/static/resolutions/IAU1991_French.pdf mentions
  Oosterhoff. P.T. 1957 Trans IAU Vol. IX pp. 69. 202 to be that standard
  reference, which Morton refers to as well.
 */
/*----------------------------------------------------------------------------*/
double
muse_astro_wavelength_vacuum_to_air(double aVac)
{
  return aVac / (1. + 2.735182e-4 + 131.4182 / pow(aVac, 2)
                 + 2.76249e8 / pow(aVac, 4));
} /* muse_astro_wavelength_vacuum_to_air() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Generate a rotation matrix from a set of Euler angles.
  @param   aMode    list of axes to rotate about (intrinsic rotations)
  @param   aPhi     angle of the first rotation
  @param   aTheta   angle of the second rotation
  @param   aPsi     angle of the third rotation
  @return  The function returns the created 3x3 rotation matrix, or @c NULL in
           case of error.

  The function creates a rotation matrix from the Euler angles @em aPhi,
  @em aTheta and @em aPsi, by applying three successive rotations of the
  respective angle about the cartesian axes given in the list @em aMode.

  The rotations are intrinsic rotations and are carried out in the order
  the axes are given by @em aMode. The axes designations which may appear
  in the list mode are 'x', 'y', and 'z' for the first, second, and third
  axis respectively.

  The angles are defined according to the right hand rule, i.e. they are
  positive if they represent a rotation which appears counter-clockwise
  when observed from a point on the positive part of the rotation axis, and
  they are negative for clockwise rotations.

  Originally copied from the GIRAFFE pipeline into the MUSE code.
 */
/*----------------------------------------------------------------------------*/
static cpl_matrix *
muse_astro_create_rotation(const char *aMode, double aPhi, double aTheta,
                           double aPsi)
{
  int nrotations = strlen(aMode);
  nrotations = nrotations <= 3 ? nrotations : 3;
  cpl_matrix *self = cpl_matrix_new(3, 3);

  /* Initialize the rotation matrix as identity */
  cpl_matrix_fill_diagonal(self, 1., 0);

  if (!nrotations) {
    return self;
  }

  /* Create the general rotation matrix by successively left *
   * multiplying the rotation matrices about the given axes. */
  cpl_matrix *R = cpl_matrix_new(3, 3);
  double angle[3] = { aPhi, aTheta, aPsi },
         *_R = cpl_matrix_get_data(R);
  int i;
  for (i = 0; i < nrotations; ++i) {
    double sa = sin(angle[i]),
           ca = cos(angle[i]);
    cpl_matrix_fill(R, 0.);
    switch (tolower(aMode[i])) {
    case 'x':
      _R[0] =  1.;
      _R[4] =  ca;
      _R[5] =  sa;
      _R[7] = -sa;
      _R[8] =  ca;
      break;
    case 'y':
      _R[0] =  ca;
      _R[2] = -sa;
      _R[4] =  1.;
      _R[6] =  sa;
      _R[8] =  ca;
      break;
    case 'z':
      _R[0] =  ca;
      _R[1] =  sa;
      _R[3] = -sa;
      _R[4] =  ca;
      _R[8] =  1.;
      break;
    default:
      /* Invalid axis designation */
      cpl_matrix_delete(R);
      cpl_matrix_delete(self);
      return NULL;
    } /* switch */
    cpl_matrix *mt = cpl_matrix_product_create(R, self);
    cpl_matrix_delete(self);
    self = mt;
  } /* for i */
  cpl_matrix_delete(R);
  return self;
} /* muse_astro_create_rotation() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Form the matrix of precession between two epochs.
  @param   aEpoch1   fixed epoch
  @param   aEpoch2   epoch of the date
  @return  the 3x3 precession matrix

  Reference paper:
  - Simon, J. L., et al., 1994 A&A 282, 663

  This function is modeled following the idea of the slalib routine slPREL
  (P. T. Wallace / Starlink, 23 August 1996), but not using its code.
  The code is based on the prcupd.f code given by Simon et al. and cross-checked
  with their paper.
 */
/*----------------------------------------------------------------------------*/
static cpl_matrix *
muse_astro_precession_matrix(double aEpoch1, double aEpoch2)
{
  /* shortcuts to be in Simon et al. notation */
  double sigma0 = 2000., /* the basic epoch of J2000 */
         sigmaF = aEpoch1, /* the arbitrary fixed epoch */
         sigmaD = aEpoch2; /* the epoch of the date */
  /* compute the time differences, in units of 1000 years */
  double T = (sigmaF - sigma0) / 1000., /* JD(sigmaF) - JD(sigma0) / 365250 */
         t = (sigmaD - sigmaF) / 1000.; /* JD(sigmaD) - JD(sigmaF) / 365250 */
  /* the angles, in units of arcsec */
  double thetaA = (20042.0207 - 85.3131 * T - 0.2111 * T*T
                   + 0.3642 * T*T*T + 0.0008 * T*T*T*T - 0.0005 * T*T*T*T*T) * t
                + (-42.6566 - 0.2111 * T + 0.5463 * T*T
                   + 0.0017 * T*T*T - 0.0012 * T*T*T*T) * t*t
                + (-41.8238 + 0.0359 * T + 0.0027 *T*T - 0.0001 * T*T*T) * t*t*t
                + (-0.0731 + 0.0019 * T + 0.0009 *T*T) * t*t*t*t
                + (-0.0127 + 0.0011 * T) * t*t*t*t*t
                + (0.0004) * t*t*t*t*t*t,
         zetaA = (23060.9097 + 139.7495 * T - 0.0038 * T*T - 0.5918 * T*T*T
                  - 0.0037 * T*T*T*T + 0.0007 * T*T*T*T*T) * t
               + (30.2226 - 0.2523 * T - 0.3840 * T*T - 0.0014 *T*T*T
                  + 0.0007 * T*T*T*T) * t*t
               + (18.0183 - 0.1326 * T + 0.0006 * T*T + 0.0005 * T*T*T) * t*t*t
               + (-0.0583 - 0.0001 * T + 0.0007 * T*T) * t*t*t*t
               + (-0.0285) * t*t*t*t*t
               + (-0.0002) * t*t*t*t*t*t,
         zA = (23060.9097 +139.7495 * T -0.0038 * T*T -0.5918 * T*T*T
               - 0.0037 * T*T*T*T + 0.0007 * T*T*T*T*T) * t
            + (109.5270 + 0.2446 * T - 1.3913 * T*T - 0.0134 * T*T*T
               + 0.0026 * T*T*T*T) * t*t
            + (18.2667 - 1.1400 * T - 0.0173 * T*T + 0.0044 * T*T*T) * t*t*t
            + (-0.2821 - 0.0093 * T + 0.0032 * T*T) * t*t*t*t
            + (-0.0301 + 0.0006 * T) * t*t*t*t*t
            + (-0.0001) * t*t*t*t*t*t;
  /* convert the three angles to radians */
  thetaA *= CPL_MATH_RAD_DEG / 3600.;
  zetaA *= CPL_MATH_RAD_DEG / 3600.;
  zA *= CPL_MATH_RAD_DEG / 3600.;

  /* create the matrix */
  return muse_astro_create_rotation("zyz", -zetaA, thetaA, -zA);
} /* muse_astro_precession_matrix() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute the mean local sidereal time
  @param   aJD     Julian date
  @param   aLong   observer's longitude [radian]
  @return  Mean local sidereal time [radian].

  @note Constants taken from the American Ephemeris and Nautical Almanac (1980).
        Translated from the FORTRAN version written by G. Torres (1989).

  Originally copied from the GIRAFFE pipeline into the MUSE code.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_sidereal_time(double aJD, double aLong)
{
  /* Constants D1,D2,D3 for calculating Greenwich *
   * Mean Sidereal Time at 0 UT                   */
  const double d1 = 1.739935934667999,
               d2 = 6.283319509909095e02,
               d3 = 6.755878646261384e-06,
               df = 1.00273790934;
  double djd0 = floor(aJD) + 0.5;
  if (djd0 > aJD) {
    djd0 -= 1.;
  }
  double dut = (aJD - djd0) * CPL_MATH_2PI,
         dt = (djd0 - kT0) / kJulianCentury,
         dst0 = d1 + d2 * dt + d3 * dt * dt;
  dst0 = fmod(dst0, CPL_MATH_2PI);
  double dst = df * dut + dst0 - aLong;
  dst = fmod(dst + 2.*CPL_MATH_2PI, CPL_MATH_2PI);
  return dst;
} /* muse_astro_sidereal_time() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute correction from topocentric radial velocity to geocentric.
  @param   aLat    observer's geodetic latitude [radian]
  @param   aElev   observer's elevation above sea level [m]
  @param   aDEC    target declination [radian] for mean equator and equinox of
                   date
  @param   aHA     target hour angle (radians)
  @return  geocentric correction [km/s].

  Calculates the correction required to transform the topocentric radial
  velocity of a given star to geocentric. The maximum error of this routine is
  not expected to be larger than 0.1 m/s.

  @note vr = r.w.cos(aDEC).sin(aHA), where r = geocentric radius at observer's
        latitude and elevation, and w = 2.pi/t, t = length of sidereal day
        (23 hours 56 minutes and 4 seconds) in seconds.
        The hour angle aHA is positive east of the meridian.
        Other relevant equations from E. W. Woolard & G. M. Clemence (1966),
        Spherical Astronomy, p.45 and p.48

  Author:
   - G. Torres (1989)
   - G. Simond (C translation)

  Originally copied from the GIRAFFE pipeline into the MUSE code.
 */
/*----------------------------------------------------------------------------*/
static double
muse_astro_geo_correction(double aLat, double aElev, double aDEC, double aHA)
{
  /* Earth's equatorial radius (km) (Geod. ref. sys., 1980) */
  const double da = 6378.137;

  /* Polar flattening (Geod. ref. sys., 1980) */
  const double df = 1. / 298.257222;

  /* Angular rotation rate (2.pi/t) */
  const double dw = CPL_MATH_2PI / 86164.;
  const double de2 = df * (2.0 - df),
               dsdlats = sin(aLat) * sin(aLat);

  /* Calculate geocentric radius dr0 at sea level (km) */
  double d1 = 1.0 - de2 * (2.0 - de2) * dsdlats,
         d2 = 1.0 - de2 * dsdlats,
         dr0 = da * sqrt(d1 / d2);

  /* Calculate geocentric latitude dlatg */
  d1 = de2 * sin(2.0 * aLat);
  d2 = 2.0 * d2;
  double dlatg = aLat - atan(d1 / d2);

  /* Calculate geocentric radius drh at elevation aElev (km) */
  double drh = dr0 * cos(dlatg) + (aElev / 1000.) * cos(aLat);

  /* Projected component to star at declination = aDEC and *
   * at hour angle = aHA, in units of km/s                 */
  return dw * drh * cos(aDEC) * sin(aHA);
} /* muse_astro_geo_correction() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute heliocentric and barycentric velocity components of Earth.
  @param   aDJE    the data of the Julian ephermeris
  @param   aDEqu   epoch of mean equator and mean equinox
  @param   aHVel   vector to return the comonents of the heliocentric velocity
  @param   aBVel   vector to return the comonents of the barycentric velocity

  Calculates the heliocentric and barycentric velocity components of the earth.
  The largest deviations from the JPL-de96 ephemeris are 42 cm/s for both
  heliocentric and barycentric velocity components.

  aDEqu refers to equator and equinox of @em aHVel and @em aBVel. If @em aDEqu is
  @c 0, both vectors refer to the mean equator and equinox of @em aDJE.

  The components returned in the vectors @em aHVel and @em aBVel, are
  (dx/dt, dy/dt, dz/dt, k=1,2,3), in units of au/s.

  Authors:
   - P. Stumpff (ibm-version 1979): astron. astrophys. suppl. ser. 41, 1 (1980)
   - M. H.Slovak (vax 11/780 implementation 1986)
   - G. Torres (1989)
   - G. Simond (C translation)

  Originally copied from the GIRAFFE pipeline into the MUSE code.
 */
/*----------------------------------------------------------------------------*/
static void
muse_astro_earth_velocity(double aDJE, double aDEqu, double* const aHVel,
                          double* const aBVel)
{
  /* Constants dcfel(i, k) of fast-changing elements *
   *   i = 1          i = 2                 i = 3    */
  const double dcfel[][3] = {
    { 1.7400353e+00, 6.2833195099091e+02,  5.2796e-06 },
    { 6.2565836e+00, 6.2830194572674e+02, -2.6180e-06 },
    { 4.7199666e+00, 8.3997091449254e+03, -1.9780e-05 },
    { 1.9636505e-01, 8.4334662911720e+03, -5.6044e-05 },
    { 4.1547339e+00, 5.2993466764997e+01,  5.8845e-06 },
    { 4.6524223e+00, 2.1354275911213e+01,  5.6797e-06 },
    { 4.2620486e+00, 7.5025342197656e+00,  5.5317e-06 },
    { 1.4740694e+00, 3.8377331909193e+00,  5.6093e-06 }
  };

  /* Constants dceps and ccsel(i,k) of slowly changing elements *
   *   i = 1          i = 2          i = 3                      */
  const double dceps[3] = {
    4.093198e-01,   -2.271110e-04, -2.860401e-08
  };
  const double ccsel[][3] = {
    { 1.675104e-02, -4.179579e-05, -1.260516e-07 },
    { 2.220221e-01,  2.809917e-02,  1.852532e-05 },
    { 1.589963e+00,  3.418075e-02,  1.430200e-05 },
    { 2.994089e+00,  2.590824e-02,  4.155840e-06 },
    { 8.155457e-01,  2.486352e-02,  6.836840e-06 },
    { 1.735614e+00,  1.763719e-02,  6.370440e-06 },
    { 1.968564e+00,  1.524020e-02, -2.517152e-06 },
    { 1.282417e+00,  8.703393e-03,  2.289292e-05 },
    { 2.280820e+00,  1.918010e-02,  4.484520e-06 },
    { 4.833473e-02,  1.641773e-04, -4.654200e-07 },
    { 5.589232e-02, -3.455092e-04, -7.388560e-07 },
    { 4.634443e-02, -2.658234e-05,  7.757000e-08 },
    { 8.997041e-03,  6.329728e-06, -1.939256e-09 },
    { 2.284178e-02, -9.941590e-05,  6.787400e-08 },
    { 4.350267e-02, -6.839749e-05, -2.714956e-07 },
    { 1.348204e-02,  1.091504e-05,  6.903760e-07 },
    { 3.106570e-02, -1.665665e-04, -1.590188e-07 }
  };

  /* Constants of the arguments of the short-period perturbations by *
   * the planets: dcargs(i, k)                                       *
   *   i = 1           i = 2                                         */
  const double dcargs[][2] = {
    { 5.0974222e+00, -7.8604195454652e+02 },
    { 3.9584962e+00, -5.7533848094674e+02 },
    { 1.6338070e+00, -1.1506769618935e+03 },
    { 2.5487111e+00, -3.9302097727326e+02 },
    { 4.9255514e+00, -5.8849265665348e+02 },
    { 1.3363463e+00, -5.5076098609303e+02 },
    { 1.6072053e+00, -5.2237501616674e+02 },
    { 1.3629480e+00, -1.1790629318198e+03 },
    { 5.5657014e+00, -1.0977134971135e+03 },
    { 5.0708205e+00, -1.5774000881978e+02 },
    { 3.9318944e+00,  5.2963464780000e+01 },
    { 4.8989497e+00,  3.9809289073258e+01 },
    { 1.3097446e+00,  7.7540959633708e+01 },
    { 3.5147141e+00,  7.9618578146517e+01 },
    { 3.5413158e+00, -5.4868336758022e+02 }
  };

  /* Amplitudes ccamps(n, k) of the short-period perturbations        *
   *    n = 1         n = 2         n = 3         n = 4         n = 5 */
  const double ccamps[][5] = {
    { -2.279594e-5,  1.407414e-5,  8.273188e-6,  1.340565e-5, -2.490817e-7 },
    { -3.494537e-5,  2.860401e-7,  1.289448e-7,  1.627237e-5, -1.823138e-7 },
    {  6.593466e-7,  1.322572e-5,  9.258695e-6, -4.674248e-7, -3.646275e-7 },
    {  1.140767e-5, -2.049792e-5, -4.747930e-6, -2.638763e-6, -1.245408e-7 },
    {  9.516893e-6, -2.748894e-6, -1.319381e-6, -4.549908e-6, -1.864821e-7 },
    {  7.310990e-6, -1.924710e-6, -8.772849e-7, -3.334143e-6, -1.745256e-7 },
    { -2.603449e-6,  7.359472e-6,  3.168357e-6,  1.119056e-6, -1.655307e-7 },
    { -3.228859e-6,  1.308997e-7,  1.013137e-7,  2.403899e-6, -3.736225e-7 },
    {  3.442177e-7,  2.671323e-6,  1.832858e-6, -2.394688e-7, -3.478444e-7 },
    {  8.702406e-6, -8.421214e-6, -1.372341e-6, -1.455234e-6, -4.998479e-8 },
    { -1.488378e-6, -1.251789e-5,  5.226868e-7, -2.049301e-7,  0.0e0       },
    { -8.043059e-6, -2.991300e-6,  1.473654e-7, -3.154542e-7,  0.0e0       },
    {  3.699128e-6, -3.316126e-6,  2.901257e-7,  3.407826e-7,  0.0e0       },
    {  2.550120e-6, -1.241123e-6,  9.901116e-8,  2.210482e-7,  0.0e0       },
    { -6.351059e-7,  2.341650e-6,  1.061492e-6,  2.878231e-7,  0.0e0       }
  };

  /* Constants of the secular perturbations in longitude *
   * ccsec3 and ccsec(n,k)                               *
   *   n = 1         n = 2         n = 3                 */
  const double ccsec3 = -7.757020e-08,
               ccsec[][3] = {
    { 1.289600e-06, 5.550147e-01, 2.076942e+00 },
    { 3.102810e-05, 4.035027e+00, 3.525565e-01 },
    { 9.124190e-06, 9.990265e-01, 2.622706e+00 },
    { 9.793240e-07, 5.508259e+00, 1.559103e+01 }
  };

  /* Sidereal rate dcsld in longitude, rate ccsgd in mean anomaly */
  const double dcsld = 1.990987e-07,
               ccsgd = 1.990969e-07;

  /* Some constants used in the calculation of the *
   * lunar contribution                            */
  const double cckm  = 3.122140e-05,
               ccmld = 2.661699e-06,
               ccfdi = 2.399485e-07;

  /* Constants dcargm(i,k) of the arguments of the *
   * perturbations of the motion of the moon       *
   *   i = 1           i = 2                       */
  const double dcargm[][2] = {
    { 5.1679830e+00,  8.3286911095275e+03 },
    { 5.4913150e+00, -7.2140632838100e+03 },
    { 5.9598530e+00,  1.5542754389685e+04 }
  };

  /* Amplitudes ccampm(n,k) of the perturbations of the moon *
   *    n = 1         n = 2         n = 3         n = 4      */
  const double ccampm[][4] = {
    { 1.097594e-01, 2.896773e-07, 5.450474e-02,  1.438491e-07 },
    {-2.223581e-02, 5.083103e-08, 1.002548e-02, -2.291823e-08 },
    { 1.148966e-02, 5.658888e-08, 8.249439e-03,  4.063015e-08 }
  };

  /* ccpamv = a * m * dl/dt (planets) */
  const double ccpamv[4] = { 8.326827e-11, 1.843484e-11,
                             1.988712e-12, 1.881276e-12 };

  /* dc1mme = 1 - mass(earth + moon) */
  const double dc1mme = 0.99999696;

  /* Control-parameter ideq, and time-arguments */
  int ideq = (int)aDEqu;
  double dt = (aDJE - kT0) / kJulianCentury,
         t = dt,
         dtsq = dt * dt,
         tsq = dtsq;

  /* Values of all elements for the instant aDJE */
  double dml = 0.,
         forbel[7] = { 0., 0., 0., 0., 0., 0., 0. };
  int k;
  for (k = 0; k < 8; k++) {
    double dlocal = fmod(dcfel[k][0] + dt * dcfel[k][1] + dtsq * dcfel[k][2],
                         CPL_MATH_2PI);
    if (k == 0) {
      dml = dlocal;
    }
    if (k != 0) {
      forbel[k - 1] = dlocal;
    }
  } /* for k */
  double deps = fmod(dceps[0] + dt * dceps[1] + dtsq * dceps[2], CPL_MATH_2PI),
         sorbel[17];
  for (k = 0; k < 17; k++) {
    sorbel[k] = fmod(ccsel[k][0] + t * ccsel[k][1] + tsq * ccsel[k][2],
                     CPL_MATH_2PI);
  } /* for k */

  /* Secular perturbations in longitude */
  double sn[4];
  for (k = 0; k < 4; k++) {
    double a = fmod(ccsec[k][1] + t * ccsec[k][2], CPL_MATH_2PI);
    sn[k] = sin(a);
  } /* for k */

  /* Periodic perturbations of the earth-moon barycenter */
  double pertl = ccsec[0][0] * sn[0] + ccsec[1][0] * sn[1]
               + (ccsec[2][0] + t * ccsec3) * sn[2] + ccsec[3][0] * sn[3],
         pertld = 0.,
         pertr = 0.,
         pertrd = 0.;
  for (k = 0; k < 15; k++) {
    double a = fmod(dcargs[k][0] + dt * dcargs[k][1], CPL_MATH_2PI);
    pertl += (ccamps[k][0] * cos(a) + ccamps[k][1] * sin(a));
    pertr += (ccamps[k][2] * cos(a) + ccamps[k][3] * sin(a));
    if (k >= 10) {
      continue;
    }
    pertld += ((ccamps[k][1] * cos(a) - ccamps[k][0] * sin(a)) * ccamps[k][4]);
    pertrd += ((ccamps[k][3] * cos(a) - ccamps[k][2] * sin(a)) * ccamps[k][4]);
  } /* for k */

  /* Elliptic part of the motion of the earth-moon barycenter */
  double esq = sorbel[0] * sorbel[0],
         dparam = 1. - esq,
         param = dparam,
         twoe = sorbel[0] + sorbel[0],
         twog = forbel[0] + forbel[0],
         phi = twoe * ((1.0 - esq * (1.0 / 8.0)) * sin(forbel[0])
                       + sorbel[0] * (5.0 / 8.0) * sin(twog)
                       + esq * 0.5416667 * sin(forbel[0] + twog)),
         f = forbel[0] + phi,
         dpsi = dparam / (1. + sorbel[0] * cos(f)),
         phid = twoe * ccsgd * ((1.0 + esq * 1.50) * cos(f)
                                + sorbel[0] * (1.250 - sin(f) * sin(f) * 0.50)),
         psid = ccsgd * sorbel[0] * sin(f) / sqrt(param);

  /* Perturbed heliocentric motion of the earth-moon barycenter */
  double d1pdro = 1. + pertr,
         drd = d1pdro * (psid + dpsi * pertrd),
         drld = d1pdro * dpsi * (dcsld + phid + pertld),
         dtl = fmod(dml + phi + pertl, CPL_MATH_2PI),
         dxhd = drd * cos(dtl) - drld * sin(dtl),
         dyhd = drd * sin(dtl) + drld * cos(dtl);

  /* Influence of eccentricity, evection and variation of *
   * the geocentric motion of the moon                    */
  pertl = 0.;
  pertld = 0.;
  double pertp = 0.,
         pertpd = 0.;
  for (k = 0; k < 3; k++) {
    double a = fmod(dcargm[k][0] + dt * dcargm[k][1], CPL_MATH_2PI);
    pertl += ccampm[k][0] * sin(a);
    pertld += ccampm[k][1] * cos(a);
    pertp += ccampm[k][2] * cos(a);
    pertpd -= ccampm[k][3] * sin(a);
  } /* for k */

  /* Heliocentric motion of the earth */
  double tl = forbel[1] + pertl,
         sigma = cckm / (1. + pertp),
         a = sigma * (ccmld + pertld),
         b = sigma * pertpd;
  dxhd = dxhd + a * sin(tl) + b * cos(tl);
  dyhd = dyhd - a * cos(tl) + b * sin(tl);
  double dzhd = -sigma * ccfdi * cos(forbel[2]);

  /* Barycentric motion of the earth */
  double dxbd = dxhd * dc1mme,
         dybd = dyhd * dc1mme,
         dzbd = dzhd * dc1mme;
  for (k = 0; k < 4; k++) {
    double plon = forbel[k + 3],
           pomg = sorbel[k + 1],
           pecc = sorbel[k + 9];
    tl = fmod(plon + 2.0 * pecc * sin(plon - pomg), CPL_MATH_2PI);
    dxbd = dxbd + ccpamv[k] * (sin(tl) + pecc * sin(pomg));
    dybd = dybd - ccpamv[k] * (cos(tl) + pecc * cos(pomg));
    dzbd = dzbd - ccpamv[k] * sorbel[k + 13] * cos(plon - sorbel[k + 5]);
  } /* for k */

  /* Transition to mean equator of date */
  double dyahd = cos(deps) * dyhd - sin(deps) * dzhd,
         dzahd = sin(deps) * dyhd + cos(deps) * dzhd,
         dyabd = cos(deps) * dybd - sin(deps) * dzbd,
         dzabd = sin(deps) * dybd + cos(deps) * dzbd;
  if (ideq == 0) {
    aHVel[0] = dxhd;
    aHVel[1] = dyahd;
    aHVel[2] = dzahd;
    aBVel[0] = dxbd;
    aBVel[1] = dyabd;
    aBVel[2] = dzabd;
  } else {
    /* General precession from epoch aDJE to aDEqu */
    double deqdat = (aDJE - kT0 - kBesselOffset) / kTropYear + kEpoch1900;
    cpl_matrix* prec = muse_astro_precession_matrix(deqdat, aDEqu);
    int n;
    for (n = 0; n < 3; n++) {
      aHVel[n] = dxhd  * cpl_matrix_get(prec, 0, n)
               + dyahd * cpl_matrix_get(prec, 1, n)
               + dzahd * cpl_matrix_get(prec, 2, n);
      aBVel[n] = dxbd  * cpl_matrix_get(prec, 0, n)
              + dyabd * cpl_matrix_get(prec, 1, n)
              + dzabd * cpl_matrix_get(prec, 2, n);
    }
    cpl_matrix_delete(prec);
  } /* else */
} /* muse_astro_earth_velocity() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute heliocentric, barycentric and  geocentric correction.
  @param   aRV     Result structure to store the computed corrections
  @param   aJD     Heliocentric Julian date (days)
  @param   aLong   Geodetic longitude (degrees, west positive)
  @param   aLat    Geodetic latitude (degrees)
  @param   aElev   Altitude above sea level (meters)
  @param   aRA     Right ascension of star (hours)
  @param   aDEC    Declination of star (degrees)
  @param   aEqui   Mean equator and equinox for coordinates e.g., 1950.0

  The elements returnted in the aRV structure are
  - bary:  Barycentric correction [km/s]
  - helio: Heliocentric correction [km/s]
  - geo:   Geocentric correction [km/s]

  Authors:
  - G. Torres (1989)
  - Modified by D. Mink
  - G. Simond (C translation)

  Originally copied from the GIRAFFE pipeline into the MUSE code.
 */
/*----------------------------------------------------------------------------*/
static void
muse_rvcorrection_compute(muse_astro_rvcorr *aRV, double aJD, double aLong,
                          double aLat, double aElev, double aRA, double aDEC,
                          double aEqui)
{
  /* Precess RA and DEC to mean equator and equinox of date */
  double eqt = (aJD - kT0 - kBesselOffset) / kTropYear + kEpoch1900,
         dc[3] = { cos(aRA * 15.0 * CPL_MATH_RAD_DEG) * cos(aDEC * CPL_MATH_RAD_DEG),
                   sin(aRA * 15.0 * CPL_MATH_RAD_DEG) * cos(aDEC * CPL_MATH_RAD_DEG),
                   sin(aDEC * CPL_MATH_RAD_DEG) };
  cpl_matrix *precession = muse_astro_precession_matrix(aEqui, eqt);
  double dcc[3];
  int i;
  for (i = 0; i < 3; ++i) {
    dcc[i] = dc[0] * cpl_matrix_get(precession, i, 0)
           + dc[1] * cpl_matrix_get(precession, i, 1)
           + dc[2] * cpl_matrix_get(precession, i, 2);
  }
  cpl_matrix_delete(precession);
  precession = NULL;

  double ra2 = 0.,
         dec2 = asin(dcc[2]);
  if (dcc[0] != 0.) {
    ra2 = atan(dcc[1] / dcc[0]);
    if (dcc[0] < 0.) {
      ra2 += CPL_MATH_PI;
    } else {
      if (dcc[1] < 0.) {
        ra2 += CPL_MATH_2PI;
      } /* if */
    } /* else */
  } else {
    if (dcc[1] > 0.) {
      ra2 = CPL_MATH_PI_2;
    } else {
      ra2 = 1.5 * CPL_MATH_PI;
    } /* else */
  } /* else */

  /* Calculate hour angle = local sidereal time - RA *
   * convert input values to units of radians        */
  double st = muse_astro_sidereal_time(aJD, aLong * CPL_MATH_RAD_DEG),
         ha = st - ra2;

  /* Calculate observer's geocentric velocity *
   * (elevation assumed to be zero)           */
  aRV->geo = muse_astro_geo_correction(aLat * CPL_MATH_RAD_DEG, aElev, dec2, -ha);

  /* Calculate components of earth's barycentric velocity, *
   * bvel(i), i=1,2,3 in units of au/s                     */
  double hv[3] = { 0., 0., 0. },
         bv[3] = { 0., 0., 0. };
  muse_astro_earth_velocity(aJD, eqt, hv, bv);

  /* Project barycentric velocity to the direction of the *
   * star, and convert to km/s                            */
  aRV->bary = 0.;
  aRV->helio = 0.;
  for (i = 0; i < 3; ++i) {
    aRV->bary += bv[i] * dcc[i] * kAU;
    aRV->helio += hv[i] * dcc[i] * kAU;
  } /* for i */
} /* muse_rvcorrection_compute() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Compute radial velocity corrections given the header of an exposure.
  @param   aHeader   the FITS header to use
  @return  the function returns a structure with the three velocity corrections,
           or zero for every component on error.

  The function takes the information about the telescope location, the
  observation time and the epoch from aHeader and computes the barycentric,
  heliocentric and geocentric corrections for the telescope pointing. The
  motion of the observing site to the geocentric velocity is taken into account
  in the bary- and heliocentric corrections.

  The computed corrections are in units of km/s.

  @error{set CPL_ERROR_NULL_INPUT\, return zero for every component,
         aHeader is NULL}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, propagate error codes\, return zero for every component,
         aHeader does not contain all necessary info}
 */
/*----------------------------------------------------------------------------*/
muse_astro_rvcorr
muse_astro_rvcorr_compute(const cpl_propertylist *aHeader)
{
  muse_astro_rvcorr rvcorr = { 0., 0., 0. };
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, rvcorr);

  /* get all the necessary properties from the header, watching the state */
  cpl_errorstate state = cpl_errorstate_get();
  double exptime = muse_pfits_get_exptime(aHeader),
         jd = muse_pfits_get_mjdobs(aHeader),
         equinox = muse_pfits_get_equinox(aHeader),
         ra = muse_pfits_get_ra(aHeader) / 15., /* need RA in hours */
         dec = muse_pfits_get_dec(aHeader);
  /* Compute julian date of mid exposure. 2400000.5 is the offset between *
   * JD and MJD and corresponds to November 17th 1858 0:00 UT.            */
  jd += 2400000.5 + 0.5 * exptime / (24. * 3600.);
  if (!cpl_errorstate_is_equal(state)) {
    cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "Could not find all"
                          " properties necessary for radial velocity computation!");
    return rvcorr;
  }
  /* the following functions provide good defaults... */
  double lon = muse_pfits_get_geolon(aHeader),
         lat = muse_pfits_get_geolat(aHeader),
         elev = muse_pfits_get_geoelev(aHeader);
  if (!cpl_errorstate_is_equal(state)) { /* ... so ignore any errors */
    cpl_errorstate_set(state);
  }

  /* now call the function that originates from the GIRAFFE pipeline */
  muse_rvcorrection_compute(&rvcorr, jd, lon, lat, elev, ra, dec, equinox);
  /* add geocentric velocity to get total corrective velocities */
  rvcorr.bary = rvcorr.bary + rvcorr.geo;
  rvcorr.helio = rvcorr.helio + rvcorr.geo;
  return rvcorr;
} /* muse_astro_rvcorr_compute() */

/**@}*/
