UTCScale.java

/* Copyright 2002-2013 CS Systèmes d'Information
 * Licensed to CS Systèmes d'Information (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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;

import org.orekit.errors.OrekitException;
import org.orekit.errors.TimeStampedCacheException;
import org.orekit.utils.Constants;
import org.orekit.utils.ImmutableTimeStampedCache;
import org.orekit.utils.TimeStampedCache;

/** Coordinated Universal Time.
 * <p>UTC is related to TAI using step adjustments from time to time
 * according to IERS (International Earth Rotation Service) rules. Before 1972,
 * these adjustments were piecewise linear offsets. Since 1972, these adjustments
 * are piecewise constant offsets, which require introduction of leap seconds.</p>
 * <p>Leap seconds are always inserted as additional seconds at the last minute
 * of the day, pushing the next day forward. Such minutes are therefore more
 * than 60 seconds long. In theory, there may be seconds removal instead of seconds
 * insertion, but up to now (2010) it has never been used. As an example, when a
 * one second leap was introduced at the end of 2005, the UTC time sequence was
 * 2005-12-31T23:59:59 UTC, followed by 2005-12-31T23:59:60 UTC, followed by
 * 2006-01-01T00:00:00 UTC.</p>
 * <p>The OREKIT library retrieves the post-1972 constant time steps data thanks
 * to the {@link org.orekit.data.DataProvidersManager DataProvidersManager} class.
 * The linear models used between 1961 and 1972 are built-in in the class itself.</p>
 * <p>This is intended to be accessed thanks to the {@link TimeScalesFactory} class,
 * so there is no public constructor. Every call to {@link TimeScalesFactory#getUTC()}
 * will create a new {@link UTCScale} instance, sharing the UTC-TAI offset table between
 * all instances.</p>
 * @author Luc Maisonobe
 * @see AbsoluteDate
 */
public class UTCScale implements TimeScale {

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

    /** Time steps. */
    private transient TimeStampedCache<UTCTAIOffset> cache;

    /** Package private constructor for the factory.
     * Used to create the prototype instance of this class that is used to
     * clone all subsequent instances of {@link UTCScale}. Initializes the offset
     * table that is shared among all instances.
     * @param entries user supplied entries
     * @exception OrekitException if cache cannot be set up
     */
    UTCScale(final SortedMap<DateComponents, Integer> entries) throws OrekitException {
        // create cache
        final List<UTCTAIOffset> data = new Generator(entries).getOffsets();
        cache = new ImmutableTimeStampedCache<UTCTAIOffset>(2, data);
    }

    /** Generator for leap seconds entries. */
    private static class Generator {

        /** List of {@link UTCTAIOffset} entries. */
        private final List<UTCTAIOffset> offsets;

