GPSDate.java

/* Copyright 2002-2019 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.List;
import java.util.concurrent.atomic.AtomicReference;

import org.hipparchus.util.FastMath;
import org.orekit.frames.EOPEntry;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;

/** Container for date in GPS form.
 * @author Luc Maisonobe
 * @see AbsoluteDate
 * @since 9.3
 */
public class GPSDate implements Serializable, TimeStamped {

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

    /** Duration of a week in days. */
    private static final int WEEK_D = 7;

    /** Duration of a week in seconds. */
    private static final double WEEK_S = WEEK_D * Constants.JULIAN_DAY;

    /** Number of weeks in one rollover cycle. */
    private static final int CYCLE_W = 1024;

    /** Number of days in one rollover cycle. */
    private static final int CYCLE_D = WEEK_D * CYCLE_W;

    /** Conversion factor from seconds to milliseconds. */
    private static final double S_TO_MS = 1000.0;

    /** Reference date for ensuring continuity across GPS week rollover.
     * @since 9.3.1
     */
    private static AtomicReference<DateComponents> rolloverReference = new AtomicReference<DateComponents>(null);

    /** Week number since {@link AbsoluteDate#GPS_EPOCH GPS epoch}. */
    private final int weekNumber;

    /** Number of milliseconds since week start. */
    private final double milliInWeek;

    /** Corresponding date. */
    private final transient AbsoluteDate date;

    /** Build an instance corresponding to a GPS date.
     * <p>
     * GPS dates are provided as a week number starting at {@link AbsoluteDate#GPS_EPOCH GPS epoch}
     * and as a number of milliseconds since week start.
     * </p>
     * <p>
     * Many interfaces provide only the 10 lower bits of the GPS week number, just as it comes from
     * the GPS signal. In other words they use a week number modulo 1024. In order to cope with
     * this, when the week number is smaller than 1024, this constructor assumes a modulo operation
     * has been performed and it will fix the week number according to the reference date set up for
     * handling rollover (see {@link #setRolloverReference(DateComponents) setRolloverReference(reference)}).
     * If the week number is 1024 or larger, it will be used without any correction.
     * </p>
     * @param weekNumber week number, either absolute or modulo 1024
     * @param milliInWeek number of milliseconds since week start
     */
    public GPSDate(final int weekNumber, final double milliInWeek) {

        final int day = (int) FastMath.floor(milliInWeek / (Constants.JULIAN_DAY * S_TO_MS));
        final double secondsInDay = milliInWeek / S_TO_MS - day * Constants.JULIAN_DAY;

        int w = weekNumber;
        DateComponents dc = new DateComponents(DateComponents.GPS_EPOCH, weekNumber * 7 + day);
        if (weekNumber < 1024) {

            DateComponents reference = rolloverReference.get();
            if (reference == null) {
                // lazy setting of a default reference, using end of EOP entries
                final UT1Scale       ut1       = TimeScalesFactory.getUT1(IERSConventions.IERS_2010, true);
                final List<EOPEntry> eop       = ut1.getEOPHistory().getEntries();
                final int            lastMJD   = eop.get(eop.size() - 1).getMjd();
                reference = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, lastMJD);
                rolloverReference.compareAndSet(null, reference);
            }

            // fix GPS week rollover
            while (dc.getJ2000Day() < reference.getJ2000Day() - CYCLE_D / 2) {
                dc = new DateComponents(dc, CYCLE_D);
                w += CYCLE_W;
            }

        }

        this.weekNumber  = w;
        this.milliInWeek = milliInWeek;

        date = new AbsoluteDate(dc, new TimeComponents(secondsInDay), TimeScalesFactory.getGPS());

    }

    /** Build an instance from an absolute date.
     * @param date absolute date to consider
     */
    public GPSDate(final AbsoluteDate date) {

        this.weekNumber  = (int) FastMath.floor(date.durationFrom(AbsoluteDate.GPS_EPOCH) / WEEK_S);
        final AbsoluteDate weekStart = new AbsoluteDate(AbsoluteDate.GPS_EPOCH, WEEK_S * weekNumber);
        this.milliInWeek = date.durationFrom(weekStart) * S_TO_MS;
        this.date        = date;

    }

    /** Set a reference date for ensuring continuity across GPS week rollover.
     * <p>
     * Instance created using the {@link #GPSDate(int, double) GPSDate(weekNumber, milliInWeek)}
     * constructor and with a week number between 0 and 1024 after this method has been called will
     * fix the week number to ensure they correspond to dates between {@code reference - 512 weeks}
     * and {@code reference + 512 weeks}.
     * </p>
     * <p>
     * If this method is never called, a default reference date for rollover will be set using
     * the date of the last known EOP entry retrieved from {@link UT1Scale#getEOPHistory() UT1}
     * time scale.
     * </p>
     * @param reference reference date for GPS week rollover
     * @see #getRolloverReference()
     * @see #GPSDate(int, double)
     * @since 9.3.1
     */
    public static void setRolloverReference(final DateComponents reference) {
        rolloverReference.set(reference);
    }

    /** Get the reference date ensuring continuity across GPS week rollover.
     * @return reference reference date for GPS week rollover
     * @see #setRolloverReference(AbsoluteDate)
     * @see #GPSDate(int, double)
     * @since 9.3.1
     */
    public static DateComponents getRolloverReference() {
        return rolloverReference.get();
    }

    /** Get the week number since {@link AbsoluteDate#GPS_EPOCH GPS epoch}.
     * <p>
     * The week number returned here has been fixed for GPS week rollover, i.e.
     * it may be larger than 1024.
     * </p>
     * @return week number since {@link AbsoluteDate#GPS_EPOCH GPS epoch}
     */
    public int getWeekNumber() {
        return weekNumber;
    }

    /** Get the number of milliseconds since week start.
     * @return number of milliseconds since week start
     */
    public double getMilliInWeek() {
        return milliInWeek;
    }

    /** {@inheritDoc} */
    @Override
    public AbsoluteDate getDate() {
        return date;
    }

    /** Replace the instance with a data transfer object for serialization.
     * @return data transfer object that will be serialized
     */
    private Object writeReplace() {
        return new DataTransferObject(weekNumber, milliInWeek);
    }

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

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

        /** Week number since {@link AbsoluteDate#GPS_EPOCH GPS epoch}. */
        private final int weekNumber;

        /** Number of milliseconds since week start. */
        private final double milliInWeek;

        /** Simple constructor.
         * @param weekNumber week number since {@link AbsoluteDate#GPS_EPOCH GPS epoch}
         * @param milliInWeek number of milliseconds since week start
         */
        DataTransferObject(final int weekNumber, final double milliInWeek) {
            this.weekNumber  = weekNumber;
            this.milliInWeek = milliInWeek;
        }

        /** Replace the deserialized data transfer object with a {@link GPSDate}.
         * @return replacement {@link GPSDate}
         */
        private Object readResolve() {
            return new GPSDate(weekNumber, milliInWeek);
        }

    }

}