1   /* Copyright 2002-2022 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.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.concurrent.atomic.AtomicReference;
24  
25  import org.hipparchus.util.FastMath;
26  import org.orekit.annotation.DefaultDataContext;
27  import org.orekit.data.DataContext;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitMessages;
30  import org.orekit.frames.EOPEntry;
31  import org.orekit.gnss.SatelliteSystem;
32  import org.orekit.utils.Constants;
33  import org.orekit.utils.IERSConventions;
34  
35  /** Container for date in GNSS form.
36   * <p> This class can be used to handle {@link SatelliteSystem#GPS GPS},
37   * {@link SatelliteSystem#GALILEO Galileo}, {@link SatelliteSystem#BEIDOU BeiDou}
38   * and {@link SatelliteSystem#QZSS QZSS} dates. </p>
39   * @author Luc Maisonobe (original code)
40   * @author Bryan Cazabonne (generalization to all GNSS constellations)
41   * @see AbsoluteDate
42   */
43  public class GNSSDate implements Serializable, TimeStamped {
44  
45      /** Serializable UID. */
46      private static final long serialVersionUID = 201902141L;
47  
48      /** Duration of a week in days. */
49      private static final int WEEK_D = 7;
50  
51      /** Duration of a week in seconds. */
52      private static final double WEEK_S = WEEK_D * Constants.JULIAN_DAY;
53  
54      /** Conversion factor from seconds to milliseconds. */
55      private static final double S_TO_MS = 1000.0;
56  
57      /** Reference date for ensuring continuity across GNSS week rollover.
58       * @since 9.3.1
59       */
60      private static AtomicReference<DateComponents> rolloverReference = new AtomicReference<DateComponents>(null);
61  
62      /** Week number since the GNSS reference epoch. */
63      private final int weekNumber;
64  
65      /** Number of milliseconds since week start. */
66      private final double milliInWeek;
67  
68      /** Satellite system to consider. */
69      private final SatelliteSystem system;
70  
71      /** Corresponding date. */
72      private final transient AbsoluteDate date;
73  
74      /** Build an instance corresponding to a GNSS date.
75       * <p>
76       * GNSS dates are provided as a week number starting at
77       * the GNSS reference epoch and as a number of milliseconds
78       * since week start.
79       * </p>
80       * <p>
81       * Many interfaces provide week number modulo the constellation week cycle. In order to cope with
82       * this, when the week number is smaller than the week cycle, this constructor assumes a modulo operation
83       * has been performed and it will fix the week number according to the reference date set up for
84       * handling rollover (see {@link #setRolloverReference(DateComponents) setRolloverReference(reference)}).
85       * If the week number is equal to the week cycle or larger, it will be used without any correction.
86       * </p>
87       *
88       * <p>This method uses the {@link DataContext#getDefault() default data context}.
89       *
90       * @param weekNumber week number
91       * @param milliInWeek number of milliseconds since week start
92       * @param system satellite system to consider
93       * @see #GNSSDate(int, double, SatelliteSystem, TimeScales)
94       */
95      @DefaultDataContext
96      public GNSSDate(final int weekNumber, final double milliInWeek,
97                      final SatelliteSystem system) {
98          this(weekNumber, milliInWeek, system, DataContext.getDefault().getTimeScales());
99      }
100 
101     /**
102      * Build an instance corresponding to a GNSS date.
103      * <p>
104      * GNSS dates are provided as a week number starting at the GNSS reference epoch and
105      * as a number of milliseconds since week start.
106      * </p>
107      * <p>
108      * Many interfaces provide week number modulo the constellation week cycle. In order
109      * to cope with this, when the week number is smaller than the week cycle, this
110      * constructor assumes a modulo operation has been performed and it will fix the week
111      * number according to the reference date set up for handling rollover (see {@link
112      * #setRolloverReference(DateComponents) setRolloverReference(reference)}). If the
113      * week number is equal to the week cycle or larger, it will be used without any
114      * correction.
115      * </p>
116      *
117      * @param weekNumber  week number
118      * @param milliInWeek number of milliseconds since week start
119      * @param system      satellite system to consider
120      * @param timeScales  the set of time scales. Used to retrieve the appropriate time
121      *                    scale for the given {@code system}.
122      * @since 10.1
123      */
124     public GNSSDate(final int weekNumber,
125                     final double milliInWeek,
126                     final SatelliteSystem system,
127                     final TimeScales timeScales) {
128 
129         final int day = (int) FastMath.floor(milliInWeek / (Constants.JULIAN_DAY * S_TO_MS));
130         final double secondsInDay = milliInWeek / S_TO_MS - day * Constants.JULIAN_DAY;
131 
132         int w = weekNumber;
133         DateComponents dc = new DateComponents(getWeekReferenceDateComponents(system), weekNumber * 7 + day);
134         final int cycleW = GNSSDateType.getRollOverWeek(system);
135         if (weekNumber < cycleW) {
136 
137             DateComponents reference = rolloverReference.get();
138             if (reference == null) {
139                 // lazy setting of a default reference, using end of EOP entries
140                 final UT1Scale       ut1       = timeScales.getUT1(IERSConventions.IERS_2010, true);
141                 final List<EOPEntry> eop       = ut1.getEOPHistory().getEntries();
142                 final int            lastMJD   = eop.get(eop.size() - 1).getMjd();
143                 reference = new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, lastMJD);
144                 rolloverReference.compareAndSet(null, reference);
145             }
146 
147             // fix GNSS week rollover
148             final int cycleD = WEEK_D * cycleW;
149             while (dc.getJ2000Day() < reference.getJ2000Day() - cycleD / 2) {
150                 dc = new DateComponents(dc, cycleD);
151                 w += cycleW;
152             }
153 
154         }
155 
156         this.weekNumber  = w;
157         this.milliInWeek = milliInWeek;
158         this.system      = system;
159 
160         date = new AbsoluteDate(dc, new TimeComponents(secondsInDay), getTimeScale(system, timeScales));
161 
162     }
163 
164     /** Build an instance from an absolute date.
165      *
166      * <p>This method uses the {@link DataContext#getDefault() default data context}.
167      *
168      * @param date absolute date to consider
169      * @param system satellite system to consider
170      * @see #GNSSDate(AbsoluteDate, SatelliteSystem, TimeScales)
171      */
172     @DefaultDataContext
173     public GNSSDate(final AbsoluteDate date, final SatelliteSystem system) {
174         this(date, system, DataContext.getDefault().getTimeScales());
175     }
176 
177     /**
178      * Build an instance from an absolute date.
179      *
180      * @param date       absolute date to consider
181      * @param system     satellite system to consider
182      * @param timeScales the set of time scales. Used to retrieve the appropriate time
183      *                   scale for the given {@code system}.
184      * @since 10.1
185      */
186     public GNSSDate(final AbsoluteDate date,
187                     final SatelliteSystem system,
188                     final TimeScales timeScales) {
189 
190         this.system = system;
191         final AbsoluteDate epoch = getWeekReferenceAbsoluteDate(system, timeScales);
192         this.weekNumber  = (int) FastMath.floor(date.durationFrom(epoch) / WEEK_S);
193         final AbsoluteDate weekStart = new AbsoluteDate(epoch, WEEK_S * weekNumber);
194         this.milliInWeek = date.durationFrom(weekStart) * S_TO_MS;
195         this.date        = date;
196 
197     }
198 
199     /** Set a reference date for ensuring continuity across GNSS week rollover.
200      * <p>
201      * Instance created using the {@link #GNSSDate(int, double, SatelliteSystem) GNSSDate(weekNumber, milliInWeek, system)}
202      * constructor and with a week number between 0 and the constellation week cycle (cycleW) after this method has been called will
203      * fix the week number to ensure they correspond to dates between {@code reference - cycleW / 2 weeks}
204      * and {@code reference + cycleW / 2 weeks}.
205      * </p>
206      * <p>
207      * If this method is never called, a default reference date for rollover will be set using
208      * the date of the last known EOP entry retrieved from {@link UT1Scale#getEOPHistory() UT1}
209      * time scale.
210      * </p>
211      * @param reference reference date for GNSS week rollover
212      * @see #getRolloverReference()
213      * @see #GNSSDate(int, double, SatelliteSystem)
214      * @since 9.3.1
215      */
216     public static void setRolloverReference(final DateComponents reference) {
217         rolloverReference.set(reference);
218     }
219 
220     /** Get the reference date ensuring continuity across GNSS week rollover.
221      * @return reference reference date for GNSS week rollover
222      * @see #setRolloverReference(DateComponents)
223      * @see #GNSSDate(int, double, SatelliteSystem)
224      * @since 9.3.1
225      */
226     public static DateComponents getRolloverReference() {
227         return rolloverReference.get();
228     }
229 
230     /** Get the week number since the GNSS reference epoch.
231      * <p>
232      * The week number returned here has been fixed for GNSS week rollover, i.e.
233      * it may be larger than the corresponding week cycle of the constellation.
234      * </p>
235      * @return week number since since the GNSS reference epoch
236      */
237     public int getWeekNumber() {
238         return weekNumber;
239     }
240 
241     /** Get the number of milliseconds since week start.
242      * @return number of milliseconds since week start
243      */
244     public double getMilliInWeek() {
245         return milliInWeek;
246     }
247 
248     /** {@inheritDoc} */
249     @Override
250     public AbsoluteDate getDate() {
251         return date;
252     }
253 
254     /** Get the time scale related to the given satellite system.
255      * @param satellite satellite system
256      * @param timeScales set of time scales.
257      * @return the time scale
258      */
259     private TimeScale getTimeScale(final SatelliteSystem satellite,
260                                    final TimeScales timeScales) {
261         switch (satellite) {
262             case GPS     : return timeScales.getGPS();
263             case GALILEO : return timeScales.getGST();
264             case QZSS    : return timeScales.getQZSS();
265             case BEIDOU  : return timeScales.getBDT();
266             case IRNSS   : return timeScales.getIRNSS();
267             case SBAS    : return timeScales.getGPS();
268             default      : throw new OrekitException(OrekitMessages.INVALID_SATELLITE_SYSTEM, satellite);
269         }
270     }
271 
272     /** Get the reference epoch of the week number for the given satellite system.
273      * <p> Returned parameter is an AbsoluteDate. </p>
274      * @param satellite satellite system
275      * @param timeScales set of time scales.
276      * @return the reference epoch
277      */
278     private AbsoluteDate getWeekReferenceAbsoluteDate(final SatelliteSystem satellite,
279                                                       final TimeScales timeScales) {
280         switch (satellite) {
281             case GPS     : return timeScales.getGpsEpoch();
282             case GALILEO : return timeScales.getGalileoEpoch();
283             case QZSS    : return timeScales.getQzssEpoch();
284             case BEIDOU  : return timeScales.getBeidouEpoch();
285             case IRNSS   : return timeScales.getIrnssEpoch();
286             case SBAS    : return timeScales.getGpsEpoch();
287             default      : throw new OrekitException(OrekitMessages.INVALID_SATELLITE_SYSTEM, satellite);
288         }
289     }
290 
291     /** Get the reference epoch of the week number for the given satellite system.
292      * <p> Returned parameter is a DateComponents. </p>
293      * @param satellite satellite system
294      * @return the reference epoch
295      */
296     private DateComponents getWeekReferenceDateComponents(final SatelliteSystem satellite) {
297         switch (satellite) {
298             case GPS     : return DateComponents.GPS_EPOCH;
299             case GALILEO : return DateComponents.GALILEO_EPOCH;
300             case QZSS    : return DateComponents.QZSS_EPOCH;
301             case BEIDOU  : return DateComponents.BEIDOU_EPOCH;
302             case IRNSS   : return DateComponents.IRNSS_EPOCH;
303             case SBAS    : return DateComponents.GPS_EPOCH;
304             default      : throw new OrekitException(OrekitMessages.INVALID_SATELLITE_SYSTEM, satellite);
305         }
306     }
307 
308     /** Replace the instance with a data transfer object for serialization.
309      * @return data transfer object that will be serialized
310      */
311     @DefaultDataContext
312     private Object writeReplace() {
313         return new DataTransferObject(weekNumber, milliInWeek, system);
314     }
315 
316     /** Internal class used only for serialization. */
317     @DefaultDataContext
318     private static class DataTransferObject implements Serializable {
319 
320         /** Serializable UID. */
321         private static final long serialVersionUID = 201902141L;
322 
323         /** Week number since the GNSS reference epoch. */
324         private final int weekNumber;
325 
326         /** Number of milliseconds since week start. */
327         private final double milliInWeek;
328 
329         /** Satellite system to consider. */
330         private final SatelliteSystem system;
331 
332         /** Simple constructor.
333          * @param weekNumber week number since the GNSS reference epoch
334          * @param milliInWeek number of milliseconds since week start
335          * @param system satellite system to consider
336          */
337         DataTransferObject(final int weekNumber, final double milliInWeek,
338                            final SatelliteSystem system) {
339             this.weekNumber  = weekNumber;
340             this.milliInWeek = milliInWeek;
341             this.system      = system;
342         }
343 
344         /** Replace the deserialized data transfer object with a {@link GNSSDate}.
345          * @return replacement {@link GNSSDate}
346          */
347         private Object readResolve() {
348             return new GNSSDate(weekNumber, milliInWeek, system);
349         }
350 
351     }
352 
353     /** Enumerate for GNSS data. */
354     private enum GNSSDateType {
355 
356         /** GPS. */
357         GPS(SatelliteSystem.GPS, 1024),
358 
359         /** Galileo. */
360         GALILEO(SatelliteSystem.GALILEO, 4096),
361 
362         /** QZSS. */
363         QZSS(SatelliteSystem.QZSS, 1024),
364 
365         /** BeiDou. */
366         BEIDOU(SatelliteSystem.BEIDOU, 8192),
367 
368         /** IRNSS. */
369         IRNSS(SatelliteSystem.IRNSS, 1024),
370 
371         /** SBAS. */
372         SBAS(SatelliteSystem.SBAS, 1024);
373 
374         /** Map for the number of week in one GNSS rollover cycle. */
375         private static final Map<SatelliteSystem, Integer> CYCLE_MAP = new HashMap<SatelliteSystem, Integer>();
376         static {
377             for (final GNSSDateType type : values()) {
378                 final int             val       = type.getRollOverCycle();
379                 final SatelliteSystem satellite = type.getSatelliteSystem();
380                 CYCLE_MAP.put(satellite, val);
381             }
382         }
383 
384         /** Number of week in one rollover cycle. */
385         private final int numberOfWeek;
386 
387         /** Satellite system. */
388         private final SatelliteSystem satelliteSystem;
389 
390         /**
391          * Build a new instance.
392          *
393          * @param system satellite system
394          * @param rollover number of week in one rollover cycle
395          */
396         GNSSDateType(final SatelliteSystem system, final int rollover) {
397             this.satelliteSystem = system;
398             this.numberOfWeek    = rollover;
399         }
400 
401         /** Get the number of week in one rollover cycle.
402          * @return  the number of week in one rollover cycle
403          */
404         private int getRollOverCycle() {
405             return numberOfWeek;
406         }
407 
408         /** Get the satellite system.
409          * @return the satellite system
410          */
411         private SatelliteSystem getSatelliteSystem() {
412             return satelliteSystem;
413         }
414 
415         /** Get the number of week in one rollover cycle for the given satellite system.
416          *
417          * @param satellite satellite system
418          * @return the number of week in one rollover cycle for the given satellite system
419          */
420         private static int getRollOverWeek(final SatelliteSystem satellite) {
421             return CYCLE_MAP.get(satellite);
422         }
423 
424     }
425 }