OemWriter.java

/* Copyright 2016 Applied Defense Solutions (ADS)
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * ADS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.files.ccsds.ndm.odm.oem;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.hipparchus.linear.RealMatrix;
import org.orekit.data.DataContext;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.definitions.TimeSystem;
import org.orekit.files.ccsds.definitions.Units;
import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
import org.orekit.files.ccsds.section.Header;
import org.orekit.files.ccsds.section.HeaderKey;
import org.orekit.files.ccsds.section.KvnStructureKey;
import org.orekit.files.ccsds.section.MetadataKey;
import org.orekit.files.ccsds.section.XmlStructureKey;
import org.orekit.files.ccsds.utils.ContextBinding;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.generation.AbstractMessageWriter;
import org.orekit.files.ccsds.utils.generation.Generator;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.AccurateFormatter;
import org.orekit.utils.CartesianDerivativesFilter;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.TimeStampedPVCoordinates;
import org.orekit.utils.units.Unit;

/**
 * A writer for Orbit Ephemeris Message (OEM) files.
 *
 * <h2> Metadata </h2>
 *
 * <p> The OEM metadata used by this writer is described in the following table. Many
 * metadata items are optional or have default values so they do not need to be specified.
 * At a minimum the user must supply those values that are required and for which no
 * default exits: {@link OdmMetadataKey#OBJECT_NAME}, and {@link CommonMetadataKey#OBJECT_ID}. The usage
 * column in the table indicates where the metadata item is used, either in the OEM header
 * or in the metadata section at the start of an OEM ephemeris segment.
 *
 * <table>
 * <caption>OEM metadata</caption>
 *     <thead>
 *         <tr>
 *             <th>Keyword</th>
 *             <th>Usage</th>
 *             <th>Obligatory</th>
 *             <th>Default</th>
 *             <th>Reference</th>
 *    </thead>
 *    <tbody>
 *        <tr>
 *            <td>{@code CCSDS_OEM_VERS}</td>
 *            <td>Header</td>
 *            <td>Yes</td>
 *            <td>{@link Oem#FORMAT_VERSION_KEY}</td>
 *            <td>Table 5-2</td>
 *        <tr>
 *            <td>{@code COMMENT}</td>
 *            <td>Header</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-2</td>
 *        <tr>
 *            <td>{@link HeaderKey#CREATION_DATE}</td>
 *            <td>Header</td>
 *            <td>Yes</td>
 *            <td>{@link Date#Date() Now}</td>
 *            <td>Table 5.2, 6.5.9</td>
 *        <tr>
 *            <td>{@link HeaderKey#ORIGINATOR}</td>
 *            <td>Header</td>
 *            <td>Yes</td>
 *            <td>{@link #DEFAULT_ORIGINATOR}</td>
 *            <td>Table 5-2</td>
 *        <tr>
 *            <td>{@link OdmMetadataKey#OBJECT_NAME}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3</td>
 *        <tr>
 *            <td>{@link CommonMetadataKey#OBJECT_ID}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3</td>
 *        <tr>
 *            <td>{@link CommonMetadataKey#CENTER_NAME}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3</td>
 *        <tr>
 *            <td>{@link CommonMetadataKey#REF_FRAME}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3, Annex A</td>
 *        <tr>
 *            <td>{@link CommonMetadataKey#REF_FRAME_EPOCH}</td>
 *            <td>Segment</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-3, 6.5.9</td>
 *        <tr>
 *            <td>{@link MetadataKey#TIME_SYSTEM}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *        <tr>
 *            <td>{@link OemMetadataKey#START_TIME}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3, 6.5.9</td>
 *        <tr>
 *            <td>{@link OemMetadataKey#USEABLE_START_TIME}</td>
 *            <td>Segment</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-3, 6.5.9</td>
 *        <tr>
 *            <td>{@link OemMetadataKey#STOP_TIME}</td>
 *            <td>Segment</td>
 *            <td>Yes</td>
 *            <td></td>
 *            <td>Table 5-3, 6.5.9</td>
 *        <tr>
 *            <td>{@link OemMetadataKey#USEABLE_STOP_TIME}</td>
 *            <td>Segment</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-3, 6.5.9</td>
 *        <tr>
 *            <td>{@link OemMetadataKey#INTERPOLATION}</td>
 *            <td>Segment</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-3</td>
 *        <tr>
 *            <td>{@link OemMetadataKey#INTERPOLATION_DEGREE}</td>
 *            <td>Segment</td>
 *            <td>No</td>
 *            <td></td>
 *            <td>Table 5-3</td>
 *    </tbody>
 *</table>
 *
 * <p> The {@link MetadataKey#TIME_SYSTEM} must be constant for the whole file and is used
 * to interpret all dates except {@link HeaderKey#CREATION_DATE} which is always in {@link
 * TimeSystem#UTC UTC}. The guessing algorithm is not guaranteed to work so it is recommended
 * to provide values for {@link CommonMetadataKey#CENTER_NAME} and {@link MetadataKey#TIME_SYSTEM}
 * to avoid any bugs associated with incorrect guesses.
 *
 * <p> Standardized values for {@link MetadataKey#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
 * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
 * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
 * Additionally ITRF followed by a four digit year may be used.
 *
 * @author Hank Grabowski
 * @author Evan Ward
 * @since 9.0
 * @see <a href="https://public.ccsds.org/Pubs/502x0b2c1.pdf">CCSDS 502.0-B-2 Orbit Data
 *      Messages</a>
 * @see <a href="https://public.ccsds.org/Pubs/500x0g4.pdf">CCSDS 500.0-G-4 Navigation
 *      Data Definitions and Conventions</a>
 * @see StreamingOemWriter
 */
