1   /* Copyright 2002-2021 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.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 AdmMetadataKey#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 AdmMetadataKey#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_RATE QUATERNION/RATE}</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<Header, AemSegment, Aem> {
218 
219     /** Version number implemented. **/
220     public static final double CCSDS_AEM_VERS = 1.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 first 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      * <p>
262      * If the mandatory header entries are not present (or if header is null),
263      * built-in defaults will be used
264      * </p>
265      * <p>
266      * The writer is built from the complete header and partial metadata. The template
267      * metadata is used to initialize and independent local copy, that will be updated
268      * as new segments are written (with at least the segment start and stop will change,
269      * but some other parts may change too). The {@code template} argument itself is not
270      * changed.
271      * </p>
272      * @param conventions IERS Conventions
273      * @param dataContext used to retrieve frames, time scales, etc.
274      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
275      * @since 11.0
276      */
277     public AemWriter(final IERSConventions conventions, final DataContext dataContext,
278                      final AbsoluteDate missionReferenceDate) {
279         super(Aem.ROOT, Aem.FORMAT_VERSION_KEY, CCSDS_AEM_VERS,
280               new ContextBinding(
281                   () -> conventions,
282                   () -> true, () -> dataContext, () -> ParsedUnitsBehavior.STRICT_COMPLIANCE,
283                   () -> missionReferenceDate, () -> TimeSystem.UTC,
284                   () -> 0.0, () -> 1.0));
285     }
286 
287     /** {@inheritDoc} */
288     @Override
289     public void writeSegmentContent(final Generator generator, final double formatVersion,
290                                     final AemSegment segment)
291         throws IOException {
292 
293         final AemMetadata metadata = segment.getMetadata();
294         writeMetadata(generator, metadata);
295 
296         // Loop on attitude data
297         startAttitudeBlock(generator);
298         generator.writeComments(((AemSegment) segment).getData().getComments());
299         for (final TimeStampedAngularCoordinates coordinates : segment.getAngularCoordinates()) {
300             writeAttitudeEphemerisLine(generator, metadata, coordinates);
301         }
302         endAttitudeBlock(generator);
303 
304     }
305 
306     /** Write an ephemeris segment metadata.
307      * @param generator generator to use for producing output
308      * @param metadata metadata to write
309      * @throws IOException if the output stream throws one while writing.
310      */
311     void writeMetadata(final Generator generator, final AemMetadata metadata) throws IOException {
312 
313         final ContextBinding oldContext = getContext();
314         setContext(new ContextBinding(oldContext::getConventions,
315                                       oldContext::isSimpleEOP,
316                                       oldContext::getDataContext,
317                                       oldContext::getParsedUnitsBehavior,
318                                       oldContext::getReferenceDate,
319                                       metadata::getTimeSystem,
320                                       oldContext::getClockCount,
321                                       oldContext::getClockRate));
322 
323         // Start metadata
324         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
325                                KvnStructureKey.META.name() :
326                                XmlStructureKey.metadata.name());
327 
328         generator.writeComments(metadata.getComments());
329 
330         // objects
331         generator.writeEntry(AdmMetadataKey.OBJECT_NAME.name(), metadata.getObjectName(),       null, true);
332         generator.writeEntry(AdmMetadataKey.OBJECT_ID.name(),   metadata.getObjectID(),         null, true);
333         if (metadata.getCenter() != null) {
334             generator.writeEntry(AdmMetadataKey.CENTER_NAME.name(), metadata.getCenter().getName(), null, false);
335         }
336 
337         // frames
338         generator.writeEntry(AemMetadataKey.REF_FRAME_A.name(),  metadata.getEndpoints().getFrameA().getName(),     null, true);
339         generator.writeEntry(AemMetadataKey.REF_FRAME_B.name(),  metadata.getEndpoints().getFrameB().getName(),     null, true);
340         generator.writeEntry(AemMetadataKey.ATTITUDE_DIR.name(), metadata.getEndpoints().isA2b() ? A_TO_B : B_TO_A, null, true);
341 
342         // time
343         generator.writeEntry(MetadataKey.TIME_SYSTEM.name(), metadata.getTimeSystem(), true);
344         generator.writeEntry(AemMetadataKey.START_TIME.name(), getTimeConverter(), metadata.getStartTime(), true);
345         if (metadata.getUseableStartTime() != null) {
346             generator.writeEntry(AemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false);
347         }
348         if (metadata.getUseableStopTime() != null) {
349             generator.writeEntry(AemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false);
350         }
351         generator.writeEntry(AemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), true);
352 
353         // types
354         final AttitudeType attitudeType = metadata.getAttitudeType();
355         generator.writeEntry(AemMetadataKey.ATTITUDE_TYPE.name(), attitudeType.toString(), null, true);
356         if (attitudeType == AttitudeType.QUATERNION ||
357             attitudeType == AttitudeType.QUATERNION_DERIVATIVE ||
358             attitudeType == AttitudeType.QUATERNION_RATE) {
359             generator.writeEntry(AemMetadataKey.QUATERNION_TYPE.name(), metadata.isFirst() ? FIRST : LAST, null, false);
360         }
361 
362         if (attitudeType == AttitudeType.QUATERNION_RATE ||
363             attitudeType == AttitudeType.EULER_ANGLE ||
364             attitudeType == AttitudeType.EULER_ANGLE_RATE) {
365             if (metadata.getEulerRotSeq() == null) {
366                 // the keyword *will* be missing because we cannot set it
367                 throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD,
368                                           AemMetadataKey.EULER_ROT_SEQ.name(), generator.getOutputName());
369             }
370             generator.writeEntry(AemMetadataKey.EULER_ROT_SEQ.name(),
371                                  metadata.getEulerRotSeq().name().replace('X', '1').replace('Y', '2').replace('Z', '3'),
372                                  null, false);
373         }
374 
375         if (attitudeType == AttitudeType.QUATERNION_RATE ||
376             attitudeType == AttitudeType.EULER_ANGLE_RATE) {
377             generator.writeEntry(AemMetadataKey.RATE_FRAME.name(),
378                                  metadata.rateFrameIsA() ? REF_FRAME_A : REF_FRAME_B,
379                                                          null, false);
380         }
381 
382         // interpolation
383         generator.writeEntry(AemMetadataKey.INTERPOLATION_METHOD.name(),
384                              metadata.getInterpolationMethod(),
385                              null, false);
386         generator.writeEntry(AemMetadataKey.INTERPOLATION_DEGREE.name(),
387                              Integer.toString(metadata.getInterpolationDegree()),
388                              null, false);
389 
390         // Stop metadata
391         generator.exitSection();
392 
393     }
394 
395     /**
396      * Write a single attitude ephemeris line according to section 4.2.4 and Table 4-4.
397      * @param generator generator to use for producing output
398      * @param metadata metadata to use for interpreting data
399      * @param attitude the attitude information for a given date
400      * @throws IOException if the output stream throws one while writing.
401      */
402     void writeAttitudeEphemerisLine(final Generator generator, final AemMetadata metadata,
403                                     final TimeStampedAngularCoordinates attitude)
404         throws IOException {
405 
406         // Attitude data in CCSDS units
407         final String[] data = metadata.getAttitudeType().createDataFields(metadata.isFirst(),
408                                                                           metadata.getEndpoints().isExternal2SpacecraftBody(),
409                                                                           metadata.getEulerRotSeq(),
410                                                                           metadata.isSpacecraftBodyRate(),
411                                                                           attitude);
412 
413         if (generator.getFormat() == FileFormat.KVN) {
414 
415             // epoch
416             generator.writeRawData(generator.dateToString(getTimeConverter(), attitude.getDate()));
417 
418             // data
419             final int      size = data.length;
420             for (int index = 0; index < size; index++) {
421                 generator.writeRawData(' ');
422                 generator.writeRawData(data[index]);
423             }
424 
425             // end the line
426             generator.newLine();
427 
428         } else {
429             final XmlGenerator xmlGenerator = (XmlGenerator) generator;
430             xmlGenerator.enterSection(XmlSubStructureKey.attitudeState.name());
431             switch (metadata.getAttitudeType()) {
432                 case QUATERNION :
433                     writeQuaternion(xmlGenerator, metadata.isFirst(), attitude.getDate(), data);
434                     break;
435                 case QUATERNION_DERIVATIVE :
436                     writeQuaternionDerivative(xmlGenerator, metadata.isFirst(), attitude.getDate(), data);
437                     break;
438                 case QUATERNION_RATE :
439                     writeQuaternionRate(xmlGenerator, metadata.isFirst(), metadata.getEulerRotSeq(), attitude.getDate(), data);
440                     break;
441                 case EULER_ANGLE :
442                     writeEulerAngle(xmlGenerator, metadata.getEulerRotSeq(), attitude.getDate(), data);
443                     break;
444                 case EULER_ANGLE_RATE :
445                     writeEulerAngleRate(xmlGenerator, metadata.getEulerRotSeq(), attitude.getDate(), data);
446                     break;
447                 case SPIN :
448                     writeSpin(xmlGenerator, attitude.getDate(), data);
449                     break;
450 //                case SPIN_NUTATION :
451 //                    writeSpinNutation(xmlGenerator, attitude.getDate(), data);
452 //                    break;
453                 default :
454                     // this should never happen
455                     throw new OrekitInternalError(null);
456             }
457             generator.exitSection();
458         }
459 
460     }
461 
462     /** Write a quaternion entry in XML.
463      * @param xmlGenerator generator to use for producing output
464      * @param first flag for scalar component to appear first
465      * @param epoch of the entry
466      * @param data entry data
467      * @throws IOException if the output stream throws one while writing.
468      */
469     void writeQuaternion(final XmlGenerator xmlGenerator, final boolean first, final AbsoluteDate epoch, final String[] data)
470         throws IOException {
471 
472         // wrapping element
473         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
474 
475         // data part
476         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
477 
478         // quaternion part
479         int i = 0;
480         if (first) {
481             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
482         }
483         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, false);
484         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, false);
485         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, false);
486         if (!first) {
487             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, false);
488         }
489 
490         xmlGenerator.exitSection();
491 
492     }
493 
494     /** Write a quaternion/derivative entry in XML.
495      * @param xmlGenerator generator to use for producing output
496      * @param first flag for scalar component to appear first
497      * @param epoch of the entry
498      * @param data entry data
499      * @throws IOException if the output stream throws one while writing.
500      */
501     void writeQuaternionDerivative(final XmlGenerator xmlGenerator, final boolean first, final AbsoluteDate epoch, final String[] data)
502         throws IOException {
503 
504         // wrapping element
505         xmlGenerator.enterSection(AttitudeEntryKey.quaternionDerivative.name());
506 
507         // data part
508         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
509         int i = 0;
510 
511         // quaternion part
512         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
513         if (first) {
514             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
515         }
516         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
517         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
518         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
519         if (!first) {
520             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
521         }
522         xmlGenerator.exitSection();
523 
524         // derivative part
525         xmlGenerator.enterSection(AttitudeEntryKey.quaternionRate.name());
526         if (first) {
527             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
528         }
529         xmlGenerator.writeEntry(AttitudeEntryKey.Q1_DOT.name(), data[i++], Units.ONE_PER_S, true);
530         xmlGenerator.writeEntry(AttitudeEntryKey.Q2_DOT.name(), data[i++], Units.ONE_PER_S, true);
531         xmlGenerator.writeEntry(AttitudeEntryKey.Q3_DOT.name(), data[i++], Units.ONE_PER_S, true);
532         if (!first) {
533             xmlGenerator.writeEntry(AttitudeEntryKey.QC_DOT.name(), data[i++], Units.ONE_PER_S, true);
534         }
535         xmlGenerator.exitSection();
536 
537         xmlGenerator.exitSection();
538 
539     }
540 
541     /** Write a quaternion/rate entry in XML.
542      * @param xmlGenerator generator to use for producing output
543      * @param first flag for scalar component to appear first
544      * @param order Euler rotation order
545      * @param epoch of the entry
546      * @param data entry data
547      * @throws IOException if the output stream throws one while writing.
548      */
549     void writeQuaternionRate(final XmlGenerator xmlGenerator, final boolean first, final RotationOrder order,
550                              final AbsoluteDate epoch, final String[] data)
551         throws IOException {
552 
553         // wrapping element
554         xmlGenerator.enterSection(AttitudeEntryKey.quaternionEulerRate.name());
555 
556         // data part
557         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
558         int i = 0;
559 
560         // quaternion part
561         xmlGenerator.enterSection(AttitudeEntryKey.quaternion.name());
562         if (first) {
563             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
564         }
565         xmlGenerator.writeEntry(AttitudeEntryKey.Q1.name(), data[i++], Unit.ONE, true);
566         xmlGenerator.writeEntry(AttitudeEntryKey.Q2.name(), data[i++], Unit.ONE, true);
567         xmlGenerator.writeEntry(AttitudeEntryKey.Q3.name(), data[i++], Unit.ONE, true);
568         if (!first) {
569             xmlGenerator.writeEntry(AttitudeEntryKey.QC.name(), data[i++], Unit.ONE, true);
570         }
571         xmlGenerator.exitSection();
572 
573         // derivative part
574         xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
575         writeEulerRate(xmlGenerator, 0, order.name(), data[i++]);
576         writeEulerRate(xmlGenerator, 1, order.name(), data[i++]);
577         writeEulerRate(xmlGenerator, 2, order.name(), data[i++]);
578         xmlGenerator.exitSection();
579 
580         xmlGenerator.exitSection();
581 
582     }
583 
584     /** Write a Euler angles entry in XML.
585      * @param xmlGenerator generator to use for producing output
586      * @param order Euler rotation order
587      * @param epoch of the entry
588      * @param data entry data
589      * @throws IOException if the output stream throws one while writing.
590      */
591     void writeEulerAngle(final XmlGenerator xmlGenerator, final RotationOrder order,
592                          final AbsoluteDate epoch, final String[] data)
593         throws IOException {
594 
595         // wrapping element
596         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngle.name());
597 
598         // data part
599         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
600         int i = 0;
601 
602         // angle part
603         xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
604         writeEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
605         writeEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
606         writeEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
607         xmlGenerator.exitSection();
608 
609         xmlGenerator.exitSection();
610 
611     }
612 
613     /** Write a Euler angles/rates entry in XML.
614      * @param xmlGenerator generator to use for producing output
615      * @param order Euler rotation order
616      * @param epoch of the entry
617      * @param data entry data
618      * @throws IOException if the output stream throws one while writing.
619      */
620     void writeEulerAngleRate(final XmlGenerator xmlGenerator, final RotationOrder order,
621                              final AbsoluteDate epoch, final String[] data)
622         throws IOException {
623 
624         // wrapping element
625         xmlGenerator.enterSection(AttitudeEntryKey.eulerAngle.name());
626 
627         // data part
628         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
629         int i = 0;
630 
631         // angle part
632         xmlGenerator.enterSection(AttitudeEntryKey.rotationAngles.name());
633         writeEulerAngle(xmlGenerator, 0, order.name(), data[i++]);
634         writeEulerAngle(xmlGenerator, 1, order.name(), data[i++]);
635         writeEulerAngle(xmlGenerator, 2, order.name(), data[i++]);
636         xmlGenerator.exitSection();
637 
638         // rates part
639         xmlGenerator.enterSection(AttitudeEntryKey.rotationRates.name());
640         writeEulerRate(xmlGenerator, 0, order.name(), data[i++]);
641         writeEulerRate(xmlGenerator, 1, order.name(), data[i++]);
642         writeEulerRate(xmlGenerator, 2, order.name(), data[i++]);
643         xmlGenerator.exitSection();
644 
645         xmlGenerator.exitSection();
646 
647     }
648 
649     /** Write a spin entry in XML.
650      * @param xmlGenerator generator to use for producing output
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 writeSpin(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
656         throws IOException {
657 
658         // wrapping element
659         xmlGenerator.enterSection(AttitudeEntryKey.spin.name());
660 
661         // data part
662         xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
663         int i = 0;
664         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
665         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
666         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
667         xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
668 
669         xmlGenerator.exitSection();
670 
671     }
672 
673 //    /** Write a spin/nutation entry in XML.
674 //     * @param xmlGenerator generator to use for producing output
675 //     * @param epoch of the entry
676 //     * @param data entry data
677 //     * @throws IOException if the output stream throws one while writing.
678 //     */
679 //    void writeSpinNutation(final XmlGenerator xmlGenerator, final AbsoluteDate epoch, final String[] data)
680 //        throws IOException {
681 //
682 //        // wrapping element
683 //        xmlGenerator.enterSection(AttitudeEntryKey.spin.name());
684 //
685 //        // data part
686 //        xmlGenerator.writeEntry(AttitudeEntryKey.EPOCH.name(), getTimeConverter(), epoch, true);
687 //        int i = 0;
688 //        xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ALPHA.name(),     data[i++], Unit.DEGREE,     true);
689 //        xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_DELTA.name(),     data[i++], Unit.DEGREE,     true);
690 //        xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE.name(),     data[i++], Unit.DEGREE,     true);
691 //        xmlGenerator.writeEntry(AttitudeEntryKey.SPIN_ANGLE_VEL.name(), data[i++], Units.DEG_PER_S, true);
692 //        xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION.name(),       data[i++], Unit.DEGREE,     true);
693 //        xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PER.name(),   data[i++], Unit.SECOND,     true);
694 //        xmlGenerator.writeEntry(AttitudeEntryKey.NUTATION_PHASE.name(), data[i++], Unit.DEGREE,     true);
695 //
696 //        xmlGenerator.exitSection();
697 //
698 //    }
699 
700     /** Write an angle from an Euler sequence.
701      * @param xmlGenerator generator to use
702      * @param index angle index
703      * @param seq Euler sequence
704      * @param angle angle value
705      * @throws IOException if the output stream throws one while writing.
706      */
707     private void writeEulerAngle(final XmlGenerator xmlGenerator, final int index, final String seq, final String angle)
708         throws IOException {
709         if (xmlGenerator.writeUnits(Unit.DEGREE)) {
710             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), angle,
711                                                    ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX,
712                                                    XmlGenerator.UNITS,
713                                                    xmlGenerator.siToCcsdsName(Unit.DEGREE.getName()));
714         } else {
715             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), angle,
716                                                   ANGLE_ATTRIBUTE, seq.charAt(index) + ANGLE_SUFFIX);
717         }
718     }
719 
720     /** Write a rate from an Euler sequence.
721      * @param xmlGenerator generator to use
722      * @param index angle index
723      * @param seq Euler sequence
724      * @param rate rate value
725      * @throws IOException if the output stream throws one while writing.
726      */
727     private void writeEulerRate(final XmlGenerator xmlGenerator, final int index, final String seq, final String rate)
728         throws IOException {
729         if (xmlGenerator.writeUnits(Units.DEG_PER_S)) {
730             xmlGenerator.writeTwoAttributesElement(ROTATION + (index + 1), rate,
731                                                    RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX,
732                                                    XmlGenerator.UNITS,
733                                                    xmlGenerator.siToCcsdsName(Units.DEG_PER_S.getName()));
734         } else {
735             xmlGenerator.writeOneAttributeElement(ROTATION + (index + 1), rate,
736                                                   RATE_ATTRIBUTE, seq.charAt(index) + RATE_SUFFIX);
737         }
738     }
739 
740     /** Start of an attitude block.
741      * @param generator generator to use for producing output
742      * @throws IOException if the output stream throws one while writing.
743      */
744     void startAttitudeBlock(final Generator generator) throws IOException {
745         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
746                                KvnStructureKey.DATA.name() :
747                                XmlStructureKey.data.name());
748     }
749 
750     /** End of an attitude block.
751      * @param generator generator to use for producing output
752      * @throws IOException if the output stream throws one while writing.
753      */
754     void endAttitudeBlock(final Generator generator) throws IOException {
755         generator.exitSection();
756     }
757 
758 }