StreamingOemWriter.java

  1. /* Contributed in the public domain.
  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.ccsds;

  18. import java.io.IOException;
  19. import java.text.DecimalFormat;
  20. import java.text.DecimalFormatSymbols;
  21. import java.time.ZoneOffset;
  22. import java.time.ZonedDateTime;
  23. import java.time.format.DateTimeFormatter;
  24. import java.util.Arrays;
  25. import java.util.Collections;
  26. import java.util.Date;
  27. import java.util.LinkedHashMap;
  28. import java.util.List;
  29. import java.util.Locale;
  30. import java.util.Map;

  31. import org.hipparchus.exception.LocalizedCoreFormats;
  32. import org.hipparchus.linear.RealMatrix;
  33. import org.orekit.bodies.CelestialBodyFactory;
  34. import org.orekit.errors.OrekitException;
  35. import org.orekit.errors.OrekitMessages;
  36. import org.orekit.files.ccsds.OEMFile.CovarianceMatrix;
  37. import org.orekit.frames.FactoryManagedFrame;
  38. import org.orekit.frames.Frame;
  39. import org.orekit.frames.LOFType;
  40. import org.orekit.frames.Predefined;
  41. import org.orekit.frames.VersionedITRF;
  42. import org.orekit.propagation.Propagator;
  43. import org.orekit.propagation.SpacecraftState;
  44. import org.orekit.propagation.sampling.OrekitFixedStepHandler;
  45. import org.orekit.time.AbsoluteDate;
  46. import org.orekit.time.DateTimeComponents;
  47. import org.orekit.time.TimeComponents;
  48. import org.orekit.time.TimeScale;
  49. import org.orekit.utils.TimeStampedPVCoordinates;

  50. /**
  51.  * A writer for OEM files.
  52.  *
  53.  * <p> Each instance corresponds to a single OEM file. A new OEM ephemeris segment is
  54.  * started by calling {@link #newSegment(Frame, Map)}.
  55.  *
  56.  * <h3> Metadata </h3>
  57.  *
  58.  * <p> The OEM metadata used by this writer is described in the following table. Many
  59.  * metadata items are optional or have default values so they do not need to be specified.
  60.  * At a minimum the user must supply those values that are required and for which no
  61.  * default exits: {@link Keyword#OBJECT_NAME}, and {@link Keyword#OBJECT_ID}. The usage
  62.  * column in the table indicates where the metadata item is used, either in the OEM header
  63.  * or in the metadata section at the start of an OEM ephemeris segment.
  64.  *
  65.  * <p> The OEM metadata for the whole OEM file is set in the {@link
  66.  * #StreamingOemWriter(Appendable, TimeScale, Map) constructor}. Any of the metadata may
  67.  * be overridden for a particular segment using the {@code metadata} argument to {@link
  68.  * #newSegment(Frame, Map)}.
  69.  *
  70.  * <table summary="OEM metada">
  71.  *     <thead>
  72.  *         <tr>
  73.  *             <th>Keyword
  74.  *             <th>Usage
  75.  *             <th>Obligatory
  76.  *             <th>Default
  77.  *             <th>Reference
  78.  *    </thead>
  79.  *    <tbody>
  80.  *        <tr>
  81.  *            <td>{@link Keyword#CCSDS_OEM_VERS}
  82.  *            <td>Header
  83.  *            <td>Yes
  84.  *            <td>{@link #CCSDS_OEM_VERS}
  85.  *            <td>Table 5-2
  86.  *        <tr>
  87.  *            <td>{@link Keyword#COMMENT}
  88.  *            <td>Header
  89.  *            <td>No
  90.  *            <td>
  91.  *            <td>Table 5-2
  92.  *        <tr>
  93.  *            <td>{@link Keyword#CREATION_DATE}
  94.  *            <td>Header
  95.  *            <td>Yes
  96.  *            <td>{@link Date#Date() Now}
  97.  *            <td>Table 5.2, 6.5.9
  98.  *        <tr>
  99.  *            <td>{@link Keyword#ORIGINATOR}
  100.  *            <td>Header
  101.  *            <td>Yes
  102.  *            <td>{@link #DEFAULT_ORIGINATOR}
  103.  *            <td>Table 5-2
  104.  *        <tr>
  105.  *            <td>{@link Keyword#OBJECT_NAME}
  106.  *            <td>Segment
  107.  *            <td>Yes
  108.  *            <td>
  109.  *            <td>Table 5-3
  110.  *        <tr>
  111.  *            <td>{@link Keyword#OBJECT_ID}
  112.  *            <td>Segment
  113.  *            <td>Yes
  114.  *            <td>
  115.  *            <td>Table 5-3
  116.  *        <tr>
  117.  *            <td>{@link Keyword#CENTER_NAME}
  118.  *            <td>Segment
  119.  *            <td>Yes
  120.  *            <td>Guessed from the {@link #newSegment(Frame, Map) segment}'s {@code frame}
  121.  *            <td>Table 5-3
  122.  *        <tr>
  123.  *            <td>{@link Keyword#REF_FRAME}
  124.  *            <td>Segment
  125.  *            <td>Yes
  126.  *            <td>Guessed from the {@link #newSegment(Frame, Map) segment}'s {@code frame}
  127.  *            <td>Table 5-3, Annex A
  128.  *        <tr>
  129.  *            <td>{@link Keyword#REF_FRAME_EPOCH}
  130.  *            <td>Segment
  131.  *            <td>No
  132.  *            <td>
  133.  *            <td>Table 5-3, 6.5.9
  134.  *        <tr>
  135.  *            <td>{@link Keyword#TIME_SYSTEM}
  136.  *            <td>Segment
  137.  *            <td>Yes
  138.  *            <td>Guessed from {@code timeScale} set in the
  139.  *                {@link #StreamingOemWriter(Appendable, TimeScale, Map) constructor}.
  140.  *            <td>Table 5-3, Annex A
  141.  *        <tr>
  142.  *            <td>{@link Keyword#START_TIME}
  143.  *            <td>Segment
  144.  *            <td>Yes
  145.  *            <td>Date of initial state in {@link Segment#init(SpacecraftState,
  146.  *                AbsoluteDate, double) Segment.init(...)}
  147.  *            <td>Table 5-3, 6.5.9
  148.  *        <tr>
  149.  *            <td>{@link Keyword#USEABLE_START_TIME}
  150.  *            <td>Segment
  151.  *            <td>No
  152.  *            <td>
  153.  *            <td>Table 5-3, 6.5.9
  154.  *        <tr>
  155.  *            <td>{@link Keyword#STOP_TIME}
  156.  *            <td>Segment
  157.  *            <td>Yes
  158.  *            <td>Target date in {@link Segment#init(SpacecraftState,
  159.  *                AbsoluteDate, double) Segment.init(...)}
  160.  *            <td>Table 5-3, 6.5.9
  161.  *        <tr>
  162.  *            <td>{@link Keyword#USEABLE_STOP_TIME}
  163.  *            <td>Segment
  164.  *            <td>No
  165.  *            <td>
  166.  *            <td>Table 5-3, 6.5.9
  167.  *        <tr>
  168.  *            <td>{@link Keyword#INTERPOLATION}
  169.  *            <td>Segment
  170.  *            <td>No
  171.  *            <td>
  172.  *            <td>Table 5-3
  173.  *        <tr>
  174.  *            <td>{@link Keyword#INTERPOLATION_DEGREE}
  175.  *            <td>Segment
  176.  *            <td>No
  177.  *            <td>
  178.  *            <td>Table 5-3
  179.  *    </tbody>
  180.  *</table>
  181.  *
  182.  * <p> The {@link Keyword#TIME_SYSTEM} must be constant for the whole file and is used
  183.  * to interpret all dates except {@link Keyword#CREATION_DATE}. The guessing algorithm
  184.  * is not guaranteed to work so it is recommended to provide values for {@link
  185.  * Keyword#CENTER_NAME}, {@link Keyword#REF_FRAME}, and {@link Keyword#TIME_SYSTEM} to
  186.  * avoid any bugs associated with incorrect guesses.
  187.  *
  188.  * <p> Standardized values for {@link Keyword#TIME_SYSTEM} are GMST, GPS, ME, MRT, SCLK,
  189.  * TAI, TCB, TDB, TCG, TT, UT1, and UTC. Standardized values for {@link Keyword#REF_FRAME}
  190.  * are EME2000, GCRF, GRC, ICRF, ITRF2000, ITRF-93, ITRF-97, MCI, TDR, TEME, and TOD.
  191.  * Additionally ITRF followed by a four digit year may be used.
  192.  *
  193.  * <h3> Examples </h3>
  194.  *
  195.  * <p> This class can be used as a step handler for a {@link Propagator}, or on its own.
  196.  * Either way the object name and ID must be specified. The following example shows its
  197.  * use as a step handler.
  198.  *
  199.  * <pre>{@code
  200.  * Propagator propagator = ...; // pre-configured propagator
  201.  * Appendable out = ...; // set-up output stream
  202.  * Map<Keyword, String> metadata = new LinkedHashMap<>();
  203.  * metadata.put(Keyword.OBJECT_NAME, "Vanguard");
  204.  * metadata.put(Keyword.OBJECT_ID, "1958-002B");
  205.  * StreamingOemWriter writer = new StreamingOemWriter(out, utc, metadata);
  206.  * writer.writeHeader();
  207.  * Segment segment = writer.newSegment(frame, Collections.emptyMap());
  208.  * propagator.setMasterMode(step, segment);
  209.  * propagator.propagate(startDate, stopDate);
  210.  * }</pre>
  211.  *
  212.  * Alternatively a collection of state vectors can be written without the use of a
  213.  * Propagator. In this case the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME}
  214.  * need to be specified as part of the metadata.
  215.  *
  216.  * <pre>{@code
  217.  * List<TimeStampedPVCoordinates> states = ...; // pre-generated states
  218.  * Appendable out = ...; // set-up output stream
  219.  * Map<Keyword, String> metadata = new LinkedHashMap<>();
  220.  * metadata.put(Keyword.OBJECT_NAME, "Vanguard");
  221.  * metadata.put(Keyword.OBJECT_ID, "1958-002B");
  222.  * StreamingOemWriter writer = new StreamingOemWriter(out, utc, metadata);
  223.  * writer.writeHeader();
  224.  * // manually set start and stop times for this segment
  225.  * Map<Keyword, String> segmentData = new LinkedHashMap<>();
  226.  * segmentData.put(Keyword.START_TIME, start.toString());
  227.  * segmentData.put(Keyword.STOP_TIME, stop.toString());
  228.  * Segment segment = writer.newSegment(frame, segmentData);
  229.  * segment.writeMetadata(); // output metadata block
  230.  * for (TimeStampedPVCoordinates state : states) {
  231.  *     segment.writeEphemerisLine(state);
  232.  * }
  233.  * }</pre>
  234.  *
  235.  * @author Evan Ward
  236.  * @see <a href="https://public.ccsds.org/Pubs/502x0b2c1.pdf">CCSDS 502.0-B-2 Orbit Data
  237.  *      Messages</a>
  238.  * @see <a href="https://public.ccsds.org/Pubs/500x0g3.pdf">CCSDS 500.0-G-3 Navigation
  239.  *      Data Definitions and Conventions</a>
  240.  * @see OEMWriter
  241.  */
  242. public class StreamingOemWriter {

  243.     /** Version number implemented. **/
  244.     public static final String CCSDS_OEM_VERS = "2.0";
  245.     /** Default value for {@link Keyword#ORIGINATOR}. */
  246.     public static final String DEFAULT_ORIGINATOR = "OREKIT";

  247.     /** New line separator for output file. See 6.3.6. */
  248.     private static final String NEW_LINE = "\n";
  249.     /**
  250.      * Standardized locale to use, to ensure files can be exchanged without
  251.      * internationalization issues.
  252.      */
  253.     private static final Locale STANDARDIZED_LOCALE = Locale.US;
  254.     /** String format used for all key/value pair lines. **/
  255.     private static final String KV_FORMAT = "%s = %s%n";
  256.     /** Factor for converting meters to km. */
  257.     private static final double M_TO_KM = 1e-3;
  258.     /** Suffix of the name of the inertial frame attached to a planet. */
  259.     private static final String INERTIAL_FRAME_SUFFIX = "/inertial";

  260.     /** Output stream. */
  261.     private final Appendable writer;
  262.     /** Metadata for this OEM file. */
  263.     private final Map<Keyword, String> metadata;
  264.     /** Time scale for all dates except {@link Keyword#CREATION_DATE}. */
  265.     private final TimeScale timeScale;

  266.     /**
  267.      * Create an OEM writer than streams data to the given output stream.
  268.      *
  269.      * @param writer    The output stream for the OEM file. Most methods will append data
  270.      *                  to this {@code writer}.
  271.      * @param timeScale for all times in the OEM except {@link Keyword#CREATION_DATE}. See
  272.      *                  Section 5.2.4.5 and Annex A.
  273.      * @param metadata  for the satellite. Can be overridden in {@link #newSegment(Frame,
  274.      *                  Map)} for a specific segment. See {@link StreamingOemWriter}.
  275.      */
  276.     public StreamingOemWriter(final Appendable writer,
  277.                               final TimeScale timeScale,
  278.                               final Map<Keyword, String> metadata) {

  279.         this.writer = writer;
  280.         this.timeScale = timeScale;
  281.         this.metadata = new LinkedHashMap<>(metadata);
  282.         // set default metadata
  283.         this.metadata.putIfAbsent(Keyword.CCSDS_OEM_VERS, CCSDS_OEM_VERS);
  284.         // creation date is informational only
  285.         this.metadata.putIfAbsent(Keyword.CREATION_DATE,
  286.                 ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
  287.         this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
  288.         this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
  289.     }

  290.     /**
  291.      * Guesses names from Table 5-3 and Annex A.
  292.      *
  293.      * <p> The goal of this method is to perform the opposite mapping of {@link
  294.      * CCSDSFrame}.
  295.      *
  296.      * @param frame a reference frame for ephemeris output.
  297.      * @return the string to use in the OEM file to identify {@code frame}.
  298.      */
  299.     static String guessFrame(final Frame frame) {
  300.         // define some constant strings to make checkstyle happy
  301.         final String tod = "TOD";
  302.         final String itrf = "ITRF";
  303.         // Try to determine the CCSDS name from Annex A by examining the Orekit name.
  304.         final String name = frame.getName();
  305.         if (Arrays.stream(CCSDSFrame.values())
  306.                 .map(CCSDSFrame::name)
  307.                 .anyMatch(name::equals)) {
  308.             // should handle J2000, GCRF, TEME, and some frames created by OEMParser.
  309.             return name;
  310.         } else if (frame instanceof CcsdsModifiedFrame) {
  311.             return ((CcsdsModifiedFrame) frame).getRefFrame();
  312.         } else if ((CelestialBodyFactory.MARS + INERTIAL_FRAME_SUFFIX).equals(name)) {
  313.             return "MCI";
  314.         } else if ((CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER + INERTIAL_FRAME_SUFFIX)
  315.                 .equals(name)) {
  316.             return "ICRF";
  317.         } else if (name.contains("GTOD")) {
  318.             return "TDR";
  319.         } else if (name.contains(tod)) { // check after GTOD
  320.             return tod;
  321.         } else if (name.contains("Equinox") && name.contains(itrf)) {
  322.             return "GRC";
  323.         } else if (frame instanceof VersionedITRF) {
  324.             return ((VersionedITRF) frame).getITRFVersion().getName().replace("-", "");
  325.         } else if (name.contains("CIO") && name.contains(itrf)) {
  326.             return "ITRF2014";
  327.         } else {
  328.             // don't know how to map it to a CCSDS reference frame
  329.             return name;
  330.         }
  331.     }

  332.     /**
  333.      * Guess the name of the center of the reference frame.
  334.      *
  335.      * @param frame a reference frame for ephemeris output.
  336.      * @return the string to use in the OEM file to describe the origin of {@code frame}.
  337.      */
  338.     static String guessCenter(final Frame frame) {
  339.         final String name = frame.getName();
  340.         if (name.endsWith(INERTIAL_FRAME_SUFFIX) || name.endsWith("/rotating")) {
  341.             return name.substring(0, name.length() - 9).toUpperCase(STANDARDIZED_LOCALE);
  342.         } else if (frame instanceof CcsdsModifiedFrame) {
  343.             return ((CcsdsModifiedFrame) frame).getCenterName();
  344.         } else if (frame.getName().equals(Predefined.ICRF.getName())) {
  345.             return CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER.toUpperCase(STANDARDIZED_LOCALE);
  346.         } else if (frame.getDepth() == 0 || frame instanceof FactoryManagedFrame) {
  347.             return "EARTH";
  348.         } else {
  349.             return "UNKNOWN";
  350.         }
  351.     }

  352.     /**
  353.      * Write a single key and value to the stream using Key Value Notation (KVN).
  354.      *
  355.      * @param key   the keyword to write
  356.      * @param value the value to write
  357.      * @throws IOException if an I/O error occurs.
  358.      */
  359.     private void writeKeyValue(final Keyword key, final String value) throws IOException {
  360.         writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
  361.     }

  362.     /**
  363.      * Writes the standard OEM header for the file.
  364.      *
  365.      * @throws IOException if the stream cannot write to stream
  366.      */
  367.     public void writeHeader() throws IOException {
  368.         writeKeyValue(Keyword.CCSDS_OEM_VERS, this.metadata.get(Keyword.CCSDS_OEM_VERS));
  369.         final String comment = this.metadata.get(Keyword.COMMENT);
  370.         if (comment != null) {
  371.             writeKeyValue(Keyword.COMMENT, comment);
  372.         }
  373.         writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
  374.         writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
  375.         writer.append(NEW_LINE);
  376.     }

  377.     /**
  378.      * Create a writer for a new OEM ephemeris segment.
  379.      *
  380.      * <p> The returned writer can only write a single ephemeris segment in an OEM. This
  381.      * method must be called to create a writer for each ephemeris segment.
  382.      *
  383.      * @param frame           the reference frame to use for the segment. If this value is
  384.      *                        {@code null} then {@link Segment#handleStep(SpacecraftState,
  385.      *                        boolean)} will throw a {@link NullPointerException} and the
  386.      *                        metadata item {@link Keyword#REF_FRAME} must be specified in
  387.      *                        the metadata.
  388.      * @param segmentMetadata the metadata to use for the segment. Overrides for this
  389.      *                        segment any other source of meta data values. See {@link
  390.      *                        #StreamingOemWriter} for a description of which metadata are
  391.      *                        required and how they are determined.
  392.      * @return a new OEM segment, ready for writing.
  393.      */
  394.     public Segment newSegment(final Frame frame,
  395.                               final Map<Keyword, String> segmentMetadata) {
  396.         final Map<Keyword, String> meta = new LinkedHashMap<>(this.metadata);
  397.         meta.putAll(segmentMetadata);
  398.         if (!meta.containsKey(Keyword.REF_FRAME)) {
  399.             meta.put(Keyword.REF_FRAME, guessFrame(frame));
  400.         }
  401.         if (!meta.containsKey(Keyword.CENTER_NAME)) {
  402.             meta.put(Keyword.CENTER_NAME, guessCenter(frame));
  403.         }
  404.         return new Segment(frame, meta);
  405.     }

  406.     /** A writer for a segment of an OEM. */
  407.     public class Segment implements OrekitFixedStepHandler {

  408.         /** Reference frame of the output states. */
  409.         private final Frame frame;
  410.         /** Metadata for this OEM Segment. */
  411.         private final Map<Keyword, String> metadata;

  412.         /**
  413.          * Create a new segment writer.
  414.          *
  415.          * @param frame    for the output states. Used by {@link #handleStep(SpacecraftState,
  416.          *                 boolean)}.
  417.          * @param metadata to use when writing this segment.
  418.          */
  419.         private Segment(final Frame frame, final Map<Keyword, String> metadata) {
  420.             this.frame = frame;
  421.             this.metadata = metadata;
  422.         }

  423.         /**
  424.          * Write the ephemeris segment metadata.
  425.          *
  426.          * <p> See {@link StreamingOemWriter} for a description of how the metadata is
  427.          * set.
  428.          *
  429.          * @throws IOException if the output stream throws one while writing.
  430.          */
  431.         public void writeMetadata() throws IOException {
  432.             writer.append("META_START").append(NEW_LINE);
  433.             if (this.frame != null) {
  434.                 writer.append("COMMENT ").append("Orekit frame: ")
  435.                         .append(this.frame.toString()).append(NEW_LINE);
  436.             }
  437.             // Table 5.3
  438.             writeKeyValue(Keyword.OBJECT_NAME, this.metadata.get(Keyword.OBJECT_NAME));
  439.             writeKeyValue(Keyword.OBJECT_ID, this.metadata.get(Keyword.OBJECT_ID));
  440.             writeKeyValue(Keyword.CENTER_NAME, this.metadata.get(Keyword.CENTER_NAME));
  441.             writeKeyValue(Keyword.REF_FRAME, this.metadata.get(Keyword.REF_FRAME));
  442.             final String refFrameEpoch = this.metadata.get(Keyword.REF_FRAME_EPOCH);
  443.             if (refFrameEpoch != null) {
  444.                 writeKeyValue(Keyword.REF_FRAME_EPOCH, refFrameEpoch);
  445.             }
  446.             writeKeyValue(Keyword.TIME_SYSTEM, this.metadata.get(Keyword.TIME_SYSTEM));
  447.             writeKeyValue(Keyword.START_TIME, this.metadata.get(Keyword.START_TIME));
  448.             final String usableStartTime = this.metadata.get(Keyword.USEABLE_START_TIME);
  449.             if (usableStartTime != null) {
  450.                 writeKeyValue(Keyword.USEABLE_START_TIME, usableStartTime);
  451.             }
  452.             writeKeyValue(Keyword.STOP_TIME, this.metadata.get(Keyword.STOP_TIME));
  453.             final String usableStopTime = this.metadata.get(Keyword.USEABLE_STOP_TIME);
  454.             if (usableStopTime != null) {
  455.                 writeKeyValue(Keyword.USEABLE_STOP_TIME, usableStopTime);
  456.             }
  457.             final String interpolation = this.metadata.get(Keyword.INTERPOLATION);
  458.             if (interpolation != null) {
  459.                 writeKeyValue(Keyword.INTERPOLATION, interpolation);
  460.             }
  461.             final String interpolationDegree =
  462.                     this.metadata.get(Keyword.INTERPOLATION_DEGREE);
  463.             if (interpolationDegree != null) {
  464.                 writeKeyValue(Keyword.INTERPOLATION_DEGREE, interpolationDegree);
  465.             }
  466.             writer.append("META_STOP").append(NEW_LINE).append(NEW_LINE);
  467.         }

  468.         /**
  469.          * Write a single ephemeris line according to section 5.2.4. This method does not
  470.          * write the optional acceleration terms.
  471.          *
  472.          * @param pv the time, position, and velocity to write.
  473.          * @throws IOException if the output stream throws one while writing.
  474.          */
  475.         public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
  476.                 throws IOException {
  477.             final String epoch = dateToString(pv.getDate().getComponents(timeScale));
  478.             writer.append(epoch).append(" ");
  479.             // output in km, see Section 6.6.2.1
  480.             writer.append(Double.toString(pv.getPosition().getX() * M_TO_KM)).append(" ");
  481.             writer.append(Double.toString(pv.getPosition().getY() * M_TO_KM)).append(" ");
  482.             writer.append(Double.toString(pv.getPosition().getZ() * M_TO_KM)).append(" ");
  483.             writer.append(Double.toString(pv.getVelocity().getX() * M_TO_KM)).append(" ");
  484.             writer.append(Double.toString(pv.getVelocity().getY() * M_TO_KM)).append(" ");
  485.             writer.append(Double.toString(pv.getVelocity().getZ() * M_TO_KM));
  486.             writer.append(NEW_LINE);
  487.         }

  488.         /**
  489.          * Write covariance matrices of the segment according to section 5.2.5.
  490.          *
  491.          * @param covarianceMatrices the list of covariance matrices related to the segment.
  492.          * @throws IOException if the output stream throws one while writing.
  493.          */
  494.         public void writeCovarianceMatrices(final List<CovarianceMatrix> covarianceMatrices)
  495.                 throws IOException {
  496.             writer.append("COVARIANCE_START").append(NEW_LINE);
  497.             // Sort to ensure having the matrices in chronological order when
  498.             // they are in the same data section (see section 5.2.5.7)
  499.             Collections.sort(covarianceMatrices, (mat1, mat2)->mat1.getEpoch().compareTo(mat2.getEpoch()));
  500.             for (final CovarianceMatrix covarianceMatrix : covarianceMatrices) {
  501.                 final String epoch = dateToString(covarianceMatrix.getEpoch().getComponents(timeScale));
  502.                 writeKeyValue(Keyword.EPOCH, epoch);

  503.                 if (covarianceMatrix.getFrame() != null ) {
  504.                     writeKeyValue(Keyword.COV_REF_FRAME, guessFrame(covarianceMatrix.getFrame()));
  505.                 } else if (covarianceMatrix.getLofType() != null) {
  506.                     if (covarianceMatrix.getLofType() == LOFType.QSW) {
  507.                         writeKeyValue(Keyword.COV_REF_FRAME, "RTN");
  508.                     } else if (covarianceMatrix.getLofType() == LOFType.TNW) {
  509.                         writeKeyValue(Keyword.COV_REF_FRAME, covarianceMatrix.getLofType().name());
  510.                     } else {
  511.                         throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, toString());
  512.                     }
  513.                 }

  514.                 final RealMatrix covRealMatrix = covarianceMatrix.getMatrix();
  515.                 for (int i = 0; i < covRealMatrix.getRowDimension(); i++) {
  516.                     writer.append(Double.toString(covRealMatrix.getEntry(i, 0)));
  517.                     for (int j = 1; j < i + 1; j++) {
  518.                         writer.append(" ").append(Double.toString(covRealMatrix.getEntry(i, j)));
  519.                     }
  520.                     writer.append(NEW_LINE);
  521.                 }
  522.             }
  523.             writer.append("COVARIANCE_STOP").append(NEW_LINE).append(NEW_LINE);
  524.         }

  525.         /**
  526.          * {@inheritDoc}
  527.          *
  528.          * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
  529.          * segment's metadata if not already set by the user. Then calls {@link
  530.          * #writeMetadata()} to start the segment.
  531.          */
  532.         @Override
  533.         public void init(final SpacecraftState s0,
  534.                          final AbsoluteDate t,
  535.                          final double step) {
  536.             try {
  537.                 final String start = dateToString(s0.getDate().getComponents(timeScale));
  538.                 final String stop = dateToString(t.getComponents(timeScale));
  539.                 this.metadata.putIfAbsent(Keyword.START_TIME, start);
  540.                 this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
  541.                 this.writeMetadata();
  542.             } catch (IOException e) {
  543.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  544.                         e.getLocalizedMessage());
  545.             }
  546.         }

  547.         @Override
  548.         public void handleStep(final SpacecraftState s,
  549.                                final boolean isLast) {
  550.             try {
  551.                 writeEphemerisLine(s.getPVCoordinates(this.frame));
  552.             } catch (IOException e) {
  553.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  554.                         e.getLocalizedMessage());
  555.             }

  556.         }

  557.     }

  558.     /**
  559.      * Convert a date to a string with more precision.
  560.      *
  561.      * @param components to convert to a String.
  562.      * @return the String form of {@code date} with at least 9 digits of precision.
  563.      */
  564.     static String dateToString(final DateTimeComponents components) {
  565.         final TimeComponents time = components.getTime();
  566.         final int hour = time.getHour();
  567.         final int minute = time.getMinute();
  568.         final double second = time.getSecond();
  569.         // Decimal formatting classes could be static final if they were thread safe.
  570.         final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
  571.         final DecimalFormat twoDigits = new DecimalFormat("00", locale);
  572.         final DecimalFormat precise = new DecimalFormat("00.0########", locale);
  573.         return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
  574.                 twoDigits.format(minute) + ":" + precise.format(second);
  575.     }

  576. }