public class OemWriter extends AbstractMessageWriter<Header, OemSegment, Oem> {

    /** Version number implemented. **/
    public static final double CCSDS_OEM_VERS = 3.0;

    /** Default file name for error messages. */
    public static final String DEFAULT_FILE_NAME = "<OEM output>";

    /** Padding width for aligning the '=' sign. */
    public static final int KVN_PADDING_WIDTH = 20;

    /**
     * Constructor used to create a new OEM writer configured with the necessary parameters
     * to successfully fill in all required fields that aren't part of a standard object.
     * <p>
     * If the mandatory header entries are not present (or if header is null),
     * built-in defaults will be used
     * </p>
     * <p>
     * The writer is built from the complete header and partial metadata. The template
     * metadata is used to initialize and independent local copy, that will be updated
     * as new segments are written (with at least the segment start and stop will change,
     * but some other parts may change too). The {@code template} argument itself is not
     * changed.
     * </p>
     * <p>
     * Calling this constructor directly is not recommended. Users should rather use
     * {@link org.orekit.files.ccsds.ndm.WriterBuilder#buildOemWriter()
     * writerBuilder.buildOemWriter()}.
     * </p>
     * @param conventions IERS Conventions
     * @param dataContext used to retrieve frames, time scales, etc.
     * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
     * @since 11.0
     * @see #DEFAULT_FILE_NAME
     */
    public OemWriter(final IERSConventions conventions, final DataContext dataContext,
                     final AbsoluteDate missionReferenceDate) {
        super(Oem.ROOT, Oem.FORMAT_VERSION_KEY, CCSDS_OEM_VERS,
              new ContextBinding(
                  () -> conventions, () -> true, () -> dataContext,
                  () -> ParsedUnitsBehavior.STRICT_COMPLIANCE,
                  () -> missionReferenceDate, () -> TimeSystem.UTC, () -> 0.0, () -> 1.0));
    }

