RinexNavigationWriter.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.navigation;

import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.files.rinex.navigation.writers.ionosphere.BDGIMMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.BeidouCivilianNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.BeidouLegacyNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.EarthOrientationParametersMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.GPSCivilianNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.GPSLegacyNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.GalileoNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ionosphere.GlonassCDMSMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.GlonassFdmaNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ionosphere.KlobucharMessageWriter;
import org.orekit.files.rinex.navigation.writers.ionosphere.NavICKlobucharMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.NavICL1NVNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.NavICLegacyNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ionosphere.NavICNeQuickNMessageWriter;
import org.orekit.files.rinex.navigation.writers.NavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ionosphere.NequickGMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.QZSSCivilianNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.QZSSLegacyNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.ephemeris.SBASNavigationMessageWriter;
import org.orekit.files.rinex.navigation.writers.SystemTimeOffsetMessageWriter;
import org.orekit.files.rinex.observation.ObservationLabel;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.utils.BaseRinexWriter;
import org.orekit.gnss.PredefinedObservationType;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.propagation.analytical.gnss.data.NavigationMessage;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.GNSSDate;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.formatting.FastDoubleFormatter;
import org.orekit.utils.formatting.FastLongFormatter;
import org.orekit.utils.formatting.FastScientificFormatter;
import org.orekit.utils.units.Unit;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;

/** Writer for Rinex navigation file.
 * @author Luc Maisonobe
 * @since 14.0
 */
public class RinexNavigationWriter extends BaseRinexWriter<RinexNavigationHeader> {

    /** Format for one 9 digits integer field. */
    private static final FastLongFormatter NINE_DIGITS_INTEGER = new FastLongFormatter(9, false);

    /** Format for one 12 digits float field. */
    private static final FastDoubleFormatter TWELVE_DIGITS_SCIENTIFIC = new FastScientificFormatter(12);

    /** Format for one 16 digits float field. */
    private static final FastDoubleFormatter SIXTEEN_DIGITS_SCIENTIFIC = new FastScientificFormatter(16);

    /** Format for one 17 digits float field. */
    private static final FastDoubleFormatter SEVENTEEN_DIGITS_SCIENTIFIC = new FastScientificFormatter(17);

    /** Identifier for system time offset messages.
     * <p>
     * The identifier is prefixed with "00_" so all time offset messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String STO_IDENTIFIER = "00_STO";

    /** Identifier for Earth Orientation Parameters messages.
     * <p>
     * The identifier is prefixed with "00_" so all EOP messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String EOP_IDENTIFIER = "00_EOP";

    /** Identifier for Klobuchar model ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all Klobuchar messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String KLOBUCHAR_IDENTIFIER = "00_IONO_KLOBUCHAR";

    /** Identifier for NeQuick G ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all NeQuick messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String NEQUICK_IDENTIFIER = "00_IONO_NEQUICK";

    /** Identifier for BDGIM ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all BDGIM messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String BDGIM_IDENTIFIER = "00_IONO_BDGIM";

    /** Identifier for NavIC Klobuchar ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all NavIC Klobuchar messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String NAVIC_KLOBUCHAR_IDENTIFIER = "00_IONO_NAVIC_KLOBUCHAR";

    /** Identifier for NavIC NeQuick N ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all NavIC NeQuick N messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String NAVIC_NEQUICK_N_IDENTIFIER = "00_IONO_NAVIC_NEQUICK_N";

    /** Identifier for GLONASS CDMS ionospheric messages.
     * <p>
     * The identifier is prefixed with "00_" so all GLONASS CDMS messages
     * are grouped together before satellite messages
     * </p>
     */
    private static final String GLONASS_CDMS_IDENTIFIER = "00_IONO_GLONASS_CDMS_N";

    /** Mapper from satellite system to time scales. */
    private final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder;

    /** Set of time scales. */
    private final TimeScales timeScales;

    /** Simple constructor.
     * <p>
     * This constructor uses the {@link DataContext#getDefault() default data context}
     * and recognizes only {@link PredefinedObservationType} and {@link SatelliteSystem}
     * with non-null {@link SatelliteSystem#getObservationTimeScale() time scales}
     * (i.e. neither user-defined, nor {@link SatelliteSystem#SBAS}, nor {@link SatelliteSystem#MIXED}).
     * </p>
     * @param output destination of generated output
     * @param outputName output name for error messages
     */
    @DefaultDataContext
    public RinexNavigationWriter(final Appendable output, final String outputName) {
        this(output, outputName,
             (system, ts) -> system.getObservationTimeScale() == null ?
                             null :
                             system.getObservationTimeScale().getTimeScale(ts),
             DataContext.getDefault().getTimeScales());
    }

