BaseRinexWriter.java

/* Copyright 2022-2025 Thales Alenia Space
 * 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.rinex.utils;

import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.section.Label;
import org.orekit.files.rinex.section.RinexBaseHeader;
import org.orekit.files.rinex.section.RinexComment;
import org.orekit.time.DateTimeComponents;
import org.orekit.utils.formatting.FastDecimalFormatter;
import org.orekit.utils.formatting.FastDoubleFormatter;
import org.orekit.utils.formatting.FastLongFormatter;
import org.orekit.utils.formatting.FastScientificFormatter;

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

/** Base write for Rinex files.
 * @param <T> type of the header
 * @author Luc Maisonobe
 * @since 14.0
 */
public abstract class BaseRinexWriter<T extends RinexBaseHeader> implements AutoCloseable {

    /** Format for one 2 digits integer field. */
    public static final FastLongFormatter TWO_DIGITS_INTEGER = new FastLongFormatter(2, false);

    /** Format for one 2 digits integer field. */
    public static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);

    /** Format for one 3 digits integer field. */
    public static final FastLongFormatter THREE_DIGITS_INTEGER = new FastLongFormatter(3, false);

    /** Format for one 4 digits integer field. */
    public static final FastLongFormatter FOUR_DIGITS_INTEGER = new FastLongFormatter(4, false);

    /** Format for one 4 digits integer field. */
    public static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);

    /** Format for one 6 digits integer field. */
    public static final FastLongFormatter SIX_DIGITS_INTEGER = new FastLongFormatter(6, false);

    /** Format for one 9.2 digits float field. */
    public static final FastDoubleFormatter NINE_TWO_DIGITS_FLOAT = new FastDecimalFormatter(9, 2);

    /** Format for one 19 characters wide field in scientific notation. */
    public static final FastDoubleFormatter NINETEEN_SCIENTIFIC_FLOAT = new FastScientificFormatter(19);

    /** Destination of generated output. */
    private final Appendable output;

    /** Output name for error messages. */
    private final String outputName;

    /** Indicator of closed output. */
    private boolean closed;

    /** Saved header. */
    private T savedHeader;

    /** Index of the labels in header lines. */
    private int savedLabelIndex;

    /** Saved comments. */
    private List<RinexComment> savedComments;

    /** Line number. */
    private int lineNumber;

    /** Column number. */
    private int column;

    /** Simple constructor.
     * @param output destination of generated output
     * @param outputName output name for error messages
     */
    protected BaseRinexWriter(final Appendable output, final String outputName) {
        this.output        = output;
        this.outputName    = outputName;
        this.savedComments = Collections.emptyList();
        this.closed        = false;
    }

    /** {@inheritDoc} */
    @Override
    public void close() throws IOException {
        try {
            if (!closed && output instanceof AutoCloseable) {
                ((AutoCloseable) output).close();
            }
            closed = true;
        // CHECKSTYLE: stop IllegalCatch check
        } catch (Exception ex) {
        // CHECKSTYLE: resume IllegalCatch check
            throw new IOException(ex);
        }
    }

    /** Prepare comments to be emitted at specified lines.
     * @param comments comments to be emitted
     */
    public void prepareComments(final List<RinexComment> comments) {
        savedComments = comments;
    }

    /** Write header.
     * @param header     header to write
     * @param labelIndex index of the label in header
     * @exception IOException if an I/O error occurs.
     */
    protected void writeHeader(final T header, final int labelIndex) throws IOException {

        // check header is written exactly once
        if (savedHeader != null) {
            throw new OrekitException(OrekitMessages.HEADER_ALREADY_WRITTEN, outputName);
        }
        savedHeader     = header;
        savedLabelIndex = labelIndex;
        lineNumber      = 1;

    }

    /** Get the header.
     * @return header
     */
    protected T getHeader() {
        return savedHeader;
    }

    /** Get column number.
     * @return column number
     */
    protected int getColumn() {
        return column;
    }

    /** Finish one header line.
     * @param label line label
     * @throws IOException if an I/O error occurs.
     */
    protected void finishHeaderLine(final Label label) throws IOException {
        checkOutputNotClosed();
        for (int i = column; i < savedLabelIndex; ++i) {
            output.append(' ');
        }
        output.append(label.getLabel());
        finishLine();
    }

    /** Finish one line.
     * @throws IOException if an I/O error occurs.
     */
    public void finishLine() throws IOException {

        checkOutputNotClosed();

        // pending line
        output.append(System.lineSeparator());
        lineNumber++;
        column = 0;

        // emit comments that should be placed at next lines
        for (final RinexComment comment : savedComments) {
            if (comment.getLineNumber() == lineNumber) {
                outputField(comment.getText(), savedLabelIndex, true);
                output.append(CommonLabel.COMMENT.getLabel());
                output.append(System.lineSeparator());
                lineNumber++;
                column = 0;
            } else if (comment.getLineNumber() > lineNumber) {
                break;
            }
        }

    }

    /** Write one header string.
     * @param s string data (may be null)
     * @param label line label
     * @throws IOException if an I/O error occurs.
     */
    protected void writeHeaderLine(final String s, final Label label) throws IOException {
        if (s != null) {
            outputField(s, s.length(), true);
            finishHeaderLine(label);
        }
    }

    /** Check header has been written.
     */
    protected void checkHeaderWritten() {
        if (savedHeader == null) {
            throw new OrekitException(OrekitMessages.HEADER_NOT_WRITTEN, outputName);
        }
    }

    /** Check if column exceeds header line length.
     * @param tentative tentative column number
     * @return true if tentative column number exceeds header line length
     */
    protected boolean exceedsHeaderLength(final int tentative) {
        return tentative > savedLabelIndex;
    }

    /** Write the PGM / RUN BY / DATE header line.
     * @param header header to write
     * @throws IOException if an I/O error occurs.
     */
    protected void writeProgramRunByDate(final RinexBaseHeader header)
        throws IOException {
        outputField(header.getProgramName(), 20, true);
        outputField(header.getRunByName(),   40, true);
        final DateTimeComponents dtc = header.getCreationDateComponents();
        if (header.getFormatVersion() < 3.0 && dtc.getTime().getSecond() < 0.5) {
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 42);
            outputField('-', 43);
            outputField(dtc.getDate().getMonthEnum().getUpperCaseAbbreviation(), 46,  true);
            outputField('-', 47);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 49);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 52);
            outputField(':', 53);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 55);
            outputField(header.getCreationTimeZone(), 58, true);
        } else {
            outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 44);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 46);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 48);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 51);
            outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 53);
            outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 55);
            outputField(header.getCreationTimeZone(), 59, false);
        }
        finishHeaderLine(CommonLabel.PROGRAM);
    }

    /** Output one single character field.
     * @param c field value
     * @param next target column for next field
     * @throws IOException if an I/O error occurs.
     */
    public void outputField(final char c, final int next) throws IOException {
        outputField(Character.toString(c), next, false);
    }

    /** Output one integer field.
     * @param formatter formatter to use
     * @param value field value
     * @param next target column for next field
     * @throws IOException if an I/O error occurs.
     */
    public void outputField(final FastLongFormatter formatter, final int value, final int next) throws IOException {
        outputField(formatter.toString(value), next, false);
    }

    /** Output one long integer field.
     * @param formatter formatter to use
     * @param value field value
     * @param next target column for next field
     * @throws IOException if an I/O error occurs.
     */
    public void outputField(final FastLongFormatter formatter, final long value, final int next) throws IOException {
        outputField(formatter.toString(value), next, false);
    }

    /** Output one double field.
     * @param formatter formatter to use
     * @param value field value
     * @param next target column for next field
     * @throws IOException if an I/O error occurs.
     */
    public void outputField(final FastDoubleFormatter formatter, final double value, final int next) throws IOException {
        if (Double.isNaN(value)) {
            // NaN values are replaced by blank fields
            outputField("", next, true);
        } else {
            outputField(formatter.toString(value), next, false);
        }
    }

    /** Output one field.
     * @param field field to output
     * @param next target column for next field
     * @param leftJustified if true, field is left-justified
     * @throws IOException if an I/O error occurs.
     */
    public void outputField(final String field, final int next, final boolean leftJustified) throws IOException {
        final int padding = next - (field == null ? 0 : field.length()) - column;
        if (padding < 0) {
            throw new OrekitException(OrekitMessages.FIELD_TOO_LONG, field, next - column);
        }
        checkOutputNotClosed();
        if (leftJustified && field != null) {
            output.append(field);
        }
        for (int i = 0; i < padding; ++i) {
            output.append(' ');
        }
        if (!leftJustified && field != null) {
            output.append(field);
        }
        column = next;
    }

    /** Check that output has not been closed.
     */
    private void checkOutputNotClosed() {
        if (closed) {
            throw new OrekitException(OrekitMessages.OUTPUT_ALREADY_CLOSED, outputName);
        }
    }

}