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