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