        /** Simple constructor.
         * @param entries user supplied entries
         */
        public Generator(final SortedMap<DateComponents, Integer> entries) {

            offsets = new ArrayList<UTCTAIOffset>();

            // set up the linear offsets used between 1961-01-01 and 1971-12-31
            // excerpt from UTC-TAI.history file:
            //  1961  Jan.  1 - 1961  Aug.  1     1.422 818 0s + (MJD - 37 300) x 0.001 296s
            //        Aug.  1 - 1962  Jan.  1     1.372 818 0s +        ""
            //  1962  Jan.  1 - 1963  Nov.  1     1.845 858 0s + (MJD - 37 665) x 0.001 123 2s
            //  1963  Nov.  1 - 1964  Jan.  1     1.945 858 0s +        ""
            //  1964  Jan.  1 -       April 1     3.240 130 0s + (MJD - 38 761) x 0.001 296s
            //        April 1 -       Sept. 1     3.340 130 0s +        ""
            //        Sept. 1 - 1965  Jan.  1     3.440 130 0s +        ""
            //  1965  Jan.  1 -       March 1     3.540 130 0s +        ""
            //        March 1 -       Jul.  1     3.640 130 0s +        ""
            //        Jul.  1 -       Sept. 1     3.740 130 0s +        ""
            //        Sept. 1 - 1966  Jan.  1     3.840 130 0s +        ""
            //  1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
            //  1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
            addOffsetModel(new DateComponents(1961,  1, 1), 37300, 1.4228180, 0.0012960);
            addOffsetModel(new DateComponents(1961,  8, 1), 37300, 1.3728180, 0.0012960);
            addOffsetModel(new DateComponents(1962,  1, 1), 37665, 1.8458580, 0.0011232);
            addOffsetModel(new DateComponents(1963, 11, 1), 37665, 1.9458580, 0.0011232);
            addOffsetModel(new DateComponents(1964,  1, 1), 38761, 3.2401300, 0.0012960);
            addOffsetModel(new DateComponents(1964,  4, 1), 38761, 3.3401300, 0.0012960);
            addOffsetModel(new DateComponents(1964,  9, 1), 38761, 3.4401300, 0.0012960);
            addOffsetModel(new DateComponents(1965,  1, 1), 38761, 3.5401300, 0.0012960);
            addOffsetModel(new DateComponents(1965,  3, 1), 38761, 3.6401300, 0.0012960);
            addOffsetModel(new DateComponents(1965,  7, 1), 38761, 3.7401300, 0.0012960);
            addOffsetModel(new DateComponents(1965,  9, 1), 38761, 3.8401300, 0.0012960);
            addOffsetModel(new DateComponents(1966,  1, 1), 39126, 4.3131700, 0.0025920);
            addOffsetModel(new DateComponents(1968,  2, 1), 39126, 4.2131700, 0.0025920);

            // add leap second entries in chronological order
            for (Map.Entry<DateComponents, Integer> entry : entries.entrySet()) {
                addOffsetModel(entry.getKey(), 0, entry.getValue(), 0);
            }

        }

        /** Retrieve the generated offsets.
         *
         * @return the {@link UTCTAIOffset}s.
         */
        public List<UTCTAIOffset> getOffsets() {
            return this.offsets;
        }

        /** Add an offset model.
         * <p>
         * This method <em>must</em> be called in chronological order.
         * </p>
         * @param date date of the constant offset model start
         * @param mjdRef reference date of the linear model as a modified julian day
         * @param offset offset at reference date in seconds (TAI minus UTC)
         * @param slope offset slope in seconds per UTC day (TAI minus UTC / dUTC)
         */
        private void addOffsetModel(final DateComponents date, final int mjdRef,
                                    final double offset, final double slope) {

            final TimeScale tai = TimeScalesFactory.getTAI();

            // start of the leap
            final UTCTAIOffset previous    = offsets.isEmpty() ? null : offsets.get(offsets.size() - 1);
            final double previousOffset    = (previous == null) ? 0.0 : previous.getOffset(date, TimeComponents.H00);
            final AbsoluteDate leapStart   = new AbsoluteDate(date, tai).shiftedBy(previousOffset);

            // end of the leap
            final double startOffset       = offset + slope * (date.getMJD() - mjdRef);
            final AbsoluteDate leapEnd     = new AbsoluteDate(date, tai).shiftedBy(startOffset);

            // leap computed at leap start and in UTC scale
            final double normalizedSlope   = slope / Constants.JULIAN_DAY;
            final double leap              = leapEnd.durationFrom(leapStart) / (1 + normalizedSlope);

            if (previous != null) {
                previous.setValidityEnd(leapStart);
            }
            offsets.add(new UTCTAIOffset(leapStart, date.getMJD(), leap, offset, mjdRef, normalizedSlope));

        }

    }

    /** {@inheritDoc} */
    public double offsetFromTAI(final AbsoluteDate date) {
        if (cache.getEarliest().getDate().compareTo(date) > 0) {
            // the date is before the first known leap
            return 0;
        } else if (cache.getLatest().getDate().compareTo(date) < 0) {
            // the date is after the last known leap
            return -cache.getLatest().getOffset(date);
        } else {
            // the date is nominally bracketed by two leaps
            try {
                return -cache.getNeighbors(date).get(0).getOffset(date);
            } catch (TimeStampedCacheException tce) {
                // this should never happen as boundaries have been handled in the previous statements
                throw OrekitException.createInternalError(tce);
            }
        }
    }

