UTCTAIOffset.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.time;

import java.io.Serializable;

import org.hipparchus.CalculusFieldElement;

/** Offset between {@link UTCScale UTC} and  {@link TAIScale TAI} time scales.
 * <p>The {@link UTCScale UTC} and  {@link TAIScale TAI} time scales are two
 * scales offset with respect to each other. The {@link TAIScale TAI} scale is
 * continuous whereas the {@link UTCScale UTC} includes some discontinuity when
 * leap seconds are introduced by the <a href="http://www.iers.org/">International
 * Earth Rotation Service</a> (IERS).</p>
 * <p>This class represents the offset between the two scales that is
 * valid between two leap seconds occurrences. It handles both the linear offsets
 * used from 1961-01-01 to 1971-12-31 and the constant integer offsets used since
 * 1972-01-01.</p>
 * @author Luc Maisonobe
 * @see UTCScale
 * @see UTCTAIHistoryFilesLoader
 */
public class UTCTAIOffset implements TimeStamped, Serializable {

    /** Serializable UID. */
    private static final long serialVersionUID = 20240720L;

    /** Nanoseconds in one second. */
    private static final int NANOS_IN_SECOND = 1000000000;

    /** Leap date. */
    private final AbsoluteDate leapDate;

    /** Leap date in Modified Julian Day. */
    private final int leapDateMJD;

    /** Offset start of validity date. */
    private final AbsoluteDate validityStart;

    /** Reference date for the slope multiplication as Modified Julian Day. */
    private final int mjdRef;

    /** Reference date for the slope multiplication. */
    private final AbsoluteDate reference;

    /** Value of the leap at offset validity start (in seconds). */
    private final TimeOffset leap;

    /** Offset at validity start in seconds (TAI minus UTC). */
    private final TimeOffset offset;

    /** Offset slope in nanoseconds per UTC second (TAI minus UTC / dUTC). */
    private final int slope;

    /** Simple constructor for a linear model.
     * @param leapDate leap date
     * @param leapDateMJD leap date in Modified Julian Day
     * @param leap value of the leap at offset validity start (in seconds)
     * @param offset offset in seconds (TAI minus UTC)
     * @param mjdRef reference date for the slope multiplication as Modified Julian Day
     * @param slope offset slope in nanoseconds per UTC second (TAI minus UTC / dUTC)
     * @param reference date for slope computations.
     */
    UTCTAIOffset(final AbsoluteDate leapDate, final int leapDateMJD,
                 final TimeOffset leap, final TimeOffset offset,
                 final int mjdRef, final int slope, final AbsoluteDate reference) {
        this.leapDate      = leapDate;
        this.leapDateMJD   = leapDateMJD;
        this.validityStart = leapDate.shiftedBy(leap);
        this.mjdRef        = mjdRef;
        this.reference     = reference;
        this.leap          = leap;
        this.offset        = offset;

        // at some absolute instant t₀, we can associate reading a₀ on a TAI clock and u₀ on a UTC clock
        // at this instant, the offset between TAI and UTC is therefore τ₀ = a₀ - u₀
        // at another absolute instant t₁, we can associate reading a₁ on a TAI clock and u₁ on a UTC clock
        // at this instant, the offset between TAI and UTC is therefore τ₁ = a₁ - u₁
        // the slope is defined according to offsets counted in UTC, i.e.:
        // τ₁ = τ₀ + (u₁ - u₀) * slope/n (where n = 10⁹ because the slope is in ns/s)
        // if we have a₁ - a₀ (i.e. dates in TAI) instead of u₁ - u₀, we need to invert the expression
        // we get: τ₁ = τ₀ + (a₁ - a₀) * slope / (n + slope)
        this.slope = slope;

    }

    /** Get the date of the start of the leap.
     * @return date of the start of the leap
     * @see #getValidityStart()
     */
    public AbsoluteDate getDate() {
        return leapDate;
    }

    /** Get the date of the start of the leap as Modified Julian Day.
     * @return date of the start of the leap as Modified Julian Day
     */
    public int getMJD() {
        return leapDateMJD;
    }

    /** Get the start time of validity for this offset.
     * <p>The start of the validity of the offset is {@link #getLeap()}
     * seconds after the start of the leap itself.</p>
     * @return start of validity date
     * @see #getDate()
     */
    public AbsoluteDate getValidityStart() {
        return validityStart;
    }

    /** Get the value of the leap at offset validity start.
     * @return value of the leap at offset validity start
     */
    public TimeOffset getLeap() {
        return leap;
    }

    /** Get the TAI - UTC offset in seconds.
     * @param date date at which the offset is requested
     * @return TAI - UTC offset in seconds.
     */
    public TimeOffset getOffset(final AbsoluteDate date) {
        if (slope == 0) {
            // we use an if statement here so the offset computation returns
            // a finite value when date is AbsoluteDate.FUTURE_INFINITY
            // without this if statement, the multiplication between an
            // infinite duration and a zero slope would induce a NaN offset
            return offset;
        } else {

            // time during which slope applies
            final TimeOffset delta = date.accurateDurationFrom(reference);

            // accumulated drift
            final TimeOffset drift = delta.multiply(slope).divide(slope + NANOS_IN_SECOND);

            return offset.add(drift);

        }
    }

    /** Get the TAI - UTC offset in seconds.
     * @param date date at which the offset is requested
     * @param <T> type of the filed elements
     * @return TAI - UTC offset in seconds.
     * @since 9.0
     */
    public <T extends CalculusFieldElement<T>> T getOffset(final FieldAbsoluteDate<T> date) {
        if (slope == 0) {
            // we use an if statement here so the offset computation returns
            // a finite value when date is FieldAbsoluteDate.getFutureInfinity(field)
            // without this if statement, the multiplication between an
            // infinite duration and a zero slope would induce a NaN offset
            return date.getField().getZero().newInstance(offset.toDouble());
        } else {
            // TODO perform complete computation
            return date.getField().getZero().newInstance(getOffset(date.toAbsoluteDate()).toDouble());
        }
    }

    /** Get the TAI - UTC offset in seconds.
     * @param date date components (in UTC) at which the offset is requested
     * @param time time components (in UTC) at which the offset is requested
     * @return TAI - UTC offset in seconds.
     */
    public TimeOffset getOffset(final DateComponents date, final TimeComponents time) {
        if (slope == 0) {
            return offset;
        } else {

            // time during which slope applies
            final TimeOffset delta = new TimeOffset((date.getMJD() - mjdRef) * TimeOffset.DAY.getSeconds() +
                                                  time.getHour() * TimeOffset.HOUR.getSeconds() +
                                                  time.getMinute() * TimeOffset.MINUTE.getSeconds() +
                                                  time.getSplitSecond().getSeconds(),
                                                    time.getSplitSecond().getAttoSeconds());

            // accumulated drift
            final TimeOffset drift = delta.multiply(slope).divide(NANOS_IN_SECOND);

            return offset.add(drift);

        }
    }

}