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.Arrays;
  23. import java.util.Collections;
  24. import java.util.List;
  25. import java.util.Locale;
  26. import java.util.Scanner;
  27. import java.util.function.Function;
  28. import java.util.regex.Pattern;

  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.frames.Frame;
  40. import org.orekit.frames.ITRFVersion;
  41. import org.orekit.gnss.IGSUtils;
  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="https://files.igs.org/pub/data/format/sp3_docu.txt">SP3-a file format</a>
  59.  * @see <a href="https://files.igs.org/pub/data/format/sp3c.txt">SP3-c file format</a>
  60.  * @see <a href="https://files.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.     /** String representation of the center of ephemeris coordinate system.
  66.      * @deprecated as of 12.1 not used anymore
  67.      */
  68.     @Deprecated
  69.     public static final String SP3_FRAME_CENTER_STRING = "EARTH";

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

  72.     /** Standard gravitational parameter in m³/s². */
  73.     private final double mu;

  74.     /** Number of data points to use in interpolation. */
  75.     private final int interpolationSamples;

  76.     /** Mapping from frame identifier in the file to a {@link Frame}. */
  77.     private final Function<? super String, ? extends Frame> frameBuilder;

  78.     /** Set of time scales. */
  79.     private final TimeScales timeScales;

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

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

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

  137.     /**
  138.      * Default string to {@link Frame} conversion for {@link #SP3Parser()}.
  139.      *
  140.      * <p>
  141.      * This method uses the {@link DataContext#getDefault() default data context}.
  142.      * If the frame names has a form like IGS##, or ITR##, or SLR##, where ##
  143.      * is a two digits number, then this number will be used to build the
  144.      * appropriate {@link ITRFVersion}. Otherwise (for example if name is
  145.      * UNDEF or WGS84), then a default {@link
  146.      * org.orekit.frames.Frames#getITRF(IERSConventions, boolean) ITRF}
  147.      * will be created.
  148.      * </p>
  149.      *
  150.      * @param name of the frame.
  151.      * @return ITRF based on 2010 conventions,
  152.      * with tidal effects considered during EOP interpolation
  153.      * @deprecated as of 12.1, replaced by {@link IGSUtils#guessFrame(String)}
  154.      */
  155.     @Deprecated
  156.     @DefaultDataContext
  157.     public static Frame guessFrame(final String name) {
  158.         return IGSUtils.guessFrame(name);
  159.     }

  160.     @Override
  161.     public SP3 parse(final DataSource source) {

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

  164.             if (br == null) {
  165.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  166.             }

  167.             // initialize internal data structures
  168.             final ParseInfo pi = new ParseInfo(source.getName(), this);

  169.             int lineNumber = 0;
  170.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  171.             nextLine:
  172.                 for (String line = br.readLine(); line != null; line = br.readLine()) {
  173.                     ++lineNumber;
  174.                     for (final LineParser candidate : candidateParsers) {
  175.                         if (candidate.canHandle(line)) {
  176.                             try {
  177.                                 candidate.parse(line, pi);
  178.                                 if (pi.done) {
  179.                                     break nextLine;
  180.                                 }
  181.                                 candidateParsers = candidate.allowedNext();
  182.                                 continue nextLine;
  183.                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  184.                                 throw new OrekitException(e,
  185.                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  186.                                                           lineNumber, pi.fileName, line);
  187.                             }
  188.                         }
  189.                     }

  190.                     // no parsers found for this line
  191.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  192.                                               lineNumber, pi.fileName, line);

  193.                 }

  194.             pi.file.validate(true, pi.fileName);
  195.             return pi.file;

  196.         } catch (IOException ioe) {
  197.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  198.         }

  199.     }

  200.     /** Transient data used for parsing a sp3 file. The data is kept in a
  201.      * separate data structure to make the parser thread-safe.
  202.      * <p><b>Note</b>: The class intentionally does not provide accessor
  203.      * methods, as it is only used internally for parsing a SP3 file.</p>
  204.      */
  205.     private static class ParseInfo {

  206.         /** File name.
  207.          * @since 12.0
  208.          */
  209.         private final String fileName;

  210.         /** Englobing parser. */
  211.         private final SP3Parser parser;

  212.         /** The corresponding SP3File object. */
  213.         private SP3 file;

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

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

  218.         /** The latest position accuracy as read from the SP3 file.
  219.          * @since 12.0
  220.          */
  221.         private Vector3D latestPositionAccuracy;

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

  224.         /** The latest clock value as read from the SP3 file.
  225.          * @since 12.0
  226.          */
  227.         private double latestClockAccuracy;

  228.         /** The latest clock event flag as read from the SP3 file.
  229.          * @since 12.0
  230.          */
  231.         private boolean latestClockEvent;

  232.         /** The latest clock prediction flag as read from the SP3 file.
  233.          * @since 12.0
  234.          */
  235.         private boolean latestClockPrediction;

  236.         /** The latest orbit maneuver event flag as read from the SP3 file.
  237.          * @since 12.0
  238.          */
  239.         private boolean latestOrbitManeuverEvent;

  240.         /** The latest orbit prediction flag as read from the SP3 file.
  241.          * @since 12.0
  242.          */
  243.         private boolean latestOrbitPrediction;

  244.         /** Indicates if the SP3 file has velocity entries. */
  245.         private boolean hasVelocityEntries;

  246.         /** The timescale used in the SP3 file. */
  247.         private TimeScale timeScale;

  248.         /** Date and time of the file. */
  249.         private DateTimeComponents epoch;

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

  252.         /** The number of satellites accuracies already seen. */
  253.         private int nbAccuracies;

  254.         /** End Of File reached indicator. */
  255.         private boolean done;

  256.         /** Create a new {@link ParseInfo} object.
  257.          * @param fileName file name
  258.          * @param parser englobing parser
  259.          */
  260.         protected ParseInfo(final String fileName,
  261.                             final SP3Parser parser) {
  262.             this.fileName      = fileName;
  263.             this.parser        = parser;
  264.             latestEpoch        = null;
  265.             latestPosition     = null;
  266.             latestClock        = 0.0;
  267.             hasVelocityEntries = false;
  268.             epoch              = DateTimeComponents.JULIAN_EPOCH;
  269.             timeScale          = parser.timeScales.getGPS();
  270.             maxSatellites      = 0;
  271.             nbAccuracies       = 0;
  272.             done               = false;
  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 SP3Header header = new SP3Header();
  288.                     header.setVersion(v.substring(0, 1).toLowerCase().charAt(0));

  289.                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
  290.                     header.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.                     pi.epoch = new DateTimeComponents(year, month, day,
  300.                                                       hour, minute, second);

  301.                     final int numEpochs = scanner.nextInt();
  302.                     header.setNumberOfEpochs(numEpochs);

  303.                     // data used indicator
  304.                     final String fullSpec = scanner.next();
  305.                     final List<DataUsed> dataUsed = new ArrayList<>();
  306.                     for (final String specifier : fullSpec.split("\\+")) {
  307.                         dataUsed.add(DataUsed.parse(specifier, pi.fileName, header.getVersion()));
  308.                     }
  309.                     header.setDataUsed(dataUsed);

  310.                     header.setCoordinateSystem(scanner.next());
  311.                     header.setOrbitTypeKey(scanner.next());
  312.                     header.setAgency(scanner.hasNext() ? scanner.next() : "");
  313.                     pi.file = new SP3(header, pi.parser.mu, pi.parser.interpolationSamples,
  314.                                       pi.parser.frameBuilder.apply(header.getCoordinateSystem()));
  315.                 }
  316.             }

  317.             /** {@inheritDoc} */
  318.             @Override
  319.             public Iterable<LineParser> allowedNext() {
  320.                 return Collections.singleton(HEADER_DATE_TIME_REFERENCE);
  321.             }

  322.         },

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

  325.             /** {@inheritDoc} */
  326.             @Override
  327.             public void parse(final String line, final ParseInfo pi) {
  328.                 try (Scanner s1      = new Scanner(line);
  329.                      Scanner s2      = s1.useDelimiter(SPACES);
  330.                      Scanner scanner = s2.useLocale(Locale.US)) {
  331.                     scanner.skip("##");

  332.                     // gps week
  333.                     pi.file.getHeader().setGpsWeek(scanner.nextInt());
  334.                     // seconds of week
  335.                     pi.file.getHeader().setSecondsOfWeek(scanner.nextDouble());
  336.                     // epoch interval
  337.                     pi.file.getHeader().setEpochInterval(scanner.nextDouble());
  338.                     // modified julian day
  339.                     pi.file.getHeader().setModifiedJulianDay(scanner.nextInt());
  340.                     // day fraction
  341.                     pi.file.getHeader().setDayFraction(scanner.nextDouble());
  342.                 }
  343.             }

  344.             /** {@inheritDoc} */
  345.             @Override
  346.             public Iterable<LineParser> allowedNext() {
  347.                 return Collections.singleton(HEADER_SAT_IDS);
  348.             }

  349.         },

  350.         /** Parser for satellites identifiers. */
  351.         HEADER_SAT_IDS("^\\+ .*") {

  352.             /** {@inheritDoc} */
  353.             @Override
  354.             public void parse(final String line, final ParseInfo pi) {

  355.                 if (pi.maxSatellites == 0) {
  356.                     // this is the first ids line, it also contains the number of satellites
  357.                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
  358.                 }

  359.                 final int lineLength = line.length();
  360.                 int count = pi.file.getSatelliteCount();
  361.                 int startIdx = 9;
  362.                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  363.                     final String satId = line.substring(startIdx, startIdx + 3).trim();
  364.                     if (!satId.isEmpty()) {
  365.                         pi.file.addSatellite(satId);
  366.                     }
  367.                     startIdx += 3;
  368.                 }
  369.             }

  370.             /** {@inheritDoc} */
  371.             @Override
  372.             public Iterable<LineParser> allowedNext() {
  373.                 return Arrays.asList(HEADER_SAT_IDS, HEADER_ACCURACY);
  374.             }

  375.         },

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

  378.             /** {@inheritDoc} */
  379.             @Override
  380.             public void parse(final String line, final ParseInfo pi) {
  381.                 final int lineLength = line.length();
  382.                 int startIdx = 9;
  383.                 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  384.                     final String sub = line.substring(startIdx, startIdx + 3).trim();
  385.                     if (!sub.isEmpty()) {
  386.                         final int exponent = Integer.parseInt(sub);
  387.                         // the accuracy is calculated as 2**exp (in mm)
  388.                         pi.file.getHeader().setAccuracy(pi.nbAccuracies++,
  389.                                                         SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  390.                                                                             SP3Utils.POS_VEL_BASE_ACCURACY,
  391.                                                                             exponent));
  392.                     }
  393.                     startIdx += 3;
  394.                 }
  395.             }

  396.             /** {@inheritDoc} */
  397.             @Override
  398.             public Iterable<LineParser> allowedNext() {
  399.                 return Arrays.asList(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
  400.             }

  401.         },

  402.         /** Parser for time system. */
  403.         HEADER_TIME_SYSTEM("^%c.*") {

  404.             /** {@inheritDoc} */
  405.             @Override
  406.             public void parse(final String line, final ParseInfo pi) {

  407.                 if (pi.file.getHeader().getType() == null) {
  408.                     // this the first custom fields line, the only one really used
  409.                     pi.file.getHeader().setType(SP3FileType.parse(line.substring(3, 5).trim()));

  410.                     // now identify the time system in use
  411.                     final String tsStr = line.substring(9, 12).trim();
  412.                     final TimeSystem ts;
  413.                     if (tsStr.equalsIgnoreCase("ccc")) {
  414.                         ts = TimeSystem.GPS;
  415.                     } else {
  416.                         ts = TimeSystem.parseTimeSystem(tsStr);
  417.                     }
  418.                     pi.file.getHeader().setTimeSystem(ts);
  419.                     pi.timeScale = ts.getTimeScale(pi.parser.timeScales);

  420.                     // now we know the time scale used, we can set the file epoch
  421.                     pi.file.getHeader().setEpoch(new AbsoluteDate(pi.epoch, pi.timeScale));
  422.                 }

  423.             }

  424.             /** {@inheritDoc} */
  425.             @Override
  426.             public Iterable<LineParser> allowedNext() {
  427.                 return Arrays.asList(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
  428.             }

  429.         },

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

  432.             /** {@inheritDoc} */
  433.             @Override
  434.             public void parse(final String line, final ParseInfo pi) {
  435.                 final double posVelBase = Double.parseDouble(line.substring(3, 13).trim());
  436.                 if (posVelBase != 0.0) {
  437.                     // (mm or 10⁻⁴ mm/s)
  438.                     pi.file.getHeader().setPosVelBase(posVelBase);
  439.                 }

  440.                 final double clockBase = Double.parseDouble(line.substring(14, 26).trim());
  441.                 if (clockBase != 0.0) {
  442.                     // (ps or 10⁻⁴ ps/s)
  443.                     pi.file.getHeader().setClockBase(clockBase);
  444.                 }
  445.             }

  446.             /** {@inheritDoc} */
  447.             @Override
  448.             public Iterable<LineParser> allowedNext() {
  449.                 return Arrays.asList(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
  450.             }

  451.         },

  452.         /** Parser for custom parameters. */
  453.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

  454.             /** {@inheritDoc} */
  455.             @Override
  456.             public void parse(final String line, final ParseInfo pi) {
  457.                 // ignore additional custom parameters
  458.             }

  459.             /** {@inheritDoc} */
  460.             @Override
  461.             public Iterable<LineParser> allowedNext() {
  462.                 return Arrays.asList(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  463.             }

  464.         },

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

  467.             /** {@inheritDoc} */
  468.             @Override
  469.             public void parse(final String line, final ParseInfo pi) {
  470.                 pi.file.getHeader().addComment(line.substring(line.indexOf('*') + 1).trim());
  471.             }

  472.             /** {@inheritDoc} */
  473.             @Override
  474.             public Iterable<LineParser> allowedNext() {
  475.                 return Arrays.asList(HEADER_COMMENTS, DATA_EPOCH);
  476.             }

  477.         },

  478.         /** Parser for epoch. */
  479.         DATA_EPOCH("^\\* .*") {

  480.             /** {@inheritDoc} */
  481.             @Override
  482.             public void parse(final String line, final ParseInfo pi) {
  483.                 final int    year;
  484.                 final int    month;
  485.                 final int    day;
  486.                 final int    hour;
  487.                 final int    minute;
  488.                 final double second;
  489.                 try (Scanner s1      = new Scanner(line);
  490.                      Scanner s2      = s1.useDelimiter(SPACES);
  491.                      Scanner scanner = s2.useLocale(Locale.US)) {
  492.                     scanner.skip("\\*");
  493.                     year   = scanner.nextInt();
  494.                     month  = scanner.nextInt();
  495.                     day    = scanner.nextInt();
  496.                     hour   = scanner.nextInt();
  497.                     minute = scanner.nextInt();
  498.                     second = scanner.nextDouble();
  499.                 }

  500.                 // some SP3 files have weird epochs as in the following three examples, where
  501.                 // the middle dates are wrong
  502.                 //
  503.                 // *  2016  7  6 16 58  0.00000000
  504.                 // PL51  11872.234459   3316.551981    101.400098 999999.999999
  505.                 // VL51   8054.606014 -27076.640110 -53372.762255 999999.999999
  506.                 // *  2016  7  6 16 60  0.00000000
  507.                 // PL51  11948.228978   2986.113872   -538.901114 999999.999999
  508.                 // VL51   4605.419303 -27972.588048 -53316.820671 999999.999999
  509.                 // *  2016  7  6 17  2  0.00000000
  510.                 // PL51  11982.652569   2645.786926  -1177.549463 999999.999999
  511.                 // VL51   1128.248622 -28724.293303 -53097.358387 999999.999999
  512.                 //
  513.                 // *  2016  7  6 23 58  0.00000000
  514.                 // PL51   3215.382310  -7958.586164   8812.395707
  515.                 // VL51 -18058.659942 -45834.335707 -34496.540437
  516.                 // *  2016  7  7 24  0  0.00000000
  517.                 // PL51   2989.229334  -8494.421415   8385.068555
  518.                 // VL51 -19617.027447 -43444.824985 -36706.159070
  519.                 // *  2016  7  7  0  2  0.00000000
  520.                 // PL51   2744.983592  -9000.639164   7931.904779
  521.                 // VL51 -21072.925764 -40899.633288 -38801.567078
  522.                 //
  523.                 // * 2021 12 31  0  0  0.00000000
  524.                 // PL51   6578.459330   5572.231927  -8703.502054
  525.                 // VL51  -5356.007694 -48869.881161 -35036.676469
  526.                 // * 2022  1  0  0  2  0.00000000
  527.                 // PL51   6499.035610   4978.263048  -9110.135595
  528.                 // VL51  -7881.633197 -50092.564035 -32717.740919
  529.                 // * 2022  1  0  0  4  0.00000000
  530.                 // PL51   6389.313975   4370.794537  -9488.314264
  531.                 // VL51 -10403.797055 -51119.231402 -30295.421935
  532.                 // In the first case, the date should really be 2016  7  6 17  0  0.00000000,
  533.                 // i.e as the minutes field overflows, the hours field should be incremented
  534.                 // In the second case, the date should really be 2016  7  7  0  0  0.00000000,
  535.                 // i.e. as the hours field overflows, the day field should be kept as is
  536.                 // we cannot be sure how carry was managed when these bogus files were written
  537.                 // so we try different options, incrementing or not previous field, and selecting
  538.                 // the closest one to expected date
  539.                 // In the third case, there are two different errors: the date is globally
  540.                 // shifted to the left by one character, and the day is 0 instead of 1
  541.                 DateComponents dc = day == 0 ?
  542.                                     new DateComponents(new DateComponents(year, month, 1), -1) :
  543.                                     new DateComponents(year, month, day);
  544.                 final List<AbsoluteDate> candidates = new ArrayList<>();
  545.                 int h = hour;
  546.                 int m = minute;
  547.                 double s = second;
  548.                 if (s >= 60.0) {
  549.                     s -= 60;
  550.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  551.                     m++;
  552.                 }
  553.                 if (m > 59) {
  554.                     m = 0;
  555.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  556.                     h++;
  557.                 }
  558.                 if (h > 23) {
  559.                     h = 0;
  560.                     addCandidate(candidates, dc, h, m, s, pi.timeScale);
  561.                     dc = new DateComponents(dc, 1);
  562.                 }
  563.                 addCandidate(candidates, dc, h, m, s, pi.timeScale);
  564.                 final AbsoluteDate expected = pi.latestEpoch == null ?
  565.                                               pi.file.getHeader().getEpoch() :
  566.                                               pi.latestEpoch.shiftedBy(pi.file.getHeader().getEpochInterval());
  567.                 pi.latestEpoch = null;
  568.                 for (final AbsoluteDate candidate : candidates) {
  569.                     if (FastMath.abs(candidate.durationFrom(expected)) < 0.01 * pi.file.getHeader().getEpochInterval()) {
  570.                         pi.latestEpoch = candidate;
  571.                     }
  572.                 }
  573.                 if (pi.latestEpoch == null) {
  574.                     // no date recognized, just parse again the initial fields
  575.                     // in order to generate again an exception
  576.                     pi.latestEpoch = new AbsoluteDate(year, month, day, hour, minute, second, pi.timeScale);
  577.                 }

  578.             }

  579.             /** Add an epoch candidate to a list.
  580.              * @param candidates list of candidates
  581.              * @param dc date components
  582.              * @param hour hour number from 0 to 23
  583.              * @param minute minute number from 0 to 59
  584.              * @param second second number from 0.0 to 60.0 (excluded)
  585.              * @param timeScale time scale
  586.              * @since 11.1.1
  587.              */
  588.             private void addCandidate(final List<AbsoluteDate> candidates, final DateComponents dc,
  589.                                       final int hour, final int minute, final double second,
  590.                                       final TimeScale timeScale) {
  591.                 try {
  592.                     candidates.add(new AbsoluteDate(dc, new TimeComponents(hour, minute, second), timeScale));
  593.                 } catch (OrekitIllegalArgumentException oiae) {
  594.                     // ignored
  595.                 }
  596.             }

  597.             /** {@inheritDoc} */
  598.             @Override
  599.             public Iterable<LineParser> allowedNext() {
  600.                 return Collections.singleton(DATA_POSITION);
  601.             }

  602.         },

  603.         /** Parser for position. */
  604.         DATA_POSITION("^P.*") {

  605.             /** {@inheritDoc} */
  606.             @Override
  607.             public void parse(final String line, final ParseInfo pi) {
  608.                 final String satelliteId = line.substring(1, 4).trim();

  609.                 if (!pi.file.containsSatellite(satelliteId)) {
  610.                     pi.latestPosition = Vector3D.ZERO;
  611.                 } else {

  612.                     final SP3Header header = pi.file.getHeader();

  613.                     // the position values are in km and have to be converted to m
  614.                     pi.latestPosition = new Vector3D(SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(4, 18).trim())),
  615.                                                      SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(18, 32).trim())),
  616.                                                      SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(32, 46).trim())));

  617.                     // clock (microsec)
  618.                     pi.latestClock = SP3Utils.CLOCK_UNIT.toSI(line.trim().length() <= 46 ?
  619.                                                               SP3Utils.DEFAULT_CLOCK_VALUE :
  620.                                                               Double.parseDouble(line.substring(46, 60).trim()));

  621.                     if (pi.latestPosition.getNorm() > 0) {

  622.                         if (line.length() < 69 ||
  623.                             line.substring(61, 63).trim().isEmpty() ||
  624.                             line.substring(64, 66).trim().isEmpty() ||
  625.                             line.substring(67, 69).trim().isEmpty()) {
  626.                             pi.latestPositionAccuracy = null;
  627.                         } else {
  628.                             pi.latestPositionAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  629.                                                                                          header.getPosVelBase(),
  630.                                                                                          Integer.parseInt(line.substring(61, 63).trim())),
  631.                                                                      SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  632.                                                                                          header.getPosVelBase(),
  633.                                                                                          Integer.parseInt(line.substring(64, 66).trim())),
  634.                                                                      SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  635.                                                                                          header.getPosVelBase(),
  636.                                                                                          Integer.parseInt(line.substring(67, 69).trim())));
  637.                         }

  638.                         if (line.length() < 73 || line.substring(70, 73).trim().isEmpty()) {
  639.                             pi.latestClockAccuracy    = Double.NaN;
  640.                         } else {
  641.                             pi.latestClockAccuracy    = SP3Utils.siAccuracy(SP3Utils.CLOCK_ACCURACY_UNIT,
  642.                                                                             header.getClockBase(),
  643.                                                                             Integer.parseInt(line.substring(70, 73).trim()));
  644.                         }

  645.                         pi.latestClockEvent         = line.length() >= 75 && line.charAt(74) == 'E';
  646.                         pi.latestClockPrediction    = line.length() >= 76 && line.charAt(75) == 'P';
  647.                         pi.latestOrbitManeuverEvent = line.length() >= 79 && line.charAt(78) == 'M';
  648.                         pi.latestOrbitPrediction    = line.length() >= 80 && line.charAt(79) == 'P';

  649.                         if (!pi.hasVelocityEntries) {
  650.                             final SP3Coordinate coord =
  651.                                             new SP3Coordinate(pi.latestEpoch,
  652.                                                               pi.latestPosition,           pi.latestPositionAccuracy,
  653.                                                               Vector3D.ZERO,               null,
  654.                                                               pi.latestClock,              pi.latestClockAccuracy,
  655.                                                               0.0,                         Double.NaN,
  656.                                                               pi.latestClockEvent,         pi.latestClockPrediction,
  657.                                                               pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
  658.                             pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
  659.                         }
  660.                     }
  661.                 }
  662.             }

  663.             /** {@inheritDoc} */
  664.             @Override
  665.             public Iterable<LineParser> allowedNext() {
  666.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
  667.             }

  668.         },

  669.         /** Parser for position correlation. */
  670.         DATA_POSITION_CORRELATION("^EP.*") {

  671.             /** {@inheritDoc} */
  672.             @Override
  673.             public void parse(final String line, final ParseInfo pi) {
  674.                 // ignored for now
  675.             }

  676.             /** {@inheritDoc} */
  677.             @Override
  678.             public Iterable<LineParser> allowedNext() {
  679.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
  680.             }

  681.         },

  682.         /** Parser for velocity. */
  683.         DATA_VELOCITY("^V.*") {

  684.             /** {@inheritDoc} */
  685.             @Override
  686.             public void parse(final String line, final ParseInfo pi) {
  687.                 final String satelliteId = line.substring(1, 4).trim();

  688.                 if (pi.file.containsSatellite(satelliteId) && pi.latestPosition.getNorm() > 0) {

  689.                     final SP3Header header = pi.file.getHeader();

  690.                     // the velocity values are in dm/s and have to be converted to m/s
  691.                     final Vector3D velocity = new Vector3D(SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(4, 18).trim())),
  692.                                                            SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(18, 32).trim())),
  693.                                                            SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(32, 46).trim())));

  694.                     // clock rate in file is 1e-4 us / s
  695.                     final double clockRateChange = SP3Utils.CLOCK_RATE_UNIT.toSI(line.trim().length() <= 46 ?
  696.                                                                                  SP3Utils.DEFAULT_CLOCK_RATE_VALUE :
  697.                                                                                  Double.parseDouble(line.substring(46, 60).trim()));

  698.                     final Vector3D velocityAccuracy;
  699.                     if (line.length() < 69 ||
  700.                         line.substring(61, 63).trim().isEmpty() ||
  701.                         line.substring(64, 66).trim().isEmpty() ||
  702.                         line.substring(67, 69).trim().isEmpty()) {
  703.                         velocityAccuracy  = null;
  704.                     } else {
  705.                         velocityAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  706.                                                                             header.getPosVelBase(),
  707.                                                                             Integer.parseInt(line.substring(61, 63).trim())),
  708.                                                         SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  709.                                                                             header.getPosVelBase(),
  710.                                                                             Integer.parseInt(line.substring(64, 66).trim())),
  711.                                                         SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  712.                                                                             header.getPosVelBase(),
  713.                                                                             Integer.parseInt(line.substring(67, 69).trim())));
  714.                     }

  715.                     final double clockRateAccuracy;
  716.                     if (line.length() < 73 || line.substring(70, 73).trim().isEmpty()) {
  717.                         clockRateAccuracy = Double.NaN;
  718.                     } else {
  719.                         clockRateAccuracy = SP3Utils.siAccuracy(SP3Utils.CLOCK_RATE_ACCURACY_UNIT,
  720.                                                                 header.getClockBase(),
  721.                                                                 Integer.parseInt(line.substring(70, 73).trim()));
  722.                     }

  723.                     final SP3Coordinate coord =
  724.                             new SP3Coordinate(pi.latestEpoch,
  725.                                               pi.latestPosition,           pi.latestPositionAccuracy,
  726.                                               velocity,                    velocityAccuracy,
  727.                                               pi.latestClock,              pi.latestClockAccuracy,
  728.                                               clockRateChange,             clockRateAccuracy,
  729.                                               pi.latestClockEvent,         pi.latestClockPrediction,
  730.                                               pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
  731.                     pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
  732.                 }
  733.             }

  734.             /** {@inheritDoc} */
  735.             @Override
  736.             public Iterable<LineParser> allowedNext() {
  737.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  738.             }

  739.         },

  740.         /** Parser for velocity correlation. */
  741.         DATA_VELOCITY_CORRELATION("^EV.*") {

  742.             /** {@inheritDoc} */
  743.             @Override
  744.             public void parse(final String line, final ParseInfo pi) {
  745.                 // ignored for now
  746.             }

  747.             /** {@inheritDoc} */
  748.             @Override
  749.             public Iterable<LineParser> allowedNext() {
  750.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, EOF);
  751.             }

  752.         },

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

  755.             /** {@inheritDoc} */
  756.             @Override
  757.             public void parse(final String line, final ParseInfo pi) {
  758.                 pi.done = true;
  759.             }

  760.             /** {@inheritDoc} */
  761.             @Override
  762.             public Iterable<LineParser> allowedNext() {
  763.                 return Collections.singleton(EOF);
  764.             }

  765.         };

  766.         /** Pattern for identifying line. */
  767.         private final Pattern pattern;

  768.         /** Simple constructor.
  769.          * @param lineRegexp regular expression for identifying line
  770.          */
  771.         LineParser(final String lineRegexp) {
  772.             pattern = Pattern.compile(lineRegexp);
  773.         }

  774.         /** Parse a line.
  775.          * @param line line to parse
  776.          * @param pi holder for transient data
  777.          */
  778.         public abstract void parse(String line, ParseInfo pi);

  779.         /** Get the allowed parsers for next line.
  780.          * @return allowed parsers for next line
  781.          */
  782.         public abstract Iterable<LineParser> allowedNext();

  783.         /** Check if parser can handle line.
  784.          * @param line line to parse
  785.          * @return true if parser can handle the specified line
  786.          */
  787.         public boolean canHandle(final String line) {
  788.             return pattern.matcher(line).matches();
  789.         }

  790.     }

  791. }