1   /* Copyright 2002-2022 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.time;
18  
19  import java.io.Serializable;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.Comparator;
23  import java.util.List;
24  
25  import org.hipparchus.CalculusFieldElement;
26  import org.hipparchus.util.FastMath;
27  import org.orekit.annotation.DefaultDataContext;
28  import org.orekit.data.DataContext;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitInternalError;
31  import org.orekit.utils.Constants;
32  
33  /** Coordinated Universal Time.
34   * <p>UTC is related to TAI using step adjustments from time to time
35   * according to IERS (International Earth Rotation Service) rules. Before 1972,
36   * these adjustments were piecewise linear offsets. Since 1972, these adjustments
37   * are piecewise constant offsets, which require introduction of leap seconds.</p>
38   * <p>Leap seconds are always inserted as additional seconds at the last minute
39   * of the day, pushing the next day forward. Such minutes are therefore more
40   * than 60 seconds long. In theory, there may be seconds removal instead of seconds
41   * insertion, but up to now (2010) it has never been used. As an example, when a
42   * one second leap was introduced at the end of 2005, the UTC time sequence was
43   * 2005-12-31T23:59:59 UTC, followed by 2005-12-31T23:59:60 UTC, followed by
44   * 2006-01-01T00:00:00 UTC.</p>
45   * <p>This is intended to be accessed thanks to {@link TimeScales},
46   * so there is no public constructor.</p>
47   * @author Luc Maisonobe
48   * @see AbsoluteDate
49   */
50  public class UTCScale implements TimeScale {
51  
52      /** Serializable UID. */
53      private static final long serialVersionUID = 20150402L;
54  
55      /** UTC-TAI offsets. */
56      private UTCTAIOffset[] offsets;
57  
58      /** Package private constructor for the factory.
59       * Used to create the prototype instance of this class that is used to
60       * clone all subsequent instances of {@link UTCScale}. Initializes the offset
61       * table that is shared among all instances.
62       * @param tai TAI time scale this UTC time scale references.
63       * @param offsets UTC-TAI offsets
64       */
65      UTCScale(final TimeScale tai, final Collection<? extends OffsetModel> offsets) {
66          // copy input so the original list is unmodified
67          final List<OffsetModel> offsetModels = new ArrayList<>(offsets);
68          offsetModels.sort(Comparator.comparing(OffsetModel::getStart));
69          if (offsetModels.get(0).getStart().getYear() > 1968) {
70              // the pre-1972 linear offsets are missing, add them manually
71              // excerpt from UTC-TAI.history file:
72              //  1961  Jan.  1 - 1961  Aug.  1     1.422 818 0s + (MJD - 37 300) x 0.001 296s
73              //        Aug.  1 - 1962  Jan.  1     1.372 818 0s +        ""
74              //  1962  Jan.  1 - 1963  Nov.  1     1.845 858 0s + (MJD - 37 665) x 0.001 123 2s
75              //  1963  Nov.  1 - 1964  Jan.  1     1.945 858 0s +        ""
76              //  1964  Jan.  1 -       April 1     3.240 130 0s + (MJD - 38 761) x 0.001 296s
77              //        April 1 -       Sept. 1     3.340 130 0s +        ""
78              //        Sept. 1 - 1965  Jan.  1     3.440 130 0s +        ""
79              //  1965  Jan.  1 -       March 1     3.540 130 0s +        ""
80              //        March 1 -       Jul.  1     3.640 130 0s +        ""
81              //        Jul.  1 -       Sept. 1     3.740 130 0s +        ""
82              //        Sept. 1 - 1966  Jan.  1     3.840 130 0s +        ""
83              //  1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
84              //  1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
85              offsetModels.add( 0, new OffsetModel(new DateComponents(1961,  1, 1), 37300, 1.4228180, 0.0012960));
86              offsetModels.add( 1, new OffsetModel(new DateComponents(1961,  8, 1), 37300, 1.3728180, 0.0012960));
87              offsetModels.add( 2, new OffsetModel(new DateComponents(1962,  1, 1), 37665, 1.8458580, 0.0011232));
88              offsetModels.add( 3, new OffsetModel(new DateComponents(1963, 11, 1), 37665, 1.9458580, 0.0011232));
89              offsetModels.add( 4, new OffsetModel(new DateComponents(1964,  1, 1), 38761, 3.2401300, 0.0012960));
90              offsetModels.add( 5, new OffsetModel(new DateComponents(1964,  4, 1), 38761, 3.3401300, 0.0012960));
91              offsetModels.add( 6, new OffsetModel(new DateComponents(1964,  9, 1), 38761, 3.4401300, 0.0012960));
92              offsetModels.add( 7, new OffsetModel(new DateComponents(1965,  1, 1), 38761, 3.5401300, 0.0012960));
93              offsetModels.add( 8, new OffsetModel(new DateComponents(1965,  3, 1), 38761, 3.6401300, 0.0012960));
94              offsetModels.add( 9, new OffsetModel(new DateComponents(1965,  7, 1), 38761, 3.7401300, 0.0012960));
95              offsetModels.add(10, new OffsetModel(new DateComponents(1965,  9, 1), 38761, 3.8401300, 0.0012960));
96              offsetModels.add(11, new OffsetModel(new DateComponents(1966,  1, 1), 39126, 4.3131700, 0.0025920));
97              offsetModels.add(12, new OffsetModel(new DateComponents(1968,  2, 1), 39126, 4.2131700, 0.0025920));
98          }
99  
100         // create cache
101         this.offsets = new UTCTAIOffset[offsetModels.size()];
102 
103         UTCTAIOffset previous = null;
104 
105         // link the offsets together
106         for (int i = 0; i < offsetModels.size(); ++i) {
107 
108             final OffsetModel    o      = offsetModels.get(i);
109             final DateComponents date   = o.getStart();
110             final int            mjdRef = o.getMJDRef();
111             final double         offset = o.getOffset();
112             final double         slope  = o.getSlope();
113 
114             // start of the leap
115             final double previousOffset    = (previous == null) ? 0.0 : previous.getOffset(date, TimeComponents.H00);
116             final AbsoluteDate leapStart   = new AbsoluteDate(date, tai).shiftedBy(previousOffset);
117 
118             // end of the leap
119             final double startOffset       = offset + slope * (date.getMJD() - mjdRef);
120             final AbsoluteDate leapEnd     = new AbsoluteDate(date, tai).shiftedBy(startOffset);
121 
122             // leap computed at leap start and in UTC scale
123             final double normalizedSlope   = slope / Constants.JULIAN_DAY;
124             final double leap              = leapEnd.durationFrom(leapStart) / (1 + normalizedSlope);
125 
126             final AbsoluteDate reference = AbsoluteDate.createMJDDate(mjdRef, 0, tai)
127                     .shiftedBy(offset);
128             previous = new UTCTAIOffset(leapStart, date.getMJD(), leap, offset, mjdRef,
129                     normalizedSlope, reference);
130             this.offsets[i] = previous;
131 
132         }
133 
134     }
135 
136     /**
137      * Returns the UTC-TAI offsets underlying this UTC scale.
138      * <p>
139      * Modifications to the returned list will not affect this UTC scale instance.
140      * @return new non-null modifiable list of UTC-TAI offsets time-sorted from
141      *         earliest to latest
142      */
143     public List<UTCTAIOffset> getUTCTAIOffsets() {
144         final List<UTCTAIOffset> offsetList = new ArrayList<>(offsets.length);
145         for (int i = 0; i < offsets.length; ++i) {
146             offsetList.add(offsets[i]);
147         }
148         return offsetList;
149     }
150 
151     /** {@inheritDoc} */
152     @Override
153     public double offsetFromTAI(final AbsoluteDate date) {
154         final int offsetIndex = findOffsetIndex(date);
155         if (offsetIndex < 0) {
156             // the date is before the first known leap
157             return 0;
158         } else {
159             return -offsets[offsetIndex].getOffset(date);
160         }
161     }
162 
163     /** {@inheritDoc} */
164     @Override
165     public <T extends CalculusFieldElement<T>> T offsetFromTAI(final FieldAbsoluteDate<T> date) {
166         final int offsetIndex = findOffsetIndex(date.toAbsoluteDate());
167         if (offsetIndex < 0) {
168             // the date is before the first known leap
169             return date.getField().getZero();
170         } else {
171             return offsets[offsetIndex].getOffset(date).negate();
172         }
173     }
174 
175     /** {@inheritDoc} */
176     @Override
177     public double offsetToTAI(final DateComponents date,
178                               final TimeComponents time) {
179 
180         // take offset from local time into account, but ignoring seconds,
181         // so when we parse an hour like 23:59:60.5 during leap seconds introduction,
182         // we do not jump to next day
183         final int minuteInDay = time.getHour() * 60 + time.getMinute() - time.getMinutesFromUTC();
184         final int correction  = minuteInDay < 0 ? (minuteInDay - 1439) / 1440 : minuteInDay / 1440;
185 
186         // find close neighbors, assuming date in TAI, i.e a date earlier than real UTC date
187         final int mjd = date.getMJD() + correction;
188         final UTCTAIOffset offset = findOffset(mjd);
189         if (offset == null) {
190             // the date is before the first known leap
191             return 0;
192         } else {
193             return offset.getOffset(date, time);
194         }
195 
196     }
197 
198     /** {@inheritDoc} */
199     public String getName() {
200         return "UTC";
201     }
202 
203     /** {@inheritDoc} */
204     public String toString() {
205         return getName();
206     }
207 
208     /** Get the date of the first known leap second.
209      * @return date of the first known leap second
210      */
211     public AbsoluteDate getFirstKnownLeapSecond() {
212         return offsets[0].getDate();
213     }
214 
215     /** Get the date of the last known leap second.
216      * @return date of the last known leap second
217      */
218     public AbsoluteDate getLastKnownLeapSecond() {
219         return offsets[offsets.length - 1].getDate();
220     }
221 
222     /** {@inheritDoc} */
223     @Override
224     public boolean insideLeap(final AbsoluteDate date) {
225         final int offsetIndex = findOffsetIndex(date);
226         if (offsetIndex < 0) {
227             // the date is before the first known leap
228             return false;
229         } else {
230             return date.compareTo(offsets[offsetIndex].getValidityStart()) < 0;
231         }
232     }
233 
234     /** {@inheritDoc} */
235     @Override
236     public <T extends CalculusFieldElement<T>> boolean insideLeap(final FieldAbsoluteDate<T> date) {
237         return insideLeap(date.toAbsoluteDate());
238     }
239 
240     /** {@inheritDoc} */
241     @Override
242     public int minuteDuration(final AbsoluteDate date) {
243         final int offsetIndex = findOffsetIndex(date);
244         final UTCTAIOffset offset;
245         if (offsetIndex >= 0 &&
246                 date.compareTo(offsets[offsetIndex].getValidityStart()) < 0) {
247             // the date is during the leap itself
248             offset = offsets[offsetIndex];
249         } else if (offsetIndex + 1 < offsets.length &&
250             offsets[offsetIndex + 1].getDate().durationFrom(date) <= 60.0) {
251             // the date is after a leap, but it may be just before the next one
252             // the next leap will start in one minute, it will extend the current minute
253             offset = offsets[offsetIndex + 1];
254         } else {
255             offset = null;
256         }
257         if (offset != null) {
258             // since this method returns an int we can't return the precise duration in
259             // all cases, but we can bound it. Some leaps are more than 1s. See #694
260             return 60 + (int) FastMath.ceil(offset.getLeap());
261         }
262         // no leap is expected within the next minute
263         return 60;
264     }
265 
266     /** {@inheritDoc} */
267     @Override
268     public <T extends CalculusFieldElement<T>> int minuteDuration(final FieldAbsoluteDate<T> date) {
269         return minuteDuration(date.toAbsoluteDate());
270     }
271 
272     /** {@inheritDoc} */
273     @Override
274     public double getLeap(final AbsoluteDate date) {
275         final int offsetIndex = findOffsetIndex(date);
276         if (offsetIndex < 0) {
277             // the date is before the first known leap
278             return 0;
279         } else {
280             return offsets[offsetIndex].getLeap();
281         }
282     }
283 
284     /** {@inheritDoc} */
285     @Override
286     public <T extends CalculusFieldElement<T>> T getLeap(final FieldAbsoluteDate<T> date) {
287         return date.getField().getZero().add(getLeap(date.toAbsoluteDate()));
288     }
289 
290     /** Find the index of the offset valid at some date.
291      * @param date date at which offset is requested
292      * @return index of the offset valid at this date, or -1 if date is before first offset.
293      */
294     private int findOffsetIndex(final AbsoluteDate date) {
295         int inf = 0;
296         int sup = offsets.length;
297         while (sup - inf > 1) {
298             final int middle = (inf + sup) >>> 1;
299             if (date.compareTo(offsets[middle].getDate()) < 0) {
300                 sup = middle;
301             } else {
302                 inf = middle;
303             }
304         }
305         if (sup == offsets.length) {
306             // the date is after the last known leap second
307             return offsets.length - 1;
308         } else if (date.compareTo(offsets[inf].getDate()) < 0) {
309             // the date is before the first known leap
310             return -1;
311         } else {
312             return inf;
313         }
314     }
315 
316     /** Find the offset valid at some date.
317      * @param mjd Modified Julian Day of the date at which offset is requested
318      * @return offset valid at this date, or null if date is before first offset.
319      */
320     private UTCTAIOffset findOffset(final int mjd) {
321         int inf = 0;
322         int sup = offsets.length;
323         while (sup - inf > 1) {
324             final int middle = (inf + sup) >>> 1;
325             if (mjd < offsets[middle].getMJD()) {
326                 sup = middle;
327             } else {
328                 inf = middle;
329             }
330         }
331         if (sup == offsets.length) {
332             // the date is after the last known leap second
333             return offsets[offsets.length - 1];
334         } else if (mjd < offsets[inf].getMJD()) {
335             // the date is before the first known leap
336             return null;
337         } else {
338             return offsets[inf];
339         }
340     }
341 
342     /** Replace the instance with a data transfer object for serialization.
343      * @return data transfer object that will be serialized
344      */
345     @DefaultDataContext
346     private Object writeReplace() {
347         return new DataTransferObject();
348     }
349 
350     /** Internal class used only for serialization. */
351     @DefaultDataContext
352     private static class DataTransferObject implements Serializable {
353 
354         /** Serializable UID. */
355         private static final long serialVersionUID = 20131209L;
356 
357         /** Replace the deserialized data transfer object with a {@link UTCScale}.
358          * @return replacement {@link UTCScale}
359          */
360         private Object readResolve() {
361             try {
362                 return DataContext.getDefault().getTimeScales().getUTC();
363             } catch (OrekitException oe) {
364                 throw new OrekitInternalError(oe);
365             }
366         }
367 
368     }
369 
370 }