RinexClockWriter.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.clock;

import org.hipparchus.util.FastMath;
import org.orekit.data.DataContext;
import org.orekit.files.rinex.AppliedDCBS;
import org.orekit.files.rinex.AppliedPCVS;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.utils.BaseRinexWriter;
import org.orekit.gnss.ObservationType;
import org.orekit.gnss.SatInSystem;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.utils.TimeSpanMap;
import org.orekit.utils.formatting.FastDecimalFormatter;
import org.orekit.utils.formatting.FastDoubleFormatter;
import org.orekit.utils.formatting.FastLongFormatter;
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;

/** Writer for Rinex clock file.
 * @author Luc Maisonobe
 * @since 14.0
 */
public class RinexClockWriter extends BaseRinexWriter<RinexClockHeader> {

    /** Millimeter unit. */
    private static final Unit MILLIMETER = Unit.parse("mm");

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

    /** Format for one 4.2 digits float field. */
    private static final FastDoubleFormatter FOUR_TWO_DIGITS_FLOAT = new FastDecimalFormatter(4, 2);

    /** Format for one 9.6 digits float field. */
    private static final FastDoubleFormatter NINE_SIX_DIGITS_FLOAT = new FastDecimalFormatter(9, 6);

    /** Format for one 10.6 digits float field. */
    private static final FastDoubleFormatter TEN_SIX_DIGITS_FLOAT = new FastDecimalFormatter(10, 6);

    /** Simple constructor.
     * <p>
     * This constructor uses the {@link DataContext#getDefault() default data context}
     * and recognizes only {@link org.orekit.gnss.PredefinedTimeSystem}.
     * </p>
     * @param output destination of generated output
     * @param outputName output name for error messages
     */
    public RinexClockWriter(final Appendable output, final String outputName) {
        super(output, outputName);
    }

    /** Write a complete clock file.
     * <p>
     * This method calls {@link #prepareComments(List)} and
     * {@link #writeHeader(RinexClockHeader)} once and then loops on
     * calling {@link #writeClockDataLine(ClockDataLine)}
     * for all data lines in the file
     * </p>
     * @param rinexClock Rinex clock file to write
     * @see #writeHeader(RinexClockHeader)
     * @see #writeClockDataLine(ClockDataLine)
     * @exception IOException if an I/O error occurs.
     */
    public void writeCompleteFile(final RinexClock rinexClock)
        throws IOException {
        prepareComments(rinexClock.getComments());
        writeHeader(rinexClock.getHeader());

        // prepare chronological iteration
        final List<PendingLines> pending = new ArrayList<>();
        for (final Map.Entry<String, List<ClockDataLine>> entry : rinexClock.getClockData().entrySet()) {
            pending.add(new PendingLines(entry.getKey(), entry.getValue()));
        }
        pending.sort(Comparator.comparing(pl -> pl.name));

        // write lines in chronological order
        for (AbsoluteDate date = earliest(pending); date.isFinite(); date = earliest(pending)) {
            // write all lines that correspond to this date
            for (final PendingLines pl : pending) {
                final ClockDataLine dataLine = pl.lineAtDate(date);
                if (dataLine != null) {
                    writeClockDataLine(dataLine);
                }
            }
        }

    }

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

