1   /* Copyright 2002-2012 Space Applications Services
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.sp3;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.Reader;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Locale;
25  import java.util.Optional;
26  import java.util.Scanner;
27  import java.util.function.Function;
28  import java.util.regex.Pattern;
29  import java.util.stream.Stream;
30  
31  import org.hipparchus.exception.LocalizedCoreFormats;
32  import org.hipparchus.geometry.euclidean.threed.Vector3D;
33  import org.hipparchus.util.FastMath;
34  import org.orekit.annotation.DefaultDataContext;
35  import org.orekit.data.DataContext;
36  import org.orekit.data.DataSource;
37  import org.orekit.errors.OrekitException;
38  import org.orekit.errors.OrekitIllegalArgumentException;
39  import org.orekit.errors.OrekitMessages;
40  import org.orekit.files.general.EphemerisFileParser;
41  import org.orekit.files.sp3.SP3.SP3Coordinate;
42  import org.orekit.files.sp3.SP3.SP3FileType;
43  import org.orekit.frames.Frame;
44  import org.orekit.gnss.TimeSystem;
45  import org.orekit.time.AbsoluteDate;
46  import org.orekit.time.DateComponents;
47  import org.orekit.time.DateTimeComponents;
48  import org.orekit.time.TimeComponents;
49  import org.orekit.time.TimeScale;
50  import org.orekit.time.TimeScales;
51  import org.orekit.utils.CartesianDerivativesFilter;
52  import org.orekit.utils.Constants;
53  import org.orekit.utils.IERSConventions;
54  
55  /** A parser for the SP3 orbit file format. It supports all formats from sp3-a
56   * to sp3-d.
57   * <p>
58   * <b>Note:</b> this parser is thread-safe, so calling {@link #parse} from
59   * different threads is allowed.
60   * </p>
61   * @see <a href="ftp://igs.org/pub/data/format/sp3_docu.txt">SP3-a file format</a>
62   * @see <a href="ftp://igs.org/pub/data/format/sp3c.txt">SP3-c file format</a>
63   * @see <a href="ftp://igs.org/pub/data/format/sp3d.pdf">SP3-d file format</a>
64   * @author Thomas Neidhart
65   * @author Luc Maisonobe
66   */
67  public class SP3Parser implements EphemerisFileParser<SP3> {
68  
69      /** Bad or absent clock values are to be set to 999999.999999. */
70      public static final double DEFAULT_CLOCK_VALUE = 999999.999999;
71  
72      /** Spaces delimiters. */
73      private static final String SPACES = "\\s+";
74  
75      /** One millimeter, in meters. */
76      private static final double MILLIMETER = 1.0e-3;
77  
78      /** Standard gravitational parameter in m^3 / s^2. */
79      private final double mu;
80      /** Number of data points to use in interpolation. */
81      private final int interpolationSamples;
82      /** Mapping from frame identifier in the file to a {@link Frame}. */
83      private final Function<? super String, ? extends Frame> frameBuilder;
84      /** Set of time scales. */
85      private final TimeScales timeScales;
86  
87      /**
88       * Create an SP3 parser using default values.
89       *
90       * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
91       *
92       * @see #SP3Parser(double, int, Function)
93       */
94      @DefaultDataContext
95      public SP3Parser() {
96          this(Constants.EIGEN5C_EARTH_MU, 7, SP3Parser::guessFrame);
97      }
98  
99      /**
100      * Create an SP3 parser and specify the extra information needed to create a {@link
101      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
102      *
103      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
104      *
105      * @param mu                   is the standard gravitational parameter to use for
106      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
107      *                             the ephemeris data. See {@link Constants}.
108      * @param interpolationSamples is the number of samples to use when interpolating.
109      * @param frameBuilder         is a function that can construct a frame from an SP3
110      *                             coordinate system string. The coordinate system can be
111      *                             any 5 character string e.g. ITR92, IGb08.
112      * @see #SP3Parser(double, int, Function, TimeScales)
113      */
114     @DefaultDataContext
115     public SP3Parser(final double mu,
116                      final int interpolationSamples,
117                      final Function<? super String, ? extends Frame> frameBuilder) {
118         this(mu, interpolationSamples, frameBuilder,
119                 DataContext.getDefault().getTimeScales());
120     }
121 
122     /**
123      * Create an SP3 parser and specify the extra information needed to create a {@link
124      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
125      *
126      * @param mu                   is the standard gravitational parameter to use for
127      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
128      *                             the ephemeris data. See {@link Constants}.
129      * @param interpolationSamples is the number of samples to use when interpolating.
130      * @param frameBuilder         is a function that can construct a frame from an SP3
131      *                             coordinate system string. The coordinate system can be
132      * @param timeScales           the set of time scales used for parsing dates.
133      * @since 10.1
134      */
135     public SP3Parser(final double mu,
136                      final int interpolationSamples,
137                      final Function<? super String, ? extends Frame> frameBuilder,
138                      final TimeScales timeScales) {
139         this.mu = mu;
140         this.interpolationSamples = interpolationSamples;
141         this.frameBuilder = frameBuilder;
142         this.timeScales = timeScales;
143     }
144 
145     /**
146      * Default string to {@link Frame} conversion for {@link #SP3Parser()}.
147      *
148      * <p>This method uses the {@link DataContext#getDefault() default data context}.
149      *
150      * @param name of the frame.
151      * @return ITRF based on 2010 conventions,
152      * with tidal effects considered during EOP interpolation.
153      */
154     @DefaultDataContext
155     private static Frame guessFrame(final String name) {
156         return DataContext.getDefault().getFrames()
157                 .getITRF(IERSConventions.IERS_2010, false);
158     }
159 
160     @Override
161     public SP3 parse(final DataSource source) {
162 
163         try (Reader reader = source.getOpener().openReaderOnce();
164              BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {
165 
166             if (br == null) {
167                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
168             }
169 
170             // initialize internal data structures
171             final ParseInfo pi = new ParseInfo();
172 
173             int lineNumber = 0;
174             Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
175             for (String line = br.readLine(); line != null; line = br.readLine()) {
176                 ++lineNumber;
177                 final String l = line;
178                 final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
179                 if (selected.isPresent()) {
180                     try {
181                         selected.get().parse(line, pi);
182                     } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
183                         throw new OrekitException(e,
184                                                   OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
185                                                   lineNumber, source.getName(), line);
186                     }
187                     candidateParsers = selected.get().allowedNext();
188                 } else {
189                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
190                                               lineNumber, source.getName(), line);
191                 }
192                 if (pi.done) {
193                     if (pi.nbEpochs != pi.file.getNumberOfEpochs()) {
194                         throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
195                                                   pi.nbEpochs, source.getName(), pi.file.getNumberOfEpochs());
196                     }
197                     return pi.file;
198                 }
199             }
200 
201             // we never reached the EOF marker
202             throw new OrekitException(OrekitMessages.SP3_UNEXPECTED_END_OF_FILE, lineNumber);
203 
204         } catch (IOException ioe) {
205             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
206         }
207 
208     }
209 
210     /** Returns the {@link SP3FileType} that corresponds to a given string in a SP3 file.
211      * @param fileType file type as string
212      * @return file type as enum
213      */
214     private static SP3FileType getFileType(final String fileType) {
215         SP3FileType type = SP3FileType.UNDEFINED;
216         if ("G".equalsIgnoreCase(fileType)) {
217             type = SP3FileType.GPS;
218         } else if ("M".equalsIgnoreCase(fileType)) {
219             type = SP3FileType.MIXED;
220         } else if ("R".equalsIgnoreCase(fileType)) {
221             type = SP3FileType.GLONASS;
222         } else if ("L".equalsIgnoreCase(fileType)) {
223             type = SP3FileType.LEO;
224         } else if ("S".equalsIgnoreCase(fileType)) {
225             type = SP3FileType.SBAS;
226         } else if ("I".equalsIgnoreCase(fileType)) {
227             type = SP3FileType.IRNSS;
228         } else if ("E".equalsIgnoreCase(fileType)) {
229             type = SP3FileType.GALILEO;
230         } else if ("C".equalsIgnoreCase(fileType)) {
231             type = SP3FileType.COMPASS;
232         } else if ("J".equalsIgnoreCase(fileType)) {
233             type = SP3FileType.QZSS;
234         }
235         return type;
236     }
237 
238     /** Transient data used for parsing a sp3 file. The data is kept in a
239      * separate data structure to make the parser thread-safe.
240      * <p><b>Note</b>: The class intentionally does not provide accessor
241      * methods, as it is only used internally for parsing a SP3 file.</p>
242      */
243     private class ParseInfo {
244 
245         /** Set of time scales for parsing dates. */
246         private final TimeScales timeScales;
247 
248         /** The corresponding SP3File object. */
249         private SP3 file;
250 
251         /** The latest epoch as read from the SP3 file. */
252         private AbsoluteDate latestEpoch;
253 
254         /** The latest position as read from the SP3 file. */
255         private Vector3D latestPosition;
256 
257         /** The latest clock value as read from the SP3 file. */
258         private double latestClock;
259 
260         /** Indicates if the SP3 file has velocity entries. */
261         private boolean hasVelocityEntries;
262 
263         /** The timescale used in the SP3 file. */
264         private TimeScale timeScale;
265 
266         /** Date and time of the file. */
267         private DateTimeComponents epoch;
268 
269         /** The number of satellites as contained in the SP3 file. */
270         private int maxSatellites;
271 
272         /** The number of satellites accuracies already seen. */
273         private int nbAccuracies;
274 
275         /** The number of epochs already seen. */
276         private int nbEpochs;
277 
278         /** End Of File reached indicator. */
279         private boolean done;
280 
281         /** The base for pos/vel. */
282         //private double posVelBase;
283 
284         /** The base for clock/rate. */
285         //private double clockBase;
286 
287         /** Create a new {@link ParseInfo} object. */
288         protected ParseInfo() {
289             this.timeScales = SP3Parser.this.timeScales;
290             file               = new SP3(mu, interpolationSamples, frameBuilder);
291             latestEpoch        = null;
292             latestPosition     = null;
293             latestClock        = 0.0;
294             hasVelocityEntries = false;
295             epoch              = DateTimeComponents.JULIAN_EPOCH;
296             timeScale          = timeScales.getGPS();
297             maxSatellites      = 0;
298             nbAccuracies       = 0;
299             nbEpochs           = 0;
300             done               = false;
301             //posVelBase = 2d;
302             //clockBase = 2d;
303         }
304     }
305 
306     /** Parsers for specific lines. */
307     private enum LineParser {
308 
309         /** Parser for version, epoch, data used and agency information. */
310         HEADER_VERSION("^#[a-z].*") {
311 
312             /** {@inheritDoc} */
313             @Override
314             public void parse(final String line, final ParseInfo pi) {
315                 try (Scanner s1      = new Scanner(line);
316                      Scanner s2      = s1.useDelimiter(SPACES);
317                      Scanner scanner = s2.useLocale(Locale.US)) {
318                     scanner.skip("#");
319                     final String v = scanner.next();
320 
321                     final char version = v.substring(0, 1).toLowerCase().charAt(0);
322                     if (version != 'a' && version != 'b' && version != 'c' && version != 'd') {
323                         throw new OrekitException(OrekitMessages.SP3_UNSUPPORTED_VERSION, version);
324                     }
325 
326                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
327                     pi.file.setFilter(pi.hasVelocityEntries ?
328                                       CartesianDerivativesFilter.USE_PV :
329                                       CartesianDerivativesFilter.USE_P);
330 
331                     final int    year   = Integer.parseInt(v.substring(2));
332                     final int    month  = scanner.nextInt();
333                     final int    day    = scanner.nextInt();
334                     final int    hour   = scanner.nextInt();
335                     final int    minute = scanner.nextInt();
336                     final double second = scanner.nextDouble();
337 
338                     pi.epoch = new DateTimeComponents(year, month, day,
339                                                       hour, minute, second);
340 
341                     final int numEpochs = scanner.nextInt();
342                     pi.file.setNumberOfEpochs(numEpochs);
343 
344                     // data used indicator
345                     pi.file.setDataUsed(scanner.next());
346 
347                     pi.file.setCoordinateSystem(scanner.next());
348                     pi.file.setOrbitTypeKey(scanner.next());
349                     pi.file.setAgency(scanner.next());
350                 }
351             }
352 
353             /** {@inheritDoc} */
354             @Override
355             public Stream<LineParser> allowedNext() {
356                 return Stream.of(HEADER_DATE_TIME_REFERENCE);
357             }
358 
359         },
360 
361         /** Parser for additional date/time references in gps/julian day notation. */
362         HEADER_DATE_TIME_REFERENCE("^##.*") {
363 
364             /** {@inheritDoc} */
365             @Override
366             public void parse(final String line, final ParseInfo pi) {
367                 try (Scanner s1      = new Scanner(line);
368                      Scanner s2      = s1.useDelimiter(SPACES);
369                      Scanner scanner = s2.useLocale(Locale.US)) {
370                     scanner.skip("##");
371 
372                     // gps week
373                     pi.file.setGpsWeek(scanner.nextInt());
374                     // seconds of week
375                     pi.file.setSecondsOfWeek(scanner.nextDouble());
376                     // epoch interval
377                     pi.file.setEpochInterval(scanner.nextDouble());
378                     // julian day
379                     pi.file.setJulianDay(scanner.nextInt());
380                     // day fraction
381                     pi.file.setDayFraction(scanner.nextDouble());
382                 }
383             }
384 
385             /** {@inheritDoc} */
386             @Override
387             public Stream<LineParser> allowedNext() {
388                 return Stream.of(HEADER_SAT_IDS);
389             }
390 
391         },
392 
393         /** Parser for satellites identifiers. */
394         HEADER_SAT_IDS("^\\+ .*") {
395 
396             /** {@inheritDoc} */
397             @Override
398             public void parse(final String line, final ParseInfo pi) {
399 
400                 if (pi.maxSatellites == 0) {
401                     // this is the first ids line, it also contains the number of satellites
402                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
403                 }
404 
405                 final int lineLength = line.length();
406                 int count = pi.file.getSatelliteCount();
407                 int startIdx = 9;
408                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
409                     final String satId = line.substring(startIdx, startIdx + 3).trim();
410                     if (satId.length() > 0) {
411                         pi.file.addSatellite(satId);
412                     }
413                     startIdx += 3;
414                 }
415             }
416 
417             /** {@inheritDoc} */
418             @Override
419             public Stream<LineParser> allowedNext() {
420                 return Stream.of(HEADER_SAT_IDS, HEADER_ACCURACY);
421             }
422 
423         },
424 
425         /** Parser for general accuracy information for each satellite. */
426         HEADER_ACCURACY("^\\+\\+.*") {
427 
428             /** {@inheritDoc} */
429             @Override
430             public void parse(final String line, final ParseInfo pi) {
431                 final int lineLength = line.length();
432                 int startIdx = 9;
433                 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
434                     final String sub = line.substring(startIdx, startIdx + 3).trim();
435                     if (sub.length() > 0) {
436                         final int exponent = Integer.parseInt(sub);
437                         // the accuracy is calculated as 2**exp (in mm)
438                         pi.file.setAccuracy(pi.nbAccuracies++, (2 << exponent) * MILLIMETER);
439                     }
440                     startIdx += 3;
441                 }
442             }
443 
444             /** {@inheritDoc} */
445             @Override
446             public Stream<LineParser> allowedNext() {
447                 return Stream.of(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
448             }
449 
450         },
451 
452         /** Parser for time system. */
453         HEADER_TIME_SYSTEM("^%c.*") {
454 
455             /** {@inheritDoc} */
456             @Override
457             public void parse(final String line, final ParseInfo pi) {
458 
459                 if (pi.file.getType() == null) {
460                     // this the first custom fields line, the only one really used
461                     pi.file.setType(getFileType(line.substring(3, 5).trim()));
462 
463                     // now identify the time system in use
464                     final String tsStr = line.substring(9, 12).trim();
465                     final TimeSystem ts;
466                     if (tsStr.equalsIgnoreCase("ccc")) {
467                         ts = TimeSystem.GPS;
468                     } else {
469                         ts = TimeSystem.valueOf(tsStr);
470                     }
471                     pi.file.setTimeSystem(ts);
472                     pi.timeScale = ts.getTimeScale(pi.timeScales);
473 
474                     // now we know the time scale used, we can set the file epoch
475                     pi.file.setEpoch(new AbsoluteDate(pi.epoch, pi.timeScale));
476                 }
477 
478             }
479 
480             /** {@inheritDoc} */
481             @Override
482             public Stream<LineParser> allowedNext() {
483                 return Stream.of(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
484             }
485 
486         },
487 
488         /** Parser for standard deviations of position/velocity/clock components. */
489         HEADER_STANDARD_DEVIATIONS("^%f.*") {
490 
491             /** {@inheritDoc} */
492             @Override
493             public void parse(final String line, final ParseInfo pi) {
494                 // String base = line.substring(3, 13).trim();
495                 // if (!base.equals("0.0000000")) {
496                 //    // (mm or 10**-4 mm/sec)
497                 //    pi.posVelBase = Double.valueOf(base);
498                 // }
499 
500                 // base = line.substring(14, 26).trim();
501                 // if (!base.equals("0.000000000")) {
502                 //    // (psec or 10**-4 psec/sec)
503                 //    pi.clockBase = Double.valueOf(base);
504                 // }
505             }
506 
507             /** {@inheritDoc} */
508             @Override
509             public Stream<LineParser> allowedNext() {
510                 return Stream.of(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
511             }
512 
513         },
514 
515         /** Parser for custom parameters. */
516         HEADER_CUSTOM_PARAMETERS("^%i.*") {
517 
518             /** {@inheritDoc} */
519             @Override
520             public void parse(final String line, final ParseInfo pi) {
521                 // ignore additional custom parameters
522             }
523 
524             /** {@inheritDoc} */
525             @Override
526             public Stream<LineParser> allowedNext() {
527                 return Stream.of(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
528             }
529 
530         },
531 
532         /** Parser for comments. */
533         HEADER_COMMENTS("^[%]?/\\*.*|") {
534 
535             /** {@inheritDoc} */
536             @Override
537             public void parse(final String line, final ParseInfo pi) {
538                 // ignore comments
539             }
540 
541             /** {@inheritDoc} */
542             @Override
543             public Stream<LineParser> allowedNext() {
544                 return Stream.of(HEADER_COMMENTS, DATA_EPOCH);
545             }
546 
547         },
548 
549         /** Parser for epoch. */
550         DATA_EPOCH("^\\* .*") {
551 
552             /** {@inheritDoc} */
553             @Override
554             public void parse(final String line, final ParseInfo pi) {
555                 final int    year   = Integer.parseInt(line.substring(3, 7).trim());
556                 final int    month  = Integer.parseInt(line.substring(8, 10).trim());
557                 final int    day    = Integer.parseInt(line.substring(11, 13).trim());
558                 final int    hour   = Integer.parseInt(line.substring(14, 16).trim());
559                 final int    minute = Integer.parseInt(line.substring(17, 19).trim());
560                 final double second = Double.parseDouble(line.substring(20).trim());
561 
562                 // some SP3 files have weird epochs as in the following two examples, where
563                 // the middle dates are wrong
564                 //
565                 // *  2016  7  6 16 58  0.00000000
566                 // PL51  11872.234459   3316.551981    101.400098 999999.999999
567                 // VL51   8054.606014 -27076.640110 -53372.762255 999999.999999
568                 // *  2016  7  6 16 60  0.00000000
569                 // PL51  11948.228978   2986.113872   -538.901114 999999.999999
570                 // VL51   4605.419303 -27972.588048 -53316.820671 999999.999999
571                 // *  2016  7  6 17  2  0.00000000
572                 // PL51  11982.652569   2645.786926  -1177.549463 999999.999999
573                 // VL51   1128.248622 -28724.293303 -53097.358387 999999.999999
574                 //
575                 // *  2016  7  6 23 58  0.00000000
576                 // PL51   3215.382310  -7958.586164   8812.395707
577                 // VL51 -18058.659942 -45834.335707 -34496.540437
578                 // *  2016  7  7 24  0  0.00000000
579                 // PL51   2989.229334  -8494.421415   8385.068555
580                 // VL51 -19617.027447 -43444.824985 -36706.159070
581                 // *  2016  7  7  0  2  0.00000000
582                 // PL51   2744.983592  -9000.639164   7931.904779
583                 // VL51 -21072.925764 -40899.633288 -38801.567078
584                 //
585                 // In the first case, the date should really be 2016  7  6 17  0  0.00000000,
586                 // i.e as the minutes field overflows, the hours field should be incremented
587                 // In the second case, the date should really be 2016  7  7  0  0  0.00000000,
588                 // i.e. as the hours field overflows, the day field should be kept as is
589                 // we cannot be sure how carry was managed when these bogus files were written
590                 // so we try different options, incrementing or not previous field, and selecting
591                 // the closest one to expected date
592                 DateComponents dc = new DateComponents(year, month, day);
593                 final List<AbsoluteDate> candidates = new ArrayList<>();
594                 int h = hour;
595                 int m = minute;
596                 double s = second;
597                 if (s >= 60.0) {
598                     s -= 60;
599                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
600                     m++;
601                 }
602                 if (m > 59) {
603                     m = 0;
604                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
605                     h++;
606                 }
607                 if (h > 23) {
608                     h = 0;
609                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
610                     dc = new DateComponents(dc, 1);
611                 }
612                 addCandidate(candidates, dc, h, m, s, pi.timeScale);
613                 final AbsoluteDate expected = pi.latestEpoch == null ?
614                                               pi.file.getEpoch() :
615                                                   pi.latestEpoch.shiftedBy(pi.file.getEpochInterval());
616                 pi.latestEpoch = null;
617                 for (final AbsoluteDate candidate : candidates) {
618                     if (FastMath.abs(candidate.durationFrom(expected)) < 0.01 * pi.file.getEpochInterval()) {
619                         pi.latestEpoch = candidate;
620                     }
621                 }
622                 if (pi.latestEpoch == null) {
623                     // no date recognized, just parse again the initial fields
624                     // in order to generate again an exception
625                     pi.latestEpoch = new AbsoluteDate(year, month, day, hour, minute, second, pi.timeScale);
626                 }
627                 pi.nbEpochs++;
628             }
629 
630             /** Add an epoch candidate to a list.
631              * @param candidates list of candidates
632              * @param dc date components
633              * @param hour hour number from 0 to 23
634              * @param minute minute number from 0 to 59
635              * @param second second number from 0.0 to 60.0 (excluded)
636              * @param timeScale time scale
637              * @since 11.1.1
638              */
639             private void addCandidate(final List<AbsoluteDate> candidates, final DateComponents dc,
640                                       final int hour, final int minute, final double second,
641                                       final TimeScale timeScale) {
642                 try {
643                     candidates.add(new AbsoluteDate(dc, new TimeComponents(hour, minute, second), timeScale));
644                 } catch (OrekitIllegalArgumentException oiae) {
645                     // ignored
646                 }
647             }
648 
649             /** {@inheritDoc} */
650             @Override
651             public Stream<LineParser> allowedNext() {
652                 return Stream.of(DATA_POSITION);
653             }
654 
655         },
656 
657         /** Parser for position. */
658         DATA_POSITION("^P.*") {
659 
660             /** {@inheritDoc} */
661             @Override
662             public void parse(final String line, final ParseInfo pi) {
663                 final String satelliteId = line.substring(1, 4).trim();
664 
665                 if (!pi.file.containsSatellite(satelliteId)) {
666                     pi.latestPosition = null;
667                 } else {
668                     final double x = Double.parseDouble(line.substring(4, 18).trim());
669                     final double y = Double.parseDouble(line.substring(18, 32).trim());
670                     final double z = Double.parseDouble(line.substring(32, 46).trim());
671 
672                     // the position values are in km and have to be converted to m
673                     pi.latestPosition = new Vector3D(x * 1000, y * 1000, z * 1000);
674 
675                     // clock (microsec)
676                     pi.latestClock = line.trim().length() <= 46 ?
677                                                           DEFAULT_CLOCK_VALUE :
678                                                               Double.parseDouble(line.substring(46, 60).trim()) * 1e-6;
679 
680                     // the additional items are optional and not read yet
681 
682                     // if (line.length() >= 73) {
683                     // // x-sdev (b**n mm)
684                     // int xStdDevExp = Integer.valueOf(line.substring(61,
685                     // 63).trim());
686                     // // y-sdev (b**n mm)
687                     // int yStdDevExp = Integer.valueOf(line.substring(64,
688                     // 66).trim());
689                     // // z-sdev (b**n mm)
690                     // int zStdDevExp = Integer.valueOf(line.substring(67,
691                     // 69).trim());
692                     // // c-sdev (b**n psec)
693                     // int cStdDevExp = Integer.valueOf(line.substring(70,
694                     // 73).trim());
695                     //
696                     // pi.posStdDevRecord =
697                     // new PositionStdDevRecord(FastMath.pow(pi.posVelBase, xStdDevExp),
698                     // FastMath.pow(pi.posVelBase,
699                     // yStdDevExp), FastMath.pow(pi.posVelBase, zStdDevExp),
700                     // FastMath.pow(pi.clockBase, cStdDevExp));
701                     //
702                     // String clockEventFlag = line.substring(74, 75);
703                     // String clockPredFlag = line.substring(75, 76);
704                     // String maneuverFlag = line.substring(78, 79);
705                     // String orbitPredFlag = line.substring(79, 80);
706                     // }
707 
708                     if (!pi.hasVelocityEntries) {
709                         final SP3Coordinate coord =
710                                 new SP3Coordinate(pi.latestEpoch,
711                                                   pi.latestPosition,
712                                                   pi.latestClock);
713                         pi.file.addSatelliteCoordinate(satelliteId, coord);
714                     }
715                 }
716             }
717 
718             /** {@inheritDoc} */
719             @Override
720             public Stream<LineParser> allowedNext() {
721                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
722             }
723 
724         },
725 
726         /** Parser for position correlation. */
727         DATA_POSITION_CORRELATION("^EP.*") {
728 
729             /** {@inheritDoc} */
730             @Override
731             public void parse(final String line, final ParseInfo pi) {
732                 // ignored for now
733             }
734 
735             /** {@inheritDoc} */
736             @Override
737             public Stream<LineParser> allowedNext() {
738                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
739             }
740 
741         },
742 
743         /** Parser for velocity. */
744         DATA_VELOCITY("^V.*") {
745 
746             /** {@inheritDoc} */
747             @Override
748             public void parse(final String line, final ParseInfo pi) {
749                 final String satelliteId = line.substring(1, 4).trim();
750 
751                 if (pi.file.containsSatellite(satelliteId)) {
752                     final double xv = Double.parseDouble(line.substring(4, 18).trim());
753                     final double yv = Double.parseDouble(line.substring(18, 32).trim());
754                     final double zv = Double.parseDouble(line.substring(32, 46).trim());
755 
756                     // the velocity values are in dm/s and have to be converted to m/s
757                     final Vector3D velocity = new Vector3D(xv / 10d, yv / 10d, zv / 10d);
758 
759                     // clock rate in file is 1e-4 us / s
760                     final double clockRateChange = line.trim().length() <= 46 ?
761                                                                         DEFAULT_CLOCK_VALUE :
762                                                                             Double.parseDouble(line.substring(46, 60).trim()) * 1e-4;
763 
764                     // the additional items are optional and not read yet
765 
766                     // if (line.length() >= 73) {
767                     // // xvel-sdev (b**n 10**-4 mm/sec)
768                     // int xVstdDevExp = Integer.valueOf(line.substring(61,
769                     // 63).trim());
770                     // // yvel-sdev (b**n 10**-4 mm/sec)
771                     // int yVstdDevExp = Integer.valueOf(line.substring(64,
772                     // 66).trim());
773                     // // zvel-sdev (b**n 10**-4 mm/sec)
774                     // int zVstdDevExp = Integer.valueOf(line.substring(67,
775                     // 69).trim());
776                     // // clkrate-sdev (b**n 10**-4 psec/sec)
777                     // int clkStdDevExp = Integer.valueOf(line.substring(70,
778                     // 73).trim());
779                     // }
780 
781                     final SP3Coordinate coord =
782                             new SP3Coordinate(pi.latestEpoch,
783                                               pi.latestPosition,
784                                               velocity,
785                                               pi.latestClock,
786                                               clockRateChange);
787                     pi.file.addSatelliteCoordinate(satelliteId, coord);
788                 }
789             }
790 
791             /** {@inheritDoc} */
792             @Override
793             public Stream<LineParser> allowedNext() {
794                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
795             }
796 
797         },
798 
799         /** Parser for velocity correlation. */
800         DATA_VELOCITY_CORRELATION("^EV.*") {
801 
802             /** {@inheritDoc} */
803             @Override
804             public void parse(final String line, final ParseInfo pi) {
805                 // ignored for now
806             }
807 
808             /** {@inheritDoc} */
809             @Override
810             public Stream<LineParser> allowedNext() {
811                 return Stream.of(DATA_EPOCH, DATA_POSITION, EOF);
812             }
813 
814         },
815 
816         /** Parser for End Of File marker. */
817         EOF("^[eE][oO][fF]\\s*$") {
818 
819             /** {@inheritDoc} */
820             @Override
821             public void parse(final String line, final ParseInfo pi) {
822                 pi.done = true;
823             }
824 
825             /** {@inheritDoc} */
826             @Override
827             public Stream<LineParser> allowedNext() {
828                 return Stream.of(EOF);
829             }
830 
831         };
832 
833         /** Pattern for identifying line. */
834         private final Pattern pattern;
835 
836         /** Simple constructor.
837          * @param lineRegexp regular expression for identifying line
838          */
839         LineParser(final String lineRegexp) {
840             pattern = Pattern.compile(lineRegexp);
841         }
842 
843         /** Parse a line.
844          * @param line line to parse
845          * @param pi holder for transient data
846          */
847         public abstract void parse(String line, ParseInfo pi);
848 
849         /** Get the allowed parsers for next line.
850          * @return allowed parsers for next line
851          */
852         public abstract Stream<LineParser> allowedNext();
853 
854         /** Check if parser can handle line.
855          * @param line line to parse
856          * @return true if parser can handle the specified line
857          */
858         public boolean canHandle(final String line) {
859             return pattern.matcher(line).matches();
860         }
861 
862     }
863 
864 }