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.clock;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.Reader;
23  import java.nio.file.Paths;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.InputMismatchException;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Scanner;
31  import java.util.function.Function;
32  import java.util.regex.Pattern;
33  
34  import org.hipparchus.exception.LocalizedCoreFormats;
35  import org.orekit.annotation.DefaultDataContext;
36  import org.orekit.data.DataContext;
37  import org.orekit.data.DataSource;
38  import org.orekit.errors.OrekitException;
39  import org.orekit.errors.OrekitMessages;
40  import org.orekit.files.rinex.AppliedDCBS;
41  import org.orekit.files.rinex.AppliedPCVS;
42  import org.orekit.files.rinex.clock.RinexClock.ClockDataType;
43  import org.orekit.files.rinex.clock.RinexClock.Receiver;
44  import org.orekit.files.rinex.clock.RinexClock.ReferenceClock;
45  import org.orekit.frames.Frame;
46  import org.orekit.gnss.IGSUtils;
47  import org.orekit.gnss.ObservationType;
48  import org.orekit.gnss.PredefinedObservationType;
49  import org.orekit.gnss.SatelliteSystem;
50  import org.orekit.gnss.TimeSystem;
51  import org.orekit.time.AbsoluteDate;
52  import org.orekit.time.DateComponents;
53  import org.orekit.time.TimeComponents;
54  import org.orekit.time.TimeScale;
55  import org.orekit.time.TimeScales;
56  
57  /** A parser for the clock file from the IGS.
58   * This parser handles versions 2.0 to 3.04 of the RINEX clock files.
59   * <p> It is able to manage some mistakes in file writing and format compliance such as wrong date format,
60   * misplaced header blocks or missing information. </p>
61   * <p> A time system should be specified in the file. However, if it is not, default time system will be chosen
62   * regarding the satellite system. If it is mixed or not specified, default time system will be UTC. </p>
63   * <p> Caution, files with missing information in header can lead to wrong data dates and station positions.
64   * It is advised to check the correctness and format compliance of the clock file to be parsed. </p>
65   * @see <a href="https://files.igs.org/pub/data/format/rinex_clock300.txt"> 3.00 clock file format</a>
66   * @see <a href="https://files.igs.org/pub/data/format/rinex_clock302.txt"> 3.02 clock file format</a>
67   * @see <a href="https://files.igs.org/pub/data/format/rinex_clock304.txt"> 3.04 clock file format</a>
68   *
69   * @author Thomas Paulet
70   * @since 11.0
71   */
72  public class RinexClockParser {
73  
74      /** Handled clock file format versions. */
75      private static final List<Double> HANDLED_VERSIONS = Arrays.asList(2.00, 3.00, 3.01, 3.02, 3.04);
76  
77      /** Pattern for date format yyyy-mm-dd hh:mm. */
78      private static final Pattern DATE_PATTERN_1 = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");
79  
80      /** Pattern for date format yyyymmdd hhmmss zone or YYYYMMDD  HHMMSS zone. */
81      private static final Pattern DATE_PATTERN_2 = Pattern.compile("^[0-9]{8}\\s{1,2}[0-9]{6}.*$");
82  
83      /** Pattern for date format dd-MONTH-yyyy hh:mm zone or d-MONTH-yyyy hh:mm zone. */
84      private static final Pattern DATE_PATTERN_3 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}.*$");
85  
86      /** Pattern for date format dd-MONTH-yy hh:mm zone or d-MONTH-yy hh:mm zone. */
87      private static final Pattern DATE_PATTERN_4 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");
88  
89      /** Pattern for date format yyyy MONTH dd hh:mm:ss or yyyy MONTH d hh:mm:ss. */
90      private static final Pattern DATE_PATTERN_5 = Pattern.compile("^[0-9]{4} [a-z,A-Z]{3} [0-9]{1,2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*$");
91  
92      /** Spaces delimiters. */
93      private static final String SPACES = "\\s+";
94  
95      /** SYS string for line browsing stop. */
96      private static final String SYS = "SYS";
97  
98      /** One millimeter, in meters. */
99      private static final double MILLIMETER = 1.0e-3;
100 
101     /** Mapping from frame identifier in the file to a {@link Frame}. */
102     private final Function<? super String, ? extends Frame> frameBuilder;
103 
104     /** Set of time scales. */
105     private final TimeScales timeScales;
106 
107     /** Mapper from string to observation type.
108      * @since 13.0
109      */
110     private final Function<? super String, ? extends ObservationType> typeBuilder;
111 
112     /** Create a clock file parser using default values.
113      * <p>
114      * This constructor uses the {@link DataContext#getDefault() default data context},
115      * and {@link IGSUtils#guessFrame} and recognizes only {@link PredefinedObservationType}.
116      * </p>
117      * @see #RinexClockParser(Function)
118      */
119     @DefaultDataContext
120     public RinexClockParser() {
121         this(IGSUtils::guessFrame);
122     }
123 
124     /** Create a clock file parser and specify the frame builder.
125      * <p>
126      * This constructor uses the {@link DataContext#getDefault() default data context}
127      * and recognizes only {@link PredefinedObservationType}.
128      * </p>
129      * @param frameBuilder is a function that can construct a frame from a clock file
130      *                     coordinate system string. The coordinate system can be
131      *                     any 5 character string e.g. ITR92, IGb08.
132      * @see #RinexClockParser(Function, Function, TimeScales)
133      */
134     @DefaultDataContext
135     public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder) {
136         this(frameBuilder, PredefinedObservationType::valueOf,
137              DataContext.getDefault().getTimeScales());
138     }
139 
140     /** Constructor, build the IGS clock file parser.
141      * @param frameBuilder is a function that can construct a frame from a clock file
142      *                     coordinate system string. The coordinate system can be
143      *                     any 5 character string e.g. ITR92, IGb08.
144      * @param typeBuilder mapper from string to observation type
145      * @param timeScales   the set of time scales used for parsing dates.
146      * @since 13.0
147      */
148     public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder,
149                             final Function<? super String, ? extends ObservationType> typeBuilder,
150                             final TimeScales timeScales) {
151         this.frameBuilder = frameBuilder;
152         this.typeBuilder  = typeBuilder;
153         this.timeScales   = timeScales;
154     }
155 
156     /**
157      * Parse an IGS clock file from an input stream using the UTF-8 charset.
158      *
159      * <p> This method creates a {@link BufferedReader} from the stream and as such this
160      * method may read more data than necessary from {@code stream} and the additional
161      * data will be lost. The other parse methods do not have this issue.
162      *
163      * @param stream to read the IGS clock file from
164      * @return a parsed IGS clock file
165      * @see #parse(String)
166      * @see #parse(BufferedReader, String)
167      * @see #parse(DataSource)
168      */
169     public RinexClock parse(final InputStream stream) {
170         return parse(new DataSource("<stream>", () -> stream));
171     }
172 
173     /**
174      * Parse an IGS clock file from a file on the local file system.
175      * @param fileName file name
176      * @return a parsed IGS clock file
177      * @see #parse(InputStream)
178      * @see #parse(BufferedReader, String)
179      * @see #parse(DataSource)
180      */
181     public RinexClock parse(final String fileName) {
182         return parse(new DataSource(Paths.get(fileName).toFile()));
183     }
184 
185     /**
186      * Parse an IGS clock file from a stream.
187      * @param reader containing the clock file
188      * @param fileName file name
189      * @return a parsed IGS clock file
190      * @see #parse(InputStream)
191      * @see #parse(String)
192      * @see #parse(DataSource)
193      */
194     public RinexClock parse(final BufferedReader reader, final String fileName) {
195         return parse(new DataSource(fileName, () -> reader));
196     }
197 
198     /** Parse an IGS clock file from a {@link DataSource}.
199      * @param source source for clock file
200      * @return a parsed IGS clock file
201      * @see #parse(InputStream)
202      * @see #parse(String)
203      * @see #parse(BufferedReader, String)
204      * @since 12.1
205      */
206     public RinexClock parse(final DataSource source) {
207 
208         // initialize internal data structures
209         final ParseInfo pi = new ParseInfo();
210 
211         try (Reader reader = source.getOpener().openReaderOnce();
212              BufferedReader br = new BufferedReader(reader)) {
213             pi.lineNumber = 0;
214             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
215             nextLine:
216             for (String line = br.readLine(); line != null; line = br.readLine()) {
217                 ++pi.lineNumber;
218                 for (final LineParser candidate : candidateParsers) {
219                     if (candidate.canHandle(line)) {
220                         try {
221                             candidate.parse(line, pi);
222                             candidateParsers = candidate.allowedNext();
223                             continue nextLine;
224                         } catch (StringIndexOutOfBoundsException |
225                             NumberFormatException | InputMismatchException e) {
226                             throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
227                                                       pi.lineNumber, source.getName(), line);
228                         }
229                     }
230                 }
231 
232                 // no parsers found for this line
233                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
234                                           pi.lineNumber, source.getName(), line);
235 
236             }
237 
238         } catch (IOException ioe) {
239             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
240         }
241 
242         return pi.file;
243 
244     }
245 
246     /** Transient data used for parsing a clock file. */
247     private class ParseInfo {
248 
249         /** Current line number of the navigation message. */
250         private int lineNumber;
251 
252         /** Set of time scales for parsing dates. */
253         private final TimeScales timeScales;
254 
255         /** The corresponding clock file object. */
256         private final RinexClock file;
257 
258         /** Current satellite system for observation type parsing. */
259         private SatelliteSystem currentSatelliteSystem;
260 
261         /** Current start date for reference clocks. */
262         private AbsoluteDate referenceClockStartDate;
263 
264         /** Current end date for reference clocks. */
265         private AbsoluteDate referenceClockEndDate;
266 
267         /** Pending reference clocks list. */
268         private List<ReferenceClock> pendingReferenceClocks;
269 
270         /** Current clock data type. */
271         private ClockDataType currentDataType;
272 
273         /** Current receiver/satellite name. */
274         private String currentName;
275 
276         /** Current data date components. */
277         private DateComponents currentDateComponents;
278 
279         /** Current data time components. */
280         private TimeComponents currentTimeComponents;
281 
282         /** Current data number of data values to follow. */
283         private int currentNumberOfValues;
284 
285         /** Current data values. */
286         private double[] currentDataValues;
287 
288         /** Constructor, build the ParseInfo object. */
289         protected ParseInfo () {
290             this.timeScales = RinexClockParser.this.timeScales;
291             this.file = new RinexClock(frameBuilder);
292             this.pendingReferenceClocks = new ArrayList<>();
293         }
294 
295         /** Build an observation type.
296          * @param type observation type
297          * @return built type
298          */
299         ObservationType buildType(final String type) {
300             return RinexClockParser.this.typeBuilder.apply(type);
301         }
302 
303     }
304 
305 
306     /** Parsers for specific lines. */
307     private enum LineParser {
308 
309         /** Parser for version, file type and satellite system. */
310         HEADER_VERSION("^.+RINEX VERSION / TYPE( )*$") {
311 
312             /** {@inheritDoc} */
313             @Override
314             public void parse(final String line, final ParseInfo pi) {
315                 try (Scanner s1      = new Scanner(line);
316                      Scanner s2      = s1.useDelimiter(SPACES);
317                      Scanner scanner = s2.useLocale(Locale.US)) {
318 
319                     // First element of the line is format version
320                     final double version = scanner.nextDouble();
321 
322                     // Throw exception if format version is not handled
323                     if (!HANDLED_VERSIONS.contains(version)) {
324                         throw new OrekitException(OrekitMessages.CLOCK_FILE_UNSUPPORTED_VERSION, version);
325                     }
326 
327                     pi.file.setFormatVersion(version);
328 
329                     // Second element is clock file indicator, not used here
330 
331                     // Last element is the satellite system, might be missing
332                     final String satelliteSystemString = line.substring(40, 45).trim();
333 
334                     // Check satellite if system is recorded
335                     if (!satelliteSystemString.isEmpty()) {
336                         // Record satellite system and default time system in clock file object
337                         final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(satelliteSystemString);
338                         pi.file.setSatelliteSystem(satelliteSystem);
339                         if (satelliteSystem.getObservationTimeScale() != null) {
340                             pi.file.setTimeScale(satelliteSystem.getObservationTimeScale().getTimeScale(pi.timeScales));
341                         }
342                     }
343                     // Set time scale to UTC by default
344                     if (pi.file.getTimeScale() == null) {
345                         pi.file.setTimeScale(pi.timeScales.getUTC());
346                     }
347                 }
348             }
349 
350         },
351 
352         /** Parser for generating program and emiting agency. */
353         HEADER_PROGRAM("^.+PGM / RUN BY / DATE( )*$") {
354 
355             /** {@inheritDoc} */
356             @Override
357             public void parse(final String line, final ParseInfo pi) {
358 
359                 // First element of the name of the generating program
360                 final String programName = line.substring(0, 20).trim();
361                 pi.file.setProgramName(programName);
362 
363                 // Second element is the name of the emiting agency
364                 final String agencyName = line.substring(20, 40).trim();
365                 pi.file.setAgencyName(agencyName);
366 
367                 // Third element is date
368                 String dateString = "";
369 
370                 if (pi.file.getFormatVersion() < 3.04) {
371 
372                     // Date string location before 3.04 format version
373                     dateString = line.substring(40, 60);
374 
375                 } else {
376 
377                     // Date string location after 3.04 format version
378                     dateString = line.substring(42, 65);
379 
380                 }
381 
382                 parseDateTimeZone(dateString, pi);
383 
384             }
385 
386         },
387 
388         /** Parser for comments. */
389         HEADER_COMMENT("^.+COMMENT( )*$") {
390 
391             /** {@inheritDoc} */
392             @Override
393             public void parse(final String line, final ParseInfo pi) {
394 
395                 if (pi.file.getFormatVersion() < 3.04) {
396                     pi.file.addComment(line.substring(0, 60).trim());
397                 } else {
398                     pi.file.addComment(line.substring(0, 65).trim());
399                 }
400             }
401 
402         },
403 
404         /** Parser for satellite system and related observation types. */
405         HEADER_SYSTEM_OBS("^[A-Z] .*SYS / # / OBS TYPES( )*$") {
406 
407             /** {@inheritDoc} */
408             @Override
409             public void parse(final String line, final ParseInfo pi) {
410                 try (Scanner s1      = new Scanner(line);
411                      Scanner s2      = s1.useDelimiter(SPACES);
412                      Scanner scanner = s2.useLocale(Locale.US)) {
413 
414                     // First element of the line is satellite system code
415                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(scanner.next());
416                     pi.currentSatelliteSystem = satelliteSystem;
417 
418                     // Second element is the number of different observation types
419                     scanner.nextInt();
420 
421                     // Parse all observation types
422                     String currentObsType = scanner.next();
423                     while (!currentObsType.equals(SYS)) {
424                         pi.file.addSystemObservationType(satelliteSystem, pi.buildType(currentObsType));
425                         currentObsType = scanner.next();
426                     }
427                 }
428             }
429 
430         },
431 
432         /** Parser for continuation of satellite system and related observation types. */
433         HEADER_SYSTEM_OBS_CONTINUATION("^ .*SYS / # / OBS TYPES( )*$") {
434 
435             /** {@inheritDoc} */
436             @Override
437             public void parse(final String line, final ParseInfo pi) {
438                 try (Scanner s1      = new Scanner(line);
439                      Scanner s2      = s1.useDelimiter(SPACES);
440                      Scanner scanner = s2.useLocale(Locale.US)) {
441 
442                     // This is a continuation line, there are only observation types
443                     // Parse all observation types
444                     String currentObsType = scanner.next();
445                     while (!currentObsType.equals(SYS)) {
446                         pi.file.addSystemObservationType(pi.currentSatelliteSystem, pi.buildType(currentObsType));
447                         currentObsType = scanner.next();
448                     }
449                 }
450             }
451 
452         },
453 
454         /** Parser for data time system. */
455         HEADER_TIME_SYSTEM("^.+TIME SYSTEM ID( )*$") {
456 
457             /** {@inheritDoc} */
458             @Override
459             public void parse(final String line, final ParseInfo pi) {
460                 try (Scanner s1      = new Scanner(line);
461                      Scanner s2      = s1.useDelimiter(SPACES);
462                      Scanner scanner = s2.useLocale(Locale.US)) {
463 
464                     // Only element is the time system code
465                     final TimeSystem timeSystem = TimeSystem.parseTimeSystem(scanner.next());
466                     final TimeScale timeScale = timeSystem.getTimeScale(pi.timeScales);
467                     pi.file.setTimeSystem(timeSystem);
468                     pi.file.setTimeScale(timeScale);
469                 }
470             }
471 
472         },
473 
474         /** Parser for leap seconds. */
475         HEADER_LEAP_SECONDS("^.+LEAP SECONDS( )*$") {
476 
477             /** {@inheritDoc} */
478             @Override
479             public void parse(final String line, final ParseInfo pi) {
480                 try (Scanner s1      = new Scanner(line);
481                      Scanner s2      = s1.useDelimiter(SPACES);
482                      Scanner scanner = s2.useLocale(Locale.US)) {
483 
484                     // Only element is the number of leap seconds
485                     final int numberOfLeapSeconds = scanner.nextInt();
486                     pi.file.setNumberOfLeapSeconds(numberOfLeapSeconds);
487                 }
488             }
489 
490         },
491 
492         /** Parser for leap seconds GNSS. */
493         HEADER_LEAP_SECONDS_GNSS("^.+LEAP SECONDS GNSS( )*$") {
494 
495             /** {@inheritDoc} */
496             @Override
497             public void parse(final String line, final ParseInfo pi) {
498                 try (Scanner s1      = new Scanner(line);
499                      Scanner s2      = s1.useDelimiter(SPACES);
500                      Scanner scanner = s2.useLocale(Locale.US)) {
501 
502                     // Only element is the number of leap seconds GNSS
503                     final int numberOfLeapSecondsGNSS = scanner.nextInt();
504                     pi.file.setNumberOfLeapSecondsGNSS(numberOfLeapSecondsGNSS);
505                 }
506             }
507 
508         },
509 
510         /** Parser for applied differencial code bias corrections. */
511         HEADER_DCBS("^.+SYS / DCBS APPLIED( )*$") {
512 
513             /** {@inheritDoc} */
514             @Override
515             public void parse(final String line, final ParseInfo pi) {
516                 // First element, if present, is the related satellite system
517                 final String system = line.substring(0, 1);
518                 if (!" ".equals(system)) {
519                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);
520 
521                     // Second element is the program name
522                     final String progDCBS = line.substring(2, 20).trim();
523 
524                     // Third element is the source of the corrections
525                     String sourceDCBS = "";
526                     if (pi.file.getFormatVersion() < 3.04) {
527                         sourceDCBS = line.substring(19, 60).trim();
528                     } else {
529                         sourceDCBS = line.substring(22, 65).trim();
530                     }
531 
532                     // Check if sought fields were not actually blanks
533                     if (!progDCBS.isEmpty()) {
534                         pi.file.addAppliedDCBS(new AppliedDCBS(satelliteSystem, progDCBS, sourceDCBS));
535                     }
536                 }
537             }
538 
539         },
540 
541         /** Parser for applied phase center variation corrections. */
542         HEADER_PCVS("^.+SYS / PCVS APPLIED( )*$") {
543 
544             /** {@inheritDoc} */
545             @Override
546             public void parse(final String line, final ParseInfo pi) {
547 
548                 // First element, if present, is the related satellite system
549                 final String system = line.substring(0, 1);
550                 if (!" ".equals(system)) {
551                     final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(system);
552 
553                     // Second element is the program name
554                     final String progPCVS = line.substring(2, 20).trim();
555 
556                     // Third element is the source of the corrections
557                     String sourcePCVS = "";
558                     if (pi.file.getFormatVersion() < 3.04) {
559                         sourcePCVS = line.substring(19, 60).trim();
560                     } else {
561                         sourcePCVS = line.substring(22, 65).trim();
562                     }
563 
564                     // Check if sought fields were not actually blanks
565                     if (!progPCVS.isEmpty() || !sourcePCVS.isEmpty()) {
566                         pi.file.addAppliedPCVS(new AppliedPCVS(satelliteSystem, progPCVS, sourcePCVS));
567                     }
568                 }
569             }
570 
571         },
572 
573         /** Parser for the different clock data types that are stored in the file. */
574         HEADER_TYPES_OF_DATA("^.+# / TYPES OF DATA( )*$") {
575 
576             /** {@inheritDoc} */
577             @Override
578             public void parse(final String line, final ParseInfo pi) {
579                 try (Scanner s1      = new Scanner(line);
580                      Scanner s2      = s1.useDelimiter(SPACES);
581                      Scanner scanner = s2.useLocale(Locale.US)) {
582 
583                     // First element is the number of different types of data
584                     final int numberOfDifferentDataTypes = scanner.nextInt();
585 
586                     // Loop over data types
587                     for (int i = 0; i < numberOfDifferentDataTypes; i++) {
588                         final ClockDataType dataType = ClockDataType.parseClockDataType(scanner.next());
589                         pi.file.addClockDataType(dataType);
590                     }
591                 }
592             }
593 
594         },
595 
596         /** Parser for the station with reference clock. */
597         HEADER_STATIONS_NAME("^.+STATION NAME / NUM( )*$") {
598 
599             /** {@inheritDoc} */
600             @Override
601             public void parse(final String line, final ParseInfo pi) {
602                 try (Scanner s1      = new Scanner(line);
603                      Scanner s2      = s1.useDelimiter(SPACES);
604                      Scanner scanner = s2.useLocale(Locale.US)) {
605 
606                     // First element is the station clock reference ID
607                     final String stationName = scanner.next();
608                     pi.file.setStationName(stationName);
609 
610                     // Second element is the station clock reference identifier
611                     final String stationIdentifier = scanner.next();
612                     pi.file.setStationIdentifier(stationIdentifier);
613                 }
614             }
615 
616         },
617 
618         /** Parser for the reference clock in case of calibration data. */
619         HEADER_STATION_CLOCK_REF("^.+STATION CLK REF( )*$") {
620 
621             /** {@inheritDoc} */
622             @Override
623             public void parse(final String line, final ParseInfo pi) {
624                 if (pi.file.getFormatVersion() < 3.04) {
625                     pi.file.setExternalClockReference(line.substring(0, 60).trim());
626                 } else {
627                     pi.file.setExternalClockReference(line.substring(0, 65).trim());
628                 }
629             }
630 
631         },
632 
633         /** Parser for the analysis center. */
634         HEADER_ANALYSIS_CENTER("^.+ANALYSIS CENTER( )*$") {
635 
636             /** {@inheritDoc} */
637             @Override
638             public void parse(final String line, final ParseInfo pi) {
639 
640                 // First element is IGS AC designator
641                 final String analysisCenterID = line.substring(0, 3).trim();
642                 pi.file.setAnalysisCenterID(analysisCenterID);
643 
644                 // Then, the full name of the analysis center
645                 String analysisCenterName = "";
646                 if (pi.file.getFormatVersion() < 3.04) {
647                     analysisCenterName = line.substring(5, 60).trim();
648                 } else {
649                     analysisCenterName = line.substring(5, 65).trim();
650                 }
651                 pi.file.setAnalysisCenterName(analysisCenterName);
652             }
653 
654         },
655 
656         /** Parser for the number of reference clocks over a period. */
657         HEADER_NUMBER_OF_CLOCK_REF("^.+# OF CLK REF( )*$") {
658 
659             /** {@inheritDoc} */
660             @Override
661             public void parse(final String line, final ParseInfo pi) {
662                 try (Scanner s1      = new Scanner(line);
663                      Scanner s2      = s1.useDelimiter(SPACES);
664                      Scanner scanner = s2.useLocale(Locale.US)) {
665 
666                     if (!pi.pendingReferenceClocks.isEmpty()) {
667                         // Modify time span map of the reference clocks to accept the pending reference clock
668                         pi.file.addReferenceClockList(pi.pendingReferenceClocks,
669                                                       pi.referenceClockStartDate);
670                         pi.pendingReferenceClocks = new ArrayList<>();
671                     }
672 
673                     // First element is the number of reference clocks corresponding to the period
674                     scanner.nextInt();
675 
676                     if (scanner.hasNextInt()) {
677                         // Second element is the start epoch of the period
678                         final int startYear   = scanner.nextInt();
679                         final int startMonth  = scanner.nextInt();
680                         final int startDay    = scanner.nextInt();
681                         final int startHour   = scanner.nextInt();
682                         final int startMin    = scanner.nextInt();
683                         final double startSec = scanner.nextDouble();
684                         final AbsoluteDate startEpoch = new AbsoluteDate(startYear, startMonth, startDay,
685                                                                          startHour, startMin, startSec,
686                                                                          pi.file.getTimeScale());
687                         pi.referenceClockStartDate = startEpoch;
688 
689                         // Third element is the end epoch of the period
690                         final int endYear   = scanner.nextInt();
691                         final int endMonth  = scanner.nextInt();
692                         final int endDay    = scanner.nextInt();
693                         final int endHour   = scanner.nextInt();
694                         final int endMin    = scanner.nextInt();
695                         double endSec       = 0.0;
696                         if (pi.file.getFormatVersion() < 3.04) {
697                             endSec = Double.parseDouble(line.substring(51, 60));
698                         } else {
699                             endSec = scanner.nextDouble();
700                         }
701                         final AbsoluteDate endEpoch = new AbsoluteDate(endYear, endMonth, endDay,
702                                                                        endHour, endMin, endSec,
703                                                                        pi.file.getTimeScale());
704                         pi.referenceClockEndDate = endEpoch;
705                     } else {
706                         pi.referenceClockStartDate = AbsoluteDate.PAST_INFINITY;
707                         pi.referenceClockEndDate = AbsoluteDate.FUTURE_INFINITY;
708                     }
709                 }
710             }
711 
712         },
713 
714         /** Parser for the reference clock over a period. */
715         HEADER_ANALYSIS_CLOCK_REF("^.+ANALYSIS CLK REF( )*$") {
716 
717             /** {@inheritDoc} */
718             @Override
719             public void parse(final String line, final ParseInfo pi) {
720                 try (Scanner s1      = new Scanner(line);
721                      Scanner s2      = s1.useDelimiter(SPACES);
722                      Scanner scanner = s2.useLocale(Locale.US)) {
723 
724                     // First element is the name of the receiver/satellite embedding the reference clock
725                     final String referenceName = scanner.next();
726 
727                     // Second element is the reference clock ID
728                     final String clockID = scanner.next();
729 
730                     // Optionally, third element is an a priori clock constraint, by default equal to zero
731                     double clockConstraint = 0.0;
732                     if (scanner.hasNextDouble()) {
733                         clockConstraint = scanner.nextDouble();
734                     }
735 
736                     // Add reference clock to current reference clock list
737                     final ReferenceClock referenceClock = new ReferenceClock(referenceName, clockID, clockConstraint,
738                                                                              pi.referenceClockStartDate, pi.referenceClockEndDate);
739                     pi.pendingReferenceClocks.add(referenceClock);
740 
741                 }
742             }
743 
744         },
745 
746         /** Parser for the number of stations embedded in the file and the related frame. */
747         HEADER_NUMBER_OF_SOLN_STATIONS("^.+SOLN STA / TRF( )*$") {
748 
749             /** {@inheritDoc} */
750             @Override
751             public void parse(final String line, final ParseInfo pi) {
752                 try (Scanner s1      = new Scanner(line);
753                      Scanner s2      = s1.useDelimiter(SPACES);
754                      Scanner scanner = s2.useLocale(Locale.US)) {
755 
756                     // First element is the number of receivers embedded in the file
757                     scanner.nextInt();
758 
759                     // Second element is the frame linked to given receiver positions
760                     final String frameString = scanner.next();
761                     pi.file.setFrameName(frameString);
762                 }
763             }
764 
765         },
766 
767         /** Parser for the stations embedded in the file and the related positions. */
768         HEADER_SOLN_STATIONS("^.+SOLN STA NAME / NUM( )*$") {
769 
770             /** {@inheritDoc} */
771             @Override
772             public void parse(final String line, final ParseInfo pi) {
773 
774                 // First element is the receiver designator
775                 String designator = line.substring(0, 10).trim();
776 
777                 // Second element is the receiver identifier
778                 String receiverIdentifier = line.substring(10, 30).trim();
779 
780                 // Third element if X coordinates, in millimeters in the file frame.
781                 String xString = "";
782 
783                 // Fourth element if Y coordinates, in millimeters in the file frame.
784                 String yString = "";
785 
786                 // Fifth element if Z coordinates, in millimeters in the file frame.
787                 String zString = "";
788 
789                 if (pi.file.getFormatVersion() < 3.04) {
790                     designator = line.substring(0, 4).trim();
791                     receiverIdentifier = line.substring(5, 25).trim();
792                     xString = line.substring(25, 36).trim();
793                     yString = line.substring(37, 48).trim();
794                     zString = line.substring(49, 60).trim();
795                 } else {
796                     designator = line.substring(0, 10).trim();
797                     receiverIdentifier = line.substring(10, 30).trim();
798                     xString = line.substring(30, 41).trim();
799                     yString = line.substring(42, 53).trim();
800                     zString = line.substring(54, 65).trim();
801                 }
802 
803                 final double x = MILLIMETER * Double.parseDouble(xString);
804                 final double y = MILLIMETER * Double.parseDouble(yString);
805                 final double z = MILLIMETER * Double.parseDouble(zString);
806 
807                 final Receiver receiver = new Receiver(designator, receiverIdentifier, x, y, z);
808                 pi.file.addReceiver(receiver);
809 
810             }
811 
812         },
813 
814         /** Parser for the number of satellites embedded in the file. */
815         HEADER_NUMBER_OF_SOLN_SATS("^.+# OF SOLN SATS( )*$") {
816 
817             /** {@inheritDoc} */
818             @Override
819             public void parse(final String line, final ParseInfo pi) {
820 
821                     // Only element in the line is number of satellites, not used here.
822                     // Do nothing...
823             }
824 
825         },
826 
827         /** Parser for the satellites embedded in the file. */
828         HEADER_PRN_LIST("^.+PRN LIST( )*$") {
829 
830             /** {@inheritDoc} */
831             @Override
832             public void parse(final String line, final ParseInfo pi) {
833                 try (Scanner s1      = new Scanner(line);
834                      Scanner s2      = s1.useDelimiter(SPACES);
835                      Scanner scanner = s2.useLocale(Locale.US)) {
836 
837                     // Only PRN numbers are stored in these lines
838                     // Initialize first PRN number
839                     String prn = scanner.next();
840 
841                     // Browse the line until its end
842                     while (!prn.equals("PRN")) {
843                         pi.file.addSatellite(prn);
844                         prn = scanner.next();
845                     }
846                 }
847             }
848 
849         },
850 
851         /** Parser for the end of header. */
852         HEADER_END("^.+END OF HEADER( )*$") {
853 
854             /** {@inheritDoc} */
855             @Override
856             public void parse(final String line, final ParseInfo pi) {
857                 if (!pi.pendingReferenceClocks.isEmpty()) {
858                     // Modify time span map of the reference clocks to accept the pending reference clock
859                     pi.file.addReferenceClockList(pi.pendingReferenceClocks, pi.referenceClockStartDate);
860                 }
861             }
862 
863             /** {@inheritDoc} */
864             @Override
865             public Iterable<LineParser> allowedNext() {
866                 return Collections.singleton(CLOCK_DATA);
867             }
868         },
869 
870         /** Parser for a clock data line. */
871         CLOCK_DATA("(^AR |^AS |^CR |^DR |^MS ).+$") {
872 
873             /** {@inheritDoc} */
874             @Override
875             public void parse(final String line, final ParseInfo pi) {
876                 try (Scanner s1      = new Scanner(line);
877                      Scanner s2      = s1.useDelimiter(SPACES);
878                      Scanner scanner = s2.useLocale(Locale.US)) {
879 
880                     // Initialise current values
881                     pi.currentDataValues = new double[6];
882 
883                     // First element is clock data type
884                     pi.currentDataType = ClockDataType.parseClockDataType(scanner.next());
885 
886                     // Second element is receiver/satellite name
887                     pi.currentName = scanner.next();
888 
889                     // Third element is data epoch
890                     final int year   = scanner.nextInt();
891                     final int month  = scanner.nextInt();
892                     final int day    = scanner.nextInt();
893                     final int hour   = scanner.nextInt();
894                     final int min    = scanner.nextInt();
895                     final double sec = scanner.nextDouble();
896                     pi.currentDateComponents = new DateComponents(year, month, day);
897                     pi.currentTimeComponents = new TimeComponents(hour, min, sec);
898 
899                     // Fourth element is number of data values
900                     pi.currentNumberOfValues = scanner.nextInt();
901 
902                     // Get the values in this line, there are at most 2.
903                     // Some entries claim less values than there actually are.
904                     // All values are added to the set, regardless of their claimed number.
905                     int i = 0;
906                     while (scanner.hasNextDouble()) {
907                         pi.currentDataValues[i++] = scanner.nextDouble();
908                     }
909 
910                     // Check if continuation line is required
911                     if (pi.currentNumberOfValues <= 2) {
912                         // No continuation line is required
913                         pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
914                                                                                        pi.currentName,
915                                                                                        pi.currentDateComponents,
916                                                                                        pi.currentTimeComponents,
917                                                                                        pi.currentNumberOfValues,
918                                                                                        pi.currentDataValues[0],
919                                                                                        pi.currentDataValues[1],
920                                                                                        0.0, 0.0, 0.0, 0.0));
921                     }
922                 }
923             }
924 
925             /** {@inheritDoc} */
926             @Override
927             public Iterable<LineParser> allowedNext() {
928                 return Arrays.asList(CLOCK_DATA, CLOCK_DATA_CONTINUATION);
929             }
930         },
931 
932         /** Parser for a continuation clock data line. */
933         CLOCK_DATA_CONTINUATION("^   .+") {
934 
935             /** {@inheritDoc} */
936             @Override
937             public void parse(final String line, final ParseInfo pi) {
938                 try (Scanner s1      = new Scanner(line);
939                      Scanner s2      = s1.useDelimiter(SPACES);
940                      Scanner scanner = s2.useLocale(Locale.US)) {
941 
942                     // Get the values in this continuation line.
943                     // Some entries claim less values than there actually are.
944                     // All values are added to the set, regardless of their claimed number.
945                     int i = 2;
946                     while (scanner.hasNextDouble()) {
947                         pi.currentDataValues[i++] = scanner.nextDouble();
948                     }
949 
950                     // Add clock data line
951                     pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
952                                                                                    pi.currentName,
953                                                                                    pi.currentDateComponents,
954                                                                                    pi.currentTimeComponents,
955                                                                                    pi.currentNumberOfValues,
956                                                                                    pi.currentDataValues[0],
957                                                                                    pi.currentDataValues[1],
958                                                                                    pi.currentDataValues[2],
959                                                                                    pi.currentDataValues[3],
960                                                                                    pi.currentDataValues[4],
961                                                                                    pi.currentDataValues[5]));
962 
963                 }
964             }
965 
966             /** {@inheritDoc} */
967             @Override
968             public Iterable<LineParser> allowedNext() {
969                 return Collections.singleton(CLOCK_DATA);
970             }
971         };
972 
973         /** Pattern for identifying line. */
974         private final Pattern pattern;
975 
976         /** Simple constructor.
977          * @param lineRegexp regular expression for identifying line
978          */
979         LineParser(final String lineRegexp) {
980             pattern = Pattern.compile(lineRegexp);
981         }
982 
983         /** Parse a line.
984          * @param line line to parse
985          * @param pi holder for transient data
986          */
987         public abstract void parse(String line, ParseInfo pi);
988 
989         /** Get the allowed parsers for next line.
990          * <p>
991          * Because the standard only recommends an order for header keys,
992          * the default implementation of the method returns all the
993          * header keys. Specific implementations must overrides the method.
994          * </p>
995          * @return allowed parsers for next line
996          */
997         public Iterable<LineParser> allowedNext() {
998             return Arrays.asList(HEADER_PROGRAM, HEADER_COMMENT, HEADER_SYSTEM_OBS, HEADER_SYSTEM_OBS_CONTINUATION, HEADER_TIME_SYSTEM, HEADER_LEAP_SECONDS,
999                                  HEADER_LEAP_SECONDS_GNSS, HEADER_DCBS, HEADER_PCVS, HEADER_TYPES_OF_DATA, HEADER_STATIONS_NAME, HEADER_STATION_CLOCK_REF,
1000                                  HEADER_ANALYSIS_CENTER, HEADER_NUMBER_OF_CLOCK_REF, HEADER_ANALYSIS_CLOCK_REF, HEADER_NUMBER_OF_SOLN_STATIONS,
1001                                  HEADER_SOLN_STATIONS, HEADER_NUMBER_OF_SOLN_SATS, HEADER_PRN_LIST, HEADER_END);
1002         }
1003 
1004         /** Check if parser can handle line.
1005          * @param line line to parse
1006          * @return true if parser can handle the specified line
1007          */
1008         public boolean canHandle(final String line) {
1009             return pattern.matcher(line).matches();
1010         }
1011 
1012         /** Parse existing date - time - zone formats.
1013          * If zone field is not missing, a proper Orekit date can be created and set into clock file object.
1014          * This feature depends on the date format.
1015          * @param dateString the whole date - time - zone string
1016          * @param pi holder for transient data
1017          */
1018         private static void parseDateTimeZone(final String dateString, final ParseInfo pi) {
1019 
1020             String date = "";
1021             String time = "";
1022             String zone = "";
1023             DateComponents dateComponents = null;
1024             TimeComponents timeComponents = null;
1025 
1026             if (DATE_PATTERN_1.matcher(dateString).matches()) {
1027 
1028                 date = dateString.substring(0, 10).trim();
1029                 time = dateString.substring(11, 16).trim();
1030                 zone = dateString.substring(16).trim();
1031 
1032             } else if (DATE_PATTERN_2.matcher(dateString).matches()) {
1033 
1034                 date = dateString.substring(0, 8).trim();
1035                 time = dateString.substring(9, 16).trim();
1036                 zone = dateString.substring(16).trim();
1037 
1038                 if (!zone.isEmpty()) {
1039                     // Get date and time components
1040                     dateComponents = new DateComponents(Integer.parseInt(date.substring(0, 4)),
1041                                                         Integer.parseInt(date.substring(4, 6)),
1042                                                         Integer.parseInt(date.substring(6, 8)));
1043                     timeComponents = new TimeComponents(Integer.parseInt(time.substring(0, 2)),
1044                                                         Integer.parseInt(time.substring(2, 4)),
1045                                                         Integer.parseInt(time.substring(4, 6)));
1046 
1047                 }
1048 
1049             } else if (DATE_PATTERN_3.matcher(dateString).matches()) {
1050 
1051                 date = dateString.substring(0, 11).trim();
1052                 time = dateString.substring(11, 17).trim();
1053                 zone = dateString.substring(17).trim();
1054 
1055             } else if (DATE_PATTERN_4.matcher(dateString).matches()) {
1056 
1057                 date = dateString.substring(0, 9).trim();
1058                 time = dateString.substring(9, 15).trim();
1059                 zone = dateString.substring(15).trim();
1060 
1061             } else if (DATE_PATTERN_5.matcher(dateString).matches()) {
1062 
1063                 date = dateString.substring(0, 11).trim();
1064                 time = dateString.substring(11, 20).trim();
1065 
1066             } else {
1067                 // Format is not handled or date is missing. Do nothing...
1068             }
1069 
1070             pi.file.setCreationDateString(date);
1071             pi.file.setCreationTimeString(time);
1072             pi.file.setCreationTimeZoneString(zone);
1073 
1074             if (dateComponents != null) {
1075                 pi.file.setCreationDate(new AbsoluteDate(dateComponents,
1076                                                          timeComponents,
1077                                                          TimeSystem.parseTimeSystem(zone).getTimeScale(pi.timeScales)));
1078             }
1079         }
1080     }
1081 
1082 }