1   /* Copyright 2002-2022 CS GROUP
2    * Licensed to CS GROUP (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  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.nio.charset.StandardCharsets;
22  import java.text.ParseException;
23  import java.util.ArrayList;
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  
31  import org.hipparchus.CalculusFieldElement;
32  import org.hipparchus.util.FastMath;
33  import org.orekit.annotation.DefaultDataContext;
34  import org.orekit.data.AbstractSelfFeedingLoader;
35  import org.orekit.data.DataContext;
36  import org.orekit.data.DataLoader;
37  import org.orekit.data.DataProvidersManager;
38  import org.orekit.errors.OrekitException;
39  import org.orekit.errors.OrekitInternalError;
40  import org.orekit.errors.OrekitMessages;
41  import org.orekit.errors.TimeStampedCacheException;
42  import org.orekit.frames.Frame;
43  import org.orekit.frames.Predefined;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.time.ChronologicalComparator;
46  import org.orekit.time.DateComponents;
47  import org.orekit.time.FieldAbsoluteDate;
48  import org.orekit.time.TimeComponents;
49  import org.orekit.time.TimeScale;
50  import org.orekit.time.TimeScales;
51  import org.orekit.utils.Constants;
52  import org.orekit.utils.FieldPVCoordinates;
53  import org.orekit.utils.OrekitConfiguration;
54  import org.orekit.utils.PVCoordinates;
55  import org.orekit.utils.GenericTimeStampedCache;
56  import org.orekit.utils.TimeStampedGenerator;
57  import org.orekit.utils.units.UnitsConverter;
58  
59  /** Loader for JPL ephemerides binary files (DE 4xx) and similar formats (INPOP 06/08/10).
60   * <p>JPL ephemerides binary files contain ephemerides for all solar system planets.</p>
61   * <p>The JPL ephemerides binary files are recognized thanks to their base names,
62   * which must match the pattern <code>[lu]nx[mp]####.ddd</code> (or
63   * <code>[lu]nx[mp]####.ddd.gz</code> for gzip-compressed files) where # stands for a
64   * digit character and where ddd is an ephemeris type (typically 405 or 406).</p>
65   * <p>The loader supports files encoded in big-endian as well as in little-endian notation.
66   * Usually, big-endian files are named <code>unx[mp]####.ddd</code>, while little-endian files
67   * are named <code>lnx[mp]####.ddd</code>.</p>
68   * <p>The IMCCE ephemerides binary files are recognized thanks to their base names,
69   * which must match the pattern <code>inpop*.dat</code> (or
70   * <code>inpop*.dat.gz</code> for gzip-compressed files) where * stands for any string.</p>
71   * <p>The loader supports files encoded in big-endian as well as in little-endian notation.
72   * Usually, big-endian files contain <code>bigendian</code> in their names, while little-endian files
73   * contain <code>littleendian</code> in their names.</p>
74   * <p>The loader supports files in TDB or TCB time scales.</p>
75   * @author Luc Maisonobe
76   */
77  public class JPLEphemeridesLoader extends AbstractSelfFeedingLoader
78          implements CelestialBodyLoader {
79  
80      /** Default supported files name pattern for JPL DE files. */
81      public static final String DEFAULT_DE_SUPPORTED_NAMES = "^[lu]nx([mp](\\d\\d\\d\\d))+\\.(?:4\\d\\d)$";
82  
83      /** Default supported files name pattern for IMCCE INPOP files. */
84      public static final String DEFAULT_INPOP_SUPPORTED_NAMES = "^inpop.*\\.dat$";
85  
86      /** 50 days in seconds. */
87      private static final double FIFTY_DAYS = 50 * Constants.JULIAN_DAY;
88  
89      /** DE number used by INPOP files. */
90      private static final int INPOP_DE_NUMBER = 100;
91  
92      /** Maximal number of constants in headers. */
93      private static final int CONSTANTS_MAX_NUMBER           = 400;
94  
95      /** Offset of the ephemeris type in first header record. */
96      private static final int HEADER_EPHEMERIS_TYPE_OFFSET   = 2840;
97  
98      /** Offset of the record size (for INPOP files) in first header record. */
99      private static final int HEADER_RECORD_SIZE_OFFSET      = 2856;
100 
101     /** Offset of the start epoch in first header record. */
102     private static final int HEADER_START_EPOCH_OFFSET      = 2652;
103 
104     /** Offset of the end epoch in first header record. */
105     private static final int HEADER_END_EPOCH_OFFSET        = 2660;
106 
107     /** Offset of the astronomical unit in first header record. */
108     private static final int HEADER_ASTRONOMICAL_UNIT_OFFSET = 2680;
109 
110     /** Offset of the Earth-Moon mass ratio in first header record. */
111     private static final int HEADER_EM_RATIO_OFFSET         = 2688;
112 
113     /** Offset of Chebishev coefficients indices in first header record. */
114     private static final int HEADER_CHEBISHEV_INDICES_OFFSET = 2696;
115 
116     /** Offset of libration coefficients indices in first header record. */
117     private static final int HEADER_LIBRATION_INDICES_OFFSET = 2844;
118 
119     /** Offset of chunks duration in first header record. */
120     private static final int HEADER_CHUNK_DURATION_OFFSET    = 2668;
121 
122     /** Offset of the constants names in first header record. */
123     private static final int HEADER_CONSTANTS_NAMES_OFFSET  = 252;
124 
125     /** Offset of the constants values in second header record. */
126     private static final int HEADER_CONSTANTS_VALUES_OFFSET = 0;
127 
128     /** Offset of the range start in the data records. */
129     private static final int DATA_START_RANGE_OFFSET        = 0;
130 
131     /** Offset of the range end in the data records. */
132     private static final int DATE_END_RANGE_OFFSET          = 8;
133 
134     /** The constant name for the astronomical unit. */
135     private static final String CONSTANT_AU = "AU";
136 
137     /** The constant name for the earth-moon mass ratio. */
138     private static final String CONSTANT_EMRAT = "EMRAT";
139 
140     /** List of supported ephemerides types. */
141     public enum EphemerisType {
142 
143         /** Constant for solar system barycenter. */
144         SOLAR_SYSTEM_BARYCENTER,
145 
146         /** Constant for the Sun. */
147         SUN,
148 
149         /** Constant for Mercury. */
150         MERCURY,
151 
152         /** Constant for Venus. */
153         VENUS,
154 
155         /** Constant for the Earth-Moon barycenter. */
156         EARTH_MOON,
157 
158         /** Constant for the Earth. */
159         EARTH,
160 
161         /** Constant for the Moon. */
162         MOON,
163 
164         /** Constant for Mars. */
165         MARS,
166 
167         /** Constant for Jupiter. */
168         JUPITER,
169 
170         /** Constant for Saturn. */
171         SATURN,
172 
173         /** Constant for Uranus. */
174         URANUS,
175 
176         /** Constant for Neptune. */
177         NEPTUNE,
178 
179         /** Constant for Pluto. */
180         PLUTO
181 
182     }
183 
184     /** Interface for raw position-velocity retrieval. */
185     public interface RawPVProvider {
186 
187         /** Get the position-velocity at date.
188          * @param date date at which the position-velocity is desired
189          * @return position-velocity at the specified date
190          */
191         PVCoordinates getRawPV(AbsoluteDate date);
192 
193         /** Get the position-velocity at date.
194          * @param date date at which the position-velocity is desired
195          * @param <T> type of the field elements
196          * @return position-velocity at the specified date
197          */
198         <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> getRawPV(FieldAbsoluteDate<T> date);
199 
200     }
201 
202     /** Ephemeris for selected body. */
203     private final GenericTimeStampedCache<PosVelChebyshev> ephemerides;
204 
205     /** Constants defined in the file. */
206     private final AtomicReference<Map<String, Double>> constants;
207 
208     /** Ephemeris type to generate. */
209     private final EphemerisType generateType;
210 
211     /** Ephemeris type to load. */
212     private final EphemerisType loadType;
213 
214     /** Time scales to use when loading data. */
215     private final TimeScales timeScales;
216 
217     /** The GCRF implementation. */
218     private final Frame gcrf;
219 
220     /** Current file start epoch. */
221     private AbsoluteDate startEpoch;
222 
223     /** Current file final epoch. */
224     private AbsoluteDate finalEpoch;
225 
226     /** Chunks duration (in seconds). */
227     private double maxChunksDuration;
228 
229     /** Current file chunks duration (in seconds). */
230     private double chunksDuration;
231 
232     /** Index of the first data for selected body. */
233     private int firstIndex;
234 
235     /** Number of coefficients for selected body. */
236     private int coeffs;
237 
238     /** Number of chunks for the selected body. */
239     private int chunks;
240 
241     /** Number of components contained in the file. */
242     private int components;
243 
244     /** Unit of the position coordinates (as a multiple of meters). */
245     private double positionUnit;
246 
247     /** Time scale of the date coordinates. */
248     private TimeScale timeScale;
249 
250     /** Indicator for binary file endianness. */
251     private boolean bigEndian;
252 
253     /** Create a loader for JPL ephemerides binary files. This constructor uses the {@link
254      * DataContext#getDefault() default data context}.
255      *
256      * @param supportedNames regular expression for supported files names
257      * @param generateType ephemeris type to generate
258      * @see #JPLEphemeridesLoader(String, EphemerisType, DataProvidersManager, TimeScales,
259      * Frame)
260      */
261     @DefaultDataContext
262     public JPLEphemeridesLoader(final String supportedNames, final EphemerisType generateType) {
263         this(supportedNames, generateType,
264                 DataContext.getDefault().getDataProvidersManager(),
265                 DataContext.getDefault().getTimeScales(),
266                 DataContext.getDefault().getFrames().getGCRF());
267     }
268 
269     /** Create a loader for JPL ephemerides binary files.
270      * @param supportedNames regular expression for supported files names
271      * @param generateType ephemeris type to generate
272      * @param dataProvidersManager provides access to the ephemeris files.
273      * @param timeScales used to access the TCB and TDB time scales while loading data.
274      * @param gcrf Earth centered frame aligned with ICRF.
275      * @since 10.1
276      */
277     public JPLEphemeridesLoader(final String supportedNames,
278                                 final EphemerisType generateType,
279                                 final DataProvidersManager dataProvidersManager,
280                                 final TimeScales timeScales,
281                                 final Frame gcrf) {
282         super(supportedNames, dataProvidersManager);
283 
284         this.timeScales = timeScales;
285         this.gcrf = gcrf;
286         constants = new AtomicReference<>();
287 
288         this.generateType  = generateType;
289         if (generateType == EphemerisType.SOLAR_SYSTEM_BARYCENTER) {
290             loadType = EphemerisType.EARTH_MOON;
291         } else if (generateType == EphemerisType.EARTH_MOON) {
292             loadType = EphemerisType.MOON;
293         } else {
294             loadType = generateType;
295         }
296 
297         ephemerides = new GenericTimeStampedCache<>(
298                 2, OrekitConfiguration.getCacheSlotsNumber(),
299                 Double.POSITIVE_INFINITY, FIFTY_DAYS,
300                 new EphemerisParser());
301         maxChunksDuration = Double.NaN;
302         chunksDuration    = Double.NaN;
303 
304     }
305 
306     /** Load celestial body.
307      * @param name name of the celestial body
308      * @return loaded celestial body
309      */
310     public CelestialBody loadCelestialBody(final String name) {
311 
312         final double gm       = getLoadedGravitationalCoefficient(generateType);
313         final IAUPole iauPole = PredefinedIAUPoles
314                 .getIAUPole(generateType, timeScales);
315         final double scale;
316         final Frame definingFrameAlignedWithICRF;
317         final RawPVProvider rawPVProvider;
318         String inertialFrameName = null;
319         String bodyOrientedFrameName = null;
320         switch (generateType) {
321             case SOLAR_SYSTEM_BARYCENTER : {
322                 scale = -1.0;
323                 final JPLEphemeridesLoader parentLoader = new JPLEphemeridesLoader(
324                         getSupportedNames(),
325                         EphemerisType.EARTH_MOON,
326                         getDataProvidersManager(),
327                         timeScales,
328                         gcrf);
329                 final CelestialBody parentBody =
330                         parentLoader.loadCelestialBody(CelestialBodyFactory.EARTH_MOON);
331                 definingFrameAlignedWithICRF = parentBody.getInertiallyOrientedFrame();
332                 rawPVProvider = new EphemerisRawPVProvider();
333                 inertialFrameName = Predefined.ICRF.getName();
334                 bodyOrientedFrameName = null;
335                 break;
336             }
337             case EARTH_MOON :
338                 scale         = 1.0 / (1.0 + getLoadedEarthMoonMassRatio());
339                 definingFrameAlignedWithICRF = gcrf;
340                 rawPVProvider = new EphemerisRawPVProvider();
341                 break;
342             case EARTH :
343                 scale         = 1.0;
344                 definingFrameAlignedWithICRF = gcrf;
345                 rawPVProvider = new ZeroRawPVProvider();
346                 break;
347             case MOON :
348                 scale         =  1.0;
349                 definingFrameAlignedWithICRF = gcrf;
350                 rawPVProvider = new EphemerisRawPVProvider();
351                 break;
352             default : {
353                 scale = 1.0;
354                 final JPLEphemeridesLoader parentLoader = new JPLEphemeridesLoader(
355                         getSupportedNames(),
356                         EphemerisType.SOLAR_SYSTEM_BARYCENTER,
357                         getDataProvidersManager(),
358                         timeScales,
359                         gcrf);
360                 final CelestialBody parentBody =
361                         parentLoader.loadCelestialBody(CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER);
362                 definingFrameAlignedWithICRF = parentBody.getInertiallyOrientedFrame();
363                 rawPVProvider = new EphemerisRawPVProvider();
364             }
365         }
366 
367         // build the celestial body
368         return new JPLCelestialBody(name, getSupportedNames(), generateType, rawPVProvider,
369                                     gm, scale, iauPole, definingFrameAlignedWithICRF,
370                                     inertialFrameName, bodyOrientedFrameName);
371 
372     }
373 
374     /** Get astronomical unit.
375      * @return astronomical unit in meters
376      */
377     public double getLoadedAstronomicalUnit() {
378         return UnitsConverter.KILOMETRES_TO_METRES.convert(getLoadedConstant(CONSTANT_AU));
379     }
380 
381     /** Get Earth/Moon mass ratio.
382      * @return Earth/Moon mass ratio
383      */
384     public double getLoadedEarthMoonMassRatio() {
385         return getLoadedConstant(CONSTANT_EMRAT);
386     }
387 
388     /** Get the gravitational coefficient of a body.
389      * @param body body for which the gravitational coefficient is requested
390      * @return gravitational coefficient in m³/s²
391      */
392     public double getLoadedGravitationalCoefficient(final EphemerisType body) {
393 
394         // coefficient in au³/day²
395         final double rawGM;
396         switch (body) {
397             case SOLAR_SYSTEM_BARYCENTER :
398                 return getLoadedGravitationalCoefficient(EphemerisType.SUN)        +
399                         getLoadedGravitationalCoefficient(EphemerisType.MERCURY)    +
400                         getLoadedGravitationalCoefficient(EphemerisType.VENUS)      +
401                         getLoadedGravitationalCoefficient(EphemerisType.EARTH_MOON) +
402                         getLoadedGravitationalCoefficient(EphemerisType.MARS)       +
403                         getLoadedGravitationalCoefficient(EphemerisType.JUPITER)    +
404                         getLoadedGravitationalCoefficient(EphemerisType.SATURN)     +
405                         getLoadedGravitationalCoefficient(EphemerisType.URANUS)     +
406                         getLoadedGravitationalCoefficient(EphemerisType.NEPTUNE)    +
407                         getLoadedGravitationalCoefficient(EphemerisType.PLUTO);
408             case SUN :
409                 rawGM = getLoadedConstant("GMS", "GM_Sun");
410                 break;
411             case MERCURY :
412                 rawGM = getLoadedConstant("GM1", "GM_Mer");
413                 break;
414             case VENUS :
415                 rawGM = getLoadedConstant("GM2", "GM_Ven");
416                 break;
417             case EARTH_MOON :
418                 rawGM = getLoadedConstant("GMB", "GM_EMB");
419                 break;
420             case EARTH :
421                 return getLoadedEarthMoonMassRatio() *
422                         getLoadedGravitationalCoefficient(EphemerisType.MOON);
423             case MOON :
424                 return getLoadedGravitationalCoefficient(EphemerisType.EARTH_MOON) /
425                         (1.0 + getLoadedEarthMoonMassRatio());
426             case MARS :
427                 rawGM = getLoadedConstant("GM4", "GM_Mar");
428                 break;
429             case JUPITER :
430                 rawGM = getLoadedConstant("GM5", "GM_Jup");
431                 break;
432             case SATURN :
433                 rawGM = getLoadedConstant("GM6", "GM_Sat");
434                 break;
435             case URANUS :
436                 rawGM = getLoadedConstant("GM7", "GM_Ura");
437                 break;
438             case NEPTUNE :
439                 rawGM = getLoadedConstant("GM8", "GM_Nep");
440                 break;
441             case PLUTO :
442                 rawGM = getLoadedConstant("GM9", "GM_Plu");
443                 break;
444             default :
445                 throw new OrekitInternalError(null);
446         }
447 
448         final double au    = getLoadedAstronomicalUnit();
449         return rawGM * au * au * au / (Constants.JULIAN_DAY * Constants.JULIAN_DAY);
450 
451     }
452 
453     /** Get a constant defined in the ephemerides headers.
454      * <p>Note that since constants are defined in the JPL headers
455      * files, they are available as soon as one file is available, even
456      * if it doesn't match the desired central date. This is because the
457      * header must be parsed before the dates can be checked.</p>
458      * <p>
459      * There are alternate names for constants since for example JPL names are
460      * different from INPOP names (Sun gravity: GMS or GM_Sun, Mars gravity:
461      * GM4 or GM_Mar...).
462      * </p>
463      * @param names alternate names of the constant
464      * @return value of the constant of NaN if the constant is not defined
465      */
466     public double getLoadedConstant(final String... names) {
467 
468         // lazy loading of constants
469         Map<String, Double> map = constants.get();
470         if (map == null) {
471             final ConstantsParser parser = new ConstantsParser();
472             if (!feed(parser)) {
473                 throw new OrekitException(OrekitMessages.NO_JPL_EPHEMERIDES_BINARY_FILES_FOUND);
474             }
475             map = parser.getConstants();
476             constants.compareAndSet(null, map);
477         }
478 
479         for (final String name : names) {
480             if (map.containsKey(name)) {
481                 return map.get(name);
482             }
483         }
484 
485         return Double.NaN;
486 
487     }
488 
489     /** Get the maximal chunks duration.
490      * @return chunks maximal duration in seconds
491      */
492     public double getMaxChunksDuration() {
493         return maxChunksDuration;
494     }
495 
496     /** Parse the first header record.
497      * @param record first header record
498      * @param name name of the file (or zip entry)
499      */
500     private void parseFirstHeaderRecord(final byte[] record, final String name) {
501 
502         // get the ephemerides type
503         final int deNum = extractInt(record, HEADER_EPHEMERIS_TYPE_OFFSET);
504 
505         // as default, 3 polynomial coefficients for the Cartesian coordinates
506         // (x, y, z) are contained in the file, positions are in kilometers
507         // and times are in TDB
508         components   = 3;
509         positionUnit = UnitsConverter.KILOMETRES_TO_METRES.convert(1.0);
510         timeScale    = timeScales.getTDB();
511 
512         if (deNum == INPOP_DE_NUMBER) {
513             // an INPOP file may contain 6 components (including coefficients for the velocity vector)
514             final double format = getLoadedConstant("FORMAT");
515             if (!Double.isNaN(format) && (int) FastMath.IEEEremainder(format, 10) != 1) {
516                 components = 6;
517             }
518 
519             // INPOP files may have their polynomials expressed in AU
520             final double unite = getLoadedConstant("UNITE");
521             if (!Double.isNaN(unite) && (int) unite == 0) {
522                 positionUnit = getLoadedAstronomicalUnit();
523             }
524 
525             // INPOP files may have their times expressed in TCB
526             final double timesc = getLoadedConstant("TIMESC");
527             if (!Double.isNaN(timesc) && (int) timesc == 1) {
528                 timeScale = timeScales.getTCB();
529             }
530 
531         }
532 
533         // extract covered date range
534         startEpoch = extractDate(record, HEADER_START_EPOCH_OFFSET);
535         finalEpoch = extractDate(record, HEADER_END_EPOCH_OFFSET);
536         boolean ok = finalEpoch.compareTo(startEpoch) > 0;
537 
538         // indices of the Chebyshev coefficients for each ephemeris
539         for (int i = 0; i < 12; ++i) {
540             final int row1 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET     + 12 * i);
541             final int row2 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET + 4 + 12 * i);
542             final int row3 = extractInt(record, HEADER_CHEBISHEV_INDICES_OFFSET + 8 + 12 * i);
543             ok = ok && row1 >= 0 && row2 >= 0 && row3 >= 0;
544             if (i ==  0 && loadType == EphemerisType.MERCURY    ||
545                     i ==  1 && loadType == EphemerisType.VENUS      ||
546                     i ==  2 && loadType == EphemerisType.EARTH_MOON ||
547                     i ==  3 && loadType == EphemerisType.MARS       ||
548                     i ==  4 && loadType == EphemerisType.JUPITER    ||
549                     i ==  5 && loadType == EphemerisType.SATURN     ||
550                     i ==  6 && loadType == EphemerisType.URANUS     ||
551                     i ==  7 && loadType == EphemerisType.NEPTUNE    ||
552                     i ==  8 && loadType == EphemerisType.PLUTO      ||
553                     i ==  9 && loadType == EphemerisType.MOON       ||
554                     i == 10 && loadType == EphemerisType.SUN) {
555                 firstIndex = row1;
556                 coeffs     = row2;
557                 chunks     = row3;
558             }
559         }
560 
561         // compute chunks duration
562         final double timeSpan = extractDouble(record, HEADER_CHUNK_DURATION_OFFSET);
563         ok = ok && timeSpan > 0 && timeSpan < 100;
564         chunksDuration = Constants.JULIAN_DAY * (timeSpan / chunks);
565         if (Double.isNaN(maxChunksDuration)) {
566             maxChunksDuration = chunksDuration;
567         } else {
568             maxChunksDuration = FastMath.max(maxChunksDuration, chunksDuration);
569         }
570 
571         // sanity checks
572         if (!ok) {
573             throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
574         }
575 
576     }
577 
578     /** Read first header record.
579      * @param input input stream
580      * @param name name of the file (or zip entry)
581      * @return record record where to put bytes
582      * @exception IOException if a read error occurs
583      */
584     private byte[] readFirstRecord(final InputStream input, final String name)
585         throws IOException {
586 
587         // read first part of record, up to the ephemeris type
588         final byte[] firstPart = new byte[HEADER_RECORD_SIZE_OFFSET + 4];
589         if (!readInRecord(input, firstPart, 0)) {
590             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
591         }
592 
593         // detect the endian format
594         detectEndianess(firstPart);
595 
596         // get the ephemerides type
597         final int deNum = extractInt(firstPart, HEADER_EPHEMERIS_TYPE_OFFSET);
598 
599         // the record size for this file
600         final int recordSize;
601 
602         if (deNum == INPOP_DE_NUMBER) {
603             // INPOP files have an extended DE format, which includes also the record size
604             recordSize = extractInt(firstPart, HEADER_RECORD_SIZE_OFFSET) << 3;
605         } else {
606             // compute the record size for original JPL files
607             recordSize = computeRecordSize(firstPart, name);
608         }
609 
610         if (recordSize <= 0) {
611             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
612         }
613 
614         // build a record with the proper size and finish read of the first complete record
615         final int start = firstPart.length;
616         final byte[] record = new byte[recordSize];
617         System.arraycopy(firstPart, 0, record, 0, firstPart.length);
618         if (!readInRecord(input, record, start)) {
619             throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
620         }
621 
622         return record;
623 
624     }
625 
626     /** Parse constants from first two header records.
627      * @param first first header record
628      * @param second second header record
629      * @return map of parsed constants
630      */
631     private Map<String, Double> parseConstants(final byte[] first, final byte[] second) {
632 
633         final Map<String, Double> map = new HashMap<>();
634 
635         for (int i = 0; i < CONSTANTS_MAX_NUMBER; ++i) {
636             // Note: for extracting the strings from the binary file, it makes no difference
637             //       if the file is stored in big-endian or little-endian notation
638             final String constantName = extractString(first, HEADER_CONSTANTS_NAMES_OFFSET + i * 6, 6);
639             if (constantName.length() == 0) {
640                 // no more constants to read
641                 break;
642             }
643             final double constantValue = extractDouble(second, HEADER_CONSTANTS_VALUES_OFFSET + 8 * i);
644             map.put(constantName, constantValue);
645         }
646 
647         // INPOP files do not have constants for AU and EMRAT, thus extract them from
648         // the header record and create a constant for them to be consistent with JPL files
649         if (!map.containsKey(CONSTANT_AU)) {
650             map.put(CONSTANT_AU, extractDouble(first, HEADER_ASTRONOMICAL_UNIT_OFFSET));
651         }
652 
653         if (!map.containsKey(CONSTANT_EMRAT)) {
654             map.put(CONSTANT_EMRAT, extractDouble(first, HEADER_EM_RATIO_OFFSET));
655         }
656 
657         return map;
658 
659     }
660 
661     /** Read bytes into the current record array.
662      * @param input input stream
663      * @param record record where to put bytes
664      * @param start start index where to put bytes
665      * @return true if record has been filled up
666      * @exception IOException if a read error occurs
667      */
668     private boolean readInRecord(final InputStream input, final byte[] record, final int start)
669         throws IOException {
670         int index = start;
671         while (index != record.length) {
672             final int n = input.read(record, index, record.length - index);
673             if (n < 0) {
674                 return false;
675             }
676             index += n;
677         }
678         return true;
679     }
680 
681     /** Detect whether the JPL ephemerides file is stored in big-endian or
682      * little-endian notation.
683      * @param record the array containing the binary JPL header
684      */
685     private void detectEndianess(final byte[] record) {
686 
687         // default to big-endian
688         bigEndian = true;
689 
690         // first try to read the DE number in big-endian format
691         // the number is stored as unsigned int, so we have to convert it properly
692         final long deNum = extractInt(record, HEADER_EPHEMERIS_TYPE_OFFSET) & 0xffffffffL;
693 
694         // simple heuristic: if the read value is larger than half the range of an integer
695         //                   assume the file is in little-endian format
696         if (deNum > (1 << 15)) {
697             bigEndian = false;
698         }
699 
700     }
701 
702     /** Calculate the record size of a JPL ephemerides file.
703      * @param record the byte array containing the header record
704      * @param name the name of the data file
705      * @return the record size for this file
706      */
707     private int computeRecordSize(final byte[] record, final String name) {
708 
709         int recordSize = 0;
710         boolean ok = true;
711         // JPL files always have 3 position components
712         final int nComp = 3;
713 
714         // iterate over the coefficient ptr array and sum up the record size
715         // the coeffPtr array has the dimensions [12][nComp]
716         for (int j = 0; j < 12; j++) {
717             final int nCompCur = (j == 11) ? 2 : nComp;
718 
719             // Note: the array element coeffPtr[j][0] is not needed for the calculation
720             final int idx = HEADER_CHEBISHEV_INDICES_OFFSET + j * nComp * 4;
721             final int coeffPtr1 = extractInt(record, idx + 4);
722             final int coeffPtr2 = extractInt(record, idx + 8);
723 
724             // sanity checks
725             ok = ok && (coeffPtr1 >= 0 || coeffPtr2 >= 0);
726 
727             recordSize += coeffPtr1 * coeffPtr2 * nCompCur;
728         }
729 
730         // the libration ptr array has the dimension [3]
731         // Note: the array element libratPtr[0] is not needed for the calculation
732         final int libratPtr1 = extractInt(record, HEADER_LIBRATION_INDICES_OFFSET + 4);
733         final int libratPtr2 = extractInt(record, HEADER_LIBRATION_INDICES_OFFSET + 8);
734 
735         // sanity checks
736         ok = ok && (libratPtr1 >= 0 || libratPtr2 >= 0);
737 
738         recordSize += libratPtr1 * libratPtr2 * nComp + 2;
739         recordSize <<= 3;
740 
741         if (!ok || recordSize <= 0) {
742             throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
743         }
744 
745         return recordSize;
746 
747     }
748 
749     /** Extract a date from a record.
750      * @param record record to parse
751      * @param offset offset of the double within the record
752      * @return extracted date
753      */
754     private AbsoluteDate extractDate(final byte[] record, final int offset) {
755 
756         final double t = extractDouble(record, offset);
757         int    jDay    = (int) FastMath.floor(t);
758         double seconds = (t + 0.5 - jDay) * Constants.JULIAN_DAY;
759         if (seconds >= Constants.JULIAN_DAY) {
760             ++jDay;
761             seconds -= Constants.JULIAN_DAY;
762         }
763         return new AbsoluteDate(new DateComponents(DateComponents.JULIAN_EPOCH, jDay),
764                                 new TimeComponents(seconds), timeScale);
765     }
766 
767     /** Extract a double from a record.
768      * <p>Double numbers are stored according to IEEE 754 standard, with
769      * most significant byte first.</p>
770      * @param record record to parse
771      * @param offset offset of the double within the record
772      * @return extracted double
773      */
774     private double extractDouble(final byte[] record, final int offset) {
775         final long l8 = ((long) record[offset + 0]) & 0xffl;
776         final long l7 = ((long) record[offset + 1]) & 0xffl;
777         final long l6 = ((long) record[offset + 2]) & 0xffl;
778         final long l5 = ((long) record[offset + 3]) & 0xffl;
779         final long l4 = ((long) record[offset + 4]) & 0xffl;
780         final long l3 = ((long) record[offset + 5]) & 0xffl;
781         final long l2 = ((long) record[offset + 6]) & 0xffl;
782         final long l1 = ((long) record[offset + 7]) & 0xffl;
783         final long l;
784         if (bigEndian) {
785             l = (l8 << 56) | (l7 << 48) | (l6 << 40) | (l5 << 32) |
786                 (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
787         } else {
788             l = (l1 << 56) | (l2 << 48) | (l3 << 40) | (l4 << 32) |
789                 (l5 << 24) | (l6 << 16) | (l7 <<  8) | l8;
790         }
791         return Double.longBitsToDouble(l);
792     }
793 
794     /** Extract an int from a record.
795      * @param record record to parse
796      * @param offset offset of the double within the record
797      * @return extracted int
798      */
799     private int extractInt(final byte[] record, final int offset) {
800         final int l4 = ((int) record[offset + 0]) & 0xff;
801         final int l3 = ((int) record[offset + 1]) & 0xff;
802         final int l2 = ((int) record[offset + 2]) & 0xff;
803         final int l1 = ((int) record[offset + 3]) & 0xff;
804 
805         if (bigEndian) {
806             return (l4 << 24) | (l3 << 16) | (l2 <<  8) | l1;
807         } else {
808             return (l1 << 24) | (l2 << 16) | (l3 <<  8) | l4;
809         }
810     }
811 
812     /** Extract a String from a record.
813      * @param record record to parse
814      * @param offset offset of the string within the record
815      * @param length maximal length of the string
816      * @return extracted string, with whitespace characters stripped
817      */
818     private String extractString(final byte[] record, final int offset, final int length) {
819         return new String(record, offset, length, StandardCharsets.US_ASCII).trim();
820     }
821 
822     /** Local parser for header constants. */
823     private class ConstantsParser implements DataLoader {
824 
825         /** Local constants map. */
826         private Map<String, Double> localConstants;
827 
828        /** Get the local constants map.
829          * @return local constants map
830          */
831         public Map<String, Double> getConstants() {
832             return localConstants;
833         }
834 
835         /** {@inheritDoc} */
836         public boolean stillAcceptsData() {
837             return localConstants == null;
838         }
839 
840         /** {@inheritDoc} */
841         public void loadData(final InputStream input, final String name)
842             throws IOException, ParseException, OrekitException {
843 
844             // read first header record
845             final byte[] first = readFirstRecord(input, name);
846 
847             // the second record contains the values of the constants used for least-square filtering
848             final byte[] second = new byte[first.length];
849             if (!readInRecord(input, second, 0)) {
850                 throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
851             }
852 
853             localConstants = parseConstants(first, second);
854 
855         }
856 
857     }
858 
859     /** Local parser for Chebyshev polynomials. */
860     private class EphemerisParser implements DataLoader, TimeStampedGenerator<PosVelChebyshev> {
861 
862         /** Set of Chebyshev polynomials read. */
863         private final SortedSet<PosVelChebyshev> entries;
864 
865         /** Start of range we are interested in. */
866         private AbsoluteDate start;
867 
868         /** End of range we are interested in. */
869         private AbsoluteDate end;
870 
871         /** Simple constructor.
872          */
873         EphemerisParser() {
874             entries = new TreeSet<>(new ChronologicalComparator());
875         }
876 
877         /** {@inheritDoc} */
878         public List<PosVelChebyshev> generate(final AbsoluteDate existingDate, final AbsoluteDate date) {
879             try {
880 
881                 // prepare reading
882                 entries.clear();
883                 if (existingDate == null) {
884                     // we want ephemeris data for the first time, set up an arbitrary first range
885                     start = date.shiftedBy(-FIFTY_DAYS);
886                     end   = date.shiftedBy(+FIFTY_DAYS);
887                 } else if (existingDate.compareTo(date) <= 0) {
888                     // we want to extend an existing range towards future dates
889                     start = existingDate;
890                     end   = date;
891                 } else {
892                     // we want to extend an existing range towards past dates
893                     start = date;
894                     end   = existingDate;
895                 }
896 
897                 // get new entries in the specified data range
898                 if (!feed(this)) {
899                     throw new OrekitException(OrekitMessages.NO_JPL_EPHEMERIDES_BINARY_FILES_FOUND);
900                 }
901 
902                 return new ArrayList<>(entries);
903 
904             } catch (OrekitException oe) {
905                 throw new TimeStampedCacheException(oe);
906             }
907         }
908 
909         /** {@inheritDoc} */
910         public boolean stillAcceptsData() {
911 
912             // special case for Earth: we do not really load any ephemeris data
913             if (generateType == EphemerisType.EARTH) {
914                 return false;
915             }
916 
917             // we have to look for data in all available ephemerides files as there may be
918             // data overlaps that result in incomplete data
919             if (entries.isEmpty()) {
920                 return true;
921             } else {
922                 // if the requested range is already filled, we do not need to look further
923                 return !(entries.first().getDate().compareTo(start) < 0 &&
924                          entries.last().getDate().compareTo(end)    > 0);
925             }
926 
927         }
928 
929         /** {@inheritDoc} */
930         public void loadData(final InputStream input, final String name)
931             throws IOException {
932 
933             // read first header record
934             final byte[] first = readFirstRecord(input, name);
935 
936             // the second record contains the values of the constants used for least-square filtering
937             final byte[] second = new byte[first.length];
938             if (!readInRecord(input, second, 0)) {
939                 throw new OrekitException(OrekitMessages.UNABLE_TO_READ_JPL_HEADER, name);
940             }
941 
942             if (constants.get() == null) {
943                 constants.compareAndSet(null, parseConstants(first, second));
944             }
945 
946             // check astronomical unit consistency
947             final double au = 1000 * extractDouble(first, HEADER_ASTRONOMICAL_UNIT_OFFSET);
948             if (au < 1.4e11 || au > 1.6e11) {
949                 throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
950             }
951             if (FastMath.abs(getLoadedAstronomicalUnit() - au) >= 10.0) {
952                 throw new OrekitException(OrekitMessages.INCONSISTENT_ASTRONOMICAL_UNIT_IN_FILES,
953                                           getLoadedAstronomicalUnit(), au);
954             }
955 
956             // check Earth-Moon mass ratio consistency
957             final double emRat = extractDouble(first, HEADER_EM_RATIO_OFFSET);
958             if (emRat < 80 || emRat > 82) {
959                 throw new OrekitException(OrekitMessages.NOT_A_JPL_EPHEMERIDES_BINARY_FILE, name);
960             }
961             if (FastMath.abs(getLoadedEarthMoonMassRatio() - emRat) >= 1.0e-5) {
962                 throw new OrekitException(OrekitMessages.INCONSISTENT_EARTH_MOON_RATIO_IN_FILES,
963                                           getLoadedEarthMoonMassRatio(), emRat);
964             }
965 
966             // parse first header record
967             parseFirstHeaderRecord(first, name);
968 
969             if (startEpoch.compareTo(end) < 0 && finalEpoch.compareTo(start) > 0) {
970                 // this file contains data in the range we are looking for, read it
971                 final byte[] record = new byte[first.length];
972                 while (readInRecord(input, record, 0)) {
973                     final AbsoluteDate rangeStart = parseDataRecord(record);
974                     if (rangeStart.compareTo(end) > 0) {
975                         // we have already exceeded the range we were interested in,
976                         // we interrupt parsing here
977                         return;
978                     }
979                 }
980             }
981 
982         }
983 
984         /** Parse regular ephemeris record.
985          * @param record record to parse
986          * @return date of the last parsed chunk
987          */
988         private AbsoluteDate parseDataRecord(final byte[] record) {
989 
990             // extract time range covered by the record
991             final AbsoluteDate rangeStart = extractDate(record, DATA_START_RANGE_OFFSET);
992             if (rangeStart.compareTo(startEpoch) < 0) {
993                 throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE_BEFORE,
994                         rangeStart, startEpoch, finalEpoch, startEpoch.durationFrom(rangeStart));
995             }
996 
997             final AbsoluteDate rangeEnd   = extractDate(record, DATE_END_RANGE_OFFSET);
998             if (rangeEnd.compareTo(finalEpoch) > 0) {
999                 throw new OrekitException(OrekitMessages.OUT_OF_RANGE_EPHEMERIDES_DATE_AFTER,
1000                         rangeEnd, startEpoch, finalEpoch, rangeEnd.durationFrom(finalEpoch));
1001             }
1002 
1003             if (rangeStart.compareTo(end) > 0 || rangeEnd.compareTo(start) < 0) {
1004                 // we are not interested in this record, don't parse it
1005                 return rangeEnd;
1006             }
1007 
1008             // loop over chunks inside the time range
1009             AbsoluteDate chunkEnd = rangeStart;
1010             final int nbChunks    = chunks;
1011             final int nbCoeffs    = coeffs;
1012             final int first       = firstIndex;
1013             final double duration = chunksDuration;
1014             for (int i = 0; i < nbChunks; ++i) {
1015 
1016                 // set up chunk validity range
1017                 final AbsoluteDate chunkStart = chunkEnd;
1018                 chunkEnd = (i == nbChunks - 1) ? rangeEnd : rangeStart.shiftedBy((i + 1) * duration);
1019 
1020                 // extract Chebyshev coefficients for the selected body
1021                 // and convert them from kilometers to meters
1022                 final double[] xCoeffs = new double[nbCoeffs];
1023                 final double[] yCoeffs = new double[nbCoeffs];
1024                 final double[] zCoeffs = new double[nbCoeffs];
1025 
1026                 for (int k = 0; k < nbCoeffs; ++k) {
1027                     // by now, only use the position components
1028                     // if there are also velocity components contained in the file, ignore them
1029                     final int index = first + components * i * nbCoeffs + k - 1;
1030                     xCoeffs[k] = positionUnit * extractDouble(record, 8 * index);
1031                     yCoeffs[k] = positionUnit * extractDouble(record, 8 * (index +  nbCoeffs));
1032                     zCoeffs[k] = positionUnit * extractDouble(record, 8 * (index + 2 * nbCoeffs));
1033                 }
1034 
1035                 // build the position-velocity model for current chunk
1036                 entries.add(new PosVelChebyshev(chunkStart, timeScale, duration, xCoeffs, yCoeffs, zCoeffs));
1037 
1038             }
1039 
1040             return rangeStart;
1041 
1042         }
1043 
1044     }
1045 
1046     /** Raw position-velocity provider using ephemeris. */
1047     private class EphemerisRawPVProvider implements RawPVProvider {
1048 
1049         /** {@inheritDoc} */
1050         public PVCoordinates getRawPV(final AbsoluteDate date) {
1051 
1052             // get raw PV from Chebyshev polynomials
1053             PosVelChebyshev chebyshev;
1054             try {
1055                 chebyshev = ephemerides.getNeighbors(date).findFirst().get();
1056             } catch (TimeStampedCacheException tce) {
1057                 // we cannot bracket the date, check if the last available chunk covers the specified date
1058                 chebyshev = ephemerides.getLatest();
1059                 if (!chebyshev.inRange(date)) {
1060                     // we were not able to recover from the error, the date is too far
1061                     throw tce;
1062                 }
1063             }
1064 
1065             // evaluate the Chebyshev polynomials
1066             return chebyshev.getPositionVelocityAcceleration(date);
1067 
1068         }
1069 
1070         /** {@inheritDoc} */
1071         public <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> getRawPV(final FieldAbsoluteDate<T> date) {
1072 
1073             // get raw PV from Chebyshev polynomials
1074             PosVelChebyshev chebyshev;
1075             try {
1076                 chebyshev = ephemerides.getNeighbors(date.toAbsoluteDate()).findFirst().get();
1077             } catch (TimeStampedCacheException tce) {
1078                 // we cannot bracket the date, check if the last available chunk covers the specified date
1079                 chebyshev = ephemerides.getLatest();
1080                 if (!chebyshev.inRange(date.toAbsoluteDate())) {
1081                     // we were not able to recover from the error, the date is too far
1082                     throw tce;
1083                 }
1084             }
1085 
1086             // evaluate the Chebyshev polynomials
1087             return chebyshev.getPositionVelocityAcceleration(date);
1088 
1089         }
1090 
1091     }
1092 
1093     /** Raw position-velocity provider providing always zero. */
1094     private static class ZeroRawPVProvider implements RawPVProvider {
1095 
1096         /** {@inheritDoc} */
1097         public PVCoordinates getRawPV(final AbsoluteDate date) {
1098             return PVCoordinates.ZERO;
1099         }
1100 
1101         /** {@inheritDoc} */
1102         public <T extends CalculusFieldElement<T>> FieldPVCoordinates<T> getRawPV(final FieldAbsoluteDate<T> date) {
1103             return FieldPVCoordinates.getZero(date.getField());
1104         }
1105 
1106     }
1107 
1108 }