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

  244.     /** Version number implemented. **/
  245.     public static final String CCSDS_OEM_VERS = "2.0";

  246.     /** Default value for {@link Keyword#ORIGINATOR}. */
  247.     public static final String DEFAULT_ORIGINATOR = "OREKIT";

  248.     /**
  249.      * Default format used for position ephemeris data output: 3 digits
  250.      * after the decimal point and leading space for positive values.
  251.      */
  252.     public static final String DEFAULT_POSITION_FORMAT = "% .3f";

  253.     /**
  254.      * Default format used for velocity ephemeris data output: 5 digits
  255.      * after the decimal point and leading space for positive values.
  256.      */
  257.     public static final String DEFAULT_VELOCITY_FORMAT = "% .5f";

  258.     /** New line separator for output file. See 6.3.6. */
  259.     private static final String NEW_LINE = "\n";

  260.     /**
  261.      * Standardized locale to use, to ensure files can be exchanged without
  262.      * internationalization issues.
  263.      */
  264.     private static final Locale STANDARDIZED_LOCALE = Locale.US;

  265.     /** String format used for all key/value pair lines. **/
  266.     private static final String KV_FORMAT = "%s = %s%n";

  267.     /** Factor for converting meters to km. */
  268.     private static final double M_TO_KM = 1e-3;

  269.     /** Suffix of the name of the inertial frame attached to a planet. */
  270.     private static final String INERTIAL_FRAME_SUFFIX = "/inertial";

  271.     /** Output stream. */
  272.     private final Appendable writer;

  273.     /** Metadata for this OEM file. */
  274.     private final Map<Keyword, String> metadata;

  275.     /** Time scale for all dates except {@link Keyword#CREATION_DATE}. */
  276.     private final TimeScale timeScale;

  277.     /** Format for position ephemeris data output. */
  278.     private final String positionFormat;

  279.     /** Format for velocity ephemeris data output. */
  280.     private final String velocityFormat;

  281.     /**
  282.      * Create an OEM writer than streams data to the given output stream. Default formatting for
  283.      * {@link #DEFAULT_POSITION_FORMAT position} and {@link #DEFAULT_VELOCITY_FORMAT velocity}
  284.      * will be used for position and velocity ephemeris data.
  285.      *
  286.      * @param writer    The output stream for the OEM file. Most methods will append data
  287.      *                  to this {@code writer}.
  288.      * @param timeScale for all times in the OEM except {@link Keyword#CREATION_DATE}. See
  289.      *                  Section 5.2.4.5 and Annex A.
  290.      * @param metadata  for the satellite. Can be overridden in {@link #newSegment(Frame,
  291.      *                  Map)} for a specific segment. See {@link StreamingOemWriter}.
  292.      */
  293.     public StreamingOemWriter(final Appendable writer,
  294.                               final TimeScale timeScale,
  295.                               final Map<Keyword, String> metadata) {
  296.         this(writer, timeScale, metadata, DEFAULT_POSITION_FORMAT, DEFAULT_VELOCITY_FORMAT);
  297.     }

  298.     /**
  299.      * Create an OEM writer than streams data to the given output stream as
  300.      * {@link #StreamingOemWriter(Appendable, TimeScale, Map)} with
  301.      * {@link java.util.Formatter format parameters} for position and velocity ephemeris data.
  302.      *
  303.      * @param writer    The output stream for the OEM file. Most methods will append data
  304.      *                  to this {@code writer}.
  305.      * @param timeScale for all times in the OEM except {@link Keyword#CREATION_DATE}. See
  306.      *                  Section 5.2.4.5 and Annex A.
  307.      * @param metadata  for the satellite. Can be overridden in {@link #newSegment(Frame,
  308.      *                  Map)} for a specific segment. See {@link StreamingOemWriter}.
  309.      * @param positionFormat format parameters for position ephemeris data output.
  310.      * @param velocityFormat format parameters for velocity ephemeris data output.
  311.      * @since 10.3
  312.      */
  313.     public StreamingOemWriter(final Appendable writer,
  314.                               final TimeScale timeScale,
  315.                               final Map<Keyword, String> metadata,
  316.                               final String positionFormat,
  317.                               final String velocityFormat) {
  318.         this.writer = writer;
  319.         this.timeScale = timeScale;
  320.         this.metadata = new LinkedHashMap<>(metadata);
  321.         // set default metadata
  322.         this.metadata.putIfAbsent(Keyword.CCSDS_OEM_VERS, CCSDS_OEM_VERS);
  323.         // creation date is informational only
  324.         this.metadata.putIfAbsent(Keyword.CREATION_DATE,
  325.                 ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
  326.         this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
  327.         this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
  328.         this.positionFormat = positionFormat;
  329.         this.velocityFormat = velocityFormat;
  330.     }

  331.     /**
  332.      * Guesses names from Table 5-3 and Annex A.
  333.      *
  334.      * <p> The goal of this method is to perform the opposite mapping of {@link
  335.      * CCSDSFrame}.
  336.      *
  337.      * @param frame a reference frame for ephemeris output.
  338.      * @return the string to use in the OEM file to identify {@code frame}.
  339.      */
  340.     static String guessFrame(final Frame frame) {
  341.         // define some constant strings to make checkstyle happy
  342.         final String tod = "TOD";
  343.         final String itrf = "ITRF";
  344.         // Try to determine the CCSDS name from Annex A by examining the Orekit name.
  345.         final String name = frame.getName();
  346.         if (Arrays.stream(CCSDSFrame.values())
  347.                 .map(CCSDSFrame::name)
  348.                 .anyMatch(name::equals)) {
  349.             // should handle J2000, GCRF, TEME, and some frames created by OEMParser.
  350.             return name;
  351.         } else if (frame instanceof CcsdsModifiedFrame) {
  352.             return ((CcsdsModifiedFrame) frame).getRefFrame();
  353.         } else if ((CelestialBodyFactory.MARS + INERTIAL_FRAME_SUFFIX).equals(name)) {
  354.             return "MCI";
  355.         } else if ((CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER + INERTIAL_FRAME_SUFFIX)
  356.                 .equals(name)) {
  357.             return "ICRF";
  358.         } else if (name.contains("GTOD")) {
  359.             return "TDR";
  360.         } else if (name.contains(tod)) { // check after GTOD
  361.             return tod;
  362.         } else if (name.contains("Equinox") && name.contains(itrf)) {
  363.             return "GRC";
  364.         } else if (frame instanceof VersionedITRF) {
  365.             return ((VersionedITRF) frame).getITRFVersion().getName().replace("-", "");
  366.         } else if (name.contains("CIO") && name.contains(itrf)) {
  367.             return "ITRF2014";
  368.         } else {
  369.             // don't know how to map it to a CCSDS reference frame
  370.             return name;
  371.         }
  372.     }

  373.     /**
  374.      * Guess the name of the center of the reference frame.
  375.      *
  376.      * @param frame a reference frame for ephemeris output.
  377.      * @return the string to use in the OEM file to describe the origin of {@code frame}.
  378.      */
  379.     static String guessCenter(final Frame frame) {
  380.         final String name = frame.getName();
  381.         if (name.endsWith(INERTIAL_FRAME_SUFFIX) || name.endsWith("/rotating")) {
  382.             return name.substring(0, name.length() - 9).toUpperCase(STANDARDIZED_LOCALE);
  383.         } else if (frame instanceof CcsdsModifiedFrame) {
  384.             return ((CcsdsModifiedFrame) frame).getCenterName();
  385.         } else if (frame.getName().equals(Predefined.ICRF.getName())) {
  386.             return CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER.toUpperCase(STANDARDIZED_LOCALE);
  387.         } else if (frame.getDepth() == 0 || frame instanceof FactoryManagedFrame) {
  388.             return "EARTH";
  389.         } else {
  390.             return "UNKNOWN";
  391.         }
  392.     }

  393.     /**
  394.      * Write a single key and value to the stream using Key Value Notation (KVN).
  395.      *
  396.      * @param key   the keyword to write
  397.      * @param value the value to write
  398.      * @throws IOException if an I/O error occurs.
  399.      */
  400.     private void writeKeyValue(final Keyword key, final String value) throws IOException {
  401.         writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
  402.     }

  403.     /**
  404.      * Writes the standard OEM header for the file.
  405.      *
  406.      * @throws IOException if the stream cannot write to stream
  407.      */
  408.     public void writeHeader() throws IOException {
  409.         writeKeyValue(Keyword.CCSDS_OEM_VERS, this.metadata.get(Keyword.CCSDS_OEM_VERS));
  410.         final String comment = this.metadata.get(Keyword.COMMENT);
  411.         if (comment != null) {
  412.             writeKeyValue(Keyword.COMMENT, comment);
  413.         }
  414.         writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
  415.         writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
  416.         writer.append(NEW_LINE);
  417.     }

  418.     /**
  419.      * Create a writer for a new OEM ephemeris segment.
  420.      *
  421.      * <p> The returned writer can only write a single ephemeris segment in an OEM. This
  422.      * method must be called to create a writer for each ephemeris segment.
  423.      *
  424.      * @param frame           the reference frame to use for the segment. If this value is
  425.      *                        {@code null} then {@link Segment#handleStep(SpacecraftState,
  426.      *                        boolean)} will throw a {@link NullPointerException} and the
  427.      *                        metadata item {@link Keyword#REF_FRAME} must be specified in
  428.      *                        the metadata.
  429.      * @param segmentMetadata the metadata to use for the segment. Overrides for this
  430.      *                        segment any other source of meta data values. See {@link
  431.      *                        #StreamingOemWriter} for a description of which metadata are
  432.      *                        required and how they are determined.
  433.      * @return a new OEM segment, ready for writing.
  434.      */
  435.     public Segment newSegment(final Frame frame,
  436.                               final Map<Keyword, String> segmentMetadata) {
  437.         final Map<Keyword, String> meta = new LinkedHashMap<>(this.metadata);
  438.         meta.putAll(segmentMetadata);
  439.         if (!meta.containsKey(Keyword.REF_FRAME)) {
  440.             meta.put(Keyword.REF_FRAME, guessFrame(frame));
  441.         }
  442.         if (!meta.containsKey(Keyword.CENTER_NAME)) {
  443.             meta.put(Keyword.CENTER_NAME, guessCenter(frame));
  444.         }
  445.         return new Segment(frame, meta);
  446.     }

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

  449.         /** Reference frame of the output states. */
  450.         private final Frame frame;
  451.         /** Metadata for this OEM Segment. */
  452.         private final Map<Keyword, String> metadata;

  453.         /**
  454.          * Create a new segment writer.
  455.          *
  456.          * @param frame    for the output states. Used by {@link #handleStep(SpacecraftState,
  457.          *                 boolean)}.
  458.          * @param metadata to use when writing this segment.
  459.          */
  460.         private Segment(final Frame frame, final Map<Keyword, String> metadata) {
  461.             this.frame = frame;
  462.             this.metadata = metadata;
  463.         }

  464.         /**
  465.          * Write the ephemeris segment metadata.
  466.          *
  467.          * <p> See {@link StreamingOemWriter} for a description of how the metadata is
  468.          * set.
  469.          *
  470.          * @throws IOException if the output stream throws one while writing.
  471.          */
  472.         public void writeMetadata() throws IOException {
  473.             writer.append("META_START").append(NEW_LINE);
  474.             if (this.frame != null) {
  475.                 writer.append("COMMENT ").append("Orekit frame: ")
  476.                         .append(this.frame.toString()).append(NEW_LINE);
  477.             }
  478.             // Table 5.3
  479.             writeKeyValue(Keyword.OBJECT_NAME, this.metadata.get(Keyword.OBJECT_NAME));
  480.             writeKeyValue(Keyword.OBJECT_ID, this.metadata.get(Keyword.OBJECT_ID));
  481.             writeKeyValue(Keyword.CENTER_NAME, this.metadata.get(Keyword.CENTER_NAME));
  482.             writeKeyValue(Keyword.REF_FRAME, this.metadata.get(Keyword.REF_FRAME));
  483.             final String refFrameEpoch = this.metadata.get(Keyword.REF_FRAME_EPOCH);
  484.             if (refFrameEpoch != null) {
  485.                 writeKeyValue(Keyword.REF_FRAME_EPOCH, refFrameEpoch);
  486.             }
  487.             writeKeyValue(Keyword.TIME_SYSTEM, this.metadata.get(Keyword.TIME_SYSTEM));
  488.             writeKeyValue(Keyword.START_TIME, this.metadata.get(Keyword.START_TIME));
  489.             final String usableStartTime = this.metadata.get(Keyword.USEABLE_START_TIME);
  490.             if (usableStartTime != null) {
  491.                 writeKeyValue(Keyword.USEABLE_START_TIME, usableStartTime);
  492.             }
  493.             writeKeyValue(Keyword.STOP_TIME, this.metadata.get(Keyword.STOP_TIME));
  494.             final String usableStopTime = this.metadata.get(Keyword.USEABLE_STOP_TIME);
  495.             if (usableStopTime != null) {
  496.                 writeKeyValue(Keyword.USEABLE_STOP_TIME, usableStopTime);
  497.             }
  498.             final String interpolation = this.metadata.get(Keyword.INTERPOLATION);
  499.             if (interpolation != null) {
  500.                 writeKeyValue(Keyword.INTERPOLATION, interpolation);
  501.             }
  502.             final String interpolationDegree =
  503.                     this.metadata.get(Keyword.INTERPOLATION_DEGREE);
  504.             if (interpolationDegree != null) {
  505.                 writeKeyValue(Keyword.INTERPOLATION_DEGREE, interpolationDegree);
  506.             }
  507.             writer.append("META_STOP").append(NEW_LINE).append(NEW_LINE);
  508.         }

  509.         /**
  510.          * Write a single ephemeris line according to section 5.2.4. This method does not
  511.          * write the optional acceleration terms.
  512.          *
  513.          * @param pv the time, position, and velocity to write.
  514.          * @throws IOException if the output stream throws one while writing.
  515.          */
  516.         public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
  517.                 throws IOException {
  518.             final String epoch = dateToString(pv.getDate().getComponents(timeScale));
  519.             writer.append(epoch).append(" ");
  520.             // output in km, see Section 6.6.2.1
  521.             writer.append(String.format(STANDARDIZED_LOCALE, positionFormat,
  522.                                         pv.getPosition().getX() * M_TO_KM)).append(" ");
  523.             writer.append(String.format(STANDARDIZED_LOCALE, positionFormat,
  524.                                         pv.getPosition().getY() * M_TO_KM)).append(" ");
  525.             writer.append(String.format(STANDARDIZED_LOCALE, positionFormat,
  526.                                         pv.getPosition().getZ() * M_TO_KM)).append(" ");
  527.             writer.append(String.format(STANDARDIZED_LOCALE, velocityFormat,
  528.                                         pv.getVelocity().getX() * M_TO_KM)).append(" ");
  529.             writer.append(String.format(STANDARDIZED_LOCALE, velocityFormat,
  530.                                         pv.getVelocity().getY() * M_TO_KM)).append(" ");
  531.             writer.append(String.format(STANDARDIZED_LOCALE, velocityFormat,
  532.                                         pv.getVelocity().getZ() * M_TO_KM));
  533.             writer.append(NEW_LINE);
  534.         }

  535.         /**
  536.          * Write covariance matrices of the segment according to section 5.2.5.
  537.          *
  538.          * @param covarianceMatrices the list of covariance matrices related to the segment.
  539.          * @throws IOException if the output stream throws one while writing.
  540.          */
  541.         public void writeCovarianceMatrices(final List<CovarianceMatrix> covarianceMatrices)
  542.                 throws IOException {
  543.             writer.append("COVARIANCE_START").append(NEW_LINE);
  544.             // Sort to ensure having the matrices in chronological order when
  545.             // they are in the same data section (see section 5.2.5.7)
  546.             Collections.sort(covarianceMatrices, (mat1, mat2)->mat1.getEpoch().compareTo(mat2.getEpoch()));
  547.             for (final CovarianceMatrix covarianceMatrix : covarianceMatrices) {
  548.                 final String epoch = dateToString(covarianceMatrix.getEpoch().getComponents(timeScale));
  549.                 writeKeyValue(Keyword.EPOCH, epoch);

  550.                 if (covarianceMatrix.getFrame() != null ) {
  551.                     writeKeyValue(Keyword.COV_REF_FRAME, guessFrame(covarianceMatrix.getFrame()));
  552.                 } else if (covarianceMatrix.getLofType() != null) {
  553.                     if (covarianceMatrix.getLofType() == LOFType.QSW) {
  554.                         writeKeyValue(Keyword.COV_REF_FRAME, "RTN");
  555.                     } else if (covarianceMatrix.getLofType() == LOFType.TNW) {
  556.                         writeKeyValue(Keyword.COV_REF_FRAME, covarianceMatrix.getLofType().name());
  557.                     } else {
  558.                         throw new OrekitException(OrekitMessages.CCSDS_INVALID_FRAME, toString());
  559.                     }
  560.                 }

  561.                 final RealMatrix covRealMatrix = covarianceMatrix.getMatrix();
  562.                 for (int i = 0; i < covRealMatrix.getRowDimension(); i++) {
  563.                     writer.append(Double.toString(covRealMatrix.getEntry(i, 0)));
  564.                     for (int j = 1; j < i + 1; j++) {
  565.                         writer.append(" ").append(Double.toString(covRealMatrix.getEntry(i, j)));
  566.                     }
  567.                     writer.append(NEW_LINE);
  568.                 }
  569.             }
  570.             writer.append("COVARIANCE_STOP").append(NEW_LINE).append(NEW_LINE);
  571.         }

  572.         /**
  573.          * {@inheritDoc}
  574.          *
  575.          * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
  576.          * segment's metadata if not already set by the user. Then calls {@link
  577.          * #writeMetadata()} to start the segment.
  578.          */
  579.         @Override
  580.         public void init(final SpacecraftState s0,
  581.                          final AbsoluteDate t,
  582.                          final double step) {
  583.             try {
  584.                 final String start = dateToString(s0.getDate().getComponents(timeScale));
  585.                 final String stop = dateToString(t.getComponents(timeScale));
  586.                 this.metadata.putIfAbsent(Keyword.START_TIME, start);
  587.                 this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
  588.                 this.writeMetadata();
  589.             } catch (IOException e) {
  590.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  591.                         e.getLocalizedMessage());
  592.             }
  593.         }

  594.         @Override
  595.         public void handleStep(final SpacecraftState s,
  596.                                final boolean isLast) {
  597.             try {
  598.                 writeEphemerisLine(s.getPVCoordinates(this.frame));
  599.             } catch (IOException e) {
  600.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  601.                         e.getLocalizedMessage());
  602.             }

  603.         }

  604.     }

  605.     /**
  606.      * Convert a date to a string with more precision.
  607.      *
  608.      * @param components to convert to a String.
  609.      * @return the String form of {@code date} with at least 9 digits of precision.
  610.      */
  611.     static String dateToString(final DateTimeComponents components) {
  612.         final TimeComponents time = components.getTime();
  613.         final int hour = time.getHour();
  614.         final int minute = time.getMinute();
  615.         final double second = time.getSecond();
  616.         // Decimal formatting classes could be static final if they were thread safe.
  617.         final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
  618.         final DecimalFormat twoDigits = new DecimalFormat("00", locale);
  619.         final DecimalFormat precise = new DecimalFormat("00.0########", locale);
  620.         return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
  621.                 twoDigits.format(minute) + ":" + precise.format(second);
  622.     }

  623. }