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