    /** {@inheritDoc} */
    @Override
    public void writeSegmentContent(final Generator generator, final double formatVersion,
                                    final OemSegment segment)
        throws IOException {

        final OemMetadata metadata = segment.getMetadata();
        writeMetadata(generator, metadata);

        startData(generator);

        // write data comments
        generator.writeComments(segment.getData().getComments());

        // Loop on orbit data
        final CartesianDerivativesFilter filter = segment.getAvailableDerivatives();
        if (filter == CartesianDerivativesFilter.USE_P) {
            throw new OrekitException(OrekitMessages.MISSING_VELOCITY);
        }
        final boolean useAcceleration = filter.equals(CartesianDerivativesFilter.USE_PVA);
        for (final TimeStampedPVCoordinates coordinates : segment.getCoordinates()) {
            writeOrbitEphemerisLine(generator, metadata, coordinates, useAcceleration);
        }

        // output covariance data
        writeCovariances(generator, segment.getMetadata(), segment.getData().getCovarianceMatrices());

        endData(generator);

    }

    /** Write an ephemeris segment metadata.
     * @param generator generator to use for producing output
     * @param metadata metadata to write
     * @throws IOException if the output stream throws one while writing.
     */
    void writeMetadata(final Generator generator, final OemMetadata metadata)
        throws IOException {

        // add an empty line for presentation
        generator.newLine();

        final ContextBinding oldContext = getContext();
        setContext(new ContextBinding(oldContext::getConventions,
                                      oldContext::isSimpleEOP,
                                      oldContext::getDataContext,
                                      oldContext::getParsedUnitsBehavior,
                                      oldContext::getReferenceDate,
                                      metadata::getTimeSystem,
                                      oldContext::getClockCount,
                                      oldContext::getClockRate));

        // Start metadata
        generator.enterSection(generator.getFormat() == FileFormat.KVN ?
                               KvnStructureKey.META.name() :
                               XmlStructureKey.metadata.name());

        generator.writeComments(metadata.getComments());

        // objects
        generator.writeEntry(OdmMetadataKey.OBJECT_NAME.name(),    metadata.getObjectName(),       null, true);
        generator.writeEntry(CommonMetadataKey.OBJECT_ID.name(),   metadata.getObjectID(),         null, true);
        generator.writeEntry(CommonMetadataKey.CENTER_NAME.name(), metadata.getCenter().getName(), null, false);

        // frames
        generator.writeEntry(CommonMetadataKey.REF_FRAME.name(), metadata.getReferenceFrame().getName(), null, true);
        if (metadata.getFrameEpoch() != null) {
            generator.writeEntry(CommonMetadataKey.REF_FRAME_EPOCH.name(),
                                 getTimeConverter(), metadata.getFrameEpoch(),
                                 false);
        }

        // time
        generator.writeEntry(MetadataKey.TIME_SYSTEM.name(), metadata.getTimeSystem(), true);
        generator.writeEntry(OemMetadataKey.START_TIME.name(), getTimeConverter(), metadata.getStartTime(), true);
        if (metadata.getUseableStartTime() != null) {
            generator.writeEntry(OemMetadataKey.USEABLE_START_TIME.name(), getTimeConverter(), metadata.getUseableStartTime(), false);
        }
        if (metadata.getUseableStopTime() != null) {
            generator.writeEntry(OemMetadataKey.USEABLE_STOP_TIME.name(), getTimeConverter(), metadata.getUseableStopTime(), false);
        }
        generator.writeEntry(OemMetadataKey.STOP_TIME.name(), getTimeConverter(), metadata.getStopTime(), true);

        // interpolation
        generator.writeEntry(OemMetadataKey.INTERPOLATION.name(), metadata.getInterpolationMethod(), false);
        // treat degree < 0 as equivalent to null
        if (metadata.getInterpolationDegree() >= 0) {
            generator.writeEntry(OemMetadataKey.INTERPOLATION_DEGREE.name(),
                    Integer.toString(metadata.getInterpolationDegree()),
                    null, false);
        }

        // Stop metadata
        generator.exitSection();

        // add an empty line for presentation
        generator.newLine();

    }

