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