StreamingAemWriter.java

  1. /* Copyright 2002-2020 CS GROUP
  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.Date;
  25. import java.util.LinkedHashMap;
  26. import java.util.Locale;
  27. import java.util.Map;

  28. import org.hipparchus.exception.LocalizedCoreFormats;
  29. import org.hipparchus.geometry.euclidean.threed.RotationOrder;
  30. import org.orekit.errors.OrekitException;
  31. import org.orekit.files.ccsds.AEMParser.AEMRotationOrder;
  32. import org.orekit.propagation.SpacecraftState;
  33. import org.orekit.propagation.sampling.OrekitFixedStepHandler;
  34. import org.orekit.time.AbsoluteDate;
  35. import org.orekit.time.DateTimeComponents;
  36. import org.orekit.time.TimeComponents;
  37. import org.orekit.time.TimeScale;
  38. import org.orekit.utils.TimeStampedAngularCoordinates;

  39. /**
  40.  * A writer for AEM files.
  41.  *
  42.  * <p> Each instance corresponds to a single AEM file.
  43.  *
  44.  * <h3> Metadata </h3>
  45.  *
  46.  * <p> The AEM metadata used by this writer is described in the following table. Many
  47.  * metadata items are optional or have default values so they do not need to be specified.
  48.  * At a minimum the user must supply those values that are required and for which no
  49.  * default exits: {@link Keyword#OBJECT_NAME}, and {@link Keyword#OBJECT_ID}. The usage
  50.  * column in the table indicates where the metadata item is used, either in the AEM header
  51.  * or in the metadata section at the start of an AEM attitude segment.
  52.  *
  53.  * <p> The AEM metadata for the whole AEM file is set in the {@link
  54.  * #StreamingAemWriter(Appendable, TimeScale, Map) constructor}.
  55.  *
  56.  * <table summary="AEM metada">
  57.  *     <thead>
  58.  *         <tr>
  59.  *             <th>Keyword
  60.  *             <th>Usage
  61.  *             <th>Obligatory
  62.  *             <th>Default
  63.  *             <th>Reference
  64.  *    </thead>
  65.  *    <tbody>
  66.  *        <tr>
  67.  *            <td>{@link Keyword#CCSDS_AEM_VERS}
  68.  *            <td>Header
  69.  *            <td>Yes
  70.  *            <td>{@link #CCSDS_AEM_VERS}
  71.  *            <td>Table 4-2
  72.  *        <tr>
  73.  *            <td>{@link Keyword#COMMENT}
  74.  *            <td>Header
  75.  *            <td>No
  76.  *            <td>
  77.  *            <td>Table 4-2
  78.  *        <tr>
  79.  *            <td>{@link Keyword#CREATION_DATE}
  80.  *            <td>Header
  81.  *            <td>Yes
  82.  *            <td>{@link Date#Date() Now}
  83.  *            <td>Table 4-2
  84.  *        <tr>
  85.  *            <td>{@link Keyword#ORIGINATOR}
  86.  *            <td>Header
  87.  *            <td>Yes
  88.  *            <td>{@link #DEFAULT_ORIGINATOR}
  89.  *            <td>Table 4-2
  90.  *        <tr>
  91.  *            <td>{@link Keyword#OBJECT_NAME}
  92.  *            <td>Segment
  93.  *            <td>Yes
  94.  *            <td>
  95.  *            <td>Table 4-3
  96.  *        <tr>
  97.  *            <td>{@link Keyword#OBJECT_ID}
  98.  *            <td>Segment
  99.  *            <td>Yes
  100.  *            <td>
  101.  *            <td>Table 4-3
  102.  *        <tr>
  103.  *            <td>{@link Keyword#CENTER_NAME}
  104.  *            <td>Segment
  105.  *            <td>No
  106.  *            <td>
  107.  *            <td>Table 4-3
  108.  *        <tr>
  109.  *            <td>{@link Keyword#REF_FRAME_A}
  110.  *            <td>Segment
  111.  *            <td>Yes
  112.  *            <td>
  113.  *            <td>Table 4-3
  114.  *        <tr>
  115.  *            <td>{@link Keyword#REF_FRAME_B}
  116.  *            <td>Segment
  117.  *            <td>Yes
  118.  *            <td>
  119.  *            <td>Table 4-3
  120.  *        <tr>
  121.  *            <td>{@link Keyword#ATTITUDE_DIR}
  122.  *            <td>Segment
  123.  *            <td>Yes
  124.  *            <td>
  125.  *            <td>Table 4-3
  126.  *        <tr>
  127.  *            <td>{@link Keyword#TIME_SYSTEM}
  128.  *            <td>Segment
  129.  *            <td>Yes
  130.  *            <td>
  131.  *            <td>Table 4-3, Annex A
  132.  *        <tr>
  133.  *            <td>{@link Keyword#START_TIME}
  134.  *            <td>Segment
  135.  *            <td>Yes
  136.  *            <td>
  137.  *            <td>Table 4-3
  138.  *        <tr>
  139.  *            <td>{@link Keyword#USEABLE_START_TIME}
  140.  *            <td>Segment
  141.  *            <td>No
  142.  *            <td>
  143.  *            <td>Table 4-3
  144.  *        <tr>
  145.  *            <td>{@link Keyword#STOP_TIME}
  146.  *            <td>Segment
  147.  *            <td>Yes
  148.  *            <td>
  149.  *            <td>Table 4-3
  150.  *        <tr>
  151.  *            <td>{@link Keyword#USEABLE_STOP_TIME}
  152.  *            <td>Segment
  153.  *            <td>No
  154.  *            <td>
  155.  *            <td>Table 4-3
  156.  *        <tr>
  157.  *            <td>{@link Keyword#ATTITUDE_TYPE}
  158.  *            <td>Segment
  159.  *            <td>Yes
  160.  *            <td>
  161.  *            <td>Table 4-3, 4-4
  162.  *        <tr>
  163.  *            <td>{@link Keyword#QUATERNION_TYPE}
  164.  *            <td>Segment
  165.  *            <td>No
  166.  *            <td>
  167.  *            <td>Table 4-3, 4-4
  168.  *        <tr>
  169.  *            <td>{@link Keyword#EULER_ROT_SEQ}
  170.  *            <td>Segment
  171.  *            <td>No
  172.  *            <td>
  173.  *            <td>Table 4-3
  174.  *        <tr>
  175.  *            <td>{@link Keyword#RATE_FRAME}
  176.  *            <td>Segment
  177.  *            <td>No
  178.  *            <td>
  179.  *            <td>Table 4-3
  180.  *        <tr>
  181.  *            <td>{@link Keyword#INTERPOLATION_METHOD}
  182.  *            <td>Segment
  183.  *            <td>No
  184.  *            <td>
  185.  *            <td>Table 4-3
  186.  *        <tr>
  187.  *            <td>{@link Keyword#INTERPOLATION_DEGREE}
  188.  *            <td>Segment
  189.  *            <td>No
  190.  *            <td>
  191.  *            <td>Table 4-3
  192.  *    </tbody>
  193.  *</table>
  194.  *
  195.  * <p> The {@link Keyword#TIME_SYSTEM} must be constant for the whole file and is used
  196.  * to interpret all dates except {@link Keyword#CREATION_DATE}. The guessing algorithm
  197.  * is not guaranteed to work so it is recommended to provide values for {@link
  198.  * Keyword#CENTER_NAME} and {@link Keyword#TIME_SYSTEM} to avoid any bugs associated with
  199.  * incorrect guesses.
  200.  *
  201.  * <p> Standardized values for {@link Keyword#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
  202.  * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
  203.  * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
  204.  * Additionally ITRF followed by a four digit year may be used.
  205.  *
  206.  * @author Bryan Cazabonne
  207.  * @see <a href="https://public.ccsds.org/Pubs/504x0b1c1.pdf">CCSDS 504.0-B-1 Attitude Data Messages</a>
  208.  * @see AEMWriter
  209.  * @since 10.2
  210.  */
  211. public class StreamingAemWriter {

  212.     /** Version number implemented. **/
  213.     public static final String CCSDS_AEM_VERS = "1.0";

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

  216.     /** New line separator for output file. See 5.4.5. */
  217.     private static final String NEW_LINE = "\n";

  218.     /** Standardized locale to use, to ensure files can be exchanged without internationalization issues. */
  219.     private static final Locale STANDARDIZED_LOCALE = Locale.US;

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

  222.     /** Output stream. */
  223.     private final Appendable writer;

  224.     /** Metadata for this AEM file. */
  225.     private final Map<Keyword, String> metadata;

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

  228.     /**
  229.      * Create an AEM writer that streams data to the given output stream.
  230.      *
  231.      * @param writer    The output stream for the AEM file. Most methods will append data
  232.      *                  to this {@code writer}.
  233.      * @param timeScale for all times in the AEM except {@link Keyword#CREATION_DATE}. See
  234.      *                  Section 4.2.5.4.2 and Annex A.
  235.      * @param metadata  for the satellite.
  236.      */
  237.     public StreamingAemWriter(final Appendable writer,
  238.                               final TimeScale timeScale,
  239.                               final Map<Keyword, String> metadata) {
  240.         this.writer    = writer;
  241.         this.timeScale = timeScale;
  242.         this.metadata  = new LinkedHashMap<>(metadata);

  243.         // Set default metadata
  244.         this.metadata.putIfAbsent(Keyword.CCSDS_AEM_VERS, CCSDS_AEM_VERS);

  245.         // creation date is informational only
  246.         this.metadata.putIfAbsent(Keyword.CREATION_DATE,
  247.                 ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
  248.         this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
  249.         this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
  250.     }

  251.     /**
  252.      * Write a single key and value to the stream using Key Value Notation (KVN).
  253.      * @param key   the keyword to write
  254.      * @param value the value to write
  255.      * @throws IOException if an I/O error occurs.
  256.      */
  257.     private void writeKeyValue(final Keyword key, final String value) throws IOException {
  258.         writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
  259.     }

  260.     /**
  261.      * Writes the standard AEM header for the file.
  262.      * @throws IOException if the stream cannot write to stream
  263.      */
  264.     public void writeHeader() throws IOException {
  265.         writeKeyValue(Keyword.CCSDS_AEM_VERS, this.metadata.get(Keyword.CCSDS_AEM_VERS));
  266.         final String comment = this.metadata.get(Keyword.COMMENT);
  267.         if (comment != null) {
  268.             writeKeyValue(Keyword.COMMENT, comment);
  269.         }
  270.         writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
  271.         writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
  272.         writer.append(NEW_LINE);
  273.     }

  274.     /**
  275.      * Create a writer for a new AEM attitude ephemeris segment.
  276.      * <p> The returned writer can only write a single attitude ephemeris segment in an AEM.
  277.      * This method must be called to create a writer for each attitude ephemeris segment.
  278.      * @param segmentMetadata the metadata to use for the segment. Overrides for this
  279.      *                        segment any other source of meta data values. See {@link
  280.      *                        #StreamingAemWriter} for a description of which metadata are
  281.      *                        required and how they are determined.
  282.      * @return a new AEM segment, ready for writing.
  283.      */
  284.     public AEMSegment newSegment(final Map<Keyword, String> segmentMetadata) {
  285.         final Map<Keyword, String> meta = new LinkedHashMap<>(this.metadata);
  286.         meta.putAll(segmentMetadata);
  287.         return new AEMSegment(meta);
  288.     }

  289.     /**
  290.      * Convert a date to a string with more precision.
  291.      *
  292.      * @param components to convert to a String.
  293.      * @return the String form of {@code date} with at least 9 digits of precision.
  294.      */
  295.     static String dateToString(final DateTimeComponents components) {
  296.         final TimeComponents time = components.getTime();
  297.         final int hour = time.getHour();
  298.         final int minute = time.getMinute();
  299.         final double second = time.getSecond();
  300.         // Decimal formatting classes could be static final if they were thread safe.
  301.         final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
  302.         final DecimalFormat twoDigits = new DecimalFormat("00", locale);
  303.         final DecimalFormat precise = new DecimalFormat("00.0########", locale);
  304.         return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
  305.                 twoDigits.format(minute) + ":" + precise.format(second);
  306.     }

  307.     /** A writer for a segment of an AEM. */
  308.     public class AEMSegment implements OrekitFixedStepHandler {

  309.         /** Metadata for this AEM Segment. */
  310.         private final Map<Keyword, String> metadata;

  311.         /**
  312.          * Create a new segment writer.
  313.          * @param metadata to use when writing this segment.
  314.          */
  315.         private AEMSegment(final Map<Keyword, String> metadata) {
  316.             this.metadata = metadata;
  317.         }

  318.         /**
  319.          * Write the ephemeris segment metadata.
  320.          *
  321.          * <p> See {@link StreamingAemWriter} for a description of how the metadata is
  322.          * set.
  323.          *
  324.          * @throws IOException if the output stream throws one while writing.
  325.          */
  326.         public void writeMetadata() throws IOException {
  327.             // Start metadata
  328.             writer.append("META_START").append(NEW_LINE);

  329.             // Table 4.3
  330.             writeKeyValue(Keyword.OBJECT_NAME,  this.metadata.get(Keyword.OBJECT_NAME));
  331.             writeKeyValue(Keyword.OBJECT_ID,    this.metadata.get(Keyword.OBJECT_ID));
  332.             writeKeyValue(Keyword.CENTER_NAME,  this.metadata.get(Keyword.CENTER_NAME));
  333.             writeKeyValue(Keyword.REF_FRAME_A,  this.metadata.get(Keyword.REF_FRAME_A));
  334.             writeKeyValue(Keyword.REF_FRAME_B,  this.metadata.get(Keyword.REF_FRAME_B));
  335.             writeKeyValue(Keyword.ATTITUDE_DIR, this.metadata.get(Keyword.ATTITUDE_DIR));
  336.             writeKeyValue(Keyword.TIME_SYSTEM,  this.metadata.get(Keyword.TIME_SYSTEM));
  337.             writeKeyValue(Keyword.START_TIME,   this.metadata.get(Keyword.START_TIME));

  338.             // Optional values: USEABLE_START_TIME & USEABLE_STOP_TIME
  339.             final String usableStartTime = this.metadata.get(Keyword.USEABLE_START_TIME);
  340.             if (usableStartTime != null) {
  341.                 writeKeyValue(Keyword.USEABLE_START_TIME, usableStartTime);
  342.             }
  343.             final String usableStopTime = this.metadata.get(Keyword.USEABLE_STOP_TIME);
  344.             if (usableStopTime != null) {
  345.                 writeKeyValue(Keyword.USEABLE_STOP_TIME, usableStopTime);
  346.             }

  347.             // Table 4.3
  348.             writeKeyValue(Keyword.STOP_TIME,     this.metadata.get(Keyword.STOP_TIME));
  349.             writeKeyValue(Keyword.ATTITUDE_TYPE, this.metadata.get(Keyword.ATTITUDE_TYPE));

  350.             // Optional values: QUATERNION_ TYPE; EULER_ROT_SEQ; RATE_FRAME; INTERPOLATION_METHOD and INTERPOLATION_DEGREE
  351.             final String quaternionType = this.metadata.get(Keyword.QUATERNION_TYPE);
  352.             if (quaternionType != null) {
  353.                 writeKeyValue(Keyword.QUATERNION_TYPE, quaternionType);
  354.             }
  355.             final String eulerRotSeq = this.metadata.get(Keyword.EULER_ROT_SEQ);
  356.             if (eulerRotSeq != null) {
  357.                 writeKeyValue(Keyword.EULER_ROT_SEQ, eulerRotSeq);
  358.             }
  359.             final String rateFrame = this.metadata.get(Keyword.RATE_FRAME);
  360.             if (rateFrame != null) {
  361.                 writeKeyValue(Keyword.RATE_FRAME, rateFrame);
  362.             }
  363.             final String interpolationMethod = this.metadata.get(Keyword.INTERPOLATION_METHOD);
  364.             if (interpolationMethod != null) {
  365.                 writeKeyValue(Keyword.INTERPOLATION_METHOD, interpolationMethod);
  366.             }
  367.             final String interpolationDegree = this.metadata.get(Keyword.INTERPOLATION_DEGREE);
  368.             if (interpolationDegree != null) {
  369.                 writeKeyValue(Keyword.INTERPOLATION_DEGREE, interpolationDegree);
  370.             }

  371.             // Stop metadata
  372.             writer.append("META_STOP").append(NEW_LINE).append(NEW_LINE);
  373.         }

  374.         /**
  375.          * Write a single attitude ephemeris line according to section 4.2.4 and Table 4-4.
  376.          * @param attitude the attitude information for a given date.
  377.          * @param isFirst true if QC is the first element in the attitude data
  378.          * @param attitudeName name of the attitude type
  379.          * @param rotationOrder rotation order
  380.          * @throws IOException if the output stream throws one while writing.
  381.          */
  382.         public void writeAttitudeEphemerisLine(final TimeStampedAngularCoordinates attitude,
  383.                                                final boolean isFirst,
  384.                                                final String attitudeName,
  385.                                                final RotationOrder rotationOrder)
  386.             throws IOException {
  387.             // Epoch
  388.             final String epoch = dateToString(attitude.getDate().getComponents(timeScale));
  389.             writer.append(epoch).append(" ");
  390.             // Attitude data in degrees
  391.             final AEMAttitudeType type = AEMAttitudeType.getAttitudeType(attitudeName);
  392.             final double[]        data = type.getAttitudeData(attitude, isFirst, rotationOrder);
  393.             final int             size = data.length;
  394.             for (int index = 0; index < size; index++) {
  395.                 writer.append(Double.toString(data[index]));
  396.                 final String space = (index == size - 1) ? "" : " ";
  397.                 writer.append(space);
  398.             }
  399.             // end the line
  400.             writer.append(NEW_LINE);
  401.         }

  402.         /**
  403.          * {@inheritDoc}
  404.          *
  405.          * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
  406.          * segment's metadata if not already set by the user. Then calls {@link
  407.          * #writeMetadata()} to start the segment.
  408.          */
  409.         @Override
  410.         public void init(final SpacecraftState s0,
  411.                          final AbsoluteDate t,
  412.                          final double step) {
  413.             try {
  414.                 final String start = dateToString(s0.getDate().getComponents(timeScale));
  415.                 final String stop = dateToString(t.getComponents(timeScale));
  416.                 this.metadata.putIfAbsent(Keyword.START_TIME, start);
  417.                 this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
  418.                 this.writeMetadata();
  419.             } catch (IOException e) {
  420.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  421.                         e.getLocalizedMessage());
  422.             }
  423.         }

  424.         /** {@inheritDoc}. */
  425.         @Override
  426.         public void handleStep(final SpacecraftState currentState, final boolean isLast) {
  427.             try {

  428.                 // Quaternion type
  429.                 final String quaternionType = this.metadata.get(Keyword.QUATERNION_TYPE);
  430.                 // If the QUATERNION_TYPE keyword is not present in the file, this means that
  431.                 // the attitude data are not given using quaternion. Therefore, the computation
  432.                 // of the attitude data will not be sensitive to this parameter. A default value
  433.                 // can be set
  434.                 boolean isFirst = false;
  435.                 if (quaternionType != null) {
  436.                     isFirst = (quaternionType.equals("FIRST")) ? true : false;
  437.                 }

  438.                 // Attitude type
  439.                 final String attitudeType = this.metadata.get(Keyword.ATTITUDE_TYPE);

  440.                 // Rotation order
  441.                 final String eulerRotSeq = this.metadata.get(Keyword.EULER_ROT_SEQ);
  442.                 final RotationOrder order = (eulerRotSeq == null) ? null : AEMRotationOrder.getRotationOrder(eulerRotSeq);

  443.                 // Write attitude ephemeris data
  444.                 writeAttitudeEphemerisLine(currentState.getAttitude().getOrientation(), isFirst,
  445.                                            attitudeType, order);

  446.             } catch (IOException e) {
  447.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  448.                         e.getLocalizedMessage());
  449.             }

  450.         }

  451.         /**
  452.          * Start of an attitude block.
  453.          * @throws IOException if the output stream throws one while writing.
  454.          */
  455.         void startAttitudeBlock() throws IOException {
  456.             writer.append("DATA_START").append(NEW_LINE);
  457.         }

  458.         /**
  459.          * End of an attitude block.
  460.          * @throws IOException if the output stream throws one while writing.
  461.          */
  462.         void endAttitudeBlock() throws IOException {
  463.             writer.append("DATA_STOP").append(NEW_LINE);
  464.         }

  465.     }

  466. }