    /**
     * Write a single orbit ephemeris line .
     * @param generator generator to use for producing output
     * @param metadata metadata to use for interpreting data
     * @param coordinates orbit information for a given date
     * @param useAcceleration is true, the acceleration data must be used
     * @throws IOException if the output stream throws one while writing.
     */
    void writeOrbitEphemerisLine(final Generator generator, final OemMetadata metadata,
                                 final TimeStampedPVCoordinates coordinates,
                                 final boolean useAcceleration)
        throws IOException {

        if (generator.getFormat() == FileFormat.KVN) {

            // Epoch
            generator.writeRawData(generator.dateToString(getTimeConverter(), coordinates.getDate()));

            // Position data in km
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getX()))));
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getY()))));
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Unit.KILOMETRE.fromSI(coordinates.getPosition().getZ()))));

            // Velocity data in km/s
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getX()))));
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getY()))));
            generator.writeRawData(' ');
            generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S.fromSI(coordinates.getVelocity().getZ()))));

            // Acceleration data in km/s²
            if (useAcceleration) {
                generator.writeRawData(' ');
                generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getX()))));
                generator.writeRawData(' ');
                generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getY()))));
                generator.writeRawData(' ');
                generator.writeRawData(String.format(AccurateFormatter.format(Units.KM_PER_S2.fromSI(coordinates.getAcceleration().getZ()))));
            }

            // end the line
            generator.newLine();
        } else {
            generator.enterSection(OemDataSubStructureKey.stateVector.name());

            // Epoch
            generator.writeEntry(StateVectorKey.EPOCH.name(), getTimeConverter(), coordinates.getDate(), true);

            // Position data in km
            generator.writeEntry(StateVectorKey.X.name(), coordinates.getPosition().getX(), Unit.KILOMETRE, true);
            generator.writeEntry(StateVectorKey.Y.name(), coordinates.getPosition().getY(), Unit.KILOMETRE, true);
            generator.writeEntry(StateVectorKey.Z.name(), coordinates.getPosition().getZ(), Unit.KILOMETRE, true);

            // Velocity data in km/s
            generator.writeEntry(StateVectorKey.X_DOT.name(), coordinates.getVelocity().getX(), Units.KM_PER_S, true);
            generator.writeEntry(StateVectorKey.Y_DOT.name(), coordinates.getVelocity().getY(), Units.KM_PER_S, true);
            generator.writeEntry(StateVectorKey.Z_DOT.name(), coordinates.getVelocity().getZ(), Units.KM_PER_S, true);

            // Acceleration data in km/s²
            if (useAcceleration) {
                generator.writeEntry(StateVectorKey.X_DDOT.name(), coordinates.getAcceleration().getX(), Units.KM_PER_S2, true);
                generator.writeEntry(StateVectorKey.Y_DDOT.name(), coordinates.getAcceleration().getY(), Units.KM_PER_S2, true);
                generator.writeEntry(StateVectorKey.Z_DDOT.name(), coordinates.getAcceleration().getZ(), Units.KM_PER_S2, true);
            }

            generator.exitSection();

        }
    }

    /**
     * Write a covariance matrices.
     * @param generator generator to use for producing output
     * @param metadata metadata to use for interpreting data
     * @param covariances covariances to write
     * @throws IOException if the output stream throws one while writing.
     */
    void writeCovariances(final Generator generator, final OemMetadata metadata,
                          final List<CartesianCovariance> covariances)
        throws IOException {
        if (covariances != null && !covariances.isEmpty()) {

            // enter the global covariance section in KVN
            if (generator.getFormat() == FileFormat.KVN) {
                generator.enterSection(OemDataSubStructureKey.COVARIANCE.name());
            }

            for (final CartesianCovariance covariance : covariances) {
                writeCovariance(generator, metadata, covariance);
            }

            // exit the global covariance section in KVN
            if (generator.getFormat() == FileFormat.KVN) {
                generator.exitSection();
            }

        }
    }

    /**
     * Write a single covariance matrix.
     * @param generator generator to use for producing output
     * @param metadata metadata to use for interpreting data
     * @param covariance covariance to write
     * @throws IOException if the output stream throws one while writing.
     */
    private void writeCovariance(final Generator generator, final OemMetadata metadata,
                                 final CartesianCovariance covariance)
        throws IOException {

        // wrapper for a single matrix in XML
        if (generator.getFormat() == FileFormat.XML) {
            generator.enterSection(OemDataSubStructureKey.covarianceMatrix.name());
        }

        // epoch
        generator.writeEntry(CartesianCovarianceKey.EPOCH.name(), getTimeConverter(), covariance.getEpoch(), true);

        // reference frame
        if (covariance.getReferenceFrame() != metadata.getReferenceFrame()) {
            generator.writeEntry(CartesianCovarianceKey.COV_REF_FRAME.name(), covariance.getReferenceFrame().getName(), null, false);
        }

        // matrix data
        final RealMatrix m = covariance.getCovarianceMatrix();
        if (generator.getFormat() == FileFormat.KVN) {
            for (int i = 0; i < m.getRowDimension(); ++i) {

                // write triangular matrix entries
                for (int j = 0; j <= i; ++j) {
                    if (j > 0) {
                        generator.writeRawData(' ');
                    }
                    generator.writeRawData(AccurateFormatter.format(Units.KM2.fromSI(m.getEntry(i, j))));
                }

                // end the line
                generator.newLine();

            }
        } else {
            generator.writeEntry(CartesianCovarianceKey.CX_X.name(),         m.getEntry(0, 0), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CY_X.name(),         m.getEntry(1, 0), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CY_Y.name(),         m.getEntry(1, 1), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CZ_X.name(),         m.getEntry(2, 0), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CZ_Y.name(),         m.getEntry(2, 1), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CZ_Z.name(),         m.getEntry(2, 2), Units.KM2,        true);
            generator.writeEntry(CartesianCovarianceKey.CX_DOT_X.name(),     m.getEntry(3, 0), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CX_DOT_Y.name(),     m.getEntry(3, 1), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CX_DOT_Z.name(),     m.getEntry(3, 2), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CX_DOT_X_DOT.name(), m.getEntry(3, 3), Units.KM2_PER_S2, true);
            generator.writeEntry(CartesianCovarianceKey.CY_DOT_X.name(),     m.getEntry(4, 0), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y.name(),     m.getEntry(4, 1), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CY_DOT_Z.name(),     m.getEntry(4, 2), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CY_DOT_X_DOT.name(), m.getEntry(4, 3), Units.KM2_PER_S2, true);
            generator.writeEntry(CartesianCovarianceKey.CY_DOT_Y_DOT.name(), m.getEntry(4, 4), Units.KM2_PER_S2, true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X.name(),     m.getEntry(5, 0), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y.name(),     m.getEntry(5, 1), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z.name(),     m.getEntry(5, 2), Units.KM2_PER_S,  true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_X_DOT.name(), m.getEntry(5, 3), Units.KM2_PER_S2, true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Y_DOT.name(), m.getEntry(5, 4), Units.KM2_PER_S2, true);
            generator.writeEntry(CartesianCovarianceKey.CZ_DOT_Z_DOT.name(), m.getEntry(5, 5), Units.KM2_PER_S2, true);
        }

        // wrapper for a single matrix in XML
        if (generator.getFormat() == FileFormat.XML) {
            generator.exitSection();
        }

    }

    /** Start of a data block.
     * @param generator generator to use for producing output
     * @throws IOException if the output stream throws one while writing.
     */
    void startData(final Generator generator) throws IOException {
        if (generator.getFormat() == FileFormat.XML) {
            generator.enterSection(XmlStructureKey.data.name());
        }
    }

    /** End of a data block.
     * @param generator generator to use for producing output
     * @throws IOException if the output stream throws one while writing.
     */
    void endData(final Generator generator) throws IOException {
        if (generator.getFormat() == FileFormat.XML) {
            generator.exitSection();
        }
    }

}