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