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