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