STKEphemerisFileParser.java

  1. /* Copyright 2002-2025 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. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.Reader;
  21. import java.util.ArrayList;
  22. import java.util.Arrays;
  23. import java.util.Collections;
  24. import java.util.EnumMap;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Objects;
  28. import java.util.SortedSet;
  29. import java.util.TreeSet;
  30. import java.util.regex.Pattern;
  31. import java.util.stream.Stream;

  32. import org.hipparchus.exception.LocalizedCoreFormats;
  33. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  34. import org.orekit.data.DataSource;
  35. import org.orekit.errors.OrekitException;
  36. import org.orekit.errors.OrekitMessages;
  37. import org.orekit.files.general.EphemerisFileParser;
  38. import org.orekit.files.stk.STKEphemerisFile.STKCoordinateSystem;
  39. import org.orekit.files.stk.STKEphemerisFile.STKEphemeris;
  40. import org.orekit.files.stk.STKEphemerisFile.STKEphemerisSegment;
  41. import org.orekit.frames.Frame;
  42. import org.orekit.time.AbsoluteDate;
  43. import org.orekit.time.DateTimeComponents;
  44. import org.orekit.time.Month;
  45. import org.orekit.time.UTCScale;
  46. import org.orekit.utils.CartesianDerivativesFilter;
  47. import org.orekit.utils.PVCoordinates;
  48. import org.orekit.utils.TimeStampedPVCoordinates;

  49. /**
  50.  * Parser of {@link STKEphemerisFile}s.
  51.  *
  52.  * <p> The STK ephemeris file format specification is quite extensive and this implementation does not
  53.  * attempt (nor is it possible, given the lack of an STK scenario to provide context) to support all
  54.  * possible variations of the format. The following keywords are recognized (case-insensitive):
  55.  * <table>
  56.  *     <caption>Recognized Keywords</caption>
  57.  *     <thead>
  58.  *         <tr>
  59.  *             <th>Keyword</th>
  60.  *             <th>Supported</th>
  61.  *             <th>Comment</th>
  62.  *         </tr>
  63.  *     </thead>
  64.  *     <tbody>
  65.  *         <tr>
  66.  *             <td>stk.v.*.*</td>
  67.  *             <td>Yes</td>
  68.  *             <td>STK version number</td>
  69.  *         </tr>
  70.  *         <tr>
  71.  *             <td>BEGIN/END Ephemeris</td>
  72.  *             <td>Yes</td>
  73.  *             <td></td>
  74.  *         </tr>
  75.  *         <tr>
  76.  *             <td>ScenarioEpoch</td>
  77.  *             <td>Yes</td>
  78.  *             <td>Gregorian UTC time format (<code>dd mmm yyyy hh:mm:ss.s</code>) assumed;
  79.  *                 the <code>TimeFormat</code> keyword is not recognized.</td>
  80.  *         </tr>
  81.  *         <tr>
  82.  *             <td>CentralBody</td>
  83.  *             <td>No</td>
  84.  *             <td>Class constructors require gravitational parameter.</td>
  85.  *         </tr>
  86.  *         <tr>
  87.  *             <td>CoordinateSystem</td>
  88.  *             <td>Yes</td>
  89.  *             <td>Implementation uses a frame mapping to map {@link STKCoordinateSystem}s to {@link Frame}s.</td>
  90.  *         </tr>
  91.  *         <tr>
  92.  *             <td>DistanceUnit</td>
  93.  *             <td>Yes</td>
  94.  *             <td>Only <code>Meters</code> and <code>Kilometers</code> are supported.</td>
  95.  *         </tr>
  96.  *         <tr>
  97.  *             <td>InterpolationMethod</td>
  98.  *             <td>No</td>
  99.  *             <td>The Orekit EphemerisSegmentPropagator class uses
  100.  *             {@link org.orekit.utils.TimeStampedPVCoordinatesHermiteInterpolator#interpolate(AbsoluteDate, Stream)}
  101.  *             to do Hermite interpolation, so the value of <code>InterpolationMethod</code>, if present, is
  102.  *             ignored.</td>
  103.  *         </tr>
  104.  *         <tr>
  105.  *             <td>InterpolationSamplesM1</td>
  106.  *             <td>Yes</td>
  107.  *             <td>Note that the <code>InterpolationMethod</code> keyword is ignored, but the value of
  108.  *             <code>InterpolationSamplesM1</code> will be used to determine the number of sample points in the
  109.  *             Hermite interpolator used by Orekit.</td>
  110.  *         </tr>
  111.  *         <tr>
  112.  *             <td>NumberOfEphemerisPoints</td>
  113.  *             <td>Yes</td>
  114.  *             <td></td>
  115.  *         </tr>
  116.  *         <tr>
  117.  *             <td>BEGIN/END SegmentBoundaryTimes</td>
  118.  *             <td>Yes</td>
  119.  *             <td></td>
  120.  *         </tr>
  121.  *     </tbody>
  122.  * </table>
  123.  *
  124.  * <p> Any keyword in the format specification which is not explicitly named in the above table is not recognized and
  125.  * will cause a parse exception. Those keywords that are listed above as recognized but not supported are simply
  126.  * ignored.
  127.  *
  128.  * <p> The following ephemeris formats are recognized and supported:
  129.  * <ul>
  130.  *     <li>EphemerisTimePos</li>
  131.  *     <li>EphemerisTimePosVel</li>
  132.  *     <li>EphemerisTimePosVelAcc</li>
  133.  * </ul>
  134.  * Any ephemeris format in the format specification which is not explicitly named in the above list is not recognized
  135.  * and will cause an exception.
  136.  *
  137.  * @author Andrew Goetz
  138.  * @since 12.0
  139.  */
  140. public class STKEphemerisFileParser implements EphemerisFileParser<STKEphemerisFile> {

  141.     /** Pattern for delimiting regular expressions. */
  142.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  143.     /** Pattern for ignorable lines. Comments are preceded by '#'. */
  144.     private static final Pattern IGNORABLE_LINE = Pattern.compile("^\\s*(#.*)?");

  145.     /** Regular expression that matches anything. */
  146.     private static final String MATCH_ANY_REGEX = ".*";

  147.     /** Recognized keywords. */
  148.     private static final List<LineParser> KEYWORDS = Arrays.asList(
  149.             LineParser.NUMBER_OF_EPHEMERIS_POINTS,
  150.             LineParser.SCENARIO_EPOCH,
  151.             LineParser.INTERPOLATION_METHOD,
  152.             LineParser.INTERPOLATION_SAMPLESM1,
  153.             LineParser.CENTRAL_BODY,
  154.             LineParser.COORDINATE_SYSTEM,
  155.             LineParser.BEGIN_SEGMENT_BOUNDARY_TIMES,
  156.             LineParser.EPHEMERIS_TIME_POS,
  157.             LineParser.EPHEMERIS_TIME_POS_VEL,
  158.             LineParser.EPHEMERIS_TIME_POS_VEL_ACC
  159.     );

  160.     /** Satellite id. */
  161.     private final String satelliteId;

  162.     /** Gravitational parameter (m^3/s^2). */
  163.     private final double mu;

  164.     /** UTC time scale. */
  165.     private final UTCScale utc;

  166.     /** Mapping of STK coordinate system to Orekit reference frame. */
  167.     private final Map<STKCoordinateSystem, Frame> frameMapping;

  168.     /**
  169.      * Constructs a {@link STKEphemerisFileParser} instance.
  170.      * @param satelliteId satellite id for satellites parsed by the parser
  171.      * @param mu gravitational parameter (m^3/s^2)
  172.      * @param utc UTC scale for parsed dates
  173.      * @param frameMapping mapping from STK coordinate system to Orekit frame
  174.      */
  175.     public STKEphemerisFileParser(final String satelliteId, final double mu, final UTCScale utc,
  176.             final Map<STKCoordinateSystem, Frame> frameMapping) {
  177.         this.satelliteId = Objects.requireNonNull(satelliteId);
  178.         this.mu = mu;
  179.         this.utc = Objects.requireNonNull(utc);
  180.         this.frameMapping = Collections.unmodifiableMap(new EnumMap<>(frameMapping));
  181.     }

  182.     @Override
  183.     public STKEphemerisFile parse(final DataSource source) {

  184.         try (Reader reader = source.getOpener().openReaderOnce();
  185.              BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {

  186.             if (br == null) {
  187.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  188.             }

  189.             // initialize internal data structures
  190.             final ParseInfo pi = new ParseInfo();

  191.             int lineNumber = 0;
  192.             Iterable<LineParser> parsers = Collections.singleton(LineParser.VERSION);
  193.             nextLine:
  194.                 for (String line = br.readLine(); line != null; line = br.readLine()) {
  195.                 ++lineNumber;
  196.                 if (pi.file != null) {
  197.                     break;
  198.                 } else if (IGNORABLE_LINE.matcher(line).matches()) {
  199.                     continue;
  200.                 }
  201.                 for (final LineParser candidate : parsers) {
  202.                     if (candidate.canHandle(line)) {
  203.                         try {
  204.                             candidate.parse(line, pi);
  205.                             parsers = candidate.allowedNext();
  206.                             continue nextLine;
  207.                         } catch (StringIndexOutOfBoundsException | IllegalArgumentException e) {
  208.                             throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber,
  209.                                                       source.getName(), line);
  210.                         }
  211.                     }
  212.                 }

  213.                 // no parsers found for this line
  214.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, source.getName(),
  215.                                           line);

  216.                 }

  217.             if (pi.file != null) {
  218.                 return pi.file;
  219.             } else {
  220.                 throw new OrekitException(OrekitMessages.STK_UNEXPECTED_END_OF_FILE, lineNumber);
  221.             }

  222.         } catch (IOException ioe) {
  223.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  224.         }
  225.     }

  226.     /**
  227.      * Transient data used for parsing an STK ephemeris file. The data is kept in a
  228.      * separate data structure to make the parser thread-safe.
  229.      * <p>
  230.      * <b>Note</b>: The class intentionally does not provide accessor methods, as it
  231.      * is only used internally for parsing an STK ephemeris file.
  232.      * </p>
  233.      */
  234.     private final class ParseInfo {

  235.         /** STK version. */
  236.         private String stkVersion;

  237.         /** Scenario epoch. */
  238.         private AbsoluteDate scenarioEpoch; // technically optional but required here b/c no STK scenario for context

  239.         /** Number of ephemeris points. */
  240.         private Integer numberOfEphemerisPoints;

  241.         /** One less than the number of points used in the interpolation. */
  242.         private int interpolationSamplesM1;

  243.         /** Cartesian derivatives filter for interpolation. */
  244.         private CartesianDerivativesFilter cartesianDerivativesFilter;

  245.         /** Coordinate system. */
  246.         private STKCoordinateSystem coordinateSystem;

  247.         /** Distance unit. */
  248.         private STKDistanceUnit distanceUnit;

  249.         /** Number of ephemeris points read. */
  250.         private int numberOfEphemerisPointsRead;

  251.         /** Segment boundary times. */
  252.         private SortedSet<Double> segmentBoundaryTimes;

  253.         /** Ephemeris segments. */
  254.         private List<STKEphemerisSegment> ephemerisSegments;

  255.         /** Last-saved ephemeris. */
  256.         private TimeStampedPVCoordinates lastSavedEphemeris;

  257.         /** Ephemeris for current segment. */
  258.         private List<TimeStampedPVCoordinates> segmentEphemeris;

  259.         /** Completely parsed ephemeris file. */
  260.         private STKEphemerisFile file;

  261.         /**
  262.          * Constructs a {@link ParseInfo} instance.
  263.          */
  264.         private ParseInfo() {
  265.             // Set defaults.
  266.             this.distanceUnit = STKDistanceUnit.METERS;
  267.             this.interpolationSamplesM1 = 5;
  268.             this.coordinateSystem = STKCoordinateSystem.FIXED;

  269.             // Other initialization.
  270.             this.ephemerisSegments = new ArrayList<>();
  271.             this.segmentBoundaryTimes = new TreeSet<>();
  272.             this.segmentEphemeris = new ArrayList<>();
  273.         }

  274.         /**
  275.          * Returns the UTC scale.
  276.          * @return UTC scale
  277.          */
  278.         private UTCScale getUTCScale() {
  279.             return utc;
  280.         }

  281.         /**
  282.          * Adds an ephemeris point.
  283.          * @param time time
  284.          * @param pvCoordinates position/velocity coordinates
  285.          */
  286.         private void addEphemeris(final double time, final PVCoordinates pvCoordinates) {
  287.             if (numberOfEphemerisPoints != null && numberOfEphemerisPointsRead == numberOfEphemerisPoints) {
  288.                 return;
  289.             }
  290.             final AbsoluteDate date = scenarioEpoch.shiftedBy(time);
  291.             final TimeStampedPVCoordinates timeStampedPVCoordinates = new TimeStampedPVCoordinates(date, pvCoordinates);
  292.             if (segmentBoundaryTimes.contains(time) && numberOfEphemerisPointsRead > 0) {
  293.                 if (segmentEphemeris.isEmpty()) { // begin new segment
  294.                     if (!date.equals(lastSavedEphemeris.getDate())) {
  295.                         segmentEphemeris.add(lastSavedEphemeris); // no gaps allowed
  296.                     }
  297.                     segmentEphemeris.add(timeStampedPVCoordinates);
  298.                 } else { // end segment
  299.                     segmentEphemeris.add(timeStampedPVCoordinates);
  300.                     ephemerisSegments.add(new STKEphemerisSegment(mu, getFrame(), 1 + interpolationSamplesM1,
  301.                             cartesianDerivativesFilter, segmentEphemeris));
  302.                     segmentEphemeris = new ArrayList<>();
  303.                 }
  304.             } else {
  305.                 segmentEphemeris.add(timeStampedPVCoordinates);
  306.             }
  307.             lastSavedEphemeris = timeStampedPVCoordinates;
  308.             ++numberOfEphemerisPointsRead;
  309.         }

  310.         /**
  311.          * Returns the frame.
  312.          * @return frame
  313.          */
  314.         private Frame getFrame() {
  315.             final STKCoordinateSystem stkCoordinateSystem = coordinateSystem == null ? STKCoordinateSystem.FIXED :
  316.                     coordinateSystem;
  317.             final Frame frame = frameMapping.get(stkCoordinateSystem);
  318.             if (frame == null) {
  319.                 throw new OrekitException(OrekitMessages.STK_UNMAPPED_COORDINATE_SYSTEM, stkCoordinateSystem);
  320.             }
  321.             return frame;
  322.         }

  323.         /**
  324.          * Completes parsing.
  325.          */
  326.         private void complete() {
  327.             if (!segmentEphemeris.isEmpty()) {
  328.                 ephemerisSegments.add(new STKEphemerisSegment(mu, getFrame(), 1 + interpolationSamplesM1,
  329.                         cartesianDerivativesFilter, segmentEphemeris));
  330.             }
  331.             final STKEphemeris ephemeris = new STKEphemeris(satelliteId, mu, ephemerisSegments);
  332.             file = new STKEphemerisFile(stkVersion, satelliteId, ephemeris);
  333.         }

  334.     }

  335.     /** Parser for specific line. */
  336.     private enum LineParser {

  337.         /** STK version. */
  338.         VERSION("^stk\\.v\\.\\d+\\.\\d+$") {

  339.             @Override
  340.             public void parse(final String line, final ParseInfo pi) {
  341.                 pi.stkVersion = line;
  342.             }

  343.             @Override
  344.             public Iterable<LineParser> allowedNext() {
  345.                 return Collections.singleton(BEGIN_EPHEMERIS);
  346.             }

  347.         },

  348.         /** BEGIN Ephemeris keyword. */
  349.         BEGIN_EPHEMERIS("^\\s*BEGIN Ephemeris\\s*(#.*)?$") {

  350.             @Override
  351.             public void parse(final String line, final ParseInfo pi) {
  352.                 // nothing to do
  353.             }

  354.             @Override
  355.             public Iterable<LineParser> allowedNext() {
  356.                 return KEYWORDS;
  357.             }

  358.         },

  359.         /** NumberOfEphemerisPoints keyword. */
  360.         NUMBER_OF_EPHEMERIS_POINTS("^\\s*NumberOfEphemerisPoints\\s*\\d+\\s*(#.*)?$") {

  361.             @Override
  362.             public void parse(final String line, final ParseInfo pi) {
  363.                 pi.numberOfEphemerisPoints = Integer.parseInt(SEPARATOR.split(line.trim())[1]);
  364.             }

  365.             @Override
  366.             public Iterable<LineParser> allowedNext() {
  367.                 return KEYWORDS;
  368.             }

  369.         },

  370.         /** ScenarioEpoch keyword. */
  371.         SCENARIO_EPOCH("^\\s*ScenarioEpoch\\s* \\d{2} [a-zA-Z]{3} \\d{4} \\d{2}:\\d{2}:\\d{2}(\\.\\d*)?\\s*(#.*)?$") {

  372.             @Override
  373.             public void parse(final String line, final ParseInfo pi) {
  374.                 final String[] tokens = SEPARATOR.split(line.trim());
  375.                 final int dayOfMonth = Integer.parseInt(tokens[1]);
  376.                 final Month month = Month.parseMonth(tokens[2]);
  377.                 final int year = Integer.parseInt(tokens[3]);
  378.                 final int hour = Integer.parseInt(tokens[4].substring(0, 2));
  379.                 final int minute = Integer.parseInt(tokens[4].substring(3, 5));
  380.                 final double seconds = Double.parseDouble(tokens[4].substring(6));
  381.                 final DateTimeComponents dateTimeComponents = new DateTimeComponents(year, month, dayOfMonth, hour, minute, seconds);
  382.                 pi.scenarioEpoch = new AbsoluteDate(dateTimeComponents, pi.getUTCScale());
  383.             }

  384.             @Override
  385.             public Iterable<LineParser> allowedNext() {
  386.                 return KEYWORDS;
  387.             }

  388.         },

  389.         /** InterpolationMethod keyword. */
  390.         INTERPOLATION_METHOD("^\\s*InterpolationMethod\\s+[a-zA-Z]+\\s*(#.*)?$") {

  391.             @Override
  392.             public void parse(final String line, final ParseInfo pi) {
  393.                 // do nothing; this keyword is recognized, but ignored and unsupported
  394.             }

  395.             @Override
  396.             public Iterable<LineParser> allowedNext() {
  397.                 return KEYWORDS;
  398.             }

  399.         },

  400.         /** InterpolationSamplesM1 keyword. */
  401.         INTERPOLATION_SAMPLESM1("^\\s*InterpolationSamplesM1\\s+\\d+\\s*(#.*)?$") {

  402.             @Override
  403.             public void parse(final String line, final ParseInfo pi) {
  404.                 pi.interpolationSamplesM1 = Integer.parseInt(SEPARATOR.split(line.trim())[1]);
  405.             }

  406.             @Override
  407.             public Iterable<LineParser> allowedNext() {
  408.                 return KEYWORDS;
  409.             }

  410.         },

  411.         /** CentralBody keyword. */
  412.         CENTRAL_BODY("^\\s*CentralBody\\s+[a-zA-Z]+\\s*(#.*)?$") {

  413.             @Override
  414.             public void parse(final String line, final ParseInfo pi) {
  415.                 // do nothing; this keyword is recognized, but ignored and unsupported; Earth
  416.                 // assumed
  417.             }

  418.             @Override
  419.             public Iterable<LineParser> allowedNext() {
  420.                 return KEYWORDS;
  421.             }

  422.         },

  423.         /** CoordinateSystem keyword. */
  424.         COORDINATE_SYSTEM("^\\s*CoordinateSystem\\s+[a-zA-Z0-9]+\\s*(#.*)?$") {

  425.             @Override
  426.             public void parse(final String line, final ParseInfo pi) {
  427.                 pi.coordinateSystem = STKCoordinateSystem.parse(SEPARATOR.split(line.trim())[1]);
  428.             }

  429.             @Override
  430.             public Iterable<LineParser> allowedNext() {
  431.                 return KEYWORDS;
  432.             }

  433.         },

  434.         /** DistanceUnit keyword. */
  435.         DISTANCE_UNIT("^\\s*DistanceUnit\\s+[a-zA-Z0-9]+\\s*(#.*)?$") {

  436.             @Override
  437.             public void parse(final String line, final ParseInfo pi) {
  438.                 pi.distanceUnit = STKDistanceUnit.valueOf(SEPARATOR.split(line.trim())[1].toUpperCase());
  439.             }

  440.             @Override
  441.             public Iterable<LineParser> allowedNext() {
  442.                 return KEYWORDS;
  443.             }

  444.         },

  445.         /** BEGIN SegmentBoundaryTimes keyword. */
  446.         BEGIN_SEGMENT_BOUNDARY_TIMES("^\\s*BEGIN SegmentBoundaryTimes\\s*(#.*)?$") {

  447.             @Override
  448.             public void parse(final String line, final ParseInfo pi) {
  449.                 // nothing to be done
  450.             }

  451.             @Override
  452.             public Iterable<LineParser> allowedNext() {
  453.                 return Collections.singleton(SEGMENT_BOUNDARY_TIME);
  454.             }

  455.         },

  456.         /** Segment boundary time. */
  457.         SEGMENT_BOUNDARY_TIME(MATCH_ANY_REGEX) {

  458.             @Override
  459.             public void parse(final String line, final ParseInfo pi) {
  460.                 pi.segmentBoundaryTimes.add(Double.parseDouble(SEPARATOR.split(line.trim())[0]));
  461.             }

  462.             @Override
  463.             public Iterable<LineParser> allowedNext() {
  464.                 return Arrays.asList(END_SEGMENT_BOUNDARY_TIMES, SEGMENT_BOUNDARY_TIME);
  465.             }

  466.         },

  467.         /** END SegmentBoundaryTimes keyword. */
  468.         END_SEGMENT_BOUNDARY_TIMES("^\\s*END SegmentBoundaryTimes\\s*(#.*)?$") {

  469.             @Override
  470.             public void parse(final String line, final ParseInfo pi) {
  471.                 // nothing to be done
  472.             }

  473.             @Override
  474.             public Iterable<LineParser> allowedNext() {
  475.                 return KEYWORDS;
  476.             }

  477.         },

  478.         /** EphemerisTimePos keyword. */
  479.         EPHEMERIS_TIME_POS("^\\s*EphemerisTimePos\\s*(#.*)?$") {

  480.             @Override
  481.             public void parse(final String line, final ParseInfo pi) {
  482.                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_P;
  483.             }

  484.             @Override
  485.             public Iterable<LineParser> allowedNext() {
  486.                 return Collections.singleton(EPHEMERIS_TIME_POS_DATUM);
  487.             }

  488.         },

  489.         /** EphemerisTimePos datum. */
  490.         EPHEMERIS_TIME_POS_DATUM(MATCH_ANY_REGEX) {

  491.             @Override
  492.             public void parse(final String line, final ParseInfo pi) {
  493.                 final String[] tokens = SEPARATOR.split(line.trim());
  494.                 final double time = Double.parseDouble(tokens[0]);
  495.                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
  496.                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
  497.                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;

  498.                 final Vector3D position = new Vector3D(px, py, pz);
  499.                 final Vector3D velocity = Vector3D.ZERO;

  500.                 pi.addEphemeris(time, new PVCoordinates(position, velocity));
  501.             }

  502.             @Override
  503.             public Iterable<LineParser> allowedNext() {
  504.                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_DATUM);
  505.             }

  506.         },

  507.         /** EphemerisTimePosVel keyword. */
  508.         EPHEMERIS_TIME_POS_VEL("^\\s*EphemerisTimePosVel\\s*(#.*)?$") {

  509.             @Override
  510.             public void parse(final String line, final ParseInfo pi) {
  511.                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_PV;
  512.             }

  513.             @Override
  514.             public Iterable<LineParser> allowedNext() {
  515.                 return Collections.singleton(EPHEMERIS_TIME_POS_VEL_DATUM);
  516.             }

  517.         },

  518.         /** EphemerisTimePosVel datum. */
  519.         EPHEMERIS_TIME_POS_VEL_DATUM(MATCH_ANY_REGEX) {

  520.             @Override
  521.             public void parse(final String line, final ParseInfo pi) {
  522.                 final String[] tokens = SEPARATOR.split(line.trim());
  523.                 final double time = Double.parseDouble(tokens[0]);
  524.                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
  525.                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
  526.                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;
  527.                 final double vx = Double.parseDouble(tokens[4]) * pi.distanceUnit.conversionToMetersFactor;
  528.                 final double vy = Double.parseDouble(tokens[5]) * pi.distanceUnit.conversionToMetersFactor;
  529.                 final double vz = Double.parseDouble(tokens[6]) * pi.distanceUnit.conversionToMetersFactor;

  530.                 final Vector3D position = new Vector3D(px, py, pz);
  531.                 final Vector3D velocity = new Vector3D(vx, vy, vz);

  532.                 pi.addEphemeris(time, new PVCoordinates(position, velocity));
  533.             }

  534.             @Override
  535.             public Iterable<LineParser> allowedNext() {
  536.                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_VEL_DATUM);
  537.             }

  538.         },

  539.         /** EphemerisTimePosVelAcc keyword. */
  540.         EPHEMERIS_TIME_POS_VEL_ACC("^\\s*EphemerisTimePosVelAcc\\s*(#.*)?$") {

  541.             @Override
  542.             public void parse(final String line, final ParseInfo pi) {
  543.                 pi.cartesianDerivativesFilter = CartesianDerivativesFilter.USE_PVA;
  544.             }

  545.             @Override
  546.             public Iterable<LineParser> allowedNext() {
  547.                 return Collections.singleton(EPHEMERIS_TIME_POS_VEL_ACC_DATUM);
  548.             }

  549.         },

  550.         /** EphemerisTimePosVelAcc datum. */
  551.         EPHEMERIS_TIME_POS_VEL_ACC_DATUM(MATCH_ANY_REGEX) {

  552.             @Override
  553.             public void parse(final String line, final ParseInfo pi) {
  554.                 final String[] tokens = SEPARATOR.split(line.trim());
  555.                 final double time = Double.parseDouble(tokens[0]);
  556.                 final double px = Double.parseDouble(tokens[1]) * pi.distanceUnit.conversionToMetersFactor;
  557.                 final double py = Double.parseDouble(tokens[2]) * pi.distanceUnit.conversionToMetersFactor;
  558.                 final double pz = Double.parseDouble(tokens[3]) * pi.distanceUnit.conversionToMetersFactor;
  559.                 final double vx = Double.parseDouble(tokens[4]) * pi.distanceUnit.conversionToMetersFactor;
  560.                 final double vy = Double.parseDouble(tokens[5]) * pi.distanceUnit.conversionToMetersFactor;
  561.                 final double vz = Double.parseDouble(tokens[6]) * pi.distanceUnit.conversionToMetersFactor;
  562.                 final double ax = Double.parseDouble(tokens[7]) * pi.distanceUnit.conversionToMetersFactor;
  563.                 final double ay = Double.parseDouble(tokens[8]) * pi.distanceUnit.conversionToMetersFactor;
  564.                 final double az = Double.parseDouble(tokens[9]) * pi.distanceUnit.conversionToMetersFactor;

  565.                 final Vector3D position = new Vector3D(px, py, pz);
  566.                 final Vector3D velocity = new Vector3D(vx, vy, vz);
  567.                 final Vector3D acceleration = new Vector3D(ax, ay, az);

  568.                 pi.addEphemeris(time, new PVCoordinates(position, velocity, acceleration));
  569.             }

  570.             @Override
  571.             public Iterable<LineParser> allowedNext() {
  572.                 return Arrays.asList(END_EPHEMERIS, EPHEMERIS_TIME_POS_VEL_ACC_DATUM);
  573.             }

  574.         },

  575.         /** END Ephemeris keyword. */
  576.         END_EPHEMERIS("\\s*END Ephemeris\\s*(#.*)?") {

  577.             @Override
  578.             public void parse(final String line, final ParseInfo pi) {
  579.                 pi.complete();
  580.             }

  581.             @Override
  582.             public Iterable<LineParser> allowedNext() {
  583.                 return Collections.emptyList();
  584.             }

  585.         };

  586.         /** Pattern for identifying line. */
  587.         private final Pattern pattern;

  588.         /**
  589.          * Constructs a {@link LineParser} instance.
  590.          * @param regex regular expression for identifying line
  591.          */
  592.         LineParser(final String regex) {
  593.             pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
  594.         }

  595.         /**
  596.          * Parses a line.
  597.          * @param line line to parse
  598.          * @param pi holder for transient data
  599.          */
  600.         public abstract void parse(String line, ParseInfo pi);

  601.         /**
  602.          * Returns the allowed parsers for the next line.
  603.          * @return returns the allowed parsers for the next line
  604.          */
  605.         public abstract Iterable<LineParser> allowedNext();

  606.         /**
  607.          * Checks if a parser can handle line.
  608.          * @param line line to parse
  609.          * @return true if parser can handle the specified line
  610.          */
  611.         public boolean canHandle(final String line) {
  612.             return pattern.matcher(line).matches();
  613.         }

  614.     }

  615.     /** STK distance unit. */
  616.     private enum STKDistanceUnit {

  617.         /** Kilometers. */
  618.         KILOMETERS(1000.0),

  619.         /** Meters. */
  620.         METERS(1.0);

  621.         /** Factor by which to multiply to convert the distance unit to meters. */
  622.         private final double conversionToMetersFactor;

  623.         /**
  624.          * Constructs a {@link STKDistanceUnit} instance.
  625.          * @param conversionToMetersFactor factor by which to multiply to
  626.          *        convert the distance unit to meters
  627.          */
  628.         STKDistanceUnit(final double conversionToMetersFactor) {
  629.             this.conversionToMetersFactor = conversionToMetersFactor;
  630.         }

  631.     }

  632. }