1   /* Copyright 2002-2023 Andrew Goetz
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.stk;
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.EnumMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.SortedSet;
30  import java.util.TreeSet;
31  import java.util.regex.Pattern;
32  import java.util.stream.Stream;
33  
34  import org.hipparchus.exception.LocalizedCoreFormats;
35  import org.hipparchus.geometry.euclidean.threed.Vector3D;
36  import org.orekit.data.DataSource;
37  import org.orekit.errors.OrekitException;
38  import org.orekit.errors.OrekitMessages;
39  import org.orekit.files.general.EphemerisFileParser;
40  import org.orekit.files.stk.STKEphemerisFile.STKCoordinateSystem;
41  import org.orekit.files.stk.STKEphemerisFile.STKEphemeris;
42  import org.orekit.files.stk.STKEphemerisFile.STKEphemerisSegment;
43  import org.orekit.frames.Frame;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.time.DateTimeComponents;
46  import org.orekit.time.Month;
47  import org.orekit.time.UTCScale;
48  import org.orekit.utils.CartesianDerivativesFilter;
49  import org.orekit.utils.PVCoordinates;
50  import org.orekit.utils.TimeStampedPVCoordinates;
51  
52  /**
53   * Parser of {@link STKEphemerisFile}s.
54   *
55   * <p> The STK ephemeris file format specification is quite extensive and this implementation does not
56   * attempt (nor is it possible, given the lack of an STK scenario to provide context) to support all
57   * possible variations of the format. The following keywords are recognized (case-insensitive):
58   * <table>
59   *     <caption>Recognized Keywords</caption>
60   *     <thead>
61   *         <tr>
62   *             <th>Keyword</th>
63   *             <th>Supported</th>
64   *             <th>Comment</th>
65   *         </tr>
66   *     </thead>
67   *     <tbody>
68   *         <tr>
69   *             <td>stk.v.*.*</td>
70   *             <td>Yes</td>
71   *             <td>STK version number</td>
72   *         </tr>
73   *         <tr>
74   *             <td>BEGIN/END Ephemeris</td>
75   *             <td>Yes</td>
76   *             <td></td>
77   *         </tr>
78   *         <tr>
79   *             <td>ScenarioEpoch</td>
80   *             <td>Yes</td>
81   *             <td>Gregorian UTC time format (<code>dd mmm yyyy hh:mm:ss.s</code>) assumed;
82   *                 the <code>TimeFormat</code> keyword is not recognized.</td>
83   *         </tr>
84   *         <tr>
85   *             <td>CentralBody</td>
86   *             <td>No</td>
87   *             <td>Class constructors require gravitational parameter.</td>
88   *         </tr>
89   *         <tr>
90   *             <td>CoordinateSystem</td>
91   *             <td>Yes</td>
92   *             <td>Implementation uses a frame mapping to map {@link STKCoordinateSystem}s to {@link Frame}s.</td>
93   *         </tr>
94   *         <tr>
95   *             <td>DistanceUnit</td>
96   *             <td>Yes</td>
97   *             <td>Only <code>Meters</code> and <code>Kilometers</code> are supported.</td>
98   *         </tr>
99   *         <tr>
100  *             <td>InterpolationMethod</td>
101  *             <td>No</td>
102  *             <td>The Orekit EphemerisSegmentPropagator class uses
103  *             {@link org.orekit.utils.TimeStampedPVCoordinatesHermiteInterpolator#interpolate(AbsoluteDate, Stream)}
104  *             to do Hermite interpolation, so the value of <code>InterpolationMethod</code>, if present, is
105  *             ignored.</td>
106  *         </tr>
107  *         <tr>
108  *             <td>InterpolationSamplesM1</td>
109  *             <td>Yes</td>
110  *             <td>Note that the <code>InterpolationMethod</code> keyword is ignored, but the value of
111  *             <code>InterpolationSamplesM1</code> will be used to determine the number of sample points in the
112  *             Hermite interpolator used by Orekit.</td>
113  *         </tr>
114  *         <tr>
115  *             <td>NumberOfEphemerisPoints</td>
116  *             <td>Yes</td>
117  *             <td></td>
118  *         </tr>
119  *         <tr>
120  *             <td>BEGIN/END SegmentBoundaryTimes</td>
121  *             <td>Yes</td>
122  *             <td></td>
123  *         </tr>
124  *     </tbody>
125  * </table>
126  *
127  * <p> Any keyword in the format specification which is not explicitly named in the above table is not recognized and
128  * will cause a parse exception. Those keywords that are listed above as recognized but not supported are simply
129  * ignored.
130  *
131  * <p> The following ephemeris formats are recognized and supported:
132  * <ul>
133  *     <li>EphemerisTimePos</li>
134  *     <li>EphemerisTimePosVel</li>
135  *     <li>EphemerisTimePosVelAcc</li>
136  * </ul>
137  * Any ephemeris format in the format specification which is not explicitly named in the above list is not recognized
138  * and will cause an exception.
139  *
140  * @author Andrew Goetz
141  * @since 12.0
142  */
143 public class STKEphemerisFileParser implements EphemerisFileParser<STKEphemerisFile> {
144 
145     /** Pattern for delimiting regular expressions. */
146     private static final Pattern SEPARATOR = Pattern.compile("\\s+");
147 
148     /** Pattern for ignorable lines. Comments are preceded by '#'. */
149     private static final Pattern IGNORABLE_LINE = Pattern.compile("^\\s*(#.*)?");
150 
151     /** Regular expression that matches anything. */
152     private static final String MATCH_ANY_REGEX = ".*";
153 
154     /** Recognized keywords. */
155     private static final List<LineParser> KEYWORDS = Arrays.asList(
156             LineParser.NUMBER_OF_EPHEMERIS_POINTS,
157             LineParser.SCENARIO_EPOCH,
158             LineParser.INTERPOLATION_METHOD,
159             LineParser.INTERPOLATION_SAMPLESM1,
160             LineParser.CENTRAL_BODY,
161             LineParser.COORDINATE_SYSTEM,
162             LineParser.BEGIN_SEGMENT_BOUNDARY_TIMES,
163             LineParser.EPHEMERIS_TIME_POS,
164             LineParser.EPHEMERIS_TIME_POS_VEL,
165             LineParser.EPHEMERIS_TIME_POS_VEL_ACC
166     );
167 
168     /** Satellite id. */
169     private final String satelliteId;
170 
171     /** Gravitational parameter (m^3/s^2). */
172     private final double mu;
173 
174     /** UTC time scale. */
175     private final UTCScale utc;
176 
177     /** Mapping of STK coordinate system to Orekit reference frame. */
178     private final Map<STKCoordinateSystem, Frame> frameMapping;
179 
180     /**
181      * Constructs a {@link STKEphemerisFileParser} instance.
182      * @param satelliteId satellite id for satellites parsed by the parser
183      * @param mu gravitational parameter (m^3/s^2)
184      * @param utc UTC scale for parsed dates
185      * @param frameMapping mapping from STK coordinate system to Orekit frame
186      */
187     public STKEphemerisFileParser(final String satelliteId, final double mu, final UTCScale utc,
188             final Map<STKCoordinateSystem, Frame> frameMapping) {
189         this.satelliteId = Objects.requireNonNull(satelliteId);
190         this.mu = mu;
191         this.utc = Objects.requireNonNull(utc);
192         this.frameMapping = Collections.unmodifiableMap(new EnumMap<>(frameMapping));
193     }
194 
195     @Override
196     public STKEphemerisFile parse(final DataSource source) {
197 
198         try (Reader reader = source.getOpener().openReaderOnce();
199              BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {
200 
201             if (br == null) {
202                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
203             }
204 
205             // initialize internal data structures
206             final ParseInfo pi = new ParseInfo();
207 
208             int lineNumber = 0;
209             Iterable<LineParser> parsers = Collections.singleton(LineParser.VERSION);
210             nextLine:
211                 for (String line = br.readLine(); line != null; line = br.readLine()) {
212                 ++lineNumber;
213                 if (pi.file != null) {
214                     break;
215                 } else if (IGNORABLE_LINE.matcher(line).matches()) {
216                     continue;
217                 }
218                 for (final LineParser candidate : parsers) {
219                     if (candidate.canHandle(line)) {
220                         try {
221                             candidate.parse(line, pi);
222                             parsers = candidate.allowedNext();
223                             continue nextLine;
224                         } catch (StringIndexOutOfBoundsException | IllegalArgumentException e) {
225                             throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber,
226                                                       source.getName(), line);
227                         }
228                     }
229                 }
230 
231                 // no parsers found for this line
232                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, source.getName(),
233                                           line);
234 
235                 }
236 
237             if (pi.file != null) {
238                 return pi.file;
239             } else {
240                 throw new OrekitException(OrekitMessages.STK_UNEXPECTED_END_OF_FILE, lineNumber);
241             }
242 
243         } catch (IOException ioe) {
244             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
245         }
246     }
247 
248     /**
249      * Transient data used for parsing an STK ephemeris file. The data is kept in a
250      * separate data structure to make the parser thread-safe.
251      * <p>
252      * <b>Note</b>: The class intentionally does not provide accessor methods, as it
253      * is only used internally for parsing an STK ephemeris file.
254      * </p>
255      */
256     private final class ParseInfo {
257 
258         /** STK version. */
259         private String stkVersion;
260 
261         /** Scenario epoch. */
262         private AbsoluteDate scenarioEpoch; // technically optional but required here b/c no STK scenario for context
263 
264         /** Number of ephemeris points. */
265         private Integer numberOfEphemerisPoints;
266 
267         /** One less than the number of points used in the interpolation. */
268         private int interpolationSamplesM1;
269 
270         /** Cartesian derivatives filter for interpolation. */
271         private CartesianDerivativesFilter cartesianDerivativesFilter;
272 
273         /** Coordinate system. */
274         private STKCoordinateSystem coordinateSystem;
275 
276         /** Distance unit. */
277         private STKDistanceUnit distanceUnit;
278 
279         /** Number of ephemeris points read. */
280         private int numberOfEphemerisPointsRead;
281 
282         /** Segment boundary times. */
283         private SortedSet<Double> segmentBoundaryTimes;
284 
285         /** Ephemeris segments. */
286         private List<STKEphemerisSegment> ephemerisSegments;
287 
288         /** Last-saved ephemeris. */
289         private TimeStampedPVCoordinates lastSavedEphemeris;
290 
291         /** Ephemeris for current segment. */
292         private List<TimeStampedPVCoordinates> segmentEphemeris;
293 
294         /** Completely parsed ephemeris file. */
295         private STKEphemerisFile file;
296 
297         /**
298          * Constructs a {@link ParseInfo} instance.
299          */
300         private ParseInfo() {
301             // Set defaults.
302             this.distanceUnit = STKDistanceUnit.METERS;
303             this.interpolationSamplesM1 = 5;
304             this.coordinateSystem = STKCoordinateSystem.FIXED;
305 
306             // Other initialization.
307             this.ephemerisSegments = new ArrayList<>();
308             this.segmentBoundaryTimes = new TreeSet<>();
309             this.segmentEphemeris = new ArrayList<>();
310         }
311 
312         /**
313          * Returns the UTC scale.
314          * @return UTC scale
315          */
316         private UTCScale getUTCScale() {
317             return utc;
318         }
319 
320         /**
321          * Adds an ephemeris point.
322          * @param time time
323          * @param pvCoordinates position/velocity coordinates
324          */
325         private void addEphemeris(final double time, final PVCoordinates pvCoordinates) {
326             if (numberOfEphemerisPoints != null && numberOfEphemerisPointsRead == numberOfEphemerisPoints) {
327                 return;
328             }
329             final AbsoluteDate date = scenarioEpoch.shiftedBy(time);
330             final TimeStampedPVCoordinates timeStampedPVCoordinates = new TimeStampedPVCoordinates(date, pvCoordinates);
331             if (segmentBoundaryTimes.contains(time) && numberOfEphemerisPointsRead > 0) {
332                 if (segmentEphemeris.isEmpty()) { // begin new segment
333                     if (!date.equals(lastSavedEphemeris.getDate())) {
334                         segmentEphemeris.add(lastSavedEphemeris); // no gaps allowed
335                     }
336                     segmentEphemeris.add(timeStampedPVCoordinates);
337                 } else { // end segment
338                     segmentEphemeris.add(timeStampedPVCoordinates);
339                     ephemerisSegments.add(new STKEphemerisSegment(mu, getFrame(), 1 + interpolationSamplesM1,
340                             cartesianDerivativesFilter, segmentEphemeris));
341                     segmentEphemeris = new ArrayList<>();
342                 }
343             } else {
344                 segmentEphemeris.add(timeStampedPVCoordinates);
345             }
346             lastSavedEphemeris = timeStampedPVCoordinates;
347             ++numberOfEphemerisPointsRead;
348         }
349 
350         /**
351          * Returns the frame.
352          * @return frame
353          */
354         private Frame getFrame() {
355             final STKCoordinateSystem stkCoordinateSystem = coordinateSystem == null ? STKCoordinateSystem.FIXED :
356                     coordinateSystem;
357             final Frame frame = frameMapping.get(stkCoordinateSystem);
358             if (frame == null) {
359                 throw new OrekitException(OrekitMessages.STK_UNMAPPED_COORDINATE_SYSTEM, stkCoordinateSystem);
360             }
361             return frame;
362         }
363 
364         /**
365          * Completes parsing.
366          */
367         private void complete() {
368             if (!segmentEphemeris.isEmpty()) {
369                 ephemerisSegments.add(new STKEphemerisSegment(mu, getFrame(), 1 + interpolationSamplesM1,
370                         cartesianDerivativesFilter, segmentEphemeris));
371             }
372             final STKEphemeris ephemeris = new STKEphemeris(satelliteId, mu, ephemerisSegments);
373             file = new STKEphemerisFile(stkVersion, satelliteId, ephemeris);
374         }
375 
376     }
377 
378     /** Parser for specific line. */
379     private enum LineParser {
380 
381         /** STK version. */
382         VERSION("^stk\\.v\\.\\d+\\.\\d+$") {
383 
384             @Override
385             public void parse(final String line, final ParseInfo pi) {
386                 pi.stkVersion = line;
387             }
388 
389             @Override
390             public Iterable<LineParser> allowedNext() {
391                 return Collections.singleton(BEGIN_EPHEMERIS);
392             }
393 
394         },
395 
396         /** BEGIN Ephemeris keyword. */
397         BEGIN_EPHEMERIS("^\\s*BEGIN Ephemeris\\s*(#.*)?$") {
398 
399             @Override
400             public void parse(final String line, final ParseInfo pi) {
401                 // nothing to do
402             }
403 
404             @Override
405             public Iterable<LineParser> allowedNext() {
406                 return KEYWORDS;
407             }
408 
409         },
410 
411         /** NumberOfEphemerisPoints keyword. */
412         NUMBER_OF_EPHEMERIS_POINTS("^\\s*NumberOfEphemerisPoints\\s*\\d+\\s*(#.*)?$") {
413 
414             @Override
415             public void parse(final String line, final ParseInfo pi) {
416                 pi.numberOfEphemerisPoints = Integer.parseInt(SEPARATOR.split(line.trim())[1]);
417             }
418 
419             @Override
420             public Iterable<LineParser> allowedNext() {
421                 return KEYWORDS;
422             }
423 
424         },
425 
426         /** ScenarioEpoch keyword. */
427         SCENARIO_EPOCH("^\\s*ScenarioEpoch\\s* \\d{2} [a-zA-Z]{3} \\d{4} \\d{2}:\\d{2}:\\d{2}(\\.\\d*)?\\s*(#.*)?$") {
428 
429             @Override
430             public void parse(final String line, final ParseInfo pi) {
431                 final String[] tokens = SEPARATOR.split(line.trim());
432                 final int dayOfMonth = Integer.parseInt(tokens[1]);
433                 final Month month = Month.parseMonth(tokens[2]);
434                 final int year = Integer.parseInt(tokens[3]);
435                 final int hour = Integer.parseInt(tokens[4].substring(0, 2));
436                 final int minute = Integer.parseInt(tokens[4].substring(3, 5));
437                 final double seconds = Double.parseDouble(tokens[4].substring(6));
438                 final DateTimeComponents dateTimeComponents = new DateTimeComponents(year, month, dayOfMonth, hour, minute, seconds);
439                 pi.scenarioEpoch = new AbsoluteDate(dateTimeComponents, pi.getUTCScale());
440             }
441 
442             @Override
443             public Iterable<LineParser> allowedNext() {
444                 return KEYWORDS;
445             }
446 
447         },
448 
449         /** InterpolationMethod keyword. */
450         INTERPOLATION_METHOD("^\\s*InterpolationMethod\\s+[a-zA-Z]+\\s*(#.*)?$") {
451 
452             @Override
453             public void parse(final String line, final ParseInfo pi) {
454                 // do nothing; this keyword is recognized, but ignored and unsupported
455             }
456 
457             @Override
458             public Iterable<LineParser> allowedNext() {
459                 return KEYWORDS;
460             }
461 
462         },
463 
464         /** InterpolationSamplesM1 keyword. */
465         INTERPOLATION_SAMPLESM1("^\\s*InterpolationSamplesM1\\s+\\d+\\s*(#.*)?$") {
466 
467             @Override
468             public void parse(final String line, final ParseInfo pi) {
469                 pi.interpolationSamplesM1 = Integer.parseInt(SEPARATOR.split(line.trim())[1]);
470             }
471 
472             @Override
473             public Iterable<LineParser> allowedNext() {
474                 return KEYWORDS;
475             }
476 
477         },
478 
479         /** CentralBody keyword. */
480         CENTRAL_BODY("^\\s*CentralBody\\s+[a-zA-Z]+\\s*(#.*)?$") {
481 
482             @Override
483             public void parse(final String line, final ParseInfo pi) {
484                 // do nothing; this keyword is recognized, but ignored and unsupported; Earth
485                 // assumed
486             }
487 
488             @Override
489             public Iterable<LineParser> allowedNext() {
490                 return KEYWORDS;
491             }
492 
493         },
494 
495         /** CoordinateSystem keyword. */
496         COORDINATE_SYSTEM("^\\s*CoordinateSystem\\s+[a-zA-Z0-9]+\\s*(#.*)?$") {
497 
498             @Override
499             public void parse(final String line, final ParseInfo pi) {
500                 pi.coordinateSystem = STKCoordinateSystem.parse(SEPARATOR.split(line.trim())[1]);
501             }
502 
503             @Override
504             public Iterable<LineParser> allowedNext() {
505                 return KEYWORDS;
506             }
507 
508         },
509 
510         /** DistanceUnit keyword. */
511         DISTANCE_UNIT("^\\s*DistanceUnit\\s+[a-zA-Z0-9]+\\s*(#.*)?$") {
512 
513             @Override
514             public void parse(final String line, final ParseInfo pi) {
515                 pi.distanceUnit = STKDistanceUnit.valueOf(SEPARATOR.split(line.trim())[1].toUpperCase());
516             }
517 
518             @Override
519             public Iterable<LineParser> allowedNext() {
520                 return KEYWORDS;
521             }
522 
523         },
524 
525         /** BEGIN SegmentBoundaryTimes keyword. */
526         BEGIN_SEGMENT_BOUNDARY_TIMES("^\\s*BEGIN SegmentBoundaryTimes\\s*(#.*)?$") {
527 
528             @Override
529             public void parse(final String line, final ParseInfo pi) {
530                 // nothing to be done
531             }
532 
533             @Override
534             public Iterable<LineParser> allowedNext() {
535                 return Collections.singleton(SEGMENT_BOUNDARY_TIME);
536             }
537 
538         },
539 
540         /** Segment boundary time. */
541         SEGMENT_BOUNDARY_TIME(MATCH_ANY_REGEX) {
542 
543             @Override
544             public void parse(final String line, final ParseInfo pi) {
545                 pi.segmentBoundaryTimes.add(Double.parseDouble(SEPARATOR.split(line.trim())[0]));
546             }
547 
548             @Override
549             public Iterable<LineParser> allowedNext() {
550                 return Arrays.asList(END_SEGMENT_BOUNDARY_TIMES, SEGMENT_BOUNDARY_TIME);
551             }
552 
553         },
554 
555         /** END SegmentBoundaryTimes keyword. */
556         END_SEGMENT_BOUNDARY_TIMES("^\\s*END SegmentBoundaryTimes\\s*(#.*)?$") {
557 
558             @Override
559             public void parse(final String line, final ParseInfo pi) {
560                 // nothing to be done
561             }
562 
563             @Override
564             public Iterable<LineParser> allowedNext() {
565                 return KEYWORDS;
566             }
567 
568         },
569 
570         /** EphemerisTimePos keyword. */
571         EPHEMERIS_TIME_POS("^\\s*EphemerisTimePos\\s*(#.*)?$") {
572 
573             @Override
574             public void parse(final String line, final ParseInfo pi) {
575                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_P;
576             }
577 
578             @Override
579             public Iterable<LineParser> allowedNext() {
580                 return Collections.singleton(EPHEMERIS_TIME_POS_DATUM);
581             }
582 
583         },
584 
585         /** EphemerisTimePos datum. */
586         EPHEMERIS_TIME_POS_DATUM(MATCH_ANY_REGEX) {
587 
588             @Override
589             public void parse(final String line, final ParseInfo pi) {
590                 final String[] tokens = SEPARATOR.split(line.trim());
591                 final double time = Double.parseDouble(tokens[0]);
592                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
593                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
594                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;
595 
596                 final Vector3D position = new Vector3D(px, py, pz);
597                 final Vector3D velocity = Vector3D.ZERO;
598 
599                 pi.addEphemeris(time, new PVCoordinates(position, velocity));
600             }
601 
602             @Override
603             public Iterable<LineParser> allowedNext() {
604                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_DATUM);
605             }
606 
607         },
608 
609         /** EphemerisTimePosVel keyword. */
610         EPHEMERIS_TIME_POS_VEL("^\\s*EphemerisTimePosVel\\s*(#.*)?$") {
611 
612             @Override
613             public void parse(final String line, final ParseInfo pi) {
614                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_PV;
615             }
616 
617             @Override
618             public Iterable<LineParser> allowedNext() {
619                 return Collections.singleton(EPHEMERIS_TIME_POS_VEL_DATUM);
620             }
621 
622         },
623 
624         /** EphemerisTimePosVel datum. */
625         EPHEMERIS_TIME_POS_VEL_DATUM(MATCH_ANY_REGEX) {
626 
627             @Override
628             public void parse(final String line, final ParseInfo pi) {
629                 final String[] tokens = SEPARATOR.split(line.trim());
630                 final double time = Double.parseDouble(tokens[0]);
631                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
632                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
633                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;
634                 final double vx = Double.parseDouble(tokens[4]) * pi.distanceUnit.conversionToMetersFactor;
635                 final double vy = Double.parseDouble(tokens[5]) * pi.distanceUnit.conversionToMetersFactor;
636                 final double vz = Double.parseDouble(tokens[6]) * pi.distanceUnit.conversionToMetersFactor;
637 
638                 final Vector3D position = new Vector3D(px, py, pz);
639                 final Vector3D velocity = new Vector3D(vx, vy, vz);
640 
641                 pi.addEphemeris(time, new PVCoordinates(position, velocity));
642             }
643 
644             @Override
645             public Iterable<LineParser> allowedNext() {
646                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_VEL_DATUM);
647             }
648 
649         },
650 
651         /** EphemerisTimePosVelAcc keyword. */
652         EPHEMERIS_TIME_POS_VEL_ACC("^\\s*EphemerisTimePosVelAcc\\s*(#.*)?$") {
653 
654             @Override
655             public void parse(final String line, final ParseInfo pi) {
656                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_PVA;
657             }
658 
659             @Override
660             public Iterable<LineParser> allowedNext() {
661                 return Collections.singleton(EPHEMERIS_TIME_POS_VEL_ACC_DATUM);
662             }
663 
664         },
665 
666         /** EphemerisTimePosVelAcc datum. */
667         EPHEMERIS_TIME_POS_VEL_ACC_DATUM(MATCH_ANY_REGEX) {
668 
669             @Override
670             public void parse(final String line, final ParseInfo pi) {
671                 final String[] tokens = SEPARATOR.split(line.trim());
672                 final double time = Double.parseDouble(tokens[0]);
673                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
674                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
675                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;
676                 final double vx = Double.parseDouble(tokens[4]) * pi.distanceUnit.conversionToMetersFactor;
677                 final double vy = Double.parseDouble(tokens[5]) * pi.distanceUnit.conversionToMetersFactor;
678                 final double vz = Double.parseDouble(tokens[6]) * pi.distanceUnit.conversionToMetersFactor;
679                 final double ax = Double.parseDouble(tokens[7]) * pi.distanceUnit.conversionToMetersFactor;
680                 final double ay = Double.parseDouble(tokens[8]) * pi.distanceUnit.conversionToMetersFactor;
681                 final double az = Double.parseDouble(tokens[9]) * pi.distanceUnit.conversionToMetersFactor;
682 
683                 final Vector3D position = new Vector3D(px, py, pz);
684                 final Vector3D velocity = new Vector3D(vx, vy, vz);
685                 final Vector3D acceleration = new Vector3D(ax, ay, az);
686 
687                 pi.addEphemeris(time, new PVCoordinates(position, velocity, acceleration));
688             }
689 
690             @Override
691             public Iterable<LineParser> allowedNext() {
692                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_VEL_ACC_DATUM);
693             }
694 
695         },
696 
697         /** END Ephemeris keyword. */
698         END_EPHEMERIS("\\s*END Ephemeris\\s*(#.*)?") {
699 
700             @Override
701             public void parse(final String line, final ParseInfo pi) {
702                 pi.complete();
703             }
704 
705             @Override
706             public Iterable<LineParser> allowedNext() {
707                 return Collections.emptyList();
708             }
709 
710         };
711 
712         /** Pattern for identifying line. */
713         private final Pattern pattern;
714 
715         /**
716          * Constructs a {@link LineParser} instance.
717          * @param regex regular expression for identifying line
718          */
719         LineParser(final String regex) {
720             pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
721         }
722 
723         /**
724          * Parses a line.
725          * @param line line to parse
726          * @param pi holder for transient data
727          */
728         public abstract void parse(String line, ParseInfo pi);
729 
730         /**
731          * Returns the allowed parsers for the next line.
732          * @return returns the allowed parsers for the next line
733          */
734         public abstract Iterable<LineParser> allowedNext();
735 
736         /**
737          * Checks if a parser can handle line.
738          * @param line line to parse
739          * @return true if parser can handle the specified line
740          */
741         public boolean canHandle(final String line) {
742             return pattern.matcher(line).matches();
743         }
744 
745     }
746 
747     /** STK distance unit. */
748     private enum STKDistanceUnit {
749 
750         /** Kilometers. */
751         KILOMETERS(1000.0),
752 
753         /** Meters. */
754         METERS(1.0);
755 
756         /** Factor by which to multiply to convert the distance unit to meters. */
757         private final double conversionToMetersFactor;
758 
759         /**
760          * Constructs a {@link STKDistanceUnit} instance.
761          * @param conversionToMetersFactor factor by which to multiply to
762          *        convert the distance unit to meters
763          */
764         STKDistanceUnit(final double conversionToMetersFactor) {
765             this.conversionToMetersFactor = conversionToMetersFactor;
766         }
767 
768     }
769 
770 }