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