SP3Parser.java

  1. /* Copyright 2002-2012 Space Applications Services
  2.  * Licensed to CS Systèmes d'Information (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. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.nio.file.Files;
  24. import java.nio.file.Paths;
  25. import java.util.Locale;
  26. import java.util.Optional;
  27. import java.util.Scanner;
  28. import java.util.function.Function;
  29. import java.util.regex.Pattern;
  30. import java.util.stream.Stream;

  31. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  32. import org.orekit.errors.OrekitException;
  33. import org.orekit.errors.OrekitMessages;
  34. import org.orekit.files.general.EphemerisFileParser;
  35. import org.orekit.files.sp3.SP3File.SP3Coordinate;
  36. import org.orekit.files.sp3.SP3File.SP3FileType;
  37. import org.orekit.files.sp3.SP3File.TimeSystem;
  38. import org.orekit.frames.Frame;
  39. import org.orekit.frames.FramesFactory;
  40. import org.orekit.time.AbsoluteDate;
  41. import org.orekit.time.TimeScale;
  42. import org.orekit.time.TimeScalesFactory;
  43. import org.orekit.utils.CartesianDerivativesFilter;
  44. import org.orekit.utils.Constants;
  45. import org.orekit.utils.IERSConventions;

  46. /** A parser for the SP3 orbit file format. It supports all formats from sp3-a
  47.  * to sp3-d.
  48.  * <p>
  49.  * <b>Note:</b> this parser is thread-safe, so calling {@link #parse} from
  50.  * different threads is allowed.
  51.  * </p>
  52.  * @see <a href="ftp://igs.org/pub/data/format/sp3_docu.txt">SP3-a file format</a>
  53.  * @see <a href="ftp://igs.org/pub/data/format/sp3c.txt">SP3-c file format</a>
  54.  * @see <a href="ftp://igs.org/pub/data/format/sp3d.pdf">SP3-d file format</a>
  55.  * @author Thomas Neidhart
  56.  * @author Luc Maisonobe
  57.  */
  58. public class SP3Parser implements EphemerisFileParser {

  59.     /** Spaces delimiters. */
  60.     private static final String SPACES = "\\s+";

  61.     /** One millimeter, in meters. */
  62.     private static final double MILLIMETER = 1.0e-3;

  63.     /** Standard gravitational parameter in m^3 / s^2. */
  64.     private final double mu;
  65.     /** Number of data points to use in interpolation. */
  66.     private final int interpolationSamples;
  67.     /** Mapping from frame identifier in the file to a {@link Frame}. */
  68.     private final Function<? super String, ? extends Frame> frameBuilder;

  69.     /**
  70.      * Create an SP3 parser using default values.
  71.      *
  72.      * @see #SP3Parser(double, int, Function)
  73.      */
  74.     public SP3Parser() {
  75.         this(Constants.EIGEN5C_EARTH_MU, 7, SP3Parser::guessFrame);
  76.     }

  77.     /**
  78.      * Create an SP3 parser and specify the extra information needed to create a {@link
  79.      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
  80.      *
  81.      * @param mu                   is the standard gravitational parameter to use for
  82.      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
  83.      *                             the ephemeris data. See {@link Constants}.
  84.      * @param interpolationSamples is the number of samples to use when interpolating.
  85.      * @param frameBuilder         is a function that can construct a frame from an SP3
  86.      *                             coordinate system string. The coordinate system can be
  87.      *                             any 5 character string e.g. ITR92, IGb08.
  88.      */
  89.     public SP3Parser(final double mu,
  90.                      final int interpolationSamples,
  91.                      final Function<? super String, ? extends Frame> frameBuilder) {
  92.         this.mu = mu;
  93.         this.interpolationSamples = interpolationSamples;
  94.         this.frameBuilder = frameBuilder;
  95.     }

  96.     /**
  97.      * Default string to {@link Frame} conversion for {@link #SP3Parser()}.
  98.      *
  99.      * @param name of the frame.
  100.      * @return ITRF based on 2010 conventions,
  101.      * with tidal effects considered during EOP interpolation.
  102.      */
  103.     private static Frame guessFrame(final String name) {
  104.         return FramesFactory.getITRF(IERSConventions.IERS_2010, false);
  105.     }

  106.     /**
  107.      * Parse a SP3 file from an input stream using the UTF-8 charset.
  108.      *
  109.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  110.      * method may read more data than necessary from {@code stream} and the additional
  111.      * data will be lost. The other parse methods do not have this issue.
  112.      *
  113.      * @param stream to read the SP3 file from.
  114.      * @return a parsed SP3 file.
  115.      * @throws IOException     if {@code stream} throws one.
  116.      * @see #parse(String)
  117.      * @see #parse(BufferedReader, String)
  118.      */
  119.     public SP3File parse(final InputStream stream) throws IOException {
  120.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
  121.             return parse(reader, stream.toString());
  122.         }
  123.     }

  124.     @Override
  125.     public SP3File parse(final String fileName) throws IOException, OrekitException {
  126.         try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
  127.                                                              StandardCharsets.UTF_8)) {
  128.             return parse(reader, fileName);
  129.         }
  130.     }

  131.     @Override
  132.     public SP3File parse(final BufferedReader reader,
  133.                          final String fileName) throws IOException {

  134.         // initialize internal data structures
  135.         final ParseInfo pi = new ParseInfo();

  136.         int lineNumber = 0;
  137.         Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
  138.         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  139.             ++lineNumber;
  140.             final String l = line;
  141.             final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
  142.             if (selected.isPresent()) {
  143.                 try {
  144.                     selected.get().parse(line, pi);
  145.                 } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  146.                     throw new OrekitException(e,
  147.                                               OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  148.                                               lineNumber, fileName, line);
  149.                 }
  150.                 candidateParsers = selected.get().allowedNext();
  151.             } else {
  152.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  153.                                           lineNumber, fileName, line);
  154.             }
  155.             if (pi.done) {
  156.                 if (pi.nbEpochs != pi.file.getNumberOfEpochs()) {
  157.                     throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
  158.                                               pi.nbEpochs, fileName, pi.file.getNumberOfEpochs());
  159.                 }
  160.                 return pi.file;
  161.             }
  162.         }

  163.         // we never reached the EOF marker
  164.         throw new OrekitException(OrekitMessages.SP3_UNEXPECTED_END_OF_FILE, lineNumber);

  165.     }

  166.     /** Returns the {@link SP3FileType} that corresponds to a given string in a SP3 file.
  167.      * @param fileType file type as string
  168.      * @return file type as enum
  169.      */
  170.     private static SP3FileType getFileType(final String fileType) {
  171.         SP3FileType type = SP3FileType.UNDEFINED;
  172.         if ("G".equalsIgnoreCase(fileType)) {
  173.             type = SP3FileType.GPS;
  174.         } else if ("M".equalsIgnoreCase(fileType)) {
  175.             type = SP3FileType.MIXED;
  176.         } else if ("R".equalsIgnoreCase(fileType)) {
  177.             type = SP3FileType.GLONASS;
  178.         } else if ("L".equalsIgnoreCase(fileType)) {
  179.             type = SP3FileType.LEO;
  180.         } else if ("E".equalsIgnoreCase(fileType)) {
  181.             type = SP3FileType.GALILEO;
  182.         } else if ("C".equalsIgnoreCase(fileType)) {
  183.             type = SP3FileType.COMPASS;
  184.         } else if ("J".equalsIgnoreCase(fileType)) {
  185.             type = SP3FileType.QZSS;
  186.         }
  187.         return type;
  188.     }

  189.     /** Transient data used for parsing a sp3 file. The data is kept in a
  190.      * separate data structure to make the parser thread-safe.
  191.      * <p><b>Note</b>: The class intentionally does not provide accessor
  192.      * methods, as it is only used internally for parsing a SP3 file.</p>
  193.      */
  194.     private class ParseInfo {

  195.         /** The corresponding SP3File object. */
  196.         private SP3File file;

  197.         /** The latest epoch as read from the SP3 file. */
  198.         private AbsoluteDate latestEpoch;

  199.         /** The latest position as read from the SP3 file. */
  200.         private Vector3D latestPosition;

  201.         /** The latest clock value as read from the SP3 file. */
  202.         private double latestClock;

  203.         /** Indicates if the SP3 file has velocity entries. */
  204.         private boolean hasVelocityEntries;

  205.         /** The timescale used in the SP3 file. */
  206.         private TimeScale timeScale;

  207.         /** The number of satellites as contained in the SP3 file. */
  208.         private int maxSatellites;

  209.         /** The number of satellites accuracies already seen. */
  210.         private int nbAccuracies;

  211.         /** The number of epochs already seen. */
  212.         private int nbEpochs;

  213.         /** End Of File reached indicator. */
  214.         private boolean done;

  215.         /** The base for pos/vel. */
  216.         //private double posVelBase;

  217.         /** The base for clock/rate. */
  218.         //private double clockBase;

  219.         /** Create a new {@link ParseInfo} object. */
  220.         protected ParseInfo() {
  221.             file               = new SP3File(mu, interpolationSamples, frameBuilder);
  222.             latestEpoch        = null;
  223.             latestPosition     = null;
  224.             latestClock        = 0.0;
  225.             hasVelocityEntries = false;
  226.             timeScale          = TimeScalesFactory.getGPS();
  227.             maxSatellites      = 0;
  228.             nbAccuracies       = 0;
  229.             nbEpochs           = 0;
  230.             done               = false;
  231.             //posVelBase = 2d;
  232.             //clockBase = 2d;
  233.         }
  234.     }

  235.     /** Parsers for specific lines. */
  236.     private enum LineParser {

  237.         /** Parser for version, epoch, data used and agency information. */
  238.         HEADER_VERSION("^#[a-z].*") {

  239.             /** {@inheritDoc} */
  240.             @Override
  241.             public void parse(final String line, final ParseInfo pi) {
  242.                 try (Scanner s1      = new Scanner(line);
  243.                      Scanner s2      = s1.useDelimiter(SPACES);
  244.                      Scanner scanner = s2.useLocale(Locale.US)) {
  245.                     scanner.skip("#");
  246.                     final String v = scanner.next();

  247.                     final char version = v.substring(0, 1).toLowerCase().charAt(0);
  248.                     if (version != 'a' && version != 'b' && version != 'c' && version != 'd') {
  249.                         throw new OrekitException(OrekitMessages.SP3_UNSUPPORTED_VERSION, version);
  250.                     }

  251.                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
  252.                     pi.file.setFilter(pi.hasVelocityEntries ?
  253.                                       CartesianDerivativesFilter.USE_PV :
  254.                                       CartesianDerivativesFilter.USE_P);

  255.                     final int    year   = Integer.parseInt(v.substring(2));
  256.                     final int    month  = scanner.nextInt();
  257.                     final int    day    = scanner.nextInt();
  258.                     final int    hour   = scanner.nextInt();
  259.                     final int    minute = scanner.nextInt();
  260.                     final double second = scanner.nextDouble();

  261.                     final AbsoluteDate epoch = new AbsoluteDate(year, month, day,
  262.                                                                 hour, minute, second,
  263.                                                                 TimeScalesFactory.getGPS());

  264.                     pi.file.setEpoch(epoch);

  265.                     final int numEpochs = scanner.nextInt();
  266.                     pi.file.setNumberOfEpochs(numEpochs);

  267.                     // data used indicator
  268.                     pi.file.setDataUsed(scanner.next());

  269.                     pi.file.setCoordinateSystem(scanner.next());
  270.                     pi.file.setOrbitTypeKey(scanner.next());
  271.                     pi.file.setAgency(scanner.next());
  272.                 }
  273.             }

  274.             /** {@inheritDoc} */
  275.             @Override
  276.             public Stream<LineParser> allowedNext() {
  277.                 return Stream.of(HEADER_DATE_TIME_REFERENCE);
  278.             }

  279.         },

  280.         /** Parser for additional date/time references in gps/julian day notation. */
  281.         HEADER_DATE_TIME_REFERENCE("^##.*") {

  282.             /** {@inheritDoc} */
  283.             @Override
  284.             public void parse(final String line, final ParseInfo pi) {
  285.                 try (Scanner s1      = new Scanner(line);
  286.                      Scanner s2      = s1.useDelimiter(SPACES);
  287.                      Scanner scanner = s2.useLocale(Locale.US)) {
  288.                     scanner.skip("##");

  289.                     // gps week
  290.                     pi.file.setGpsWeek(scanner.nextInt());
  291.                     // seconds of week
  292.                     pi.file.setSecondsOfWeek(scanner.nextDouble());
  293.                     // epoch interval
  294.                     pi.file.setEpochInterval(scanner.nextDouble());
  295.                     // julian day
  296.                     pi.file.setJulianDay(scanner.nextInt());
  297.                     // day fraction
  298.                     pi.file.setDayFraction(scanner.nextDouble());
  299.                 }
  300.             }

  301.             /** {@inheritDoc} */
  302.             @Override
  303.             public Stream<LineParser> allowedNext() {
  304.                 return Stream.of(HEADER_SAT_IDS);
  305.             }

  306.         },

  307.         /** Parser for satellites identifiers. */
  308.         HEADER_SAT_IDS("^\\+ .*") {

  309.             /** {@inheritDoc} */
  310.             @Override
  311.             public void parse(final String line, final ParseInfo pi) {

  312.                 if (pi.maxSatellites == 0) {
  313.                     // this is the first ids line, it also contains the number of satellites
  314.                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
  315.                 }

  316.                 final int lineLength = line.length();
  317.                 int count = pi.file.getSatelliteCount();
  318.                 int startIdx = 9;
  319.                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  320.                     final String satId = line.substring(startIdx, startIdx + 3).trim();
  321.                     if (satId.length() > 0) {
  322.                         pi.file.addSatellite(satId);
  323.                     }
  324.                     startIdx += 3;
  325.                 }
  326.             }

  327.             /** {@inheritDoc} */
  328.             @Override
  329.             public Stream<LineParser> allowedNext() {
  330.                 return Stream.of(HEADER_SAT_IDS, HEADER_ACCURACY);
  331.             }

  332.         },

  333.         /** Parser for general accuracy information for each satellite. */
  334.         HEADER_ACCURACY("^\\+\\+.*") {

  335.             /** {@inheritDoc} */
  336.             @Override
  337.             public void parse(final String line, final ParseInfo pi) {
  338.                 final int lineLength = line.length();
  339.                 int startIdx = 9;
  340.                 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  341.                     final String sub = line.substring(startIdx, startIdx + 3).trim();
  342.                     if (sub.length() > 0) {
  343.                         final int exponent = Integer.parseInt(sub);
  344.                         // the accuracy is calculated as 2**exp (in mm)
  345.                         pi.file.setAccuracy(pi.nbAccuracies++, (2 << exponent) * MILLIMETER);
  346.                     }
  347.                     startIdx += 3;
  348.                 }
  349.             }

  350.             /** {@inheritDoc} */
  351.             @Override
  352.             public Stream<LineParser> allowedNext() {
  353.                 return Stream.of(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
  354.             }

  355.         },

  356.         /** Parser for time system. */
  357.         HEADER_TIME_SYSTEM("^%c.*") {

  358.             /** {@inheritDoc} */
  359.             @Override
  360.             public void parse(final String line, final ParseInfo pi) {

  361.                 if (pi.file.getType() == null) {
  362.                     // this the first custom fields line, the only one really used
  363.                     pi.file.setType(getFileType(line.substring(3, 5).trim()));

  364.                     // now identify the time system in use
  365.                     final String tsStr = line.substring(9, 12).trim();
  366.                     pi.file.setTimeScaleString(tsStr);
  367.                     final TimeSystem ts;
  368.                     if (tsStr.equalsIgnoreCase("ccc")) {
  369.                         ts = TimeSystem.GPS;
  370.                     } else {
  371.                         ts = TimeSystem.valueOf(tsStr);
  372.                     }
  373.                     pi.file.setTimeSystem(ts);

  374.                     switch (ts) {
  375.                         case GPS:
  376.                             pi.timeScale = TimeScalesFactory.getGPS();
  377.                             break;

  378.                         case GAL:
  379.                             pi.timeScale = TimeScalesFactory.getGST();
  380.                             break;

  381.                         case GLO:
  382.                             pi.timeScale = TimeScalesFactory.getGLONASS();
  383.                             break;

  384.                         case QZS:
  385.                             pi.timeScale = TimeScalesFactory.getQZSS();
  386.                             break;

  387.                         case TAI:
  388.                             pi.timeScale = TimeScalesFactory.getTAI();
  389.                             break;

  390.                         case UTC:
  391.                             pi.timeScale = TimeScalesFactory.getUTC();
  392.                             break;

  393.                         default:
  394.                             pi.timeScale = TimeScalesFactory.getGPS();
  395.                             break;
  396.                     }
  397.                     pi.file.setTimeScale(pi.timeScale);
  398.                 }

  399.             }

  400.             /** {@inheritDoc} */
  401.             @Override
  402.             public Stream<LineParser> allowedNext() {
  403.                 return Stream.of(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
  404.             }

  405.         },

  406.         /** Parser for standard deviations of position/velocity/clock components. */
  407.         HEADER_STANDARD_DEVIATIONS("^%f.*") {

  408.             /** {@inheritDoc} */
  409.             @Override
  410.             public void parse(final String line, final ParseInfo pi) {
  411.                 // String base = line.substring(3, 13).trim();
  412.                 // if (!base.equals("0.0000000")) {
  413.                 //    // (mm or 10**-4 mm/sec)
  414.                 //    pi.posVelBase = Double.valueOf(base);
  415.                 // }

  416.                 // base = line.substring(14, 26).trim();
  417.                 // if (!base.equals("0.000000000")) {
  418.                 //    // (psec or 10**-4 psec/sec)
  419.                 //    pi.clockBase = Double.valueOf(base);
  420.                 // }
  421.             }

  422.             /** {@inheritDoc} */
  423.             @Override
  424.             public Stream<LineParser> allowedNext() {
  425.                 return Stream.of(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
  426.             }

  427.         },

  428.         /** Parser for custom parameters. */
  429.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

  430.             /** {@inheritDoc} */
  431.             @Override
  432.             public void parse(final String line, final ParseInfo pi) {
  433.                 // ignore additional custom parameters
  434.             }

  435.             /** {@inheritDoc} */
  436.             @Override
  437.             public Stream<LineParser> allowedNext() {
  438.                 return Stream.of(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  439.             }

  440.         },

  441.         /** Parser for comments. */
  442.         HEADER_COMMENTS("^/\\*.*") {

  443.             /** {@inheritDoc} */
  444.             @Override
  445.             public void parse(final String line, final ParseInfo pi) {
  446.                 // ignore comments
  447.             }

  448.             /** {@inheritDoc} */
  449.             @Override
  450.             public Stream<LineParser> allowedNext() {
  451.                 return Stream.of(HEADER_COMMENTS, DATA_EPOCH);
  452.             }

  453.         },

  454.         /** Parser for epoch. */
  455.         DATA_EPOCH("^\\* .*") {

  456.             /** {@inheritDoc} */
  457.             @Override
  458.             public void parse(final String line, final ParseInfo pi) {
  459.                 final int    year   = Integer.parseInt(line.substring(3, 7).trim());
  460.                 final int    month  = Integer.parseInt(line.substring(8, 10).trim());
  461.                 final int    day    = Integer.parseInt(line.substring(11, 13).trim());
  462.                 final int    hour   = Integer.parseInt(line.substring(14, 16).trim());
  463.                 final int    minute = Integer.parseInt(line.substring(17, 19).trim());
  464.                 final double second = Double.parseDouble(line.substring(20, 31).trim());

  465.                 pi.latestEpoch = new AbsoluteDate(year, month, day,
  466.                                                   hour, minute, second,
  467.                                                   pi.timeScale);
  468.                 pi.nbEpochs++;
  469.             }

  470.             /** {@inheritDoc} */
  471.             @Override
  472.             public Stream<LineParser> allowedNext() {
  473.                 return Stream.of(DATA_POSITION);
  474.             }

  475.         },

  476.         /** Parser for position. */
  477.         DATA_POSITION("^P.*") {

  478.             /** {@inheritDoc} */
  479.             @Override
  480.             public void parse(final String line, final ParseInfo pi) {
  481.                 final String satelliteId = line.substring(1, 4).trim();

  482.                 if (!pi.file.containsSatellite(satelliteId)) {
  483.                     pi.latestPosition = null;
  484.                 } else {
  485.                     final double x = Double.parseDouble(line.substring(4, 18).trim());
  486.                     final double y = Double.parseDouble(line.substring(18, 32).trim());
  487.                     final double z = Double.parseDouble(line.substring(32, 46).trim());

  488.                     // the position values are in km and have to be converted to m
  489.                     pi.latestPosition = new Vector3D(x * 1000, y * 1000, z * 1000);

  490.                     // clock (microsec)
  491.                     pi.latestClock =
  492.                             Double.parseDouble(line.substring(46, 60).trim()) * 1e6;

  493.                     // the additional items are optional and not read yet

  494.                     // if (line.length() >= 73) {
  495.                     // // x-sdev (b**n mm)
  496.                     // int xStdDevExp = Integer.valueOf(line.substring(61,
  497.                     // 63).trim());
  498.                     // // y-sdev (b**n mm)
  499.                     // int yStdDevExp = Integer.valueOf(line.substring(64,
  500.                     // 66).trim());
  501.                     // // z-sdev (b**n mm)
  502.                     // int zStdDevExp = Integer.valueOf(line.substring(67,
  503.                     // 69).trim());
  504.                     // // c-sdev (b**n psec)
  505.                     // int cStdDevExp = Integer.valueOf(line.substring(70,
  506.                     // 73).trim());
  507.                     //
  508.                     // pi.posStdDevRecord =
  509.                     // new PositionStdDevRecord(FastMath.pow(pi.posVelBase, xStdDevExp),
  510.                     // FastMath.pow(pi.posVelBase,
  511.                     // yStdDevExp), FastMath.pow(pi.posVelBase, zStdDevExp),
  512.                     // FastMath.pow(pi.clockBase, cStdDevExp));
  513.                     //
  514.                     // String clockEventFlag = line.substring(74, 75);
  515.                     // String clockPredFlag = line.substring(75, 76);
  516.                     // String maneuverFlag = line.substring(78, 79);
  517.                     // String orbitPredFlag = line.substring(79, 80);
  518.                     // }

  519.                     if (!pi.hasVelocityEntries) {
  520.                         final SP3Coordinate coord =
  521.                                 new SP3Coordinate(pi.latestEpoch,
  522.                                                   pi.latestPosition,
  523.                                                   pi.latestClock);
  524.                         pi.file.addSatelliteCoordinate(satelliteId, coord);
  525.                     }
  526.                 }
  527.             }

  528.             /** {@inheritDoc} */
  529.             @Override
  530.             public Stream<LineParser> allowedNext() {
  531.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
  532.             }

  533.         },

  534.         /** Parser for position correlation. */
  535.         DATA_POSITION_CORRELATION("^EP.*") {

  536.             /** {@inheritDoc} */
  537.             @Override
  538.             public void parse(final String line, final ParseInfo pi) {
  539.                 // ignored for now
  540.             }

  541.             /** {@inheritDoc} */
  542.             @Override
  543.             public Stream<LineParser> allowedNext() {
  544.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
  545.             }

  546.         },

  547.         /** Parser for velocity. */
  548.         DATA_VELOCITY("^V.*") {

  549.             /** {@inheritDoc} */
  550.             @Override
  551.             public void parse(final String line, final ParseInfo pi) {
  552.                 final String satelliteId = line.substring(1, 4).trim();

  553.                 if (pi.file.containsSatellite(satelliteId)) {
  554.                     final double xv = Double.parseDouble(line.substring(4, 18).trim());
  555.                     final double yv = Double.parseDouble(line.substring(18, 32).trim());
  556.                     final double zv = Double.parseDouble(line.substring(32, 46).trim());

  557.                     // the velocity values are in dm/s and have to be converted to m/s
  558.                     final Vector3D velocity = new Vector3D(xv / 10d, yv / 10d, zv / 10d);

  559.                     // clock rate in file is 1e-4 us / s
  560.                     final double clockRateChange =
  561.                             Double.parseDouble(line.substring(46, 60).trim()) * 1e10;

  562.                     // the additional items are optional and not read yet

  563.                     // if (line.length() >= 73) {
  564.                     // // xvel-sdev (b**n 10**-4 mm/sec)
  565.                     // int xVstdDevExp = Integer.valueOf(line.substring(61,
  566.                     // 63).trim());
  567.                     // // yvel-sdev (b**n 10**-4 mm/sec)
  568.                     // int yVstdDevExp = Integer.valueOf(line.substring(64,
  569.                     // 66).trim());
  570.                     // // zvel-sdev (b**n 10**-4 mm/sec)
  571.                     // int zVstdDevExp = Integer.valueOf(line.substring(67,
  572.                     // 69).trim());
  573.                     // // clkrate-sdev (b**n 10**-4 psec/sec)
  574.                     // int clkStdDevExp = Integer.valueOf(line.substring(70,
  575.                     // 73).trim());
  576.                     // }

  577.                     final SP3Coordinate coord =
  578.                             new SP3Coordinate(pi.latestEpoch,
  579.                                               pi.latestPosition,
  580.                                               velocity,
  581.                                               pi.latestClock,
  582.                                               clockRateChange);
  583.                     pi.file.addSatelliteCoordinate(satelliteId, coord);
  584.                 }
  585.             }

  586.             /** {@inheritDoc} */
  587.             @Override
  588.             public Stream<LineParser> allowedNext() {
  589.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  590.             }

  591.         },

  592.         /** Parser for velocity correlation. */
  593.         DATA_VELOCITY_CORRELATION("^EV.*") {

  594.             /** {@inheritDoc} */
  595.             @Override
  596.             public void parse(final String line, final ParseInfo pi) {
  597.                 // ignored for now
  598.             }

  599.             /** {@inheritDoc} */
  600.             @Override
  601.             public Stream<LineParser> allowedNext() {
  602.                 return Stream.of(DATA_EPOCH, DATA_POSITION, EOF);
  603.             }

  604.         },

  605.         /** Parser for End Of File marker. */
  606.         EOF("^[eE][oO][fF]\\s*$") {

  607.             /** {@inheritDoc} */
  608.             @Override
  609.             public void parse(final String line, final ParseInfo pi) {
  610.                 pi.done = true;
  611.             }

  612.             /** {@inheritDoc} */
  613.             @Override
  614.             public Stream<LineParser> allowedNext() {
  615.                 return Stream.of(EOF);
  616.             }

  617.         };

  618.         /** Pattern for identifying line. */
  619.         private final Pattern pattern;

  620.         /** Simple constructor.
  621.          * @param lineRegexp regular expression for identifying line
  622.          */
  623.         LineParser(final String lineRegexp) {
  624.             pattern = Pattern.compile(lineRegexp);
  625.         }

  626.         /** Parse a line.
  627.          * @param line line to parse
  628.          * @param pi holder for transient data
  629.          */
  630.         public abstract void parse(String line, ParseInfo pi);

  631.         /** Get the allowed parsers for next line.
  632.          * @return allowed parsers for next line
  633.          */
  634.         public abstract Stream<LineParser> allowedNext();

  635.         /** Check if parser can handle line.
  636.          * @param line line to parse
  637.          * @return true if parser can handle the specified line
  638.          */
  639.         public boolean canHandle(final String line) {
  640.             return pattern.matcher(line).matches();
  641.         }

  642.     }

  643. }