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