SP3Parser.java

  1. /* Copyright 2002-2012 Space Applications Services
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.sp3;

  18. 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.annotation.DefaultDataContext;
  33. import org.orekit.data.DataContext;
  34. import org.orekit.errors.OrekitException;
  35. import org.orekit.errors.OrekitMessages;
  36. import org.orekit.files.general.EphemerisFileParser;
  37. import org.orekit.files.sp3.SP3File.SP3Coordinate;
  38. import org.orekit.files.sp3.SP3File.SP3FileType;
  39. import org.orekit.files.sp3.SP3File.TimeSystem;
  40. import org.orekit.frames.Frame;
  41. import org.orekit.time.AbsoluteDate;
  42. import org.orekit.time.TimeScale;
  43. import org.orekit.time.TimeScales;
  44. import org.orekit.utils.CartesianDerivativesFilter;
  45. import org.orekit.utils.Constants;
  46. import org.orekit.utils.IERSConventions;

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

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

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

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

  72.     /**
  73.      * Create an SP3 parser using default values.
  74.      *
  75.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  76.      *
  77.      * @see #SP3Parser(double, int, Function)
  78.      */
  79.     @DefaultDataContext
  80.     public SP3Parser() {
  81.         this(Constants.EIGEN5C_EARTH_MU, 7, SP3Parser::guessFrame);
  82.     }

  83.     /**
  84.      * Create an SP3 parser and specify the extra information needed to create a {@link
  85.      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
  86.      *
  87.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  88.      *
  89.      * @param mu                   is the standard gravitational parameter to use for
  90.      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
  91.      *                             the ephemeris data. See {@link Constants}.
  92.      * @param interpolationSamples is the number of samples to use when interpolating.
  93.      * @param frameBuilder         is a function that can construct a frame from an SP3
  94.      *                             coordinate system string. The coordinate system can be
  95.      *                             any 5 character string e.g. ITR92, IGb08.
  96.      * @see #SP3Parser(double, int, Function, TimeScales)
  97.      */
  98.     @DefaultDataContext
  99.     public SP3Parser(final double mu,
  100.                      final int interpolationSamples,
  101.                      final Function<? super String, ? extends Frame> frameBuilder) {
  102.         this(mu, interpolationSamples, frameBuilder,
  103.                 DataContext.getDefault().getTimeScales());
  104.     }

  105.     /**
  106.      * Create an SP3 parser and specify the extra information needed to create a {@link
  107.      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
  108.      *
  109.      * @param mu                   is the standard gravitational parameter to use for
  110.      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
  111.      *                             the ephemeris data. See {@link Constants}.
  112.      * @param interpolationSamples is the number of samples to use when interpolating.
  113.      * @param frameBuilder         is a function that can construct a frame from an SP3
  114.      *                             coordinate system string. The coordinate system can be
  115.      * @param timeScales           the set of time scales used for parsing dates.
  116.      * @since 10.1
  117.      */
  118.     public SP3Parser(final double mu,
  119.                      final int interpolationSamples,
  120.                      final Function<? super String, ? extends Frame> frameBuilder,
  121.                      final TimeScales timeScales) {
  122.         this.mu = mu;
  123.         this.interpolationSamples = interpolationSamples;
  124.         this.frameBuilder = frameBuilder;
  125.         this.timeScales = timeScales;
  126.     }

  127.     /**
  128.      * Default string to {@link Frame} conversion for {@link #SP3Parser()}.
  129.      *
  130.      * <p>This method uses the {@link DataContext#getDefault() default data context}.
  131.      *
  132.      * @param name of the frame.
  133.      * @return ITRF based on 2010 conventions,
  134.      * with tidal effects considered during EOP interpolation.
  135.      */
  136.     @DefaultDataContext
  137.     private static Frame guessFrame(final String name) {
  138.         return DataContext.getDefault().getFrames()
  139.                 .getITRF(IERSConventions.IERS_2010, false);
  140.     }

  141.     /**
  142.      * Parse a SP3 file from an input stream using the UTF-8 charset.
  143.      *
  144.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  145.      * method may read more data than necessary from {@code stream} and the additional
  146.      * data will be lost. The other parse methods do not have this issue.
  147.      *
  148.      * @param stream to read the SP3 file from.
  149.      * @return a parsed SP3 file.
  150.      * @throws IOException     if {@code stream} throws one.
  151.      * @see #parse(String)
  152.      * @see #parse(BufferedReader, String)
  153.      */
  154.     public SP3File parse(final InputStream stream) throws IOException {
  155.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
  156.             return parse(reader, stream.toString());
  157.         }
  158.     }

  159.     @Override
  160.     public SP3File parse(final String fileName) throws IOException, OrekitException {
  161.         try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
  162.                                                              StandardCharsets.UTF_8)) {
  163.             return parse(reader, fileName);
  164.         }
  165.     }

  166.     @Override
  167.     public SP3File parse(final BufferedReader reader,
  168.                          final String fileName) throws IOException {

  169.         // initialize internal data structures
  170.         final ParseInfo pi = new ParseInfo();

  171.         int lineNumber = 0;
  172.         Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
  173.         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  174.             ++lineNumber;
  175.             final String l = line;
  176.             final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
  177.             if (selected.isPresent()) {
  178.                 try {
  179.                     selected.get().parse(line, pi);
  180.                 } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  181.                     throw new OrekitException(e,
  182.                                               OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  183.                                               lineNumber, fileName, line);
  184.                 }
  185.                 candidateParsers = selected.get().allowedNext();
  186.             } else {
  187.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  188.                                           lineNumber, fileName, line);
  189.             }
  190.             if (pi.done) {
  191.                 if (pi.nbEpochs != pi.file.getNumberOfEpochs()) {
  192.                     throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
  193.                                               pi.nbEpochs, fileName, pi.file.getNumberOfEpochs());
  194.                 }
  195.                 return pi.file;
  196.             }
  197.         }

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

  200.     }

  201.     /** Returns the {@link SP3FileType} that corresponds to a given string in a SP3 file.
  202.      * @param fileType file type as string
  203.      * @return file type as enum
  204.      */
  205.     private static SP3FileType getFileType(final String fileType) {
  206.         SP3FileType type = SP3FileType.UNDEFINED;
  207.         if ("G".equalsIgnoreCase(fileType)) {
  208.             type = SP3FileType.GPS;
  209.         } else if ("M".equalsIgnoreCase(fileType)) {
  210.             type = SP3FileType.MIXED;
  211.         } else if ("R".equalsIgnoreCase(fileType)) {
  212.             type = SP3FileType.GLONASS;
  213.         } else if ("L".equalsIgnoreCase(fileType)) {
  214.             type = SP3FileType.LEO;
  215.         } else if ("E".equalsIgnoreCase(fileType)) {
  216.             type = SP3FileType.GALILEO;
  217.         } else if ("C".equalsIgnoreCase(fileType)) {
  218.             type = SP3FileType.COMPASS;
  219.         } else if ("J".equalsIgnoreCase(fileType)) {
  220.             type = SP3FileType.QZSS;
  221.         }
  222.         return type;
  223.     }

  224.     /** Transient data used for parsing a sp3 file. The data is kept in a
  225.      * separate data structure to make the parser thread-safe.
  226.      * <p><b>Note</b>: The class intentionally does not provide accessor
  227.      * methods, as it is only used internally for parsing a SP3 file.</p>
  228.      */
  229.     private class ParseInfo {

  230.         /** Set of time scales for parsing dates. */
  231.         private final TimeScales timeScales;

  232.         /** The corresponding SP3File object. */
  233.         private SP3File file;

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

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

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

  240.         /** Indicates if the SP3 file has velocity entries. */
  241.         private boolean hasVelocityEntries;

  242.         /** The timescale used in the SP3 file. */
  243.         private TimeScale timeScale;

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

  246.         /** The number of satellites accuracies already seen. */
  247.         private int nbAccuracies;

  248.         /** The number of epochs already seen. */
  249.         private int nbEpochs;

  250.         /** End Of File reached indicator. */
  251.         private boolean done;

  252.         /** The base for pos/vel. */
  253.         //private double posVelBase;

  254.         /** The base for clock/rate. */
  255.         //private double clockBase;

  256.         /** Create a new {@link ParseInfo} object. */
  257.         protected ParseInfo() {
  258.             this.timeScales = SP3Parser.this.timeScales;
  259.             file               = new SP3File(mu, interpolationSamples, frameBuilder);
  260.             latestEpoch        = null;
  261.             latestPosition     = null;
  262.             latestClock        = 0.0;
  263.             hasVelocityEntries = false;
  264.             timeScale          = timeScales.getGPS();
  265.             maxSatellites      = 0;
  266.             nbAccuracies       = 0;
  267.             nbEpochs           = 0;
  268.             done               = false;
  269.             //posVelBase = 2d;
  270.             //clockBase = 2d;
  271.         }
  272.     }

  273.     /** Parsers for specific lines. */
  274.     private enum LineParser {

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

  277.             /** {@inheritDoc} */
  278.             @Override
  279.             public void parse(final String line, final ParseInfo pi) {
  280.                 try (Scanner s1      = new Scanner(line);
  281.                      Scanner s2      = s1.useDelimiter(SPACES);
  282.                      Scanner scanner = s2.useLocale(Locale.US)) {
  283.                     scanner.skip("#");
  284.                     final String v = scanner.next();

  285.                     final char version = v.substring(0, 1).toLowerCase().charAt(0);
  286.                     if (version != 'a' && version != 'b' && version != 'c' && version != 'd') {
  287.                         throw new OrekitException(OrekitMessages.SP3_UNSUPPORTED_VERSION, version);
  288.                     }

  289.                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
  290.                     pi.file.setFilter(pi.hasVelocityEntries ?
  291.                                       CartesianDerivativesFilter.USE_PV :
  292.                                       CartesianDerivativesFilter.USE_P);

  293.                     final int    year   = Integer.parseInt(v.substring(2));
  294.                     final int    month  = scanner.nextInt();
  295.                     final int    day    = scanner.nextInt();
  296.                     final int    hour   = scanner.nextInt();
  297.                     final int    minute = scanner.nextInt();
  298.                     final double second = scanner.nextDouble();

  299.                     final AbsoluteDate epoch = new AbsoluteDate(year, month, day,
  300.                                                                 hour, minute, second,
  301.                                                                 pi.timeScales.getGPS());

  302.                     pi.file.setEpoch(epoch);

  303.                     final int numEpochs = scanner.nextInt();
  304.                     pi.file.setNumberOfEpochs(numEpochs);

  305.                     // data used indicator
  306.                     pi.file.setDataUsed(scanner.next());

  307.                     pi.file.setCoordinateSystem(scanner.next());
  308.                     pi.file.setOrbitTypeKey(scanner.next());
  309.                     pi.file.setAgency(scanner.next());
  310.                 }
  311.             }

  312.             /** {@inheritDoc} */
  313.             @Override
  314.             public Stream<LineParser> allowedNext() {
  315.                 return Stream.of(HEADER_DATE_TIME_REFERENCE);
  316.             }

  317.         },

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

  320.             /** {@inheritDoc} */
  321.             @Override
  322.             public void parse(final String line, final ParseInfo pi) {
  323.                 try (Scanner s1      = new Scanner(line);
  324.                      Scanner s2      = s1.useDelimiter(SPACES);
  325.                      Scanner scanner = s2.useLocale(Locale.US)) {
  326.                     scanner.skip("##");

  327.                     // gps week
  328.                     pi.file.setGpsWeek(scanner.nextInt());
  329.                     // seconds of week
  330.                     pi.file.setSecondsOfWeek(scanner.nextDouble());
  331.                     // epoch interval
  332.                     pi.file.setEpochInterval(scanner.nextDouble());
  333.                     // julian day
  334.                     pi.file.setJulianDay(scanner.nextInt());
  335.                     // day fraction
  336.                     pi.file.setDayFraction(scanner.nextDouble());
  337.                 }
  338.             }

  339.             /** {@inheritDoc} */
  340.             @Override
  341.             public Stream<LineParser> allowedNext() {
  342.                 return Stream.of(HEADER_SAT_IDS);
  343.             }

  344.         },

  345.         /** Parser for satellites identifiers. */
  346.         HEADER_SAT_IDS("^\\+ .*") {

  347.             /** {@inheritDoc} */
  348.             @Override
  349.             public void parse(final String line, final ParseInfo pi) {

  350.                 if (pi.maxSatellites == 0) {
  351.                     // this is the first ids line, it also contains the number of satellites
  352.                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
  353.                 }

  354.                 final int lineLength = line.length();
  355.                 int count = pi.file.getSatelliteCount();
  356.                 int startIdx = 9;
  357.                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  358.                     final String satId = line.substring(startIdx, startIdx + 3).trim();
  359.                     if (satId.length() > 0) {
  360.                         pi.file.addSatellite(satId);
  361.                     }
  362.                     startIdx += 3;
  363.                 }
  364.             }

  365.             /** {@inheritDoc} */
  366.             @Override
  367.             public Stream<LineParser> allowedNext() {
  368.                 return Stream.of(HEADER_SAT_IDS, HEADER_ACCURACY);
  369.             }

  370.         },

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

  373.             /** {@inheritDoc} */
  374.             @Override
  375.             public void parse(final String line, final ParseInfo pi) {
  376.                 final int lineLength = line.length();
  377.                 int startIdx = 9;
  378.                 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  379.                     final String sub = line.substring(startIdx, startIdx + 3).trim();
  380.                     if (sub.length() > 0) {
  381.                         final int exponent = Integer.parseInt(sub);
  382.                         // the accuracy is calculated as 2**exp (in mm)
  383.                         pi.file.setAccuracy(pi.nbAccuracies++, (2 << exponent) * MILLIMETER);
  384.                     }
  385.                     startIdx += 3;
  386.                 }
  387.             }

  388.             /** {@inheritDoc} */
  389.             @Override
  390.             public Stream<LineParser> allowedNext() {
  391.                 return Stream.of(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
  392.             }

  393.         },

  394.         /** Parser for time system. */
  395.         HEADER_TIME_SYSTEM("^%c.*") {

  396.             /** {@inheritDoc} */
  397.             @Override
  398.             public void parse(final String line, final ParseInfo pi) {

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

  402.                     // now identify the time system in use
  403.                     final String tsStr = line.substring(9, 12).trim();
  404.                     pi.file.setTimeScaleString(tsStr);
  405.                     final TimeSystem ts;
  406.                     if (tsStr.equalsIgnoreCase("ccc")) {
  407.                         ts = TimeSystem.GPS;
  408.                     } else {
  409.                         ts = TimeSystem.valueOf(tsStr);
  410.                     }
  411.                     pi.file.setTimeSystem(ts);

  412.                     switch (ts) {
  413.                         case GPS:
  414.                             pi.timeScale = pi.timeScales.getGPS();
  415.                             break;

  416.                         case GAL:
  417.                             pi.timeScale = pi.timeScales.getGST();
  418.                             break;

  419.                         case GLO:
  420.                             pi.timeScale = pi.timeScales.getGLONASS();
  421.                             break;

  422.                         case QZS:
  423.                             pi.timeScale = pi.timeScales.getQZSS();
  424.                             break;

  425.                         case TAI:
  426.                             pi.timeScale = pi.timeScales.getTAI();
  427.                             break;

  428.                         case UTC:
  429.                             pi.timeScale = pi.timeScales.getUTC();
  430.                             break;

  431.                         default:
  432.                             pi.timeScale = pi.timeScales.getGPS();
  433.                             break;
  434.                     }
  435.                     pi.file.setTimeScale(pi.timeScale);
  436.                 }

  437.             }

  438.             /** {@inheritDoc} */
  439.             @Override
  440.             public Stream<LineParser> allowedNext() {
  441.                 return Stream.of(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
  442.             }

  443.         },

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

  446.             /** {@inheritDoc} */
  447.             @Override
  448.             public void parse(final String line, final ParseInfo pi) {
  449.                 // String base = line.substring(3, 13).trim();
  450.                 // if (!base.equals("0.0000000")) {
  451.                 //    // (mm or 10**-4 mm/sec)
  452.                 //    pi.posVelBase = Double.valueOf(base);
  453.                 // }

  454.                 // base = line.substring(14, 26).trim();
  455.                 // if (!base.equals("0.000000000")) {
  456.                 //    // (psec or 10**-4 psec/sec)
  457.                 //    pi.clockBase = Double.valueOf(base);
  458.                 // }
  459.             }

  460.             /** {@inheritDoc} */
  461.             @Override
  462.             public Stream<LineParser> allowedNext() {
  463.                 return Stream.of(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
  464.             }

  465.         },

  466.         /** Parser for custom parameters. */
  467.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

  468.             /** {@inheritDoc} */
  469.             @Override
  470.             public void parse(final String line, final ParseInfo pi) {
  471.                 // ignore additional custom parameters
  472.             }

  473.             /** {@inheritDoc} */
  474.             @Override
  475.             public Stream<LineParser> allowedNext() {
  476.                 return Stream.of(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  477.             }

  478.         },

  479.         /** Parser for comments. */
  480.         HEADER_COMMENTS("^/\\*.*") {

  481.             /** {@inheritDoc} */
  482.             @Override
  483.             public void parse(final String line, final ParseInfo pi) {
  484.                 // ignore comments
  485.             }

  486.             /** {@inheritDoc} */
  487.             @Override
  488.             public Stream<LineParser> allowedNext() {
  489.                 return Stream.of(HEADER_COMMENTS, DATA_EPOCH);
  490.             }

  491.         },

  492.         /** Parser for epoch. */
  493.         DATA_EPOCH("^\\* .*") {

  494.             /** {@inheritDoc} */
  495.             @Override
  496.             public void parse(final String line, final ParseInfo pi) {
  497.                 final int    year   = Integer.parseInt(line.substring(3, 7).trim());
  498.                 final int    month  = Integer.parseInt(line.substring(8, 10).trim());
  499.                 final int    day    = Integer.parseInt(line.substring(11, 13).trim());
  500.                 final int    hour   = Integer.parseInt(line.substring(14, 16).trim());
  501.                 final int    minute = Integer.parseInt(line.substring(17, 19).trim());
  502.                 final double second = Double.parseDouble(line.substring(20, 31).trim());

  503.                 pi.latestEpoch = new AbsoluteDate(year, month, day,
  504.                                                   hour, minute, second,
  505.                                                   pi.timeScale);
  506.                 pi.nbEpochs++;
  507.             }

  508.             /** {@inheritDoc} */
  509.             @Override
  510.             public Stream<LineParser> allowedNext() {
  511.                 return Stream.of(DATA_POSITION);
  512.             }

  513.         },

  514.         /** Parser for position. */
  515.         DATA_POSITION("^P.*") {

  516.             /** {@inheritDoc} */
  517.             @Override
  518.             public void parse(final String line, final ParseInfo pi) {
  519.                 final String satelliteId = line.substring(1, 4).trim();

  520.                 if (!pi.file.containsSatellite(satelliteId)) {
  521.                     pi.latestPosition = null;
  522.                 } else {
  523.                     final double x = Double.parseDouble(line.substring(4, 18).trim());
  524.                     final double y = Double.parseDouble(line.substring(18, 32).trim());
  525.                     final double z = Double.parseDouble(line.substring(32, 46).trim());

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

  528.                     // clock (microsec)
  529.                     pi.latestClock =
  530.                             Double.parseDouble(line.substring(46, 60).trim()) * 1e-6;

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

  532.                     // if (line.length() >= 73) {
  533.                     // // x-sdev (b**n mm)
  534.                     // int xStdDevExp = Integer.valueOf(line.substring(61,
  535.                     // 63).trim());
  536.                     // // y-sdev (b**n mm)
  537.                     // int yStdDevExp = Integer.valueOf(line.substring(64,
  538.                     // 66).trim());
  539.                     // // z-sdev (b**n mm)
  540.                     // int zStdDevExp = Integer.valueOf(line.substring(67,
  541.                     // 69).trim());
  542.                     // // c-sdev (b**n psec)
  543.                     // int cStdDevExp = Integer.valueOf(line.substring(70,
  544.                     // 73).trim());
  545.                     //
  546.                     // pi.posStdDevRecord =
  547.                     // new PositionStdDevRecord(FastMath.pow(pi.posVelBase, xStdDevExp),
  548.                     // FastMath.pow(pi.posVelBase,
  549.                     // yStdDevExp), FastMath.pow(pi.posVelBase, zStdDevExp),
  550.                     // FastMath.pow(pi.clockBase, cStdDevExp));
  551.                     //
  552.                     // String clockEventFlag = line.substring(74, 75);
  553.                     // String clockPredFlag = line.substring(75, 76);
  554.                     // String maneuverFlag = line.substring(78, 79);
  555.                     // String orbitPredFlag = line.substring(79, 80);
  556.                     // }

  557.                     if (!pi.hasVelocityEntries) {
  558.                         final SP3Coordinate coord =
  559.                                 new SP3Coordinate(pi.latestEpoch,
  560.                                                   pi.latestPosition,
  561.                                                   pi.latestClock);
  562.                         pi.file.addSatelliteCoordinate(satelliteId, coord);
  563.                     }
  564.                 }
  565.             }

  566.             /** {@inheritDoc} */
  567.             @Override
  568.             public Stream<LineParser> allowedNext() {
  569.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
  570.             }

  571.         },

  572.         /** Parser for position correlation. */
  573.         DATA_POSITION_CORRELATION("^EP.*") {

  574.             /** {@inheritDoc} */
  575.             @Override
  576.             public void parse(final String line, final ParseInfo pi) {
  577.                 // ignored for now
  578.             }

  579.             /** {@inheritDoc} */
  580.             @Override
  581.             public Stream<LineParser> allowedNext() {
  582.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
  583.             }

  584.         },

  585.         /** Parser for velocity. */
  586.         DATA_VELOCITY("^V.*") {

  587.             /** {@inheritDoc} */
  588.             @Override
  589.             public void parse(final String line, final ParseInfo pi) {
  590.                 final String satelliteId = line.substring(1, 4).trim();

  591.                 if (pi.file.containsSatellite(satelliteId)) {
  592.                     final double xv = Double.parseDouble(line.substring(4, 18).trim());
  593.                     final double yv = Double.parseDouble(line.substring(18, 32).trim());
  594.                     final double zv = Double.parseDouble(line.substring(32, 46).trim());

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

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

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

  601.                     // if (line.length() >= 73) {
  602.                     // // xvel-sdev (b**n 10**-4 mm/sec)
  603.                     // int xVstdDevExp = Integer.valueOf(line.substring(61,
  604.                     // 63).trim());
  605.                     // // yvel-sdev (b**n 10**-4 mm/sec)
  606.                     // int yVstdDevExp = Integer.valueOf(line.substring(64,
  607.                     // 66).trim());
  608.                     // // zvel-sdev (b**n 10**-4 mm/sec)
  609.                     // int zVstdDevExp = Integer.valueOf(line.substring(67,
  610.                     // 69).trim());
  611.                     // // clkrate-sdev (b**n 10**-4 psec/sec)
  612.                     // int clkStdDevExp = Integer.valueOf(line.substring(70,
  613.                     // 73).trim());
  614.                     // }

  615.                     final SP3Coordinate coord =
  616.                             new SP3Coordinate(pi.latestEpoch,
  617.                                               pi.latestPosition,
  618.                                               velocity,
  619.                                               pi.latestClock,
  620.                                               clockRateChange);
  621.                     pi.file.addSatelliteCoordinate(satelliteId, coord);
  622.                 }
  623.             }

  624.             /** {@inheritDoc} */
  625.             @Override
  626.             public Stream<LineParser> allowedNext() {
  627.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  628.             }

  629.         },

  630.         /** Parser for velocity correlation. */
  631.         DATA_VELOCITY_CORRELATION("^EV.*") {

  632.             /** {@inheritDoc} */
  633.             @Override
  634.             public void parse(final String line, final ParseInfo pi) {
  635.                 // ignored for now
  636.             }

  637.             /** {@inheritDoc} */
  638.             @Override
  639.             public Stream<LineParser> allowedNext() {
  640.                 return Stream.of(DATA_EPOCH, DATA_POSITION, EOF);
  641.             }

  642.         },

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

  645.             /** {@inheritDoc} */
  646.             @Override
  647.             public void parse(final String line, final ParseInfo pi) {
  648.                 pi.done = true;
  649.             }

  650.             /** {@inheritDoc} */
  651.             @Override
  652.             public Stream<LineParser> allowedNext() {
  653.                 return Stream.of(EOF);
  654.             }

  655.         };

  656.         /** Pattern for identifying line. */
  657.         private final Pattern pattern;

  658.         /** Simple constructor.
  659.          * @param lineRegexp regular expression for identifying line
  660.          */
  661.         LineParser(final String lineRegexp) {
  662.             pattern = Pattern.compile(lineRegexp);
  663.         }

  664.         /** Parse a line.
  665.          * @param line line to parse
  666.          * @param pi holder for transient data
  667.          */
  668.         public abstract void parse(String line, ParseInfo pi);

  669.         /** Get the allowed parsers for next line.
  670.          * @return allowed parsers for next line
  671.          */
  672.         public abstract Stream<LineParser> allowedNext();

  673.         /** Check if parser can handle line.
  674.          * @param line line to parse
  675.          * @return true if parser can handle the specified line
  676.          */
  677.         public boolean canHandle(final String line) {
  678.             return pattern.matcher(line).matches();
  679.         }

  680.     }

  681. }