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.OdmMetadataKey;
34  import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
35  import org.orekit.files.ccsds.section.Header;
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<Header, 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     public 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                                  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(), true);
312         if (metadata.getUseableStartTime() != null) {
313             generator.writeEntry(OemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false);
314         }
315         if (metadata.getUseableStopTime() != null) {
316             generator.writeEntry(OemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false);
317         }
318         generator.writeEntry(OemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), true);
319 
320         // interpolation
321         generator.writeEntry(OemMetadataKey.INTERPOLATION.name(), metadata.getInterpolationMethod(), false);
322         generator.writeEntry(OemMetadataKey.INTERPOLATION_DEGREE.name(),
323                              Integer.toString(metadata.getInterpolationDegree()),
324                              null, false);
325 
326         // Stop metadata
327         generator.exitSection();
328 
329         // add an empty line for presentation
330         generator.newLine();
331 
332     }
333 
334     /**
335      * Write a single orbit ephemeris line .
336      * @param generator generator to use for producing output
337      * @param metadata metadata to use for interpreting data
338      * @param coordinates orbit information for a given date
339      * @param useAcceleration is true, the acceleration data must be used
340      * @throws IOException if the output stream throws one while writing.
341      */
342     void writeOrbitEphemerisLine(final Generator generator, final OemMetadata metadata,
343                                  final TimeStampedPVCoordinates coordinates,
344                                  final boolean useAcceleration)
345         throws IOException {
346 
347         if (generator.getFormat() == FileFormat.KVN) {
348 
349             // Epoch
350             generator.writeRawData(generator.dateToString(getTimeConverter(), coordinates.getDate()));
351 
352             // Position data in km
353             generator.writeRawData(' ');
354             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getX()))));
355             generator.writeRawData(' ');
356             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getY()))));
357             generator.writeRawData(' ');
358             generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getZ()))));
359 
360             // Velocity data in km/s
361             generator.writeRawData(' ');
362             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getX()))));
363             generator.writeRawData(' ');
364             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getY()))));
365             generator.writeRawData(' ');
366             generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getZ()))));
367 
368             // Acceleration data in km/s²
369             if (useAcceleration) {
370                 generator.writeRawData(' ');
371                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getX()))));
372                 generator.writeRawData(' ');
373                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getY()))));
374                 generator.writeRawData(' ');
375                 generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getZ()))));
376             }
377 
378             // end the line
379             generator.newLine();
380         } else {
381             generator.enterSection(OemDataSubStructureKey.stateVector.name());
382 
383             // Epoch
384             generator.writeEntry(StateVectorKey.EPOCH.name(), getTimeConverter(), coordinates.getDate(), true);
385 
386             // Position data in km
387             generator.writeEntry(StateVectorKey.X.name(), coordinates.getPosition().getX(), Unit.KILOMETRE, true);
388             generator.writeEntry(StateVectorKey.Y.name(), coordinates.getPosition().getY(), Unit.KILOMETRE, true);
389             generator.writeEntry(StateVectorKey.Z.name(), coordinates.getPosition().getZ(), Unit.KILOMETRE, true);
390 
391             // Velocity data in km/s
392             generator.writeEntry(StateVectorKey.X_DOT.name(), coordinates.getVelocity().getX(), Units.KM_PER_S, true);
393             generator.writeEntry(StateVectorKey.Y_DOT.name(), coordinates.getVelocity().getY(), Units.KM_PER_S, true);
394             generator.writeEntry(StateVectorKey.Z_DOT.name(), coordinates.getVelocity().getZ(), Units.KM_PER_S, true);
395 
396             // Acceleration data in km/s²
397             if (useAcceleration) {
398                 generator.writeEntry(StateVectorKey.X_DDOT.name(), coordinates.getAcceleration().getX(), Units.KM_PER_S2, true);
399                 generator.writeEntry(StateVectorKey.Y_DDOT.name(), coordinates.getAcceleration().getY(), Units.KM_PER_S2, true);
400                 generator.writeEntry(StateVectorKey.Z_DDOT.name(), coordinates.getAcceleration().getZ(), Units.KM_PER_S2, true);
401             }
402 
403             generator.exitSection();
404 
405         }
406     }
407 
408     /**
409      * Write a covariance matrices.
410      * @param generator generator to use for producing output
411      * @param metadata metadata to use for interpreting data
412      * @param covariances covariances to write
413      * @throws IOException if the output stream throws one while writing.
414      */
415     void writeCovariances(final Generator generator, final OemMetadata metadata,
416                           final List<CartesianCovariance> covariances)
417         throws IOException {
418         if (covariances != null && !covariances.isEmpty()) {
419 
420             // enter the global covariance section in KVN
421             if (generator.getFormat() == FileFormat.KVN) {
422                 generator.enterSection(OemDataSubStructureKey.COVARIANCE.name());
423             }
424 
425             for (final CartesianCovariance covariance : covariances) {
426                 writeCovariance(generator, metadata, covariance);
427             }
428 
429             // exit the global covariance section in KVN
430             if (generator.getFormat() == FileFormat.KVN) {
431                 generator.exitSection();
432             }
433 
434         }
435     }
436 
437     /**
438      * Write a single covariance matrix.
439      * @param generator generator to use for producing output
440      * @param metadata metadata to use for interpreting data
441      * @param covariance covariance to write
442      * @throws IOException if the output stream throws one while writing.
443      */
444     private void writeCovariance(final Generator generator, final OemMetadata metadata,
445                                  final CartesianCovariance covariance)
446         throws IOException {
447 
448         // wrapper for a single matrix in XML
449         if (generator.getFormat() == FileFormat.XML) {
450             generator.enterSection(OemDataSubStructureKey.covarianceMatrix.name());
451         }
452 
453         // epoch
454         generator.writeEntry(CartesianCovarianceKey.EPOCH.name(), getTimeConverter(), covariance.getEpoch(), true);
455 
456         // reference frame
457         if (covariance.getReferenceFrame() != metadata.getReferenceFrame()) {
458             generator.writeEntry(CartesianCovarianceKey.COV_REF_FRAME.name(), covariance.getReferenceFrame().getName(), null, false);
459         }
460 
461         // matrix data
462         final RealMatrix m = covariance.getCovarianceMatrix();
463         if (generator.getFormat() == FileFormat.KVN) {
464             for (int i = 0; i < m.getRowDimension(); ++i) {
465 
466                 // write triangular matrix entries
467                 for (int j = 0; j <= i; ++j) {
468                     if (j > 0) {
469                         generator.writeRawData(' ');
470                     }
471                     generator.writeRawData(AccurateFormatter.format(Units.KM2.fromSI(m.getEntry(i, j))));
472                 }
473 
474                 // end the line
475                 generator.newLine();
476 
477             }
478         } else {
479             generator.writeEntry(CartesianCovarianceKey.CX_X.name(),         m.getEntry(0, 0), Units.KM2,        true);
480             generator.writeEntry(CartesianCovarianceKey.CY_X.name(),         m.getEntry(1, 0), Units.KM2,        true);
481             generator.writeEntry(CartesianCovarianceKey.CY_Y.name(),         m.getEntry(1, 1), Units.KM2,        true);
482             generator.writeEntry(CartesianCovarianceKey.CZ_X.name(),         m.getEntry(2, 0), Units.KM2,        true);
483             generator.writeEntry(CartesianCovarianceKey.CZ_Y.name(),         m.getEntry(2, 1), Units.KM2,        true);
484             generator.writeEntry(CartesianCovarianceKey.CZ_Z.name(),         m.getEntry(2, 2), Units.KM2,        true);
485             generator.writeEntry(CartesianCovarianceKey.CX_DOT_X.name(),     m.getEntry(3, 0), Units.KM2_PER_S,  true);
486             generator.writeEntry(CartesianCovarianceKey.CX_DOT_Y.name(),     m.getEntry(3, 1), Units.KM2_PER_S,  true);
487             generator.writeEntry(CartesianCovarianceKey.CX_DOT_Z.name(),     m.getEntry(3, 2), Units.KM2_PER_S,  true);
488             generator.writeEntry(CartesianCovarianceKey.CX_DOT_X_DOT.name(), m.getEntry(3, 3), Units.KM2_PER_S2, true);
489             generator.writeEntry(CartesianCovarianceKey.CY_DOT_X.name(),     m.getEntry(4, 0), Units.KM2_PER_S,  true);
490             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y.name(),     m.getEntry(4, 1), Units.KM2_PER_S,  true);
491             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Z.name(),     m.getEntry(4, 2), Units.KM2_PER_S,  true);
492             generator.writeEntry(CartesianCovarianceKey.CY_DOT_X_DOT.name(), m.getEntry(4, 3), Units.KM2_PER_S2, true);
493             generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y_DOT.name(), m.getEntry(4, 4), Units.KM2_PER_S2, true);
494             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X.name(),     m.getEntry(5, 0), Units.KM2_PER_S,  true);
495             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y.name(),     m.getEntry(5, 1), Units.KM2_PER_S,  true);
496             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z.name(),     m.getEntry(5, 2), Units.KM2_PER_S,  true);
497             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X_DOT.name(), m.getEntry(5, 3), Units.KM2_PER_S2, true);
498             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y_DOT.name(), m.getEntry(5, 4), Units.KM2_PER_S2, true);
499             generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z_DOT.name(), m.getEntry(5, 5), Units.KM2_PER_S2, true);
500         }
501 
502         // wrapper for a single matrix in XML
503         if (generator.getFormat() == FileFormat.XML) {
504             generator.exitSection();
505         }
506 
507     }
508 
509     /** Start of a data block.
510      * @param generator generator to use for producing output
511      * @throws IOException if the output stream throws one while writing.
512      */
513     void startData(final Generator generator) throws IOException {
514         if (generator.getFormat() == FileFormat.XML) {
515             generator.enterSection(XmlStructureKey.data.name());
516         }
517     }
518 
519     /** End of a data block.
520      * @param generator generator to use for producing output
521      * @throws IOException if the output stream throws one while writing.
522      */
523     void endData(final Generator generator) throws IOException {
524         if (generator.getFormat() == FileFormat.XML) {
525             generator.exitSection();
526         }
527     }
528 
529 }