StreamingCpfWriter.java

/* Copyright 2002-2024 CS GROUP
 * 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.
 * CS 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.ilrs;

import java.io.IOException;
import java.util.Locale;

import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.orekit.errors.OrekitException;
import org.orekit.frames.Frame;
import org.orekit.propagation.Propagator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.sampling.OrekitFixedStepHandler;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.TimeStampedPVCoordinates;

/**
 * A writer for CPF files.
 *
 * <p> Each instance corresponds to a single CPF file.
 *
 * <p> This class can be used as a step handler for a {@link Propagator}.
 * The following example shows its use as a step handler.
 *
 * <p>
 * <b>Note:</b> By default, only required header keys are wrote (H1 and H2). Furthermore, only position data can be written.
 * Other keys (optionals) are simply ignored.
 * Contributions are welcome to support more fields in the format.
 *
 * @author Bryan Cazabonne
 * @since 10.3
 */
public class StreamingCpfWriter {

    /** New line separator for output file. */
    private static final String NEW_LINE = "\n";

    /** String A2 Format. */
    private static final String A1 = "%1s";

    /** String A2 Format. */
    private static final String A2 = "%2s";

    /** String A3 Format. */
    private static final String A3 = "%3s";

    /** String A4 Format. */
    private static final String A4 = "%4s";

    /** String A8 Format. */
    private static final String A8 = "%8s";

    /** String A10 Format. */
    private static final String A10 = "%10s";

    /** Integer I1 Format. */
    private static final String I1 = "%1d";

    /** Integer I2 Format. */
    private static final String I2 = "%2d";

    /** Integer I3 Format. */
    private static final String I3 = "%3d";

    /** Integer I4 Format. */
    private static final String I4 = "%4d";

    /** Integer I5 Format. */
    private static final String I5 = "%5d";

    /** Real 13.6 Format. */
    private static final String F13_6 = "%13.6f";

    /** Real 17.3 Format. */
    private static final String F17_3 = "%17.3f";

    /** Real 19.6 Format. */
    private static final String F19_6 = "%19.6f";

    /** Space. */
    private static final String SPACE = " ";

    /** Empty string. */
    private static final String EMPTY_STRING = "";

    /** File format. */
    private static final String FORMAT = "CPF";

    /** Default locale. */
    private static final Locale STANDARDIZED_LOCALE = Locale.US;

    /** Default value for direction flag in position record. */
    private static final int DEFAULT_DIRECTION_FLAG = 0;

    /** Output stream. */
    private final Appendable writer;

    /** Time scale for all dates. */
    private final TimeScale timeScale;

    /** Container for header data. */
    private final CPFHeader header;

    /** Flag for optional velocity record. */
    private final boolean velocityFlag;

    /**
     * Create a CPF writer than streams data to the given output stream.
     * <p>
     * Using this constructor, velocity data are not written.
     * </p>
     * @param writer     the output stream for the CPF file.
     * @param timeScale  for all times in the CPF
     * @param header     container for header data
     * @see #StreamingCpfWriter(Appendable, TimeScale, CPFHeader, boolean)
     */
    public StreamingCpfWriter(final Appendable writer,
                              final TimeScale timeScale,
                              final CPFHeader header) {
        this(writer, timeScale, header, false);
    }

    /**
     * Create a CPF writer than streams data to the given output stream.
     *
     * @param writer       the output stream for the CPF file.
     * @param timeScale    for all times in the CPF
     * @param header       container for header data
     * @param velocityFlag true if velocity must be written
     * @since 11.2
     */
    public StreamingCpfWriter(final Appendable writer,
                              final TimeScale timeScale,
                              final CPFHeader header,
                              final boolean velocityFlag) {
        this.writer       = writer;
        this.timeScale    = timeScale;
        this.header       = header;
        this.velocityFlag = velocityFlag;
    }

    /**
     * Writes the CPF header for the file.
     * @throws IOException if the stream cannot write to stream
     */
    public void writeHeader() throws IOException {

        // Write H1
        HeaderLineWriter.H1.write(header, writer, timeScale);
        writer.append(NEW_LINE);

        // Write H2
        HeaderLineWriter.H2.write(header, writer, timeScale);
        writer.append(NEW_LINE);

        // End of header
        writer.append("H9");
        writer.append(NEW_LINE);

    }

    /**
     * Write end of file.
     * @throws IOException if the stream cannot write to stream
     */
    public void writeEndOfFile() throws IOException {
        writer.append("99");
    }

