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. import org.orekit.utils.formatting.FastDoubleFormatter;
  31. import org.orekit.utils.formatting.FastLongFormatter;

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

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

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

  41.     /** Prefix for comment lines. */
  42.     private static final String COMMENT_LINE_PREFIX = "/* ";

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

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

  47.     /** Format for one 2 digits integer field. */
  48.     private static final FastLongFormatter TWO_DIGITS_INTEGER = new FastLongFormatter(2, false);

  49.     /** Format for one 3 digits integer field. */
  50.     private static final FastLongFormatter THREE_DIGITS_INTEGER = new FastLongFormatter(3, false);

  51.     /** Format for one 14.6 digits float field. */
  52.     private static final FastDoubleFormatter FOURTEEN_SIX_DIGITS_FLOAT = new FastDoubleFormatter(14, 6);

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

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

  57.     /** Destination of generated output. */
  58.     private final Appendable output;

  59.     /** Output name for error messages. */
  60.     private final String outputName;

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

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

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

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

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

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

  98.             for (final CoordinatesIterator iter : iterators) {

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

  109.                 // position
  110.                 writePosition(sp3.getHeader(), iter.id, coordinate);

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

  115.             }

  116.         }

  117.         output.append("EOF").
  118.                append(EOL);

  119.     }

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

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

  141.         final StringBuilder lineBuilder = new StringBuilder();

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

  148.         // clock
  149.         FOURTEEN_SIX_DIGITS_FLOAT.appendTo(lineBuilder,
  150.                                          SP3Utils.CLOCK_UNIT.fromSI(coordinate.getClockCorrection()));

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

  170.         // clock accuracy
  171.         lineBuilder.append(' ');
  172.         if (Double.isNaN(coordinate.getClockAccuracy())) {
  173.             lineBuilder.append(THREE_BLANKS);
  174.         } else {
  175.             THREE_DIGITS_INTEGER.appendTo(lineBuilder,
  176.                                           SP3Utils.indexAccuracy(SP3Utils.CLOCK_ACCURACY_UNIT, header.getClockBase(),
  177.                                                                  coordinate.getClockAccuracy()));
  178.         }

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

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

  188.     }

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

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

  204.         // clock rate
  205.         FOURTEEN_SIX_DIGITS_FLOAT.appendTo(lineBuilder, SP3Utils.CLOCK_RATE_UNIT.fromSI(coordinate.getClockRateChange()));

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

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

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

  235.     }

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

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

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

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

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

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

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

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

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

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

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

  371.     }

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

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

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

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

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

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

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

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

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

  407.         }

  408.     }

  409. }