1   /* Copyright 2002-2026 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.Serial;
20  import java.io.Serializable;
21  
22  import org.hipparchus.CalculusFieldElement;
23  
24  /** Offset between {@link UTCScale UTC} and  {@link TAIScale TAI} time scales.
25   * <p>The {@link UTCScale UTC} and  {@link TAIScale TAI} time scales are two
26   * scales offset with respect to each other. The {@link TAIScale TAI} scale is
27   * continuous whereas the {@link UTCScale UTC} includes some discontinuity when
28   * leap seconds are introduced by the <a href="https://www.iers.org/">International
29   * Earth Rotation Service</a> (IERS).</p>
30   * <p>This class represents the offset between the two scales that is
31   * valid between two leap seconds occurrences. It handles both the linear offsets
32   * used from 1961-01-01 to 1971-12-31 and the constant integer offsets used since
33   * 1972-01-01.</p>
34   * @author Luc Maisonobe
35   * @see UTCScale
36   * @see UTCTAIHistoryFilesLoader
37   */
38  public class UTCTAIOffset implements TimeStamped, Serializable {
39  
40      /** Serializable UID. */
41      @Serial
42      private static final long serialVersionUID = 20240720L;
43  
44      /** Nanoseconds in one second. */
45      private static final int NANOS_IN_SECOND = 1000000000;
46  
47      /** Leap date. */
48      private final AbsoluteDate leapDate;
49  
50      /** Leap date in Modified Julian Day. */
51      private final int leapDateMJD;
52  
53      /** Offset start of validity date. */
54      private final AbsoluteDate validityStart;
55  
56      /** Reference date for the slope multiplication as Modified Julian Day. */
57      private final int mjdRef;
58  
59      /** Reference date for the slope multiplication. */
60      private final AbsoluteDate reference;
61  
62      /** Value of the leap at offset validity start (in seconds). */
63      private final TimeOffset leap;
64  
65      /** Offset at validity start in seconds (TAI minus UTC). */
66      private final TimeOffset offset;
67  
68      /** Offset slope in nanoseconds per UTC second (TAI minus UTC / dUTC). */
69      private final int slope;
70  
71      /** Simple constructor for a linear model.
72       * @param leapDate leap date
73       * @param leapDateMJD leap date in Modified Julian Day
74       * @param leap value of the leap at offset validity start (in seconds)
75       * @param offset offset in seconds (TAI minus UTC)
76       * @param mjdRef reference date for the slope multiplication as Modified Julian Day
77       * @param slope offset slope in nanoseconds per UTC second (TAI minus UTC / dUTC)
78       * @param reference date for slope computations.
79       */
80      UTCTAIOffset(final AbsoluteDate leapDate, final int leapDateMJD,
81                   final TimeOffset leap, final TimeOffset offset,
82                   final int mjdRef, final int slope, final AbsoluteDate reference) {
83          this.leapDate      = leapDate;
84          this.leapDateMJD   = leapDateMJD;
85          this.validityStart = leapDate.shiftedBy(leap);
86          this.mjdRef        = mjdRef;
87          this.reference     = reference;
88          this.leap          = leap;
89          this.offset        = offset;
90  
91          // at some absolute instant t₀, we can associate reading a₀ on a TAI clock and u₀ on a UTC clock
92          // at this instant, the offset between TAI and UTC is therefore τ₀ = a₀ - u₀
93          // at another absolute instant t₁, we can associate reading a₁ on a TAI clock and u₁ on a UTC clock
94          // at this instant, the offset between TAI and UTC is therefore τ₁ = a₁ - u₁
95          // the slope is defined according to offsets counted in UTC, i.e.:
96          // τ₁ = τ₀ + (u₁ - u₀) * slope/n (where n = 10⁹ because the slope is in ns/s)
97          // if we have a₁ - a₀ (i.e. dates in TAI) instead of u₁ - u₀, we need to invert the expression
98          // we get: τ₁ = τ₀ + (a₁ - a₀) * slope / (n + slope)
99          this.slope = slope;
100 
101     }
102 
103     /** Get the date of the start of the leap.
104      * @return date of the start of the leap
105      * @see #getValidityStart()
106      */
107     public AbsoluteDate getDate() {
108         return leapDate;
109     }
110 
111     /** Get the date of the start of the leap as Modified Julian Day.
112      * @return date of the start of the leap as Modified Julian Day
113      */
114     public int getMJD() {
115         return leapDateMJD;
116     }
117 
118     /** Get the start time of validity for this offset.
119      * <p>The start of the validity of the offset is {@link #getLeap()}
120      * seconds after the start of the leap itself.</p>
121      * @return start of validity date
122      * @see #getDate()
123      */
124     public AbsoluteDate getValidityStart() {
125         return validityStart;
126     }
127 
128     /** Get the value of the leap at offset validity start.
129      * @return value of the leap at offset validity start
130      */
131     public TimeOffset getLeap() {
132         return leap;
133     }
134 
135     /** Get the TAI - UTC offset in seconds.
136      * @param date date at which the offset is requested
137      * @return TAI - UTC offset in seconds.
138      */
139     public TimeOffset getOffset(final AbsoluteDate date) {
140         if (slope == 0) {
141             // we use an if statement here so the offset computation returns
142             // a finite value when date is AbsoluteDate.FUTURE_INFINITY
143             // without this if statement, the multiplication between an
144             // infinite duration and a zero slope would induce a NaN offset
145             return offset;
146         } else {
147 
148             // time during which slope applies
149             final TimeOffset delta = date.accurateDurationFrom(reference);
150 
151             // accumulated drift
152             final TimeOffset drift = delta.multiply(slope).divide(slope + NANOS_IN_SECOND);
153 
154             return offset.add(drift);
155 
156         }
157     }
158 
159     /** Get the TAI - UTC offset in seconds.
160      * @param date date at which the offset is requested
161      * @param <T> type of the filed elements
162      * @return TAI - UTC offset in seconds.
163      * @since 9.0
164      */
165     public <T extends CalculusFieldElement<T>> T getOffset(final FieldAbsoluteDate<T> date) {
166         if (slope == 0) {
167             // we use an if statement here so the offset computation returns
168             // a finite value when date is FieldAbsoluteDate.getFutureInfinity(field)
169             // without this if statement, the multiplication between an
170             // infinite duration and a zero slope would induce a NaN offset
171             return date.getField().getZero().newInstance(offset.toDouble());
172         } else {
173             // TODO perform complete computation
174             return date.getField().getZero().newInstance(getOffset(date.toAbsoluteDate()).toDouble());
175         }
176     }
177 
178     /** Get the TAI - UTC offset in seconds.
179      * @param date date components (in UTC) at which the offset is requested
180      * @param time time components (in UTC) at which the offset is requested
181      * @return TAI - UTC offset in seconds.
182      */
183     public TimeOffset getOffset(final DateComponents date, final TimeComponents time) {
184         if (slope == 0) {
185             return offset;
186         } else {
187 
188             // time during which slope applies
189             final TimeOffset delta = new TimeOffset((date.getMJD() - mjdRef) * TimeOffset.DAY.getSeconds() +
190                                                   time.getHour() * TimeOffset.HOUR.getSeconds() +
191                                                   time.getMinute() * TimeOffset.MINUTE.getSeconds() +
192                                                   time.getSplitSecond().getSeconds(),
193                                                     time.getSplitSecond().getAttoSeconds());
194 
195             // accumulated drift
196             final TimeOffset drift = delta.multiply(slope).divide(NANOS_IN_SECOND);
197 
198             return offset.add(drift);
199 
200         }
201     }
202 
203 }