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