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