JPLEphemeridesLoader.java

  1. /* Copyright 2002-2018 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.bodies;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.UnsupportedEncodingException;
  21. import java.text.ParseException;
  22. import java.util.ArrayList;
  23. import java.util.Comparator;
  24. import java.util.HashMap;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.SortedSet;
  28. import java.util.TreeSet;
  29. import java.util.concurrent.atomic.AtomicReference;

  30. import org.hipparchus.RealFieldElement;
  31. import org.hipparchus.util.FastMath;
  32. import org.orekit.data.DataLoader;
  33. import org.orekit.data.DataProvidersManager;
  34. import org.orekit.errors.OrekitException;
  35. import org.orekit.errors.OrekitInternalError;
  36. import org.orekit.errors.OrekitMessages;
  37. import org.orekit.errors.TimeStampedCacheException;
  38. import org.orekit.frames.Frame;
  39. import org.orekit.frames.FramesFactory;
  40. import org.orekit.time.AbsoluteDate;
  41. import org.orekit.time.DateComponents;
  42. import org.orekit.time.FieldAbsoluteDate;
  43. import org.orekit.time.TimeComponents;
  44. import org.orekit.time.TimeScale;
  45. import org.orekit.time.TimeScalesFactory;
  46. import org.orekit.utils.Constants;
  47. import org.orekit.utils.FieldPVCoordinates;
  48. import org.orekit.utils.OrekitConfiguration;
  49. import org.orekit.utils.PVCoordinates;
  50. import org.orekit.utils.GenericTimeStampedCache;
  51. import org.orekit.utils.TimeStampedGenerator;

  52. /** Loader for JPL ephemerides binary files (DE 4xx) and similar formats (INPOP 06/08/10).
  53.  * <p>JPL ephemerides binary files contain ephemerides for all solar system planets.</p>
  54.  * <p>The JPL ephemerides binary files are recognized thanks to their base names,
  55.  * which must match the pattern <code>[lu]nx[mp]####.ddd</code> (or
  56.  * <code>[lu]nx[mp]####.ddd.gz</code> for gzip-compressed files) where # stands for a
  57.  * digit character and where ddd is an ephemeris type (typically 405 or 406).</p>
  58.  * <p>The loader supports files encoded in big-endian as well as in little-endian notation.
  59.  * Usually, big-endian files are named <code>unx[mp]####.ddd</code>, while little-endian files
  60.  * are named <code>lnx[mp]####.ddd</code>.</p>
  61.  * <p>The IMCCE ephemerides binary files are recognized thanks to their base names,
  62.  * which must match the pattern <code>inpop*.dat</code> (or
  63.  * <code>inpop*.dat.gz</code> for gzip-compressed files) where * stands for any string.</p>
  64.  * <p>The loader supports files encoded in big-endian as well as in little-endian notation.
  65.  * Usually, big-endian files contain <code>bigendian</code> in their names, while little-endian files
  66.  * contain <code>littleendian</code> in their names.</p>
  67.  * <p>The loader supports files in TDB or TCB time scales.</p>
  68.  * @author Luc Maisonobe
  69.  */
  70. public class JPLEphemeridesLoader implements CelestialBodyLoader {

  71.     /** Default supported files name pattern for JPL DE files. */
  72.     public static final String DEFAULT_DE_SUPPORTED_NAMES = "^[lu]nx([mp](\\d\\d\\d\\d))+\\.(?:4\\d\\d)$";

  73.     /** Default supported files name pattern for IMCCE INPOP files. */
  74.     public static final String DEFAULT_INPOP_SUPPORTED_NAMES = "^inpop.*\\.dat$";

  75.     /** 50 days in seconds. */
  76.     private static final double FIFTY_DAYS = 50 * Constants.JULIAN_DAY;

  77.     /** DE number used by INPOP files. */
  78.     private static final int INPOP_DE_NUMBER = 100;

  79.     /** Maximal number of constants in headers. */
  80.     private static final int CONSTANTS_MAX_NUMBER           = 400;

  81.     /** Offset of the ephemeris type in first header record. */
  82.     private static final int HEADER_EPHEMERIS_TYPE_OFFSET   = 2840;

  83.     /** Offset of the record size (for INPOP files) in first header record. */
  84.     private static final int HEADER_RECORD_SIZE_OFFSET      = 2856;

  85.     /** Offset of the start epoch in first header record. */
  86.     private static final int HEADER_START_EPOCH_OFFSET      = 2652;

  87.     /** Offset of the end epoch in first header record. */
  88.     private static final int HEADER_END_EPOCH_OFFSET        = 2660;

  89.     /** Offset of the astronomical unit in first header record. */
  90.     private static final int HEADER_ASTRONOMICAL_UNIT_OFFSET = 2680;

  91.     /** Offset of the Earth-Moon mass ratio in first header record. */
  92.     private static final int HEADER_EM_RATIO_OFFSET         = 2688;

  93.     /** Offset of Chebishev coefficients indices in first header record. */
  94.     private static final int HEADER_CHEBISHEV_INDICES_OFFSET = 2696;

  95.     /** Offset of libration coefficients indices in first header record. */
  96.     private static final int HEADER_LIBRATION_INDICES_OFFSET = 2844;

  97.     /** Offset of chunks duration in first header record. */
  98.     private static final int HEADER_CHUNK_DURATION_OFFSET    = 2668;

  99.     /** Offset of the constants names in first header record. */
  100.     private static final int HEADER_CONSTANTS_NAMES_OFFSET  = 252;

  101.     /** Offset of the constants values in second header record. */
  102.     private static final int HEADER_CONSTANTS_VALUES_OFFSET = 0;

  103.     /** Offset of the range start in the data records. */
  104.     private static final int DATA_START_RANGE_OFFSET        = 0;

  105.     /** Offset of the range end in the data records. */
  106.     private static final int DATE_END_RANGE_OFFSET          = 8;

  107.     /** The constant name for the astronomical unit. */
  108.     private static final String CONSTANT_AU = "AU";

  109.     /** The constant name for the earth-moon mass ratio. */
  110.     private static final String CONSTANT_EMRAT = "EMRAT";

  111.     /** List of supported ephemerides types. */
  112.     public enum EphemerisType {

  113.         /** Constant for solar system barycenter. */
  114.         SOLAR_SYSTEM_BARYCENTER,

  115.         /** Constant for the Sun. */
  116.         SUN,

  117.         /** Constant for Mercury. */
  118.         MERCURY,

  119.         /** Constant for Venus. */
  120.         VENUS,

  121.         /** Constant for the Earth-Moon barycenter. */
  122.         EARTH_MOON,

  123.         /** Constant for the Earth. */
  124.         EARTH,

  125.         /** Constant for the Moon. */
  126.         MOON,

  127.         /** Constant for Mars. */
  128.         MARS,

  129.         /** Constant for Jupiter. */
  130.         JUPITER,

  131.         /** Constant for Saturn. */
  132.         SATURN,

  133.         /** Constant for Uranus. */
  134.         URANUS,

  135.         /** Constant for Neptune. */
  136.         NEPTUNE,

  137.         /** Constant for Pluto. */
  138.         PLUTO

  139.     }

  140.     /** Interface for raw position-velocity retrieval. */
  141.     public interface RawPVProvider {

  142.         /** Get the position-velocity at date.
  143.          * @param date date at which the position-velocity is desired
  144.          * @return position-velocity at the specified date
  145.          * @exception OrekitException if the date is not available to the loader
  146.          */
  147.         PVCoordinates getRawPV(AbsoluteDate date) throws OrekitException;

  148.         /** Get the position-velocity at date.
  149.          * @param date date at which the position-velocity is desired
  150.          * @param <T> type of the field elements
  151.          * @return position-velocity at the specified date
  152.          * @exception OrekitException if the date is not available to the loader
  153.          */
  154.         <T extends RealFieldElement<T>> FieldPVCoordinates<T> getRawPV(FieldAbsoluteDate<T> date) throws OrekitException;

  155.     }

  156.     /** Regular expression for supported files names. */
  157.     private final String supportedNames;

  158.     /** Ephemeris for selected body. */
  159.     private final GenericTimeStampedCache<PosVelChebyshev> ephemerides;

  160.     /** Constants defined in the file. */
  161.     private final AtomicReference<Map<String, Double>> constants;

  162.     /** Ephemeris type to generate. */
  163.     private final EphemerisType generateType;

  164.     /** Ephemeris type to load. */
  165.     private final EphemerisType loadType;

  166.     /** Current file start epoch. */
  167.     private AbsoluteDate startEpoch;

  168.     /** Current file final epoch. */
  169.     private AbsoluteDate finalEpoch;

  170.     /** Chunks duration (in seconds). */
  171.     private double maxChunksDuration;

  172.     /** Current file chunks duration (in seconds). */
  173.     private double chunksDuration;

  174.     /** Index of the first data for selected body. */
  175.     private int firstIndex;

  176.     /** Number of coefficients for selected body. */
  177.     private int coeffs;

  178.     /** Number of chunks for the selected body. */
  179.     private int chunks;

  180.     /** Number of components contained in the file. */
  181.     private int components;

  182.     /** Unit of the position coordinates (as a multiple of meters). */
  183.     private double positionUnit;

  184.     /** Time scale of the date coordinates. */
  185.     private TimeScale timeScale;

  186.     /** Indicator for binary file endianness. */
  187.     private boolean bigEndian;

  188.     /** Create a loader for JPL ephemerides binary files.
  189.      * @param supportedNames regular expression for supported files names
  190.      * @param generateType ephemeris type to generate
  191.      * @exception OrekitException if the header constants cannot be read
  192.      */
  193.     public JPLEphemeridesLoader(final String supportedNames, final EphemerisType generateType)
  194.         throws OrekitException {

  195.         this.supportedNames = supportedNames;
  196.         constants = new AtomicReference<Map<String, Double>>();

  197.         this.generateType  = generateType;
  198.         if (generateType == EphemerisType.SOLAR_SYSTEM_BARYCENTER) {
  199.             loadType = EphemerisType.EARTH_MOON;
  200.         } else if (generateType == EphemerisType.EARTH_MOON) {
  201.             loadType = EphemerisType.MOON;
  202.         } else {
  203.             loadType = generateType;
  204.         }

  205.         ephemerides = new GenericTimeStampedCache<PosVelChebyshev>(2, OrekitConfiguration.getCacheSlotsNumber(),
  206.                                                                    Double.POSITIVE_INFINITY, FIFTY_DAYS,
  207.                                                                    new EphemerisParser());
  208.         maxChunksDuration = Double.NaN;
  209.         chunksDuration    = Double.NaN;

  210.     }

  211.     /** Load celestial body.
  212.      * @param name name of the celestial body
  213.      * @return loaded celestial body
  214.      * @throws OrekitException if the body cannot be loaded
  215.      */
  216.     public CelestialBody loadCelestialBody(final String name) throws OrekitException {

  217.         final double gm       = getLoadedGravitationalCoefficient(generateType);
  218.         final IAUPole iauPole = PredefinedIAUPoles.getIAUPole(generateType);
  219.         final double scale;
  220.         final Frame definingFrameAlignedWithICRF;
  221.         final RawPVProvider rawPVProvider;
  222.         switch (generateType) {
  223.             case SOLAR_SYSTEM_BARYCENTER : {
  224.                 scale = -1.0;
  225.                 final JPLEphemeridesLoader parentLoader =
  226.                         new JPLEphemeridesLoader(supportedNames, EphemerisType.EARTH_MOON);
  227.                 final CelestialBody parentBody =
  228.                         parentLoader.loadCelestialBody(CelestialBodyFactory.EARTH_MOON);
  229.                 definingFrameAlignedWithICRF = parentBody.getInertiallyOrientedFrame();
  230.                 rawPVProvider = new EphemerisRawPVProvider();
  231.                 break;
  232.             }
  233.             case EARTH_MOON :
  234.                 scale         = 1.0 / (1.0 + getLoadedEarthMoonMassRatio());
  235.                 definingFrameAlignedWithICRF =  FramesFactory.getGCRF();
  236.                 rawPVProvider = new EphemerisRawPVProvider();
  237.                 break;
  238.             case EARTH :
  239.                 scale         = 1.0;
  240.                 definingFrameAlignedWithICRF = FramesFactory.getGCRF();
  241.                 rawPVProvider = new ZeroRawPVProvider();
  242.                 break;
  243.             case MOON :
  244.                 scale         =  1.0;
  245.                 definingFrameAlignedWithICRF =  FramesFactory.getGCRF();
  246.                 rawPVProvider = new EphemerisRawPVProvider();
  247.                 break;
  248.             default : {
  249.                 scale = 1.0;
  250.                 final JPLEphemeridesLoader parentLoader =
  251.                         new JPLEphemeridesLoader(supportedNames, EphemerisType.SOLAR_SYSTEM_BARYCENTER);
  252.                 final CelestialBody parentBody =
  253.                         parentLoader.loadCelestialBody(CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER);
  254.                 definingFrameAlignedWithICRF = parentBody.getInertiallyOrientedFrame();
  255.                 rawPVProvider = new EphemerisRawPVProvider();
  256.             }
  257.         }

  258.         // build the celestial body
  259.         return new JPLCelestialBody(name, supportedNames, generateType, rawPVProvider,
  260.                                     gm, scale, iauPole, definingFrameAlignedWithICRF);

  261.     }

  262.     /** Get astronomical unit.
  263.      * @return astronomical unit in meters
  264.      * @exception OrekitException if constants cannot be loaded
  265.      */
  266.     public double getLoadedAstronomicalUnit() throws OrekitException {
  267.         return 1000.0 * getLoadedConstant(CONSTANT_AU);
  268.     }

  269.     /** Get Earth/Moon mass ratio.
  270.      * @return Earth/Moon mass ratio
  271.      * @exception OrekitException if constants cannot be loaded
  272.      */
  273.     public double getLoadedEarthMoonMassRatio() throws OrekitException {
  274.         return getLoadedConstant(CONSTANT_EMRAT);
  275.     }

  276.     /** Get the gravitational coefficient of a body.
  277.      * @param body body for which the gravitational coefficient is requested
  278.      * @return gravitational coefficient in m³/s²
  279.      * @exception OrekitException if constants cannot be loaded
  280.      */
  281.     public double getLoadedGravitationalCoefficient(final EphemerisType body)
  282.         throws OrekitException {

  283.         // coefficient in au³/day²
  284.         final double rawGM;
  285.         switch (body) {
  286.             case SOLAR_SYSTEM_BARYCENTER :
  287.                 return getLoadedGravitationalCoefficient(EphemerisType.SUN)        +
  288.                         getLoadedGravitationalCoefficient(EphemerisType.MERCURY)    +
  289.                         getLoadedGravitationalCoefficient(EphemerisType.VENUS)      +
  290.                         getLoadedGravitationalCoefficient(EphemerisType.EARTH_MOON) +
  291.                         getLoadedGravitationalCoefficient(EphemerisType.MARS)       +
  292.                         getLoadedGravitationalCoefficient(EphemerisType.JUPITER)    +
  293.                         getLoadedGravitationalCoefficient(EphemerisType.SATURN)     +
  294.                         getLoadedGravitationalCoefficient(EphemerisType.URANUS)     +
  295.                         getLoadedGravitationalCoefficient(EphemerisType.NEPTUNE)    +
  296.                         getLoadedGravitationalCoefficient(EphemerisType.PLUTO);
  297.             case SUN :
  298.                 rawGM = getLoadedConstant("GMS", "GM_Sun");
  299.                 break;
  300.             case MERCURY :
  301.                 rawGM = getLoadedConstant("GM1", "GM_Mer");
  302.                 break;
  303.             case VENUS :
  304.                 rawGM = getLoadedConstant("GM2", "GM_Ven");
  305.                 break;
  306.             case EARTH_MOON :
  307.                 rawGM = getLoadedConstant("GMB", "GM_EMB");
  308.                 break;
  309.             case EARTH :
  310.                 return getLoadedEarthMoonMassRatio() *
  311.                         getLoadedGravitationalCoefficient(EphemerisType.MOON);
  312.             case MOON :
  313.                 return getLoadedGravitationalCoefficient(EphemerisType.EARTH_MOON) /
  314.                         (1.0 + getLoadedEarthMoonMassRatio());
  315.             case MARS :
  316.                 rawGM = getLoadedConstant("GM4", "GM_Mar");
  317.                 break;
  318.             case JUPITER :
  319.                 rawGM = getLoadedConstant("GM5", "GM_Jup");
  320.                 break;
  321.             case SATURN :
  322.                 rawGM = getLoadedConstant("GM6", "GM_Sat");
  323.                 break;
  324.             case URANUS :
  325.                 rawGM = getLoadedConstant("GM7", "GM_Ura");
  326.                 break;
  327.             case NEPTUNE :
  328.                 rawGM = getLoadedConstant("GM8", "GM_Nep");
  329.                 break;
  330.             case PLUTO :
  331.                 rawGM = getLoadedConstant("GM9", "GM_Plu");
  332.                 break;
  333.             default :
  334.                 throw new OrekitInternalError(null);
  335.         }

  336.         final double au    = getLoadedAstronomicalUnit();
  337.         return rawGM * au * au * au / (Constants.JULIAN_DAY * Constants.JULIAN_DAY);

  338.     }

  339.     /** Get a constant defined in the ephemerides headers.
  340.      * <p>Note that since constants are defined in the JPL headers
  341.      * files, they are available as soon as one file is available, even
  342.      * if it doesn't match the desired central date. This is because the
  343.      * header must be parsed before the dates can be checked.</p>
  344.      * <p>
  345.      * There are alternate names for constants since for example JPL names are
  346.      * different from INPOP names (Sun gravity: GMS or GM_Sun, Mars gravity:
  347.      * GM4 or GM_Mar...).
  348.      * </p>
  349.      * @param names alternate names of the constant
  350.      * @return value of the constant of NaN if the constant is not defined
  351.      * @exception OrekitException if constants cannot be loaded
  352.      */
  353.     public double getLoadedConstant(final String... names) throws OrekitException {

  354.         // lazy loading of constants
  355.         Map<String, Double> map = constants.get();
  356.         if (map == null) {
  357.             final ConstantsParser parser = new ConstantsParser();
  358.             if (!DataProvidersManager.getInstance().feed(supportedNames, parser)) {
  359.                 throw new OrekitException(OrekitMessages.NO_JPL_EPHEMERIDES_BINARY_FILES_FOUND);
  360.             }
  361.             map = parser.getConstants();
  362.             constants.compareAndSet(null, map);
  363.         }

  364.         for (final String name : names) {
  365.             if (map.containsKey(name)) {
  366.                 return map.get(name).doubleValue();
  367.             }
  368.         }

  369.         return Double.NaN;

  370.     }

  371.     /** Get the maximal chunks duration.
  372.      * @return chunks maximal duration in seconds
  373.      */
  374.     public double getMaxChunksDuration() {
  375.         return maxChunksDuration;
  376.     }

  377.     /** Parse the first header record.
  378.      * @param record first header record
  379.      * @param name name of the file (or zip entry)
  380.      * @exception OrekitException if the header is not a JPL ephemerides binary file header
  381.      */
  382.     private void parseFirstHeaderRecord(final byte[] record, final String name)
  383.         throws OrekitException {

  384.         // get the ephemerides type
  385.         final int deNum = extractInt(record, HEADER_EPHEMERIS_TYPE_OFFSET);

  386.         // as default, 3 polynomial coefficients for the Cartesian coordinates
  387.         // (x, y, z) are contained in the file, positions are in kilometers
  388.         // and times are in TDB
  389.         components   = 3;
  390.         positionUnit = 1000.0;
  391.         timeScale    = TimeScalesFactory.getTDB();

  392.         if (deNum == INPOP_DE_NUMBER) {
  393.             // an INPOP file may contain 6 components (including coefficients for the velocity vector)
  394.             final double format = getLoadedConstant("FORMAT");
  395.             if (!Double.isNaN(format) && (int) FastMath.IEEEremainder(format, 10) != 1) {
  396.                 components = 6;
  397.             }

  398.             // INPOP files may have their polynomials expressed in AU
  399.             final double unite = getLoadedConstant("UNITE");
  400.             if (!Double.isNaN(unite) && (int) unite == 0) {
  401.                 positionUnit = getLoadedAstronomicalUnit();
  402.             }

  403.             // INPOP files may have their times expressed in TCB
  404.             final double timesc = getLoadedConstant("TIMESC");
  405.             if (!Double.isNaN(timesc) && (int) timesc == 1) {
  406.                 timeScale = TimeScalesFactory.getTCB();
  407.             }

  408.         }

  409.         // extract covered date range
  410.         startEpoch = extractDate(record, HEADER_START_EPOCH_OFFSET);
  411.         finalEpoch = extractDate(record, HEADER_END_EPOCH_OFFSET);
  412.         boolean ok = finalEpoch.compareTo(startEpoch) > 0;

  413.         // indices of the Chebyshev coefficients for each ephemeris
  414.         for (int i = 0; i < 12; ++i) {
  415.             final int row1 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET     + 12 * i);
  416.             final int row2 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET + 4 + 12 * i);
  417.             final int row3 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET + 8 + 12 * i);
  418.             ok = ok && (row1 >= 0) && (row2 >= 0) && (row3 >= 0);
  419.             if (((i ==  0) && (loadType == EphemerisType.MERCURY))    ||
  420.                     ((i ==  1) && (loadType == EphemerisType.VENUS))      ||
  421.                     ((i ==  2) && (loadType == EphemerisType.EARTH_MOON)) ||
  422.                     ((i ==  3) && (loadType == EphemerisType.MARS))       ||
  423.                     ((i ==  4) && (loadType == EphemerisType.JUPITER))    ||
  424.                     ((i ==  5) && (loadType == EphemerisType.SATURN))     ||
  425.                     ((i ==  6) && (loadType == EphemerisType.URANUS))     ||
  426.                     ((i ==  7) && (loadType == EphemerisType.NEPTUNE))    ||
  427.                     ((i ==  8) && (loadType == EphemerisType.PLUTO))      ||
  428.                     ((i ==  9) && (loadType == EphemerisType.MOON))       ||
  429.                     ((i == 10) && (loadType == EphemerisType.SUN))) {
  430.                 firstIndex = row1;
  431.                 coeffs     = row2;
  432.                 chunks     = row3;
  433.             }
  434.         }

  435.         // compute chunks duration
  436.         final double timeSpan = extractDouble(record, HEADER_CHUNK_DURATION_OFFSET);
  437.         ok = ok && (timeSpan > 0) && (timeSpan < 100);
  438.         chunksDuration = Constants.JULIAN_DAY * (timeSpan / chunks);
  439.         if (Double.isNaN(maxChunksDuration)) {
  440.             maxChunksDuration = chunksDuration;
  441.         } else {
  442.             maxChunksDuration = FastMath.max(maxChunksDuration, chunksDuration);
  443.         }

  444.         // sanity checks
  445.         if (!ok) {
  446.             throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
  447.         }

  448.     }

  449.     /** Read first header record.
  450.      * @param input input stream
  451.      * @param name name of the file (or zip entry)
  452.      * @return record record where to put bytes
  453.      * @exception OrekitException if the stream does not contain a JPL ephemeris
  454.      * @exception IOException if a read error occurs
  455.      */
  456.     private byte[] readFirstRecord(final InputStream input, final String name)
  457.         throws OrekitException, IOException {

  458.         // read first part of record, up to the ephemeris type
  459.         final byte[] firstPart = new byte[HEADER_RECORD_SIZE_OFFSET + 4];
  460.         if (!readInRecord(input, firstPart, 0)) {
  461.             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
  462.         }

  463.         // detect the endian format
  464.         detectEndianess(firstPart);

  465.         // get the ephemerides type
  466.         final int deNum = extractInt(firstPart, HEADER_EPHEMERIS_TYPE_OFFSET);

  467.         // the record size for this file
  468.         int recordSize = 0;

  469.         if (deNum == INPOP_DE_NUMBER) {
  470.             // INPOP files have an extended DE format, which includes also the record size
  471.             recordSize = extractInt(firstPart, HEADER_RECORD_SIZE_OFFSET) << 3;
  472.         } else {
  473.             // compute the record size for original JPL files
  474.             recordSize = computeRecordSize(firstPart, name);
  475.         }

  476.         if (recordSize <= 0) {
  477.             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
  478.         }

  479.         // build a record with the proper size and finish read of the first complete record
  480.         final int start = firstPart.length;
  481.         final byte[] record = new byte[recordSize];
  482.         System.arraycopy(firstPart, 0, record, 0, firstPart.length);
  483.         if (!readInRecord(input, record, start)) {
  484.             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
  485.         }

  486.         return record;

  487.     }

  488.     /** Parse constants from first two header records.
  489.      * @param first first header record
  490.      * @param second second header record
  491.      * @param name name of the file (or zip entry)
  492.      * @return map of parsed constants
  493.      */
  494.     private Map<String, Double> parseConstants(final byte[] first, final byte[] second, final String name) {

  495.         final Map<String, Double> map = new HashMap<String, Double>();

  496.         for (int i = 0; i < CONSTANTS_MAX_NUMBER; ++i) {
  497.             // Note: for extracting the strings from the binary file, it makes no difference
  498.             //       if the file is stored in big-endian or little-endian notation
  499.             final String constantName = extractString(first, HEADER_CONSTANTS_NAMES_OFFSET + i * 6, 6);
  500.             if (constantName.length() == 0) {
  501.                 // no more constants to read
  502.                 break;
  503.             }
  504.             final double constantValue = extractDouble(second, HEADER_CONSTANTS_VALUES_OFFSET + 8 * i);
  505.             map.put(constantName, constantValue);
  506.         }

  507.         // INPOP files do not have constants for AU and EMRAT, thus extract them from
  508.         // the header record and create a constant for them to be consistent with JPL files
  509.         if (!map.containsKey(CONSTANT_AU)) {
  510.             map.put(CONSTANT_AU, extractDouble(first, HEADER_ASTRONOMICAL_UNIT_OFFSET));
  511.         }

  512.         if (!map.containsKey(CONSTANT_EMRAT)) {
  513.             map.put(CONSTANT_EMRAT, extractDouble(first, HEADER_EM_RATIO_OFFSET));
  514.         }

  515.         return map;

  516.     }

  517.     /** Read bytes into the current record array.
  518.      * @param input input stream
  519.      * @param record record where to put bytes
  520.      * @param start start index where to put bytes
  521.      * @return true if record has been filled up
  522.      * @exception IOException if a read error occurs
  523.      */
  524.     private boolean readInRecord(final InputStream input, final byte[] record, final int start)
  525.         throws IOException {
  526.         int index = start;
  527.         while (index != record.length) {
  528.             final int n = input.read(record, index, record.length - index);
  529.             if (n < 0) {
  530.                 return false;
  531.             }
  532.             index += n;
  533.         }
  534.         return true;
  535.     }

  536.     /** Detect whether the JPL ephemerides file is stored in big-endian or
  537.      * little-endian notation.
  538.      * @param record the array containing the binary JPL header
  539.      */
  540.     private void detectEndianess(final byte[] record) {

  541.         // default to big-endian
  542.         bigEndian = true;

  543.         // first try to read the DE number in big-endian format
  544.         // the number is stored as unsigned int, so we have to convert it properly
  545.         final long deNum = extractInt(record, HEADER_EPHEMERIS_TYPE_OFFSET) & 0xffffffffL;

  546.         // simple heuristic: if the read value is larger than half the range of an integer
  547.         //                   assume the file is in little-endian format
  548.         if (deNum > (1 << 15)) {
  549.             bigEndian = false;
  550.         }

  551.     }

  552.     /** Calculate the record size of a JPL ephemerides file.
  553.      * @param record the byte array containing the header record
  554.      * @param name the name of the data file
  555.      * @return the record size for this file
  556.      * @throws OrekitException if the file contains unexpected data
  557.      */
  558.     private int computeRecordSize(final byte[] record, final String name)
  559.         throws OrekitException {

  560.         int recordSize = 0;
  561.         boolean ok = true;
  562.         // JPL files always have 3 position components
  563.         final int nComp = 3;

  564.         // iterate over the coefficient ptr array and sum up the record size
  565.         // the coeffPtr array has the dimensions [12][nComp]
  566.         for (int j = 0; j < 12; j++) {
  567.             final int nCompCur = (j == 11) ? 2 : nComp;

  568.             // Note: the array element coeffPtr[j][0] is not needed for the calculation
  569.             final int idx = HEADER_CHEBISHEV_INDICES_OFFSET + j * nComp * 4;
  570.             final int coeffPtr1 = extractInt(record, idx + 4);
  571.             final int coeffPtr2 = extractInt(record, idx + 8);

  572.             // sanity checks
  573.             ok = ok && (coeffPtr1 >= 0 || coeffPtr2 >= 0);

  574.             recordSize += coeffPtr1 * coeffPtr2 * nCompCur;
  575.         }

  576.         // the libration ptr array has the dimension [3]
  577.         // Note: the array element libratPtr[0] is not needed for the calculation
  578.         final int libratPtr1 = extractInt(record, HEADER_LIBRATION_INDICES_OFFSET + 4);
  579.         final int libratPtr2 = extractInt(record, HEADER_LIBRATION_INDICES_OFFSET + 8);

  580.         // sanity checks
  581.         ok = ok && (libratPtr1 >= 0 || libratPtr2 >= 0);

  582.         recordSize += libratPtr1 * libratPtr2 * nComp + 2;
  583.         recordSize <<= 3;

  584.         if (!ok || recordSize <= 0) {
  585.             throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
  586.         }

  587.         return recordSize;

  588.     }

  589.     /** Extract a date from a record.
  590.      * @param record record to parse
  591.      * @param offset offset of the double within the record
  592.      * @return extracted date
  593.      */
  594.     private AbsoluteDate extractDate(final byte[] record, final int offset) {

  595.         final double t = extractDouble(record, offset);
  596.         int    jDay    = (int) FastMath.floor(t);
  597.         double seconds = (t + 0.5 - jDay) * Constants.JULIAN_DAY;
  598.         if (seconds >= Constants.JULIAN_DAY) {
  599.             ++jDay;
  600.             seconds -= Constants.JULIAN_DAY;
  601.         }
  602.         return new AbsoluteDate(new DateComponents(DateComponents.JULIAN_EPOCH, jDay),
  603.                                 new TimeComponents(seconds), timeScale);
  604.     }

  605.     /** Extract a double from a record.
  606.      * <p>Double numbers are stored according to IEEE 754 standard, with
  607.      * most significant byte first.</p>
  608.      * @param record record to parse
  609.      * @param offset offset of the double within the record
  610.      * @return extracted double
  611.      */
  612.     private double extractDouble(final byte[] record, final int offset) {
  613.         final long l8 = ((long) record[offset + 0]) & 0xffl;
  614.         final long l7 = ((long) record[offset + 1]) & 0xffl;
  615.         final long l6 = ((long) record[offset + 2]) & 0xffl;
  616.         final long l5 = ((long) record[offset + 3]) & 0xffl;
  617.         final long l4 = ((long) record[offset + 4]) & 0xffl;
  618.         final long l3 = ((long) record[offset + 5]) & 0xffl;
  619.         final long l2 = ((long) record[offset + 6]) & 0xffl;
  620.         final long l1 = ((long) record[offset + 7]) & 0xffl;
  621.         final long l;
  622.         if (bigEndian) {
  623.             l = (l8 << 56) | (l7 << 48) | (l6 << 40) | (l5 << 32) |
  624.                 (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
  625.         } else {
  626.             l = (l1 << 56) | (l2 << 48) | (l3 << 40) | (l4 << 32) |
  627.                 (l5 << 24) | (l6 << 16) | (l7 <<  8) | l8;
  628.         }
  629.         return Double.longBitsToDouble(l);
  630.     }

  631.     /** Extract an int from a record.
  632.      * @param record record to parse
  633.      * @param offset offset of the double within the record
  634.      * @return extracted int
  635.      */
  636.     private int extractInt(final byte[] record, final int offset) {
  637.         final int l4 = ((int) record[offset + 0]) & 0xff;
  638.         final int l3 = ((int) record[offset + 1]) & 0xff;
  639.         final int l2 = ((int) record[offset + 2]) & 0xff;
  640.         final int l1 = ((int) record[offset + 3]) & 0xff;

  641.         if (bigEndian) {
  642.             return (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
  643.         } else {
  644.             return (l1 << 24) | (l2 << 16) | (l3 <<  8) | l4;
  645.         }
  646.     }

  647.     /** Extract a String from a record.
  648.      * @param record record to parse
  649.      * @param offset offset of the string within the record
  650.      * @param length maximal length of the string
  651.      * @return extracted string, with whitespace characters stripped
  652.      */
  653.     private String extractString(final byte[] record, final int offset, final int length) {
  654.         try {
  655.             return new String(record, offset, length, "US-ASCII").trim();
  656.         } catch (UnsupportedEncodingException uee) {
  657.             throw new OrekitInternalError(uee);
  658.         }
  659.     }

  660.     /** Local parser for header constants. */
  661.     private class ConstantsParser implements DataLoader {

  662.         /** Local constants map. */
  663.         private Map<String, Double> localConstants;

  664.        /** Get the local constants map.
  665.          * @return local constants map
  666.          */
  667.         public Map<String, Double> getConstants() {
  668.             return localConstants;
  669.         }

  670.         /** {@inheritDoc} */
  671.         public boolean stillAcceptsData() {
  672.             return localConstants == null;
  673.         }

  674.         /** {@inheritDoc} */
  675.         public void loadData(final InputStream input, final String name)
  676.             throws IOException, ParseException, OrekitException {

  677.             // read first header record
  678.             final byte[] first = readFirstRecord(input, name);

  679.             // the second record contains the values of the constants used for least-square filtering
  680.             final byte[] second = new byte[first.length];
  681.             if (!readInRecord(input, second, 0)) {
  682.                 throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
  683.             }

  684.             localConstants = parseConstants(first, second, name);

  685.         }

  686.     }

  687.     /** Local parser for Chebyshev polynomials. */
  688.     private class EphemerisParser implements DataLoader, TimeStampedGenerator<PosVelChebyshev> {

  689.         /** Set of Chebyshev polynomials read. */
  690.         private final SortedSet<PosVelChebyshev> entries;

  691.         /** Start of range we are interested in. */
  692.         private AbsoluteDate start;

  693.         /** End of range we are interested in. */
  694.         private AbsoluteDate end;

  695.         /** Simple constructor.
  696.          */
  697.         EphemerisParser() {
  698.             entries = new TreeSet<PosVelChebyshev>(new Comparator<PosVelChebyshev>() {
  699.                 public int compare(final PosVelChebyshev o1, final PosVelChebyshev o2) {
  700.                     return o1.getDate().compareTo(o2.getDate());
  701.                 }
  702.             });
  703.         }

  704.         /** {@inheritDoc} */
  705.         public List<PosVelChebyshev> generate(final AbsoluteDate existingDate, final AbsoluteDate date)
  706.             throws TimeStampedCacheException {
  707.             try {

  708.                 // prepare reading
  709.                 entries.clear();
  710.                 if (existingDate == null) {
  711.                     // we want ephemeris data for the first time, set up an arbitrary first range
  712.                     start = date.shiftedBy(-FIFTY_DAYS);
  713.                     end   = date.shiftedBy(+FIFTY_DAYS);
  714.                 } else if (existingDate.compareTo(date) <= 0) {
  715.                     // we want to extend an existing range towards future dates
  716.                     start = existingDate;
  717.                     end   = date;
  718.                 } else {
  719.                     // we want to extend an existing range towards past dates
  720.                     start = date;
  721.                     end   = existingDate;
  722.                 }

  723.                 // get new entries in the specified data range
  724.                 if (!DataProvidersManager.getInstance().feed(supportedNames, this)) {
  725.                     throw new OrekitException(OrekitMessages.NO_JPL_EPHEMERIDES_BINARY_FILES_FOUND);
  726.                 }

  727.                 return new ArrayList<PosVelChebyshev>(entries);

  728.             } catch (OrekitException oe) {
  729.                 throw new TimeStampedCacheException(oe);
  730.             }
  731.         }

  732.         /** {@inheritDoc} */
  733.         public boolean stillAcceptsData() {

  734.             // special case for Earth: we do not really load any ephemeris data
  735.             if (generateType == EphemerisType.EARTH) {
  736.                 return false;
  737.             }

  738.             // we have to look for data in all available ephemerides files as there may be
  739.             // data overlaps that result in incomplete data
  740.             if (entries.isEmpty()) {
  741.                 return true;
  742.             } else {
  743.                 // if the requested range is already filled, we do not need to look further
  744.                 return !(entries.first().getDate().compareTo(start) < 0 &&
  745.                          entries.last().getDate().compareTo(end)    > 0);
  746.             }

  747.         }

  748.         /** {@inheritDoc} */
  749.         public void loadData(final InputStream input, final String name)
  750.             throws OrekitException, IOException {

  751.             // read first header record
  752.             final byte[] first = readFirstRecord(input, name);

  753.             // the second record contains the values of the constants used for least-square filtering
  754.             final byte[] second = new byte[first.length];
  755.             if (!readInRecord(input, second, 0)) {
  756.                 throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
  757.             }

  758.             if (constants.get() == null) {
  759.                 constants.compareAndSet(null, parseConstants(first, second, name));
  760.             }

  761.             // check astronomical unit consistency
  762.             final double au = 1000 * extractDouble(first, HEADER_ASTRONOMICAL_UNIT_OFFSET);
  763.             if ((au < 1.4e11) || (au > 1.6e11)) {
  764.                 throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
  765.             }
  766.             if (FastMath.abs(getLoadedAstronomicalUnit() - au) >= 10.0) {
  767.                 throw new OrekitException(OrekitMessages.INCONSISTENT_ASTRONOMICAL_UNIT_IN_FILES,
  768.                                           getLoadedAstronomicalUnit(), au);
  769.             }

  770.             // check Earth-Moon mass ratio consistency
  771.             final double emRat = extractDouble(first, HEADER_EM_RATIO_OFFSET);
  772.             if ((emRat < 80) || (emRat > 82)) {
  773.                 throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
  774.             }
  775.             if (FastMath.abs(getLoadedEarthMoonMassRatio() - emRat) >= 1.0e-5) {
  776.                 throw new OrekitException(OrekitMessages.INCONSISTENT_EARTH_MOON_RATIO_IN_FILES,
  777.                                           getLoadedEarthMoonMassRatio(), emRat);
  778.             }

  779.             // parse first header record
  780.             parseFirstHeaderRecord(first, name);

  781.             if (startEpoch.compareTo(end) < 0 && finalEpoch.compareTo(start) > 0) {
  782.                 // this file contains data in the range we are looking for, read it
  783.                 final byte[] record = new byte[first.length];
  784.                 while (readInRecord(input, record, 0)) {
  785.                     final AbsoluteDate rangeStart = parseDataRecord(record);
  786.                     if (rangeStart.compareTo(end) > 0) {
  787.                         // we have already exceeded the range we were interested in,
  788.                         // we interrupt parsing here
  789.                         return;
  790.                     }
  791.                 }
  792.             }

  793.         }

  794.         /** Parse regular ephemeris record.
  795.          * @param record record to parse
  796.          * @return date of the last parsed chunk
  797.          * @exception OrekitException if the header is not a JPL ephemerides binary file header
  798.          */
  799.         private AbsoluteDate parseDataRecord(final byte[] record) throws OrekitException {

  800.             // extract time range covered by the record
  801.             final AbsoluteDate rangeStart = extractDate(record, DATA_START_RANGE_OFFSET);
  802.             if (rangeStart.compareTo(startEpoch) < 0) {
  803.                 throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE, rangeStart, startEpoch, finalEpoch);
  804.             }

  805.             final AbsoluteDate rangeEnd   = extractDate(record, DATE_END_RANGE_OFFSET);
  806.             if (rangeEnd.compareTo(finalEpoch) > 0) {
  807.                 throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE, rangeEnd, startEpoch, finalEpoch);
  808.             }

  809.             if (rangeStart.compareTo(end) > 0 || rangeEnd.compareTo(start) < 0) {
  810.                 // we are not interested in this record, don't parse it
  811.                 return rangeEnd;
  812.             }

  813.             // loop over chunks inside the time range
  814.             AbsoluteDate chunkEnd = rangeStart;
  815.             final int nbChunks    = chunks;
  816.             final int nbCoeffs    = coeffs;
  817.             final int first       = firstIndex;
  818.             final double duration = chunksDuration;
  819.             for (int i = 0; i < nbChunks; ++i) {

  820.                 // set up chunk validity range
  821.                 final AbsoluteDate chunkStart = chunkEnd;
  822.                 chunkEnd = (i == nbChunks - 1) ? rangeEnd : rangeStart.shiftedBy((i + 1) * duration);

  823.                 // extract Chebyshev coefficients for the selected body
  824.                 // and convert them from kilometers to meters
  825.                 final double[] xCoeffs = new double[nbCoeffs];
  826.                 final double[] yCoeffs = new double[nbCoeffs];
  827.                 final double[] zCoeffs = new double[nbCoeffs];

  828.                 for (int k = 0; k < nbCoeffs; ++k) {
  829.                     // by now, only use the position components
  830.                     // if there are also velocity components contained in the file, ignore them
  831.                     final int index = first + components * i * nbCoeffs + k - 1;
  832.                     xCoeffs[k] = positionUnit * extractDouble(record, 8 * index);
  833.                     yCoeffs[k] = positionUnit * extractDouble(record, 8 * (index +  nbCoeffs));
  834.                     zCoeffs[k] = positionUnit * extractDouble(record, 8 * (index + 2 * nbCoeffs));
  835.                 }

  836.                 // build the position-velocity model for current chunk
  837.                 entries.add(new PosVelChebyshev(chunkStart, timeScale, duration, xCoeffs, yCoeffs, zCoeffs));

  838.             }

  839.             return rangeStart;

  840.         }

  841.     }

  842.     /** Raw position-velocity provider using ephemeris. */
  843.     private class EphemerisRawPVProvider implements RawPVProvider {

  844.         /** {@inheritDoc} */
  845.         public PVCoordinates getRawPV(final AbsoluteDate date) throws TimeStampedCacheException {

  846.             // get raw PV from Chebyshev polynomials
  847.             PosVelChebyshev chebyshev;
  848.             try {
  849.                 chebyshev = ephemerides.getNeighbors(date).findFirst().get();
  850.             } catch (TimeStampedCacheException tce) {
  851.                 // we cannot bracket the date, check if the last available chunk covers the specified date
  852.                 chebyshev = ephemerides.getLatest();
  853.                 if (!chebyshev.inRange(date)) {
  854.                     // we were not able to recover from the error, the date is too far
  855.                     throw tce;
  856.                 }
  857.             }

  858.             // evaluate the Chebyshev polynomials
  859.             return chebyshev.getPositionVelocityAcceleration(date);

  860.         }

  861.         /** {@inheritDoc} */
  862.         public <T extends RealFieldElement<T>> FieldPVCoordinates<T> getRawPV(final FieldAbsoluteDate<T> date)
  863.             throws TimeStampedCacheException {

  864.             // get raw PV from Chebyshev polynomials
  865.             PosVelChebyshev chebyshev;
  866.             try {
  867.                 chebyshev = ephemerides.getNeighbors(date.toAbsoluteDate()).findFirst().get();
  868.             } catch (TimeStampedCacheException tce) {
  869.                 // we cannot bracket the date, check if the last available chunk covers the specified date
  870.                 chebyshev = ephemerides.getLatest();
  871.                 if (!chebyshev.inRange(date.toAbsoluteDate())) {
  872.                     // we were not able to recover from the error, the date is too far
  873.                     throw tce;
  874.                 }
  875.             }

  876.             // evaluate the Chebyshev polynomials
  877.             return chebyshev.getPositionVelocityAcceleration(date);

  878.         }

  879.     }

  880.     /** Raw position-velocity provider providing always zero. */
  881.     private static class ZeroRawPVProvider implements RawPVProvider {

  882.         /** {@inheritDoc} */
  883.         public PVCoordinates getRawPV(final AbsoluteDate date) {
  884.             return PVCoordinates.ZERO;
  885.         }

  886.         /** {@inheritDoc} */
  887.         public <T extends RealFieldElement<T>> FieldPVCoordinates<T> getRawPV(final FieldAbsoluteDate<T> date) {
  888.             return FieldPVCoordinates.getZero(date.getField());
  889.         }

  890.     }

  891. }