    /** Simple constructor.
     * @param output destination of generated output
     * @param outputName output name for error messages
     * @param timeScaleBuilder mapper from satellite system to time scales (useful for user-defined satellite systems)
     * @param timeScales the set of time scales to use when parsing dates
     * @since 13.0
     */
    public RinexNavigationWriter(final Appendable output, final String outputName,
                                 final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
                                 final TimeScales timeScales) {
        super(output, outputName);
        this.timeScaleBuilder = timeScaleBuilder;
        this.timeScales       = timeScales;
    }

    /** Get the known time scales.
     * @return known time scales
     */
    public TimeScales getTimeScales() {
        return timeScales;
    }

    /** Write a complete navigation file.
     * @param rinexNavigation Rinex navigation file to write
     * @exception IOException if an I/O error occurs.
     */
    public void writeCompleteFile(final RinexNavigation rinexNavigation) throws IOException {

        prepareComments(rinexNavigation.getComments());
        writeHeader(rinexNavigation.getHeader());

        // prepare chronological iteration
        final List<PendingMessages<?>> pending = new ArrayList<>();

        // ephemeris messages
        pending.addAll(createHandlers(rinexNavigation.getGPSLegacyNavigationMessages(),
                                      new GPSLegacyNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getGPSCivilianNavigationMessages(),
                                      new GPSCivilianNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getGalileoNavigationMessages(),
                                      new GalileoNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getBeidouLegacyNavigationMessages(),
                                      new BeidouLegacyNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getBeidouCivilianNavigationMessages(),
                                      new BeidouCivilianNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getQZSSLegacyNavigationMessages(),
                                      new QZSSLegacyNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getQZSSCivilianNavigationMessages(),
                                      new QZSSCivilianNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getNavICLegacyNavigationMessages(),
                                      new NavICLegacyNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getNavICL1NVNavigationMessages(),
                                      new NavICL1NVNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getGlonassNavigationMessages(),
                                      new GlonassFdmaNavigationMessageWriter()));
        pending.addAll(createHandlers(rinexNavigation.getSBASNavigationMessages(),
                                      new SBASNavigationMessageWriter()));

        // STO messages
        addHandler(pending, STO_IDENTIFIER,
                   rinexNavigation.getSystemTimeOffsets(), new SystemTimeOffsetMessageWriter());

        // EOP messages
        addHandler(pending, EOP_IDENTIFIER,
                   rinexNavigation.getEarthOrientationParameters(), new EarthOrientationParametersMessageWriter());

        // ION messages
        addHandler(pending, KLOBUCHAR_IDENTIFIER,
                   rinexNavigation.getKlobucharMessages(), new KlobucharMessageWriter());
        addHandler(pending, NEQUICK_IDENTIFIER,
                   rinexNavigation.getNequickGMessages(), new NequickGMessageWriter());
        addHandler(pending, BDGIM_IDENTIFIER,
                   rinexNavigation.getBDGIMMessages(), new BDGIMMessageWriter());
        addHandler(pending, NAVIC_KLOBUCHAR_IDENTIFIER,
                   rinexNavigation.getNavICKlobucharMessages(), new NavICKlobucharMessageWriter());
        addHandler(pending, NAVIC_NEQUICK_N_IDENTIFIER,
                   rinexNavigation.getNavICNeQuickNMessages(), new NavICNeQuickNMessageWriter());
        addHandler(pending, GLONASS_CDMS_IDENTIFIER,
                   rinexNavigation.getGlonassCDMSMessages(), new GlonassCDMSMessageWriter());

        pending.sort(Comparator.comparing(pl -> pl.identifier));

        // write messages in chronological order
        for (AbsoluteDate date = earliest(pending); date.isFinite(); date = earliest(pending)) {
            // write all messages that correspond to this date
            for (final PendingMessages<?> pm : pending) {
                pm.writeMessageAtDate(date, getHeader());
            }
        }

    }

    /** Create messages handler for one message type.
     * @param <T> type of the navigation message
     * @param map messages map
     * @param messageWriter writer for the current message type
     * @return list of handlers for one message type
     */
    private <T extends NavigationMessage> List<PendingMessages<?>> createHandlers(final Map<String, List<T>> map,
                                                                                  final NavigationMessageWriter<T> messageWriter) {
        final List<PendingMessages<?>> handlers = new ArrayList<>();
        for (final Map.Entry<String, List<T>> entry : map.entrySet()) {
            addHandler(handlers, entry.getKey(), entry.getValue(), messageWriter);
        }
        return handlers;
    }

