AemWriter.java

  1. /* Copyright 2002-2025 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.ndm.adm.aem;

  18. import java.io.IOException;
  19. import java.util.Date;

  20. import org.hipparchus.geometry.euclidean.threed.RotationOrder;
  21. import org.orekit.data.DataContext;
  22. import org.orekit.errors.OrekitInternalError;
  23. import org.orekit.files.ccsds.definitions.TimeSystem;
  24. import org.orekit.files.ccsds.definitions.Units;
  25. import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
  26. import org.orekit.files.ccsds.ndm.adm.AdmCommonMetadataKey;
  27. import org.orekit.files.ccsds.ndm.adm.AdmHeader;
  28. import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
  29. import org.orekit.files.ccsds.ndm.adm.AttitudeType;
  30. import org.orekit.files.ccsds.section.Header;
  31. import org.orekit.files.ccsds.section.HeaderKey;
  32. import org.orekit.files.ccsds.section.KvnStructureKey;
  33. import org.orekit.files.ccsds.section.MetadataKey;
  34. import org.orekit.files.ccsds.section.XmlStructureKey;
  35. import org.orekit.files.ccsds.utils.ContextBinding;
  36. import org.orekit.files.ccsds.utils.FileFormat;
  37. import org.orekit.files.ccsds.utils.generation.AbstractMessageWriter;
  38. import org.orekit.files.ccsds.utils.generation.Generator;
  39. import org.orekit.files.ccsds.utils.generation.XmlGenerator;
  40. import org.orekit.time.AbsoluteDate;
  41. import org.orekit.utils.IERSConventions;
  42. import org.orekit.utils.TimeStampedAngularCoordinates;
  43. import org.orekit.utils.units.Unit;

  44. /**
  45.  * A writer for Attitude Ephemeris Messsage (AEM) files.
  46.  *
  47.  * <h2> Metadata </h2>
  48.  *
  49.  * <p> The AEM header and metadata used by this writer are described in the following tables.
  50.  * Many 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 AdmMetadataKey#OBJECT_NAME}, {@link AdmCommonMetadataKey#OBJECT_ID},
  53.  * {@link AemMetadataKey#START_TIME} and {@link AemMetadataKey#STOP_TIME}.
  54.  * The usage column in the table indicates where the metadata item is used, either in the AEM header
  55.  * or in the metadata section at the start of an AEM attitude segment.
  56.  * </p>
  57.  *
  58.  * <p> The AEM header for the whole AEM file is set when calling {@link #writeHeader(Generator, Header)},
  59.  * the entries are defined in table 4-2 of the ADM standard.
  60.  *
  61.  * <table>
  62.  * <caption>AEM metadata</caption>
  63.  *     <thead>
  64.  *         <tr>
  65.  *             <th>Keyword</th>
  66.  *             <th>Mandatory</th>
  67.  *             <th>Default in Orekit</th>
  68.  *         </tr>
  69.  *    </thead>
  70.  *    <tbody>
  71.  *        <tr>
  72.  *            <td>{@link Aem#FORMAT_VERSION_KEY CCSDS_AEM_VERS}</td>
  73.  *            <td>Yes</td>
  74.  *            <td>{@link #CCSDS_AEM_VERS}</td>
  75.  *        </tr>
  76.  *        <tr>
  77.  *            <td>{@link HeaderKey#COMMENT}</td>
  78.  *            <td>No</td>
  79.  *            <td>empty</td>
  80.  *        </tr>
  81.  *        <tr>
  82.  *            <td>{@link HeaderKey#CREATION_DATE}</td>
  83.  *            <td>Yes</td>
  84.  *            <td>{@link Date#Date() Now}</td>
  85.  *        </tr>
  86.  *        <tr>
  87.  *            <td>{@link HeaderKey#ORIGINATOR}</td>
  88.  *            <td>Yes</td>
  89.  *            <td>{@link #DEFAULT_ORIGINATOR}</td>
  90.  *        </tr>
  91.  *    </tbody>
  92.  *    </table>
  93.  *
  94.  * <p> The AEM metadata for the AEM file is set when calling {@link #writeSegmentContent(Generator, double, AemSegment)},
  95.  * the entries are defined in tables 4-3, 4-4 and annex A of the ADM standard.
  96.  *
  97.  * <table>
  98.  * <caption>AEM metadata</caption>
  99.  *     <thead>
  100.  *         <tr>
  101.  *             <th>Keyword</th>
  102.  *             <th>Mandatory</th>
  103.  *             <th>Default in Orekit</th>
  104.  *         </tr>
  105.  *    </thead>
  106.  *    <tbody>
  107.  *        <tr>
  108.  *            <td>{@link MetadataKey#COMMENT}</td>
  109.  *            <td>No</td>
  110.  *            <td>empty</td>
  111.  *        </tr>
  112.  *        <tr>
  113.  *            <td>{@link AdmMetadataKey#OBJECT_NAME}</td>
  114.  *            <td>Yes</td>
  115.  *            <td></td>
  116.  *        </tr>
  117.  *        <tr>
  118.  *            <td>{@link AdmCommonMetadataKey#OBJECT_ID}</td>
  119.  *            <td>Yes</td>
  120.  *            <td></td>
  121.  *        </tr>
  122.  *        <tr>
  123.  *            <td>{@link AdmMetadataKey#CENTER_NAME}</td>
  124.  *            <td>No</td>
  125.  *            <td></td>
  126.  *        </tr>
  127.  *        <tr>
  128.  *            <td>{@link AemMetadataKey#REF_FRAME_A}</td>
  129.  *            <td>Yes</td>
  130.  *            <td></td>
  131.  *        </tr>
  132.  *        <tr>
  133.  *            <td>{@link AemMetadataKey#REF_FRAME_B}</td>
  134.  *            <td>Yes</td>
  135.  *            <td></td>
  136.  *        </tr>
  137.  *        <tr>
  138.  *            <td>{@link AemMetadataKey#ATTITUDE_DIR}</td>
  139.  *            <td>Yes</td>
  140.  *            <td></td>
  141.  *        </tr>
  142.  *        <tr>
  143.  *            <td>{@link MetadataKey#TIME_SYSTEM}</td>
  144.  *            <td>Yes</td>
  145.  *            <td></td>
  146.  *        </tr>
  147.  *        <tr>
  148.  *            <td>{@link AemMetadataKey#START_TIME}</td>
  149.  *            <td>Yes</td>
  150.  *            <td>default to propagation start time (for forward propagation)</td>
  151.  *        </tr>
  152.  *        <tr>
  153.  *            <td>{@link AemMetadataKey#USEABLE_START_TIME}</td>
  154.  *            <td>No</td>
  155.  *            <td></td>
  156.  *        </tr>
  157.  *        <tr>
  158.  *            <td>{@link AemMetadataKey#USEABLE_STOP_TIME}</td>
  159.  *            <td>No</td>
  160.  *            <td></td>
  161.  *        </tr>
  162.  *        <tr>
  163.  *            <td>{@link AemMetadataKey#STOP_TIME}</td>
  164.  *            <td>Yes</td>
  165.  *            <td>default to propagation target time (for forward propagation)</td>
  166.  *        </tr>
  167.  *        <tr>
  168.  *            <td>{@link AemMetadataKey#ATTITUDE_TYPE}</td>
  169.  *            <td>Yes</td>
  170.  *            <td>{@link AttitudeType#QUATERNION_DERIVATIVE QUATERNION/DERIVATIVE}</td>
  171.  *        </tr>
  172.  *        <tr>
  173.  *            <td>{@link AemMetadataKey#QUATERNION_TYPE}</td>
  174.  *            <td>No</td>
  175.  *            <td>{@code FIRST}</td>
  176.  *        </tr>
  177.  *        <tr>
  178.  *            <td>{@link AemMetadataKey#EULER_ROT_SEQ}</td>
  179.  *            <td>No</td>
  180.  *            <td></td>
  181.  *        </tr>
  182.  *        <tr>
  183.  *            <td>{@link AemMetadataKey#RATE_FRAME}</td>
  184.  *            <td>No</td>
  185.  *            <td>{@code REF_FRAME_B}</td>
  186.  *        </tr>
  187.  *        <tr>
  188.  *            <td>{@link AemMetadataKey#INTERPOLATION_METHOD}</td>
  189.  *            <td>No</td>
  190.  *            <td></td>
  191.  *        </tr>
  192.  *        <tr>
  193.  *            <td>{@link AemMetadataKey#INTERPOLATION_DEGREE}</td>
  194.  *            <td>No</td>
  195.  *            <td>always set in {@link AemMetadata}</td>
  196.  *        </tr>
  197.  *    </tbody>
  198.  *</table>
  199.  *
  200.  * <p> The {@link MetadataKey#TIME_SYSTEM} must be constant for the whole file and is used
  201.  * to interpret all dates except {@link HeaderKey#CREATION_DATE} which is always in {@link
  202.  * TimeSystem#UTC UTC}. The guessing algorithm is not guaranteed to work so it is recommended
  203.  * to provide values for {@link AdmMetadataKey#CENTER_NAME} and {@link MetadataKey#TIME_SYSTEM}
  204.  * to avoid any bugs associated with incorrect guesses.
  205.  *
  206.  * <p> Standardized values for {@link MetadataKey#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
  207.  * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
  208.  * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
  209.  * Additionally ITRF followed by a four digit year may be used.
  210.  *
  211.  * @author Bryan Cazabonne
  212.  * @since 10.2
  213.  */
  214. public class AemWriter extends AbstractMessageWriter<AdmHeader, AemSegment, Aem> {

  215.     /** Version number implemented. **/
  216.     public static final double CCSDS_AEM_VERS = 2.0;

  217.     /** Padding width for aligning the '=' sign. */
  218.     public static final int KVN_PADDING_WIDTH = 20;

  219.     /** Constant for frame A to frame B attitude. */
  220.     private static final String A_TO_B = "A2B";

  221.     /** Constant for frame B to frame A attitude. */
  222.     private static final String B_TO_A = "B2A";

  223.     /** Constant for quaternions with scalar component in  position. */
  224.     private static final String FIRST = "FIRST";

  225.     /** Constant for quaternions with scalar component in last position. */
  226.     private static final String LAST = "LAST";

  227.     /** Constant for angular rates in frame A. */
  228.     private static final String REF_FRAME_A = "REF_FRAME_A";

  229.     /** Constant for angular rates in frame B. */
  230.     private static final String REF_FRAME_B = "REF_FRAME_B";

  231.     /** Prefix for Euler rotations. */
  232.     private static final String ROTATION = "rotation";

  233.     /** Attribute for Euler angles. */
  234.     private static final String ANGLE_ATTRIBUTE = "angle";

  235.     /** Suffix for Euler angles. */
  236.     private static final String ANGLE_SUFFIX = "_ANGLE";

  237.     /**Attribute for Euler rates. */
  238.     private static final String RATE_ATTRIBUTE = "rate";

  239.     /** Suffix for Euler rates. */
  240.     private static final String RATE_SUFFIX = "_RATE";

  241.     /**
  242.      * Constructor used to create a new AEM writer configured with the necessary parameters
  243.      * to successfully fill in all required fields that aren't part of a standard object.
  244.      * @param conventions IERS Conventions
  245.      * @param dataContext used to retrieve frames, time scales, etc.
  246.      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  247.      * @since 11.0
  248.      */
  249.     public AemWriter(final IERSConventions conventions, final DataContext dataContext,
  250.                      final AbsoluteDate missionReferenceDate) {
  251.         super(Aem.ROOT, Aem.FORMAT_VERSION_KEY, CCSDS_AEM_VERS,
  252.               new ContextBinding(
  253.                   () -> conventions,
  254.                   () -> true, () -> dataContext, () -> ParsedUnitsBehavior.STRICT_COMPLIANCE,
  255.                   () -> missionReferenceDate, () -> TimeSystem.UTC,
  256.                   () -> 0.0, () -> 1.0));
  257.     }

  258.     /** {@inheritDoc} */
  259.     @Override
  260.     protected void writeSegmentContent(final Generator generator, final double formatVersion,
  261.                                        final AemSegment segment)
  262.         throws IOException {

  263.         final AemMetadata metadata = segment.getMetadata();
  264.         writeMetadata(generator, formatVersion, metadata);

  265.         // Loop on attitude data
  266.         startAttitudeBlock(generator);
  267.         generator.writeComments(((AemSegment) segment).getData().getComments());
  268.         for (final TimeStampedAngularCoordinates coordinates : segment.getAngularCoordinates()) {
  269.             writeAttitudeEphemerisLine(generator, formatVersion, metadata, coordinates);
  270.         }
  271.         endAttitudeBlock(generator);

  272.     }

  273.     /** Write an ephemeris segment metadata.
  274.      * @param generator generator to use for producing output
  275.      * @param formatVersion format version
  276.      * @param metadata metadata to write
  277.      * @throws IOException if the output stream throws one while writing.
  278.      */
  279.     void writeMetadata(final Generator generator, final double formatVersion, final AemMetadata metadata)
  280.         throws IOException {

  281.         final ContextBinding oldContext = getContext();
  282.         setContext(new ContextBinding(oldContext::getConventions,
  283.                                       oldContext::isSimpleEOP,
  284.                                       oldContext::getDataContext,
  285.                                       oldContext::getParsedUnitsBehavior,
  286.                                       oldContext::getReferenceDate,
  287.                                       metadata::getTimeSystem,
  288.                                       oldContext::getClockCount,
  289.                                       oldContext::getClockRate));

  290.         // Start metadata
  291.         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
  292.                                KvnStructureKey.META.name() :
  293.                                XmlStructureKey.metadata.name());

  294.         generator.writeComments(metadata.getComments());

  295.         // objects
  296.         generator.writeEntry(AdmMetadataKey.OBJECT_NAME.name(),     metadata.getObjectName(), null, true);
  297.         generator.writeEntry(AdmCommonMetadataKey.OBJECT_ID.name(), metadata.getObjectID(),   null, true);
  298.         if (metadata.getCenter() != null) {
  299.             generator.writeEntry(AdmMetadataKey.CENTER_NAME.name(), metadata.getCenter().getName(), null, false);
  300.         }

  301.         // frames
  302.         generator.writeEntry(AemMetadataKey.REF_FRAME_A.name(),  metadata.getEndpoints().getFrameA().getName(),     null, true);
  303.         generator.writeEntry(AemMetadataKey.REF_FRAME_B.name(),  metadata.getEndpoints().getFrameB().getName(),     null, true);
  304.         if (formatVersion < 2.0) {
  305.             generator.writeEntry(AemMetadataKey.ATTITUDE_DIR.name(), metadata.getEndpoints().isA2b() ? A_TO_B : B_TO_A, null, true);
  306.         }

  307.         // time
  308.         generator.writeEntry(MetadataKey.TIME_SYSTEM.name(), metadata.getTimeSystem(), true);
  309.         generator.writeEntry(AemMetadataKey.START_TIME.name(), getTimeConverter(), metadata.getStartTime(), false, true);
  310.         if (metadata.getUseableStartTime() != null) {
  311.             generator.writeEntry(AemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false, false);
  312.         }
  313.         if (metadata.getUseableStopTime() != null) {
  314.             generator.writeEntry(AemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false, false);
  315.         }
  316.         generator.writeEntry(AemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), false, true);

  317.         // types
  318.         final AttitudeType attitudeType = metadata.getAttitudeType();
  319.         generator.writeEntry(AemMetadataKey.ATTITUDE_TYPE.name(), attitudeType.getName(formatVersion), null, true);
  320.         if (formatVersion < 2.0) {
  321.             if (attitudeType == AttitudeType.QUATERNION ||
  322.                 attitudeType == AttitudeType.QUATERNION_DERIVATIVE ||
  323.                 attitudeType == AttitudeType.QUATERNION_ANGVEL) {
  324.                 generator.writeEntry(AemMetadataKey.QUATERNION_TYPE.name(), metadata.isFirst() ? FIRST : LAST, null, false);
  325.             }
  326.         }

  327.         if (attitudeType == AttitudeType.QUATERNION_EULER_RATES ||
  328.             attitudeType == AttitudeType.EULER_ANGLE            ||
  329.             attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE ||
  330.             attitudeType == AttitudeType.EULER_ANGLE_ANGVEL) {
  331.             if (formatVersion < 2.0) {
  332.                 generator.writeEntry(AemMetadataKey.EULER_ROT_SEQ.name(),
  333.                                      metadata.getEulerRotSeq().name().replace('X', '1').replace('Y', '2').replace('Z', '3'),
  334.                                      null, false);
  335.             } else {
  336.                 generator.writeEntry(AemMetadataKey.EULER_ROT_SEQ.name(),
  337.                                      metadata.getEulerRotSeq().name(),
  338.                                      null, false);
  339.             }
  340.         }

  341.         if (formatVersion < 2 && attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE) {
  342.             generator.writeEntry(AemMetadataKey.RATE_FRAME.name(),
  343.                                  metadata.rateFrameIsA() ? REF_FRAME_A : REF_FRAME_B,
  344.                                  null, false);
  345.         }

  346.         if (attitudeType == AttitudeType.QUATERNION_ANGVEL ||
  347.             attitudeType == AttitudeType.EULER_ANGLE_ANGVEL) {
  348.             generator.writeEntry(AemMetadataKey.ANGVEL_FRAME.name(),
  349.                                  metadata.getFrameAngvelFrame().getName(),
  350.                                  null, true);
  351.         }

  352.         // interpolation
  353.         if (metadata.getInterpolationMethod() != null) {
  354.             generator.writeEntry(AemMetadataKey.INTERPOLATION_METHOD.name(),
  355.                                  metadata.getInterpolationMethod(),
  356.                                  null, true);
  357.             generator.writeEntry(AemMetadataKey.INTERPOLATION_DEGREE.name(),
  358.                                  Integer.toString(metadata.getInterpolationDegree()),
  359.                                  null, true);
  360.         }

  361.         // Stop metadata
  362.         generator.exitSection();

  363.     }

  364.     /**
  365.      * Write a single attitude ephemeris line according to section 4.2.4 and Table 4-4.
  366.      * @param generator generator to use for producing output
  367.      * @param formatVersion format version to use
  368.      * @param metadata metadata to use for interpreting data
  369.      * @param attitude the attitude information for a given date
  370.      * @throws IOException if the output stream throws one while writing.
  371.      */
  372.     void writeAttitudeEphemerisLine(final Generator generator, final double formatVersion,
  373.                                     final AemMetadata metadata,
  374.                                     final TimeStampedAngularCoordinates attitude)
  375.         throws IOException {

  376.         // Attitude data in CCSDS units
  377.         final String[] data = metadata.getAttitudeType().createDataFields(metadata.isFirst(),
  378.                                                                           metadata.getEndpoints().isExternal2SpacecraftBody(),
  379.                                                                           metadata.getEulerRotSeq(),
  380.                                                                           metadata.isSpacecraftBodyRate(),
  381.                                                                           attitude,
  382.                                                                           generator.getFormatter());

  383.         if (generator.getFormat() == FileFormat.KVN) {

  384.             // epoch
  385.             generator.writeRawData(generator.dateToString(getTimeConverter(), attitude.getDate()));

  386.             // data
  387.             for (String datum : data) {
  388.                 generator.writeRawData(' ');
  389.                 generator.writeRawData(datum);
  390.             }

  391.             // end the line
  392.             generator.newLine();

  393.         } else {
  394.             final XmlGenerator xmlGenerator = (XmlGenerator) generator;
  395.             xmlGenerator.enterSection(XmlSubStructureKey.attitudeState.name());
  396.             switch (metadata.getAttitudeType()) {
  397.                 case QUATERNION :
  398.                     writeQuaternion(xmlGenerator, formatVersion, metadata.isFirst(), attitude.getDate(), data);
  399.                     break;
  400.                 case QUATERNION_DERIVATIVE :
  401.                     writeQuaternionDerivative(xmlGenerator, formatVersion, metadata.isFirst(), attitude.getDate(), data);
  402.                     break;
  403.                 case QUATERNION_EULER_RATES :
  404.                     writeQuaternionEulerRates(xmlGenerator, metadata.isFirst(), metadata.getEulerRotSeq(), attitude.getDate(), data);
  405.                     break;
  406.                 case QUATERNION_ANGVEL :
  407.                     writeQuaternionAngularVelocity(xmlGenerator, attitude.getDate(), data);
  408.                     break;
  409.                 case EULER_ANGLE :
  410.                     writeEulerAngle(xmlGenerator, formatVersion, metadata.getEulerRotSeq(), attitude.getDate(), data);
  411.                     break;
  412.                 case EULER_ANGLE_DERIVATIVE :
  413.                     writeEulerAngleDerivative(xmlGenerator, formatVersion, metadata.getEulerRotSeq(), attitude.getDate(), data);
  414.                     break;
  415.                 case EULER_ANGLE_ANGVEL :
  416.                     writeEulerAngleAngularVelocity(xmlGenerator, formatVersion, metadata.getEulerRotSeq(), attitude.getDate(), data);
  417.                     break;
  418.                 case SPIN :
  419.                     writeSpin(xmlGenerator, attitude.getDate(), data);
  420.                     break;
  421.                 case SPIN_NUTATION :
  422.                     writeSpinNutation(xmlGenerator, attitude.getDate(), data);
  423.                     break;
  424.                 case SPIN_NUTATION_MOMENTUM :
  425.                     writeSpinNutationMomentum(xmlGenerator, attitude.getDate(), data);
  426.                     break;
  427.                 default :
  428.                     // this should never happen
  429.                     throw new OrekitInternalError(null);
  430.             }
  431.             generator.exitSection();
  432.         }

  433.     }

  434.     /** Write a quaternion entry in XML.
  435.      * @param xmlGenerator generator to use for producing output
  436.      * @param formatVersion format version to use
  437.      * @param first flag for scalar component to appear first (only relevant in ADM V1)
  438.      * @param epoch of the entry
  439.      * @param data entry data
  440.      * @throws IOException if the output stream throws one while writing.
  441.      */
  442.     void writeQuaternion(final XmlGenerator xmlGenerator, final double formatVersion,
  443.                          final boolean first, final AbsoluteDate epoch, final String[] data)
  444.         throws IOException {

  445.         xmlGenerator.enterSection(formatVersion < 2.0 ?
  446.                                   AttitudeEntryKey.quaternionState.name() :
  447.                                   AttitudeEntryKey.quaternionEphemeris.name());

  448.         // data part
  449.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);

  450.         // wrapping element
  451.         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());

  452.         // quaternion part
  453.         int i = 0;
  454.         if (formatVersion < 2.0 && first) {
  455.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
  456.         }
  457.         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, false);
  458.         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, false);
  459.         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, false);
  460.         if (!(formatVersion < 2.0 && first)) {
  461.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
  462.         }

  463.         xmlGenerator.exitSection();
  464.         xmlGenerator.exitSection();

  465.     }

  466.     /** Write a quaternion/derivative entry in XML.
  467.      * @param xmlGenerator generator to use for producing output
  468.      * @param formatVersion format version to use
  469.      * @param first flag for scalar component to appear first (only relevant in ADM V1)
  470.      * @param epoch of the entry
  471.      * @param data entry data
  472.      * @throws IOException if the output stream throws one while writing.
  473.      */
  474.     void writeQuaternionDerivative(final XmlGenerator xmlGenerator, final double formatVersion,
  475.                                    final boolean first, final AbsoluteDate epoch, final String[] data)
  476.         throws IOException {

  477.         // wrapping element
  478.         xmlGenerator.enterSection(AttitudeEntryKey.quaternionDerivative.name());

  479.         // data part
  480.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  481.         int i = 0;

  482.         // quaternion part
  483.         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
  484.         if (formatVersion < 2.0 && first) {
  485.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
  486.         }
  487.         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
  488.         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
  489.         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
  490.         if (!(formatVersion < 2.0 && first)) {
  491.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
  492.         }
  493.         xmlGenerator.exitSection();

  494.         // derivative part
  495.         xmlGenerator.enterSection(formatVersion < 2.0 ?
  496.                                   AttitudeEntryKey.quaternionRate.name() :
  497.                                   AttitudeEntryKey.quaternionDot.name());
  498.         if (formatVersion < 2.0 && first) {
  499.             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
  500.         }
  501.         xmlGenerator.writeEntry(AttitudeEntryKey.Q1_DOT.name(), data[i++], Units.ONE_PER_S, true);
  502.         xmlGenerator.writeEntry(AttitudeEntryKey.Q2_DOT.name(), data[i++], Units.ONE_PER_S, true);
  503.         xmlGenerator.writeEntry(AttitudeEntryKey.Q3_DOT.name(), data[i++], Units.ONE_PER_S, true);
  504.         if (!(formatVersion < 2.0 && first)) {
  505.             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
  506.         }
  507.         xmlGenerator.exitSection();

  508.         xmlGenerator.exitSection();

  509.     }

  510.     /** Write a quaternion/Euler rates entry in XML.
  511.      * @param xmlGenerator generator to use for producing output
  512.      * @param first flag for scalar component to appear first (only relevant in ADM V1)
  513.      * @param order Euler rotation order
  514.      * @param epoch of the entry
  515.      * @param data entry data
  516.      * @throws IOException if the output stream throws one while writing.
  517.      */
  518.     void writeQuaternionEulerRates(final XmlGenerator xmlGenerator, final boolean first, final RotationOrder order,
  519.                                    final AbsoluteDate epoch, final String[] data)
  520.         throws IOException {

  521.         // wrapping element
  522.         xmlGenerator.enterSection(AttitudeEntryKey.quaternionEulerRate.name());

  523.         // data part
  524.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  525.         int i = 0;

  526.         // quaternion part
  527.         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
  528.         if (first) {
  529.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
  530.         }
  531.         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
  532.         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
  533.         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
  534.         if (!first) {
  535.             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
  536.         }
  537.         xmlGenerator.exitSection();

  538.         // derivative part
  539.         xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
  540.         writeLabeledEulerRate(xmlGenerator, 0, order.name(), data[i++]);
  541.         writeLabeledEulerRate(xmlGenerator, 1, order.name(), data[i++]);
  542.         writeLabeledEulerRate(xmlGenerator, 2, order.name(), data[i++]);
  543.         xmlGenerator.exitSection();

  544.         xmlGenerator.exitSection();

  545.     }

  546.     /** Write a quaternion/rate entry in XML.
  547.      * @param xmlGenerator generator to use for producing output
  548.      * @param epoch of the entry
  549.      * @param data entry data
  550.      * @throws IOException if the output stream throws one while writing.
  551.      */
  552.     void writeQuaternionAngularVelocity(final XmlGenerator xmlGenerator,
  553.                                         final AbsoluteDate epoch, final String[] data)
  554.         throws IOException {

  555.         // wrapping element
  556.         xmlGenerator.enterSection(AttitudeEntryKey.quaternionAngVel.name());

  557.         // data part
  558.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  559.         int i = 0;

  560.         // quaternion part
  561.         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
  562.         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
  563.         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
  564.         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
  565.         xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
  566.         xmlGenerator.exitSection();

  567.         // angular velocity part
  568.         xmlGenerator.enterSection(AttitudeEntryKey.angVel.name());
  569.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_X.name(), data[i++], Units.DEG_PER_S, true);
  570.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Y.name(), data[i++], Units.DEG_PER_S, true);
  571.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Z.name(), data[i++], Units.DEG_PER_S, true);
  572.         xmlGenerator.exitSection();

  573.         xmlGenerator.exitSection();

  574.     }

  575.     /** Write a Euler angles entry in XML.
  576.      * @param xmlGenerator generator to use for producing output
  577.      * @param formatVersion format version to use
  578.      * @param order Euler rotation order
  579.      * @param epoch of the entry
  580.      * @param data entry data
  581.      * @throws IOException if the output stream throws one while writing.
  582.      */
  583.     void writeEulerAngle(final XmlGenerator xmlGenerator, final double formatVersion,
  584.                          final RotationOrder order, final AbsoluteDate epoch, final String[] data)
  585.         throws IOException {

  586.         // wrapping element
  587.         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngle.name());

  588.         // data part
  589.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  590.         int i = 0;

  591.         // angle part
  592.         if (formatVersion < 2.0) {
  593.             xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
  594.             writeLabeledEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
  595.             writeLabeledEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
  596.             writeLabeledEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
  597.             xmlGenerator.exitSection();
  598.         } else {
  599.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(), data[i++], Unit.DEGREE, true);
  600.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(), data[i++], Unit.DEGREE, true);
  601.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(), data[i++], Unit.DEGREE, true);
  602.         }

  603.         xmlGenerator.exitSection();

  604.     }

  605.     /** Write a Euler angles entry in XML.
  606.      * @param xmlGenerator generator to use for producing output
  607.      * @param formatVersion format version to use
  608.      * @param order Euler rotation order
  609.      * @param epoch of the entry
  610.      * @param data entry data
  611.      * @throws IOException if the output stream throws one while writing.
  612.      */
  613.     void writeEulerAngleDerivative(final XmlGenerator xmlGenerator, final double formatVersion,
  614.                                    final RotationOrder order, final AbsoluteDate epoch, final String[] data)
  615.         throws IOException {

  616.         // wrapping element
  617.         xmlGenerator.enterSection(formatVersion < 2.0 ?
  618.                                   AttitudeEntryKey.eulerAngleRate.name() :
  619.                                   AttitudeEntryKey.eulerAngleDerivative.name());

  620.         // data part
  621.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  622.         int i = 0;

  623.         // angle part
  624.         if (formatVersion < 2.0) {
  625.             xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
  626.             writeLabeledEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
  627.             writeLabeledEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
  628.             writeLabeledEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
  629.             xmlGenerator.exitSection();
  630.             xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
  631.             writeLabeledEulerRate(xmlGenerator, 0, order.name(), data[i++]);
  632.             writeLabeledEulerRate(xmlGenerator, 1, order.name(), data[i++]);
  633.             writeLabeledEulerRate(xmlGenerator, 2, order.name(), data[i++]);
  634.             xmlGenerator.exitSection();
  635.         } else {
  636.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(),     data[i++], Unit.DEGREE,     true);
  637.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(),     data[i++], Unit.DEGREE,     true);
  638.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(),     data[i++], Unit.DEGREE,     true);
  639.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1_DOT.name(), data[i++], Units.DEG_PER_S, true);
  640.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2_DOT.name(), data[i++], Units.DEG_PER_S, true);
  641.             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3_DOT.name(), data[i++], Units.DEG_PER_S, true);
  642.         }

  643.         xmlGenerator.exitSection();

  644.     }

  645.     /** Write a Euler angles/angular velocity entry in XML.
  646.      * @param xmlGenerator generator to use for producing output
  647.      * @param formatVersion format version to use
  648.      * @param order Euler rotation order
  649.      * @param epoch of the entry
  650.      * @param data entry data
  651.      * @throws IOException if the output stream throws one while writing.
  652.      */
  653.     void writeEulerAngleAngularVelocity(final XmlGenerator xmlGenerator, final double formatVersion, final RotationOrder order,
  654.                                         final AbsoluteDate epoch, final String[] data)
  655.         throws IOException {

  656.         // wrapping element
  657.         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngleAngVel.name());

  658.         // data part
  659.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  660.         int i = 0;

  661.         // angle part
  662.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(), data[i++], Unit.DEGREE, true);
  663.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(), data[i++], Unit.DEGREE, true);
  664.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(), data[i++], Unit.DEGREE, true);

  665.         // angular velocity part
  666.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_X.name(), data[i++], Units.DEG_PER_S, true);
  667.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Y.name(), data[i++], Units.DEG_PER_S, true);
  668.         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Z.name(), data[i++], Units.DEG_PER_S, true);

  669.         xmlGenerator.exitSection();

  670.     }

  671.     /** Write a spin entry in XML.
  672.      * @param xmlGenerator generator to use for producing output
  673.      * @param epoch of the entry
  674.      * @param data entry data
  675.      * @throws IOException if the output stream throws one while writing.
  676.      */
  677.     void writeSpin(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
  678.         throws IOException {

  679.         // wrapping element
  680.         xmlGenerator.enterSection(AttitudeEntryKey.spin.name());

  681.         // data part
  682.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  683.         int i = 0;
  684.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
  685.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
  686.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
  687.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);

  688.         xmlGenerator.exitSection();

  689.     }

  690.     /** Write a spin/nutation entry in XML.
  691.      * @param xmlGenerator generator to use for producing output
  692.      * @param epoch of the entry
  693.      * @param data entry data
  694.      * @throws IOException if the output stream throws one while writing.
  695.      */
  696.     void writeSpinNutation(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
  697.         throws IOException {

  698.         // wrapping element
  699.         xmlGenerator.enterSection(AttitudeEntryKey.spinNutation.name());

  700.         // data part
  701.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  702.         int i = 0;
  703.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
  704.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
  705.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
  706.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
  707.         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION.name(),       data[i++], Unit.DEGREE,     true);
  708.         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PER.name(),   data[i++], Unit.SECOND,     true);
  709.         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PHASE.name(), data[i++], Unit.DEGREE,     true);

  710.         xmlGenerator.exitSection();

  711.     }

  712.     /** Write a spin/nutation/momentum entry in XML.
  713.      * @param xmlGenerator generator to use for producing output
  714.      * @param epoch of the entry
  715.      * @param data entry data
  716.      * @throws IOException if the output stream throws one while writing.
  717.      */
  718.     void writeSpinNutationMomentum(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
  719.         throws IOException {

  720.         // wrapping element
  721.         xmlGenerator.enterSection(AttitudeEntryKey.spinNutationMom.name());

  722.         // data part
  723.         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
  724.         int i = 0;
  725.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
  726.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
  727.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
  728.         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
  729.         xmlGenerator.writeEntry(AttitudeEntryKey.MOMENTUM_ALPHA.name(), data[i++], Unit.DEGREE,     true);
  730.         xmlGenerator.writeEntry(AttitudeEntryKey.MOMENTUM_DELTA.name(), data[i++], Unit.DEGREE,     true);
  731.         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_VEL.name(),   data[i++], Units.DEG_PER_S, true);

  732.         xmlGenerator.exitSection();

  733.     }

  734.     /** Write an angle from an Euler sequence.
  735.      * @param xmlGenerator generator to use
  736.      * @param index angle index
  737.      * @param seq Euler sequence
  738.      * @param angle angle value
  739.      * @throws IOException if the output stream throws one while writing.
  740.      */
  741.     private void writeLabeledEulerAngle(final XmlGenerator xmlGenerator, final int index,
  742.                                         final String seq, final String angle)
  743.         throws IOException {
  744.         if (xmlGenerator.writeUnits(Unit.DEGREE)) {
  745.             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), angle,
  746.                                                    ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX,
  747.                                                    XmlGenerator.UNITS,
  748.                                                    xmlGenerator.siToCcsdsName(Unit.DEGREE.getName()));
  749.         } else {
  750.             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), angle,
  751.                                                   ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX);
  752.         }
  753.     }

  754.     /** Write a rate from an Euler sequence.
  755.      * @param xmlGenerator generator to use
  756.      * @param index angle index
  757.      * @param seq Euler sequence
  758.      * @param rate rate value
  759.      * @throws IOException if the output stream throws one while writing.
  760.      */
  761.     private void writeLabeledEulerRate(final XmlGenerator xmlGenerator, final int index, final String seq, final String rate)
  762.         throws IOException {
  763.         if (xmlGenerator.writeUnits(Units.DEG_PER_S)) {
  764.             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), rate,
  765.                                                    RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX,
  766.                                                    XmlGenerator.UNITS,
  767.                                                    xmlGenerator.siToCcsdsName(Units.DEG_PER_S.getName()));
  768.         } else {
  769.             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), rate,
  770.                                                   RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX);
  771.         }
  772.     }

  773.     /** Start of an attitude block.
  774.      * @param generator generator to use for producing output
  775.      * @throws IOException if the output stream throws one while writing.
  776.      */
  777.     void startAttitudeBlock(final Generator generator) throws IOException {
  778.         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
  779.                                KvnStructureKey.DATA.name() :
  780.                                XmlStructureKey.data.name());
  781.     }

  782.     /** End of an attitude block.
  783.      * @param generator generator to use for producing output
  784.      * @throws IOException if the output stream throws one while writing.
  785.      */
  786.     void endAttitudeBlock(final Generator generator) throws IOException {
  787.         generator.exitSection();
  788.     }

  789. }