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