StreamingOemWriter.java

  1. /* Contributed in the public domain.
  2.  * Licensed to CS Systèmes d'Information (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.util.Arrays;
  22. import java.util.Date;
  23. import java.util.LinkedHashMap;
  24. import java.util.Locale;
  25. import java.util.Map;

  26. import org.hipparchus.exception.LocalizedCoreFormats;
  27. import org.orekit.bodies.CelestialBodyFactory;
  28. import org.orekit.errors.OrekitException;
  29. import org.orekit.frames.FactoryManagedFrame;
  30. import org.orekit.frames.Frame;
  31. import org.orekit.frames.Predefined;
  32. import org.orekit.frames.VersionedITRF;
  33. import org.orekit.propagation.Propagator;
  34. import org.orekit.propagation.SpacecraftState;
  35. import org.orekit.propagation.sampling.OrekitFixedStepHandler;
  36. import org.orekit.time.AbsoluteDate;
  37. import org.orekit.time.DateTimeComponents;
  38. import org.orekit.time.TimeComponents;
  39. import org.orekit.time.TimeScale;
  40. import org.orekit.time.TimeScalesFactory;
  41. import org.orekit.utils.TimeStampedPVCoordinates;

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

  235.     /** Version number implemented. **/
  236.     public static final String CCSDS_OEM_VERS = "2.0";
  237.     /** Default value for {@link Keyword#ORIGINATOR}. */
  238.     public static final String DEFAULT_ORIGINATOR = "OREKIT";

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

  252.     /** Output stream. */
  253.     private final Appendable writer;
  254.     /** Metadata for this OEM file. */
  255.     private final Map<Keyword, String> metadata;
  256.     /** Time scale for all dates except {@link Keyword#CREATION_DATE}. */
  257.     private final TimeScale timeScale;

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

  271.         this.writer = writer;
  272.         this.timeScale = timeScale;
  273.         this.metadata = new LinkedHashMap<>(metadata);
  274.         // set default metadata
  275.         this.metadata.putIfAbsent(Keyword.CCSDS_OEM_VERS, CCSDS_OEM_VERS);
  276.         this.metadata.putIfAbsent(Keyword.CREATION_DATE,
  277.                 new AbsoluteDate(new Date(), TimeScalesFactory.getUTC()).toString());
  278.         this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
  279.         this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
  280.     }

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

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

  343.     /**
  344.      * Write a single key and value to the stream using Key Value Notation (KVN).
  345.      *
  346.      * @param key   the keyword to write
  347.      * @param value the value to write
  348.      * @throws IOException if an I/O error occurs.
  349.      */
  350.     private void writeKeyValue(final Keyword key, final String value) throws IOException {
  351.         writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
  352.     }

  353.     /**
  354.      * Writes the standard OEM header for the file.
  355.      *
  356.      * @throws IOException if the stream cannot write to stream
  357.      */
  358.     public void writeHeader() throws IOException {
  359.         writeKeyValue(Keyword.CCSDS_OEM_VERS, this.metadata.get(Keyword.CCSDS_OEM_VERS));
  360.         final String comment = this.metadata.get(Keyword.COMMENT);
  361.         if (comment != null) {
  362.             writeKeyValue(Keyword.COMMENT, comment);
  363.         }
  364.         writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
  365.         writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
  366.         writer.append(NEW_LINE);
  367.     }

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

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

  399.         /** Reference frame of the output states. */
  400.         private final Frame frame;
  401.         /** Metadata for this OEM Segment. */
  402.         private final Map<Keyword, String> metadata;

  403.         /**
  404.          * Create a new segment writer.
  405.          *
  406.          * @param frame    for the output states. Used by {@link #handleStep(SpacecraftState,
  407.          *                 boolean)}.
  408.          * @param metadata to use when writing this segment.
  409.          */
  410.         private Segment(final Frame frame, final Map<Keyword, String> metadata) {
  411.             this.frame = frame;
  412.             this.metadata = metadata;
  413.         }

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

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

  479.         /**
  480.          * {@inheritDoc}
  481.          *
  482.          * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
  483.          * segment's metadata if not already set by the user. Then calls {@link
  484.          * #writeMetadata()} to start the segment.
  485.          */
  486.         @Override
  487.         public void init(final SpacecraftState s0,
  488.                          final AbsoluteDate t,
  489.                          final double step) {
  490.             try {
  491.                 final String start = dateToString(s0.getDate().getComponents(timeScale));
  492.                 final String stop = dateToString(t.getComponents(timeScale));
  493.                 this.metadata.putIfAbsent(Keyword.START_TIME, start);
  494.                 this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
  495.                 this.writeMetadata();
  496.             } catch (IOException e) {
  497.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  498.                         e.getLocalizedMessage());
  499.             }
  500.         }

  501.         @Override
  502.         public void handleStep(final SpacecraftState s,
  503.                                final boolean isLast) {
  504.             try {
  505.                 writeEphemerisLine(s.getPVCoordinates(this.frame));
  506.             } catch (IOException e) {
  507.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  508.                         e.getLocalizedMessage());
  509.             }

  510.         }

  511.     }

  512.     /**
  513.      * Convert a date to a string with more precision.
  514.      *
  515.      * @param components to convert to a String.
  516.      * @return the String form of {@code date} with at least 9 digits of precision.
  517.      */
  518.     static String dateToString(final DateTimeComponents components) {
  519.         final TimeComponents time = components.getTime();
  520.         final int hour = time.getHour();
  521.         final int minute = time.getMinute();
  522.         final double second = time.getSecond();
  523.         // Decimal formatting classes could be static final if they were thread safe.
  524.         final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
  525.         final DecimalFormat twoDigits = new DecimalFormat("00", locale);
  526.         final DecimalFormat precise = new DecimalFormat("00.0########", locale);
  527.         return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
  528.                 twoDigits.format(minute) + ":" + precise.format(second);
  529.     }

  530. }