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 }