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.Reader;
  21. import java.util.ArrayList;
  22. import java.util.List;
  23. import java.util.Locale;
  24. import java.util.Optional;
  25. import java.util.Scanner;
  26. import java.util.function.Function;
  27. import java.util.regex.Pattern;
  28. import java.util.stream.Stream;

  29. import org.hipparchus.exception.LocalizedCoreFormats;
  30. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  31. import org.hipparchus.util.FastMath;
  32. import org.orekit.annotation.DefaultDataContext;
  33. import org.orekit.data.DataContext;
  34. import org.orekit.data.DataSource;
  35. import org.orekit.errors.OrekitException;
  36. import org.orekit.errors.OrekitIllegalArgumentException;
  37. import org.orekit.errors.OrekitMessages;
  38. import org.orekit.files.general.EphemerisFileParser;
  39. import org.orekit.files.sp3.SP3.SP3Coordinate;
  40. import org.orekit.files.sp3.SP3.SP3FileType;
  41. import org.orekit.frames.Frame;
  42. import org.orekit.gnss.TimeSystem;
  43. import org.orekit.time.AbsoluteDate;
  44. import org.orekit.time.DateComponents;
  45. import org.orekit.time.DateTimeComponents;
  46. import org.orekit.time.TimeComponents;
  47. import org.orekit.time.TimeScale;
  48. import org.orekit.time.TimeScales;
  49. import org.orekit.utils.CartesianDerivativesFilter;
  50. import org.orekit.utils.Constants;
  51. import org.orekit.utils.IERSConventions;

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

  65.     /** Bad or absent clock values are to be set to 999999.999999. */
  66.     public static final double DEFAULT_CLOCK_VALUE = 999999.999999;

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

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

  71.     /** Standard gravitational parameter in m^3 / s^2. */
  72.     private final double mu;
  73.     /** Number of data points to use in interpolation. */
  74.     private final int interpolationSamples;
  75.     /** Mapping from frame identifier in the file to a {@link Frame}. */
  76.     private final Function<? super String, ? extends Frame> frameBuilder;
  77.     /** Set of time scales. */
  78.     private final TimeScales timeScales;

  79.     /**
  80.      * Create an SP3 parser using default values.
  81.      *
  82.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  83.      *
  84.      * @see #SP3Parser(double, int, Function)
  85.      */
  86.     @DefaultDataContext
  87.     public SP3Parser() {
  88.         this(Constants.EIGEN5C_EARTH_MU, 7, SP3Parser::guessFrame);
  89.     }

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

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

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

  148.     @Override
  149.     public SP3 parse(final DataSource source) {

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

  152.             if (br == null) {
  153.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  154.             }

  155.             // initialize internal data structures
  156.             final ParseInfo pi = new ParseInfo();

  157.             int lineNumber = 0;
  158.             Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
  159.             for (String line = br.readLine(); line != null; line = br.readLine()) {
  160.                 ++lineNumber;
  161.                 final String l = line;
  162.                 final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
  163.                 if (selected.isPresent()) {
  164.                     try {
  165.                         selected.get().parse(line, pi);
  166.                     } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  167.                         throw new OrekitException(e,
  168.                                                   OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  169.                                                   lineNumber, source.getName(), line);
  170.                     }
  171.                     candidateParsers = selected.get().allowedNext();
  172.                 } else {
  173.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  174.                                               lineNumber, source.getName(), line);
  175.                 }
  176.                 if (pi.done) {
  177.                     if (pi.nbEpochs != pi.file.getNumberOfEpochs()) {
  178.                         throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
  179.                                                   pi.nbEpochs, source.getName(), pi.file.getNumberOfEpochs());
  180.                     }
  181.                     return pi.file;
  182.                 }
  183.             }

  184.             // Sometimes, the "EOF" key is not available in the file
  185.             // If the expected number of entries has been read
  186.             // we can suppose that the file has been read properly
  187.             if (pi.nbEpochs == pi.file.getNumberOfEpochs()) {
  188.                 return pi.file;
  189.             }

  190.             // we never reached the EOF marker or number of epochs doesn't correspond to the expected number
  191.             throw new OrekitException(OrekitMessages.SP3_UNEXPECTED_END_OF_FILE, lineNumber);

  192.         } catch (IOException ioe) {
  193.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  194.         }

  195.     }

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

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

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

  231.         /** The corresponding SP3File object. */
  232.         private SP3 file;

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

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

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

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

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

  243.         /** Date and time of the file. */
  244.         private DateTimeComponents epoch;

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

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

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

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

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

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

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

  275.     /** Parsers for specific lines. */
  276.     private enum LineParser {

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

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

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

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

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

  301.                     pi.epoch = new DateTimeComponents(year, month, day,
  302.                                                       hour, minute, second);

  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.                     final TimeSystem ts;
  405.                     if (tsStr.equalsIgnoreCase("ccc")) {
  406.                         ts = TimeSystem.GPS;
  407.                     } else {
  408.                         ts = TimeSystem.valueOf(tsStr);
  409.                     }
  410.                     pi.file.setTimeSystem(ts);
  411.                     pi.timeScale = ts.getTimeScale(pi.timeScales);

  412.                     // now we know the time scale used, we can set the file epoch
  413.                     pi.file.setEpoch(new AbsoluteDate(pi.epoch, pi.timeScale));
  414.                 }

  415.             }

  416.             /** {@inheritDoc} */
  417.             @Override
  418.             public Stream<LineParser> allowedNext() {
  419.                 return Stream.of(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
  420.             }

  421.         },

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

  424.             /** {@inheritDoc} */
  425.             @Override
  426.             public void parse(final String line, final ParseInfo pi) {
  427.                 // String base = line.substring(3, 13).trim();
  428.                 // if (!base.equals("0.0000000")) {
  429.                 //    // (mm or 10**-4 mm/sec)
  430.                 //    pi.posVelBase = Double.valueOf(base);
  431.                 // }

  432.                 // base = line.substring(14, 26).trim();
  433.                 // if (!base.equals("0.000000000")) {
  434.                 //    // (psec or 10**-4 psec/sec)
  435.                 //    pi.clockBase = Double.valueOf(base);
  436.                 // }
  437.             }

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

  443.         },

  444.         /** Parser for custom parameters. */
  445.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

  446.             /** {@inheritDoc} */
  447.             @Override
  448.             public void parse(final String line, final ParseInfo pi) {
  449.                 // ignore additional custom parameters
  450.             }

  451.             /** {@inheritDoc} */
  452.             @Override
  453.             public Stream<LineParser> allowedNext() {
  454.                 return Stream.of(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  455.             }

  456.         },

  457.         /** Parser for comments. */
  458.         HEADER_COMMENTS("^[%]?/\\*.*|") {

  459.             /** {@inheritDoc} */
  460.             @Override
  461.             public void parse(final String line, final ParseInfo pi) {
  462.                 // ignore comments
  463.             }

  464.             /** {@inheritDoc} */
  465.             @Override
  466.             public Stream<LineParser> allowedNext() {
  467.                 return Stream.of(HEADER_COMMENTS, DATA_EPOCH);
  468.             }

  469.         },

  470.         /** Parser for epoch. */
  471.         DATA_EPOCH("^\\* .*") {

  472.             /** {@inheritDoc} */
  473.             @Override
  474.             public void parse(final String line, final ParseInfo pi) {
  475.                 final int    year   = Integer.parseInt(line.substring(3, 7).trim());
  476.                 final int    month  = Integer.parseInt(line.substring(8, 10).trim());
  477.                 final int    day    = Integer.parseInt(line.substring(11, 13).trim());
  478.                 final int    hour   = Integer.parseInt(line.substring(14, 16).trim());
  479.                 final int    minute = Integer.parseInt(line.substring(17, 19).trim());
  480.                 final double second = Double.parseDouble(line.substring(20).trim());

  481.                 // some SP3 files have weird epochs as in the following two examples, where
  482.                 // the middle dates are wrong
  483.                 //
  484.                 // *  2016  7  6 16 58  0.00000000
  485.                 // PL51  11872.234459   3316.551981    101.400098 999999.999999
  486.                 // VL51   8054.606014 -27076.640110 -53372.762255 999999.999999
  487.                 // *  2016  7  6 16 60  0.00000000
  488.                 // PL51  11948.228978   2986.113872   -538.901114 999999.999999
  489.                 // VL51   4605.419303 -27972.588048 -53316.820671 999999.999999
  490.                 // *  2016  7  6 17  2  0.00000000
  491.                 // PL51  11982.652569   2645.786926  -1177.549463 999999.999999
  492.                 // VL51   1128.248622 -28724.293303 -53097.358387 999999.999999
  493.                 //
  494.                 // *  2016  7  6 23 58  0.00000000
  495.                 // PL51   3215.382310  -7958.586164   8812.395707
  496.                 // VL51 -18058.659942 -45834.335707 -34496.540437
  497.                 // *  2016  7  7 24  0  0.00000000
  498.                 // PL51   2989.229334  -8494.421415   8385.068555
  499.                 // VL51 -19617.027447 -43444.824985 -36706.159070
  500.                 // *  2016  7  7  0  2  0.00000000
  501.                 // PL51   2744.983592  -9000.639164   7931.904779
  502.                 // VL51 -21072.925764 -40899.633288 -38801.567078
  503.                 //
  504.                 // In the first case, the date should really be 2016  7  6 17  0  0.00000000,
  505.                 // i.e as the minutes field overflows, the hours field should be incremented
  506.                 // In the second case, the date should really be 2016  7  7  0  0  0.00000000,
  507.                 // i.e. as the hours field overflows, the day field should be kept as is
  508.                 // we cannot be sure how carry was managed when these bogus files were written
  509.                 // so we try different options, incrementing or not previous field, and selecting
  510.                 // the closest one to expected date
  511.                 DateComponents dc = new DateComponents(year, month, day);
  512.                 final List<AbsoluteDate> candidates = new ArrayList<>();
  513.                 int h = hour;
  514.                 int m = minute;
  515.                 double s = second;
  516.                 if (s >= 60.0) {
  517.                     s -= 60;
  518.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  519.                     m++;
  520.                 }
  521.                 if (m > 59) {
  522.                     m = 0;
  523.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  524.                     h++;
  525.                 }
  526.                 if (h > 23) {
  527.                     h = 0;
  528.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  529.                     dc = new DateComponents(dc, 1);
  530.                 }
  531.                 addCandidate(candidates, dc, h, m, s, pi.timeScale);
  532.                 final AbsoluteDate expected = pi.latestEpoch == null ?
  533.                                               pi.file.getEpoch() :
  534.                                                   pi.latestEpoch.shiftedBy(pi.file.getEpochInterval());
  535.                 pi.latestEpoch = null;
  536.                 for (final AbsoluteDate candidate : candidates) {
  537.                     if (FastMath.abs(candidate.durationFrom(expected)) < 0.01 * pi.file.getEpochInterval()) {
  538.                         pi.latestEpoch = candidate;
  539.                     }
  540.                 }
  541.                 if (pi.latestEpoch == null) {
  542.                     // no date recognized, just parse again the initial fields
  543.                     // in order to generate again an exception
  544.                     pi.latestEpoch = new AbsoluteDate(year, month, day, hour, minute, second, pi.timeScale);
  545.                 }
  546.                 pi.nbEpochs++;
  547.             }

  548.             /** Add an epoch candidate to a list.
  549.              * @param candidates list of candidates
  550.              * @param dc date components
  551.              * @param hour hour number from 0 to 23
  552.              * @param minute minute number from 0 to 59
  553.              * @param second second number from 0.0 to 60.0 (excluded)
  554.              * @param timeScale time scale
  555.              * @since 11.1.1
  556.              */
  557.             private void addCandidate(final List<AbsoluteDate> candidates, final DateComponents dc,
  558.                                       final int hour, final int minute, final double second,
  559.                                       final TimeScale timeScale) {
  560.                 try {
  561.                     candidates.add(new AbsoluteDate(dc, new TimeComponents(hour, minute, second), timeScale));
  562.                 } catch (OrekitIllegalArgumentException oiae) {
  563.                     // ignored
  564.                 }
  565.             }

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

  571.         },

  572.         /** Parser for position. */
  573.         DATA_POSITION("^P.*") {

  574.             /** {@inheritDoc} */
  575.             @Override
  576.             public void parse(final String line, final ParseInfo pi) {
  577.                 final String satelliteId = line.substring(1, 4).trim();

  578.                 if (!pi.file.containsSatellite(satelliteId)) {
  579.                     pi.latestPosition = null;
  580.                 } else {
  581.                     final double x = Double.parseDouble(line.substring(4, 18).trim());
  582.                     final double y = Double.parseDouble(line.substring(18, 32).trim());
  583.                     final double z = Double.parseDouble(line.substring(32, 46).trim());

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

  586.                     // clock (microsec)
  587.                     pi.latestClock = line.trim().length() <= 46 ?
  588.                                                           DEFAULT_CLOCK_VALUE :
  589.                                                               Double.parseDouble(line.substring(46, 60).trim()) * 1e-6;

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

  591.                     // if (line.length() >= 73) {
  592.                     // // x-sdev (b**n mm)
  593.                     // int xStdDevExp = Integer.valueOf(line.substring(61,
  594.                     // 63).trim());
  595.                     // // y-sdev (b**n mm)
  596.                     // int yStdDevExp = Integer.valueOf(line.substring(64,
  597.                     // 66).trim());
  598.                     // // z-sdev (b**n mm)
  599.                     // int zStdDevExp = Integer.valueOf(line.substring(67,
  600.                     // 69).trim());
  601.                     // // c-sdev (b**n psec)
  602.                     // int cStdDevExp = Integer.valueOf(line.substring(70,
  603.                     // 73).trim());
  604.                     //
  605.                     // pi.posStdDevRecord =
  606.                     // new PositionStdDevRecord(FastMath.pow(pi.posVelBase, xStdDevExp),
  607.                     // FastMath.pow(pi.posVelBase,
  608.                     // yStdDevExp), FastMath.pow(pi.posVelBase, zStdDevExp),
  609.                     // FastMath.pow(pi.clockBase, cStdDevExp));
  610.                     //
  611.                     // String clockEventFlag = line.substring(74, 75);
  612.                     // String clockPredFlag = line.substring(75, 76);
  613.                     // String maneuverFlag = line.substring(78, 79);
  614.                     // String orbitPredFlag = line.substring(79, 80);
  615.                     // }

  616.                     if (!pi.hasVelocityEntries) {
  617.                         final SP3Coordinate coord =
  618.                                 new SP3Coordinate(pi.latestEpoch,
  619.                                                   pi.latestPosition,
  620.                                                   pi.latestClock);
  621.                         pi.file.addSatelliteCoordinate(satelliteId, coord);
  622.                     }
  623.                 }
  624.             }

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

  630.         },

  631.         /** Parser for position correlation. */
  632.         DATA_POSITION_CORRELATION("^EP.*") {

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

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

  643.         },

  644.         /** Parser for velocity. */
  645.         DATA_VELOCITY("^V.*") {

  646.             /** {@inheritDoc} */
  647.             @Override
  648.             public void parse(final String line, final ParseInfo pi) {
  649.                 final String satelliteId = line.substring(1, 4).trim();

  650.                 if (pi.file.containsSatellite(satelliteId)) {
  651.                     final double xv = Double.parseDouble(line.substring(4, 18).trim());
  652.                     final double yv = Double.parseDouble(line.substring(18, 32).trim());
  653.                     final double zv = Double.parseDouble(line.substring(32, 46).trim());

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

  656.                     // clock rate in file is 1e-4 us / s
  657.                     final double clockRateChange = line.trim().length() <= 46 ?
  658.                                                                         DEFAULT_CLOCK_VALUE :
  659.                                                                             Double.parseDouble(line.substring(46, 60).trim()) * 1e-4;

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

  661.                     // if (line.length() >= 73) {
  662.                     // // xvel-sdev (b**n 10**-4 mm/sec)
  663.                     // int xVstdDevExp = Integer.valueOf(line.substring(61,
  664.                     // 63).trim());
  665.                     // // yvel-sdev (b**n 10**-4 mm/sec)
  666.                     // int yVstdDevExp = Integer.valueOf(line.substring(64,
  667.                     // 66).trim());
  668.                     // // zvel-sdev (b**n 10**-4 mm/sec)
  669.                     // int zVstdDevExp = Integer.valueOf(line.substring(67,
  670.                     // 69).trim());
  671.                     // // clkrate-sdev (b**n 10**-4 psec/sec)
  672.                     // int clkStdDevExp = Integer.valueOf(line.substring(70,
  673.                     // 73).trim());
  674.                     // }

  675.                     final SP3Coordinate coord =
  676.                             new SP3Coordinate(pi.latestEpoch,
  677.                                               pi.latestPosition,
  678.                                               velocity,
  679.                                               pi.latestClock,
  680.                                               clockRateChange);
  681.                     pi.file.addSatelliteCoordinate(satelliteId, coord);
  682.                 }
  683.             }

  684.             /** {@inheritDoc} */
  685.             @Override
  686.             public Stream<LineParser> allowedNext() {
  687.                 return Stream.of(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  688.             }

  689.         },

  690.         /** Parser for velocity correlation. */
  691.         DATA_VELOCITY_CORRELATION("^EV.*") {

  692.             /** {@inheritDoc} */
  693.             @Override
  694.             public void parse(final String line, final ParseInfo pi) {
  695.                 // ignored for now
  696.             }

  697.             /** {@inheritDoc} */
  698.             @Override
  699.             public Stream<LineParser> allowedNext() {
  700.                 return Stream.of(DATA_EPOCH, DATA_POSITION, EOF);
  701.             }

  702.         },

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

  705.             /** {@inheritDoc} */
  706.             @Override
  707.             public void parse(final String line, final ParseInfo pi) {
  708.                 pi.done = true;
  709.             }

  710.             /** {@inheritDoc} */
  711.             @Override
  712.             public Stream<LineParser> allowedNext() {
  713.                 return Stream.of(EOF);
  714.             }

  715.         };

  716.         /** Pattern for identifying line. */
  717.         private final Pattern pattern;

  718.         /** Simple constructor.
  719.          * @param lineRegexp regular expression for identifying line
  720.          */
  721.         LineParser(final String lineRegexp) {
  722.             pattern = Pattern.compile(lineRegexp);
  723.         }

  724.         /** Parse a line.
  725.          * @param line line to parse
  726.          * @param pi holder for transient data
  727.          */
  728.         public abstract void parse(String line, ParseInfo pi);

  729.         /** Get the allowed parsers for next line.
  730.          * @return allowed parsers for next line
  731.          */
  732.         public abstract Stream<LineParser> allowedNext();

  733.         /** Check if parser can handle line.
  734.          * @param line line to parse
  735.          * @return true if parser can handle the specified line
  736.          */
  737.         public boolean canHandle(final String line) {
  738.             return pattern.matcher(line).matches();
  739.         }

  740.     }

  741. }