    /**
     * Create a writer for a new CPF ephemeris segment.
     * <p>
     * The returned writer can only write a single ephemeris segment in a CPF.
     * </p>
     * @param frame the reference frame to use for the segment.
     * @return a new CPF segment, ready for writing.
     */
    public Segment newSegment(final Frame frame) {
        return new Segment(frame);
    }

    /**
     * Write a String value in the file.
     * @param cpfWriter writer
     * @param format format
     * @param value value
     * @param withSpace true if a space must be added
     * @throws IOException if value cannot be written
     */
    private static void writeValue(final Appendable cpfWriter, final String format,
                                   final String value, final boolean withSpace)
        throws IOException {
        cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
    }

    /**
     * Write a integer value in the file.
     * @param cpfWriter writer
     * @param format format
     * @param value value
     * @param withSpace true if a space must be added
     * @throws IOException if value cannot be written
     */
    private static void writeValue(final Appendable cpfWriter, final String format,
                                   final int value, final boolean withSpace)
        throws IOException {
        cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
    }

    /**
     * Write a real value in the file.
     * @param cpfWriter writer
     * @param format format
     * @param value value
     * @param withSpace true if a space must be added
     * @throws IOException if value cannot be written
     */
    private static void writeValue(final Appendable cpfWriter, final String format,
                                   final double value, final boolean withSpace)
        throws IOException {
        cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
    }

    /**
     * Write a String value in the file.
     * @param cpfWriter writer
     * @param format format
     * @param value value
     * @param withSpace true if a space must be added
     * @throws IOException if value cannot be written
     */
    private static void writeValue(final Appendable cpfWriter, final String format,
                                   final boolean value, final boolean withSpace)
        throws IOException {
        // Change to an integer value
        final int intValue = value ? 1 : 0;
        writeValue(cpfWriter, format, intValue, withSpace);
    }

    /** A writer for a segment of a CPF. */
    public class Segment implements OrekitFixedStepHandler {

        /** Reference frame of the output states. */
        private final Frame frame;

        /**
         * Create a new segment writer.
         *
         * @param frame    for the output states. Used by {@link #handleStep(SpacecraftState,
         *                 boolean)}.
         */
        private Segment(final Frame frame) {
            this.frame = frame;
        }

        /** {@inheritDoc}. */
        @Override
        public void handleStep(final SpacecraftState currentState) {
            try {

                // Write ephemeris line
                writeEphemerisLine(currentState.getPVCoordinates(frame));

            } catch (IOException e) {
                throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
                                          e.getLocalizedMessage());
            }

        }

