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