    /** Add message handler for one message type.
     * @param <T> type of the navigation message
     * @param pending list to complete
     * @param identifier identifier
     * @param list messages list
     * @param messageWriter writer for the current message type
     */
    private <T extends NavigationMessage> void addHandler(final List<PendingMessages<?>> pending,
                                                          final String identifier, final List<T> list,
                                                          final NavigationMessageWriter<T> messageWriter) {
        if (!list.isEmpty()) {
            pending.add(new PendingMessages<>(identifier, messageWriter, list));
        }
    }

    /** Find the earliest pending date.
     * @param pending pending messages
     * @return earliest pending date
     */
    private AbsoluteDate earliest(final List<PendingMessages<?>> pending) {
        AbsoluteDate earliest = AbsoluteDate.FUTURE_INFINITY;
        for (final PendingMessages<?> pm : pending) {
            if (pm.nextDate().isBefore(earliest)) {
                earliest = pm.nextDate();
            }
        }
        return earliest;
    }

    /** Write header.
     * <p>
     * This method must be called exactly once at the beginning
     * (directly or by {@link #writeCompleteFile(RinexNavigation)})
     * </p>
     * @param header header to write
     * @exception IOException if an I/O error occurs.
     */
    public void writeHeader(final RinexNavigationHeader header) throws IOException {

        super.writeHeader(header, RinexNavigationHeader.LABEL_INDEX);

        // RINEX VERSION / TYPE
        outputField(NINE_TWO_DIGITS_FLOAT, header.getFormatVersion(), 9);
        outputField("",                20, true);
        if (header.getFormatVersion() < 3.0 && header.getSatelliteSystem() == SatelliteSystem.GLONASS) {
            outputField("GLONASS NAV DATA", 40, true);
        } else {
            outputField("NAVIGATION DATA", 40, true);
        }
        outputField(header.getSatelliteSystem().getKey(), 41);
        finishHeaderLine(CommonLabel.VERSION);

        // PGM / RUN BY / DATE
        writeProgramRunByDate(header);

        if (header.getFormatVersion() < 3.0) {

            // IONOSPHERIC CORR
            for (final IonosphericCorrection correction : header.getIonosphericCorrections()) {
                if (correction.getType() != IonosphericCorrectionType.GAL) {
                    // Rinex 2 only supports Klobuchar
                    final KlobucharIonosphericCorrection klobuchar = (KlobucharIonosphericCorrection) correction;
                    final double[] alpha = klobuchar.getKlobucharAlpha();
                    final double[] beta  = klobuchar.getKlobucharBeta();
                    outputField(' ', 2);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[0], 14);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[1], 26);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[2], 38);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[3], 50);
                    finishHeaderLine(NavigationLabel.ION_ALPHA);
                    outputField(' ', 2);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[0], 14);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[1], 26);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[2], 38);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[3], 50);
                    finishHeaderLine(NavigationLabel.ION_BETA);
                }
            }

            // TIME SYSTEM CORR
            for (final TimeSystemCorrection correction : header.getTimeSystemCorrections()) {
                if ("GPUT".equals(correction.getTimeSystemCorrectionType())) {
                    final GNSSDate date = new GNSSDate(correction.getReferenceDate(), SatelliteSystem.GPS);
                    outputField(' ', 3);
                    outputField(NINETEEN_SCIENTIFIC_FLOAT, correction.getTimeSystemCorrectionA0(), 22);
                    outputField(NINETEEN_SCIENTIFIC_FLOAT, correction.getTimeSystemCorrectionA1(), 41);
                    outputField(NINE_DIGITS_INTEGER, (int) FastMath.rint(date.getSecondsInWeek()), 50);
                    outputField(NINE_DIGITS_INTEGER, date.getWeekNumber(), 59);
                    finishHeaderLine(NavigationLabel.DELTA_UTC);
                } else {
                    final DateComponents dt = correction.getReferenceDate().
                                              getComponents(getTimeScales().getGLONASS()).
                                              getDate();
                    outputField(SIX_DIGITS_INTEGER, dt.getYear(),   6);
                    outputField(SIX_DIGITS_INTEGER, dt.getMonth(), 12);
                    outputField(SIX_DIGITS_INTEGER, dt.getDay(),   18);
                    outputField(' ', 21);
                    outputField(NINETEEN_SCIENTIFIC_FLOAT, correction.getTimeSystemCorrectionA0(), 40);
                    finishHeaderLine(NavigationLabel.CORR_TO_SYSTEM_TIME);
                }
            }

        } else if (header.getFormatVersion() < 4.0) {

            // IONOSPHERIC CORR
            for (final IonosphericCorrection correction : header.getIonosphericCorrections()) {
                if (correction.getType() == IonosphericCorrectionType.GAL) {
                    final NeQuickGIonosphericCorrection nequick = (NeQuickGIonosphericCorrection) correction;
                    final double[] alpha = nequick.getNeQuickAlpha();
                    outputField(nequick.getType().toString(), 5, true);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[0], 17);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[1], 29);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[2], 41);
                    outputField("", 53, true);
                    outputField(nequick.getTimeMark(), 54);
                    finishHeaderLine(NavigationLabel.IONOSPHERIC_CORR);
                } else {
                    final KlobucharIonosphericCorrection klobuchar = (KlobucharIonosphericCorrection) correction;
                    final double[] alpha = klobuchar.getKlobucharAlpha();
                    final double[] beta  = klobuchar.getKlobucharBeta();
                    outputField(klobuchar.getType().toString(), 3, true);
                    outputField("A ", 5, true);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[0], 17);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[1], 29);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[2], 41);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, alpha[3], 53);
                    outputField(klobuchar.getTimeMark(), 54);
                    finishHeaderLine(NavigationLabel.IONOSPHERIC_CORR);
                    outputField(klobuchar.getType().toString(), 3, true);
                    outputField("B ", 5, true);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[0], 17);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[1], 29);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[2], 41);
                    outputField(TWELVE_DIGITS_SCIENTIFIC, beta[3], 53);
                    outputField(klobuchar.getTimeMark(), 54);
                    finishHeaderLine(NavigationLabel.IONOSPHERIC_CORR);
                }
            }

            // TIME SYSTEM CORR
            for (final TimeSystemCorrection correction : header.getTimeSystemCorrections()) {
                final SatelliteSystem system = header.getSatelliteSystem() == SatelliteSystem.BEIDOU ?
                                               header.getSatelliteSystem() :
                                               SatelliteSystem.GPS;
                final GNSSDate date = correction.getReferenceDate()  == null ?
                                      new GNSSDate(0, 0, system) :
                                      new GNSSDate(correction.getReferenceDate(), system);
                outputField(correction.getTimeSystemCorrectionType(), 5, true);
                outputField(SEVENTEEN_DIGITS_SCIENTIFIC, correction.getTimeSystemCorrectionA0(), 22);
                outputField(SIXTEEN_DIGITS_SCIENTIFIC,   correction.getTimeSystemCorrectionA1(), 38);
                outputField(' ', 39);
                outputField(SIX_DIGITS_INTEGER, (int) FastMath.rint(date.getSecondsInWeek()), 45);
                outputField(' ', 46);
                outputField(FOUR_DIGITS_INTEGER, date.getWeekNumber(), 50);
                outputField(' ', 51);
                outputField(correction.getSatId(), 56, false);
                outputField(' ', 57);
                outputField(TWO_DIGITS_INTEGER, correction.getUtcId(), 59);
                finishHeaderLine(NavigationLabel.TIME_SYSTEM_CORR);
            }

        } else {

            // REC # / TYPE / VERS
            if (header.getReceiverNumber() != null) {
                outputField(header.getReceiverNumber(), 20, true);
                outputField(header.getReceiverType(), 40, true);
                outputField(header.getReceiverVersion(), 60, true);
                finishHeaderLine(ObservationLabel.REC_NB_TYPE_VERS);
            }

            // MERGED FILE
            if (header.getMergedFiles() > 0) {
                outputField(NINE_DIGITS_INTEGER, header.getMergedFiles(), 9);
                finishHeaderLine(NavigationLabel.MERGED_FILE);
            }

            // DOI
            writeHeaderLine(header.getDoi(), CommonLabel.DOI);

            // LICENSE OF USE
            writeHeaderLine(header.getLicense(), CommonLabel.LICENSE);

            // STATION INFORMATION
            writeHeaderLine(header.getStationInformation(), CommonLabel.STATION_INFORMATION);

        }

        // LEAP SECONDS
        if (header.getLeapSecondsGNSS() > 0) {
            outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsGNSS(), 6);
            if (header.getFormatVersion() > 3.0) {
                // extra fields introduced in 3.01
                outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsFuture(),  12);
                outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsWeekNum(), 18);
                outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsDayNum(),  24);
            }
            finishHeaderLine(CommonLabel.LEAP_SECONDS);
        }

        // END OF HEADER
        writeHeaderLine("", CommonLabel.END);

    }

    /** Write a date.
     * @param date date to write
     * @param system satellite system
    * @exception IOException if an I/O error occurs.
     */
    public void writeDate(final AbsoluteDate date, final SatelliteSystem system) throws IOException {
        writeDate(date.getComponents(timeScaleBuilder.apply(system, timeScales)));
    }

    /** Write a date.
     * <p>
     * The date will span over 23 characters.
     * </p>
     * @param dtc date to write
     * @exception IOException if an I/O error occurs.
     */
    public void writeDate(final DateTimeComponents dtc) throws IOException {
        final DateTimeComponents rounded = dtc.roundIfNeeded(60, 0);
        final int start = getColumn();
        outputField(BaseRinexWriter.FOUR_DIGITS_INTEGER, rounded.getDate().getYear(), start + 4);
        outputField(' ', start + 5);
        outputField(BaseRinexWriter.PADDED_TWO_DIGITS_INTEGER, rounded.getDate().getMonth(), start + 7);
        outputField(' ', start + 8);
        outputField(BaseRinexWriter.PADDED_TWO_DIGITS_INTEGER, rounded.getDate().getDay(), start + 10);
        outputField(' ', start + 11);
        outputField(BaseRinexWriter.PADDED_TWO_DIGITS_INTEGER, rounded.getTime().getHour(), start + 13);
        outputField(' ', start + 14);
        outputField(BaseRinexWriter.PADDED_TWO_DIGITS_INTEGER, rounded.getTime().getMinute(), start + 16);
        outputField(' ', start + 17);
        outputField(BaseRinexWriter.PADDED_TWO_DIGITS_INTEGER,
                    (int) FastMath.round(rounded.getTime().getSecond()), start + 19);
    }

    /** Write a double field.
     * <p>
     * The field will span over 19 characters.
     * </p>
     * @param value field value to write, in SI units
     * @param unit unit to use
     * @exception IOException if an I/O error occurs.
     */
    public void writeDouble(final double value, final Unit unit) throws IOException {
        outputField(BaseRinexWriter.NINETEEN_SCIENTIFIC_FLOAT, unit.fromSI(value), getColumn() + 19);
    }

    /** Write an integer field.
     * <p>
     * The field will span over 19 characters.
     * </p>
     * @param value field value to write, in SI units
     * @exception IOException if an I/O error occurs.
     */
    public void writeInt(final int value) throws IOException {
        outputField(BaseRinexWriter.NINETEEN_SCIENTIFIC_FLOAT, value, getColumn() + 19);
    }

    /** Write an empty field.
     * <p>
     * The field will span over 19 characters.
     * </p>
     * @exception IOException if an I/O error occurs.
     */
    public void writeEmpty() throws IOException {
        outputField("", getColumn() + 19, true);
    }

    /** Start (indent) a new line.
     * @param header header
     * @exception IOException if an I/O error occurs.
     */
    public void indentLine(final RinexNavigationHeader header)
        throws
        IOException {
        if (header.getFormatVersion() < 3.0) {
            outputField("   ",  3, true);
        } else {
            outputField("    ", 4, true);
        }
    }

    /** Container for navigation messages iterator.
     * @param <T> type of the navigation message
     */
    private class PendingMessages<T extends NavigationMessage> {

        /** Threshold to consider dates are equal. */
        private static final double EPS = 1.0e-9;

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

        /** Writer for the current message type. */
        private final NavigationMessageWriter<T> messageWriter;

        /** Navigation messages. */
        private final List<T> messages;

        /** Next entry to process. */
        private int index;

        /** Simple constructor.
         * @param identifier identifier
         * @param messageWriter writer for the current message type
         * @param messages navigation messages
         */
        PendingMessages(final String identifier, final NavigationMessageWriter<T> messageWriter,
                        final List<T> messages) {
            this.identifier    = identifier;
            this.messageWriter = messageWriter;
            this.messages      = messages;
            this.index         = 0;
        }

        /** Write next entry if close to processing date.
         * @param date processing date
         * @param header file header
         * @exception IOException if an I/O error occurs.
         */
        void writeMessageAtDate(final AbsoluteDate date, final RinexNavigationHeader header) throws IOException {
            if (index < messages.size() && FastMath.abs(date.durationFrom(messages.get(index))) <= EPS) {
                // write next entry and advance
                messageWriter.writeMessage(identifier, messages.get(index++), header, RinexNavigationWriter.this);
            }
        }

        /** Get date of next entry.
         * @return date of next entry
         */
        AbsoluteDate nextDate() {
            return index <  messages.size() ? messages.get(index).getDate() : AbsoluteDate.FUTURE_INFINITY;
        }

    }

}