CRDParser.java

/* Copyright 2002-2023 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.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ilrs.CRD.AnglesMeasurement;
import org.orekit.files.ilrs.CRD.CRDDataBlock;
import org.orekit.files.ilrs.CRD.Calibration;
import org.orekit.files.ilrs.CRD.CalibrationDetail;
import org.orekit.files.ilrs.CRD.FrRangeMeasurement;
import org.orekit.files.ilrs.CRD.MeteorologicalMeasurement;
import org.orekit.files.ilrs.CRD.NptRangeMeasurement;
import org.orekit.files.ilrs.CRD.RangeMeasurement;
import org.orekit.files.ilrs.CRD.RangeSupplement;
import org.orekit.files.ilrs.CRD.SessionStatistics;
import org.orekit.files.ilrs.CRDConfiguration.CalibrationTargetConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.DetectorConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.LaserConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.MeteorologicalConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.SoftwareConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.SystemConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.TimingSystemConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.TransponderConfiguration;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.Constants;
import org.orekit.utils.units.Unit;
import org.orekit.utils.units.UnitsConverter;

/**
 * A parser for the CRD data file format.
 * <p>
 * It supports both 1.0 and 2.0 versions
 * <p>
 * <b>Note</b>: Not all the records are read by the parser. Only the most significants are parsed.
 * Contributions are welcome to support more fields in the format.
 * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2009/crd_v1.01.pdf">1.0 file format</a>
 * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2021/crd_v2.01e2.pdf">2.0 file format</a>
 * @author Bryan Cazabonne
 * @author Rongwang Li
 * @since 10.3
 */
public class CRDParser {

    /** Default supported files name pattern for CRD files. */
    public static final String DEFAULT_CRD_SUPPORTED_NAMES = "^(?!0+$)\\w{1,12}\\_\\d{6,8}.\\w{3}$";

    /** Nanometers units. */
    private static final Unit NM = Unit.parse("nm");

    /** Kilohertz units. */
    private static final Unit KHZ = Unit.parse("kHz");

    /** Microseconds units. */
    private static final Unit US = Unit.parse("µs");

    /** Nanoseconds units. */
    private static final Unit NS = Unit.parse("ns");

    /** Picoseconds units. */
    private static final Unit PS = Unit.parse("ps");

    /** mbar to bar converter. */
    private static final UnitsConverter MBAR_TO_BAR = new UnitsConverter(Unit.parse("mbar"), Unit.parse("bar"));

    /** File format. */
    private static final String FILE_FORMAT = "CRD";

    /** Pattern for delimiting regular expressions. */
    private static final Pattern SEPARATOR = Pattern.compile("\\s+");

    /** Pattern for delimiting expressions with comma. */
    private static final Pattern COMMA = Pattern.compile(",");

    /** Identifier of comment record. */
    private static final String COMMENTS_IDENTIFIER = "00";

    /** Pattern of " [-]?(na)". */
    private static final Pattern PATTERN_NA = Pattern.compile(" [-]?(na)");

    /** Time scale used to define epochs in CPF file. */
    private final TimeScale timeScale;

    /**
     * Default constructor.
     * <p>
     * This constructor uses the {@link DataContext#getDefault() default data context}.
     */
    @DefaultDataContext
    public CRDParser() {
        this(DataContext.getDefault().getTimeScales().getUTC());
    }

    /**
     * Constructor.
     * @param utc utc time scale to read epochs
     */
    public CRDParser(final TimeScale utc) {
        this.timeScale = utc;
    }

    /**
     * Get the time scale used to read the file.
     * @return the time scale used to read the file
     */
    public TimeScale getTimeScale() {
        return timeScale;
    }

