SP3Writer.java

  1. /* Copyright 2023 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.time.AbsoluteDate;
  25. import org.orekit.time.DateTimeComponents;
  26. import org.orekit.time.TimeScale;
  27. import org.orekit.time.TimeScales;
  28. import org.orekit.utils.CartesianDerivativesFilter;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  95.             for (final CoordinatesIterator iter : iterators) {

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

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

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

  112.             }

  113.         }

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

  116.     }

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

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

  138.         final StringBuilder lineBuilder = new StringBuilder();

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

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

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

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

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

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

  185.     }

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

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

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

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

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

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

  233.     }

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

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

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

  274.         // list of satellites
  275.         final List<String> satellites = header.getSatIds();
  276.         output.append(String.format(Locale.US, "+  %3d   ", satellites.size()));
  277.         int lines  = 0;
  278.         int column = 9;
  279.         int remaining = satellites.size();
  280.         for (final String satId : satellites) {
  281.             output.append(String.format(Locale.US, "%3s", satId));
  282.             --remaining;
  283.             column += 3;
  284.             if (column >= 60) {
  285.                 // finish line
  286.                 output.append(EOL);
  287.                 ++lines;
  288.                 if (remaining > 0) {
  289.                     // start new line
  290.                     output.append("+        ");
  291.                     column = 9;
  292.                 }
  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.             output.append(String.format(Locale.US, THREE_DIGITS_INTEGER, accuracyExp));
  317.             --remaining;
  318.             column += 3;
  319.             if (column >= 60) {
  320.                 // finish line
  321.                 output.append(EOL);
  322.                 ++lines;
  323.                 if (remaining > 0) {
  324.                     // start new line
  325.                     output.append(ACCURACY_LINE_PREFIX);
  326.                     column = 9;
  327.                 }
  328.             }
  329.         }
  330.         while (column < 60) {
  331.             output.append(' ').
  332.                    append(' ').
  333.                    append('0');
  334.             column += 3;
  335.         }
  336.         output.append(EOL);
  337.         ++lines;
  338.         while (lines++ < 5) {
  339.             // write extra lines to have at least 85 satellites
  340.             output.append("++         0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0").
  341.                    append(EOL);
  342.         }

  343.         // type
  344.         if (header.getVersion() == 'a') {
  345.             output.append(TIME_SYSTEM_DEFAULT).append(EOL);
  346.         } else {
  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(),
  349.                                         header.getTimeSystem().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. }