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

  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.     /** Default number of samples to use when interpolating SP3 coordinates. */
  64.     public static final int DEFAULT_INTERPOLATION_SAMPLES = 7;

  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.      * It also uses a {@link #DEFAULT_INTERPOLATION_SAMPLES default number of samples} to
  80.      * interpolate coordinates.
  81.      *
  82.      * @see #SP3Parser(double, int, Function)
  83.      * @see IGSUtils#guessFrame(String)
  84.      */
  85.     @DefaultDataContext
  86.     public SP3Parser() {
  87.         this(Constants.EIGEN5C_EARTH_MU, DEFAULT_INTERPOLATION_SAMPLES, IGSUtils::guessFrame);
  88.     }

  89.     /**
  90.      * Create an SP3 parser and specify the extra information needed to create a {@link
  91.      * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
  92.      *
  93.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  94.      *
  95.      * @param mu                   is the standard gravitational parameter to use for
  96.      *                             creating {@link org.orekit.orbits.Orbit Orbits} from
  97.      *                             the ephemeris data. See {@link Constants}.
  98.      * @param interpolationSamples is the number of samples to use when interpolating.
  99.      * @param frameBuilder         is a function that can construct a frame from an SP3
  100.      *                             coordinate system string. The coordinate system can be
  101.      *                             any 5 character string e.g. ITR92, IGb08.
  102.      * @see #SP3Parser(double, int, Function, TimeScales)
  103.      * @see IGSUtils#guessFrame(String)
  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.     @Override
  135.     public SP3 parse(final DataSource source) {

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

  138.             if (br == null) {
  139.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  140.             }

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

  143.             int lineNumber = 0;
  144.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
  145.             nextLine:
  146.                 for (String line = br.readLine(); line != null; line = br.readLine()) {
  147.                     ++lineNumber;
  148.                     for (final LineParser candidate : candidateParsers) {
  149.                         if (candidate.canHandle(line)) {
  150.                             try {
  151.                                 candidate.parse(line, pi);
  152.                                 if (pi.done) {
  153.                                     break nextLine;
  154.                                 }
  155.                                 candidateParsers = candidate.allowedNext();
  156.                                 continue nextLine;
  157.                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  158.                                 throw new OrekitException(e,
  159.                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  160.                                                           lineNumber, pi.fileName, line);
  161.                             }
  162.                         }
  163.                     }

  164.                     // no parsers found for this line
  165.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  166.                                               lineNumber, pi.fileName, line);

  167.                 }

  168.             pi.file.validate(true, pi.fileName);
  169.             return pi.file;

  170.         } catch (IOException ioe) {
  171.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  172.         }

  173.     }

  174.     /** Transient data used for parsing a sp3 file. The data is kept in a
  175.      * separate data structure to make the parser thread-safe.
  176.      * <p><b>Note</b>: The class intentionally does not provide accessor
  177.      * methods, as it is only used internally for parsing a SP3 file.</p>
  178.      */
  179.     private static class ParseInfo {

  180.         /** File name.
  181.          * @since 12.0
  182.          */
  183.         private final String fileName;

  184.         /** Englobing parser. */
  185.         private final SP3Parser parser;

  186.         /** The corresponding SP3File object. */
  187.         private SP3 file;

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

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

  192.         /** The latest position accuracy as read from the SP3 file.
  193.          * @since 12.0
  194.          */
  195.         private Vector3D latestPositionAccuracy;

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

  198.         /** The latest clock value as read from the SP3 file.
  199.          * @since 12.0
  200.          */
  201.         private double latestClockAccuracy;

  202.         /** The latest clock event flag as read from the SP3 file.
  203.          * @since 12.0
  204.          */
  205.         private boolean latestClockEvent;

  206.         /** The latest clock prediction flag as read from the SP3 file.
  207.          * @since 12.0
  208.          */
  209.         private boolean latestClockPrediction;

  210.         /** The latest orbit maneuver event flag as read from the SP3 file.
  211.          * @since 12.0
  212.          */
  213.         private boolean latestOrbitManeuverEvent;

  214.         /** The latest orbit prediction flag as read from the SP3 file.
  215.          * @since 12.0
  216.          */
  217.         private boolean latestOrbitPrediction;

  218.         /** Indicates if the SP3 file has velocity entries. */
  219.         private boolean hasVelocityEntries;

  220.         /** The timescale used in the SP3 file. */
  221.         private TimeScale timeScale;

  222.         /** Date and time of the file. */
  223.         private DateTimeComponents epoch;

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

  226.         /** The number of satellites accuracies already seen. */
  227.         private int nbAccuracies;

  228.         /** End Of File reached indicator. */
  229.         private boolean done;

  230.         /** Create a new {@link ParseInfo} object.
  231.          * @param fileName file name
  232.          * @param parser englobing parser
  233.          */
  234.         protected ParseInfo(final String fileName,
  235.                             final SP3Parser parser) {
  236.             this.fileName      = fileName;
  237.             this.parser        = parser;
  238.             latestEpoch        = null;
  239.             latestPosition     = null;
  240.             latestClock        = 0.0;
  241.             hasVelocityEntries = false;
  242.             epoch              = DateTimeComponents.JULIAN_EPOCH;
  243.             timeScale          = parser.timeScales.getGPS();
  244.             maxSatellites      = 0;
  245.             nbAccuracies       = 0;
  246.             done               = false;
  247.         }
  248.     }

  249.     /** Parsers for specific lines. */
  250.     private enum LineParser {

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

  253.             /** {@inheritDoc} */
  254.             @Override
  255.             public void parse(final String line, final ParseInfo pi) {
  256.                 try (Scanner s1      = new Scanner(line);
  257.                      Scanner s2      = s1.useDelimiter(SPACES);
  258.                      Scanner scanner = s2.useLocale(Locale.US)) {
  259.                     scanner.skip("#");
  260.                     final String v = scanner.next();

  261.                     final SP3Header header = new SP3Header();
  262.                     header.setVersion(v.substring(0, 1).toLowerCase().charAt(0));

  263.                     pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
  264.                     header.setFilter(pi.hasVelocityEntries ?
  265.                                      CartesianDerivativesFilter.USE_PV :
  266.                                      CartesianDerivativesFilter.USE_P);

  267.                     final int    year   = Integer.parseInt(v.substring(2));
  268.                     final int    month  = scanner.nextInt();
  269.                     final int    day    = scanner.nextInt();
  270.                     final int    hour   = scanner.nextInt();
  271.                     final int    minute = scanner.nextInt();
  272.                     final double second = scanner.nextDouble();

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

  275.                     final int numEpochs = scanner.nextInt();
  276.                     header.setNumberOfEpochs(numEpochs);

  277.                     // data used indicator
  278.                     final String fullSpec = scanner.next();
  279.                     final List<DataUsed> dataUsed = new ArrayList<>();
  280.                     for (final String specifier : fullSpec.split("\\+")) {
  281.                         dataUsed.add(DataUsed.parse(specifier, pi.fileName, header.getVersion()));
  282.                     }
  283.                     header.setDataUsed(dataUsed);

  284.                     header.setCoordinateSystem(scanner.next());
  285.                     header.setOrbitTypeKey(scanner.next());
  286.                     header.setAgency(scanner.hasNext() ? scanner.next() : "");
  287.                     pi.file = new SP3(header, pi.parser.mu, pi.parser.interpolationSamples,
  288.                                       pi.parser.frameBuilder.apply(header.getCoordinateSystem()));
  289.                 }
  290.             }

  291.             /** {@inheritDoc} */
  292.             @Override
  293.             public Iterable<LineParser> allowedNext() {
  294.                 return Collections.singleton(HEADER_DATE_TIME_REFERENCE);
  295.             }

  296.         },

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

  299.             /** {@inheritDoc} */
  300.             @Override
  301.             public void parse(final String line, final ParseInfo pi) {
  302.                 try (Scanner s1      = new Scanner(line);
  303.                      Scanner s2      = s1.useDelimiter(SPACES);
  304.                      Scanner scanner = s2.useLocale(Locale.US)) {
  305.                     scanner.skip("##");

  306.                     // gps week
  307.                     pi.file.getHeader().setGpsWeek(scanner.nextInt());
  308.                     // seconds of week
  309.                     pi.file.getHeader().setSecondsOfWeek(scanner.nextDouble());
  310.                     // epoch interval
  311.                     pi.file.getHeader().setEpochInterval(scanner.nextDouble());
  312.                     // modified julian day
  313.                     pi.file.getHeader().setModifiedJulianDay(scanner.nextInt());
  314.                     // day fraction
  315.                     pi.file.getHeader().setDayFraction(scanner.nextDouble());
  316.                 }
  317.             }

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

  323.         },

  324.         /** Parser for satellites identifiers. */
  325.         HEADER_SAT_IDS("^\\+ .*") {

  326.             /** {@inheritDoc} */
  327.             @Override
  328.             public void parse(final String line, final ParseInfo pi) {

  329.                 if (pi.maxSatellites == 0) {
  330.                     // this is the first ids line, it also contains the number of satellites
  331.                     pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
  332.                 }

  333.                 final int lineLength = line.length();
  334.                 int count = pi.file.getSatelliteCount();
  335.                 int startIdx = 9;
  336.                 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
  337.                     final String satId = line.substring(startIdx, startIdx + 3).trim();
  338.                     if (!satId.isEmpty()) {
  339.                         pi.file.addSatellite(satId);
  340.                     }
  341.                     startIdx += 3;
  342.                 }
  343.             }

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

  349.         },

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

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

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

  375.         },

  376.         /** Parser for time system. */
  377.         HEADER_TIME_SYSTEM("^%c.*") {

  378.             /** {@inheritDoc} */
  379.             @Override
  380.             public void parse(final String line, final ParseInfo pi) {

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

  384.                     // now identify the time system in use
  385.                     final String tsStr = line.substring(9, 12).trim();
  386.                     final TimeSystem ts;
  387.                     if (tsStr.equalsIgnoreCase("ccc")) {
  388.                         ts = TimeSystem.GPS;
  389.                     } else {
  390.                         ts = TimeSystem.parseTimeSystem(tsStr);
  391.                     }
  392.                     pi.file.getHeader().setTimeSystem(ts);
  393.                     pi.timeScale = ts.getTimeScale(pi.parser.timeScales);

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

  397.             }

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

  403.         },

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

  406.             /** {@inheritDoc} */
  407.             @Override
  408.             public void parse(final String line, final ParseInfo pi) {
  409.                 final double posVelBase = Double.parseDouble(line.substring(3, 13).trim());
  410.                 if (posVelBase != 0.0) {
  411.                     // (mm or 10⁻⁴ mm/s)
  412.                     pi.file.getHeader().setPosVelBase(posVelBase);
  413.                 }

  414.                 final double clockBase = Double.parseDouble(line.substring(14, 26).trim());
  415.                 if (clockBase != 0.0) {
  416.                     // (ps or 10⁻⁴ ps/s)
  417.                     pi.file.getHeader().setClockBase(clockBase);
  418.                 }
  419.             }

  420.             /** {@inheritDoc} */
  421.             @Override
  422.             public Iterable<LineParser> allowedNext() {
  423.                 return Arrays.asList(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
  424.             }

  425.         },

  426.         /** Parser for custom parameters. */
  427.         HEADER_CUSTOM_PARAMETERS("^%i.*") {

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

  433.             /** {@inheritDoc} */
  434.             @Override
  435.             public Iterable<LineParser> allowedNext() {
  436.                 return Arrays.asList(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
  437.             }

  438.         },

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

  441.             /** {@inheritDoc} */
  442.             @Override
  443.             public void parse(final String line, final ParseInfo pi) {
  444.                 pi.file.getHeader().addComment(line.substring(line.indexOf('*') + 1).trim());
  445.             }

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

  451.         },

  452.         /** Parser for epoch. */
  453.         DATA_EPOCH("^\\* .*") {

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

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

  552.             }

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

  571.             /** {@inheritDoc} */
  572.             @Override
  573.             public Iterable<LineParser> allowedNext() {
  574.                 return Collections.singleton(DATA_POSITION);
  575.             }

  576.         },

  577.         /** Parser for position. */
  578.         DATA_POSITION("^P.*") {

  579.             /** {@inheritDoc} */
  580.             @Override
  581.             public void parse(final String line, final ParseInfo pi) {
  582.                 final String satelliteId = line.substring(1, 4).trim();

  583.                 if (!pi.file.containsSatellite(satelliteId)) {
  584.                     pi.latestPosition = Vector3D.ZERO;
  585.                 } else {

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

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

  591.                     // clock (microsec)
  592.                     final double clockField = line.trim().length() <= 46 ?
  593.                                               SP3Utils.DEFAULT_CLOCK_VALUE :
  594.                                               Double.parseDouble(line.substring(46, 60).trim());
  595.                     pi.latestClock = FastMath.abs(clockField - SP3Utils.DEFAULT_CLOCK_VALUE) < 1.0e-6 ?
  596.                                      Double.NaN : SP3Utils.CLOCK_UNIT.toSI(clockField);

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

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

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

  621.                         pi.latestClockEvent         = line.length() >= 75 && line.charAt(74) == 'E';
  622.                         pi.latestClockPrediction    = line.length() >= 76 && line.charAt(75) == 'P';
  623.                         pi.latestOrbitManeuverEvent = line.length() >= 79 && line.charAt(78) == 'M';
  624.                         pi.latestOrbitPrediction    = line.length() >= 80 && line.charAt(79) == 'P';

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

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

  644.         },

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

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

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

  657.         },

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

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

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

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

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

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

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

  693.                     final double clockRateAccuracy;
  694.                     if (line.length() < 73 || line.substring(70, 73).trim().isEmpty()) {
  695.                         clockRateAccuracy = Double.NaN;
  696.                     } else {
  697.                         clockRateAccuracy = SP3Utils.siAccuracy(SP3Utils.CLOCK_RATE_ACCURACY_UNIT,
  698.                                                                 header.getClockBase(),
  699.                                                                 Integer.parseInt(line.substring(70, 73).trim()));
  700.                     }

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

  712.             /** {@inheritDoc} */
  713.             @Override
  714.             public Iterable<LineParser> allowedNext() {
  715.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
  716.             }

  717.         },

  718.         /** Parser for velocity correlation. */
  719.         DATA_VELOCITY_CORRELATION("^EV.*") {

  720.             /** {@inheritDoc} */
  721.             @Override
  722.             public void parse(final String line, final ParseInfo pi) {
  723.                 // ignored for now
  724.             }

  725.             /** {@inheritDoc} */
  726.             @Override
  727.             public Iterable<LineParser> allowedNext() {
  728.                 return Arrays.asList(DATA_EPOCH, DATA_POSITION, EOF);
  729.             }

  730.         },

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

  733.             /** {@inheritDoc} */
  734.             @Override
  735.             public void parse(final String line, final ParseInfo pi) {
  736.                 pi.done = true;
  737.             }

  738.             /** {@inheritDoc} */
  739.             @Override
  740.             public Iterable<LineParser> allowedNext() {
  741.                 return Collections.singleton(EOF);
  742.             }

  743.         };

  744.         /** Pattern for identifying line. */
  745.         private final Pattern pattern;

  746.         /** Simple constructor.
  747.          * @param lineRegexp regular expression for identifying line
  748.          */
  749.         LineParser(final String lineRegexp) {
  750.             pattern = Pattern.compile(lineRegexp);
  751.         }

  752.         /** Parse a line.
  753.          * @param line line to parse
  754.          * @param pi holder for transient data
  755.          */
  756.         public abstract void parse(String line, ParseInfo pi);

  757.         /** Get the allowed parsers for next line.
  758.          * @return allowed parsers for next line
  759.          */
  760.         public abstract Iterable<LineParser> allowedNext();

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

  768.     }

  769. }