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