    /** {@inheritDoc} */
    public double offsetToTAI(final DateComponents date,
                              final TimeComponents time) {

        if (cache.getEarliest().getMJD() > date.getMJD()) {
            // the date is before the first known leap
            return 0;
        } else if (cache.getLatest().getMJD() <= date.getMJD()) {
            // the date is after the last known leap
            return cache.getLatest().getOffset(date, time);
        } else {
            // the date is nominally bracketed by two leaps
            try {
                // find close neighbors, assuming date in TAI, i.e a date earlier than real UTC date
                final List<UTCTAIOffset> neighbors =
                        cache.getNeighbors(new AbsoluteDate(date, time, TimeScalesFactory.getTAI()));
                if (neighbors.get(1).getMJD() <= date.getMJD()) {
                    // the date is in fact just after a leap second!
                    return neighbors.get(1).getOffset(date, time);
                } else {
                    return neighbors.get(0).getOffset(date, time);
                }
            } catch (TimeStampedCacheException tce) {
                // this should never happen as boundaries have been handled in the previous statements
                throw OrekitException.createInternalError(tce);
            }
        }

    }

    /** {@inheritDoc} */
    public String getName() {
        return "UTC";
    }

    /** {@inheritDoc} */
    public String toString() {
        return getName();
    }

    /** Get the date of the first known leap second.
     * @return date of the first known leap second
     */
    public AbsoluteDate getFirstKnownLeapSecond() {
        return cache.getEarliest().getDate();
    }

    /** Get the date of the last known leap second.
     * @return date of the last known leap second
     */
    public AbsoluteDate getLastKnownLeapSecond() {
        return cache.getLatest().getDate();
    }

    /** Check if date is within a leap second introduction.
     * @param date date to check
     * @return true if time is within a leap second introduction
     */
    public boolean insideLeap(final AbsoluteDate date) {
        if (cache.getEarliest().getDate().compareTo(date) > 0) {
            // the date is before the first known leap
            return false;
        } else if (cache.getLatest().getDate().compareTo(date) < 0) {
            // the date is after the last known leap
            return date.compareTo(cache.getLatest().getValidityStart()) < 0;
        } else {
            // the date is nominally bracketed by two leaps
            try {
                return date.compareTo(cache.getNeighbors(date).get(0).getValidityStart()) < 0;
            } catch (TimeStampedCacheException tce) {
                // this should never happen as boundaries have been handled in the previous statements
                throw OrekitException.createInternalError(tce);
            }
        }
    }

    /** Get the value of the previous leap.
     * @param date date to check
     * @return value of the previous leap
     */
    public double getLeap(final AbsoluteDate date) {
        if (cache.getEarliest().getDate().compareTo(date) > 0) {
            return 0;
        } else if (cache.getLatest().getDate().compareTo(date) < 0) {
            // the date is after the last known leap
            return cache.getLatest().getLeap();
        } else {
            // the date is nominally bracketed by two leaps
            try {
                return cache.getNeighbors(date).get(0).getLeap();
            } catch (TimeStampedCacheException tce) {
                // this should never happen as boundaries have been handled in the previous statements
                throw OrekitException.createInternalError(tce);
            }
        }
    }

    /** Replace the instance with a data transfer object for serialization.
     * <p>
     * This intermediate class serializes only the frame key.
     * </p>
     * @return data transfer object that will be serialized
     */
    private Object writeReplace() {
        return new DataTransferObject();
    }

    /** Internal class used only for serialization. */
    private static class DataTransferObject implements Serializable {

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

        /** Replace the deserialized data transfer object with a {@link UTCScale}.
         * @return replacement {@link UTCScale}
         */
        private Object readResolve() {
            try {
                return TimeScalesFactory.getUTC();
            } catch (OrekitException oe) {
                throw OrekitException.createInternalError(oe);
            }
        }

    }

}