    /** Write header.
     * <p>
     * This method must be called exactly once at the beginning
     * (directly or by {@link #writeCompleteFile(RinexClock)})
     * </p>
     * @param header header to write
     * @exception IOException if an I/O error occurs.
     */
    public void writeHeader(final RinexClockHeader header) throws IOException {
        super.writeHeader(header,
                          header.isBefore304() ?
                          RinexClockHeader.LABEL_INDEX_300_302 :
                          RinexClockHeader.LABEL_INDEX_304_PLUS);

        // RINEX VERSION / TYPE
        if (header.isBefore304()) {
            outputField(NINE_TWO_DIGITS_FLOAT, header.getFormatVersion(), 9);
            outputField("", 20, true);
            outputField("C", 40, true);
            outputField(header.getSatelliteSystem() == null ? ' ' : header.getSatelliteSystem().getKey(), 41);
        } else {
            outputField(FOUR_TWO_DIGITS_FLOAT, header.getFormatVersion(), 4);
            outputField("", 21, true);
            outputField("C", 42, true);
            outputField(header.getSatelliteSystem() == null ? ' ' : header.getSatelliteSystem().getKey(), 43);
        }
        finishHeaderLine(CommonLabel.VERSION);

        // PGM / RUN BY / DATE
        final DateTimeComponents dtc = header.getCreationDateComponents();
        if (header.isBefore304()) {
            outputField(header.getProgramName(), 20, true);
            outputField(header.getRunByName(), 40, true);
            if (dtc != null) {
                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, true);
            }
        } else {
            outputField(header.getProgramName(), 21, true);
            outputField(header.getRunByName(), 42, true);
            if (dtc != null) {
                outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 46);
                outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 48);
                outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 50);
                outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 54);
                outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 56);
                outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 58);
                outputField(header.getCreationTimeZone(), 63, true);
            }
        }
        finishHeaderLine(CommonLabel.PROGRAM);

        // SYS / # / OBS TYPES
        for (Map.Entry<SatelliteSystem, List<ObservationType>> entry : header.getSystemObservationTypes().entrySet()) {
            outputField(entry.getKey().getKey(), 1);
            outputField(' ', 3);
            outputField(THREE_DIGITS_INTEGER, entry.getValue().size(), 6);
            if (!header.isBefore304()) {
                outputField(' ', 8);
            }
            for (final ObservationType type : entry.getValue()) {
                int next = getColumn() + 4;
                if (exceedsHeaderLength(next)) {
                    // we need to set up a continuation line
                    finishHeaderLine(CommonLabel.SYS_NB_TYPES_OF_OBSERV);
                    outputField(' ', header.isBefore304() ? 6 : 8);
                    next = getColumn() + 4;
                }
                if (header.isBefore304()) {
                    outputField(' ', next - 3);
                    outputField(type.getName(), next, false);
                } else {
                    outputField(type.getName(), next - 1, false);
                    outputField(' ', next);
                }
            }
            finishHeaderLine(CommonLabel.SYS_NB_TYPES_OF_OBSERV);
        }

        // TIME SYSTEM ID
        if (header.getFormatVersion() > 2.0) {
            outputField(header.getTimeSystem().getKey(), 6, false);
            finishHeaderLine(ClockLabel.TIME_SYSTEM_ID);
        }

        // LEAP SECONDS
        if (header.getLeapSecondsTAI() > 0) {
            outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsTAI(), 6);
            finishHeaderLine(CommonLabel.LEAP_SECONDS);
        }

        // LEAP SECONDS GNSS
        if (!header.isBefore304() && header.getLeapSecondsGNSS() > 0) {
            outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsGNSS(), 6);
            finishHeaderLine(ClockLabel.LEAP_SECONDS_GNSS);
        }

        // SYS / DCBS APPLIED
        for (final AppliedDCBS appliedDCBS : header.getListAppliedDCBS()) {
            outputField(appliedDCBS.getSatelliteSystem().getKey(),  1);
            if (header.isBefore304()) {
                outputField(' ', 2);
                outputField(appliedDCBS.getProgDCBS(), 19, true);
                outputField(' ', 20);
                outputField(appliedDCBS.getSourceDCBS(), 60, true);
            } else {
                outputField(' ', 3);
                outputField(appliedDCBS.getProgDCBS(), 20, true);
                outputField(' ', 22);
                outputField(appliedDCBS.getSourceDCBS(), 65, true);
            }
            finishHeaderLine(CommonLabel.SYS_DCBS_APPLIED);
        }

        // SYS / PCVS APPLIED
        for (final AppliedPCVS appliedPCVS : header.getListAppliedPCVS()) {
            outputField(appliedPCVS.getSatelliteSystem().getKey(), 1);
            if (header.isBefore304()) {
                outputField(' ', 2);
                outputField(appliedPCVS.getProgPCVS(), 19, true);
                outputField(' ', 20);
                outputField(appliedPCVS.getSourcePCVS(), 60, true);
            } else {
                outputField(' ', 3);
                outputField(appliedPCVS.getProgPCVS(), 20, true);
                outputField(' ', 22);
                outputField(appliedPCVS.getSourcePCVS(), 65, true);
            }
            finishHeaderLine(CommonLabel.SYS_PCVS_APPLIED);
        }

        // # / TYPES OF DATA
        outputField(SIX_DIGITS_INTEGER, header.getClockDataTypes().size(), 6);
        for (final ClockDataType clockDataType : header.getClockDataTypes()) {
            outputField(clockDataType.name(), getColumn() + 6, false);
        }
        finishHeaderLine(ClockLabel.NB_TYPES_OF_DATA);

        // STATION NAME / NUM
        if (!header.getStationName().isEmpty()) {
            if (header.isBefore304()) {
                outputField(header.getStationName(), 4, true);
                outputField(' ', 5);
                outputField(header.getStationIdentifier(), 25, true);
            } else {
                outputField(header.getStationName(), 9, true);
                outputField(' ', 10);
                outputField(header.getStationIdentifier(), 30, true);
            }
            finishHeaderLine(ClockLabel.STATION_NAME_NUM);
        }

        // STATION CLK REF
        if (!header.getExternalClockReference().isEmpty()) {
            outputField(header.getExternalClockReference(), header.isBefore304() ? 60 : 65, true);
            finishHeaderLine(ClockLabel.STATION_CLK_REF);
        }

        // ANALYSIS CENTER
        if (!header.getAnalysisCenterName().isEmpty()) {
            outputField(header.getAnalysisCenterID(),   3, true);
            outputField("", 5, true);
            outputField(header.getAnalysisCenterName(), header.isBefore304() ? 60 : 65, true);
            finishHeaderLine(ClockLabel.ANALYSIS_CENTER);
        }

        // # OF CLK REF / ANALYSIS CLK REF
        for (TimeSpanMap.Span<List<ReferenceClock>> span = header.getReferenceClocks().getFirstSpan();
             span != null;
             span = span.next()) {
            if (span.getData() != null) {
                outputField(SIX_DIGITS_INTEGER, span.getData().size(), 6);
                if (span.getStart().isFinite()) {
                    outputField(' ', 7);
                    final DateTimeComponents startDtc = span.getStart().getComponents(header.getTimeScale());
                    final DateTimeComponents endDtc   = span.getEnd().getComponents(header.getTimeScale());
                    outputField(FOUR_DIGITS_INTEGER, startDtc.getDate().getYear(), 11);
                    outputField(' ', 12);
                    outputField(PADDED_TWO_DIGITS_INTEGER, startDtc.getDate().getMonth(), 14);
                    outputField(' ', 15);
                    outputField(PADDED_TWO_DIGITS_INTEGER, startDtc.getDate().getDay(), 17);
                    outputField(' ', 18);
                    outputField(PADDED_TWO_DIGITS_INTEGER, startDtc.getTime().getHour(), 20);
                    outputField(' ', 21);
                    outputField(PADDED_TWO_DIGITS_INTEGER, startDtc.getTime().getMinute(), 23);
                    if (header.isBefore304()) {
                        outputField(TEN_SIX_DIGITS_FLOAT, startDtc.getTime().getSecond(), 33);
                        outputField(' ', 34);
                        outputField(FOUR_DIGITS_INTEGER, endDtc.getDate().getYear(), 38);
                        outputField(' ', 39);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getDate().getMonth(), 41);
                        outputField(' ', 42);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getDate().getDay(), 44);
                        outputField(' ', 45);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getTime().getHour(), 47);
                        outputField(' ', 48);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getTime().getMinute(), 50);
                        outputField(TEN_SIX_DIGITS_FLOAT, endDtc.getTime().getSecond(), 60);
                    } else {
                        outputField(' ', 24);
                        outputField(TEN_SIX_DIGITS_FLOAT, startDtc.getTime().getSecond(), 34);
                        outputField(' ', 36);
                        outputField(FOUR_DIGITS_INTEGER, endDtc.getDate().getYear(), 40);
                        outputField(' ', 41);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getDate().getMonth(), 43);
                        outputField(' ', 44);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getDate().getDay(), 46);
                        outputField(' ', 47);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getTime().getHour(), 49);
                        outputField(' ', 50);
                        outputField(PADDED_TWO_DIGITS_INTEGER, endDtc.getTime().getMinute(), 52);
                        outputField(' ', 53);
                        outputField(TEN_SIX_DIGITS_FLOAT, endDtc.getTime().getSecond(), 63);
                    }
                }
                finishHeaderLine(ClockLabel.NB_OF_CLK_REF);
                for (final ReferenceClock clock : span.getData()) {
                    if (header.isBefore304()) {
                        outputField(clock.getReferenceName(), 4, true);
                        outputField(' ', 5);
                        outputField(clock.getClockID(), 25, true);
                        outputField(' ', 40);
                    } else {
                        outputField(clock.getReferenceName(), 9, true);
                        outputField(' ', 10);
                        outputField(clock.getClockID(), 30, true);
                        outputField(' ', 45);
                    }
                    if (clock.getClockConstraint() != 0.0) {
                        outputField(NINETEEN_SCIENTIFIC_FLOAT, clock.getClockConstraint(), getColumn() + 19);
                    }
                    finishHeaderLine(ClockLabel.ANALYSIS_CLK_REF);
                }
            }
        }

        // # OF SOLN STA / TRF
        if (header.getNumberOfReceivers() > 0) {
            outputField(SIX_DIGITS_INTEGER, header.getNumberOfReceivers(), 6);
            outputField("", 10, true);
            outputField(header.getFrameName(), header.isBefore304() ? 60 : 65, true);
            finishHeaderLine(ClockLabel.NB_OF_SOLN_STA_TRF);
        }

        // SOLN STA NAME / NUM
        for (final Receiver receiver : header.getReceivers()) {
            if (header.isBefore304()) {
                outputField(receiver.getDesignator(), 4, true);
                outputField(' ', 5);
                outputField(receiver.getReceiverIdentifier(), 25, true);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getX())), 36);
                outputField(' ', 37);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getY())), 48);
                outputField(' ', 49);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getZ())), 60);
            } else {
                outputField(receiver.getDesignator(), 9, true);
                outputField(' ', 10);
                outputField(receiver.getReceiverIdentifier(), 30, true);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getX())), 41);
                outputField(' ', 42);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getY())), 53);
                outputField(' ', 54);
                outputField(ELEVEN_DIGITS_INTEGER, FastMath.round(MILLIMETER.fromSI(receiver.getZ())), 65);
            }
            finishHeaderLine(ClockLabel.SOLN_STA_NAME_NUM);
        }

        // # OF SOLN SATS
        if (header.getNumberOfSatellites() > 0) {
            outputField(SIX_DIGITS_INTEGER, header.getNumberOfSatellites(), 6);
            finishHeaderLine(ClockLabel.NB_OF_SOLN_SATS);
        }

        // PRN LIST
        boolean wrotePRN = false;
        for (final SatInSystem satInSystem : header.getSatellites()) {
            int next = getColumn() + 4;
            if (exceedsHeaderLength(next)) {
                // we need to set up a continuation line
                finishHeaderLine(ClockLabel.PRN_LIST);
                next = 4;
            }
            outputField(satInSystem.toString(), next, true);
            wrotePRN = true;
        }
        if (wrotePRN) {
            finishHeaderLine(ClockLabel.PRN_LIST);
        }

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

    }

    /** Write a clock data line.
     * @param clockDataLine clock data line to write
     * @exception IOException if an I/O error occurs.
     */
    public void writeClockDataLine(final ClockDataLine clockDataLine) throws IOException {
        checkHeaderWritten();
        if (getHeader().isBefore304()) {
            outputField(clockDataLine.getDataType().name(), 2, true);
            outputField(' ', 3);
            outputField(clockDataLine.getName(), 7, true);
            outputField(' ', 8);
            final DateTimeComponents epoch = clockDataLine.getDate().getComponents(getHeader().getTimeScale());
            outputField(FOUR_DIGITS_INTEGER, epoch.getDate().getYear(), 12);
            outputField(' ', 13);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getDate().getMonth(), 15);
            outputField(' ', 16);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getDate().getDay(), 18);
            outputField(' ', 19);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getTime().getHour(), 21);
            outputField(' ', 22);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getTime().getMinute(), 24);
            outputField(TEN_SIX_DIGITS_FLOAT, epoch.getTime().getSecond(), 34);
            outputField(THREE_DIGITS_INTEGER, clockDataLine.getNumberOfValues(), 37);
            outputField(' ', 40);
            outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockBias(), 59);
            if (clockDataLine.getNumberOfValues() > 1) {
                outputField(' ', 60);
                outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockBiasSigma(), 79);
                if (clockDataLine.getNumberOfValues() > 2) {
                    finishLine();
                    outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockRate(), 19);
                    if (clockDataLine.getNumberOfValues() > 3) {
                        outputField(' ', 20);
                        outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockRateSigma(), 39);
                        if (clockDataLine.getNumberOfValues() > 4) {
                            outputField(' ', 40);
                            outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockAcceleration(), 59);
                            if (clockDataLine.getNumberOfValues() > 5) {
                                outputField(' ', 60);
                                outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockAccelerationSigma(), 79);
                            }
                        }
                    }
                }
            }
        } else {
            outputField(clockDataLine.getDataType().name(), 2, true);
            outputField(' ', 3);
            outputField(clockDataLine.getName(), 12, true);
            outputField(' ', 13);
            final DateTimeComponents epoch = clockDataLine.getDate().getComponents(getHeader().getTimeScale());
            outputField(FOUR_DIGITS_INTEGER, epoch.getDate().getYear(), 17);
            outputField(' ', 18);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getDate().getMonth(), 20);
            outputField(' ', 21);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getDate().getDay(), 23);
            outputField(' ', 24);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getTime().getHour(), 26);
            outputField(' ', 27);
            outputField(PADDED_TWO_DIGITS_INTEGER, epoch.getTime().getMinute(), 29);
            outputField(' ', 30);
            outputField(NINE_SIX_DIGITS_FLOAT, epoch.getTime().getSecond(), 39);
            outputField(' ', 40);
            outputField(TWO_DIGITS_INTEGER, clockDataLine.getNumberOfValues(), 42);
            outputField(' ', 45);
            outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockBias(), 64);
            if (clockDataLine.getNumberOfValues() > 1) {
                outputField(' ', 66);
                outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockBiasSigma(), 85);
                if (clockDataLine.getNumberOfValues() > 2) {
                    finishLine();
                    outputField(' ', 3);
                    outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockRate(), 22);
                    if (clockDataLine.getNumberOfValues() > 3) {
                        outputField(' ', 24);
                        outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockRateSigma(), 43);
                        if (clockDataLine.getNumberOfValues() > 4) {
                            outputField(' ', 45);
                            outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockAcceleration(), 64);
                            if (clockDataLine.getNumberOfValues() > 5) {
                                outputField(' ', 66);
                                outputField(NINETEEN_SCIENTIFIC_FLOAT, clockDataLine.getClockAccelerationSigma(), 85);
                            }
                        }
                    }
                }
            }
        }
        finishLine();
    }

    /** Container for clock data lines iterator. */
    private static class PendingLines {

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

        /** Clock name. */
        private final String name;

        /** Data lines. */
        private final List<ClockDataLine> lines;

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

        /** Simple constructor.
         * @param name clock name
         * @param lines data lines
         */
        PendingLines(final String name, final List<ClockDataLine> lines) {
            this.name  = name;
            this.lines = lines;
            this.index = 0;
        }

        /** Get next entry if close to processing date.
         * @param date processing date
         * @return next entry if close to processing date, null otherwise
         */
        ClockDataLine lineAtDate(final AbsoluteDate date) {
            if (index < lines.size() && FastMath.abs(date.durationFrom(lines.get(index))) <= EPS) {
                // return next entry and advance
                return lines.get(index++);
            } else {
                // don't touch the index
                return null;
            }
        }

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

    }

}