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