        /** {@inheritDoc}. */
        @Override
        public void finish(final SpacecraftState finalState) {
            try {
                // Write ephemeris line
                writeEphemerisLine(finalState.getPVCoordinates(frame));

                // Write end of file
                writeEndOfFile();

            } catch (IOException e) {
                throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
                                          e.getLocalizedMessage());
            }

        }

        /**
         * Write ephemeris lines.
         * <p>
         * If <code>velocityFlag</code> is equals to true, both
         * position and velocity records are written. Otherwise,
         * only the position data are used.
         * </p>
         * @param pv the time, position, and velocity to write.
         * @throws IOException if the output stream throws one while writing.
         */
        public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
            throws IOException {

            // Record type and direction flag
            writeValue(writer, A2, "10",                                    true);
            writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);

            // Epoch
            final AbsoluteDate epoch = pv.getDate();
            final DateTimeComponents dtc = epoch.getComponents(timeScale);
            writeValue(writer, I5, dtc.getDate().getMJD(),                  true);
            writeValue(writer, F13_6, dtc.getTime().getSecondsInLocalDay(), true);

            // Leap second flag (default 0)
            writeValue(writer, I2, 0, true);

            // Position
            final Vector3D position = pv.getPosition();
            writeValue(writer, F17_3, position.getX(), true);
            writeValue(writer, F17_3, position.getY(), true);
            writeValue(writer, F17_3, position.getZ(), false);

            // New line
            writer.append(NEW_LINE);

            // Write the velocity record
            if (velocityFlag) {

                // Record type and direction flag
                writeValue(writer, A2, "20",                                    true);
                writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);

                // Velocity
                final Vector3D velocity = pv.getVelocity();
                writeValue(writer, F19_6, velocity.getX(), true);
                writeValue(writer, F19_6, velocity.getY(), true);
                writeValue(writer, F19_6, velocity.getZ(), false);

                // New line
                writer.append(NEW_LINE);

            }

        }

    }

    /** Writer for specific header lines. */
    public enum HeaderLineWriter {

        /** Header first line. */
        H1("H1") {

            /** {@inheritDoc} */
            @Override
            public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
                throws IOException {

                // write first keys
                writeValue(cpfWriter, A2, getIdentifier(),                           true);
                writeValue(cpfWriter, A3, FORMAT,                                    true);
                writeValue(cpfWriter, I2, cpfHeader.getVersion(),                    true);
                writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
                writeValue(cpfWriter, A3, cpfHeader.getSource(),                     true);
                writeValue(cpfWriter, I4, cpfHeader.getProductionEpoch().getYear(),  true);
                writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getMonth(), true);
                writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getDay(),   true);
                writeValue(cpfWriter, I2, cpfHeader.getProductionHour(),             true);
                writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
                writeValue(cpfWriter, I3, cpfHeader.getSequenceNumber(),             true);

                // check file version
                if (cpfHeader.getVersion() == 2) {
                    writeValue(cpfWriter, I2, cpfHeader.getSubDailySequenceNumber(), true);
                }

                // write target name from official list
                writeValue(cpfWriter, A10, cpfHeader.getName(),                      true);

                // write notes (not supported yet)
                writeValue(cpfWriter, A10, SPACE,                                    false);
            }

        },

        /** Header second line. */
        H2("H2") {

            /** {@inheritDoc} */
            @Override
            public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
                throws IOException {

                // write identifiers
                writeValue(cpfWriter, A2, getIdentifier(),                                 true);
                writeValue(cpfWriter, A8, cpfHeader.getIlrsSatelliteId(),                  true);
                writeValue(cpfWriter, A4, cpfHeader.getSic(),                              true);
                writeValue(cpfWriter, A8, cpfHeader.getNoradId(),                          true);

                // write starting epoch
                final AbsoluteDate starting = cpfHeader.getStartEpoch();
                final DateTimeComponents dtcStart = starting.getComponents(timescale);
                writeValue(cpfWriter, I4, dtcStart.getDate().getYear(),                    true);
                writeValue(cpfWriter, I2, dtcStart.getDate().getMonth(),                   true);
                writeValue(cpfWriter, I2, dtcStart.getDate().getDay(),                     true);
                writeValue(cpfWriter, I2, dtcStart.getTime().getHour(),                    true);
                writeValue(cpfWriter, I2, dtcStart.getTime().getMinute(),                  true);
                writeValue(cpfWriter, I2, (int) dtcStart.getTime().getSecond(),            true);

                // write ending epoch
                final AbsoluteDate ending = cpfHeader.getEndEpoch();
                final DateTimeComponents dtcEnd = ending.getComponents(timescale);
                writeValue(cpfWriter, I4, dtcEnd.getDate().getYear(),                      true);
                writeValue(cpfWriter, I2, dtcEnd.getDate().getMonth(),                     true);
                writeValue(cpfWriter, I2, dtcEnd.getDate().getDay(),                       true);
                writeValue(cpfWriter, I2, dtcEnd.getTime().getHour(),                      true);
                writeValue(cpfWriter, I2, dtcEnd.getTime().getMinute(),                    true);
                writeValue(cpfWriter, I2, (int)  dtcEnd.getTime().getSecond(),             true);

                // write last keys
                writeValue(cpfWriter, I5, cpfHeader.getStep(),                             true);
                writeValue(cpfWriter, I1, cpfHeader.isCompatibleWithTIVs(),                true);
                writeValue(cpfWriter, I1, cpfHeader.getTargetClass(),                      true);
                writeValue(cpfWriter, I2, cpfHeader.getRefFrameId(),                       true);
                writeValue(cpfWriter, I1, cpfHeader.getRotationalAngleType(),              true);
                if (cpfHeader.getVersion() == 1) {
                    writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), false);
                } else {
                    writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), true);
                    writeValue(cpfWriter, I2, cpfHeader.getTargetLocation(),               false);
                }

            }

        };

        /** Identifier. */
        private final String identifier;

        /** Simple constructor.
         * @param identifier regular expression for identifying line (i.e. first element)
         */
        HeaderLineWriter(final String identifier) {
            this.identifier = identifier;
        }

        /** Write a line.
         * @param cpfHeader container for header data
         * @param cpfWriter writer
         * @param timescale time scale for dates
         * @throws IOException
         *             if any buffer writing operations fail or if the underlying
         *             format doesn't support a configuration in the file
         */
        public abstract void write(CPFHeader cpfHeader, Appendable cpfWriter, TimeScale timescale)  throws IOException;

        /**
         * Get the regular expression for identifying line.
         * @return the regular expression for identifying line
         */
        public String getIdentifier() {
            return identifier;
        }

    }

}