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