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.gnss.TimeSystem;
  41. import org.orekit.time.AbsoluteDate;
  42. import org.orekit.time.DateComponents;
  43. import org.orekit.time.DateTimeComponents;
  44. import org.orekit.time.TimeComponents;
  45. import org.orekit.time.TimeScale;
  46. import org.orekit.time.TimeScales;
  47. import org.orekit.utils.CartesianDerivativesFilter;
  48. import org.orekit.utils.Constants;
  49. import org.orekit.utils.IERSConventions;

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

  63.     /** String representation of the center of ephemeris coordinate system. **/
  64.     public static final String SP3_FRAME_CENTER_STRING = "EARTH";

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

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

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

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

  73.     /** Set of time scales. */
  74.     private final TimeScales timeScales;

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

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

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

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

  144.     @Override
  145.     public SP3 parse(final DataSource source) {

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

  148.             if (br == null) {
  149.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  150.             }

  151.             // initialize internal data structures
  152.             final ParseInfo pi = new ParseInfo(source.getName());

  153.             int lineNumber = 0;
  154.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  155.             nextLine:
  156.                 for (String line = br.readLine(); line != null; line = br.readLine()) {
  157.                     ++lineNumber;
  158.                     for (final LineParser candidate : candidateParsers) {
  159.                         if (candidate.canHandle(line)) {
  160.                             try {
  161.                                 candidate.parse(line, pi);
  162.                                 if (pi.done) {
  163.                                     break nextLine;
  164.                                 }
  165.                                 candidateParsers = candidate.allowedNext();
  166.                                 continue nextLine;
  167.                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  168.                                 throw new OrekitException(e,
  169.                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  170.                                                           lineNumber, pi.fileName, line);
  171.                             }
  172.                         }
  173.                     }

  174.                     // no parsers found for this line
  175.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  176.                                               lineNumber, pi.fileName, line);

  177.                 }

  178.             pi.file.validate(true, pi.fileName);
  179.             return pi.file;

  180.         } catch (IOException ioe) {
  181.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  182.         }

  183.     }

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

  190.         /** File name.
  191.          * @since 12.0
  192.          */
  193.         private final String fileName;

  194.         /** Set of time scales for parsing dates. */
  195.         private final TimeScales timeScales;

  196.         /** The corresponding SP3File object. */
  197.         private SP3 file;

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

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

  202.         /** The latest position accuracy as read from the SP3 file.
  203.          * @since 12.0
  204.          */
  205.         private Vector3D latestPositionAccuracy;

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

  208.         /** The latest clock value as read from the SP3 file.
  209.          * @since 12.0
  210.          */
  211.         private double latestClockAccuracy;

  212.         /** The latest clock event flag as read from the SP3 file.
  213.          * @since 12.0
  214.          */
  215.         private boolean latestClockEvent;

  216.         /** The latest clock prediction flag as read from the SP3 file.
  217.          * @since 12.0
  218.          */
  219.         private boolean latestClockPrediction;

  220.         /** The latest orbit maneuver event flag as read from the SP3 file.
  221.          * @since 12.0
  222.          */
  223.         private boolean latestOrbitManeuverEvent;

  224.         /** The latest orbit prediction flag as read from the SP3 file.
  225.          * @since 12.0
  226.          */
  227.         private boolean latestOrbitPrediction;

  228.         /** Indicates if the SP3 file has velocity entries. */
  229.         private boolean hasVelocityEntries;

  230.         /** The timescale used in the SP3 file. */
  231.         private TimeScale timeScale;

  232.         /** Date and time of the file. */
  233.         private DateTimeComponents epoch;

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

  236.         /** The number of satellites accuracies already seen. */
  237.         private int nbAccuracies;

  238.         /** End Of File reached indicator. */
  239.         private boolean done;

  240.         /** Create a new {@link ParseInfo} object.
  241.          * @param fileName file name
  242.          */
  243.         protected ParseInfo(final String fileName) {
  244.             this.fileName      = fileName;
  245.             this.timeScales    = SP3Parser.this.timeScales;
  246.             file               = new SP3(mu, interpolationSamples, frameBuilder.apply(SP3_FRAME_CENTER_STRING));
  247.             latestEpoch        = null;
  248.             latestPosition     = null;
  249.             latestClock        = 0.0;
  250.             hasVelocityEntries = false;
  251.             epoch              = DateTimeComponents.JULIAN_EPOCH;
  252.             timeScale          = timeScales.getGPS();
  253.             maxSatellites      = 0;
  254.             nbAccuracies       = 0;
  255.             done               = false;
  256.         }
  257.     }

  258.     /** Parsers for specific lines. */
  259.     private enum LineParser {

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

  262.             /** {@inheritDoc} */
  263.             @Override
  264.             public void parse(final String line, final ParseInfo pi) {
  265.                 try (Scanner s1      = new Scanner(line);
  266.                      Scanner s2      = s1.useDelimiter(SPACES);
  267.                      Scanner scanner = s2.useLocale(Locale.US)) {
  268.                     scanner.skip("#");
  269.                     final String v = scanner.next();

  270.                     pi.file.getHeader().setVersion(v.substring(0, 1).toLowerCase().charAt(0));

  271.                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
  272.                     pi.file.getHeader().setFilter(pi.hasVelocityEntries ?
  273.                                                   CartesianDerivativesFilter.USE_PV :
  274.                                                   CartesianDerivativesFilter.USE_P);

  275.                     final int    year   = Integer.parseInt(v.substring(2));
  276.                     final int    month  = scanner.nextInt();
  277.                     final int    day    = scanner.nextInt();
  278.                     final int    hour   = scanner.nextInt();
  279.                     final int    minute = scanner.nextInt();
  280.                     final double second = scanner.nextDouble();

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

  283.                     final int numEpochs = scanner.nextInt();
  284.                     pi.file.getHeader().setNumberOfEpochs(numEpochs);

  285.                     // data used indicator
  286.                     final String fullSpec = scanner.next();
  287.                     final List<DataUsed> dataUsed = new ArrayList<>();
  288.                     for (final String specifier : fullSpec.split("\\+")) {
  289.                         dataUsed.add(DataUsed.parse(specifier, pi.fileName, pi.file.getHeader().getVersion()));
  290.                     }
  291.                     pi.file.getHeader().setDataUsed(dataUsed);

  292.                     pi.file.getHeader().setCoordinateSystem(scanner.next());
  293.                     pi.file.getHeader().setOrbitTypeKey(scanner.next());
  294.                     pi.file.getHeader().setAgency(scanner.next());
  295.                 }
  296.             }

  297.             /** {@inheritDoc} */
  298.             @Override
  299.             public Iterable<LineParser> allowedNext() {
  300.                 return Collections.singleton(HEADER_DATE_TIME_REFERENCE);
  301.             }

  302.         },

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

  305.             /** {@inheritDoc} */
  306.             @Override
  307.             public void parse(final String line, final ParseInfo pi) {
  308.                 try (Scanner s1      = new Scanner(line);
  309.                      Scanner s2      = s1.useDelimiter(SPACES);
  310.                      Scanner scanner = s2.useLocale(Locale.US)) {
  311.                     scanner.skip("##");

  312.                     // gps week
  313.                     pi.file.getHeader().setGpsWeek(scanner.nextInt());
  314.                     // seconds of week
  315.                     pi.file.getHeader().setSecondsOfWeek(scanner.nextDouble());
  316.                     // epoch interval
  317.                     pi.file.getHeader().setEpochInterval(scanner.nextDouble());
  318.                     // modified julian day
  319.                     pi.file.getHeader().setModifiedJulianDay(scanner.nextInt());
  320.                     // day fraction
  321.                     pi.file.getHeader().setDayFraction(scanner.nextDouble());
  322.                 }
  323.             }

  324.             /** {@inheritDoc} */
  325.             @Override
  326.             public Iterable<LineParser> allowedNext() {
  327.                 return Collections.singleton(HEADER_SAT_IDS);
  328.             }

  329.         },

  330.         /** Parser for satellites identifiers. */
  331.         HEADER_SAT_IDS("^\\+ .*") {

  332.             /** {@inheritDoc} */
  333.             @Override
  334.             public void parse(final String line, final ParseInfo pi) {

  335.                 if (pi.maxSatellites == 0) {
  336.                     // this is the first ids line, it also contains the number of satellites
  337.                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
  338.                 }

  339.                 final int lineLength = line.length();
  340.                 int count = pi.file.getSatelliteCount();
  341.                 int startIdx = 9;
  342.                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  343.                     final String satId = line.substring(startIdx, startIdx + 3).trim();
  344.                     if (satId.length() > 0) {
  345.                         pi.file.addSatellite(satId);
  346.                     }
  347.                     startIdx += 3;
  348.                 }
  349.             }

  350.             /** {@inheritDoc} */
  351.             @Override
  352.             public Iterable<LineParser> allowedNext() {
  353.                 return Arrays.asList(HEADER_SAT_IDS, HEADER_ACCURACY);
  354.             }

  355.         },

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

  358.             /** {@inheritDoc} */
  359.             @Override
  360.             public void parse(final String line, final ParseInfo pi) {
  361.                 final int lineLength = line.length();
  362.                 int startIdx = 9;
  363.                 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  364.                     final String sub = line.substring(startIdx, startIdx + 3).trim();
  365.                     if (sub.length() > 0) {
  366.                         final int exponent = Integer.parseInt(sub);
  367.                         // the accuracy is calculated as 2**exp (in mm)
  368.                         pi.file.getHeader().setAccuracy(pi.nbAccuracies++,
  369.                                                         SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  370.                                                                             SP3Utils.POS_VEL_BASE_ACCURACY,
  371.                                                                             exponent));
  372.                     }
  373.                     startIdx += 3;
  374.                 }
  375.             }

  376.             /** {@inheritDoc} */
  377.             @Override
  378.             public Iterable<LineParser> allowedNext() {
  379.                 return Arrays.asList(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
  380.             }

  381.         },

  382.         /** Parser for time system. */
  383.         HEADER_TIME_SYSTEM("^%c.*") {

  384.             /** {@inheritDoc} */
  385.             @Override
  386.             public void parse(final String line, final ParseInfo pi) {

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

  390.                     // now identify the time system in use
  391.                     final String tsStr = line.substring(9, 12).trim();
  392.                     final TimeSystem ts;
  393.                     if (tsStr.equalsIgnoreCase("ccc")) {
  394.                         ts = TimeSystem.GPS;
  395.                     } else {
  396.                         ts = TimeSystem.valueOf(tsStr);
  397.                     }
  398.                     pi.file.getHeader().setTimeSystem(ts);
  399.                     pi.timeScale = ts.getTimeScale(pi.timeScales);

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

  403.             }

  404.             /** {@inheritDoc} */
  405.             @Override
  406.             public Iterable<LineParser> allowedNext() {
  407.                 return Arrays.asList(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
  408.             }

  409.         },

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

  412.             /** {@inheritDoc} */
  413.             @Override
  414.             public void parse(final String line, final ParseInfo pi) {
  415.                 final double posVelBase = Double.parseDouble(line.substring(3, 13).trim());
  416.                 if (posVelBase != 0.0) {
  417.                     // (mm or 10⁻⁴ mm/s)
  418.                     pi.file.getHeader().setPosVelBase(posVelBase);
  419.                 }

  420.                 final double clockBase = Double.parseDouble(line.substring(14, 26).trim());
  421.                 if (clockBase != 0.0) {
  422.                     // (ps or 10⁻⁴ ps/s)
  423.                     pi.file.getHeader().setClockBase(clockBase);
  424.                 }
  425.             }

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

  431.         },

  432.         /** Parser for custom parameters. */
  433.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

  434.             /** {@inheritDoc} */
  435.             @Override
  436.             public void parse(final String line, final ParseInfo pi) {
  437.                 // ignore additional custom parameters
  438.             }

  439.             /** {@inheritDoc} */
  440.             @Override
  441.             public Iterable<LineParser> allowedNext() {
  442.                 return Arrays.asList(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  443.             }

  444.         },

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

  447.             /** {@inheritDoc} */
  448.             @Override
  449.             public void parse(final String line, final ParseInfo pi) {
  450.                 pi.file.getHeader().addComment(line.substring(line.indexOf('*') + 1).trim());
  451.             }

  452.             /** {@inheritDoc} */
  453.             @Override
  454.             public Iterable<LineParser> allowedNext() {
  455.                 return Arrays.asList(HEADER_COMMENTS, DATA_EPOCH);
  456.             }

  457.         },

  458.         /** Parser for epoch. */
  459.         DATA_EPOCH("^\\* .*") {

  460.             /** {@inheritDoc} */
  461.             @Override
  462.             public void parse(final String line, final ParseInfo pi) {
  463.                 final int    year;
  464.                 final int    month;
  465.                 final int    day;
  466.                 final int    hour;
  467.                 final int    minute;
  468.                 final double second;
  469.                 try (Scanner s1      = new Scanner(line);
  470.                      Scanner s2      = s1.useDelimiter(SPACES);
  471.                      Scanner scanner = s2.useLocale(Locale.US)) {
  472.                     scanner.skip("\\*");
  473.                     year   = scanner.nextInt();
  474.                     month  = scanner.nextInt();
  475.                     day    = scanner.nextInt();
  476.                     hour   = scanner.nextInt();
  477.                     minute = scanner.nextInt();
  478.                     second = scanner.nextDouble();
  479.                 }

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

  558.             }

  559.             /** Add an epoch candidate to a list.
  560.              * @param candidates list of candidates
  561.              * @param dc date components
  562.              * @param hour hour number from 0 to 23
  563.              * @param minute minute number from 0 to 59
  564.              * @param second second number from 0.0 to 60.0 (excluded)
  565.              * @param timeScale time scale
  566.              * @since 11.1.1
  567.              */
  568.             private void addCandidate(final List<AbsoluteDate> candidates, final DateComponents dc,
  569.                                       final int hour, final int minute, final double second,
  570.                                       final TimeScale timeScale) {
  571.                 try {
  572.                     candidates.add(new AbsoluteDate(dc, new TimeComponents(hour, minute, second), timeScale));
  573.                 } catch (OrekitIllegalArgumentException oiae) {
  574.                     // ignored
  575.                 }
  576.             }

  577.             /** {@inheritDoc} */
  578.             @Override
  579.             public Iterable<LineParser> allowedNext() {
  580.                 return Collections.singleton(DATA_POSITION);
  581.             }

  582.         },

  583.         /** Parser for position. */
  584.         DATA_POSITION("^P.*") {

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

  589.                 if (!pi.file.containsSatellite(satelliteId)) {
  590.                     pi.latestPosition = Vector3D.ZERO;
  591.                 } else {

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

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

  597.                     // clock (microsec)
  598.                     pi.latestClock = SP3Utils.CLOCK_UNIT.toSI(line.trim().length() <= 46 ?
  599.                                                               SP3Utils.DEFAULT_CLOCK_VALUE :
  600.                                                               Double.parseDouble(line.substring(46, 60).trim()));

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

  602.                         if (line.length() < 69 || line.substring(61, 69).trim().length() == 0) {
  603.                             pi.latestPositionAccuracy = null;
  604.                         } else {
  605.                             pi.latestPositionAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  606.                                                                                          header.getPosVelBase(),
  607.                                                                                          Integer.parseInt(line.substring(61, 63).trim())),
  608.                                                                      SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  609.                                                                                          header.getPosVelBase(),
  610.                                                                                          Integer.parseInt(line.substring(64, 66).trim())),
  611.                                                                      SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
  612.                                                                                          header.getPosVelBase(),
  613.                                                                                          Integer.parseInt(line.substring(67, 69).trim())));
  614.                         }

  615.                         if (line.length() < 73 || line.substring(70, 73).trim().length() == 0) {
  616.                             pi.latestClockAccuracy    = Double.NaN;
  617.                         } else {
  618.                             pi.latestClockAccuracy    = SP3Utils.siAccuracy(SP3Utils.CLOCK_ACCURACY_UNIT,
  619.                                                                             header.getClockBase(),
  620.                                                                             Integer.parseInt(line.substring(70, 73).trim()));
  621.                         }

  622.                         pi.latestClockEvent         = line.length() < 75 ? false : line.substring(74, 75).equals("E");
  623.                         pi.latestClockPrediction    = line.length() < 76 ? false : line.substring(75, 76).equals("P");
  624.                         pi.latestOrbitManeuverEvent = line.length() < 79 ? false : line.substring(78, 79).equals("M");
  625.                         pi.latestOrbitPrediction    = line.length() < 80 ? false : line.substring(79, 80).equals("P");

  626.                         if (!pi.hasVelocityEntries) {
  627.                             final SP3Coordinate coord =
  628.                                             new SP3Coordinate(pi.latestEpoch,
  629.                                                               pi.latestPosition,           pi.latestPositionAccuracy,
  630.                                                               Vector3D.ZERO,               null,
  631.                                                               pi.latestClock,              pi.latestClockAccuracy,
  632.                                                               0.0,                         Double.NaN,
  633.                                                               pi.latestClockEvent,         pi.latestClockPrediction,
  634.                                                               pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
  635.                             pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
  636.                         }
  637.                     }
  638.                 }
  639.             }

  640.             /** {@inheritDoc} */
  641.             @Override
  642.             public Iterable<LineParser> allowedNext() {
  643.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
  644.             }

  645.         },

  646.         /** Parser for position correlation. */
  647.         DATA_POSITION_CORRELATION("^EP.*") {

  648.             /** {@inheritDoc} */
  649.             @Override
  650.             public void parse(final String line, final ParseInfo pi) {
  651.                 // ignored for now
  652.             }

  653.             /** {@inheritDoc} */
  654.             @Override
  655.             public Iterable<LineParser> allowedNext() {
  656.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
  657.             }

  658.         },

  659.         /** Parser for velocity. */
  660.         DATA_VELOCITY("^V.*") {

  661.             /** {@inheritDoc} */
  662.             @Override
  663.             public void parse(final String line, final ParseInfo pi) {
  664.                 final String satelliteId = line.substring(1, 4).trim();

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

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

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

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

  675.                     final Vector3D velocityAccuracy;
  676.                     if (line.length() < 69 || line.substring(61, 69).trim().length() == 0) {
  677.                         velocityAccuracy  = null;
  678.                     } else {
  679.                         velocityAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  680.                                                                             header.getPosVelBase(),
  681.                                                                             Integer.parseInt(line.substring(61, 63).trim())),
  682.                                                         SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  683.                                                                             header.getPosVelBase(),
  684.                                                                             Integer.parseInt(line.substring(64, 66).trim())),
  685.                                                         SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
  686.                                                                             header.getPosVelBase(),
  687.                                                                             Integer.parseInt(line.substring(67, 69).trim())));
  688.                     }

  689.                     final double clockRateAccuracy;
  690.                     if (line.length() < 73 || line.substring(70, 73).trim().length() == 0) {
  691.                         clockRateAccuracy = Double.NaN;
  692.                     } else {
  693.                         clockRateAccuracy = SP3Utils.siAccuracy(SP3Utils.CLOCK_RATE_ACCURACY_UNIT,
  694.                                                                 header.getClockBase(),
  695.                                                                 Integer.parseInt(line.substring(70, 73).trim()));
  696.                     }

  697.                     final SP3Coordinate coord =
  698.                             new SP3Coordinate(pi.latestEpoch,
  699.                                               pi.latestPosition,           pi.latestPositionAccuracy,
  700.                                               velocity,                    velocityAccuracy,
  701.                                               pi.latestClock,              pi.latestClockAccuracy,
  702.                                               clockRateChange,             clockRateAccuracy,
  703.                                               pi.latestClockEvent,         pi.latestClockPrediction,
  704.                                               pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
  705.                     pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
  706.                 }
  707.             }

  708.             /** {@inheritDoc} */
  709.             @Override
  710.             public Iterable<LineParser> allowedNext() {
  711.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  712.             }

  713.         },

  714.         /** Parser for velocity correlation. */
  715.         DATA_VELOCITY_CORRELATION("^EV.*") {

  716.             /** {@inheritDoc} */
  717.             @Override
  718.             public void parse(final String line, final ParseInfo pi) {
  719.                 // ignored for now
  720.             }

  721.             /** {@inheritDoc} */
  722.             @Override
  723.             public Iterable<LineParser> allowedNext() {
  724.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, EOF);
  725.             }

  726.         },

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

  729.             /** {@inheritDoc} */
  730.             @Override
  731.             public void parse(final String line, final ParseInfo pi) {
  732.                 pi.done = true;
  733.             }

  734.             /** {@inheritDoc} */
  735.             @Override
  736.             public Iterable<LineParser> allowedNext() {
  737.                 return Collections.singleton(EOF);
  738.             }

  739.         };

  740.         /** Pattern for identifying line. */
  741.         private final Pattern pattern;

  742.         /** Simple constructor.
  743.          * @param lineRegexp regular expression for identifying line
  744.          */
  745.         LineParser(final String lineRegexp) {
  746.             pattern = Pattern.compile(lineRegexp);
  747.         }

  748.         /** Parse a line.
  749.          * @param line line to parse
  750.          * @param pi holder for transient data
  751.          */
  752.         public abstract void parse(String line, ParseInfo pi);

  753.         /** Get the allowed parsers for next line.
  754.          * @return allowed parsers for next line
  755.          */
  756.         public abstract Iterable<LineParser> allowedNext();

  757.         /** Check if parser can handle line.
  758.          * @param line line to parse
  759.          * @return true if parser can handle the specified line
  760.          */
  761.         public boolean canHandle(final String line) {
  762.             return pattern.matcher(line).matches();
  763.         }

  764.     }

  765. }