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.files.rinex.observation;
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.Reader;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.function.Function;
26  import java.util.function.Predicate;
27  
28  import org.hipparchus.exception.LocalizedCoreFormats;
29  import org.hipparchus.geometry.euclidean.threed.Vector3D;
30  import org.hipparchus.geometry.euclidean.twod.Vector2D;
31  import org.hipparchus.util.FastMath;
32  import org.orekit.annotation.DefaultDataContext;
33  import org.orekit.data.DataContext;
34  import org.orekit.data.DataSource;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitMessages;
37  import org.orekit.files.rinex.AppliedDCBS;
38  import org.orekit.files.rinex.AppliedPCVS;
39  import org.orekit.files.rinex.section.RinexLabels;
40  import org.orekit.files.rinex.utils.parsing.RinexUtils;
41  import org.orekit.gnss.ObservationTimeScale;
42  import org.orekit.gnss.ObservationType;
43  import org.orekit.gnss.SatInSystem;
44  import org.orekit.gnss.SatelliteSystem;
45  import org.orekit.time.AbsoluteDate;
46  import org.orekit.time.TimeScale;
47  import org.orekit.time.TimeScales;
48  
49  /** Parser for Rinex measurements files.
50   * <p>
51   * Supported versions are: 2.00, 2.10, 2.11, 2.12 (unofficial), 2.20 (unofficial),
52   * 3.00, 3.01, 3.02, 3.03, 3.04, 3.05, and 4.00.
53   * </p>
54   * @see <a href="https://files.igs.org/pub/data/format/rinex2.txt">rinex 2.0</a>
55   * @see <a href="https://files.igs.org/pub/data/format/rinex210.txt">rinex 2.10</a>
56   * @see <a href="https://files.igs.org/pub/data/format/rinex211.pdf">rinex 2.11</a>
57   * @see <a href="http://www.aiub.unibe.ch/download/rinex/rinex212.txt">unofficial rinex 2.12</a>
58   * @see <a href="http://www.aiub.unibe.ch/download/rinex/rnx_leo.txt">unofficial rinex 2.20</a>
59   * @see <a href="https://files.igs.org/pub/data/format/rinex300.pdf">rinex 3.00</a>
60   * @see <a href="https://files.igs.org/pub/data/format/rinex301.pdf">rinex 3.01</a>
61   * @see <a href="https://files.igs.org/pub/data/format/rinex302.pdf">rinex 3.02</a>
62   * @see <a href="https://files.igs.org/pub/data/format/rinex303.pdf">rinex 3.03</a>
63   * @see <a href="https://files.igs.org/pub/data/format/rinex304.pdf">rinex 3.04</a>
64   * @see <a href="https://files.igs.org/pub/data/format/rinex305.pdf">rinex 3.05</a>
65   * @see <a href="https://files.igs.org/pub/data/format/rinex_4.00.pdf">rinex 4.00</a>
66   * @since 12.0
67   */
68  public class RinexObservationParser {
69  
70      /** Default name pattern for rinex 2 observation files. */
71      public static final String DEFAULT_RINEX_2_NAMES = "^\\w{4}\\d{3}[0a-x](?:\\d{2})?\\.\\d{2}[oO]$";
72  
73      /** Default name pattern for rinex 3 observation files. */
74      public static final String DEFAULT_RINEX_3_NAMES = "^\\w{9}_\\w{1}_\\d{11}_\\d{2}\\w_\\d{2}\\w{1}_\\w{2}\\.rnx$";
75  
76      /** Maximum number of satellites per line in Rinex 2 format . */
77      private static final int MAX_SAT_PER_RINEX_2_LINE = 12;
78  
79      /** Maximum number of observations per line in Rinex 2 format. */
80      private static final int MAX_OBS_PER_RINEX_2_LINE = 5;
81  
82      /** Set of time scales. */
83      private final TimeScales timeScales;
84  
85      /** Simple constructor.
86       * <p>
87       * This constructor uses the {@link DataContext#getDefault() default data context}.
88       * </p>
89       */
90      @DefaultDataContext
91      public RinexObservationParser() {
92          this(DataContext.getDefault().getTimeScales());
93      }
94  
95      /**
96       * Create a RINEX loader/parser with the given source of RINEX auxiliary data files.
97       * @param timeScales the set of time scales to use when parsing dates.
98       * @since 12.0
99       */
100     public RinexObservationParser(final TimeScales timeScales) {
101         this.timeScales = timeScales;
102     }
103 
104     /**
105      * Parse RINEX observations messages.
106      * @param source source providing the data to parse
107      * @return parsed observations file
108      */
109     public RinexObservation parse(final DataSource source) {
110 
111         Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.VERSION);
112 
113         // placeholders for parsed data
114         final ParseInfo parseInfo = new ParseInfo(source.getName());
115 
116         try (Reader reader = source.getOpener().openReaderOnce();
117              BufferedReader br = new BufferedReader(reader)) {
118             ++parseInfo.lineNumber;
119             nextLine:
120                 for (String line = br.readLine(); line != null; line = br.readLine()) {
121                     for (final LineParser candidate : candidateParsers) {
122                         if (candidate.canHandle.test(line)) {
123                             try {
124                                 candidate.parsingMethod.parse(line, parseInfo);
125                                 ++parseInfo.lineNumber;
126                                 candidateParsers = candidate.allowedNextProvider.apply(parseInfo);
127                                 continue nextLine;
128                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
129                                 throw new OrekitException(e,
130                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
131                                                           parseInfo.lineNumber, source.getName(), line);
132                             }
133                         }
134                     }
135 
136                     // no parsers found for this line
137                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
138                                               parseInfo.lineNumber, source.getName(), line);
139 
140                 }
141 
142         } catch (IOException ioe) {
143             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
144         }
145 
146         return parseInfo.file;
147 
148     }
149 
150     /** Transient data used for parsing a RINEX observation messages file.
151      * @since 12.0
152      */
153     private class ParseInfo {
154 
155         /** Name of the data source. */
156         private final String name;
157 
158         /** Set of time scales for parsing dates. */
159         private final TimeScales timeScales;
160 
161         /** Current line number of the navigation message. */
162         private int lineNumber;
163 
164         /** Rinex file. */
165         private final RinexObservation file;
166 
167         /** Date of the observation. */
168         private AbsoluteDate tObs;
169 
170         /** Indicator that time of first observation was already fixed. */
171         private boolean tFirstFixed;
172 
173         /** Indicator that time of last observation was already fixed. */
174         private boolean tLastFixed;
175 
176         /** Receiver clock offset (seconds). */
177         private double rcvrClkOffset;
178 
179         /** time scale for parsing dates. */
180         private TimeScale timeScale;
181 
182         /** Number of observation types. */
183         private int nbTypes;
184 
185         /** Number of satellites in the current observations block. */
186         private int nbSatObs;
187 
188         /** Number of scaling factors. */
189         private int nbObsScaleFactor;
190 
191         /** Index of satellite in current observation. */
192         private int indexObsSat;
193 
194         /** Line number of start of next observation. */
195         private int nextObsStartLineNumber;
196 
197         /** Current satellite system. */
198         private SatelliteSystem currentSystem;
199 
200         /** Number of satellites affected by phase shifts. */
201         private int phaseShiftNbSat;
202 
203         /** Number of GLONASS satellites. */
204         private int nbGlonass;
205 
206         /** Satellites affected by phase shift. */
207         private final List<SatInSystem> satPhaseShift;
208 
209         /** Type of observation affected by phase shift. */
210         private ObservationType phaseShiftTypeObs;
211 
212         /** Phase shift correction. */
213         private double corrPhaseShift;
214 
215         /** Indicator for completed header. */
216         private boolean headerCompleted;
217 
218         /** Indicator for skipping special records (eventFlag from 2 to 5). */
219         private boolean specialRecord;
220 
221         /** Indicator for skipping cyckle slip records (enventFlag == 6). */
222         private boolean cycleSlip;
223 
224         /** Event flag. */
225         private int eventFlag;
226 
227         /** Scaling factors. */
228         private final List<ObservationType> typesObsScaleFactor;
229 
230         /** Types of observations. */
231         private final List<ObservationType> typesObs;
232 
233         /** Observations. */
234         private final List<ObservationData> observations;
235 
236         /** Satellites in current observation. */
237         private final List<SatInSystem> satObs;
238 
239         /** Current satellite. */
240         private SatInSystem currentSat;
241 
242         /** Constructor, build the ParseInfo object.
243          * @param name name of the data source
244          */
245         ParseInfo(final String name) {
246             // Initialize default values for fields
247             this.name                   = name;
248             this.timeScales             = RinexObservationParser.this.timeScales;
249             this.file                   = new RinexObservation();
250             this.lineNumber             = 0;
251             this.tObs                   = AbsoluteDate.PAST_INFINITY;
252             this.tFirstFixed            = false;
253             this.tLastFixed             = false;
254             this.timeScale              = null;
255             this.nbTypes                = -1;
256             this.nbSatObs               = -1;
257             this.nbGlonass              = -1;
258             this.phaseShiftNbSat        = -1;
259             this.nbObsScaleFactor       = -1;
260             this.nextObsStartLineNumber = -1;
261             this.typesObs               = new ArrayList<>();
262             this.observations           = new ArrayList<>();
263             this.satPhaseShift          = new ArrayList<>();
264             this.typesObsScaleFactor    = new ArrayList<>();
265             this.satObs                 = new ArrayList<>();
266         }
267 
268         /** Set observation date, taking care of receiver/absolute time scales.
269          * @param rawDate date as parsed, prior to any time scale modification
270          */
271         private void setTObs(final AbsoluteDate rawDate) {
272             final RinexObservationHeader header = file.getHeader();
273             if (header.getClockOffsetApplied()) {
274                 // date was already in an absolute time scale
275                 tObs = rawDate;
276             } else {
277                 // the epoch was expressed in receiver clock
278                 // we need to convert it to absolute date
279                 if (FastMath.abs(rawDate.durationFrom(header.getTFirstObs())) < 1.0e-6 &&
280                     !tFirstFixed) {
281                     // we need to fix the first date in the header too
282                     header.setTFirstObs(header.getTFirstObs().shiftedBy(-rcvrClkOffset));
283                     tFirstFixed = true;
284                 }
285                 if (FastMath.abs(rawDate.durationFrom(header.getTLastObs())) < 1.0e-6 &&
286                     !tLastFixed) {
287                     // we need to fix the last date in the header too
288                     header.setTLastObs(header.getTLastObs().shiftedBy(-rcvrClkOffset));
289                     tLastFixed = true;
290                 }
291                 tObs = rawDate.shiftedBy(-rcvrClkOffset);
292             }
293         }
294 
295     }
296 
297     /** Parsers for specific lines. */
298     private enum LineParser {
299 
300         /** Parser for version, file type and satellite system. */
301         VERSION(line -> RinexLabels.VERSION.matches(RinexUtils.getLabel(line)),
302                 (line, parseInfo) ->  RinexUtils.parseVersionFileTypeSatelliteSystem(line, parseInfo.name, parseInfo.file.getHeader(),
303                                                                                      2.00, 2.10, 2.11, 2.12, 2.20,
304                                                                                      3.00, 3.01, 3.02, 3.03, 3.04, 3.05, 4.00),
305                 LineParser::headerNext),
306 
307         /** Parser for generating program and emiting agency. */
308         PROGRAM(line -> RinexLabels.PROGRAM.matches(RinexUtils.getLabel(line)),
309                 (line, parseInfo) -> RinexUtils.parseProgramRunByDate(line, parseInfo.lineNumber, parseInfo.name,
310                                                                       parseInfo.timeScales, parseInfo.file.getHeader()),
311                 LineParser::headerNext),
312 
313         /** Parser for comments. */
314         COMMENT(line -> RinexLabels.COMMENT.matches(RinexUtils.getLabel(line)),
315                        (line, parseInfo) -> RinexUtils.parseComment(parseInfo.lineNumber, line, parseInfo.file),
316                        LineParser::commentNext),
317 
318         /** Parser for marker name. */
319         MARKER_NAME(line -> RinexLabels.MARKER_NAME.matches(RinexUtils.getLabel(line)),
320                     (line, parseInfo) ->  parseInfo.file.getHeader().setMarkerName(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
321                     LineParser::headerNext),
322 
323         /** Parser for marker number. */
324         MARKER_NUMBER(line -> RinexLabels.MARKER_NUMBER.matches(RinexUtils.getLabel(line)),
325                       (line, parseInfo) -> parseInfo.file.getHeader().setMarkerNumber(RinexUtils.parseString(line, 0, 20)),
326                       LineParser::headerNext),
327 
328         /** Parser for marker type. */
329         MARKER_TYPE(line -> RinexLabels.MARKER_TYPE.matches(RinexUtils.getLabel(line)),
330                     (line, parseInfo) -> parseInfo.file.getHeader().setMarkerType(RinexUtils.parseString(line, 0, 20)),
331                     LineParser::headerNext),
332 
333         /** Parser for observer agency. */
334         OBSERVER_AGENCY(line -> RinexLabels.OBSERVER_AGENCY.matches(RinexUtils.getLabel(line)),
335                         (line, parseInfo) -> {
336                             parseInfo.file.getHeader().setObserverName(RinexUtils.parseString(line, 0, 20));
337                             parseInfo.file.getHeader().setAgencyName(RinexUtils.parseString(line, 20, 40));
338                         },
339                         LineParser::headerNext),
340 
341         /** Parser for receiver number, type and version. */
342         REC_NB_TYPE_VERS(line -> RinexLabels.REC_NB_TYPE_VERS.matches(RinexUtils.getLabel(line)),
343                          (line, parseInfo) -> {
344                              parseInfo.file.getHeader().setReceiverNumber(RinexUtils.parseString(line, 0, 20));
345                              parseInfo.file.getHeader().setReceiverType(RinexUtils.parseString(line, 20, 20));
346                              parseInfo.file.getHeader().setReceiverVersion(RinexUtils.parseString(line, 40, 20));
347                          },
348                          LineParser::headerNext),
349 
350         /** Parser for antenna number and type. */
351         ANT_NB_TYPE(line -> RinexLabels.ANT_NB_TYPE.matches(RinexUtils.getLabel(line)),
352                     (line, parseInfo) -> {
353                         parseInfo.file.getHeader().setAntennaNumber(RinexUtils.parseString(line, 0, 20));
354                         parseInfo.file.getHeader().setAntennaType(RinexUtils.parseString(line, 20, 20));
355                     },
356                     LineParser::headerNext),
357 
358         /** Parser for approximative position. */
359         APPROX_POSITION_XYZ(line -> RinexLabels.APPROX_POSITION_XYZ.matches(RinexUtils.getLabel(line)),
360                             (line, parseInfo) -> parseInfo.file.getHeader().setApproxPos(new Vector3D(RinexUtils.parseDouble(line, 0, 14),
361                                                                                                   RinexUtils.parseDouble(line, 14, 14),
362                                                                                                   RinexUtils.parseDouble(line, 28, 14))),
363                             LineParser::headerNext),
364 
365         /** Parser for antenna reference point. */
366         ANTENNA_DELTA_H_E_N(line -> RinexLabels.ANTENNA_DELTA_H_E_N.matches(RinexUtils.getLabel(line)),
367                             (line, parseInfo) -> {
368                                 parseInfo.file.getHeader().setAntennaHeight(RinexUtils.parseDouble(line, 0, 14));
369                                 parseInfo.file.getHeader().setEccentricities(new Vector2D(RinexUtils.parseDouble(line, 14, 14),
370                                                                                           RinexUtils.parseDouble(line, 28, 14)));
371                             },
372                             LineParser::headerNext),
373 
374         /** Parser for antenna reference point. */
375         ANTENNA_DELTA_X_Y_Z(line -> RinexLabels.ANTENNA_DELTA_X_Y_Z.matches(RinexUtils.getLabel(line)),
376                             (line, parseInfo) -> parseInfo.file.getHeader().setAntennaReferencePoint(new Vector3D(RinexUtils.parseDouble(line, 0, 14),
377                                                                                                                   RinexUtils.parseDouble(line, 14, 14),
378                                                                                                                   RinexUtils.parseDouble(line, 28, 14))),
379                             LineParser::headerNext),
380 
381         /** Parser for antenna phase center. */
382         ANTENNA_PHASE_CENTER(line -> RinexLabels.ANTENNA_PHASE_CENTER.matches(RinexUtils.getLabel(line)),
383                              (line, parseInfo) -> {
384                                  parseInfo.file.getHeader().setPhaseCenterSystem(SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1)));
385                                  parseInfo.file.getHeader().setObservationCode(RinexUtils.parseString(line, 2, 3));
386                                  parseInfo.file.getHeader().setAntennaPhaseCenter(new Vector3D(RinexUtils.parseDouble(line, 5, 9),
387                                                                                                RinexUtils.parseDouble(line, 14, 14),
388                                                                                                RinexUtils.parseDouble(line, 28, 14)));
389                              },
390                              LineParser::headerNext),
391 
392         /** Parser for antenna bore sight. */
393         ANTENNA_B_SIGHT_XYZ(line -> RinexLabels.ANTENNA_B_SIGHT_XYZ.matches(RinexUtils.getLabel(line)),
394                             (line, parseInfo) -> parseInfo.file.getHeader().setAntennaBSight(new Vector3D(RinexUtils.parseDouble(line, 0, 14),
395                                                                                                           RinexUtils.parseDouble(line, 14, 14),
396                                                                                                           RinexUtils.parseDouble(line, 28, 14))),
397                             LineParser::headerNext),
398 
399         /** Parser for antenna zero direction. */
400         ANTENNA_ZERODIR_AZI(line -> RinexLabels.ANTENNA_ZERODIR_AZI.matches(RinexUtils.getLabel(line)),
401                             (line, parseInfo) -> parseInfo.file.getHeader().setAntennaAzimuth(FastMath.toRadians(RinexUtils.parseDouble(line, 0, 14))),
402                             LineParser::headerNext),
403 
404         /** Parser for antenna zero direction. */
405         ANTENNA_ZERODIR_XYZ(line -> RinexLabels.ANTENNA_ZERODIR_XYZ.matches(RinexUtils.getLabel(line)),
406                             (line, parseInfo) -> parseInfo.file.getHeader().setAntennaZeroDirection(new Vector3D(RinexUtils.parseDouble(line, 0, 14),
407                                                                                                                  RinexUtils.parseDouble(line, 14, 14),
408                                                                                                                  RinexUtils.parseDouble(line, 28, 14))),
409                             LineParser::headerNext),
410 
411         /** Parser for wavelength factors. */
412         WAVELENGTH_FACT_L1_2(line -> RinexLabels.WAVELENGTH_FACT_L1_2.matches(RinexUtils.getLabel(line)),
413                              (line, parseInfo) -> {
414                                  // optional line in Rinex 2 header, not stored for now
415                              },
416                              LineParser::headerNext),
417 
418         /** Parser for observations scale factor. */
419         OBS_SCALE_FACTOR(line -> RinexLabels.OBS_SCALE_FACTOR.matches(RinexUtils.getLabel(line)),
420                          (line, parseInfo) -> {
421                              final int scaleFactor      = FastMath.max(1, RinexUtils.parseInt(line, 0,  6));
422                              final int nbObsScaleFactor = RinexUtils.parseInt(line, 6, 6);
423                              final List<ObservationType> types = new ArrayList<>(nbObsScaleFactor);
424                              for (int i = 0; i < nbObsScaleFactor; i++) {
425                                  types.add(ObservationType.valueOf(RinexUtils.parseString(line, 16 + (6 * i), 2)));
426                              }
427                              parseInfo.file.getHeader().addScaleFactorCorrection(parseInfo.file.getHeader().getSatelliteSystem(),
428                                                                                  new ScaleFactorCorrection(scaleFactor, types));
429                          },
430                          LineParser::headerNext),
431 
432         /** Parser for center of mass. */
433         CENTER_OF_MASS_XYZ(line -> RinexLabels.CENTER_OF_MASS_XYZ.matches(RinexUtils.getLabel(line)),
434                            (line, parseInfo) -> parseInfo.file.getHeader().setCenterMass(new Vector3D(RinexUtils.parseDouble(line, 0, 14),
435                                                                                                       RinexUtils.parseDouble(line, 14, 14),
436                                                                                                       RinexUtils.parseDouble(line, 28, 14))),
437                            LineParser::headerNext),
438 
439         /** Parser for DOI.
440          * @since 12.0
441          */
442         DOI(line -> RinexLabels.DOI.matches(RinexUtils.getLabel(line)),
443             (line, parseInfo) -> parseInfo.file.getHeader().setDoi(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
444             LineParser::headerNext),
445 
446         /** Parser for license.
447          * @since 12.0
448          */
449         LICENSE(line -> RinexLabels.LICENSE.matches(RinexUtils.getLabel(line)),
450                 (line, parseInfo) -> parseInfo.file.getHeader().setLicense(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
451                 LineParser::headerNext),
452 
453         /** Parser for station information.
454          * @since 12.0
455          */
456         STATION_INFORMATION(line -> RinexLabels.STATION_INFORMATION.matches(RinexUtils.getLabel(line)),
457                             (line, parseInfo) -> parseInfo.file.getHeader().setStationInformation(RinexUtils.parseString(line, 0, RinexUtils.LABEL_INDEX)),
458                             LineParser::headerNext),
459 
460         /** Parser for number and types of observations. */
461         SYS_NB_TYPES_OF_OBSERV(line -> RinexLabels.SYS_NB_TYPES_OF_OBSERV.matches(RinexUtils.getLabel(line)) ||
462                                        RinexLabels.NB_TYPES_OF_OBSERV.matches(RinexUtils.getLabel(line)),
463                                (line, parseInfo) -> {
464                                    final double version = parseInfo.file.getHeader().getFormatVersion();
465                                    if (parseInfo.nbTypes < 0) {
466                                        // first line of types of observations
467                                        if (version < 3) {
468                                            // Rinex 2 has only one system
469                                            parseInfo.currentSystem = parseInfo.file.getHeader().getSatelliteSystem();
470                                            parseInfo.nbTypes       = RinexUtils.parseInt(line, 0, 6);
471                                        } else {
472                                            // Rinex 3 and above allow mixed systems
473                                            parseInfo.currentSystem = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1));
474                                            parseInfo.nbTypes       = RinexUtils.parseInt(line, 3, 3);
475                                            if (parseInfo.currentSystem != parseInfo.file.getHeader().getSatelliteSystem() &&
476                                                parseInfo.file.getHeader().getSatelliteSystem() != SatelliteSystem.MIXED) {
477                                                throw new OrekitException(OrekitMessages.INCONSISTENT_SATELLITE_SYSTEM,
478                                                                          parseInfo.lineNumber, parseInfo.name,
479                                                                          parseInfo.file.getHeader().getSatelliteSystem(),
480                                                                          parseInfo.currentSystem);
481                                            }
482                                        }
483                                    }
484 
485                                    final int firstIndex = version < 3 ? 10 : 7;
486                                    final int increment  = version < 3 ?  6 : 4;
487                                    final int size       = version < 3 ?  2 : 3;
488                                    for (int i = firstIndex;
489                                                    (i + size) <= RinexUtils.LABEL_INDEX && parseInfo.typesObs.size() < parseInfo.nbTypes;
490                                                    i += increment) {
491                                        final String type = RinexUtils.parseString(line, i, size);
492                                        try {
493                                            parseInfo.typesObs.add(ObservationType.valueOf(type));
494                                        } catch (IllegalArgumentException iae) {
495                                            throw new OrekitException(iae, OrekitMessages.UNKNOWN_RINEX_FREQUENCY,
496                                                                      type, parseInfo.name, parseInfo.lineNumber);
497                                        }
498                                    }
499 
500                                    if (parseInfo.typesObs.size() == parseInfo.nbTypes) {
501                                        // we have completed the list
502                                        parseInfo.file.getHeader().setTypeObs(parseInfo.currentSystem, parseInfo.typesObs);
503                                        parseInfo.typesObs.clear();
504                                        parseInfo.nbTypes = -1;
505                                    }
506 
507                                },
508                                LineParser::headerNbTypesObs),
509 
510         /** Parser for unit of signal strength. */
511         SIGNAL_STRENGTH_UNIT(line -> RinexLabels.SIGNAL_STRENGTH_UNIT.matches(RinexUtils.getLabel(line)),
512                              (line, parseInfo) -> parseInfo.file.getHeader().setSignalStrengthUnit(RinexUtils.parseString(line, 0, 20)),
513                              LineParser::headerNext),
514 
515         /** Parser for observation interval. */
516         INTERVAL(line -> RinexLabels.INTERVAL.matches(RinexUtils.getLabel(line)),
517                  (line, parseInfo) -> parseInfo.file.getHeader().setInterval(RinexUtils.parseDouble(line, 0, 10)),
518                  LineParser::headerNext),
519 
520         /** Parser for time of first observation. */
521         TIME_OF_FIRST_OBS(line -> RinexLabels.TIME_OF_FIRST_OBS.matches(RinexUtils.getLabel(line)),
522                           (line, parseInfo) -> {
523                               if (parseInfo.file.getHeader().getSatelliteSystem() == SatelliteSystem.MIXED) {
524                                   // in case of mixed data, time scale must be specified in the Time of First Observation line
525                                   try {
526                                       parseInfo.timeScale = ObservationTimeScale.
527                                                             valueOf(RinexUtils.parseString(line, 48, 3)).
528                                                             getTimeScale(parseInfo.timeScales);
529                                   } catch (IllegalArgumentException iae) {
530                                       throw new OrekitException(iae,
531                                                                 OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
532                                                                 parseInfo.lineNumber, parseInfo.name, line);
533                                   }
534                               } else {
535                                   final ObservationTimeScale observationTimeScale = parseInfo.file.getHeader().getSatelliteSystem().getObservationTimeScale();
536                                   if (observationTimeScale == null) {
537                                       throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
538                                                                 parseInfo.lineNumber, parseInfo.name, line);
539                                   }
540                                   parseInfo.timeScale = observationTimeScale.getTimeScale(parseInfo.timeScales);
541                               }
542                               parseInfo.file.getHeader().setTFirstObs(new AbsoluteDate(RinexUtils.parseInt(line, 0, 6),
543                                                                                        RinexUtils.parseInt(line, 6, 6),
544                                                                                        RinexUtils.parseInt(line, 12, 6),
545                                                                                        RinexUtils.parseInt(line, 18, 6),
546                                                                                        RinexUtils.parseInt(line, 24, 6),
547                                                                                        RinexUtils.parseDouble(line, 30, 13),
548                                                                                        parseInfo.timeScale));
549                           },
550                           LineParser::headerNext),
551 
552         /** Parser for time of last observation. */
553         TIME_OF_LAST_OBS(line -> RinexLabels.TIME_OF_LAST_OBS.matches(RinexUtils.getLabel(line)),
554                          (line, parseInfo) -> parseInfo.file.getHeader().setTLastObs(new AbsoluteDate(RinexUtils.parseInt(line, 0, 6),
555                                                                                                       RinexUtils.parseInt(line, 6, 6),
556                                                                                                       RinexUtils.parseInt(line, 12, 6),
557                                                                                                       RinexUtils.parseInt(line, 18, 6),
558                                                                                                       RinexUtils.parseInt(line, 24, 6),
559                                                                                                       RinexUtils.parseDouble(line, 30, 13),
560                                                                                                       parseInfo.timeScale)),
561                          LineParser::headerNext),
562 
563         /** Parser for indicator of receiver clock offset application. */
564         RCV_CLOCK_OFFS_APPL(line -> RinexLabels.RCV_CLOCK_OFFS_APPL.matches(RinexUtils.getLabel(line)),
565                             (line, parseInfo) -> parseInfo.file.getHeader().setClockOffsetApplied(RinexUtils.parseInt(line, 0, 6) > 0),
566                             LineParser::headerNext),
567 
568         /** Parser for differential code bias corrections. */
569         SYS_DCBS_APPLIED(line -> RinexLabels.SYS_DCBS_APPLIED.matches(RinexUtils.getLabel(line)),
570                          (line, parseInfo) -> parseInfo.file.getHeader().addAppliedDCBS(new AppliedDCBS(SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1)),
571                                                                                                         RinexUtils.parseString(line, 2, 17),
572                                                                                                         RinexUtils.parseString(line, 20, 40))),
573                          LineParser::headerNext),
574 
575         /** Parser for phase center variations corrections. */
576         SYS_PCVS_APPLIED(line -> RinexLabels.SYS_PCVS_APPLIED.matches(RinexUtils.getLabel(line)),
577                          (line, parseInfo) -> parseInfo.file.getHeader().addAppliedPCVS(new AppliedPCVS(SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1)),
578                                                                                                         RinexUtils.parseString(line, 2, 17),
579                                                                                                         RinexUtils.parseString(line, 20, 40))),
580                          LineParser::headerNext),
581 
582         /** Parser for scale factor. */
583         SYS_SCALE_FACTOR(line -> RinexLabels.SYS_SCALE_FACTOR.matches(RinexUtils.getLabel(line)),
584                          (line, parseInfo) -> {
585 
586                              int scaleFactor = 1;
587                              if (parseInfo.nbObsScaleFactor < 0) {
588                                  // first line of scale factor
589                                  parseInfo.currentSystem    = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1));
590                                  scaleFactor                = RinexUtils.parseInt(line, 2, 4);
591                                  parseInfo.nbObsScaleFactor = RinexUtils.parseInt(line, 8, 2);
592                              }
593 
594                              if (parseInfo.nbObsScaleFactor == 0) {
595                                  parseInfo.typesObsScaleFactor.addAll(parseInfo.file.getHeader().getTypeObs().get(parseInfo.currentSystem));
596                              } else {
597                                  for (int i = 11; i < RinexUtils.LABEL_INDEX && parseInfo.typesObsScaleFactor.size() < parseInfo.nbObsScaleFactor; i += 4) {
598                                      parseInfo.typesObsScaleFactor.add(ObservationType.valueOf(RinexUtils.parseString(line, i, 3)));
599                                  }
600                              }
601 
602                              if (parseInfo.typesObsScaleFactor.size() >= parseInfo.nbObsScaleFactor) {
603                                  // we have completed the list
604                                  parseInfo.file.getHeader().addScaleFactorCorrection(parseInfo.currentSystem,
605                                                                            new ScaleFactorCorrection(scaleFactor,
606                                                                                                      new ArrayList<>(parseInfo.typesObsScaleFactor)));
607                                  parseInfo.nbObsScaleFactor = -1;
608                                  parseInfo.typesObsScaleFactor.clear();
609                              }
610 
611                          },
612                          LineParser::headerNext),
613 
614         /** Parser for phase shift. */
615         SYS_PHASE_SHIFT(line -> RinexLabels.SYS_PHASE_SHIFT.matches(RinexUtils.getLabel(line)),
616                         (line, parseInfo) -> {
617 
618                             if (parseInfo.phaseShiftNbSat < 0) {
619                                 // first line of phase shift
620                                 parseInfo.currentSystem     = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1));
621                                 final String to             = RinexUtils.parseString(line, 2, 3);
622                                 parseInfo.phaseShiftTypeObs = to.isEmpty() ? null : ObservationType.valueOf(to.length() < 3 ? "L" + to : to);
623                                 parseInfo.corrPhaseShift    = RinexUtils.parseDouble(line, 6, 8);
624                                 parseInfo.phaseShiftNbSat   = RinexUtils.parseInt(line, 16, 2);
625                             }
626 
627                             for (int i = 19; i + 3 < RinexUtils.LABEL_INDEX && parseInfo.satPhaseShift.size() < parseInfo.phaseShiftNbSat; i += 4) {
628                                 final SatelliteSystem system = line.charAt(i) == ' ' ?
629                                                                parseInfo.currentSystem :
630                                                                SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, i, 1));
631                                 final int             prn    = RinexUtils.parseInt(line, i + 1, 2);
632                                 parseInfo.satPhaseShift.add(new SatInSystem(system,
633                                                                             system == SatelliteSystem.SBAS ?
634                                                                             prn + 100 :
635                                                                             (system == SatelliteSystem.QZSS ? prn + 192 : prn)));
636                             }
637 
638                             if (parseInfo.satPhaseShift.size() == parseInfo.phaseShiftNbSat) {
639                                 // we have completed the list
640                                 parseInfo.file.getHeader().addPhaseShiftCorrection(new PhaseShiftCorrection(parseInfo.currentSystem,
641                                                                                                             parseInfo.phaseShiftTypeObs,
642                                                                                                             parseInfo.corrPhaseShift,
643                                                                                                             new ArrayList<>(parseInfo.satPhaseShift)));
644                                 parseInfo.phaseShiftNbSat = -1;
645                                 parseInfo.satPhaseShift.clear();
646                             }
647 
648                         },
649                         LineParser::headerPhaseShift),
650 
651         /** Parser for GLONASS slot and frequency number. */
652         GLONASS_SLOT_FRQ_NB(line -> RinexLabels.GLONASS_SLOT_FRQ_NB.matches(RinexUtils.getLabel(line)),
653                             (line, parseInfo) -> {
654 
655                                 if (parseInfo.nbGlonass < 0) {
656                                     // first line of GLONASS satellite/frequency association
657                                     parseInfo.nbGlonass = RinexUtils.parseInt(line, 0, 3);
658                                 }
659 
660                                 for (int i = 4;
661                                      i < RinexUtils.LABEL_INDEX && parseInfo.file.getHeader().getGlonassChannels().size() < parseInfo.nbGlonass;
662                                      i += 7) {
663                                     final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, i, 1));
664                                     final int             prn    = RinexUtils.parseInt(line, i + 1, 2);
665                                     final int             k      = RinexUtils.parseInt(line, i + 4, 2);
666                                     parseInfo.file.getHeader().addGlonassChannel(new GlonassSatelliteChannel(new SatInSystem(system, prn), k));
667                                 }
668 
669                             },
670                             LineParser::headerNext),
671 
672         /** Parser for GLONASS phase bias corrections. */
673         GLONASS_COD_PHS_BIS(line -> RinexLabels.GLONASS_COD_PHS_BIS.matches(RinexUtils.getLabel(line)),
674                             (line, parseInfo) -> {
675 
676                                 // C1C signal
677                                 final String c1c = RinexUtils.parseString(line, 1, 3);
678                                 if (!c1c.isEmpty()) {
679                                     parseInfo.file.getHeader().setC1cCodePhaseBias(RinexUtils.parseDouble(line, 5, 8));
680                                 }
681 
682                                 // C1P signal
683                                 final String c1p = RinexUtils.parseString(line, 14, 3);
684                                 if (!c1p.isEmpty()) {
685                                     parseInfo.file.getHeader().setC1pCodePhaseBias(RinexUtils.parseDouble(line, 18, 8));
686                                 }
687 
688                                 // C2C signal
689                                 final String c2c = RinexUtils.parseString(line, 27, 3);
690                                 if (!c2c.isEmpty()) {
691                                     parseInfo.file.getHeader().setC2cCodePhaseBias(RinexUtils.parseDouble(line, 31, 8));
692                                 }
693 
694                                 // C2P signal
695                                 final String c2p = RinexUtils.parseString(line, 40, 3);
696                                 if (!c2p.isEmpty()) {
697                                     parseInfo.file.getHeader().setC2pCodePhaseBias(RinexUtils.parseDouble(line, 44, 8));
698                                 }
699 
700                             },
701                             LineParser::headerNext),
702 
703         /** Parser for leap seconds. */
704         LEAP_SECONDS(line -> RinexLabels.LEAP_SECONDS.matches(RinexUtils.getLabel(line)),
705                      (line, parseInfo) -> {
706                          parseInfo.file.getHeader().setLeapSeconds(RinexUtils.parseInt(line, 0, 6));
707                          if (parseInfo.file.getHeader().getFormatVersion() >= 3.0) {
708                              parseInfo.file.getHeader().setLeapSecondsFuture(RinexUtils.parseInt(line, 6, 6));
709                              parseInfo.file.getHeader().setLeapSecondsWeekNum(RinexUtils.parseInt(line, 12, 6));
710                              parseInfo.file.getHeader().setLeapSecondsDayNum(RinexUtils.parseInt(line, 18, 6));
711                          }
712                      },
713                      LineParser::headerNext),
714 
715         /** Parser for number of satellites. */
716         NB_OF_SATELLITES(line -> RinexLabels.NB_OF_SATELLITES.matches(RinexUtils.getLabel(line)),
717                          (line, parseInfo) -> parseInfo.file.getHeader().setNbSat(RinexUtils.parseInt(line, 0, 6)),
718                          LineParser::headerNext),
719 
720         /** Parser for PRN and number of observations . */
721         PRN_NB_OF_OBS(line -> RinexLabels.PRN_NB_OF_OBS.matches(RinexUtils.getLabel(line)),
722                       (line, parseInfo) ->  {
723                           final String systemName = RinexUtils.parseString(line, 3, 1);
724                           if (!systemName.isEmpty()) {
725                               final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(systemName);
726                               final int             prn    = RinexUtils.parseInt(line, 4, 2);
727                               parseInfo.currentSat         = new SatInSystem(system,
728                                                                              system == SatelliteSystem.SBAS ?
729                                                                              prn + 100 :
730                                                                              (system == SatelliteSystem.QZSS ? prn + 192 : prn));
731                               parseInfo.nbTypes            = 0;
732                           }
733                           final List<ObservationType> types = parseInfo.file.getHeader().getTypeObs().get(parseInfo.currentSat.getSystem());
734 
735                           final int firstIndex = 6;
736                           final int increment  = 6;
737                           final int size       = 6;
738                           for (int i = firstIndex;
739                                (i + size) <= RinexUtils.LABEL_INDEX && parseInfo.nbTypes < types.size();
740                                i += increment) {
741                               final String nb = RinexUtils.parseString(line, i, size);
742                               if (!nb.isEmpty()) {
743                                   parseInfo.file.getHeader().setNbObsPerSatellite(parseInfo.currentSat, types.get(parseInfo.nbTypes),
744                                                                         RinexUtils.parseInt(line, i, size));
745                               }
746                               ++parseInfo.nbTypes;
747                           }
748 
749                       },
750                       LineParser::headerNext),
751 
752         /** Parser for the end of header. */
753         END(line -> RinexLabels.END.matches(RinexUtils.getLabel(line)),
754             (line, parseInfo) -> {
755 
756                 parseInfo.headerCompleted = true;
757 
758                 // get rinex format version
759                 final double version = parseInfo.file.getHeader().getFormatVersion();
760 
761                 // check mandatory header fields
762                 if (version < 3) {
763                     if (parseInfo.file.getHeader().getMarkerName()                  == null ||
764                         parseInfo.file.getHeader().getObserverName()                == null ||
765                         parseInfo.file.getHeader().getReceiverNumber()              == null ||
766                         parseInfo.file.getHeader().getAntennaNumber()               == null ||
767                         parseInfo.file.getHeader().getTFirstObs()                   == null ||
768                         version < 2.20 && parseInfo.file.getHeader().getApproxPos() == null ||
769                         version < 2.20 && Double.isNaN(parseInfo.file.getHeader().getAntennaHeight()) ||
770                         parseInfo.file.getHeader().getTypeObs().isEmpty()) {
771                         throw new OrekitException(OrekitMessages.INCOMPLETE_HEADER, parseInfo.name);
772                     }
773 
774                 } else {
775                     if (parseInfo.file.getHeader().getMarkerName()           == null ||
776                         parseInfo.file.getHeader().getObserverName()         == null ||
777                         parseInfo.file.getHeader().getReceiverNumber()       == null ||
778                         parseInfo.file.getHeader().getAntennaNumber()        == null ||
779                         Double.isNaN(parseInfo.file.getHeader().getAntennaHeight()) &&
780                         parseInfo.file.getHeader().getAntennaReferencePoint() == null  ||
781                         parseInfo.file.getHeader().getTFirstObs()            == null ||
782                         parseInfo.file.getHeader().getTypeObs().isEmpty()) {
783                         throw new OrekitException(OrekitMessages.INCOMPLETE_HEADER, parseInfo.name);
784                     }
785                 }
786             },
787             LineParser::headerEndNext),
788 
789         /** Parser for Rinex 2 data list of satellites. */
790         RINEX_2_DATA_SAT_LIST(line -> true,
791                               (line, parseInfo) -> {
792                                   for (int index = 32; parseInfo.satObs.size() < parseInfo.nbSatObs && index < 68; index += 3) {
793                                       // add one PRN to the list of observed satellites
794                                       final SatelliteSystem system = line.charAt(index) == ' ' ?
795                                                                      parseInfo.file.getHeader().getSatelliteSystem() :
796                                                                      SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, index, 1));
797                                       if (system != parseInfo.file.getHeader().getSatelliteSystem() &&
798                                           parseInfo.file.getHeader().getSatelliteSystem() != SatelliteSystem.MIXED) {
799                                           throw new OrekitException(OrekitMessages.INCONSISTENT_SATELLITE_SYSTEM,
800                                                                     parseInfo.lineNumber, parseInfo.name,
801                                                                     parseInfo.file.getHeader().getSatelliteSystem(),
802                                                                     system);
803                                       }
804                                       final int             prn       = RinexUtils.parseInt(line, index + 1, 2);
805                                       final SatInSystem     satellite = new SatInSystem(system,
806                                                                                         system == SatelliteSystem.SBAS ? prn + 100 : prn);
807                                       parseInfo.satObs.add(satellite);
808                                       // note that we *must* use parseInfo.file.getHeader().getSatelliteSystem() as it was used to set up parseInfo.mapTypeObs
809                                       // and it may be MIXED to be applied to all satellites systems
810                                       final int nbObservables = parseInfo.file.getHeader().getTypeObs().get(parseInfo.file.getHeader().getSatelliteSystem()).size();
811                                       final int nbLines       = (nbObservables + MAX_OBS_PER_RINEX_2_LINE - 1) / MAX_OBS_PER_RINEX_2_LINE;
812                                       parseInfo.nextObsStartLineNumber += nbLines;
813                                   }
814                               },
815                               LineParser::first2),
816 
817         /** Parser for Rinex 2 data first line. */
818         RINEX_2_DATA_FIRST(line -> true,
819                            (line, parseInfo) -> {
820 
821                                // flag
822                                parseInfo.eventFlag = RinexUtils.parseInt(line, 28, 1);
823 
824                                // number of sats
825                                parseInfo.nbSatObs   = RinexUtils.parseInt(line, 29, 3);
826                                final int nbLinesSat = (parseInfo.nbSatObs + MAX_SAT_PER_RINEX_2_LINE - 1) / MAX_SAT_PER_RINEX_2_LINE;
827 
828                                if (parseInfo.eventFlag < 2) {
829                                    // regular observation
830                                    parseInfo.specialRecord = false;
831                                    parseInfo.cycleSlip     = false;
832                                    final int nbSat         = parseInfo.file.getHeader().getNbSat();
833                                    if (nbSat != -1 && parseInfo.nbSatObs > nbSat) {
834                                        // we check that the number of Sat in the observation is consistent
835                                        throw new OrekitException(OrekitMessages.INCONSISTENT_NUMBER_OF_SATS,
836                                                                  parseInfo.lineNumber, parseInfo.name,
837                                                                  parseInfo.nbSatObs, nbSat);
838                                    }
839                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + nbLinesSat;
840 
841                                    // read the Receiver Clock offset, if present
842                                    parseInfo.rcvrClkOffset = RinexUtils.parseDouble(line, 68, 12);
843                                    if (Double.isNaN(parseInfo.rcvrClkOffset)) {
844                                        parseInfo.rcvrClkOffset = 0.0;
845                                    }
846 
847                                } else if (parseInfo.eventFlag < 6) {
848                                    // moving antenna / new site occupation / header information / external event
849                                    // here, number of sats means number of lines to skip
850                                    parseInfo.specialRecord = true;
851                                    parseInfo.cycleSlip     = false;
852                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + parseInfo.nbSatObs + 1;
853                                } else if (parseInfo.eventFlag == 6) {
854                                    // cycle slip, we will ignore it during observations parsing
855                                    parseInfo.specialRecord = false;
856                                    parseInfo.cycleSlip     = true;
857                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + nbLinesSat;
858                                } else {
859                                    // unknown event flag
860                                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
861                                                              parseInfo.lineNumber, parseInfo.name, line);
862                                }
863 
864                                // parse the list of satellites observed
865                                parseInfo.satObs.clear();
866                                if (!parseInfo.specialRecord) {
867 
868                                    // observations epoch
869                                    parseInfo.setTObs(new AbsoluteDate(RinexUtils.convert2DigitsYear(RinexUtils.parseInt(line, 1, 2)),
870                                                                       RinexUtils.parseInt(line,  4, 2),
871                                                                       RinexUtils.parseInt(line,  7, 2),
872                                                                       RinexUtils.parseInt(line, 10, 2),
873                                                                       RinexUtils.parseInt(line, 13, 2),
874                                                                       RinexUtils.parseDouble(line, 15, 11),
875                                                                       parseInfo.timeScale));
876 
877                                    // satellites list
878                                    RINEX_2_DATA_SAT_LIST.parsingMethod.parse(line, parseInfo);
879 
880                                }
881 
882                                // prepare handling of observations for current epoch
883                                parseInfo.indexObsSat = 0;
884                                parseInfo.observations.clear();
885 
886                            },
887                            LineParser::first2),
888 
889         /** Parser for Rinex 2 special record. */
890         RINEX_2_IGNORED_SPECIAL_RECORD(line -> true,
891                            (line, parseInfo) -> {
892                                // nothing to do
893                            },
894                            LineParser::ignore2),
895 
896         /** Parser for Rinex 2 observation line. */
897         RINEX_2_OBSERVATION(line -> true,
898                             (line, parseInfo) -> {
899                                 final List<ObservationType> types = parseInfo.file.getHeader().getTypeObs().get(parseInfo.file.getHeader().getSatelliteSystem());
900                                 for (int index = 0;
901                                      parseInfo.observations.size() < types.size() && index < 80;
902                                      index += 16) {
903                                     final ObservationData observationData;
904                                     if (parseInfo.cycleSlip) {
905                                         // we are in a cycle slip data block (eventFlag = 6), we just ignore everything
906                                         observationData = null;
907                                     } else {
908                                         // this is a regular observation line
909                                         final ObservationType type    = types.get(parseInfo.observations.size());
910                                         final double          scaling = getScaling(parseInfo, type, parseInfo.currentSystem);
911                                         observationData = new ObservationData(type,
912                                                                               scaling * RinexUtils.parseDouble(line, index, 14),
913                                                                               RinexUtils.parseInt(line, index + 14, 1),
914                                                                               RinexUtils.parseInt(line, index + 15, 1));
915                                     }
916                                     parseInfo.observations.add(observationData);
917                                 }
918 
919                                 if (parseInfo.observations.size() == types.size()) {
920                                     // we have finished handling observations/cycle slips for one satellite
921                                     if (!parseInfo.cycleSlip) {
922                                         parseInfo.file.addObservationDataSet(new ObservationDataSet(parseInfo.satObs.get(parseInfo.indexObsSat),
923                                                                                                     parseInfo.tObs,
924                                                                                                     parseInfo.eventFlag,
925                                                                                                     parseInfo.rcvrClkOffset,
926                                                                                                     new ArrayList<>(parseInfo.observations)));
927                                     }
928                                     parseInfo.indexObsSat++;
929                                     parseInfo.observations.clear();
930                                 }
931 
932                             },
933                             LineParser::observation2),
934 
935         /** Parser for Rinex 3 observation line. */
936         RINEX_3_OBSERVATION(line -> true,
937                             (line, parseInfo) -> {
938                                 final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(RinexUtils.parseString(line, 0, 1));
939                                 final int             prn    = RinexUtils.parseInt(line, 1, 2);
940                                 final SatInSystem sat = new SatInSystem(system,
941                                                                         system == SatelliteSystem.SBAS ?
942                                                                         prn + 100 :
943                                                                         (system == SatelliteSystem.QZSS ? prn + 192 : prn));
944                                 final List<ObservationType> types = parseInfo.file.getHeader().getTypeObs().get(sat.getSystem());
945                                 for (int index = 3;
946                                      parseInfo.observations.size() < types.size();
947                                      index += 16) {
948                                     final ObservationData observationData;
949                                     if (parseInfo.specialRecord || parseInfo.cycleSlip) {
950                                         // we are in a special record (eventFlag < 6) or in a cycle slip data block (eventFlag = 6), we just ignore everything
951                                         observationData = null;
952                                     } else {
953                                         // this is a regular observation line
954                                         final ObservationType type    = types.get(parseInfo.observations.size());
955                                         final double          scaling = getScaling(parseInfo, type, sat.getSystem());
956                                         observationData = new ObservationData(type,
957                                                                               scaling * RinexUtils.parseDouble(line, index, 14),
958                                                                               RinexUtils.parseInt(line, index + 14, 1),
959                                                                               RinexUtils.parseInt(line, index + 15, 1));
960                                     }
961                                     parseInfo.observations.add(observationData);
962                                 }
963 
964                                 if (!(parseInfo.specialRecord || parseInfo.cycleSlip)) {
965                                     parseInfo.file.addObservationDataSet(new ObservationDataSet(sat,
966                                                                                                 parseInfo.tObs,
967                                                                                                 parseInfo.eventFlag,
968                                                                                                 parseInfo.rcvrClkOffset,
969                                                                                                 new ArrayList<>(parseInfo.observations)));
970                                 }
971                                 parseInfo.observations.clear();
972 
973                             },
974                             LineParser::observation3),
975 
976         /** Parser for Rinex 3 data first line. */
977         RINEX_3_DATA_FIRST(line -> line.startsWith(">"),
978                            (line, parseInfo) -> {
979 
980                                // flag
981                                parseInfo.eventFlag = RinexUtils.parseInt(line, 31, 1);
982 
983                                // number of sats
984                                parseInfo.nbSatObs   = RinexUtils.parseInt(line, 32, 3);
985 
986                                if (parseInfo.eventFlag < 2) {
987                                    // regular observation
988                                    parseInfo.specialRecord = false;
989                                    parseInfo.cycleSlip     = false;
990                                    final int nbSat         = parseInfo.file.getHeader().getNbSat();
991                                    if (nbSat != -1 && parseInfo.nbSatObs > nbSat) {
992                                        // we check that the number of Sat in the observation is consistent
993                                        throw new OrekitException(OrekitMessages.INCONSISTENT_NUMBER_OF_SATS,
994                                                                  parseInfo.lineNumber, parseInfo.name,
995                                                                  parseInfo.nbSatObs, nbSat);
996                                    }
997                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + parseInfo.nbSatObs + 1;
998 
999                                    // read the Receiver Clock offset, if present
1000                                    parseInfo.rcvrClkOffset = RinexUtils.parseDouble(line, 41, 15);
1001                                    if (Double.isNaN(parseInfo.rcvrClkOffset)) {
1002                                        parseInfo.rcvrClkOffset = 0.0;
1003                                    }
1004 
1005                                } else if (parseInfo.eventFlag < 6) {
1006                                    // moving antenna / new site occupation / header information / external event
1007                                    // here, number of sats means number of lines to skip
1008                                    parseInfo.specialRecord = true;
1009                                    parseInfo.cycleSlip     = false;
1010                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + parseInfo.nbSatObs + 1;
1011                                } else if (parseInfo.eventFlag == 6) {
1012                                    // cycle slip, we will ignore it during observations parsing
1013                                    parseInfo.specialRecord = false;
1014                                    parseInfo.cycleSlip     = true;
1015                                    parseInfo.nextObsStartLineNumber = parseInfo.lineNumber + parseInfo.nbSatObs + 1;
1016                                } else {
1017                                    // unknown event flag
1018                                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
1019                                                              parseInfo.lineNumber, parseInfo.name, line);
1020                                }
1021 
1022                                // parse the list of satellites observed
1023                                parseInfo.satObs.clear();
1024                                if (!parseInfo.specialRecord) {
1025 
1026                                    // observations epoch
1027                                    parseInfo.setTObs(new AbsoluteDate(RinexUtils.parseInt(line,  2, 4),
1028                                                                       RinexUtils.parseInt(line,  7, 2),
1029                                                                       RinexUtils.parseInt(line, 10, 2),
1030                                                                       RinexUtils.parseInt(line, 13, 2),
1031                                                                       RinexUtils.parseInt(line, 16, 2),
1032                                                                       RinexUtils.parseDouble(line, 18, 11),
1033                                                                       parseInfo.timeScale));
1034 
1035                                }
1036 
1037                                // prepare handling of observations for current epoch
1038                                parseInfo.observations.clear();
1039 
1040                            },
1041                            parseInfo -> Collections.singleton(RINEX_3_OBSERVATION));
1042 
1043 
1044         /** Predicate for identifying lines that can be parsed. */
1045         private final Predicate<String> canHandle;
1046 
1047         /** Parsing method. */
1048         private final ParsingMethod parsingMethod;
1049 
1050         /** Provider for next line parsers. */
1051         private final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider;
1052 
1053         /** Simple constructor.
1054          * @param canHandle predicate for identifying lines that can be parsed
1055          * @param parsingMethod parsing method
1056          * @param allowedNextProvider supplier for allowed parsers for next line
1057          */
1058         LineParser(final Predicate<String> canHandle, final ParsingMethod parsingMethod,
1059                    final Function<ParseInfo, Iterable<LineParser>> allowedNextProvider) {
1060             this.canHandle           = canHandle;
1061             this.parsingMethod       = parsingMethod;
1062             this.allowedNextProvider = allowedNextProvider;
1063         }
1064 
1065         /** Get the allowed parsers for next lines while parsing comments.
1066          * @param parseInfo holder for transient data
1067          * @return allowed parsers for next line
1068          */
1069         private static Iterable<LineParser> commentNext(final ParseInfo parseInfo) {
1070             return parseInfo.headerCompleted ? headerEndNext(parseInfo) : headerNext(parseInfo);
1071         }
1072 
1073         /** Get the allowed parsers for next lines while parsing Rinex header.
1074          * @param parseInfo holder for transient data
1075          * @return allowed parsers for next line
1076          */
1077         private static Iterable<LineParser> headerNext(final ParseInfo parseInfo) {
1078             if (parseInfo.file.getHeader().getFormatVersion() < 3) {
1079                 // Rinex 2.x header entries
1080                 return Arrays.asList(PROGRAM, COMMENT, MARKER_NAME, MARKER_NUMBER, MARKER_TYPE, OBSERVER_AGENCY,
1081                                      REC_NB_TYPE_VERS, ANT_NB_TYPE, APPROX_POSITION_XYZ, ANTENNA_DELTA_H_E_N,
1082                                      ANTENNA_DELTA_X_Y_Z, ANTENNA_B_SIGHT_XYZ, WAVELENGTH_FACT_L1_2, OBS_SCALE_FACTOR,
1083                                      CENTER_OF_MASS_XYZ, SYS_NB_TYPES_OF_OBSERV, INTERVAL, TIME_OF_FIRST_OBS, TIME_OF_LAST_OBS,
1084                                      RCV_CLOCK_OFFS_APPL, LEAP_SECONDS, NB_OF_SATELLITES, PRN_NB_OF_OBS, END);
1085             } else if (parseInfo.file.getHeader().getFormatVersion() < 4) {
1086                 // Rinex 3.x header entries
1087                 return Arrays.asList(PROGRAM, COMMENT, MARKER_NAME, MARKER_NUMBER, MARKER_TYPE, OBSERVER_AGENCY,
1088                                      REC_NB_TYPE_VERS, ANT_NB_TYPE, APPROX_POSITION_XYZ, ANTENNA_DELTA_H_E_N,
1089                                      ANTENNA_DELTA_X_Y_Z, ANTENNA_PHASE_CENTER, ANTENNA_B_SIGHT_XYZ, ANTENNA_ZERODIR_AZI,
1090                                      ANTENNA_ZERODIR_XYZ, CENTER_OF_MASS_XYZ, SYS_NB_TYPES_OF_OBSERV, SIGNAL_STRENGTH_UNIT,
1091                                      INTERVAL, TIME_OF_FIRST_OBS, TIME_OF_LAST_OBS, RCV_CLOCK_OFFS_APPL,
1092                                      SYS_DCBS_APPLIED, SYS_PCVS_APPLIED, SYS_SCALE_FACTOR, SYS_PHASE_SHIFT,
1093                                      GLONASS_SLOT_FRQ_NB, GLONASS_COD_PHS_BIS, LEAP_SECONDS, NB_OF_SATELLITES,
1094                                      PRN_NB_OF_OBS, END);
1095             } else {
1096                 // Rinex 4.x header entries
1097                 return Arrays.asList(PROGRAM, COMMENT, MARKER_NAME, MARKER_NUMBER, MARKER_TYPE, OBSERVER_AGENCY,
1098                                      REC_NB_TYPE_VERS, ANT_NB_TYPE, APPROX_POSITION_XYZ, ANTENNA_DELTA_H_E_N,
1099                                      ANTENNA_DELTA_X_Y_Z, ANTENNA_PHASE_CENTER, ANTENNA_B_SIGHT_XYZ, ANTENNA_ZERODIR_AZI,
1100                                      ANTENNA_ZERODIR_XYZ, CENTER_OF_MASS_XYZ, DOI, LICENSE, STATION_INFORMATION,
1101                                      SYS_NB_TYPES_OF_OBSERV, SIGNAL_STRENGTH_UNIT, INTERVAL, TIME_OF_FIRST_OBS, TIME_OF_LAST_OBS,
1102                                      RCV_CLOCK_OFFS_APPL, SYS_DCBS_APPLIED, SYS_PCVS_APPLIED, SYS_SCALE_FACTOR, SYS_PHASE_SHIFT,
1103                                      GLONASS_SLOT_FRQ_NB, GLONASS_COD_PHS_BIS, LEAP_SECONDS, NB_OF_SATELLITES,
1104                                      PRN_NB_OF_OBS, END);
1105             }
1106         }
1107 
1108         /** Get the allowed parsers for next lines while parsing header end.
1109          * @param parseInfo holder for transient data
1110          * @return allowed parsers for next line
1111          */
1112         private static Iterable<LineParser> headerEndNext(final ParseInfo parseInfo) {
1113             return Collections.singleton(parseInfo.file.getHeader().getFormatVersion() < 3 ?
1114                                          RINEX_2_DATA_FIRST : RINEX_3_DATA_FIRST);
1115         }
1116 
1117         /** Get the allowed parsers for next lines while parsing types of observations.
1118          * @param parseInfo holder for transient data
1119          * @return allowed parsers for next line
1120          */
1121         private static Iterable<LineParser> headerNbTypesObs(final ParseInfo parseInfo) {
1122             if (parseInfo.typesObs.size() < parseInfo.nbTypes) {
1123                 return Arrays.asList(COMMENT, SYS_NB_TYPES_OF_OBSERV);
1124             } else {
1125                 return headerNext(parseInfo);
1126             }
1127         }
1128 
1129         /** Get the allowed parsers for next lines while parsing phase shifts.
1130          * @param parseInfo holder for transient data
1131          * @return allowed parsers for next line
1132          */
1133         private static Iterable<LineParser> headerPhaseShift(final ParseInfo parseInfo) {
1134             if (parseInfo.satPhaseShift.size() < parseInfo.phaseShiftNbSat) {
1135                 return Arrays.asList(COMMENT, SYS_PHASE_SHIFT);
1136             } else {
1137                 return headerNext(parseInfo);
1138             }
1139         }
1140 
1141         /** Get the allowed parsers for next lines while parsing Rinex 2 observations first lines.
1142          * @param parseInfo holder for transient data
1143          * @return allowed parsers for next line
1144          */
1145         private static Iterable<LineParser> first2(final ParseInfo parseInfo) {
1146             if (parseInfo.specialRecord) {
1147                 return Collections.singleton(RINEX_2_IGNORED_SPECIAL_RECORD);
1148             } else if (parseInfo.satObs.size() < parseInfo.nbSatObs) {
1149                 return Collections.singleton(RINEX_2_DATA_SAT_LIST);
1150             } else {
1151                 return Collections.singleton(RINEX_2_OBSERVATION);
1152             }
1153         }
1154 
1155         /** Get the allowed parsers for next lines while parsing Rinex 2 ignored special records.
1156          * @param parseInfo holder for transient data
1157          * @return allowed parsers for next line
1158          */
1159         private static Iterable<LineParser> ignore2(final ParseInfo parseInfo) {
1160             if (parseInfo.lineNumber < parseInfo.nextObsStartLineNumber) {
1161                 return Collections.singleton(RINEX_2_IGNORED_SPECIAL_RECORD);
1162             } else {
1163                 return Arrays.asList(COMMENT, RINEX_2_DATA_FIRST);
1164             }
1165         }
1166 
1167         /** Get the allowed parsers for next lines while parsing Rinex 2 observations per se.
1168          * @param parseInfo holder for transient data
1169          * @return allowed parsers for next line
1170          */
1171         private static Iterable<LineParser> observation2(final ParseInfo parseInfo) {
1172             if (parseInfo.lineNumber < parseInfo.nextObsStartLineNumber) {
1173                 return Collections.singleton(RINEX_2_OBSERVATION);
1174             } else {
1175                 return Arrays.asList(COMMENT, RINEX_2_DATA_FIRST);
1176             }
1177         }
1178 
1179         /** Get the allowed parsers for next lines while parsing Rinex 3 observations.
1180          * @param parseInfo holder for transient data
1181          * @return allowed parsers for next line
1182          */
1183         private static Iterable<LineParser> observation3(final ParseInfo parseInfo) {
1184             if (parseInfo.lineNumber < parseInfo.nextObsStartLineNumber) {
1185                 return Collections.singleton(RINEX_3_OBSERVATION);
1186             } else {
1187                 return Arrays.asList(COMMENT, RINEX_3_DATA_FIRST);
1188             }
1189         }
1190 
1191         /** Get the scaling factor for an observation.
1192          * @param parseInfo holder for transient data
1193          * @param type type of observation
1194          * @param system satellite system for the observation
1195          * @return scaling factor
1196          */
1197         private static double getScaling(final ParseInfo parseInfo, final ObservationType type,
1198                                          final SatelliteSystem system) {
1199 
1200             for (final ScaleFactorCorrection scaleFactorCorrection :
1201                 parseInfo.file.getHeader().getScaleFactorCorrections(system)) {
1202                 // check if the next Observation Type to read needs to be scaled
1203                 if (scaleFactorCorrection.getTypesObsScaled().contains(type)) {
1204                     return 1.0 / scaleFactorCorrection.getCorrection();
1205                 }
1206             }
1207 
1208             // no scaling
1209             return 1.0;
1210 
1211         }
1212 
1213     }
1214 
1215     /** Parsing method. */
1216     @FunctionalInterface
1217     private interface ParsingMethod {
1218         /** Parse a line.
1219          * @param line line to parse
1220          * @param parseInfo holder for transient data
1221          */
1222         void parse(String line, ParseInfo parseInfo);
1223     }
1224 
1225 }