1   /* Copyright 2002-2016 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.frames;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.List;
23  
24  import org.hipparchus.analysis.interpolation.HermiteInterpolator;
25  import org.orekit.errors.OrekitException;
26  import org.orekit.errors.OrekitInternalError;
27  import org.orekit.errors.OrekitMessages;
28  import org.orekit.errors.TimeStampedCacheException;
29  import org.orekit.time.AbsoluteDate;
30  import org.orekit.time.TimeFunction;
31  import org.orekit.time.TimeStamped;
32  import org.orekit.utils.Constants;
33  import org.orekit.utils.GenericTimeStampedCache;
34  import org.orekit.utils.IERSConventions;
35  import org.orekit.utils.ImmutableTimeStampedCache;
36  import org.orekit.utils.OrekitConfiguration;
37  import org.orekit.utils.TimeStampedCache;
38  import org.orekit.utils.TimeStampedGenerator;
39  
40  /** This class loads any kind of Earth Orientation Parameter data throughout a large time range.
41   * @author Pascal Parraud
42   */
43  public class EOPHistory implements Serializable {
44  
45      /** Serializable UID. */
46      private static final long serialVersionUID = 20131010L;
47  
48      /** Number of points to use in interpolation. */
49      private static final int INTERPOLATION_POINTS = 4;
50  
51      /**
52       * If this history has any EOP data.
53       *
54       * @see #hasDataFor(AbsoluteDate)
55       */
56      private final boolean hasData;
57  
58      /** EOP history entries. */
59      private final transient ImmutableTimeStampedCache<EOPEntry> cache;
60  
61      /** IERS conventions to which EOP refers. */
62      private final IERSConventions conventions;
63  
64      /** Correction to apply to EOP (may be null). */
65      private final transient TimeFunction<double[]> tidalCorrection;
66  
67      /** Simple constructor.
68       * @param conventions IERS conventions to which EOP refers
69       * @param data the EOP data to use
70       * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
71       * @exception OrekitException if tidal correction model cannot be loaded
72       */
73      protected EOPHistory(final IERSConventions conventions,
74                           final Collection<EOPEntry> data,
75                           final boolean simpleEOP)
76          throws OrekitException {
77          this(conventions, data, simpleEOP ? null : new CachedCorrection(conventions.getEOPTidalCorrection()));
78      }
79  
80      /** Simple constructor.
81       * @param conventions IERS conventions to which EOP refers
82       * @param data the EOP data to use
83       * @param tidalCorrection correction to apply to EOP
84       * @exception OrekitException if tidal correction model cannot be loaded
85       */
86      private EOPHistory(final IERSConventions conventions,
87                           final Collection<EOPEntry> data,
88                           final TimeFunction<double[]> tidalCorrection)
89          throws OrekitException {
90          this.conventions      = conventions;
91          this.tidalCorrection  = tidalCorrection;
92          if (data.size() >= INTERPOLATION_POINTS) {
93              // enough data to interpolate
94              cache = new ImmutableTimeStampedCache<EOPEntry>(INTERPOLATION_POINTS, data);
95              hasData = true;
96          } else {
97              // not enough data to interpolate -> always use null correction
98              cache = ImmutableTimeStampedCache.emptyCache();
99              hasData = false;
100         }
101     }
102 
103     /** Get non-interpolating version of the instance.
104      * @return non-interpolatig version of the instance
105      * @exception OrekitException if tidal correction model cannot be loaded
106      */
107     public EOPHistory getNonInterpolatingEOPHistory()
108         throws OrekitException {
109         return new EOPHistory(conventions, getEntries(), conventions.getEOPTidalCorrection());
110     }
111 
112     /** Check if the instance uses interpolation on tidal corrections.
113      * @return true if the instance uses interpolation on tidal corrections
114      */
115     public boolean usesInterpolation() {
116         return tidalCorrection != null && tidalCorrection instanceof CachedCorrection;
117     }
118 
119     /** Get the IERS conventions to which these EOP apply.
120      * @return IERS conventions to which these EOP apply
121      */
122     public IERSConventions getConventions() {
123         return conventions;
124     }
125 
126     /** Get the date of the first available Earth Orientation Parameters.
127      * @return the start date of the available data
128      */
129     public AbsoluteDate getStartDate() {
130         return this.cache.getEarliest().getDate();
131     }
132 
133     /** Get the date of the last available Earth Orientation Parameters.
134      * @return the end date of the available data
135      */
136     public AbsoluteDate getEndDate() {
137         return this.cache.getLatest().getDate();
138     }
139 
140     /** Get the UT1-UTC value.
141      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
142      * @param date date at which the value is desired
143      * @return UT1-UTC in seconds (0 if date is outside covered range)
144      */
145     public double getUT1MinusUTC(final AbsoluteDate date) {
146         //check if there is data for date
147         if (!this.hasDataFor(date)) {
148             // no EOP data available for this date, we use a default 0.0 offset
149             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[2];
150         }
151         //we have EOP data -> interpolate offset
152         try {
153             final List<EOPEntry> neighbors = getNeighbors(date);
154             final HermiteInterpolator interpolator = new HermiteInterpolator();
155             final double firstDUT = neighbors.get(0).getUT1MinusUTC();
156             boolean beforeLeap = true;
157             for (final EOPEntry neighbor : neighbors) {
158                 final double dut;
159                 if (neighbor.getUT1MinusUTC() - firstDUT > 0.9) {
160                     // there was a leap second between the entries
161                     dut = neighbor.getUT1MinusUTC() - 1.0;
162                     if (neighbor.getDate().compareTo(date) <= 0) {
163                         beforeLeap = false;
164                     }
165                 } else {
166                     dut = neighbor.getUT1MinusUTC();
167                 }
168                 interpolator.addSamplePoint(neighbor.getDate().durationFrom(date),
169                                             new double[] {
170                                                 dut
171                                             });
172             }
173             double interpolated = interpolator.value(0)[0];
174             if (tidalCorrection != null) {
175                 interpolated += tidalCorrection.value(date)[2];
176             }
177             return beforeLeap ? interpolated : interpolated + 1.0;
178         } catch (TimeStampedCacheException tce) {
179             //this should not happen because of date check above
180             throw new OrekitInternalError(tce);
181         }
182     }
183 
184     /**
185      * Get the entries surrounding a central date.
186      * <p>
187      * See {@link #hasDataFor(AbsoluteDate)} to determine if the cache has data
188      * for {@code central} without throwing an exception.
189      *
190      * @param central central date
191      * @return array of cached entries surrounding specified date
192      * @exception TimeStampedCacheException if EOP data cannot be retrieved
193      */
194     protected List<EOPEntry> getNeighbors(final AbsoluteDate central) throws TimeStampedCacheException {
195         return cache.getNeighbors(central);
196     }
197 
198     /** Get the LoD (Length of Day) value.
199      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
200      * @param date date at which the value is desired
201      * @return LoD in seconds (0 if date is outside covered range)
202      */
203     public double getLOD(final AbsoluteDate date) {
204         //check if there is data for date
205         if (!this.hasDataFor(date)) {
206             // no EOP data available for this date, we use a default null correction
207             return (tidalCorrection == null) ? 0.0 : tidalCorrection.value(date)[3];
208         }
209         //we have EOP data for date -> interpolate correction
210         try {
211             final HermiteInterpolator interpolator = new HermiteInterpolator();
212             for (final EOPEntry entry : getNeighbors(date)) {
213                 interpolator.addSamplePoint(entry.getDate().durationFrom(date),
214                                             new double[] {
215                                                 entry.getLOD()
216                                             });
217             }
218             double interpolated = interpolator.value(0)[0];
219             if (tidalCorrection != null) {
220                 interpolated += tidalCorrection.value(date)[3];
221             }
222             return interpolated;
223         } catch (TimeStampedCacheException tce) {
224             // this should not happen because of date check above
225             throw new OrekitInternalError(tce);
226         }
227     }
228 
229     /** Get the pole IERS Reference Pole correction.
230      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
231      * @param date date at which the correction is desired
232      * @return pole correction ({@link PoleCorrection#NULL_CORRECTION
233      * PoleCorrection.NULL_CORRECTION} if date is outside covered range)
234      */
235     public PoleCorrection getPoleCorrection(final AbsoluteDate date) {
236         // check if there is data for date
237         if (!this.hasDataFor(date)) {
238             // no EOP data available for this date, we use a default null correction
239             if (tidalCorrection == null) {
240                 return PoleCorrection.NULL_CORRECTION;
241             } else {
242                 final double[] correction = tidalCorrection.value(date);
243                 return new PoleCorrection(correction[0], correction[1]);
244             }
245         }
246         //we have EOP data for date -> interpolate correction
247         try {
248             final HermiteInterpolator interpolator = new HermiteInterpolator();
249             for (final EOPEntry entry : getNeighbors(date)) {
250                 interpolator.addSamplePoint(entry.getDate().durationFrom(date),
251                                             new double[] {
252                                                 entry.getX(), entry.getY()
253                                             });
254             }
255             final double[] interpolated = interpolator.value(0);
256             if (tidalCorrection != null) {
257                 final double[] correction = tidalCorrection.value(date);
258                 interpolated[0] += correction[0];
259                 interpolated[1] += correction[1];
260             }
261             return new PoleCorrection(interpolated[0], interpolated[1]);
262         } catch (TimeStampedCacheException tce) {
263             // this should not happen because of date check above
264             throw new OrekitInternalError(tce);
265         }
266     }
267 
268     /** Get the correction to the nutation parameters for equinox-based paradigm.
269      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
270      * @param date date at which the correction is desired
271      * @return nutation correction in longitude ΔΨ and in obliquity Δε
272      * (zero if date is outside covered range)
273      */
274     public double[] getEquinoxNutationCorrection(final AbsoluteDate date) {
275         // check if there is data for date
276         if (!this.hasDataFor(date)) {
277             // no EOP data available for this date, we use a default null correction
278             return new double[2];
279         }
280         //we have EOP data for date -> interpolate correction
281         try {
282             final HermiteInterpolator interpolator = new HermiteInterpolator();
283             for (final EOPEntry entry : getNeighbors(date)) {
284                 interpolator.addSamplePoint(entry.getDate().durationFrom(date),
285                                             new double[] {
286                                                 entry.getDdPsi(), entry.getDdEps()
287                                             });
288             }
289             return interpolator.value(0);
290         } catch (TimeStampedCacheException tce) {
291             // this should not happen because of date check above
292             throw new OrekitInternalError(tce);
293         }
294     }
295 
296     /** Get the correction to the nutation parameters for Non-Rotating Origin paradigm.
297      * <p>The data provided comes from the IERS files. It is smoothed data.</p>
298      * @param date date at which the correction is desired
299      * @return nutation correction in Celestial Intermediat Pole coordinates
300      * δX and δY (zero if date is outside covered range)
301      */
302     public double[] getNonRotatinOriginNutationCorrection(final AbsoluteDate date) {
303         // check if there is data for date
304         if (!this.hasDataFor(date)) {
305             // no EOP data available for this date, we use a default null correction
306             return new double[2];
307         }
308         //we have EOP data for date -> interpolate correction
309         try {
310             final HermiteInterpolator interpolator = new HermiteInterpolator();
311             for (final EOPEntry entry : getNeighbors(date)) {
312                 interpolator.addSamplePoint(entry.getDate().durationFrom(date),
313                                             new double[] {
314                                                 entry.getDx(), entry.getDy()
315                                             });
316             }
317             return interpolator.value(0);
318         } catch (TimeStampedCacheException tce) {
319             // this should not happen because of date check above
320             throw new OrekitInternalError(tce);
321         }
322     }
323 
324     /** Check Earth orientation parameters continuity.
325      * @param maxGap maximal allowed gap between entries (in seconds)
326      * @exception OrekitException if there are holes in the data sequence
327      */
328     public void checkEOPContinuity(final double maxGap) throws OrekitException {
329         TimeStamped preceding = null;
330         for (final TimeStamped current : this.cache.getAll()) {
331 
332             // compare the dates of preceding and current entries
333             if ((preceding != null) && ((current.getDate().durationFrom(preceding.getDate())) > maxGap)) {
334                 throw new OrekitException(OrekitMessages.MISSING_EARTH_ORIENTATION_PARAMETERS_BETWEEN_DATES,
335                                           preceding.getDate(), current.getDate());
336             }
337 
338             // prepare next iteration
339             preceding = current;
340 
341         }
342     }
343 
344     /**
345      * Check if the cache has data for the given date using
346      * {@link #getStartDate()} and {@link #getEndDate()}.
347      *
348      * @param date the requested date
349      * @return true if the {@link #cache} has data for the requested date, false
350      *         otherwise.
351      */
352     protected boolean hasDataFor(final AbsoluteDate date) {
353         /*
354          * when there is no EOP data, short circuit getStartDate, which will
355          * throw an exception
356          */
357         return this.hasData && this.getStartDate().compareTo(date) <= 0 &&
358                date.compareTo(this.getEndDate()) <= 0;
359     }
360 
361     /** Get a non-modifiable view of the EOP entries.
362      * @return non-modifiable view of the EOP entries
363      */
364     List<EOPEntry> getEntries() {
365         return cache.getAll();
366     }
367 
368     /** Replace the instance with a data transfer object for serialization.
369      * <p>
370      * This intermediate class serializes only the frame key.
371      * </p>
372      * @return data transfer object that will be serialized
373      */
374     private Object writeReplace() {
375         return new DataTransferObject(conventions, getEntries(), tidalCorrection == null);
376     }
377 
378     /** Internal class used only for serialization. */
379     private static class DataTransferObject implements Serializable {
380 
381         /** Serializable UID. */
382         private static final long serialVersionUID = 20131010L;
383 
384         /** IERS conventions. */
385         private final IERSConventions conventions;
386 
387         /** EOP entries. */
388         private final List<EOPEntry> entries;
389 
390         /** Indicator for simple interpolation without tidal effects. */
391         private final boolean simpleEOP;
392 
393         /** Simple constructor.
394          * @param conventions IERS conventions to which EOP refers
395          * @param entries the EOP data to use
396          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
397          */
398         DataTransferObject(final IERSConventions conventions,
399                                   final List<EOPEntry> entries,
400                                   final boolean simpleEOP) {
401             this.conventions = conventions;
402             this.entries     = entries;
403             this.simpleEOP   = simpleEOP;
404         }
405 
406         /** Replace the deserialized data transfer object with a {@link EOPHistory}.
407          * @return replacement {@link EOPHistory}
408          */
409         private Object readResolve() {
410             try {
411                 // retrieve a managed frame
412                 return new EOPHistory(conventions, entries, simpleEOP);
413             } catch (OrekitException oe) {
414                 throw new OrekitInternalError(oe);
415             }
416         }
417 
418     }
419 
420     /** Internal class for caching tidal correction. */
421     private static class TidalCorrectionEntry implements TimeStamped {
422 
423         /** Entry date. */
424         private final AbsoluteDate date;
425 
426         /** Correction. */
427         private final double[] correction;
428 
429         /** Simple constructor.
430          * @param date entry date
431          * @param correction correction on the EOP parameters (xp, yp, ut1, lod)
432          */
433         TidalCorrectionEntry(final AbsoluteDate date, final double[] correction) {
434             this.date       = date;
435             this.correction = correction;
436         }
437 
438         /** {@inheritDoc} */
439         @Override
440         public AbsoluteDate getDate() {
441             return date;
442         }
443 
444     }
445 
446     /** Local generator for thread-safe cache. */
447     private static class CachedCorrection
448         implements TimeFunction<double[]>, TimeStampedGenerator<TidalCorrectionEntry> {
449 
450         /** Correction to apply to EOP (may be null). */
451         private final TimeFunction<double[]> tidalCorrection;
452 
453         /** Step between generated entries. */
454         private final double step;
455 
456         /** Tidal corrections entries cache. */
457         private final TimeStampedCache<TidalCorrectionEntry> cache;
458 
459         /** Simple constructor.
460          * @param tidalCorrection function computing the tidal correction
461          */
462         CachedCorrection(final TimeFunction<double[]> tidalCorrection) {
463             this.step            = 60 * 60;
464             this.tidalCorrection = tidalCorrection;
465             this.cache           =
466                 new GenericTimeStampedCache<TidalCorrectionEntry>(8,
467                                                                   OrekitConfiguration.getCacheSlotsNumber(),
468                                                                   Constants.JULIAN_DAY * 30,
469                                                                   Constants.JULIAN_DAY,
470                                                                   this,
471                                                                   TidalCorrectionEntry.class);
472         }
473 
474         /** {@inheritDoc} */
475         @Override
476         public double[] value(final AbsoluteDate date) {
477             try {
478                 // set up an interpolator
479                 final HermiteInterpolator interpolator = new HermiteInterpolator();
480                 for (final TidalCorrectionEntry entry : cache.getNeighbors(date)) {
481                     interpolator.addSamplePoint(entry.date.durationFrom(date), entry.correction);
482                 }
483 
484                 // interpolate to specified date
485                 return interpolator.value(0.0);
486             } catch (TimeStampedCacheException tsce) {
487                 // this should never happen
488                 throw new OrekitInternalError(tsce);
489             }
490         }
491 
492         /** {@inheritDoc} */
493         @Override
494         public List<TidalCorrectionEntry> generate(final TidalCorrectionEntry existing, final AbsoluteDate date) {
495 
496             final List<TidalCorrectionEntry> generated = new ArrayList<TidalCorrectionEntry>();
497 
498             if (existing == null) {
499 
500                 // no prior existing entries, just generate a first set
501                 for (int i = -cache.getNeighborsSize() / 2; generated.size() < cache.getNeighborsSize(); ++i) {
502                     final AbsoluteDate t = date.shiftedBy(i * step);
503                     generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
504                 }
505 
506             } else {
507 
508                 // some entries have already been generated
509                 // add the missing ones up to specified date
510 
511                 AbsoluteDate t = existing.getDate();
512                 if (date.compareTo(t) > 0) {
513                     // forward generation
514                     do {
515                         t = t.shiftedBy(step);
516                         generated.add(new TidalCorrectionEntry(t, tidalCorrection.value(t)));
517                     } while (t.compareTo(date) <= 0);
518                 } else {
519                     // backward generation
520                     do {
521                         t = t.shiftedBy(-step);
522                         generated.add(0, new TidalCorrectionEntry(t, tidalCorrection.value(t)));
523                     } while (t.compareTo(date) >= 0);
524                 }
525             }
526 
527             // return the generated transforms
528             return generated;
529 
530         }
531     }
532 
533 }