SP3Writer.java

  1. /* Copyright 2022-2025 Luc Maisonobe
  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.IOException;
  19. import java.util.Iterator;
  20. import java.util.List;
  21. import java.util.Locale;
  22. import java.util.Map;

  23. import org.hipparchus.util.FastMath;
  24. import org.orekit.gnss.TimeSystem;
  25. import org.orekit.time.AbsoluteDate;
  26. import org.orekit.time.DateTimeComponents;
  27. import org.orekit.time.TimeScale;
  28. import org.orekit.time.TimeScales;
  29. import org.orekit.utils.CartesianDerivativesFilter;

  30. /** Writer for SP3 file.
  31.  * @author Luc Maisonobe
  32.  * @since 12.0
  33.  */
  34. public class SP3Writer {

  35.     /** End Of Line. */
  36.     private static final String EOL = System.lineSeparator();

  37.     /** Prefix for accuracy lines. */
  38.     private static final String ACCURACY_LINE_PREFIX = "++       ";

  39.     /** Prefix for comment lines. */
  40.     private static final String COMMENT_LINE_PREFIX = "/* ";

  41.     /** Format for accuracy base lines. */
  42.     private static final String ACCURACY_BASE_FORMAT = "%%f %10.7f %12.9f %14.11f %18.15f%n";

  43.     /** Constant additional parameters lines. */
  44.     private static final String ADDITIONAL_PARAMETERS_LINE = "%i    0    0    0    0      0      0      0      0         0";

  45.     /** Format for one 2 digits integer field. */
  46.     private static final String TWO_DIGITS_INTEGER = "%2d";

  47.     /** Format for one 3 digits integer field. */
  48.     private static final String THREE_DIGITS_INTEGER = "%3d";

  49.     /** Format for one 14.6 digits float field. */
  50.     private static final String FOURTEEN_SIX_DIGITS_FLOAT = "%14.6f";

  51.     /** Format for three blanks field. */
  52.     private static final String THREE_BLANKS = "   ";

  53.     /** Time system default line. */
  54.     private static final String TIME_SYSTEM_DEFAULT = "%c cc cc ccc ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc";

  55.     /** Destination of generated output. */
  56.     private final Appendable output;

  57.     /** Output name for error messages. */
  58.     private final String outputName;

  59.     /** Set of time scales used for parsing dates. */
  60.     private final TimeScales timeScales;

  61.     /** Simple constructor.
  62.      * @param output destination of generated output
  63.      * @param outputName output name for error messages
  64.      * @param timeScales set of time scales used for parsing dates
  65.      */
  66.     public SP3Writer(final Appendable output, final String outputName, final TimeScales timeScales) {
  67.         this.output     = output;
  68.         this.outputName = outputName;
  69.         this.timeScales = timeScales;
  70.     }

  71.     /** Write a SP3 file.
  72.      * @param sp3 SP3 file to write
  73.      * @exception IOException if an I/O error occurs.
  74.      */
  75.     public void write(final SP3 sp3)
  76.         throws IOException {
  77.         sp3.validate(false, outputName);
  78.         writeHeader(sp3.getHeader());

  79.         // set up iterators for all satellites
  80.         final CoordinatesIterator[] iterators = new CoordinatesIterator[sp3.getSatelliteCount()];
  81.         int k = 0;
  82.         for (final Map.Entry<String, SP3Ephemeris> entry : sp3.getSatellites().entrySet()) {
  83.             iterators[k++] = new CoordinatesIterator(entry.getValue());
  84.         }

  85.         final TimeScale timeScale  = sp3.getHeader().getTimeSystem().getTimeScale(timeScales);
  86.         for (AbsoluteDate date = earliest(iterators); !date.equals(AbsoluteDate.FUTURE_INFINITY); date = earliest(iterators)) {

  87.             // epoch
  88.             final DateTimeComponents dtc = date.getComponents(timeScale).roundIfNeeded(60, 8);
  89.             output.append(String.format(Locale.US, "*  %4d %2d %2d %2d %2d %11.8f%n",
  90.                                         dtc.getDate().getYear(),
  91.                                         dtc.getDate().getMonth(),
  92.                                         dtc.getDate().getDay(),
  93.                                         dtc.getTime().getHour(),
  94.                                         dtc.getTime().getMinute(),
  95.                                         dtc.getTime().getSecond()));

  96.             for (final CoordinatesIterator iter : iterators) {

  97.                 final SP3Coordinate coordinate;
  98.                 if (iter.pending != null &&
  99.                     FastMath.abs(iter.pending.getDate().durationFrom(date)) <= 0.001 * sp3.getHeader().getEpochInterval()) {
  100.                     // the pending coordinate date matches current epoch
  101.                     coordinate = iter.pending;
  102.                     iter.advance();
  103.                 } else {
  104.                     // the pending coordinate  does not match current epoch
  105.                     coordinate = SP3Coordinate.DUMMY;
  106.                 }

  107.                 // position
  108.                 writePosition(sp3.getHeader(), iter.id, coordinate);

  109.                 if (sp3.getHeader().getFilter() != CartesianDerivativesFilter.USE_P) {
  110.                     // velocity
  111.                     writeVelocity(sp3.getHeader(), iter.id, coordinate);
  112.                 }

  113.             }

  114.         }

  115.         output.append("EOF").
  116.                append(EOL);

  117.     }

  118.     /** Find earliest date in ephemerides.
  119.      * @param iterators ephemerides iterators
  120.      * @return earliest date in iterators
  121.      */
  122.     private AbsoluteDate earliest(final CoordinatesIterator[] iterators) {
  123.         AbsoluteDate date = AbsoluteDate.FUTURE_INFINITY;
  124.         for (final CoordinatesIterator iter : iterators) {
  125.             if (iter.pending != null && iter.pending.getDate().isBefore(date)) {
  126.                 date = iter.pending.getDate();
  127.             }
  128.         }
  129.         return date;
  130.     }

  131.     /** Write position.
  132.      * @param header file header
  133.      * @param satId satellite id
  134.      * @param coordinate coordinate
  135.      * @exception IOException if an I/O error occurs.
  136.      */
  137.     private void writePosition(final SP3Header header, final String satId, final SP3Coordinate coordinate)
  138.         throws IOException {

  139.         final StringBuilder lineBuilder = new StringBuilder();

  140.         // position
  141.         lineBuilder.append(String.format(Locale.US, "P%3s%14.6f%14.6f%14.6f",
  142.                                          satId,
  143.                                          SP3Utils.POSITION_UNIT.fromSI(coordinate.getPosition().getX()),
  144.                                          SP3Utils.POSITION_UNIT.fromSI(coordinate.getPosition().getY()),
  145.                                          SP3Utils.POSITION_UNIT.fromSI(coordinate.getPosition().getZ())));

  146.         // clock
  147.         lineBuilder.append(String.format(Locale.US, FOURTEEN_SIX_DIGITS_FLOAT,
  148.                                          SP3Utils.CLOCK_UNIT.fromSI(coordinate.getClockCorrection())));

  149.         // position accuracy
  150.         if (coordinate.getPositionAccuracy() == null) {
  151.             lineBuilder.append(THREE_BLANKS).
  152.                         append(THREE_BLANKS).
  153.                         append(THREE_BLANKS);
  154.         } else {
  155.             lineBuilder.append(' ');
  156.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  157.                                              SP3Utils.indexAccuracy(SP3Utils.POSITION_ACCURACY_UNIT, header.getPosVelBase(),
  158.                                                                     coordinate.getPositionAccuracy().getX())));
  159.             lineBuilder.append(' ');
  160.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  161.                                              SP3Utils.indexAccuracy(SP3Utils.POSITION_ACCURACY_UNIT, header.getPosVelBase(),
  162.                                                                     coordinate.getPositionAccuracy().getY())));
  163.             lineBuilder.append(' ');
  164.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  165.                                              SP3Utils.indexAccuracy(SP3Utils.POSITION_ACCURACY_UNIT, header.getPosVelBase(),
  166.                                                                     coordinate.getPositionAccuracy().getZ())));
  167.         }

  168.         // clock accuracy
  169.         lineBuilder.append(' ');
  170.         if (Double.isNaN(coordinate.getClockAccuracy())) {
  171.             lineBuilder.append(THREE_BLANKS);
  172.         } else {
  173.             lineBuilder.append(String.format(Locale.US, THREE_DIGITS_INTEGER,
  174.                                              SP3Utils.indexAccuracy(SP3Utils.CLOCK_ACCURACY_UNIT, header.getClockBase(),
  175.                                                                     coordinate.getClockAccuracy())));
  176.         }

  177.         // events
  178.         lineBuilder.append(' ');
  179.         lineBuilder.append(coordinate.hasClockEvent()         ? 'E' : ' ');
  180.         lineBuilder.append(coordinate.hasClockPrediction()    ? 'P' : ' ');
  181.         lineBuilder.append(' ');
  182.         lineBuilder.append(' ');
  183.         lineBuilder.append(coordinate.hasOrbitManeuverEvent() ? 'M' : ' ');
  184.         lineBuilder.append(coordinate.hasOrbitPrediction()    ? 'P' : ' ');

  185.         output.append(lineBuilder.toString().trim()).append(EOL);

  186.     }

  187.     /** Write velocity.
  188.      * @param header file header
  189.      * @param satId satellite id
  190.      * @param coordinate coordinate
  191.      * @exception IOException if an I/O error occurs.
  192.      */
  193.     private void writeVelocity(final SP3Header header, final String satId, final SP3Coordinate coordinate)
  194.         throws IOException {

  195.         final StringBuilder lineBuilder = new StringBuilder();
  196.          // velocity
  197.         lineBuilder.append(String.format(Locale.US, "V%3s%14.6f%14.6f%14.6f",
  198.                                          satId,
  199.                                          SP3Utils.VELOCITY_UNIT.fromSI(coordinate.getVelocity().getX()),
  200.                                          SP3Utils.VELOCITY_UNIT.fromSI(coordinate.getVelocity().getY()),
  201.                                          SP3Utils.VELOCITY_UNIT.fromSI(coordinate.getVelocity().getZ())));

  202.         // clock rate
  203.         lineBuilder.append(String.format(Locale.US, FOURTEEN_SIX_DIGITS_FLOAT,
  204.                                          SP3Utils.CLOCK_RATE_UNIT.fromSI(coordinate.getClockRateChange())));

  205.         // velocity accuracy
  206.         if (coordinate.getVelocityAccuracy() == null) {
  207.             lineBuilder.append(THREE_BLANKS).
  208.                         append(THREE_BLANKS).
  209.                         append(THREE_BLANKS);
  210.         } else {
  211.             lineBuilder.append(' ');
  212.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  213.                                              SP3Utils.indexAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT, header.getPosVelBase(),
  214.                                                                     coordinate.getVelocityAccuracy().getX())));
  215.             lineBuilder.append(' ');
  216.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  217.                                              SP3Utils.indexAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT, header.getPosVelBase(),
  218.                                                                     coordinate.getVelocityAccuracy().getY())));
  219.             lineBuilder.append(' ');
  220.             lineBuilder.append(String.format(Locale.US, TWO_DIGITS_INTEGER,
  221.                                              SP3Utils.indexAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT, header.getPosVelBase(),
  222.                                                                     coordinate.getVelocityAccuracy().getZ())));
  223.         }

  224.         // clock rate accuracy
  225.         lineBuilder.append(' ');
  226.         if (Double.isNaN(coordinate.getClockRateAccuracy())) {
  227.             lineBuilder.append(THREE_BLANKS);
  228.         } else {
  229.             lineBuilder.append(String.format(Locale.US, THREE_DIGITS_INTEGER,
  230.                                              SP3Utils.indexAccuracy(SP3Utils.CLOCK_RATE_ACCURACY_UNIT, header.getClockBase(),
  231.                                                                     coordinate.getClockRateAccuracy())));
  232.         }

  233.         output.append(lineBuilder.toString().trim()).append(EOL);

  234.     }

  235.     /** Write header.
  236.      * @param header SP3 header to write
  237.      * @exception IOException if an I/O error occurs.
  238.      */
  239.     private void writeHeader(final SP3Header header)
  240.         throws IOException {
  241.         final TimeScale timeScale = header.getTimeSystem().getTimeScale(timeScales);
  242.         final DateTimeComponents dtc = header.getEpoch().getComponents(timeScale).roundIfNeeded(60, 8);
  243.         final StringBuilder dataUsedBuilder = new StringBuilder();
  244.         for (final DataUsed du : header.getDataUsed()) {
  245.             if (dataUsedBuilder.length() > 0) {
  246.                 dataUsedBuilder.append('+');
  247.             }
  248.             dataUsedBuilder.append(du.getKey());
  249.         }
  250.         final String dataUsed = dataUsedBuilder.length() <= 5 ?
  251.                                 dataUsedBuilder.toString() :
  252.                                 DataUsed.MIXED.getKey();

  253.         // header first line: version, epoch...
  254.         output.append(String.format(Locale.US, "#%c%c%4d %2d %2d %2d %2d %11.8f %7d %5s %5s %3s %4s%n",
  255.                                     header.getVersion(),
  256.                                     header.getFilter() == CartesianDerivativesFilter.USE_P ? 'P' : 'V',
  257.                                     dtc.getDate().getYear(),
  258.                                     dtc.getDate().getMonth(),
  259.                                     dtc.getDate().getDay(),
  260.                                     dtc.getTime().getHour(),
  261.                                     dtc.getTime().getMinute(),
  262.                                     dtc.getTime().getSecond(),
  263.                                     header.getNumberOfEpochs(),
  264.                                     dataUsed,
  265.                                     header.getCoordinateSystem(),
  266.                                     header.getOrbitTypeKey(),
  267.                                     header.getAgency()));

  268.         // header second line : dates
  269.         output.append(String.format(Locale.US, "## %4d %15.8f %14.8f %5d %15.13f%n",
  270.                                     header.getGpsWeek(),
  271.                                     header.getSecondsOfWeek(),
  272.                                     header.getEpochInterval(),
  273.                                     header.getModifiedJulianDay(),
  274.                                     header.getDayFraction()));

  275.         // list of satellites
  276.         final List<String> satellites = header.getSatIds();
  277.         output.append(String.format(Locale.US, "+  %3d   ", satellites.size()));
  278.         int lines  = 0;
  279.         int column = 9;
  280.         int remaining = satellites.size();
  281.         for (final String satId : satellites) {
  282.             output.append(String.format(Locale.US, "%3s", satId));
  283.             --remaining;
  284.             column += 3;
  285.             if (column >= 60 && remaining > 0) {
  286.                 // finish line
  287.                 output.append(EOL);
  288.                 ++lines;

  289.                 // start new line
  290.                 output.append("+        ");
  291.                 column = 9;
  292.             }
  293.         }
  294.         while (column < 60) {
  295.             output.append(' ').
  296.                    append(' ').
  297.                    append('0');
  298.             column += 3;
  299.         }
  300.         output.append(EOL);
  301.         ++lines;
  302.         while (lines++ < 5) {
  303.             // write extra lines to have at least 85 satellites
  304.             output.append("+          0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0").
  305.                    append(EOL);
  306.         }

  307.         // general accuracy
  308.         output.append(ACCURACY_LINE_PREFIX);
  309.         lines  = 0;
  310.         column = 9;
  311.         remaining = satellites.size();
  312.         for (final String satId : satellites) {
  313.             final double accuracy    = header.getAccuracy(satId);
  314.             final int    accuracyExp = SP3Utils.indexAccuracy(SP3Utils.POSITION_ACCURACY_UNIT, SP3Utils.POS_VEL_BASE_ACCURACY, accuracy);
  315.             output.append(String.format(Locale.US, THREE_DIGITS_INTEGER, accuracyExp));
  316.             --remaining;
  317.             column += 3;
  318.             if (column >= 60 && remaining > 0) {
  319.                 // finish line
  320.                 output.append(EOL);
  321.                 ++lines;

  322.                 // start new line
  323.                 output.append(ACCURACY_LINE_PREFIX);
  324.                 column = 9;
  325.             }
  326.         }
  327.         while (column < 60) {
  328.             output.append(' ').
  329.                    append(' ').
  330.                    append('0');
  331.             column += 3;
  332.         }
  333.         output.append(EOL);
  334.         ++lines;
  335.         while (lines++ < 5) {
  336.             // write extra lines to have at least 85 satellites
  337.             output.append("++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0").
  338.                    append(EOL);
  339.         }

  340.         // type
  341.         if (header.getVersion() == 'a') {
  342.             output.append(TIME_SYSTEM_DEFAULT).append(EOL);
  343.         } else {
  344.             final TimeSystem ts = header.getTimeSystem().getKey() == null ?
  345.                                   TimeSystem.UTC :
  346.                                   header.getTimeSystem();
  347.             output.append(String.format(Locale.US, "%%c %1s  cc %3s ccc cccc cccc cccc cccc ccccc ccccc ccccc ccccc%n",
  348.                                         header.getType().getKey(), ts.getKey()));
  349.         }
  350.         output.append(TIME_SYSTEM_DEFAULT).append(EOL);

  351.         // entries accuracy
  352.         output.append(String.format(Locale.US, ACCURACY_BASE_FORMAT,
  353.                                     header.getPosVelBase(), header.getClockBase(), 0.0, 0.0));
  354.         output.append(String.format(Locale.US, ACCURACY_BASE_FORMAT,
  355.                                     0.0, 0.0, 0.0, 0.0));

  356.         // additional parameters
  357.         output.append(ADDITIONAL_PARAMETERS_LINE).append(EOL);
  358.         output.append(ADDITIONAL_PARAMETERS_LINE).append(EOL);

  359.         // comments
  360.         int count = 0;
  361.         for (final String comment : header.getComments()) {
  362.             ++count;
  363.             output.append(COMMENT_LINE_PREFIX).append(comment).append(EOL);
  364.         }
  365.         while (count < 4) {
  366.             // add dummy comments to get at least the four comments specified for versions a, b and c
  367.             ++count;
  368.             output.append(COMMENT_LINE_PREFIX).append(EOL);
  369.         }

  370.     }

  371.     /** Iterator for coordinates. */
  372.     private static class CoordinatesIterator {

  373.         /** Satellite ID. */
  374.         private final String id;

  375.         /** Iterator over segments. */
  376.         private Iterator<SP3Segment> segmentsIterator;

  377.         /** Iterator over coordinates. */
  378.         private Iterator<SP3Coordinate> coordinatesIterator;

  379.         /** Pending coordinate. */
  380.         private SP3Coordinate pending;

  381.         /** Simple constructor.
  382.          * @param ephemeris underlying ephemeris
  383.          */
  384.         CoordinatesIterator(final SP3Ephemeris ephemeris) {
  385.             this.id                  = ephemeris.getId();
  386.             this.segmentsIterator    = ephemeris.getSegments().iterator();
  387.             this.coordinatesIterator = null;
  388.             advance();
  389.         }

  390.         /** Advance to next coordinates.
  391.          */
  392.         private void advance() {

  393.             while (coordinatesIterator == null || !coordinatesIterator.hasNext()) {
  394.                 // we have exhausted previous segment
  395.                 if (segmentsIterator != null && segmentsIterator.hasNext()) {
  396.                     coordinatesIterator = segmentsIterator.next().getCoordinates().iterator();
  397.                 } else {
  398.                     // we have exhausted the ephemeris
  399.                     segmentsIterator = null;
  400.                     pending          = null;
  401.                     return;
  402.                 }
  403.             }

  404.             // retrieve the next entry
  405.             pending = coordinatesIterator.next();

  406.         }

  407.     }

  408. }