1   /* Copyright 2002-2026 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  
19  import java.io.IOException;
20  import java.util.Date;
21  
22  import org.hipparchus.geometry.euclidean.threed.RotationOrder;
23  import org.orekit.data.DataContext;
24  import org.orekit.errors.OrekitException;
25  import org.orekit.errors.OrekitInternalError;
26  import org.orekit.errors.OrekitMessages;
27  import org.orekit.files.ccsds.definitions.TimeSystem;
28  import org.orekit.files.ccsds.definitions.Units;
29  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
30  import org.orekit.files.ccsds.ndm.adm.AdmCommonMetadataKey;
31  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
32  import org.orekit.files.ccsds.ndm.adm.AdmMetadataKey;
33  import org.orekit.files.ccsds.ndm.adm.AttitudeType;
34  import org.orekit.files.ccsds.section.Header;
35  import org.orekit.files.ccsds.section.HeaderKey;
36  import org.orekit.files.ccsds.section.KvnStructureKey;
37  import org.orekit.files.ccsds.section.MetadataKey;
38  import org.orekit.files.ccsds.section.XmlStructureKey;
39  import org.orekit.files.ccsds.utils.ContextBinding;
40  import org.orekit.files.ccsds.utils.FileFormat;
41  import org.orekit.files.ccsds.utils.generation.AbstractMessageWriter;
42  import org.orekit.files.ccsds.utils.generation.Generator;
43  import org.orekit.files.ccsds.utils.generation.XmlGenerator;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.utils.IERSConventions;
46  import org.orekit.utils.TimeStampedAngularCoordinates;
47  import org.orekit.utils.units.Unit;
48  
49  /**
50   * A writer for Attitude Ephemeris Messsage (AEM) files.
51   *
52   * <h2> Metadata </h2>
53   *
54   * <p> The AEM header and metadata used by this writer are described in the following tables.
55   * Many metadata items are optional or have default values so they do not need to be specified.
56   * At a minimum the user must supply those values that are required and for which no
57   * default exits: {@link AdmMetadataKey#OBJECT_NAME}, {@link AdmCommonMetadataKey#OBJECT_ID},
58   * {@link AemMetadataKey#START_TIME} and {@link AemMetadataKey#STOP_TIME}.
59   * The usage column in the table indicates where the metadata item is used, either in the AEM header
60   * or in the metadata section at the start of an AEM attitude segment.
61   * </p>
62   *
63   * <p> The AEM header for the whole AEM file is set when calling
64   * {@link AbstractMessageWriter#writeHeader(Generator, Header)},
65   * the entries are defined in table 4-2 of the ADM standard.
66   *
67   * <table>
68   * <caption>AEM metadata</caption>
69   *     <thead>
70   *         <tr>
71   *             <th>Keyword</th>
72   *             <th>Mandatory</th>
73   *             <th>Default in Orekit</th>
74   *         </tr>
75   *    </thead>
76   *    <tbody>
77   *        <tr>
78   *            <td>{@link Aem#FORMAT_VERSION_KEY CCSDS_AEM_VERS}</td>
79   *            <td>Yes</td>
80   *            <td>{@link #CCSDS_AEM_VERS}</td>
81   *        </tr>
82   *        <tr>
83   *            <td>{@link HeaderKey#COMMENT}</td>
84   *            <td>No</td>
85   *            <td>empty</td>
86   *        </tr>
87   *        <tr>
88   *            <td>{@link HeaderKey#CREATION_DATE}</td>
89   *            <td>Yes</td>
90   *            <td>{@link Date#Date() Now}</td>
91   *        </tr>
92   *        <tr>
93   *            <td>{@link HeaderKey#ORIGINATOR}</td>
94   *            <td>Yes</td>
95   *            <td>{@link #DEFAULT_ORIGINATOR}</td>
96   *        </tr>
97   *    </tbody>
98   *    </table>
99   *
100  * <p> The AEM metadata for the AEM file is set when calling {@link #writeSegmentContent(Generator, double, AemSegment)},
101  * the entries are defined in tables 4-3, 4-4 and annex A of the ADM standard.
102  *
103  * <table>
104  * <caption>AEM metadata</caption>
105  *     <thead>
106  *         <tr>
107  *             <th>Keyword</th>
108  *             <th>Mandatory</th>
109  *             <th>Default in Orekit</th>
110  *         </tr>
111  *    </thead>
112  *    <tbody>
113  *        <tr>
114  *            <td>{@link MetadataKey#COMMENT}</td>
115  *            <td>No</td>
116  *            <td>empty</td>
117  *        </tr>
118  *        <tr>
119  *            <td>{@link AdmMetadataKey#OBJECT_NAME}</td>
120  *            <td>Yes</td>
121  *            <td></td>
122  *        </tr>
123  *        <tr>
124  *            <td>{@link AdmCommonMetadataKey#OBJECT_ID}</td>
125  *            <td>Yes</td>
126  *            <td></td>
127  *        </tr>
128  *        <tr>
129  *            <td>{@link AdmMetadataKey#CENTER_NAME}</td>
130  *            <td>No</td>
131  *            <td></td>
132  *        </tr>
133  *        <tr>
134  *            <td>{@link AemMetadataKey#REF_FRAME_A}</td>
135  *            <td>Yes</td>
136  *            <td></td>
137  *        </tr>
138  *        <tr>
139  *            <td>{@link AemMetadataKey#REF_FRAME_B}</td>
140  *            <td>Yes</td>
141  *            <td></td>
142  *        </tr>
143  *        <tr>
144  *            <td>{@link AemMetadataKey#ATTITUDE_DIR}</td>
145  *            <td>Yes</td>
146  *            <td></td>
147  *        </tr>
148  *        <tr>
149  *            <td>{@link MetadataKey#TIME_SYSTEM}</td>
150  *            <td>Yes</td>
151  *            <td></td>
152  *        </tr>
153  *        <tr>
154  *            <td>{@link AemMetadataKey#START_TIME}</td>
155  *            <td>Yes</td>
156  *            <td>default to propagation start time (for forward propagation)</td>
157  *        </tr>
158  *        <tr>
159  *            <td>{@link AemMetadataKey#USEABLE_START_TIME}</td>
160  *            <td>No</td>
161  *            <td></td>
162  *        </tr>
163  *        <tr>
164  *            <td>{@link AemMetadataKey#USEABLE_STOP_TIME}</td>
165  *            <td>No</td>
166  *            <td></td>
167  *        </tr>
168  *        <tr>
169  *            <td>{@link AemMetadataKey#STOP_TIME}</td>
170  *            <td>Yes</td>
171  *            <td>default to propagation target time (for forward propagation)</td>
172  *        </tr>
173  *        <tr>
174  *            <td>{@link AemMetadataKey#ATTITUDE_TYPE}</td>
175  *            <td>Yes</td>
176  *            <td>{@link AttitudeType#QUATERNION_DERIVATIVE QUATERNION/DERIVATIVE}</td>
177  *        </tr>
178  *        <tr>
179  *            <td>{@link AemMetadataKey#QUATERNION_TYPE}</td>
180  *            <td>No</td>
181  *            <td>{@code FIRST}</td>
182  *        </tr>
183  *        <tr>
184  *            <td>{@link AemMetadataKey#EULER_ROT_SEQ}</td>
185  *            <td>No</td>
186  *            <td></td>
187  *        </tr>
188  *        <tr>
189  *            <td>{@link AemMetadataKey#RATE_FRAME}</td>
190  *            <td>No</td>
191  *            <td>{@code REF_FRAME_B}</td>
192  *        </tr>
193  *        <tr>
194  *            <td>{@link AemMetadataKey#INTERPOLATION_METHOD}</td>
195  *            <td>No</td>
196  *            <td></td>
197  *        </tr>
198  *        <tr>
199  *            <td>{@link AemMetadataKey#INTERPOLATION_DEGREE}</td>
200  *            <td>No</td>
201  *            <td>always set in {@link AemMetadata}</td>
202  *        </tr>
203  *    </tbody>
204  *</table>
205  *
206  * <p> The {@link MetadataKey#TIME_SYSTEM} must be constant for the whole file and is used
207  * to interpret all dates except {@link HeaderKey#CREATION_DATE} which is always in {@link
208  * TimeSystem#UTC UTC}. The guessing algorithm is not guaranteed to work so it is recommended
209  * to provide values for {@link AdmMetadataKey#CENTER_NAME} and {@link MetadataKey#TIME_SYSTEM}
210  * to avoid any bugs associated with incorrect guesses.
211  *
212  * <p> Standardized values for {@link MetadataKey#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
213  * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
214  * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
215  * Additionally ITRF followed by a four digit year may be used.
216  *
217  * @author Bryan Cazabonne
218  * @since 10.2
219  */
220 public class AemWriter extends AbstractMessageWriter<AdmHeader, AemSegment, Aem> {
221 
222     /** Version number implemented. **/
223     public static final double CCSDS_AEM_VERS = 2.0;
224 
225     /** Padding width for aligning the '=' sign. */
226     public static final int KVN_PADDING_WIDTH = 20;
227 
228     /** Constant for frame A to frame B attitude. */
229     private static final String A_TO_B = "A2B";
230 
231     /** Constant for frame B to frame A attitude. */
232     private static final String B_TO_A = "B2A";
233 
234     /** Constant for quaternions with scalar component in  position. */
235     private static final String FIRST = "FIRST";
236 
237     /** Constant for quaternions with scalar component in last position. */
238     private static final String LAST = "LAST";
239 
240     /** Constant for angular rates in frame A. */
241     private static final String REF_FRAME_A = "REF_FRAME_A";
242 
243     /** Constant for angular rates in frame B. */
244     private static final String REF_FRAME_B = "REF_FRAME_B";
245 
246     /** Prefix for Euler rotations. */
247     private static final String ROTATION = "rotation";
248 
249     /** Attribute for Euler angles. */
250     private static final String ANGLE_ATTRIBUTE = "angle";
251 
252     /** Suffix for Euler angles. */
253     private static final String ANGLE_SUFFIX = "_ANGLE";
254 
255     /**Attribute for Euler rates. */
256     private static final String RATE_ATTRIBUTE = "rate";
257 
258     /** Suffix for Euler rates. */
259     private static final String RATE_SUFFIX = "_RATE";
260 
261     /**
262      * Constructor used to create a new AEM writer configured with the necessary parameters
263      * to successfully fill in all required fields that aren't part of a standard object.
264      * @param conventions IERS Conventions
265      * @param dataContext used to retrieve frames, time scales, etc.
266      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
267      * @since 11.0
268      */
269     public AemWriter(final IERSConventions conventions, final DataContext dataContext,
270                      final AbsoluteDate missionReferenceDate) {
271         super(Aem.ROOT, Aem.FORMAT_VERSION_KEY, CCSDS_AEM_VERS,
272               new ContextBinding(
273                   () -> conventions,
274                   () -> true, () -> dataContext, () -> ParsedUnitsBehavior.STRICT_COMPLIANCE,
275                   () -> missionReferenceDate, () -> TimeSystem.UTC,
276                   () -> 0.0, () -> 1.0));
277     }
278 
279     /** {@inheritDoc} */
280     @Override
281     protected void writeSegmentContent(final Generator generator, final double formatVersion,
282                                        final AemSegment segment)
283         throws IOException {
284 
285         final AemMetadata metadata = segment.getMetadata();
286         writeMetadata(generator, formatVersion, metadata);
287 
288         // Loop on attitude data
289         startAttitudeBlock(generator);
290         generator.writeComments(segment.getData().getComments());
291         for (final TimeStampedAngularCoordinates coordinates : segment.getAngularCoordinates()) {
292             writeAttitudeEphemerisLine(generator, formatVersion, metadata, coordinates);
293         }
294         endAttitudeBlock(generator);
295 
296     }
297 
298     /** Write an ephemeris segment metadata.
299      * @param generator generator to use for producing output
300      * @param formatVersion format version
301      * @param metadata metadata to write
302      * @throws IOException if the output stream throws one while writing.
303      */
304     void writeMetadata(final Generator generator, final double formatVersion, final AemMetadata metadata)
305         throws IOException {
306 
307         final ContextBinding oldContext = getContext();
308         setContext(new ContextBinding(oldContext::getConventions,
309                                       oldContext::isSimpleEOP,
310                                       oldContext::getDataContext,
311                                       oldContext::getParsedUnitsBehavior,
312                                       oldContext::getReferenceDate,
313                                       metadata::getTimeSystem,
314                                       oldContext::getClockCount,
315                                       oldContext::getClockRate));
316 
317         // Start metadata
318         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
319                                KvnStructureKey.META.name() :
320                                XmlStructureKey.metadata.name());
321 
322         generator.writeComments(metadata.getComments());
323 
324         // objects
325         generator.writeEntry(AdmMetadataKey.OBJECT_NAME.name(),     metadata.getObjectName(), null, true);
326         generator.writeEntry(AdmCommonMetadataKey.OBJECT_ID.name(), metadata.getObjectID(),   null, true);
327         if (metadata.getCenter().isPresent()) {
328             generator.writeEntry(AdmMetadataKey.CENTER_NAME.name(), metadata.getCenter().get().getName(), null, false);
329         }
330 
331         // frames
332         generator.writeEntry(AemMetadataKey.REF_FRAME_A.name(),  metadata.getEndpoints().getFrameA().getName(),     null, true);
333         generator.writeEntry(AemMetadataKey.REF_FRAME_B.name(),  metadata.getEndpoints().getFrameB().getName(),     null, true);
334         if (formatVersion < 2.0) {
335             generator.writeEntry(AemMetadataKey.ATTITUDE_DIR.name(), metadata.getEndpoints().isA2b() ? A_TO_B : B_TO_A, null, true);
336         }
337 
338         // time
339         generator.writeEntry(MetadataKey.TIME_SYSTEM.name(), metadata.getTimeSystem(), true);
340         generator.writeEntry(AemMetadataKey.START_TIME.name(), getTimeConverter(), metadata.getStartTime(), false, true);
341         generator.writeOptionalDateEntry(AemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false, false);
342         generator.writeOptionalDateEntry(AemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false, false);
343         generator.writeEntry(AemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), false, true);
344 
345         // types
346         final AttitudeType attitudeType = metadata.getAttitudeType();
347         generator.writeEntry(AemMetadataKey.ATTITUDE_TYPE.name(), attitudeType.getName(formatVersion), null, true);
348         if (formatVersion < 2.0) {
349             if (attitudeType == AttitudeType.QUATERNION ||
350                 attitudeType == AttitudeType.QUATERNION_DERIVATIVE ||
351                 attitudeType == AttitudeType.QUATERNION_ANGVEL) {
352                 generator.writeEntry(AemMetadataKey.QUATERNION_TYPE.name(), metadata.isFirst() ? FIRST : LAST, null, false);
353             }
354         }
355 
356         if (attitudeType == AttitudeType.QUATERNION_EULER_RATES ||
357             attitudeType == AttitudeType.EULER_ANGLE            ||
358             attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE ||
359             attitudeType == AttitudeType.EULER_ANGLE_ANGVEL) {
360             if (formatVersion < 2.0) {
361                 generator.writeEntry(AemMetadataKey.EULER_ROT_SEQ.name(),
362                                      metadata.getEulerRotSeq()
363                                              .orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE))
364                                              .name()
365                                              .replace('X', '1')
366                                              .replace('Y', '2')
367                                              .replace('Z', '3'),
368                                      null, false);
369             } else {
370                 generator.writeEntry(AemMetadataKey.EULER_ROT_SEQ.name(),
371                                      metadata.getEulerRotSeq()
372                                              .orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE))
373                                              .name(),
374                                      null, false);
375             }
376         }
377 
378         if (formatVersion < 2 && attitudeType == AttitudeType.EULER_ANGLE_DERIVATIVE) {
379             generator.writeEntry(AemMetadataKey.RATE_FRAME.name(),
380                                  metadata.rateFrameIsA() ? REF_FRAME_A : REF_FRAME_B,
381                                  null, false);
382         }
383 
384         if (attitudeType == AttitudeType.QUATERNION_ANGVEL ||
385             attitudeType == AttitudeType.EULER_ANGLE_ANGVEL) {
386             generator.writeEntry(AemMetadataKey.ANGVEL_FRAME.name(),
387                                  metadata.getFrameAngvelFrame()
388                                          .orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE))
389                                          .getName(),
390                                  null, true);
391         }
392 
393         // interpolation
394         if (metadata.getInterpolationMethod().isPresent()) {
395             generator.writeOptionalStringEntry(AemMetadataKey.INTERPOLATION_METHOD.name(),
396                                  metadata.getInterpolationMethod(),
397                                  null, true);
398             generator.writeEntry(AemMetadataKey.INTERPOLATION_DEGREE.name(),
399                                  Integer.toString(metadata.getInterpolationDegree()),
400                                  null, true);
401         }
402 
403         // Stop metadata
404         generator.exitSection();
405 
406     }
407 
408     /**
409      * Write a single attitude ephemeris line according to section 4.2.4 and Table 4-4.
410      * @param generator generator to use for producing output
411      * @param formatVersion format version to use
412      * @param metadata metadata to use for interpreting data
413      * @param attitude the attitude information for a given date
414      * @throws IOException if the output stream throws one while writing.
415      */
416     void writeAttitudeEphemerisLine(final Generator generator, final double formatVersion,
417                                     final AemMetadata metadata,
418                                     final TimeStampedAngularCoordinates attitude)
419         throws IOException {
420 
421         // Attitude data in CCSDS units
422         final String[] data = metadata.getAttitudeType().createDataFields(metadata.isFirst(),
423                                                                           metadata.getEndpoints().isExternal2SpacecraftBody(),
424                                                                           metadata.getEulerRotSeq().orElse(null),
425                                                                           metadata.isSpacecraftBodyRate(),
426                                                                           attitude,
427                                                                           generator.getFormatter());
428 
429         if (generator.getFormat() == FileFormat.KVN) {
430 
431             // epoch
432             generator.writeRawData(generator.dateToString(getTimeConverter(), attitude.getDate()));
433 
434             // data
435             for (String datum : data) {
436                 generator.writeRawData(' ');
437                 generator.writeRawData(datum);
438             }
439 
440             // end the line
441             generator.newLine();
442 
443         } else {
444             final XmlGenerator xmlGenerator = (XmlGenerator) generator;
445             xmlGenerator.enterSection(XmlSubStructureKey.attitudeState.name());
446             switch (metadata.getAttitudeType()) {
447                 case QUATERNION :
448                     writeQuaternion(xmlGenerator, formatVersion, metadata.isFirst(), attitude.getDate(), data);
449                     break;
450                 case QUATERNION_DERIVATIVE :
451                     writeQuaternionDerivative(xmlGenerator, formatVersion, metadata.isFirst(), attitude.getDate(), data);
452                     break;
453                 case QUATERNION_EULER_RATES :
454                     writeQuaternionEulerRates(xmlGenerator, metadata.isFirst(),
455                                               metadata.getEulerRotSeq().orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE)),
456                                               attitude.getDate(), data);
457                     break;
458                 case QUATERNION_ANGVEL :
459                     writeQuaternionAngularVelocity(xmlGenerator, attitude.getDate(), data);
460                     break;
461                 case EULER_ANGLE :
462                     writeEulerAngle(xmlGenerator, formatVersion,
463                                     metadata.getEulerRotSeq().orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE)),
464                                     attitude.getDate(), data);
465                     break;
466                 case EULER_ANGLE_DERIVATIVE :
467                     writeEulerAngleDerivative(xmlGenerator, formatVersion,
468                                               metadata.getEulerRotSeq().orElseThrow(() -> new OrekitException(OrekitMessages.CCSDS_MISSING_OPTIONAL_VALUE)),
469                                               attitude.getDate(), data);
470                     break;
471                 case EULER_ANGLE_ANGVEL :
472                     writeEulerAngleAngularVelocity(xmlGenerator, attitude.getDate(), data);
473                     break;
474                 case SPIN :
475                     writeSpin(xmlGenerator, attitude.getDate(), data);
476                     break;
477                 case SPIN_NUTATION :
478                     writeSpinNutation(xmlGenerator, attitude.getDate(), data);
479                     break;
480                 case SPIN_NUTATION_MOMENTUM :
481                     writeSpinNutationMomentum(xmlGenerator, attitude.getDate(), data);
482                     break;
483                 default :
484                     // this should never happen
485                     throw new OrekitInternalError(null);
486             }
487             generator.exitSection();
488         }
489 
490     }
491 
492     /** Write a quaternion entry in XML.
493      * @param xmlGenerator generator to use for producing output
494      * @param formatVersion format version to use
495      * @param first flag for scalar component to appear first (only relevant in ADM V1)
496      * @param epoch of the entry
497      * @param data entry data
498      * @throws IOException if the output stream throws one while writing.
499      */
500     void writeQuaternion(final XmlGenerator xmlGenerator, final double formatVersion,
501                          final boolean first, final AbsoluteDate epoch, final String[] data)
502         throws IOException {
503 
504         xmlGenerator.enterSection(formatVersion < 2.0 ?
505                                   AttitudeEntryKey.quaternionState.name() :
506                                   AttitudeEntryKey.quaternionEphemeris.name());
507 
508         // data part
509         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
510 
511         // wrapping element
512         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
513 
514         // quaternion part
515         int i = 0;
516         if (formatVersion < 2.0 && first) {
517             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
518         }
519         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, false);
520         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, false);
521         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, false);
522         if (!(formatVersion < 2.0 && first)) {
523             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
524         }
525 
526         xmlGenerator.exitSection();
527         xmlGenerator.exitSection();
528 
529     }
530 
531     /** Write a quaternion/derivative entry in XML.
532      * @param xmlGenerator generator to use for producing output
533      * @param formatVersion format version to use
534      * @param first flag for scalar component to appear first (only relevant in ADM V1)
535      * @param epoch of the entry
536      * @param data entry data
537      * @throws IOException if the output stream throws one while writing.
538      */
539     void writeQuaternionDerivative(final XmlGenerator xmlGenerator, final double formatVersion,
540                                    final boolean first, final AbsoluteDate epoch, final String[] data)
541         throws IOException {
542 
543         // wrapping element
544         xmlGenerator.enterSection(AttitudeEntryKey.quaternionDerivative.name());
545 
546         // data part
547         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
548         int i = 0;
549 
550         // quaternion part
551         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
552         if (formatVersion < 2.0 && first) {
553             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
554         }
555         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
556         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
557         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
558         if (!(formatVersion < 2.0 && first)) {
559             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
560         }
561         xmlGenerator.exitSection();
562 
563         // derivative part
564         xmlGenerator.enterSection(formatVersion < 2.0 ?
565                                   AttitudeEntryKey.quaternionRate.name() :
566                                   AttitudeEntryKey.quaternionDot.name());
567         if (formatVersion < 2.0 && first) {
568             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
569         }
570         xmlGenerator.writeEntry(AttitudeEntryKey.Q1_DOT.name(), data[i++], Units.ONE_PER_S, true);
571         xmlGenerator.writeEntry(AttitudeEntryKey.Q2_DOT.name(), data[i++], Units.ONE_PER_S, true);
572         xmlGenerator.writeEntry(AttitudeEntryKey.Q3_DOT.name(), data[i++], Units.ONE_PER_S, true);
573         if (!(formatVersion < 2.0 && first)) {
574             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
575         }
576         xmlGenerator.exitSection();
577 
578         xmlGenerator.exitSection();
579 
580     }
581 
582     /** Write a quaternion/Euler rates entry in XML.
583      * @param xmlGenerator generator to use for producing output
584      * @param first flag for scalar component to appear first (only relevant in ADM V1)
585      * @param order Euler rotation order
586      * @param epoch of the entry
587      * @param data entry data
588      * @throws IOException if the output stream throws one while writing.
589      */
590     void writeQuaternionEulerRates(final XmlGenerator xmlGenerator, final boolean first, final RotationOrder order,
591                                    final AbsoluteDate epoch, final String[] data)
592         throws IOException {
593 
594         // wrapping element
595         xmlGenerator.enterSection(AttitudeEntryKey.quaternionEulerRate.name());
596 
597         // data part
598         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
599         int i = 0;
600 
601         // quaternion part
602         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
603         if (first) {
604             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
605         }
606         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
607         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
608         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
609         if (!first) {
610             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
611         }
612         xmlGenerator.exitSection();
613 
614         // derivative part
615         xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
616         writeLabeledEulerRate(xmlGenerator, 0, order.name(), data[i++]);
617         writeLabeledEulerRate(xmlGenerator, 1, order.name(), data[i++]);
618         writeLabeledEulerRate(xmlGenerator, 2, order.name(), data[i++]);
619         xmlGenerator.exitSection();
620 
621         xmlGenerator.exitSection();
622 
623     }
624 
625     /** Write a quaternion/rate entry in XML.
626      * @param xmlGenerator generator to use for producing output
627      * @param epoch of the entry
628      * @param data entry data
629      * @throws IOException if the output stream throws one while writing.
630      */
631     void writeQuaternionAngularVelocity(final XmlGenerator xmlGenerator,
632                                         final AbsoluteDate epoch, final String[] data)
633         throws IOException {
634 
635         // wrapping element
636         xmlGenerator.enterSection(AttitudeEntryKey.quaternionAngVel.name());
637 
638         // data part
639         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
640         int i = 0;
641 
642         // quaternion part
643         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
644         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
645         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
646         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
647         xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
648         xmlGenerator.exitSection();
649 
650         // angular velocity part
651         xmlGenerator.enterSection(AttitudeEntryKey.angVel.name());
652         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_X.name(), data[i++], Units.DEG_PER_S, true);
653         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Y.name(), data[i++], Units.DEG_PER_S, true);
654         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Z.name(), data[i++], Units.DEG_PER_S, true);
655         xmlGenerator.exitSection();
656 
657         xmlGenerator.exitSection();
658 
659     }
660 
661     /** Write a Euler angles entry in XML.
662      * @param xmlGenerator generator to use for producing output
663      * @param formatVersion format version to use
664      * @param order Euler rotation order
665      * @param epoch of the entry
666      * @param data entry data
667      * @throws IOException if the output stream throws one while writing.
668      */
669     void writeEulerAngle(final XmlGenerator xmlGenerator, final double formatVersion,
670                          final RotationOrder order, final AbsoluteDate epoch, final String[] data)
671         throws IOException {
672 
673         // wrapping element
674         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngle.name());
675 
676         // data part
677         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
678         int i = 0;
679 
680         // angle part
681         if (formatVersion < 2.0) {
682             xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
683             writeLabeledEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
684             writeLabeledEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
685             writeLabeledEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
686             xmlGenerator.exitSection();
687         } else {
688             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(), data[i++], Unit.DEGREE, true);
689             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(), data[i++], Unit.DEGREE, true);
690             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(), data[i++], Unit.DEGREE, true);
691         }
692 
693         xmlGenerator.exitSection();
694 
695     }
696 
697     /** Write a Euler angles entry in XML.
698      * @param xmlGenerator generator to use for producing output
699      * @param formatVersion format version to use
700      * @param order Euler rotation order
701      * @param epoch of the entry
702      * @param data entry data
703      * @throws IOException if the output stream throws one while writing.
704      */
705     void writeEulerAngleDerivative(final XmlGenerator xmlGenerator, final double formatVersion,
706                                    final RotationOrder order, final AbsoluteDate epoch, final String[] data)
707         throws IOException {
708 
709         // wrapping element
710         xmlGenerator.enterSection(formatVersion < 2.0 ?
711                                   AttitudeEntryKey.eulerAngleRate.name() :
712                                   AttitudeEntryKey.eulerAngleDerivative.name());
713 
714         // data part
715         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
716         int i = 0;
717 
718         // angle part
719         if (formatVersion < 2.0) {
720             xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
721             writeLabeledEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
722             writeLabeledEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
723             writeLabeledEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
724             xmlGenerator.exitSection();
725             xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
726             writeLabeledEulerRate(xmlGenerator, 0, order.name(), data[i++]);
727             writeLabeledEulerRate(xmlGenerator, 1, order.name(), data[i++]);
728             writeLabeledEulerRate(xmlGenerator, 2, order.name(), data[i++]);
729             xmlGenerator.exitSection();
730         } else {
731             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(),     data[i++], Unit.DEGREE,     true);
732             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(),     data[i++], Unit.DEGREE,     true);
733             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(),     data[i++], Unit.DEGREE,     true);
734             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1_DOT.name(), data[i++], Units.DEG_PER_S, true);
735             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2_DOT.name(), data[i++], Units.DEG_PER_S, true);
736             xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3_DOT.name(), data[i++], Units.DEG_PER_S, true);
737         }
738 
739         xmlGenerator.exitSection();
740 
741     }
742 
743     /** Write a Euler angles/angular velocity entry in XML.
744      * @param xmlGenerator generator to use for producing output
745      * @param epoch of the entry
746      * @param data entry data
747      * @throws IOException if the output stream throws one while writing.
748      */
749     void writeEulerAngleAngularVelocity(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
750         throws IOException {
751 
752         // wrapping element
753         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngleAngVel.name());
754 
755         // data part
756         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
757         int i = 0;
758 
759         // angle part
760         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_1.name(), data[i++], Unit.DEGREE, true);
761         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_2.name(), data[i++], Unit.DEGREE, true);
762         xmlGenerator.writeEntry(AttitudeEntryKey.ANGLE_3.name(), data[i++], Unit.DEGREE, true);
763 
764         // angular velocity part
765         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_X.name(), data[i++], Units.DEG_PER_S, true);
766         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Y.name(), data[i++], Units.DEG_PER_S, true);
767         xmlGenerator.writeEntry(AttitudeEntryKey.ANGVEL_Z.name(), data[i++], Units.DEG_PER_S, true);
768 
769         xmlGenerator.exitSection();
770 
771     }
772 
773     /** Write a spin entry in XML.
774      * @param xmlGenerator generator to use for producing output
775      * @param epoch of the entry
776      * @param data entry data
777      * @throws IOException if the output stream throws one while writing.
778      */
779     void writeSpin(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
780         throws IOException {
781 
782         // wrapping element
783         xmlGenerator.enterSection(AttitudeEntryKey.spin.name());
784 
785         // data part
786         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
787         int i = 0;
788         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
789         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
790         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
791         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
792 
793         xmlGenerator.exitSection();
794 
795     }
796 
797     /** Write a spin/nutation entry in XML.
798      * @param xmlGenerator generator to use for producing output
799      * @param epoch of the entry
800      * @param data entry data
801      * @throws IOException if the output stream throws one while writing.
802      */
803     void writeSpinNutation(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
804         throws IOException {
805 
806         // wrapping element
807         xmlGenerator.enterSection(AttitudeEntryKey.spinNutation.name());
808 
809         // data part
810         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
811         int i = 0;
812         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
813         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
814         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
815         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
816         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION.name(),       data[i++], Unit.DEGREE,     true);
817         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PER.name(),   data[i++], Unit.SECOND,     true);
818         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PHASE.name(), data[i++], Unit.DEGREE,     true);
819 
820         xmlGenerator.exitSection();
821 
822     }
823 
824     /** Write a spin/nutation/momentum entry in XML.
825      * @param xmlGenerator generator to use for producing output
826      * @param epoch of the entry
827      * @param data entry data
828      * @throws IOException if the output stream throws one while writing.
829      */
830     void writeSpinNutationMomentum(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
831         throws IOException {
832 
833         // wrapping element
834         xmlGenerator.enterSection(AttitudeEntryKey.spinNutationMom.name());
835 
836         // data part
837         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, false, true);
838         int i = 0;
839         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
840         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
841         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
842         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
843         xmlGenerator.writeEntry(AttitudeEntryKey.MOMENTUM_ALPHA.name(), data[i++], Unit.DEGREE,     true);
844         xmlGenerator.writeEntry(AttitudeEntryKey.MOMENTUM_DELTA.name(), data[i++], Unit.DEGREE,     true);
845         xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_VEL.name(),   data[i++], Units.DEG_PER_S, true);
846 
847         xmlGenerator.exitSection();
848 
849     }
850 
851     /** Write an angle from an Euler sequence.
852      * @param xmlGenerator generator to use
853      * @param index angle index
854      * @param seq Euler sequence
855      * @param angle angle value
856      * @throws IOException if the output stream throws one while writing.
857      */
858     private void writeLabeledEulerAngle(final XmlGenerator xmlGenerator, final int index,
859                                         final String seq, final String angle)
860         throws IOException {
861         if (xmlGenerator.writeUnits(Unit.DEGREE)) {
862             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), angle,
863                                                    ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX,
864                                                    XmlGenerator.UNITS,
865                                                    xmlGenerator.siToCcsdsName(Unit.DEGREE.getName()));
866         } else {
867             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), angle,
868                                                   ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX);
869         }
870     }
871 
872     /** Write a rate from an Euler sequence.
873      * @param xmlGenerator generator to use
874      * @param index angle index
875      * @param seq Euler sequence
876      * @param rate rate value
877      * @throws IOException if the output stream throws one while writing.
878      */
879     private void writeLabeledEulerRate(final XmlGenerator xmlGenerator, final int index, final String seq, final String rate)
880         throws IOException {
881         if (xmlGenerator.writeUnits(Units.DEG_PER_S)) {
882             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), rate,
883                                                    RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX,
884                                                    XmlGenerator.UNITS,
885                                                    xmlGenerator.siToCcsdsName(Units.DEG_PER_S.getName()));
886         } else {
887             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), rate,
888                                                   RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX);
889         }
890     }
891 
892     /** Start of an attitude block.
893      * @param generator generator to use for producing output
894      * @throws IOException if the output stream throws one while writing.
895      */
896     void startAttitudeBlock(final Generator generator) throws IOException {
897         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
898                                KvnStructureKey.DATA.name() :
899                                XmlStructureKey.data.name());
900     }
901 
902     /** End of an attitude block.
903      * @param generator generator to use for producing output
904      * @throws IOException if the output stream throws one while writing.
905      */
906     void endAttitudeBlock(final Generator generator) throws IOException {
907         generator.exitSection();
908     }
909 
910 }