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.VersionedITRF;
  32. import org.orekit.propagation.Propagator;
  33. import org.orekit.propagation.SpacecraftState;
  34. import org.orekit.propagation.sampling.OrekitFixedStepHandler;
  35. import org.orekit.time.AbsoluteDate;
  36. import org.orekit.time.DateTimeComponents;
  37. import org.orekit.time.TimeComponents;
  38. import org.orekit.time.TimeScale;
  39. import org.orekit.time.TimeScalesFactory;
  40. import org.orekit.utils.TimeStampedPVCoordinates;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  509.         }

  510.     }

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

  529. }