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