CRDParser.java

  1. /* Copyright 2002-2025 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.ilrs;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.util.Arrays;
  21. import java.util.Collections;
  22. import java.util.List;
  23. import java.util.regex.Pattern;

  24. import org.hipparchus.exception.LocalizedCoreFormats;
  25. import org.hipparchus.util.FastMath;
  26. import org.orekit.annotation.DefaultDataContext;
  27. import org.orekit.data.DataContext;
  28. import org.orekit.data.DataSource;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.files.ilrs.CRD.AnglesMeasurement;
  32. import org.orekit.files.ilrs.CRD.CRDDataBlock;
  33. import org.orekit.files.ilrs.CRD.Calibration;
  34. import org.orekit.files.ilrs.CRD.CalibrationDetail;
  35. import org.orekit.files.ilrs.CRD.FrRangeMeasurement;
  36. import org.orekit.files.ilrs.CRD.MeteorologicalMeasurement;
  37. import org.orekit.files.ilrs.CRD.NptRangeMeasurement;
  38. import org.orekit.files.ilrs.CRD.RangeMeasurement;
  39. import org.orekit.files.ilrs.CRD.RangeSupplement;
  40. import org.orekit.files.ilrs.CRD.SessionStatistics;
  41. import org.orekit.files.ilrs.CRDConfiguration.CalibrationTargetConfiguration;
  42. import org.orekit.files.ilrs.CRDConfiguration.DetectorConfiguration;
  43. import org.orekit.files.ilrs.CRDConfiguration.LaserConfiguration;
  44. import org.orekit.files.ilrs.CRDConfiguration.MeteorologicalConfiguration;
  45. import org.orekit.files.ilrs.CRDConfiguration.SoftwareConfiguration;
  46. import org.orekit.files.ilrs.CRDConfiguration.SystemConfiguration;
  47. import org.orekit.files.ilrs.CRDConfiguration.TimingSystemConfiguration;
  48. import org.orekit.files.ilrs.CRDConfiguration.TransponderConfiguration;
  49. import org.orekit.time.AbsoluteDate;
  50. import org.orekit.time.DateComponents;
  51. import org.orekit.time.TimeComponents;
  52. import org.orekit.time.TimeScale;
  53. import org.orekit.utils.Constants;
  54. import org.orekit.utils.units.Unit;
  55. import org.orekit.utils.units.UnitsConverter;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  96.     /**
  97.      * Default constructor.
  98.      * <p>
  99.      * This constructor uses the {@link DataContext#getDefault() default data context}.
  100.      */
  101.     @DefaultDataContext
  102.     public CRDParser() {
  103.         this(DataContext.getDefault().getTimeScales().getUTC());
  104.     }

  105.     /**
  106.      * Constructor.
  107.      * @param utc utc time scale to read epochs
  108.      */
  109.     public CRDParser(final TimeScale utc) {
  110.         this.timeScale = utc;
  111.     }

  112.     /**
  113.      * Get the time scale used to read the file.
  114.      * @return the time scale used to read the file
  115.      */
  116.     public TimeScale getTimeScale() {
  117.         return timeScale;
  118.     }

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

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

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

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

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

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

  146.                                 candidate.parse(line, pi);
  147.                                 if (pi.done) {
  148.                                     // Return file
  149.                                     return pi.file;
  150.                                 }
  151.                                 crdParsers = candidate.allowedNext();
  152.                                 continue nextLine;
  153.                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  154.                                 throw new OrekitException(e,
  155.                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  156.                                                           lineNumber, source.getName(), line);
  157.                             }
  158.                         }
  159.                     }
  160.                 }

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

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

  166.     }

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

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

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

  188.         /** Version. */
  189.         private int version;

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

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

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

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

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

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

  202.         /**
  203.          * Constructor.
  204.          */
  205.         protected ParseInfo() {

  206.             // Initialise default values
  207.             this.done       = false;
  208.             this.version    = 1;
  209.             this.startEpochDateComponents = DateComponents.J2000_EPOCH;

  210.             // Initialise empty object
  211.             this.file                 = new CRD();
  212.             this.header               = new CRDHeader();
  213.             this.configurationRecords = new CRDConfiguration();
  214.             this.dataBlock            = new CRDDataBlock();

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

  217.         }

  218.     }

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

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

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

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

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

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

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

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

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

  245.             }

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

  251.         },

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

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

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

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

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

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

  267.                 // Station network
  268.                 if (pi.version == 2) {
  269.                     pi.header.setStationNetword(values[6]);
  270.                 } else {
  271.                     pi.header.setStationNetword(CRD.STR_VALUE_NOT_AVAILABLE);
  272.                 }

  273.             }

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

  279.         },

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

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

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

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

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

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

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

  302.             }

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

  308.         },

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

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

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

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

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

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

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

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

  341.                     // fixed 2022-12-12
  342.                     // if yearE or monthE is -1.
  343.                     if (monthE == -1) {
  344.                         pi.header.setEndEpoch(null);
  345.                     } else {
  346.                         pi.header.setEndEpoch(new AbsoluteDate(yearE, monthE, dayE, hourE, minuteE, secondE, pi.timeScale));
  347.                     }
  348.                 }

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

  351.                 // Correction flags
  352.                 pi.header.setIsTroposphericRefractionApplied(readBoolean(values[15]));
  353.                 pi.header.setIsCenterOfMassCorrectionApplied(readBoolean(values[16]));
  354.                 pi.header.setIsReceiveAmplitudeCorrectionApplied(readBoolean(values[17]));
  355.                 pi.header.setIsStationSystemDelayApplied(readBoolean(values[18]));
  356.                 pi.header.setIsTransponderDelayApplied(readBoolean(values[19]));

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

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

  361.             }

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

  368.         },

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

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

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

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

  382.             }

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

  389.         },

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

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

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

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

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

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

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

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

  407.             }

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

  414.         },


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

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

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

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

  424.                 // Fill values
  425.                 laserRecord.setLaserId(values[2]);
  426.                 laserRecord.setLaserType(values[3]);
  427.                 laserRecord.setPrimaryWavelength(NM.toSI(Double.parseDouble(values[4])));
  428.                 laserRecord.setNominalFireRate(Double.parseDouble(values[5]));
  429.                 laserRecord.setPulseEnergy(Double.parseDouble(values[6]));
  430.                 laserRecord.setPulseWidth(Double.parseDouble(values[7]));
  431.                 laserRecord.setBeamDivergence(Double.parseDouble(values[8]));
  432.                 laserRecord.setPulseInOutgoingSemiTrain(readIntegerWithNaN(values[9], 1));

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

  435.             }

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

  441.         },

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

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

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

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

  451.                 // Fill values
  452.                 detectorRecord.setDetectorId(values[2]);
  453.                 detectorRecord.setDetectorType(values[3]);
  454.                 detectorRecord.setApplicableWavelength(NM.toSI(Double.parseDouble(values[4])));
  455.                 detectorRecord.setQuantumEfficiency(Double.parseDouble(values[5]));
  456.                 detectorRecord.setAppliedVoltage(Double.parseDouble(values[6]));
  457.                 detectorRecord.setDarkCount(KHZ.toSI(Double.parseDouble(values[7])));
  458.                 detectorRecord.setOutputPulseType(values[8]);
  459.                 detectorRecord.setOutputPulseWidth(Double.parseDouble(values[9]));
  460.                 detectorRecord.setSpectralFilter(NM.toSI(Double.parseDouble(values[10])));
  461.                 detectorRecord.setTransmissionOfSpectralFilter(Double.parseDouble(values[11]));
  462.                 detectorRecord.setSpatialFilter(Double.parseDouble(values[12]));
  463.                 detectorRecord.setExternalSignalProcessing(values[13]);

  464.                 // Check file version for additional data
  465.                 if (pi.version == 2) {
  466.                     detectorRecord.setAmplifierGain(Double.parseDouble(values[14]));
  467.                     detectorRecord.setAmplifierBandwidth(KHZ.toSI(Double.parseDouble(values[15])));
  468.                     detectorRecord.setAmplifierInUse(values[16]);
  469.                 } else {
  470.                     detectorRecord.setAmplifierGain(Double.NaN);
  471.                     detectorRecord.setAmplifierBandwidth(Double.NaN);
  472.                     detectorRecord.setAmplifierInUse(CRD.STR_VALUE_NOT_AVAILABLE);
  473.                 }

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

  476.             }

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

  483.         },

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

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

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

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

  493.                 // Fill values
  494.                 timingRecord.setLocalTimingId(values[2]);
  495.                 timingRecord.setTimeSource(values[3]);
  496.                 timingRecord.setFrequencySource(values[4]);
  497.                 timingRecord.setTimer(values[5]);
  498.                 final String timerSerialNumber = values[6];
  499.                 if (CRD.STR_NAN.equalsIgnoreCase(timerSerialNumber)) {
  500.                     // The timer serial number may be "na"
  501.                     timingRecord.setTimerSerialNumber(CRD.STR_VALUE_NOT_AVAILABLE);
  502.                 } else {
  503.                     timingRecord.setTimerSerialNumber(timerSerialNumber);
  504.                 }
  505.                 timingRecord.setEpochDelayCorrection(US.toSI(Double.parseDouble(values[7])));

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

  508.             }

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

  515.         },

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

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

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

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

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

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

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

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

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

  540.             }

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

  547.         },

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

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

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

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

  557.                 // Fill values
  558.                 softwareRecord.setSoftwareId(values[2]);
  559.                 softwareRecord.setTrackingSoftwares(COMMA.split(values[3]));
  560.                 softwareRecord.setTrackingSoftwareVersions(COMMA.split(values[4]));
  561.                 softwareRecord.setProcessingSoftwares(COMMA.split(values[5]));
  562.                 softwareRecord.setProcessingSoftwareVersions(COMMA.split(values[6]));

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

  565.             }

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

  572.         },

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

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

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

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

  582.                 // Fill values
  583.                 meteoRecord.setMeteorologicalId(values[2]);
  584.                 meteoRecord.setPressSensorManufacturer(values[3]);
  585.                 meteoRecord.setPressSensorModel(values[4]);
  586.                 meteoRecord.setPressSensorSerialNumber(values[5]);
  587.                 meteoRecord.setTempSensorManufacturer(values[6]);
  588.                 meteoRecord.setTempSensorModel(values[7]);
  589.                 meteoRecord.setTempSensorSerialNumber(values[8]);
  590.                 meteoRecord.setHumiSensorManufacturer(values[9]);
  591.                 meteoRecord.setHumiSensorModel(values[10]);
  592.                 meteoRecord.setHumiSensorSerialNumber(values[11]);

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

  595.             }

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

  602.         },

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

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

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

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

  612.                 // Fill values
  613.                 calibRecord.setConfigurationId(values[2]);
  614.                 calibRecord.setTargetName(values[3]);
  615.                 calibRecord.setSurveyedTargetDistance(Double.parseDouble(values[4]));
  616.                 calibRecord.setSurveyError(Double.parseDouble(values[5]) * 1e-3);  // mm --> m
  617.                 calibRecord.setSumOfAllConstantDelays(Double.parseDouble(values[6]));
  618.                 calibRecord.setPulseEnergy(Double.parseDouble(values[7]));
  619.                 calibRecord.setProcessingSoftwareName(values[8]);
  620.                 calibRecord.setProcessingSoftwareVersion(values[9]);

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

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

  630.         },

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

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

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

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

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

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

  658.             }

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

  665.         },

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

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

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

  673.                 // Read data
  674.                 final double   secOfDay          = Double.parseDouble(values[1]);
  675.                 final double   timeOfFlight      = Double.parseDouble(values[2]);
  676.                 final String   systemConfigId    = values[3];
  677.                 final int      epochEvent        = Integer.parseInt(values[4]);
  678.                 final double   windowLength      = Double.parseDouble(values[5]);
  679.                 final int      numberOfRawRanges = Integer.parseInt(values[6]);
  680.                 final double   binRms            = PS.toSI(Double.parseDouble(values[7]));
  681.                 final double   binSkew           = Double.parseDouble(values[8]);
  682.                 final double   binKurtosis       = Double.parseDouble(values[9]);
  683.                 final double   binPeakMinusMean  = PS.toSI(Double.parseDouble(values[10]));
  684.                 final double   returnRate        = Double.parseDouble(values[11]);
  685.                 final int      detectorChannel   = Integer.parseInt(values[12]);

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

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

  698.             }

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

  705.         },

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

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

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

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

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

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

  731.             }

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

  737.         },

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

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

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

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

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

  758.             }

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

  765.         },

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

  768.             /** {@inheritDoc} */
  769.             @Override
  770.             public void parse(final String line, final ParseInfo pi) {
  771.                 // Not implemented yet
  772.             }

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

  779.         },

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

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

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

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


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

  802.                 // Initialise a new angles measurement
  803.                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
  804.                 // Check rollover
  805.                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
  806.                 final AnglesMeasurement angles = new AnglesMeasurement(epoch, azmiuth, elevation,
  807.                         directionFlag, orginFlag,
  808.                         isRefractionCorrected,
  809.                         azimuthRate, elevationRate);
  810.                 pi.dataBlock.addAnglesData(angles);

  811.             }

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

  818.         },

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

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

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

  826.                 // Read data
  827.                 final double   secOfDay               = Double.parseDouble(values[1]);
  828.                 final int      typeOfData             = Integer.parseInt(values[2]);
  829.                 final String   systemConfigId         = values[3];
  830.                 final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
  831.                 final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
  832.                 final double   oneWayDistance         = Double.parseDouble(values[6]);
  833.                 final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
  834.                 final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
  835.                 final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
  836.                 final double   skew                   = Double.parseDouble(values[10]);
  837.                 final double   kurtosis               = Double.parseDouble(values[11]);
  838.                 final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
  839.                 final int      typeIndicator          = Integer.parseInt(values[13]);
  840.                 final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
  841.                 final int      detectorChannel        = Integer.parseInt(values[15]);

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

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

  859.             }

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

  866.         },

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

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

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

  874.                 // Read data
  875.                 final double   secOfDay               = Double.parseDouble(values[1]);
  876.                 final int      typeOfData             = Integer.parseInt(values[2]);
  877.                 final String   systemConfigId         = values[3];
  878.                 final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
  879.                 final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
  880.                 final double   oneWayDistance         = Double.parseDouble(values[6]);
  881.                 final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
  882.                 final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
  883.                 final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
  884.                 final double   skew                   = Double.parseDouble(values[10]);
  885.                 final double   kurtosis               = Double.parseDouble(values[11]);
  886.                 final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
  887.                 final int      typeIndicator          = Integer.parseInt(values[13]);
  888.                 final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
  889.                 final int      detectorChannel        = Integer.parseInt(values[15]);

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

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

  905.             }

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

  912.         },

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

  915.             /** {@inheritDoc} */
  916.             @Override
  917.             public void parse(final String line, final ParseInfo pi) {
  918.                 // Not implemented yet
  919.             }

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

  926.         },

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

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

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

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

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

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

  947.             }

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

  954.         },

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

  957.             /** {@inheritDoc} */
  958.             @Override
  959.             public void parse(final String line, final ParseInfo pi) {
  960.                 // Not implemented yet
  961.             }

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

  968.         },

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

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

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

  977.             }

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

  983.             }

  984.         },

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

  987.             /** {@inheritDoc} */
  988.             @Override
  989.             public void parse(final String line, final ParseInfo pi) {
  990.                 // Not implemented yet
  991.             }

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

  996.             }

  997.         },

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

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

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

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

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

  1015.                 // Initialize a new empty containers
  1016.                 pi.startEpochDateComponents           = DateComponents.J2000_EPOCH;
  1017.                 final CRDHeader lastHeader  = pi.header;
  1018.                 pi.header               = new CRDHeader();
  1019.                 pi.configurationRecords = new CRDConfiguration();
  1020.                 pi.dataBlock            = new CRDDataBlock();

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

  1027.                 pi.header.setStationName(lastHeader.getStationName());
  1028.                 pi.header.setSystemIdentifier(lastHeader.getSystemIdentifier());
  1029.                 pi.header.setSystemNumber(lastHeader.getSystemNumber());
  1030.                 pi.header.setSystemOccupancy(lastHeader.getSystemOccupancy());
  1031.                 pi.header.setEpochIdentifier(lastHeader.getEpochIdentifier());
  1032.                 pi.header.setStationNetword(lastHeader.getStationNetword());

  1033.                 pi.header.setName(lastHeader.getName());
  1034.                 pi.header.setIlrsSatelliteId(lastHeader.getIlrsSatelliteId());
  1035.                 pi.header.setSic(lastHeader.getSic());
  1036.                 pi.header.setNoradId(lastHeader.getNoradId());
  1037.                 pi.header.setSpacecraftEpochTimeScale(lastHeader.getSpacecraftEpochTimeScale());
  1038.                 pi.header.setTargetClass(lastHeader.getTargetClass());
  1039.                 pi.header.setTargetLocation(lastHeader.getTargetLocation());

  1040.             }

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

  1046.         },

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

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

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

  1059.         };

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

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

  1064.         /** Simple constructor.
  1065.          * @param identifier regular expression for identifying line (i.e. first element)
  1066.          */
  1067.         LineParser(final String... identifier) {
  1068.             this.identifiers = identifier;
  1069.             // Initialise patterns
  1070.             this.patterns    = new Pattern[identifiers.length];
  1071.             for (int index = 0; index < patterns.length; index++) {
  1072.                 patterns[index] = Pattern.compile(identifiers[index]);
  1073.             }
  1074.         }

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

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

  1084.         /** Check if parser can handle line.
  1085.          * @param line line to parse
  1086.          * @return true if parser can handle the specified line
  1087.          */
  1088.         public boolean canHandle(final String line) {
  1089.             // Line identifier
  1090.             final String lineId = SEPARATOR.split(line)[0];
  1091.             // Loop on patterns
  1092.             for (Pattern pattern : patterns) {
  1093.                 if (pattern.matcher(lineId).matches()) {
  1094.                     return true;
  1095.                 }
  1096.             }
  1097.             // No match
  1098.             return false;
  1099.         }

  1100.         /**
  1101.          * Read a boolean from a string value.
  1102.          * @param value input value
  1103.          * @return the correspondin boolean
  1104.          */
  1105.         private static boolean readBoolean(final String value) {
  1106.             return Integer.parseInt(value) == 1;
  1107.         }

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

  1119. }