1   /* Copyright 2022-2025 Luc Maisonobe
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.utils.parsing;
18  
19  import java.util.regex.Matcher;
20  import java.util.regex.Pattern;
21  
22  import org.hipparchus.util.FastMath;
23  import org.orekit.errors.OrekitException;
24  import org.orekit.errors.OrekitInternalError;
25  import org.orekit.errors.OrekitMessages;
26  import org.orekit.files.rinex.RinexFile;
27  import org.orekit.files.rinex.section.RinexBaseHeader;
28  import org.orekit.files.rinex.section.RinexComment;
29  import org.orekit.files.rinex.utils.RinexFileType;
30  import org.orekit.gnss.SatelliteSystem;
31  import org.orekit.gnss.TimeSystem;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.DateComponents;
34  import org.orekit.time.DateTimeComponents;
35  import org.orekit.time.Month;
36  import org.orekit.time.TimeComponents;
37  import org.orekit.time.TimeScale;
38  import org.orekit.time.TimeScales;
39  
40  /** Utilities for RINEX various messages files.
41   * @author Luc Maisonobe
42   * @since 12.0
43   *
44   */
45  public class RinexUtils {
46  
47      /** Index of label in header lines. */
48      public static final int LABEL_INDEX = 60;
49  
50      /** Pattern for splitting date, time and time zone. */
51      private static final Pattern SPLITTING_PATTERN = Pattern.compile("([0-9A-Za-z/-]+) *([0-9:]+) *([A-Z][A-Z0-9_-]*)?");
52  
53      /** Pattern for dates with month abbrevation. */
54      private static final Pattern DATE_DD_MMM_YY_PATTERN = Pattern.compile("([0-9]{2})-([A-Za-z]{3})-([0-9]{2})");
55  
56      /** Pattern for dates in ISO-8601 complete representation (basic or extended). */
57      private static final Pattern DATE_ISO_8601_PATTERN = Pattern.compile("([0-9]{4})-?([0-9]{2})-?([0-9]{2})");
58  
59      /** Pattern for dates in european format. */
60      private static final Pattern DATE_EUROPEAN_PATTERN = Pattern.compile("([0-9]{2})/([0-9]{2})/([0-9]{2})");
61  
62      /** Pattern for time. */
63      private static final Pattern TIME_PATTERN = Pattern.compile("([0-9]{2}):?([0-9]{2})(?::?([0-9]{2}))?");
64  
65      /** Private constructor.
66       * <p>This class is a utility class, it should neither have a public
67       * nor a default constructor. This private constructor prevents
68       * the compiler from generating one automatically.</p>
69       */
70      private RinexUtils() {
71      }
72  
73      /** Get the trimmed label from a header line.
74       * @param line header line to parse
75       * @return trimmed label
76       */
77      public static String getLabel(final String line) {
78          return line.length() < LABEL_INDEX ? "" : line.substring(LABEL_INDEX).trim();
79      }
80  
81      /** Check if a header line matches an expected label.
82       * @param line header line to check
83       * @param label expected label
84       * @return true if line matches expected label
85       */
86      public static boolean matchesLabel(final String line, final String label) {
87          return getLabel(line).equals(label);
88      }
89  
90      /** Parse version, file type and satellite system.
91       * @param line line to parse
92       * @param name file name (for error message generation)
93       * @param header header to fill with parsed data
94       * @param supportedVersions supported versions
95       */
96      public static void parseVersionFileTypeSatelliteSystem(final String line, final String name,
97                                                             final RinexBaseHeader header,
98                                                             final double... supportedVersions) {
99  
100         // Rinex version
101         final double parsedVersion = parseDouble(line, 0, 9);
102 
103         boolean found = false;
104         for (final double supported : supportedVersions) {
105             if (FastMath.abs(parsedVersion - supported) < 1.0e-4) {
106                 found = true;
107                 break;
108             }
109         }
110         if (!found) {
111             final StringBuilder builder = new StringBuilder();
112             for (final double supported : supportedVersions) {
113                 if (builder.length() > 0) {
114                     builder.append(", ");
115                 }
116                 builder.append(supported);
117             }
118             throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT_VERSION,
119                                       parsedVersion, name, builder.toString());
120         }
121         header.setFormatVersion(parsedVersion);
122 
123         // File type
124         if (header.getFileType() != RinexFileType.parseRinexFileType(parseString(line, 20, 1))) {
125             throw new OrekitException(OrekitMessages.WRONG_PARSING_TYPE, name);
126         }
127 
128         // Satellite system
129         switch (header.getFileType()) {
130             case OBSERVATION:
131                 // for observation files, the satellite system is in column 40, and empty defaults to GPS
132                 header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
133                 break;
134             case NAVIGATION: {
135                 if (header.getFormatVersion() < 3.0) {
136                     // the satellite system is hidden within the entry, with GPS as default
137 
138                     // set up default
139                     header.setSatelliteSystem(SatelliteSystem.GPS);
140 
141                     // look if default is overridden somewhere in the entry
142                     final String entry = parseString(line, 0, LABEL_INDEX).toUpperCase();
143                     for (final SatelliteSystem satelliteSystem : SatelliteSystem.values()) {
144                         if (entry.contains(satelliteSystem.name())) {
145                             // we found a satellite system hidden in the middle of the line
146                             header.setSatelliteSystem(satelliteSystem);
147                             break;
148                         }
149                     }
150 
151                 } else {
152                     // the satellite system is in column 40 for 3.X and later
153                     header.setSatelliteSystem(SatelliteSystem.parseSatelliteSystemWithGPSDefault(parseString(line, 40, 1)));
154                 }
155                 break;
156             }
157             default:
158                 //  this should never happen
159                 throw new OrekitInternalError(null);
160         }
161 
162     }
163 
164     /** Parse program, run/by and date.
165      * @param line line to parse
166      * @param lineNumber line number
167      * @param name file name (for error message generation)
168      * @param timeScales the set of time scales used for parsing dates.
169      * @param header header to fill with parsed data
170      */
171     public static void parseProgramRunByDate(final String line, final int lineNumber,
172                                              final String name, final TimeScales timeScales,
173                                              final RinexBaseHeader header) {
174 
175         // Name of the generating program
176         header.setProgramName(parseString(line, 0, 20));
177 
178         // Name of the run/by name
179         header.setRunByName(parseString(line, 20, 20));
180 
181         // there are several variations for date formatting in the PGM / RUN BY / DATE line
182 
183         // in versions 2.x, the pattern is expected to be:
184         // XXRINEXO V9.9       AIUB                24-MAR-01 14:43     PGM / RUN BY / DATE
185         // however, we have also found this:
186         // teqc  2016Nov7      root                20180130 10:38:06UTCPGM / RUN BY / DATE
187         // BJFMTLcsr           UTCSR               2007-09-30 05:30:06 PGM / RUN BY / DATE
188         // NEODIS              TAS                 27/05/22 10:28      PGM / RUN BY / DATE
189 
190         // in versions 3.x, the pattern is expected to be:
191         // sbf2rin-11.3.3                          20180130 002558 LCL PGM / RUN BY / DATE
192         // however, we have also found:
193         // NetR9 5.03          Receiver Operator   11-JAN-16 00:00:00  PGM / RUN BY / DATE
194 
195         // so we cannot rely on the format version, we have to check several variations
196         final Matcher splittingMatcher = SPLITTING_PATTERN.matcher(parseString(line, 40, 20));
197         if (splittingMatcher.matches()) {
198 
199             // date part
200             final DateComponents dc;
201             final Matcher abbrevMatcher = DATE_DD_MMM_YY_PATTERN.matcher(splittingMatcher.group(1));
202             if (abbrevMatcher.matches()) {
203                 // hoping this obsolete format will not be used past year 2079…
204                 dc = new DateComponents(convert2DigitsYear(Integer.parseInt(abbrevMatcher.group(3))),
205                                         Month.parseMonth(abbrevMatcher.group(2)).getNumber(),
206                                         Integer.parseInt(abbrevMatcher.group(1)));
207             } else {
208                 final Matcher isoMatcher = DATE_ISO_8601_PATTERN.matcher(splittingMatcher.group(1));
209                 if (isoMatcher.matches()) {
210                     dc = new DateComponents(Integer.parseInt(isoMatcher.group(1)),
211                                             Integer.parseInt(isoMatcher.group(2)),
212                                             Integer.parseInt(isoMatcher.group(3)));
213                 } else {
214                     final Matcher europeanMatcher = DATE_EUROPEAN_PATTERN.matcher(splittingMatcher.group(1));
215                     if (europeanMatcher.matches()) {
216                         dc = new DateComponents(convert2DigitsYear(Integer.parseInt(europeanMatcher.group(3))),
217                                                 Integer.parseInt(europeanMatcher.group(2)),
218                                                 Integer.parseInt(europeanMatcher.group(1)));
219                     } else {
220                         dc = null;
221                     }
222                 }
223             }
224 
225             // time part
226             final TimeComponents tc;
227             final Matcher timeMatcher = TIME_PATTERN.matcher(splittingMatcher.group(2));
228             if (timeMatcher.matches()) {
229                 tc = new TimeComponents(Integer.parseInt(timeMatcher.group(1)),
230                                         Integer.parseInt(timeMatcher.group(2)),
231                                         timeMatcher.group(3) != null ? Integer.parseInt(timeMatcher.group(3)) : 0);
232             } else {
233                 tc = null;
234             }
235 
236             // zone part
237             final String zone = splittingMatcher.groupCount() > 2 ? splittingMatcher.group(3) : "";
238 
239             if (dc != null && tc != null) {
240                 // we successfully parsed everything
241                 final DateTimeComponents dtc = new DateTimeComponents(dc, tc);
242                 header.setCreationDateComponents(dtc);
243                 final TimeScale timeScale = zone == null ?
244                                             timeScales.getUTC() :
245                                             TimeSystem.parseTimeSystem(zone).getTimeScale(timeScales);
246                 header.setCreationDate(new AbsoluteDate(dtc, timeScale));
247                 header.setCreationTimeZone(zone);
248                 return;
249             }
250 
251         }
252 
253         // we were not able to extract date
254         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
255                                       lineNumber, name, line);
256 
257     }
258 
259     /** Parse a comment.
260      * @param lineNumber line number
261      * @param line line to parse
262      * @param rinexFile rinex file
263      */
264     public static void parseComment(final int lineNumber, final String line, final RinexFile<?> rinexFile) {
265         rinexFile.addComment(new RinexComment(lineNumber, parseString(line, 0, 60)));
266     }
267 
268     /**
269      * Parse a double value.
270      * @param line line to parse
271      * @param startIndex start index
272      * @param size size of the value
273      * @return the parsed value
274      */
275     public static double parseDouble(final String line, final int startIndex, final int size) {
276         final String subString = parseString(line, startIndex, size);
277         if (subString == null || subString.isEmpty()) {
278             return Double.NaN;
279         } else {
280             return Double.parseDouble(subString.replace('D', 'E').trim());
281         }
282     }
283 
284     /**
285      * Parse an integer value.
286      * @param line line to parse
287      * @param startIndex start index
288      * @param size size of the value
289      * @return the parsed value
290      */
291     public static int parseInt(final String line, final int startIndex, final int size) {
292         final String subString = parseString(line, startIndex, size);
293         if (subString == null || subString.isEmpty()) {
294             return 0;
295         } else {
296             return Integer.parseInt(subString.trim());
297         }
298     }
299 
300     /**
301      * Parse a string value.
302      * @param line line to parse
303      * @param startIndex start index
304      * @param size size of the value
305      * @return the parsed value
306      */
307     public static String parseString(final String line, final int startIndex, final int size) {
308         if (line.length() > startIndex) {
309             return line.substring(startIndex, FastMath.min(line.length(), startIndex + size)).trim();
310         } else {
311             return null;
312         }
313     }
314 
315     /** Convert a 2 digits year to a complete year.
316      * @param yy year between 0 and 99
317      * @return complete year
318      * @since 12.0
319      */
320     public static int convert2DigitsYear(final int yy) {
321         return yy >= 80 ? (yy + 1900) : (yy + 2000);
322     }
323 
324 }