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