1   /* Copyright 2016 Applied Defense Solutions (ADS)
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    * ADS 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.odm.oem;
18  
19  import java.io.IOException;
20  import java.util.Date;
21  import java.util.List;
22  
23  import org.hipparchus.linear.RealMatrix;
24  import org.orekit.data.DataContext;
25  import org.orekit.errors.OrekitException;
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.odm.CartesianCovariance;
31  import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
32  import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
33  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
34  import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
35  import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
36  import org.orekit.files.ccsds.section.HeaderKey;
37  import org.orekit.files.ccsds.section.KvnStructureKey;
38  import org.orekit.files.ccsds.section.MetadataKey;
39  import org.orekit.files.ccsds.section.XmlStructureKey;
40  import org.orekit.files.ccsds.utils.ContextBinding;
41  import org.orekit.files.ccsds.utils.FileFormat;
42  import org.orekit.files.ccsds.utils.generation.AbstractMessageWriter;
43  import org.orekit.files.ccsds.utils.generation.Generator;
44  import org.orekit.time.AbsoluteDate;
45  import org.orekit.utils.AccurateFormatter;
46  import org.orekit.utils.CartesianDerivativesFilter;
47  import org.orekit.utils.IERSConventions;
48  import org.orekit.utils.TimeStampedPVCoordinates;
49  import org.orekit.utils.units.Unit;
50  
51  /**
52   * A writer for Orbit Ephemeris Message (OEM) files.
53   *
54   * <h2> Metadata </h2>
55   *
56   * <p> The OEM metadata used by this writer is described in the following table. Many
57   * metadata items are optional or have default values so they do not need to be specified.
58   * At a minimum the user must supply those values that are required and for which no
59   * default exits: {@link OdmMetadataKey#OBJECT_NAME}, and {@link CommonMetadataKey#OBJECT_ID}. The usage
60   * column in the table indicates where the metadata item is used, either in the OEM header
61   * or in the metadata section at the start of an OEM ephemeris segment.
62   *
63   * <table>
64   * <caption>OEM metadata</caption>
65   *     <thead>
66   *         <tr>
67   *             <th>Keyword</th>
68   *             <th>Usage</th>
69   *             <th>Obligatory</th>
70   *             <th>Default</th>
71   *             <th>Reference</th>
72   *    </thead>
73   *    <tbody>
74   *        <tr>
75   *            <td>{@code CCSDS_OEM_VERS}</td>
76   *            <td>Header</td>
77   *            <td>Yes</td>
78   *            <td>{@link Oem#FORMAT_VERSION_KEY}</td>
79   *            <td>Table 5-2</td>
80   *        <tr>
81   *            <td>{@code COMMENT}</td>
82   *            <td>Header</td>
83   *            <td>No</td>
84   *            <td></td>
85   *            <td>Table 5-2</td>
86   *        <tr>
87   *            <td>{@link HeaderKey#CREATION_DATE}</td>
88   *            <td>Header</td>
89   *            <td>Yes</td>
90   *            <td>{@link Date#Date() Now}</td>
91   *            <td>Table 5.2, 6.5.9</td>
92   *        <tr>
93   *            <td>{@link HeaderKey#ORIGINATOR}</td>
94   *            <td>Header</td>
95   *            <td>Yes</td>
96   *            <td>{@link #DEFAULT_ORIGINATOR}</td>
97   *            <td>Table 5-2</td>
98   *        <tr>
99   *            <td>{@link OdmMetadataKey#OBJECT_NAME}</td>
100  *            <td>Segment</td>
101  *            <td>Yes</td>
102  *            <td></td>
103  *            <td>Table 5-3</td>
104  *        <tr>
105  *            <td>{@link CommonMetadataKey#OBJECT_ID}</td>
106  *            <td>Segment</td>
107  *            <td>Yes</td>
108  *            <td></td>
109  *            <td>Table 5-3</td>
110  *        <tr>
111  *            <td>{@link CommonMetadataKey#CENTER_NAME}</td>
112  *            <td>Segment</td>
113  *            <td>Yes</td>
114  *            <td></td>
115  *            <td>Table 5-3</td>
116  *        <tr>
117  *            <td>{@link CommonMetadataKey#REF_FRAME}</td>
118  *            <td>Segment</td>
119  *            <td>Yes</td>
120  *            <td></td>
121  *            <td>Table 5-3, Annex A</td>
122  *        <tr>
123  *            <td>{@link CommonMetadataKey#REF_FRAME_EPOCH}</td>
124  *            <td>Segment</td>
125  *            <td>No</td>
126  *            <td></td>
127  *            <td>Table 5-3, 6.5.9</td>
128  *        <tr>
129  *            <td>{@link MetadataKey#TIME_SYSTEM}</td>
130  *            <td>Segment</td>
131  *            <td>Yes</td>
132  *            <td></td>
133  *        <tr>
134  *            <td>{@link OemMetadataKey#START_TIME}</td>
135  *            <td>Segment</td>
136  *            <td>Yes</td>
137  *            <td></td>
138  *            <td>Table 5-3, 6.5.9</td>
139  *        <tr>
140  *            <td>{@link OemMetadataKey#USEABLE_START_TIME}</td>
141  *            <td>Segment</td>
142  *            <td>No</td>
143  *            <td></td>
144  *            <td>Table 5-3, 6.5.9</td>
145  *        <tr>
146  *            <td>{@link OemMetadataKey#STOP_TIME}</td>
147  *            <td>Segment</td>
148  *            <td>Yes</td>
149  *            <td></td>
150  *            <td>Table 5-3, 6.5.9</td>
151  *        <tr>
152  *            <td>{@link OemMetadataKey#USEABLE_STOP_TIME}</td>
153  *            <td>Segment</td>
154  *            <td>No</td>
155  *            <td></td>
156  *            <td>Table 5-3, 6.5.9</td>
157  *        <tr>
158  *            <td>{@link OemMetadataKey#INTERPOLATION}</td>
159  *            <td>Segment</td>
160  *            <td>No</td>
161  *            <td></td>
162  *            <td>Table 5-3</td>
163  *        <tr>
164  *            <td>{@link OemMetadataKey#INTERPOLATION_DEGREE}</td>
165  *            <td>Segment</td>
166  *            <td>No</td>
167  *            <td></td>
168  *            <td>Table 5-3</td>
169  *    </tbody>
170  *</table>
171  *
172  * <p> The {@link MetadataKey#TIME_SYSTEM} must be constant for the whole file and is used
173  * to interpret all dates except {@link HeaderKey#CREATION_DATE} which is always in {@link
174  * TimeSystem#UTC UTC}. The guessing algorithm is not guaranteed to work so it is recommended
175  * to provide values for {@link CommonMetadataKey#CENTER_NAME} and {@link MetadataKey#TIME_SYSTEM}
176  * to avoid any bugs associated with incorrect guesses.
177  *
178  * <p> Standardized values for {@link MetadataKey#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
179  * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
180  * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
181  * Additionally ITRF followed by a four digit year may be used.
182  *
183  * @author Hank Grabowski
184  * @author Evan Ward
185  * @since 9.0
186  * @see <a href="https://public.ccsds.org/Pubs/502x0b2c1.pdf">CCSDS 502.0-B-2 Orbit Data
187  *      Messages</a>
188  * @see <a href="https://public.ccsds.org/Pubs/500x0g4.pdf">CCSDS 500.0-G-4 Navigation
189  *      Data Definitions and Conventions</a>
190  * @see StreamingOemWriter
191  */
192 public class OemWriter extends AbstractMessageWriter<OdmHeader, OemSegment, Oem> {
193 
194     /** Version number implemented. **/
195     public static final double CCSDS_OEM_VERS = 3.0;
196 
197     /** Default file name for error messages. */
198     public static final String DEFAULT_FILE_NAME = "<OEM output>";
199 
200     /** Padding width for aligning the '=' sign. */
201     public static final int KVN_PADDING_WIDTH = 20;
202 
203     /**
204      * Constructor used to create a new OEM writer configured with the necessary parameters
205      * to successfully fill in all required fields that aren't part of a standard object.
206      * <p>
207      * If the mandatory header entries are not present (or if header is null),
208      * built-in defaults will be used
209      * </p>
210      * <p>
211      * The writer is built from the complete header and partial metadata. The template
212      * metadata is used to initialize and independent local copy, that will be updated
213      * as new segments are written (with at least the segment start and stop will change,
214      * but some other parts may change too). The {@code template} argument itself is not
215      * changed.
216      * </p>
217      * <p>
218      * Calling this constructor directly is not recommended. Users should rather use
219      * {@link org.orekit.files.ccsds.ndm.WriterBuilder#buildOemWriter()
220      * writerBuilder.buildOemWriter()}.
221      * </p>
222      * @param conventions IERS Conventions
223      * @param dataContext used to retrieve frames, time scales, etc.
224      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
225      * @since 11.0
226      * @see #DEFAULT_FILE_NAME
227      */
228     public OemWriter(final IERSConventions conventions, final DataContext dataContext,
229                      final AbsoluteDate missionReferenceDate) {
230         super(Oem.ROOT, Oem.FORMAT_VERSION_KEY, CCSDS_OEM_VERS,
231               new ContextBinding(
232                   () -> conventions, () -> true, () -> dataContext,
233                   () -> ParsedUnitsBehavior.STRICT_COMPLIANCE,
234                   () -> missionReferenceDate, () -> TimeSystem.UTC, () -> 0.0, () -> 1.0));
235     }
236 
237     /** {@inheritDoc} */
238     @Override
239     protected void writeSegmentContent(final Generator generator, final double formatVersion,
240                                        final OemSegment segment)
241         throws IOException {
242 
243         final OemMetadata metadata = segment.getMetadata();
244         writeMetadata(generator, metadata);
245 
246         startData(generator);
247 
248         // write data comments
249         generator.writeComments(segment.getData().getComments());
250 
251         // Loop on orbit data
252         final CartesianDerivativesFilter filter = segment.getAvailableDerivatives();
253         if (filter == CartesianDerivativesFilter.USE_P) {
254             throw new OrekitException(OrekitMessages.MISSING_VELOCITY);
255         }
256         final boolean useAcceleration = filter.equals(CartesianDerivativesFilter.USE_PVA);
257         for (final TimeStampedPVCoordinates coordinates : segment.getCoordinates()) {
258             writeOrbitEphemerisLine(generator, metadata, coordinates, useAcceleration);
259         }
260 
261         // output covariance data
262         writeCovariances(generator, segment.getMetadata(), segment.getData().getCovarianceMatrices());
263 
264         endData(generator);
265 
266     }
267 
268     /** Write an ephemeris segment metadata.
269      * @param generator generator to use for producing output
270      * @param metadata metadata to write
271      * @throws IOException if the output stream throws one while writing.
272      */
273     void writeMetadata(final Generator generator, final OemMetadata metadata)
274         throws IOException {
275 
276         // add an empty line for presentation
277         generator.newLine();
278 
279         final ContextBinding oldContext = getContext();
280         setContext(new ContextBinding(oldContext::getConventions,
281                                       oldContext::isSimpleEOP,
282                                       oldContext::getDataContext,
283                                       oldContext::getParsedUnitsBehavior,
284                                       oldContext::getReferenceDate,
285                                       metadata::getTimeSystem,
286                                       oldContext::getClockCount,
287                                       oldContext::getClockRate));
288 
289         // Start metadata
290         generator.enterSection(generator.getFormat() == FileFormat.KVN ?
291                                KvnStructureKey.META.name() :
292                                XmlStructureKey.metadata.name());
293 
294         generator.writeComments(metadata.getComments());
295 
296         // objects
297         generator.writeEntry(OdmMetadataKey.OBJECT_NAME.name(),    metadata.getObjectName(),       null, true);
298         generator.writeEntry(CommonMetadataKey.OBJECT_ID.name(),   metadata.getObjectID(),         null, true);
299         generator.writeEntry(CommonMetadataKey.CENTER_NAME.name(), metadata.getCenter().getName(), null, false);
300 
301         // frames
302         generator.writeEntry(CommonMetadataKey.REF_FRAME.name(), metadata.getReferenceFrame().getName(), null, true);
303         if (metadata.getFrameEpoch() != null) {
304             generator.writeEntry(CommonMetadataKey.REF_FRAME_EPOCH.name(),
305                                  getTimeConverter(), metadata.getFrameEpoch(),
306                                  true, false);
307         }
308 
309         // time
310         generator.writeEntry(MetadataKey.TIME_SYSTEM.name(), metadata.getTimeSystem(), true);
311         generator.writeEntry(OemMetadataKey.START_TIME.name(), getTimeConverter(), metadata.getStartTime(), false, true);
312         if (metadata.getUseableStartTime() != null) {
313             generator.writeEntry(OemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false, false);
314         }
315         if (metadata.getUseableStopTime() != null) {
316             generator.writeEntry(OemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false, false);
317         }
318         generator.writeEntry(OemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), false, true);
319 
320         // interpolation
321         generator.writeEntry(OemMetadataKey.INTERPOLATION.name(), metadata.getInterpolationMethod(), false);
322         // treat degree < 0 as equivalent to null
323         if (metadata.getInterpolationDegree() >= 0) {
324             generator.writeEntry(OemMetadataKey.INTERPOLATION_DEGREE.name(),
325                     Integer.toString(metadata.getInterpolationDegree()),
326                     null, false);
327         }
328 
329         // Stop metadata
330         generator.exitSection();
331 
332         // add an empty line for presentation
333         generator.newLine();
334 
335     }
336 
337     /**
338      * Write a single orbit ephemeris line .
339      * @param generator generator to use for producing output
340      * @param metadata metadata to use for interpreting data
341      * @param coordinates orbit information for a given date
342      * @param useAcceleration is true, the acceleration data must be used
343      * @throws IOException if the output stream throws one while writing.
344      */
345     void writeOrbitEphemerisLine(final Generator generator, final OemMetadata metadata,
346                                  final TimeStampedPVCoordinates coordinates,
347                                  final boolean useAcceleration)
348         throws IOException {
349 
350         if (generator.getFormat() == FileFormat.KVN) {
351 
352             // Epoch
353             generator.writeRawData(generator.dateToString(getTimeConverter(), coordinates.getDate()));
354 
355             // Position data in km
356             generator.writeRawData(' ');
357             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getX()))));
358             generator.writeRawData(' ');
359             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getY()))));
360             generator.writeRawData(' ');
361             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getZ()))));
362 
363             // Velocity data in km/s
364             generator.writeRawData(' ');
365             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getX()))));
366             generator.writeRawData(' ');
367             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getY()))));
368             generator.writeRawData(' ');
369             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getZ()))));
370 
371             // Acceleration data in km/s²
372             if (useAcceleration) {
373                 generator.writeRawData(' ');
374                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getX()))));
375                 generator.writeRawData(' ');
376                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getY()))));
377                 generator.writeRawData(' ');
378                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getZ()))));
379             }
380 
381             // end the line
382             generator.newLine();
383         } else {
384             generator.enterSection(OemDataSubStructureKey.stateVector.name());
385 
386             // Epoch
387             generator.writeEntry(StateVectorKey.EPOCH.name(), getTimeConverter(), coordinates.getDate(), false, true);
388 
389             // Position data in km
390             generator.writeEntry(StateVectorKey.X.name(), coordinates.getPosition().getX(), Unit.KILOMETRE, true);
391             generator.writeEntry(StateVectorKey.Y.name(), coordinates.getPosition().getY(), Unit.KILOMETRE, true);
392             generator.writeEntry(StateVectorKey.Z.name(), coordinates.getPosition().getZ(), Unit.KILOMETRE, true);
393 
394             // Velocity data in km/s
395             generator.writeEntry(StateVectorKey.X_DOT.name(), coordinates.getVelocity().getX(), Units.KM_PER_S, true);
396             generator.writeEntry(StateVectorKey.Y_DOT.name(), coordinates.getVelocity().getY(), Units.KM_PER_S, true);
397             generator.writeEntry(StateVectorKey.Z_DOT.name(), coordinates.getVelocity().getZ(), Units.KM_PER_S, true);
398 
399             // Acceleration data in km/s²
400             if (useAcceleration) {
401                 generator.writeEntry(StateVectorKey.X_DDOT.name(), coordinates.getAcceleration().getX(), Units.KM_PER_S2, true);
402                 generator.writeEntry(StateVectorKey.Y_DDOT.name(), coordinates.getAcceleration().getY(), Units.KM_PER_S2, true);
403                 generator.writeEntry(StateVectorKey.Z_DDOT.name(), coordinates.getAcceleration().getZ(), Units.KM_PER_S2, true);
404             }
405 
406             generator.exitSection();
407 
408         }
409     }
410 
411     /**
412      * Write a covariance matrices.
413      * @param generator generator to use for producing output
414      * @param metadata metadata to use for interpreting data
415      * @param covariances covariances to write
416      * @throws IOException if the output stream throws one while writing.
417      */
418     void writeCovariances(final Generator generator, final OemMetadata metadata,
419                           final List<CartesianCovariance> covariances)
420         throws IOException {
421         if (covariances != null && !covariances.isEmpty()) {
422 
423             // enter the global covariance section in KVN
424             if (generator.getFormat() == FileFormat.KVN) {
425                 generator.enterSection(OemDataSubStructureKey.COVARIANCE.name());
426             }
427 
428             for (final CartesianCovariance covariance : covariances) {
429                 writeCovariance(generator, metadata, covariance);
430             }
431 
432             // exit the global covariance section in KVN
433             if (generator.getFormat() == FileFormat.KVN) {
434                 generator.exitSection();
435             }
436 
437         }
438     }
439 
440     /**
441      * Write a single covariance matrix.
442      * @param generator generator to use for producing output
443      * @param metadata metadata to use for interpreting data
444      * @param covariance covariance to write
445      * @throws IOException if the output stream throws one while writing.
446      */
447     private void writeCovariance(final Generator generator, final OemMetadata metadata,
448                                  final CartesianCovariance covariance)
449         throws IOException {
450 
451         // wrapper for a single matrix in XML
452         if (generator.getFormat() == FileFormat.XML) {
453             generator.enterSection(OemDataSubStructureKey.covarianceMatrix.name());
454         }
455 
456         // epoch
457         generator.writeEntry(CartesianCovarianceKey.EPOCH.name(), getTimeConverter(), covariance.getEpoch(), false, true);
458 
459         // reference frame
460         if (covariance.getReferenceFrame() != metadata.getReferenceFrame()) {
461             generator.writeEntry(CartesianCovarianceKey.COV_REF_FRAME.name(), covariance.getReferenceFrame().getName(), null, false);
462         }
463 
464         // matrix data
465         final RealMatrix m = covariance.getCovarianceMatrix();
466         if (generator.getFormat() == FileFormat.KVN) {
467             for (int i = 0; i < m.getRowDimension(); ++i) {
468 
469                 // write triangular matrix entries
470                 for (int j = 0; j <= i; ++j) {
471                     if (j > 0) {
472                         generator.writeRawData(' ');
473                     }
474                     generator.writeRawData(AccurateFormatter.format(Units.KM2.fromSI(m.getEntry(i, j))));
475                 }
476 
477                 // end the line
478                 generator.newLine();
479 
480             }
481         } else {
482             generator.writeEntry(CartesianCovarianceKey.CX_X.name(),         m.getEntry(0, 0), Units.KM2,        true);
483             generator.writeEntry(CartesianCovarianceKey.CY_X.name(),         m.getEntry(1, 0), Units.KM2,        true);
484             generator.writeEntry(CartesianCovarianceKey.CY_Y.name(),         m.getEntry(1, 1), Units.KM2,        true);
485             generator.writeEntry(CartesianCovarianceKey.CZ_X.name(),         m.getEntry(2, 0), Units.KM2,        true);
486             generator.writeEntry(CartesianCovarianceKey.CZ_Y.name(),         m.getEntry(2, 1), Units.KM2,        true);
487             generator.writeEntry(CartesianCovarianceKey.CZ_Z.name(),         m.getEntry(2, 2), Units.KM2,        true);
488             generator.writeEntry(CartesianCovarianceKey.CX_DOT_X.name(),     m.getEntry(3, 0), Units.KM2_PER_S,  true);
489             generator.writeEntry(CartesianCovarianceKey.CX_DOT_Y.name(),     m.getEntry(3, 1), Units.KM2_PER_S,  true);
490             generator.writeEntry(CartesianCovarianceKey.CX_DOT_Z.name(),     m.getEntry(3, 2), Units.KM2_PER_S,  true);
491             generator.writeEntry(CartesianCovarianceKey.CX_DOT_X_DOT.name(), m.getEntry(3, 3), Units.KM2_PER_S2, true);
492             generator.writeEntry(CartesianCovarianceKey.CY_DOT_X.name(),     m.getEntry(4, 0), Units.KM2_PER_S,  true);
493             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y.name(),     m.getEntry(4, 1), Units.KM2_PER_S,  true);
494             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Z.name(),     m.getEntry(4, 2), Units.KM2_PER_S,  true);
495             generator.writeEntry(CartesianCovarianceKey.CY_DOT_X_DOT.name(), m.getEntry(4, 3), Units.KM2_PER_S2, true);
496             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y_DOT.name(), m.getEntry(4, 4), Units.KM2_PER_S2, true);
497             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X.name(),     m.getEntry(5, 0), Units.KM2_PER_S,  true);
498             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y.name(),     m.getEntry(5, 1), Units.KM2_PER_S,  true);
499             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z.name(),     m.getEntry(5, 2), Units.KM2_PER_S,  true);
500             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X_DOT.name(), m.getEntry(5, 3), Units.KM2_PER_S2, true);
501             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y_DOT.name(), m.getEntry(5, 4), Units.KM2_PER_S2, true);
502             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z_DOT.name(), m.getEntry(5, 5), Units.KM2_PER_S2, true);
503         }
504 
505         // wrapper for a single matrix in XML
506         if (generator.getFormat() == FileFormat.XML) {
507             generator.exitSection();
508         }
509 
510     }
511 
512     /** Start of a data block.
513      * @param generator generator to use for producing output
514      * @throws IOException if the output stream throws one while writing.
515      */
516     void startData(final Generator generator) throws IOException {
517         if (generator.getFormat() == FileFormat.XML) {
518             generator.enterSection(XmlStructureKey.data.name());
519         }
520     }
521 
522     /** End of a data block.
523      * @param generator generator to use for producing output
524      * @throws IOException if the output stream throws one while writing.
525      */
526     void endData(final Generator generator) throws IOException {
527         if (generator.getFormat() == FileFormat.XML) {
528             generator.exitSection();
529         }
530     }
531 
532 }