    /**
     * Parse a CRD file.
     * @param source data source containing the CRD file.
     * @return a parsed CRD file.
     * @throws IOException if {@code reader} throws one.
     */
    public CRD parse(final DataSource source) throws IOException {

        // Initialize internal data structures
        final ParseInfo pi = new ParseInfo();

        int lineNumber = 0;
        Iterable<LineParser> crdParsers = Collections.singleton(LineParser.H1);
        try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
            nextLine:
                for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                    ++lineNumber;

                    if (line.startsWith(COMMENTS_IDENTIFIER)) {
                        // Comment is in the beginning of the file.
                        crdParsers = Arrays.asList(LineParser.COMMENTS);
                    }

                    for (final LineParser candidate : crdParsers) {
                        if (candidate.canHandle(line)) {
                            try {

                                // Note: since crd v2.01.
                                // The literal “na” is used instead of “-1” for fields that are not applicable or not avaiable.
                                // And there may be "-na".
                                // note: "analog" --> "aNaNlog"
                                line = PATTERN_NA.matcher(line).replaceAll(" " + CRD.STR_NAN);

                                candidate.parse(line, pi);
                                if (pi.done) {
                                    // Return file
                                    return pi.file;
                                }
                                crdParsers = candidate.allowedNext();
                                continue nextLine;
                            } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
                                throw new OrekitException(e,
                                                          OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                                          lineNumber, source.getName(), line);
                            }
                        }
                    }
                }

            // We never reached the EOF marker
            throw new OrekitException(OrekitMessages.CRD_UNEXPECTED_END_OF_FILE, lineNumber);

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

    }

    /**
     * Make sure the epoch is 'right' by doing a day shift if it is required by comparing the current and session start epoch.
     * According to the CRD document, the duration of a session must be less than one day.
     * @param epoch current epoch
     * @param startEpoch start epoch of session
     * @return epoch with rollover is handled.
     */
    private static AbsoluteDate checkRollover(final AbsoluteDate epoch, final AbsoluteDate startEpoch) {
        // If the current epoch is before the start epoch of a session, the epoch should be shifted by 1 day.
        // For METEO(20) data, the epoch may be a 'little' (10 hours?) before the session start epoch.
        // And also for CALIB(40) and CALIB_DETAILS(41)
        return epoch.durationFrom(startEpoch) < -36000 ? epoch.shiftedBy(Constants.JULIAN_DAY) : epoch;
    }

    /** Transient data used for parsing a CRD file. The data is kept in a
     * separate data structure to make the parser thread-safe.
     * <p><b>Note</b>: The class intentionally does not provide accessor
     * methods, as it is only used internally for parsing a CRD file.</p>
     */
    private class ParseInfo {

        /** The corresponding CDR file. */
        private CRD file;

        /** Version. */
        private int version;

        /** The current data block. */
        private CRDDataBlock dataBlock;

        /** Data block header. */
        private CRDHeader header;

        /** Cofiguration records. */
        private CRDConfiguration configurationRecords;

        /** Time scale. */
        private TimeScale timeScale;

        /** Current data block start epoch, DateComponents only. */
        private DateComponents startEpochDateComponents;

        /** End Of File reached indicator. */
        private boolean done;

        /**
         * Constructor.
         */
        protected ParseInfo() {

            // Initialise default values
            this.done       = false;
            this.version    = 1;
            this.startEpochDateComponents = DateComponents.J2000_EPOCH;

            // Initialise empty object
            this.file                 = new CRD();
            this.header               = new CRDHeader();
            this.configurationRecords = new CRDConfiguration();
            this.dataBlock            = new CRDDataBlock();

            // Time scale
            this.timeScale = CRDParser.this.timeScale;

        }

    }

    /** Parsers for specific lines. */
    private enum LineParser {

        /** Format header. */
        H1("H1", "h1") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Format and version
                final String format = values[1];
                pi.version = Integer.parseInt(values[2]);

                // Throw an exception if format is not equal to "CRD"
                if (!format.equalsIgnoreCase(FILE_FORMAT)) {
                    throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
                }

                // Fill first elements
                pi.header.setFormat(format);
                pi.header.setVersion(pi.version);

                // Epoch of ephemeris production
                final int year  = Integer.parseInt(values[3]);
                final int month = Integer.parseInt(values[4]);
                final int day   = Integer.parseInt(values[5]);
                pi.header.setProductionEpoch(new DateComponents(year, month, day));

                // Hour of ephemeris production
                pi.header.setProductionHour(Integer.parseInt(values[6]));

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H2, COMMENTS);
            }

        },

        /** Station header. */
        H2("H2", "h2") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Station name
                pi.header.setStationName(values[1]);

                // Crustal Dynamics Project keys
                pi.header.setSystemIdentifier(Integer.parseInt(values[2]));
                pi.header.setSystemNumber(Integer.parseInt(values[3]));
                pi.header.setSystemOccupancy(Integer.parseInt(values[4]));

                // Station epoch time scale
                pi.header.setEpochIdentifier(Integer.parseInt(values[5]));

                // Station network
                if (pi.version == 2) {
                    pi.header.setStationNetword(values[6]);
                } else {
                    pi.header.setStationNetword(CRD.STR_VALUE_NOT_AVAILABLE);
                }

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
            }

        },

        /** Target header. */
        H3("H3", "h3") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Target name
                pi.header.setName(values[1]);

                // Identifiers
                pi.header.setIlrsSatelliteId(values[2]);
                pi.header.setSic(values[3]);
                pi.header.setNoradId(values[4]);

                // Spacecraft Epoch Time Scale
                pi.header.setSpacecraftEpochTimeScale(Integer.parseInt(values[5]));

                // Target class and location (if needed)
                pi.header.setTargetClass(Integer.parseInt(values[6]));
                if (pi.version == 2) {
                    // na=unknown (for use when tracking a transponder using a Version 1 CPF)
                    // treated it as -1
                    pi.header.setTargetLocation(readIntegerWithNaN(values[7], -1));
                }

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H4, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
            }

        },

        /** Session (Pass/Pass segment) header. */
        H4("H4", "h4") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Data type
                pi.header.setDataType(Integer.parseInt(values[1]));

                // Start epoch
                final int    yearS   = Integer.parseInt(values[2]);
                final int    monthS  = Integer.parseInt(values[3]);
                final int    dayS    = Integer.parseInt(values[4]);
                final int    hourS   = Integer.parseInt(values[5]);
                final int    minuteS = Integer.parseInt(values[6]);
                final double secondS = Integer.parseInt(values[7]);

                pi.startEpochDateComponents = new DateComponents(yearS, monthS, dayS);

                pi.header.setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
                        hourS, minuteS, secondS,
                        pi.timeScale));

                // End epoch
                // since crd v2.01
                // Set the ending date and time fields to “na” if not available.
                if (pi.version == 2 && values[8].equalsIgnoreCase("")) {
                    pi.header.setEndEpoch(null);
                } else {
                    final int yearE = Integer.parseInt(values[8]);
                    final int monthE = Integer.parseInt(values[9]);
                    final int dayE = Integer.parseInt(values[10]);
                    final int hourE = Integer.parseInt(values[11]);
                    final int minuteE = Integer.parseInt(values[12]);
                    final double secondE = Integer.parseInt(values[13]);

                    // fixed 2022-12-12
                    // if yearE or monthE is -1.
                    if (monthE == -1) {
                        pi.header.setEndEpoch(null);
                    } else {
                        pi.header.setEndEpoch(new AbsoluteDate(yearE, monthE, dayE, hourE, minuteE, secondE, pi.timeScale));
                    }
                }

                // Data release
                pi.header.setDataReleaseFlag(Integer.parseInt(values[14]));

                // Correction flags
                pi.header.setIsTroposphericRefractionApplied(readBoolean(values[15]));
                pi.header.setIsCenterOfMassCorrectionApplied(readBoolean(values[16]));
                pi.header.setIsReceiveAmplitudeCorrectionApplied(readBoolean(values[17]));
                pi.header.setIsStationSystemDelayApplied(readBoolean(values[18]));
                pi.header.setIsTransponderDelayApplied(readBoolean(values[19]));

                // Range type indicator
                pi.header.setRangeType(Integer.parseInt(values[20]));

                // Data quality indicator
                pi.header.setQualityIndicator(Integer.parseInt(values[21]));

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES,
                                     CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Prediction header. */
        H5("H5", "h5") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill data
                pi.header.setPredictionType(Integer.parseInt(values[1]));
                pi.header.setYearOfCentury(Integer.parseInt(values[2]));
                pi.header.setDateAndTime(values[3]);
                pi.header.setPredictionProvider(values[4]);
                pi.header.setSequenceNumber(Integer.parseInt(values[5]));

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB,
                                     CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** System configuration record. */
        C0("C0", "c0") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty system configuration record
                final SystemConfiguration systemRecord = new SystemConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Wavelength
                systemRecord.setWavelength(NM.toSI(Double.parseDouble(values[2])));

                // System ID
                systemRecord.setSystemId(values[3]);

                // Components, A B C D E F G
                systemRecord.setComponents(Arrays.copyOfRange(values, 4, values.length));

                // Add the system configuration record
                pi.configurationRecords.addConfigurationRecord(systemRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },


        /** Laser configuration record. */
        C1("C1", "c1") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty laser configuration record
                final LaserConfiguration laserRecord = new LaserConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                laserRecord.setLaserId(values[2]);
                laserRecord.setLaserType(values[3]);
                laserRecord.setPrimaryWavelength(NM.toSI(Double.parseDouble(values[4])));
                laserRecord.setNominalFireRate(Double.parseDouble(values[5]));
                laserRecord.setPulseEnergy(Double.parseDouble(values[6]));
                laserRecord.setPulseWidth(Double.parseDouble(values[7]));
                laserRecord.setBeamDivergence(Double.parseDouble(values[8]));
                laserRecord.setPulseInOutgoingSemiTrain(readIntegerWithNaN(values[9], 1));

                // Add the laser configuration record
                pi.configurationRecords.addConfigurationRecord(laserRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(C2, C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
            }

        },

        /** Detector configuration record. */
        C2("C2", "c2") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty detector configuration record
                final DetectorConfiguration detectorRecord = new DetectorConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                detectorRecord.setDetectorId(values[2]);
                detectorRecord.setDetectorType(values[3]);
                detectorRecord.setApplicableWavelength(NM.toSI(Double.parseDouble(values[4])));
                detectorRecord.setQuantumEfficiency(Double.parseDouble(values[5]));
                detectorRecord.setAppliedVoltage(Double.parseDouble(values[6]));
                detectorRecord.setDarkCount(KHZ.toSI(Double.parseDouble(values[7])));
                detectorRecord.setOutputPulseType(values[8]);
                detectorRecord.setOutputPulseWidth(Double.parseDouble(values[9]));
                detectorRecord.setSpectralFilter(NM.toSI(Double.parseDouble(values[10])));
                detectorRecord.setTransmissionOfSpectralFilter(Double.parseDouble(values[11]));
                detectorRecord.setSpatialFilter(Double.parseDouble(values[12]));
                detectorRecord.setExternalSignalProcessing(values[13]);

                // Check file version for additional data
                if (pi.version == 2) {
                    detectorRecord.setAmplifierGain(Double.parseDouble(values[14]));
                    detectorRecord.setAmplifierBandwidth(KHZ.toSI(Double.parseDouble(values[15])));
                    detectorRecord.setAmplifierInUse(values[16]);
                } else {
                    detectorRecord.setAmplifierGain(Double.NaN);
                    detectorRecord.setAmplifierBandwidth(Double.NaN);
                    detectorRecord.setAmplifierInUse(CRD.STR_VALUE_NOT_AVAILABLE);
                }

                // Add the detector configuration record
                pi.configurationRecords.addConfigurationRecord(detectorRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Timing system configuration record. */
        C3("C3", "c3") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty timing system configuration record
                final TimingSystemConfiguration timingRecord = new TimingSystemConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                timingRecord.setLocalTimingId(values[2]);
                timingRecord.setTimeSource(values[3]);
                timingRecord.setFrequencySource(values[4]);
                timingRecord.setTimer(values[5]);
                final String timerSerialNumber = values[6];
                if (CRD.STR_NAN.equalsIgnoreCase(timerSerialNumber)) {
                    // The timer serial number may be "na"
                    timingRecord.setTimerSerialNumber(CRD.STR_VALUE_NOT_AVAILABLE);
                } else {
                    timingRecord.setTimerSerialNumber(timerSerialNumber);
                }
                timingRecord.setEpochDelayCorrection(US.toSI(Double.parseDouble(values[7])));

                // Add the timing system configuration record
                pi.configurationRecords.addConfigurationRecord(timingRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Transponder configuration record. */
        C4("C4", "c4") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty transponder configuration record
                final TransponderConfiguration transponderRecord = new TransponderConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Estimated offsets and drifts
                transponderRecord.setTransponderId(values[2]);
                transponderRecord.setStationUTCOffset(NS.toSI(Double.parseDouble(values[3])));
                transponderRecord.setStationOscDrift(Double.parseDouble(values[4]));
                transponderRecord.setTranspUTCOffset(NS.toSI(Double.parseDouble(values[5])));
                transponderRecord.setTranspOscDrift(Double.parseDouble(values[6]));

                // Transponder clock reference time
                transponderRecord.setTranspClkRefTime(Double.parseDouble(values[7]));

                // Clock and drift indicators
                transponderRecord.setStationClockAndDriftApplied(Integer.parseInt(values[8]));
                transponderRecord.setSpacecraftClockAndDriftApplied(Integer.parseInt(values[9]));

                // Spacecraft time simplified
                transponderRecord.setIsSpacecraftTimeSimplified(readBoolean(values[10]));

                // Add the transponder configuration record
                pi.configurationRecords.addConfigurationRecord(transponderRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Software configuration record. */
        C5("C5", "c5") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty software configuration record
                final SoftwareConfiguration softwareRecord = new SoftwareConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                softwareRecord.setSoftwareId(values[2]);
                softwareRecord.setTrackingSoftwares(COMMA.split(values[3]));
                softwareRecord.setTrackingSoftwareVersions(COMMA.split(values[4]));
                softwareRecord.setProcessingSoftwares(COMMA.split(values[5]));
                softwareRecord.setProcessingSoftwareVersions(COMMA.split(values[6]));

                // Add the software configuration record
                pi.configurationRecords.addConfigurationRecord(softwareRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Meteorological instrumentation configuration record. */
        C6("C6", "c6") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty meteorological configuration record
                final MeteorologicalConfiguration meteoRecord = new MeteorologicalConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                meteoRecord.setMeteorologicalId(values[2]);
                meteoRecord.setPressSensorManufacturer(values[3]);
                meteoRecord.setPressSensorModel(values[4]);
                meteoRecord.setPressSensorSerialNumber(values[5]);
                meteoRecord.setTempSensorManufacturer(values[6]);
                meteoRecord.setTempSensorModel(values[7]);
                meteoRecord.setTempSensorSerialNumber(values[8]);
                meteoRecord.setHumiSensorManufacturer(values[9]);
                meteoRecord.setHumiSensorModel(values[10]);
                meteoRecord.setHumiSensorSerialNumber(values[11]);

                // Add the meteorological configuration record
                pi.configurationRecords.addConfigurationRecord(meteoRecord);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Calibration Target configuration record. */
        C7("C7", "c7") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Initialise an empty calibration target configuration record
                final CalibrationTargetConfiguration calibRecord = new CalibrationTargetConfiguration();

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Fill values
                calibRecord.setConfigurationId(values[2]);
                calibRecord.setTargetName(values[3]);
                calibRecord.setSurveyedTargetDistance(Double.parseDouble(values[4]));
                calibRecord.setSurveyError(Double.parseDouble(values[5]) * 1e-3);  // mm --> m
                calibRecord.setSumOfAllConstantDelays(Double.parseDouble(values[6]));
                calibRecord.setPulseEnergy(Double.parseDouble(values[7]));
                calibRecord.setProcessingSoftwareName(values[8]);
                calibRecord.setProcessingSoftwareVersion(values[9]);

                // Add the calibration target configuration record
                pi.configurationRecords.addConfigurationRecord(calibRecord);
            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
                                     ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Range Record (Full rate, Sampled Engineering/Quicklook). */
        TEN("10") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double secOfDay         = Double.parseDouble(values[1]);
                final double timeOfFlight     = Double.parseDouble(values[2]);
                final String systemConfigId   = values[3];
                final int    epochEvent       = Integer.parseInt(values[4]);
                final int    filterFlag       = Integer.parseInt(values[5]);
                final int    detectorChannel  = Integer.parseInt(values[6]);
                final int    stopNumber       = Integer.parseInt(values[7]);
                final int    receiveAmplitude = readIntegerWithNaN(values[8], -1);

                int transmitAmplitude = -1;
                if (pi.version == 2) {
                    transmitAmplitude = readIntegerWithNaN(values[9], -1);
                }

                // Initialise a new Range measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final RangeMeasurement range = new FrRangeMeasurement(epoch, timeOfFlight, epochEvent, systemConfigId,
                        filterFlag, detectorChannel, stopNumber, receiveAmplitude, transmitAmplitude);
                pi.dataBlock.addRangeData(range);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
                                     COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Range Record (Normal point). */
        ELEVEN("11") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double   secOfDay          = Double.parseDouble(values[1]);
                final double   timeOfFlight      = Double.parseDouble(values[2]);
                final String   systemConfigId    = values[3];
                final int      epochEvent        = Integer.parseInt(values[4]);
                final double   windowLength      = Double.parseDouble(values[5]);
                final int      numberOfRawRanges = Integer.parseInt(values[6]);
                final double   binRms            = PS.toSI(Double.parseDouble(values[7]));
                final double   binSkew           = Double.parseDouble(values[8]);
                final double   binKurtosis       = Double.parseDouble(values[9]);
                final double   binPeakMinusMean  = PS.toSI(Double.parseDouble(values[10]));
                final double   returnRate        = Double.parseDouble(values[11]);
                final int      detectorChannel   = Integer.parseInt(values[12]);

                double snr = Double.NaN;
                if (pi.version == 2) {
                    snr    = Double.parseDouble(values[13]);
                }

                // Initialise a new Range measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final RangeMeasurement range = new NptRangeMeasurement(epoch, timeOfFlight, epochEvent, snr,
                        systemConfigId, windowLength, numberOfRawRanges, binRms, binSkew, binKurtosis, binPeakMinusMean,
                        returnRate, detectorChannel);
                pi.dataBlock.addRangeData(range);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
                                     COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Range Supplement Record. */
        TWELVE("12") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double   secOfDay                   = Double.parseDouble(values[1]);
                final String   systemConfigId             = values[2];
                final double   troposphericRefractionCorr = PS.toSI(Double.parseDouble(values[3]));
                final double   centerOfMassCorr           = Double.parseDouble(values[4]);
                final double   ndFilterValue              = Double.parseDouble(values[5]);
                final double   timeBiasApplied            = Double.parseDouble(values[6]);

                double rangeRate = Double.NaN;
                if (pi.version == 2) {
                    rangeRate    = Double.parseDouble(values[7]);
                }

                // Initialise a new Range measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final RangeSupplement rangeSup = new RangeSupplement(epoch, systemConfigId, troposphericRefractionCorr,
                        centerOfMassCorr, ndFilterValue, timeBiasApplied, rangeRate);
                pi.dataBlock.addRangeSupplementData(rangeSup);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
            }

        },

        /** Meteorological record. */
        METEO("20") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double   secOfDay       = Double.parseDouble(values[1]);
                final double   pressure       = MBAR_TO_BAR.convert(Double.parseDouble(values[2]));
                final double   temperature    = Double.parseDouble(values[3]);
                final double   humidity       = Double.parseDouble(values[4]);
                final int      originOfValues = Integer.parseInt(values[5]);

                // Initialise a new Range measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final MeteorologicalMeasurement meteo = new MeteorologicalMeasurement(epoch, pressure, temperature,
                        humidity, originOfValues);
                pi.dataBlock.addMeteoData(meteo);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Meteorological Supplement record. */
        METEO_SUPP("21") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {
                // Not implemented yet
            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Pointing Angle Record. */
        ANGLES("30") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double  secOfDay              = Double.parseDouble(values[1]);
                final double  azmiuth               = FastMath.toRadians(Double.parseDouble(values[2]));
                final double  elevation             = FastMath.toRadians(Double.parseDouble(values[3]));
                final int     directionFlag         = Integer.parseInt(values[4]);
                final int     orginFlag             = Integer.parseInt(values[5]);
                final boolean isRefractionCorrected = readBoolean(values[6]);


                // Angles rates
                double azimuthRate   = Double.NaN;
                double elevationRate = Double.NaN;
                if (pi.version == 2) {
                    // degrees/second ==> rad/s
                    azimuthRate   = FastMath.toRadians(Double.parseDouble(values[7]));
                    elevationRate = FastMath.toRadians(Double.parseDouble(values[8]));
                }

                // Initialise a new angles measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final AnglesMeasurement angles = new AnglesMeasurement(epoch, azmiuth, elevation,
                        directionFlag, orginFlag,
                        isRefractionCorrected,
                        azimuthRate, elevationRate);
                pi.dataBlock.addAnglesData(angles);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Calibration Record. */
        CALIB("40") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double   secOfDay               = Double.parseDouble(values[1]);
                final int      typeOfData             = Integer.parseInt(values[2]);
                final String   systemConfigId         = values[3];
                final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
                final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
                final double   oneWayDistance         = Double.parseDouble(values[6]);
                final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
                final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
                final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
                final double   skew                   = Double.parseDouble(values[10]);
                final double   kurtosis               = Double.parseDouble(values[11]);
                final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
                final int      typeIndicator          = Integer.parseInt(values[13]);
                final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
                final int      detectorChannel        = Integer.parseInt(values[15]);

                // Check file version for additional data
                int    span       = 0;
                double returnRate = Double.NaN;
                if (pi.version == 2) {
                    // fixed 20230321
                    // the span may be "na"
                    span       = readIntegerWithNaN(values[16], -1);
                    returnRate = Double.parseDouble(values[17]);
                }

                // Initialise a new angles measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final Calibration cal = new Calibration(epoch, typeOfData, systemConfigId, numberOfPointsRecorded,
                        numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew, kurtosis, peakMinusMean,
                        typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
                pi.dataBlock.addCalibrationData(cal);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Calibration Details Record. */
        CALIB_DETAILS("41") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final double   secOfDay               = Double.parseDouble(values[1]);
                final int      typeOfData             = Integer.parseInt(values[2]);
                final String   systemConfigId         = values[3];
                final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
                final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
                final double   oneWayDistance         = Double.parseDouble(values[6]);
                final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
                final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
                final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
                final double   skew                   = Double.parseDouble(values[10]);
                final double   kurtosis               = Double.parseDouble(values[11]);
                final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
                final int      typeIndicator          = Integer.parseInt(values[13]);
                final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
                final int      detectorChannel        = Integer.parseInt(values[15]);

                // Check file version for additional data
                int    span       = 0;
                double returnRate = Double.NaN;
                if (pi.version == 2) {
                    span       = Integer.parseInt(values[16]);
                    returnRate = Double.parseDouble(values[17]);
                }

                // Initialise a new angles measurement
                AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
                // Check rollover
                epoch = checkRollover(epoch, pi.header.getStartEpoch());
                final CalibrationDetail cal = new CalibrationDetail(epoch, typeOfData, systemConfigId,
                        numberOfPointsRecorded, numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew,
                        kurtosis, peakMinusMean, typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
                pi.dataBlock.addCalibrationDetailData(cal);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Calibration "Shot" Record. */
        CALIB_SHOT("42") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {
                // Not implemented yet
            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Session (Pass) Statistics Record. */
        STAT("50") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Data contained in the line
                final String[] values = SEPARATOR.split(line);

                // Read data
                final String systemConfigId    = values[1];
                final double rms               = PS.toSI(Double.parseDouble(values[2]));
                final double skewness          = Double.parseDouble(values[3]);
                final double kurtosis          = Double.parseDouble(values[4]);
                //
                // The peak minus mean may be "*"
                // 50 shao     35.0  -0.509   2.221 ****** 0
                final double peakMinusMean = values[5].contains("*") ? Double.NaN : PS.toSI(Double.parseDouble(values[5]));

                final int dataQualityIndicator = Integer.parseInt(values[6]);

                final SessionStatistics stat = new SessionStatistics(systemConfigId, rms, skewness, kurtosis, peakMinusMean,
                        dataQualityIndicator);
                pi.dataBlock.addSessionStatisticsData(stat);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Compatibility record. */
        COMPATIBILITY("60") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {
                // Not implemented yet
            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);
            }

        },

        /** Comments. */
        COMMENTS(COMMENTS_IDENTIFIER) {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // Comment
                final String comment = line.substring(2).trim();
                pi.file.getComments().add(comment);

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H1, H2, H3, H4, H5, H8, H9, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO,
                        METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);

            }

        },

        /** Custom. */
        CUSTOM("9\\d") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {
                // Not implemented yet
            }

            /** {@inheritDoc} */
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
                                     STAT, COMPATIBILITY, COMMENTS, CUSTOM);

            }

        },

        /** End of data block. */
        H8("H8", "h8") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {

                // fixed 2022-12-12
                // For the case of monthE is -1.
                // Use the date of the last range data as the end epoch.
                if (pi.header.getEndEpoch() == null) {
                    final List<RangeMeasurement> rangeData =  pi.dataBlock.getRangeData();
                    pi.header.setEndEpoch(rangeData.get(rangeData.size() - 1).getDate());
                }

                // Fill data block
                pi.dataBlock.setHeader(pi.header);
                pi.dataBlock.setConfigurationRecords(pi.configurationRecords);

                // Add the data block to the CRD file
                pi.file.addDataBlock(pi.dataBlock);

                // Initialize a new empty containers
                pi.startEpochDateComponents           = DateComponents.J2000_EPOCH;
                final CRDHeader lastHeader  = pi.header;
                pi.header               = new CRDHeader();
                pi.configurationRecords = new CRDConfiguration();
                pi.dataBlock            = new CRDDataBlock();

                // fill header with H1 H2 H3 if the file is for many targets, single system
                // configuration (see P31 in crd201)
                pi.header.setFormat(lastHeader.getFormat());
                pi.header.setVersion(lastHeader.getVersion());
                pi.header.setProductionEpoch(lastHeader.getProductionEpoch());
                pi.header.setProductionHour(lastHeader.getProductionHour());

                pi.header.setStationName(lastHeader.getStationName());
                pi.header.setSystemIdentifier(lastHeader.getSystemIdentifier());
                pi.header.setSystemNumber(lastHeader.getSystemNumber());
                pi.header.setSystemOccupancy(lastHeader.getSystemOccupancy());
                pi.header.setEpochIdentifier(lastHeader.getEpochIdentifier());
                pi.header.setStationNetword(lastHeader.getStationNetword());

                pi.header.setName(lastHeader.getName());
                pi.header.setIlrsSatelliteId(lastHeader.getIlrsSatelliteId());
                pi.header.setSic(lastHeader.getSic());
                pi.header.setNoradId(lastHeader.getNoradId());
                pi.header.setSpacecraftEpochTimeScale(lastHeader.getSpacecraftEpochTimeScale());
                pi.header.setTargetClass(lastHeader.getTargetClass());
                pi.header.setTargetLocation(lastHeader.getTargetLocation());

            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Arrays.asList(H1, H4, H9, COMMENTS);
            }

        },

        /** Last record in file. */
        H9("H9", "h9") {

            /** {@inheritDoc} */
            @Override
            public void parse(final String line, final ParseInfo pi) {
                pi.done = true;
            }

            /** {@inheritDoc} */
            @Override
            public Iterable<LineParser> allowedNext() {
                return Collections.singleton(H9);
            }

        };

        /** Patterns for identifying line. */
        private final Pattern[] patterns;

        /** Identifiers. */
        private final String[] identifiers;

        /** Simple constructor.
         * @param identifier regular expression for identifying line (i.e. first element)
         */
        LineParser(final String... identifier) {
            this.identifiers = identifier;
            // Initialise patterns
            this.patterns    = new Pattern[identifiers.length];
            for (int index = 0; index < patterns.length; index++) {
                patterns[index] = Pattern.compile(identifiers[index]);
            }
        }

        /** Parse a line.
         * @param line line to parse
         * @param pi holder for transient data
         */
        public abstract void parse(String line, ParseInfo pi);

        /** Get the allowed parsers for next line.
         * @return allowed parsers for next line
         */
        public abstract Iterable<LineParser> allowedNext();

        /** Check if parser can handle line.
         * @param line line to parse
         * @return true if parser can handle the specified line
         */
        public boolean canHandle(final String line) {
            // Line identifier
            final String lineId = SEPARATOR.split(line)[0];
            // Loop on patterns
            for (Pattern pattern : patterns) {
                if (pattern.matcher(lineId).matches()) {
                    return true;
                }
            }
            // No match
            return false;
        }

        /**
         * Read a boolean from a string value.
         * @param value input value
         * @return the correspondin boolean
         */
        private static boolean readBoolean(final String value) {
            return Integer.parseInt(value) == 1;
        }

        /**
         * Read an integer value taking into consideration a possible "NaN".
         * If the value is "NaN", the defaultValue is returned.
         * @param value input string
         * @param defaultValue the default value
         * @return the corresponding integer value
         */
        private static int readIntegerWithNaN(final String value, final int defaultValue) {
            return CRD.STR_NAN.equalsIgnoreCase(value) ? defaultValue : Integer.parseInt(value);
        }
    }

}