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.files.rinex.section;
18  
19  import org.hipparchus.util.FastMath;
20  import org.orekit.errors.OrekitException;
21  import org.orekit.errors.OrekitMessages;
22  import org.orekit.files.rinex.utils.ParsingUtils;
23  import org.orekit.files.rinex.utils.RinexFileType;
24  import org.orekit.gnss.PredefinedTimeSystem;
25  import org.orekit.gnss.SatelliteSystem;
26  import org.orekit.time.AbsoluteDate;
27  import org.orekit.time.DateComponents;
28  import org.orekit.time.DateTimeComponents;
29  import org.orekit.time.Month;
30  import org.orekit.time.TimeComponents;
31  import org.orekit.time.TimeScale;
32  import org.orekit.time.TimeScales;
33  
34  import java.time.ZoneId;
35  import java.time.ZonedDateTime;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  /** Base container for Rinex headers.
40   * @since 12.0
41   */
42  public abstract class RinexBaseHeader {
43  
44      /** Pattern for splitting date, time and time zone. */
45      private static final Pattern SPLITTING_PATTERN = Pattern.compile("([0-9]+[/ -]?[0-9A-Za-z]+[/ -]?[0-9]+) +([0-9:]+) *([A-Z]+)?");
46  
47      /** Pattern for dates with month abbrevation. */
48      private static final Pattern DATE_DD_MMM_YY_PATTERN = Pattern.compile("([0-9]{1,2})-([A-Za-z]{3})-([0-9]{2,4})");
49  
50      /** Pattern for dates with month abbrevation.
51       * @since 14.0
52       */
53      private static final Pattern DATE_YYYY_MMM_DD_PATTERN = Pattern.compile("([0-9]{4})[- ]([A-Za-z]{3})[- ]([0-9]{1,2})");
54  
55      /** Pattern for dates in ISO-8601 complete representation (basic or extended). */
56      private static final Pattern DATE_ISO_8601_PATTERN = Pattern.compile("([0-9]{4})-?([0-9]{2})-?([0-9]{2})");
57  
58      /** Pattern for dates in european format. */
59      private static final Pattern DATE_EUROPEAN_PATTERN = Pattern.compile("([0-9]{2})/([0-9]{2})/([0-9]{2})");
60  
61      /** Pattern for time. */
62      private static final Pattern TIME_PATTERN = Pattern.compile("([0-9]{2}):?([0-9]{2})(?::?([0-9]{2}))?");
63  
64      /** Orekit program name.
65       * @since 14.0
66       */
67      private static final String OREKIT = "Orekit";
68  
69      /** User name property.
70       * @since 14.0
71       */
72      private static final String USER_NAME = "user.name";
73  
74      /** File type . */
75      private final RinexFileType fileType;
76  
77      /** Rinex format Version. */
78      private double formatVersion;
79  
80      /** Satellite System of the Rinex file (G/R/S/E/M). */
81      private SatelliteSystem satelliteSystem;
82  
83      /** Name of the program creating current file. */
84      private String programName;
85  
86      /** Name of the creator of the current file. */
87      private String runByName;
88  
89      /** Date of the file creation. */
90      private DateTimeComponents creationDateComponents;
91  
92      /** Time zone of the file creation. */
93      private String creationTimeZone;
94  
95      /** Creation date as absolute date. */
96      private AbsoluteDate creationDate;
97  
98      /** Receiver Number.
99       * @since 14.0
100      */
101     private String receiverNumber;
102 
103     /** Receiver Type.
104      * @since 14.0
105      */
106     private String receiverType;
107 
108     /** Receiver version.
109      * @since 14.0
110      */
111     private String receiverVersion;
112 
113     /** Number of leap seconds separating UTC and GNSS time systems.
114      * <p>
115      * This is really the number of leap seconds since GPS epoch
116      * on 1980-01-06.
117      * </p>
118      * @since 14.0
119      */
120     private int leapSecondsGNSS;
121 
122     /** Future or past leap seconds ΔtLSF (BNK).
123      * i.e. future leap second if the week and day number are in the future.
124      * @since 14.0
125      */
126     private int leapSecondsFuture;
127 
128     /** Respective leap second week number.
129      * For GPS, GAL, QZS and IRN, weeks since 6-Jan-1980.
130      * When BDS only file leap seconds specified, weeks since 1-Jan-2006
131      * @since 14.0
132      */
133     private int leapSecondsWeekNum;
134 
135     /** Respective leap second day number.
136      * @since 14.0
137      */
138     private int leapSecondsDayNum;
139 
140     /** Digital Object Identifier.
141      * @since 12.0
142      */
143     private String doi;
144 
145     /** License of use.
146      * @since 12.0
147      */
148     private String license;
149 
150     /** Station information.
151      * @since 12.0
152      */
153     private String stationInformation;
154 
155     /** Simple constructor.
156      * @param fileType file type
157      */
158     protected RinexBaseHeader(final RinexFileType fileType) {
159 
160         this.fileType      = fileType;
161         this.formatVersion = Double.NaN;
162 
163         // set default creation date to now
164         final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
165         setCreationDateComponents(new DateTimeComponents(new DateComponents(now.getYear(),
166                                                                             now.getMonthValue(),
167                                                                             now.getDayOfMonth()),
168                                                          new TimeComponents(now.getHour(),
169                                                                             now.getMinute(),
170                                                                             now.getSecond())));
171 
172         // set default program name to Orekit
173         setProgramName(OREKIT);
174 
175         // set default run-by name to user
176         setRunByName(System.getProperty(USER_NAME));
177 
178     }
179 
180     /**
181      * Get the file type.
182      * @return file type
183      */
184     public RinexFileType getFileType() {
185         return fileType;
186     }
187 
188     /**
189      * Getter for the format version.
190      * @return the format version
191      */
192     public double getFormatVersion() {
193         return formatVersion;
194     }
195 
196     /**
197      * Setter for the format version.
198      * @param formatVersion the format version to set
199      */
200     public void setFormatVersion(final double formatVersion) {
201         this.formatVersion = formatVersion;
202     }
203 
204     /**
205      * Getter for the satellite system.
206      * <p>
207      * Not specified for RINEX 2.X versions (value is null).
208      * </p>
209      * @return the satellite system
210      */
211     public SatelliteSystem getSatelliteSystem() {
212         return satelliteSystem;
213     }
214 
215     /**
216      * Setter for the satellite system.
217      * @param satelliteSystem the satellite system to set
218      */
219     public void setSatelliteSystem(final SatelliteSystem satelliteSystem) {
220         this.satelliteSystem = satelliteSystem;
221     }
222 
223     /**
224      * Parse satellite system.
225      * @param line header line
226      * @param defaultSatelliteSystem satellite system to use if string is null or empty
227      * @return parsed satellite system
228      * @since 14.0
229      */
230     public abstract SatelliteSystem parseSatelliteSystem(String line, SatelliteSystem defaultSatelliteSystem);
231 
232     /**
233      * Getter for the program name.
234      * @return the program name
235      */
236     public String getProgramName() {
237         return programName;
238     }
239 
240     /**
241      * Setter for the program name.
242      * @param programName the program name to set
243      */
244     public void setProgramName(final String programName) {
245         this.programName = programName;
246     }
247 
248     /**
249      * Getter for the run/by name.
250      * @return the run/by name
251      */
252     public String getRunByName() {
253         return runByName;
254     }
255 
256     /**
257      * Setter for the run/by name.
258      * @param runByName the run/by name to set
259      */
260     public void setRunByName(final String runByName) {
261         this.runByName = runByName;
262     }
263 
264     /**
265      * Getter for the creation date of the file as a string.
266      * @return the creation date
267      */
268     public DateTimeComponents getCreationDateComponents() {
269         return creationDateComponents;
270     }
271 
272     /**
273      * Setter for the creation date as a string.
274      * @param creationDateComponents the creation date to set
275      */
276     public void setCreationDateComponents(final DateTimeComponents creationDateComponents) {
277         this.creationDateComponents = creationDateComponents;
278     }
279 
280     /**
281      * Getter for the creation time zone of the file as a string.
282      * @return the creation time zone as a string
283      */
284     public String getCreationTimeZone() {
285         return creationTimeZone;
286     }
287 
288     /**
289      * Setter for the creation time zone.
290      * @param creationTimeZone the creation time zone to set
291      */
292     public void setCreationTimeZone(final String creationTimeZone) {
293         this.creationTimeZone = creationTimeZone;
294     }
295 
296     /**
297      * Getter for the creation date.
298      * <p>
299      * The creation date seems to be mandatory, but we have seen several files
300      * missing it, even files created by IGS itself (in clock files, essentially).
301      * We accept these null dates to at least allow parsing the files
302      * as this header information does not really seem essential
303      * </p>
304      * @return the creation date
305      */
306     public AbsoluteDate getCreationDate() {
307         return creationDate;
308     }
309 
310     /**
311      * Setter for the creation date.
312      * @param creationDate the creation date to set
313      */
314     public void setCreationDate(final AbsoluteDate creationDate) {
315         this.creationDate = creationDate;
316     }
317 
318     /** Set the number of the receiver.
319      * @param receiverNumber number of the receiver
320      */
321     public void setReceiverNumber(final String receiverNumber) {
322         this.receiverNumber = receiverNumber;
323     }
324 
325     /** Get the number of the receiver.
326      * @return number of the receiver
327      */
328     public String getReceiverNumber() {
329         return receiverNumber;
330     }
331 
332     /** Set the type of the receiver.
333      * @param receiverType type of the receiver
334      */
335     public void setReceiverType(final String receiverType) {
336         this.receiverType = receiverType;
337     }
338 
339     /** Get the type of the receiver.
340      * @return type of the receiver
341      */
342     public String getReceiverType() {
343         return receiverType;
344     }
345 
346     /** Set the version of the receiver.
347      * @param receiverVersion version of the receiver
348      */
349     public void setReceiverVersion(final String receiverVersion) {
350         this.receiverVersion = receiverVersion;
351     }
352 
353     /** Get the version of the receiver.
354      * @return version of the receiver
355      */
356     public String getReceiverVersion() {
357         return receiverVersion;
358     }
359 
360     /** Getter for the number of leap second for GNSS time scales.
361      * @return the number of leap seconds for GNSS time scales
362      * @since 14.0
363      */
364     public int getLeapSecondsGNSS() {
365         return leapSecondsGNSS;
366     }
367 
368     /** Setter for the number of leap seconds for GNSS time scales.
369      * @param leapSecondsGNSS the number of leap seconds for GNSS time scales to set
370      * @since 14.0
371      */
372     public void setLeapSecondsGNSS(final int leapSecondsGNSS) {
373         this.leapSecondsGNSS = leapSecondsGNSS;
374     }
375 
376     /** Set the future or past leap seconds.
377      * @param leapSecondsFuture Future or past leap seconds
378      * @since 14.0
379      */
380     public void setLeapSecondsFuture(final int leapSecondsFuture) {
381         this.leapSecondsFuture = leapSecondsFuture;
382     }
383 
384     /** Get the future or past leap seconds.
385      * @return Future or past leap seconds
386      * @since 14.0
387      */
388     public int getLeapSecondsFuture() {
389         return leapSecondsFuture;
390     }
391 
392     /** Set the respective leap second week number.
393      * @param leapSecondsWeekNum Respective leap second week number
394      * @since 14.0
395      */
396     public void setLeapSecondsWeekNum(final int leapSecondsWeekNum) {
397         this.leapSecondsWeekNum = leapSecondsWeekNum;
398     }
399 
400     /** Get the respective leap second week number.
401      * @return Respective leap second week number
402      * @since 14.0
403      */
404     public int getLeapSecondsWeekNum() {
405         return leapSecondsWeekNum;
406     }
407 
408     /** Set the respective leap second day number.
409      * @param leapSecondsDayNum Respective leap second day number
410      * @since 14.0
411      */
412     public void setLeapSecondsDayNum(final int leapSecondsDayNum) {
413         this.leapSecondsDayNum = leapSecondsDayNum;
414     }
415 
416     /** Get the respective leap second day number.
417      * @return Respective leap second day number
418      * @since 14.0
419      */
420     public int getLeapSecondsDayNum() {
421         return leapSecondsDayNum;
422     }
423 
424     /**
425      *  Getter for the Digital Object Information.
426      * @return the Digital Object Information
427      * @since 12.0
428      */
429     public String getDoi() {
430         return doi;
431     }
432 
433     /**
434      * Setter for the Digital Object Information.
435      * @param doi the Digital Object Information to set
436      * @since 12.0
437      */
438     public void setDoi(final String doi) {
439         this.doi = doi;
440     }
441 
442     /**
443      *  Getter for the license of use.
444      * @return the license of use
445      * @since 12.0
446      */
447     public String getLicense() {
448         return license;
449     }
450 
451     /**
452      * Setter for the license of use.
453      * @param license the license of use
454      * @since 12.0
455      */
456     public void setLicense(final String license) {
457         this.license = license;
458     }
459 
460     /**
461      *  Getter for the station information.
462      * @return the station information
463      * @since 12.0
464      */
465     public String getStationInformation() {
466         return stationInformation;
467     }
468 
469     /**
470      * Setter for the station information.
471      * @param stationInformation the station information to set
472      * @since 12.0
473      */
474     public void setStationInformation(final String stationInformation) {
475         this.stationInformation = stationInformation;
476     }
477 
478     /** Parse version, file type and satellite system.
479      * @param line line to parse
480      * @param defaultSatelliteSystem satellite system to use if string is null or empty
481      * @param name file name (for error message generation)
482      * @param supportedVersions supported versions
483      * @since 14.0
484      */
485     public void parseVersionFileTypeSatelliteSystem(final String line, final SatelliteSystem defaultSatelliteSystem,
486                                                     final String name, final double... supportedVersions) {
487 
488         // Rinex version
489         final double parsedVersion = ParsingUtils.parseDouble(line, 0, 9);
490 
491         boolean found = false;
492         for (final double supported : supportedVersions) {
493             if (FastMath.abs(parsedVersion - supported) < 1.0e-4) {
494                 found = true;
495                 break;
496             }
497         }
498         if (!found) {
499             final StringBuilder builder = new StringBuilder();
500             for (final double supported : supportedVersions) {
501                 if (builder.length() > 0) {
502                     builder.append(", ");
503                 }
504                 builder.append(supported);
505             }
506             throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT_VERSION,
507                                       parsedVersion, name, builder.toString());
508         }
509         setFormatVersion(parsedVersion);
510 
511         // file type
512         checkType(line, name);
513 
514         // Satellite system
515         setSatelliteSystem(parseSatelliteSystem(line, defaultSatelliteSystem));
516 
517     }
518 
519     /** Parse program, run/by and date.
520      * @param line line to parse
521      * @param timeScales the set of time scales used for parsing dates
522      * @since 14.0
523      */
524     public abstract void parseProgramRunByDate(String line, TimeScales timeScales);
525 
526     /** Parse program, run/by and date.
527      * @param prgm  PGM field
528      * @param run  RUN BY field
529      * @param date  date field
530      * @param timeScales the set of time scales used for parsing dates
531      * @since 14.0
532      */
533     protected void parseProgramRunByDate(final String prgm, final String run, final String date,
534                                          final TimeScales timeScales) {
535 
536         // Name of the generating program
537         setProgramName(prgm);
538 
539         // Name of the run/by name
540         setRunByName(run);
541 
542         // there are several variations for date formatting in the PGM / RUN BY / DATE line
543 
544         // in versions 2.x, the pattern is expected to be:
545         // XXRINEXO V9.9       AIUB                24-MAR-01 14:43     PGM / RUN BY / DATE
546         // however, we have also found this:
547         // teqc  2016Nov7      root                20180130 10:38:06UTCPGM / RUN BY / DATE
548         // BJFMTLcsr           UTCSR               2007-09-30 05:30:06 PGM / RUN BY / DATE
549         // NEODIS              TAS                 27/05/22 10:28      PGM / RUN BY / DATE
550 
551         // in versions 3.x, the pattern is expected to be:
552         // sbf2rin-11.3.3                          20180130 002558 LCL PGM / RUN BY / DATE
553         // however, we have also found:
554         // NetR9 5.03          Receiver Operator   11-JAN-16 00:00:00  PGM / RUN BY / DATE
555 
556         // in clock files, we have found patterns like:
557         // CLKRINEX V1.0       NRCan               1-Mar-2000 20:36    PGM / RUN BY / DATE
558         // tdp2clk v1.13       JPL                 2002 Jan 3 13:36:17 PGM / RUN BY / DATE
559 
560         // so we cannot rely on the format version, we have to check several variations
561         final Matcher splittingMatcher = SPLITTING_PATTERN.matcher(date);
562         if (splittingMatcher.matches()) {
563 
564             // date part
565             final DateComponents dc;
566             final Matcher abbrev1Matcher = DATE_DD_MMM_YY_PATTERN.matcher(splittingMatcher.group(1));
567             if (abbrev1Matcher.matches()) {
568                 final int rawYear = Integer.parseInt(abbrev1Matcher.group(3));
569                 // hoping this obsolete format will not be used past year 2079…
570                 dc = new DateComponents(rawYear < 100 ? ParsingUtils.convert2DigitsYear(rawYear) : rawYear,
571                                         Month.parseMonth(abbrev1Matcher.group(2)).getNumber(),
572                                         Integer.parseInt(abbrev1Matcher.group(1)));
573             } else {
574                 final Matcher abbrev2Matcher = DATE_YYYY_MMM_DD_PATTERN.matcher(splittingMatcher.group(1));
575                 if (abbrev2Matcher.matches()) {
576                     dc = new DateComponents(Integer.parseInt(abbrev2Matcher.group(1)),
577                                             Month.parseMonth(abbrev2Matcher.group(2)).getNumber(),
578                                             Integer.parseInt(abbrev2Matcher.group(3)));
579                 } else {
580                     final Matcher isoMatcher = DATE_ISO_8601_PATTERN.matcher(splittingMatcher.group(1));
581                     if (isoMatcher.matches()) {
582                         dc = new DateComponents(Integer.parseInt(isoMatcher.group(1)),
583                                                Integer.parseInt(isoMatcher.group(2)),
584                                                Integer.parseInt(isoMatcher.group(3)));
585                     } else {
586                         final Matcher europeanMatcher = DATE_EUROPEAN_PATTERN.matcher(splittingMatcher.group(1));
587                         if (europeanMatcher.matches()) {
588                             dc = new DateComponents(
589                                 ParsingUtils.convert2DigitsYear(Integer.parseInt(europeanMatcher.group(3))),
590                                 Integer.parseInt(europeanMatcher.group(2)),
591                                 Integer.parseInt(europeanMatcher.group(1)));
592                         } else {
593                             dc = null;
594                         }
595                     }
596                 }
597             }
598 
599             // time part
600             final TimeComponents tc;
601             final Matcher timeMatcher = TIME_PATTERN.matcher(splittingMatcher.group(2));
602             if (timeMatcher.matches()) {
603                 tc = new TimeComponents(Integer.parseInt(timeMatcher.group(1)),
604                                         Integer.parseInt(timeMatcher.group(2)),
605                                         timeMatcher.group(3) != null ? Integer.parseInt(timeMatcher.group(3)) : 0);
606             } else {
607                 tc = TimeComponents.H00;
608             }
609 
610             // zone part
611             final String zone = splittingMatcher.group(3);
612             setCreationTimeZone(zone == null ? "" : zone);
613 
614             if (dc == null) {
615                 // despite the creation date seems to be mandatory, we have seen several files
616                 // missing it, even files created by IGS itself (in clock files, essentially).
617                 // We accept these null dates to at least allow parsing the files
618                 // as this header information does not really seem essential
619                 setCreationDate(null);
620             } else {
621                 // we successfully parsed everything
622                 final DateTimeComponents dtc = new DateTimeComponents(dc, tc);
623                 setCreationDateComponents(dtc);
624                 final TimeScale timeScale = zone == null ?
625                                             timeScales.getUTC() :
626                                             PredefinedTimeSystem.parseTimeSystem(zone).getTimeScale(timeScales);
627                 setCreationDate(new AbsoluteDate(dtc, timeScale));
628             }
629 
630         } else {
631             setCreationDate(null);
632             setCreationTimeZone("");
633         }
634 
635     }
636 
637     /** Check file type.
638      * @param line header line
639      * @param name file name (for error message)
640      * @since 14.0
641      */
642     public abstract void checkType(String line, String name);
643 
644     /** Check file type.
645      * @param line header line
646      * @param typeIndex index of the file type in the line
647      * @param name file name (for error message)
648      * @since 14.0
649      */
650     protected void checkType(final String line, final int typeIndex, final String name) {
651         if (fileType != RinexFileType.parseRinexFileType(line.substring(typeIndex, typeIndex + 1))) {
652             throw new OrekitException(OrekitMessages.WRONG_PARSING_TYPE, name);
653         }
654     }
655 
656     /** Get the index of the header label.
657      * @return index of the header label
658      * @since 14.0
659      */
660     public abstract int getLabelIndex();
661 
662     /** Check if a label is found in a line.
663      * @param label label to check
664      * @param line header line
665      * @return true if label is found in the header line
666      * @since 14.0
667      */
668     public abstract boolean matchFound(Label label, String line);
669 
670 }