1   /* Copyright 2002-2024 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  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.List;
24  import java.util.regex.Pattern;
25  
26  import org.hipparchus.exception.LocalizedCoreFormats;
27  import org.hipparchus.util.FastMath;
28  import org.orekit.annotation.DefaultDataContext;
29  import org.orekit.data.DataContext;
30  import org.orekit.data.DataSource;
31  import org.orekit.errors.OrekitException;
32  import org.orekit.errors.OrekitMessages;
33  import org.orekit.files.ilrs.CRD.AnglesMeasurement;
34  import org.orekit.files.ilrs.CRD.CRDDataBlock;
35  import org.orekit.files.ilrs.CRD.Calibration;
36  import org.orekit.files.ilrs.CRD.CalibrationDetail;
37  import org.orekit.files.ilrs.CRD.FrRangeMeasurement;
38  import org.orekit.files.ilrs.CRD.MeteorologicalMeasurement;
39  import org.orekit.files.ilrs.CRD.NptRangeMeasurement;
40  import org.orekit.files.ilrs.CRD.RangeMeasurement;
41  import org.orekit.files.ilrs.CRD.RangeSupplement;
42  import org.orekit.files.ilrs.CRD.SessionStatistics;
43  import org.orekit.files.ilrs.CRDConfiguration.CalibrationTargetConfiguration;
44  import org.orekit.files.ilrs.CRDConfiguration.DetectorConfiguration;
45  import org.orekit.files.ilrs.CRDConfiguration.LaserConfiguration;
46  import org.orekit.files.ilrs.CRDConfiguration.MeteorologicalConfiguration;
47  import org.orekit.files.ilrs.CRDConfiguration.SoftwareConfiguration;
48  import org.orekit.files.ilrs.CRDConfiguration.SystemConfiguration;
49  import org.orekit.files.ilrs.CRDConfiguration.TimingSystemConfiguration;
50  import org.orekit.files.ilrs.CRDConfiguration.TransponderConfiguration;
51  import org.orekit.time.AbsoluteDate;
52  import org.orekit.time.DateComponents;
53  import org.orekit.time.TimeComponents;
54  import org.orekit.time.TimeScale;
55  import org.orekit.utils.Constants;
56  import org.orekit.utils.units.Unit;
57  import org.orekit.utils.units.UnitsConverter;
58  
59  /**
60   * A parser for the CRD data file format.
61   * <p>
62   * It supports both 1.0 and 2.0 versions
63   * <p>
64   * <b>Note</b>: Not all the records are read by the parser. Only the most significants are parsed.
65   * Contributions are welcome to support more fields in the format.
66   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2009/crd_v1.01.pdf">1.0 file format</a>
67   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2021/crd_v2.01e2.pdf">2.0 file format</a>
68   * @author Bryan Cazabonne
69   * @author Rongwang Li
70   * @since 10.3
71   */
72  public class CRDParser {
73  
74      /** Default supported files name pattern for CRD files. */
75      public static final String DEFAULT_CRD_SUPPORTED_NAMES = "^(?!0+$)\\w{1,12}\\_\\d{6,8}.\\w{3}$";
76  
77      /** Nanometers units. */
78      private static final Unit NM = Unit.parse("nm");
79  
80      /** Kilohertz units. */
81      private static final Unit KHZ = Unit.parse("kHz");
82  
83      /** Microseconds units. */
84      private static final Unit US = Unit.parse("µs");
85  
86      /** Nanoseconds units. */
87      private static final Unit NS = Unit.parse("ns");
88  
89      /** Picoseconds units. */
90      private static final Unit PS = Unit.parse("ps");
91  
92      /** mbar to bar converter. */
93      private static final UnitsConverter MBAR_TO_BAR = new UnitsConverter(Unit.parse("mbar"), Unit.parse("bar"));
94  
95      /** File format. */
96      private static final String FILE_FORMAT = "CRD";
97  
98      /** Pattern for delimiting regular expressions. */
99      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
100 
101     /** Pattern for delimiting expressions with comma. */
102     private static final Pattern COMMA = Pattern.compile(",");
103 
104     /** Identifier of comment record. */
105     private static final String COMMENTS_IDENTIFIER = "00";
106 
107     /** Pattern of " [-]?(na)". */
108     private static final Pattern PATTERN_NA = Pattern.compile(" [-]?(na)");
109 
110     /** Time scale used to define epochs in CPF file. */
111     private final TimeScale timeScale;
112 
113     /**
114      * Default constructor.
115      * <p>
116      * This constructor uses the {@link DataContext#getDefault() default data context}.
117      */
118     @DefaultDataContext
119     public CRDParser() {
120         this(DataContext.getDefault().getTimeScales().getUTC());
121     }
122 
123     /**
124      * Constructor.
125      * @param utc utc time scale to read epochs
126      */
127     public CRDParser(final TimeScale utc) {
128         this.timeScale = utc;
129     }
130 
131     /**
132      * Get the time scale used to read the file.
133      * @return the time scale used to read the file
134      */
135     public TimeScale getTimeScale() {
136         return timeScale;
137     }
138 
139     /**
140      * Parse a CRD file.
141      * @param source data source containing the CRD file.
142      * @return a parsed CRD file.
143      * @throws IOException if {@code reader} throws one.
144      */
145     public CRD parse(final DataSource source) throws IOException {
146 
147         // Initialize internal data structures
148         final ParseInfo pi = new ParseInfo();
149 
150         int lineNumber = 0;
151         Iterable<LineParser> crdParsers = Collections.singleton(LineParser.H1);
152         try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
153             nextLine:
154                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
155                     ++lineNumber;
156 
157                     if (line.startsWith(COMMENTS_IDENTIFIER)) {
158                         // Comment is in the beginning of the file.
159                         crdParsers = Arrays.asList(LineParser.COMMENTS);
160                     }
161 
162                     for (final LineParser candidate : crdParsers) {
163                         if (candidate.canHandle(line)) {
164                             try {
165 
166                                 // Note: since crd v2.01.
167                                 // The literal “na” is used instead of “-1” for fields that are not applicable or not avaiable.
168                                 // And there may be "-na".
169                                 // note: "analog" --> "aNaNlog"
170                                 line = PATTERN_NA.matcher(line).replaceAll(" " + CRD.STR_NAN);
171 
172                                 candidate.parse(line, pi);
173                                 if (pi.done) {
174                                     // Return file
175                                     return pi.file;
176                                 }
177                                 crdParsers = candidate.allowedNext();
178                                 continue nextLine;
179                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
180                                 throw new OrekitException(e,
181                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
182                                                           lineNumber, source.getName(), line);
183                             }
184                         }
185                     }
186                 }
187 
188             // We never reached the EOF marker
189             throw new OrekitException(OrekitMessages.CRD_UNEXPECTED_END_OF_FILE, lineNumber);
190 
191         } catch (IOException ioe) {
192             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
193         }
194 
195     }
196 
197     /**
198      * Make sure the epoch is 'right' by doing a day shift if it is required by comparing the current and session start epoch.
199      * According to the CRD document, the duration of a session must be less than one day.
200      * @param epoch current epoch
201      * @param startEpoch start epoch of session
202      * @return epoch with rollover is handled.
203      */
204     private static AbsoluteDate checkRollover(final AbsoluteDate epoch, final AbsoluteDate startEpoch) {
205         // If the current epoch is before the start epoch of a session, the epoch should be shifted by 1 day.
206         // For METEO(20) data, the epoch may be a 'little' (10 hours?) before the session start epoch.
207         // And also for CALIB(40) and CALIB_DETAILS(41)
208         return epoch.durationFrom(startEpoch) < -36000 ? epoch.shiftedBy(Constants.JULIAN_DAY) : epoch;
209     }
210 
211     /** Transient data used for parsing a CRD file. The data is kept in a
212      * separate data structure to make the parser thread-safe.
213      * <p><b>Note</b>: The class intentionally does not provide accessor
214      * methods, as it is only used internally for parsing a CRD file.</p>
215      */
216     private class ParseInfo {
217 
218         /** The corresponding CDR file. */
219         private CRD file;
220 
221         /** Version. */
222         private int version;
223 
224         /** The current data block. */
225         private CRDDataBlock dataBlock;
226 
227         /** Data block header. */
228         private CRDHeader header;
229 
230         /** Cofiguration records. */
231         private CRDConfiguration configurationRecords;
232 
233         /** Time scale. */
234         private TimeScale timeScale;
235 
236         /** Current data block start epoch, DateComponents only. */
237         private DateComponents startEpochDateComponents;
238 
239         /** End Of File reached indicator. */
240         private boolean done;
241 
242         /**
243          * Constructor.
244          */
245         protected ParseInfo() {
246 
247             // Initialise default values
248             this.done       = false;
249             this.version    = 1;
250             this.startEpochDateComponents = DateComponents.J2000_EPOCH;
251 
252             // Initialise empty object
253             this.file                 = new CRD();
254             this.header               = new CRDHeader();
255             this.configurationRecords = new CRDConfiguration();
256             this.dataBlock            = new CRDDataBlock();
257 
258             // Time scale
259             this.timeScale = CRDParser.this.timeScale;
260 
261         }
262 
263     }
264 
265     /** Parsers for specific lines. */
266     private enum LineParser {
267 
268         /** Format header. */
269         H1("H1", "h1") {
270 
271             /** {@inheritDoc} */
272             @Override
273             public void parse(final String line, final ParseInfo pi) {
274 
275                 // Data contained in the line
276                 final String[] values = SEPARATOR.split(line);
277 
278                 // Format and version
279                 final String format = values[1];
280                 pi.version = Integer.parseInt(values[2]);
281 
282                 // Throw an exception if format is not equal to "CRD"
283                 if (!format.equalsIgnoreCase(FILE_FORMAT)) {
284                     throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
285                 }
286 
287                 // Fill first elements
288                 pi.header.setFormat(format);
289                 pi.header.setVersion(pi.version);
290 
291                 // Epoch of ephemeris production
292                 final int year  = Integer.parseInt(values[3]);
293                 final int month = Integer.parseInt(values[4]);
294                 final int day   = Integer.parseInt(values[5]);
295                 pi.header.setProductionEpoch(new DateComponents(year, month, day));
296 
297                 // Hour of ephemeris production
298                 pi.header.setProductionHour(Integer.parseInt(values[6]));
299 
300             }
301 
302             /** {@inheritDoc} */
303             @Override
304             public Iterable<LineParser> allowedNext() {
305                 return Arrays.asList(H2, COMMENTS);
306             }
307 
308         },
309 
310         /** Station header. */
311         H2("H2", "h2") {
312 
313             /** {@inheritDoc} */
314             @Override
315             public void parse(final String line, final ParseInfo pi) {
316 
317                 // Data contained in the line
318                 final String[] values = SEPARATOR.split(line);
319 
320                 // Station name
321                 pi.header.setStationName(values[1]);
322 
323                 // Crustal Dynamics Project keys
324                 pi.header.setSystemIdentifier(Integer.parseInt(values[2]));
325                 pi.header.setSystemNumber(Integer.parseInt(values[3]));
326                 pi.header.setSystemOccupancy(Integer.parseInt(values[4]));
327 
328                 // Station epoch time scale
329                 pi.header.setEpochIdentifier(Integer.parseInt(values[5]));
330 
331                 // Station network
332                 if (pi.version == 2) {
333                     pi.header.setStationNetword(values[6]);
334                 } else {
335                     pi.header.setStationNetword(CRD.STR_VALUE_NOT_AVAILABLE);
336                 }
337 
338             }
339 
340             /** {@inheritDoc} */
341             @Override
342             public Iterable<LineParser> allowedNext() {
343                 return Arrays.asList(H3, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
344             }
345 
346         },
347 
348         /** Target header. */
349         H3("H3", "h3") {
350 
351             /** {@inheritDoc} */
352             @Override
353             public void parse(final String line, final ParseInfo pi) {
354 
355                 // Data contained in the line
356                 final String[] values = SEPARATOR.split(line);
357 
358                 // Target name
359                 pi.header.setName(values[1]);
360 
361                 // Identifiers
362                 pi.header.setIlrsSatelliteId(values[2]);
363                 pi.header.setSic(values[3]);
364                 pi.header.setNoradId(values[4]);
365 
366                 // Spacecraft Epoch Time Scale
367                 pi.header.setSpacecraftEpochTimeScale(Integer.parseInt(values[5]));
368 
369                 // Target class and location (if needed)
370                 pi.header.setTargetClass(Integer.parseInt(values[6]));
371                 if (pi.version == 2) {
372                     // na=unknown (for use when tracking a transponder using a Version 1 CPF)
373                     // treated it as -1
374                     pi.header.setTargetLocation(readIntegerWithNaN(values[7], -1));
375                 }
376 
377             }
378 
379             /** {@inheritDoc} */
380             @Override
381             public Iterable<LineParser> allowedNext() {
382                 return Arrays.asList(H4, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
383             }
384 
385         },
386 
387         /** Session (Pass/Pass segment) header. */
388         H4("H4", "h4") {
389 
390             /** {@inheritDoc} */
391             @Override
392             public void parse(final String line, final ParseInfo pi) {
393 
394                 // Data contained in the line
395                 final String[] values = SEPARATOR.split(line);
396 
397                 // Data type
398                 pi.header.setDataType(Integer.parseInt(values[1]));
399 
400                 // Start epoch
401                 final int    yearS   = Integer.parseInt(values[2]);
402                 final int    monthS  = Integer.parseInt(values[3]);
403                 final int    dayS    = Integer.parseInt(values[4]);
404                 final int    hourS   = Integer.parseInt(values[5]);
405                 final int    minuteS = Integer.parseInt(values[6]);
406                 final double secondS = Integer.parseInt(values[7]);
407 
408                 pi.startEpochDateComponents = new DateComponents(yearS, monthS, dayS);
409 
410                 pi.header.setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
411                         hourS, minuteS, secondS,
412                         pi.timeScale));
413 
414                 // End epoch
415                 // since crd v2.01
416                 // Set the ending date and time fields to “na” if not available.
417                 if (pi.version == 2 && values[8].equalsIgnoreCase("")) {
418                     pi.header.setEndEpoch(null);
419                 } else {
420                     final int yearE = Integer.parseInt(values[8]);
421                     final int monthE = Integer.parseInt(values[9]);
422                     final int dayE = Integer.parseInt(values[10]);
423                     final int hourE = Integer.parseInt(values[11]);
424                     final int minuteE = Integer.parseInt(values[12]);
425                     final double secondE = Integer.parseInt(values[13]);
426 
427                     // fixed 2022-12-12
428                     // if yearE or monthE is -1.
429                     if (monthE == -1) {
430                         pi.header.setEndEpoch(null);
431                     } else {
432                         pi.header.setEndEpoch(new AbsoluteDate(yearE, monthE, dayE, hourE, minuteE, secondE, pi.timeScale));
433                     }
434                 }
435 
436                 // Data release
437                 pi.header.setDataReleaseFlag(Integer.parseInt(values[14]));
438 
439                 // Correction flags
440                 pi.header.setIsTroposphericRefractionApplied(readBoolean(values[15]));
441                 pi.header.setIsCenterOfMassCorrectionApplied(readBoolean(values[16]));
442                 pi.header.setIsReceiveAmplitudeCorrectionApplied(readBoolean(values[17]));
443                 pi.header.setIsStationSystemDelayApplied(readBoolean(values[18]));
444                 pi.header.setIsTransponderDelayApplied(readBoolean(values[19]));
445 
446                 // Range type indicator
447                 pi.header.setRangeType(Integer.parseInt(values[20]));
448 
449                 // Data quality indicator
450                 pi.header.setQualityIndicator(Integer.parseInt(values[21]));
451 
452             }
453 
454             /** {@inheritDoc} */
455             @Override
456             public Iterable<LineParser> allowedNext() {
457                 return Arrays.asList(H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES,
458                                      CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
459             }
460 
461         },
462 
463         /** Prediction header. */
464         H5("H5", "h5") {
465 
466             /** {@inheritDoc} */
467             @Override
468             public void parse(final String line, final ParseInfo pi) {
469 
470                 // Data contained in the line
471                 final String[] values = SEPARATOR.split(line);
472 
473                 // Fill data
474                 pi.header.setPredictionType(Integer.parseInt(values[1]));
475                 pi.header.setYearOfCentury(Integer.parseInt(values[2]));
476                 pi.header.setDateAndTime(values[3]);
477                 pi.header.setPredictionProvider(values[4]);
478                 pi.header.setSequenceNumber(Integer.parseInt(values[5]));
479 
480             }
481 
482             /** {@inheritDoc} */
483             @Override
484             public Iterable<LineParser> allowedNext() {
485                 return Arrays.asList(C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB,
486                                      CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
487             }
488 
489         },
490 
491         /** System configuration record. */
492         C0("C0", "c0") {
493 
494             /** {@inheritDoc} */
495             @Override
496             public void parse(final String line, final ParseInfo pi) {
497 
498                 // Initialise an empty system configuration record
499                 final SystemConfiguration systemRecord = new SystemConfiguration();
500 
501                 // Data contained in the line
502                 final String[] values = SEPARATOR.split(line);
503 
504                 // Wavelength
505                 systemRecord.setWavelength(NM.toSI(Double.parseDouble(values[2])));
506 
507                 // System ID
508                 systemRecord.setSystemId(values[3]);
509 
510                 // Components, A B C D E F G
511                 systemRecord.setComponents(Arrays.copyOfRange(values, 4, values.length));
512 
513                 // Add the system configuration record
514                 pi.configurationRecords.addConfigurationRecord(systemRecord);
515 
516             }
517 
518             /** {@inheritDoc} */
519             @Override
520             public Iterable<LineParser> allowedNext() {
521                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
522                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
523             }
524 
525         },
526 
527 
528         /** Laser configuration record. */
529         C1("C1", "c1") {
530 
531             /** {@inheritDoc} */
532             @Override
533             public void parse(final String line, final ParseInfo pi) {
534 
535                 // Initialise an empty laser configuration record
536                 final LaserConfiguration laserRecord = new LaserConfiguration();
537 
538                 // Data contained in the line
539                 final String[] values = SEPARATOR.split(line);
540 
541                 // Fill values
542                 laserRecord.setLaserId(values[2]);
543                 laserRecord.setLaserType(values[3]);
544                 laserRecord.setPrimaryWavelength(NM.toSI(Double.parseDouble(values[4])));
545                 laserRecord.setNominalFireRate(Double.parseDouble(values[5]));
546                 laserRecord.setPulseEnergy(Double.parseDouble(values[6]));
547                 laserRecord.setPulseWidth(Double.parseDouble(values[7]));
548                 laserRecord.setBeamDivergence(Double.parseDouble(values[8]));
549                 laserRecord.setPulseInOutgoingSemiTrain(readIntegerWithNaN(values[9], 1));
550 
551                 // Add the laser configuration record
552                 pi.configurationRecords.addConfigurationRecord(laserRecord);
553 
554             }
555 
556             /** {@inheritDoc} */
557             @Override
558             public Iterable<LineParser> allowedNext() {
559                 return Arrays.asList(C2, C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
560             }
561 
562         },
563 
564         /** Detector configuration record. */
565         C2("C2", "c2") {
566 
567             /** {@inheritDoc} */
568             @Override
569             public void parse(final String line, final ParseInfo pi) {
570 
571                 // Initialise an empty detector configuration record
572                 final DetectorConfiguration detectorRecord = new DetectorConfiguration();
573 
574                 // Data contained in the line
575                 final String[] values = SEPARATOR.split(line);
576 
577                 // Fill values
578                 detectorRecord.setDetectorId(values[2]);
579                 detectorRecord.setDetectorType(values[3]);
580                 detectorRecord.setApplicableWavelength(NM.toSI(Double.parseDouble(values[4])));
581                 detectorRecord.setQuantumEfficiency(Double.parseDouble(values[5]));
582                 detectorRecord.setAppliedVoltage(Double.parseDouble(values[6]));
583                 detectorRecord.setDarkCount(KHZ.toSI(Double.parseDouble(values[7])));
584                 detectorRecord.setOutputPulseType(values[8]);
585                 detectorRecord.setOutputPulseWidth(Double.parseDouble(values[9]));
586                 detectorRecord.setSpectralFilter(NM.toSI(Double.parseDouble(values[10])));
587                 detectorRecord.setTransmissionOfSpectralFilter(Double.parseDouble(values[11]));
588                 detectorRecord.setSpatialFilter(Double.parseDouble(values[12]));
589                 detectorRecord.setExternalSignalProcessing(values[13]);
590 
591                 // Check file version for additional data
592                 if (pi.version == 2) {
593                     detectorRecord.setAmplifierGain(Double.parseDouble(values[14]));
594                     detectorRecord.setAmplifierBandwidth(KHZ.toSI(Double.parseDouble(values[15])));
595                     detectorRecord.setAmplifierInUse(values[16]);
596                 } else {
597                     detectorRecord.setAmplifierGain(Double.NaN);
598                     detectorRecord.setAmplifierBandwidth(Double.NaN);
599                     detectorRecord.setAmplifierInUse(CRD.STR_VALUE_NOT_AVAILABLE);
600                 }
601 
602                 // Add the detector configuration record
603                 pi.configurationRecords.addConfigurationRecord(detectorRecord);
604 
605             }
606 
607             /** {@inheritDoc} */
608             @Override
609             public Iterable<LineParser> allowedNext() {
610                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
611                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
612             }
613 
614         },
615 
616         /** Timing system configuration record. */
617         C3("C3", "c3") {
618 
619             /** {@inheritDoc} */
620             @Override
621             public void parse(final String line, final ParseInfo pi) {
622 
623                 // Initialise an empty timing system configuration record
624                 final TimingSystemConfiguration timingRecord = new TimingSystemConfiguration();
625 
626                 // Data contained in the line
627                 final String[] values = SEPARATOR.split(line);
628 
629                 // Fill values
630                 timingRecord.setLocalTimingId(values[2]);
631                 timingRecord.setTimeSource(values[3]);
632                 timingRecord.setFrequencySource(values[4]);
633                 timingRecord.setTimer(values[5]);
634                 final String timerSerialNumber = values[6];
635                 if (CRD.STR_NAN.equalsIgnoreCase(timerSerialNumber)) {
636                     // The timer serial number may be "na"
637                     timingRecord.setTimerSerialNumber(CRD.STR_VALUE_NOT_AVAILABLE);
638                 } else {
639                     timingRecord.setTimerSerialNumber(timerSerialNumber);
640                 }
641                 timingRecord.setEpochDelayCorrection(US.toSI(Double.parseDouble(values[7])));
642 
643                 // Add the timing system configuration record
644                 pi.configurationRecords.addConfigurationRecord(timingRecord);
645 
646             }
647 
648             /** {@inheritDoc} */
649             @Override
650             public Iterable<LineParser> allowedNext() {
651                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
652                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
653             }
654 
655         },
656 
657         /** Transponder configuration record. */
658         C4("C4", "c4") {
659 
660             /** {@inheritDoc} */
661             @Override
662             public void parse(final String line, final ParseInfo pi) {
663 
664                 // Initialise an empty transponder configuration record
665                 final TransponderConfiguration transponderRecord = new TransponderConfiguration();
666 
667                 // Data contained in the line
668                 final String[] values = SEPARATOR.split(line);
669 
670                 // Estimated offsets and drifts
671                 transponderRecord.setTransponderId(values[2]);
672                 transponderRecord.setStationUTCOffset(NS.toSI(Double.parseDouble(values[3])));
673                 transponderRecord.setStationOscDrift(Double.parseDouble(values[4]));
674                 transponderRecord.setTranspUTCOffset(NS.toSI(Double.parseDouble(values[5])));
675                 transponderRecord.setTranspOscDrift(Double.parseDouble(values[6]));
676 
677                 // Transponder clock reference time
678                 transponderRecord.setTranspClkRefTime(Double.parseDouble(values[7]));
679 
680                 // Clock and drift indicators
681                 transponderRecord.setStationClockAndDriftApplied(Integer.parseInt(values[8]));
682                 transponderRecord.setSpacecraftClockAndDriftApplied(Integer.parseInt(values[9]));
683 
684                 // Spacecraft time simplified
685                 transponderRecord.setIsSpacecraftTimeSimplified(readBoolean(values[10]));
686 
687                 // Add the transponder configuration record
688                 pi.configurationRecords.addConfigurationRecord(transponderRecord);
689 
690             }
691 
692             /** {@inheritDoc} */
693             @Override
694             public Iterable<LineParser> allowedNext() {
695                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
696                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
697             }
698 
699         },
700 
701         /** Software configuration record. */
702         C5("C5", "c5") {
703 
704             /** {@inheritDoc} */
705             @Override
706             public void parse(final String line, final ParseInfo pi) {
707 
708                 // Initialise an empty software configuration record
709                 final SoftwareConfiguration softwareRecord = new SoftwareConfiguration();
710 
711                 // Data contained in the line
712                 final String[] values = SEPARATOR.split(line);
713 
714                 // Fill values
715                 softwareRecord.setSoftwareId(values[2]);
716                 softwareRecord.setTrackingSoftwares(COMMA.split(values[3]));
717                 softwareRecord.setTrackingSoftwareVersions(COMMA.split(values[4]));
718                 softwareRecord.setProcessingSoftwares(COMMA.split(values[5]));
719                 softwareRecord.setProcessingSoftwareVersions(COMMA.split(values[6]));
720 
721                 // Add the software configuration record
722                 pi.configurationRecords.addConfigurationRecord(softwareRecord);
723 
724             }
725 
726             /** {@inheritDoc} */
727             @Override
728             public Iterable<LineParser> allowedNext() {
729                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
730                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
731             }
732 
733         },
734 
735         /** Meteorological instrumentation configuration record. */
736         C6("C6", "c6") {
737 
738             /** {@inheritDoc} */
739             @Override
740             public void parse(final String line, final ParseInfo pi) {
741 
742                 // Initialise an empty meteorological configuration record
743                 final MeteorologicalConfiguration meteoRecord = new MeteorologicalConfiguration();
744 
745                 // Data contained in the line
746                 final String[] values = SEPARATOR.split(line);
747 
748                 // Fill values
749                 meteoRecord.setMeteorologicalId(values[2]);
750                 meteoRecord.setPressSensorManufacturer(values[3]);
751                 meteoRecord.setPressSensorModel(values[4]);
752                 meteoRecord.setPressSensorSerialNumber(values[5]);
753                 meteoRecord.setTempSensorManufacturer(values[6]);
754                 meteoRecord.setTempSensorModel(values[7]);
755                 meteoRecord.setTempSensorSerialNumber(values[8]);
756                 meteoRecord.setHumiSensorManufacturer(values[9]);
757                 meteoRecord.setHumiSensorModel(values[10]);
758                 meteoRecord.setHumiSensorSerialNumber(values[11]);
759 
760                 // Add the meteorological configuration record
761                 pi.configurationRecords.addConfigurationRecord(meteoRecord);
762 
763             }
764 
765             /** {@inheritDoc} */
766             @Override
767             public Iterable<LineParser> allowedNext() {
768                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
769                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
770             }
771 
772         },
773 
774         /** Calibration Target configuration record. */
775         C7("C7", "c7") {
776 
777             /** {@inheritDoc} */
778             @Override
779             public void parse(final String line, final ParseInfo pi) {
780 
781                 // Initialise an empty calibration target configuration record
782                 final CalibrationTargetConfiguration calibRecord = new CalibrationTargetConfiguration();
783 
784                 // Data contained in the line
785                 final String[] values = SEPARATOR.split(line);
786 
787                 // Fill values
788                 calibRecord.setConfigurationId(values[2]);
789                 calibRecord.setTargetName(values[3]);
790                 calibRecord.setSurveyedTargetDistance(Double.parseDouble(values[4]));
791                 calibRecord.setSurveyError(Double.parseDouble(values[5]) * 1e-3);  // mm --> m
792                 calibRecord.setSumOfAllConstantDelays(Double.parseDouble(values[6]));
793                 calibRecord.setPulseEnergy(Double.parseDouble(values[7]));
794                 calibRecord.setProcessingSoftwareName(values[8]);
795                 calibRecord.setProcessingSoftwareVersion(values[9]);
796 
797                 // Add the calibration target configuration record
798                 pi.configurationRecords.addConfigurationRecord(calibRecord);
799             }
800 
801             /** {@inheritDoc} */
802             @Override
803             public Iterable<LineParser> allowedNext() {
804                 return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
805                                      ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
806             }
807 
808         },
809 
810         /** Range Record (Full rate, Sampled Engineering/Quicklook). */
811         TEN("10") {
812 
813             /** {@inheritDoc} */
814             @Override
815             public void parse(final String line, final ParseInfo pi) {
816 
817                 // Data contained in the line
818                 final String[] values = SEPARATOR.split(line);
819 
820                 // Read data
821                 final double secOfDay         = Double.parseDouble(values[1]);
822                 final double timeOfFlight     = Double.parseDouble(values[2]);
823                 final String systemConfigId   = values[3];
824                 final int    epochEvent       = Integer.parseInt(values[4]);
825                 final int    filterFlag       = Integer.parseInt(values[5]);
826                 final int    detectorChannel  = Integer.parseInt(values[6]);
827                 final int    stopNumber       = Integer.parseInt(values[7]);
828                 final int    receiveAmplitude = readIntegerWithNaN(values[8], -1);
829 
830                 int transmitAmplitude = -1;
831                 if (pi.version == 2) {
832                     transmitAmplitude = readIntegerWithNaN(values[9], -1);
833                 }
834 
835                 // Initialise a new Range measurement
836                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
837                 // Check rollover
838                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
839                 final RangeMeasurement range = new FrRangeMeasurement(epoch, timeOfFlight, epochEvent, systemConfigId,
840                         filterFlag, detectorChannel, stopNumber, receiveAmplitude, transmitAmplitude);
841                 pi.dataBlock.addRangeData(range);
842 
843             }
844 
845             /** {@inheritDoc} */
846             @Override
847             public Iterable<LineParser> allowedNext() {
848                 return Arrays.asList(H8, TEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
849                                      COMPATIBILITY, COMMENTS, CUSTOM);
850             }
851 
852         },
853 
854         /** Range Record (Normal point). */
855         ELEVEN("11") {
856 
857             /** {@inheritDoc} */
858             @Override
859             public void parse(final String line, final ParseInfo pi) {
860 
861                 // Data contained in the line
862                 final String[] values = SEPARATOR.split(line);
863 
864                 // Read data
865                 final double   secOfDay          = Double.parseDouble(values[1]);
866                 final double   timeOfFlight      = Double.parseDouble(values[2]);
867                 final String   systemConfigId    = values[3];
868                 final int      epochEvent        = Integer.parseInt(values[4]);
869                 final double   windowLength      = Double.parseDouble(values[5]);
870                 final int      numberOfRawRanges = Integer.parseInt(values[6]);
871                 final double   binRms            = PS.toSI(Double.parseDouble(values[7]));
872                 final double   binSkew           = Double.parseDouble(values[8]);
873                 final double   binKurtosis       = Double.parseDouble(values[9]);
874                 final double   binPeakMinusMean  = PS.toSI(Double.parseDouble(values[10]));
875                 final double   returnRate        = Double.parseDouble(values[11]);
876                 final int      detectorChannel   = Integer.parseInt(values[12]);
877 
878                 double snr = Double.NaN;
879                 if (pi.version == 2) {
880                     snr    = Double.parseDouble(values[13]);
881                 }
882 
883                 // Initialise a new Range measurement
884                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
885                 // Check rollover
886                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
887                 final RangeMeasurement range = new NptRangeMeasurement(epoch, timeOfFlight, epochEvent, snr,
888                         systemConfigId, windowLength, numberOfRawRanges, binRms, binSkew, binKurtosis, binPeakMinusMean,
889                         returnRate, detectorChannel);
890                 pi.dataBlock.addRangeData(range);
891 
892             }
893 
894             /** {@inheritDoc} */
895             @Override
896             public Iterable<LineParser> allowedNext() {
897                 return Arrays.asList(H8, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
898                                      COMPATIBILITY, COMMENTS, CUSTOM);
899             }
900 
901         },
902 
903         /** Range Supplement Record. */
904         TWELVE("12") {
905 
906             /** {@inheritDoc} */
907             @Override
908             public void parse(final String line, final ParseInfo pi) {
909 
910                 // Data contained in the line
911                 final String[] values = SEPARATOR.split(line);
912 
913                 // Read data
914                 final double   secOfDay                   = Double.parseDouble(values[1]);
915                 final String   systemConfigId             = values[2];
916                 final double   troposphericRefractionCorr = PS.toSI(Double.parseDouble(values[3]));
917                 final double   centerOfMassCorr           = Double.parseDouble(values[4]);
918                 final double   ndFilterValue              = Double.parseDouble(values[5]);
919                 final double   timeBiasApplied            = Double.parseDouble(values[6]);
920 
921                 double rangeRate = Double.NaN;
922                 if (pi.version == 2) {
923                     rangeRate    = Double.parseDouble(values[7]);
924                 }
925 
926                 // Initialise a new Range measurement
927                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
928                 // Check rollover
929                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
930                 final RangeSupplement rangeSup = new RangeSupplement(epoch, systemConfigId, troposphericRefractionCorr,
931                         centerOfMassCorr, ndFilterValue, timeBiasApplied, rangeRate);
932                 pi.dataBlock.addRangeSupplementData(rangeSup);
933 
934             }
935 
936             /** {@inheritDoc} */
937             @Override
938             public Iterable<LineParser> allowedNext() {
939                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
940             }
941 
942         },
943 
944         /** Meteorological record. */
945         METEO("20") {
946 
947             /** {@inheritDoc} */
948             @Override
949             public void parse(final String line, final ParseInfo pi) {
950 
951                 // Data contained in the line
952                 final String[] values = SEPARATOR.split(line);
953 
954                 // Read data
955                 final double   secOfDay       = Double.parseDouble(values[1]);
956                 final double   pressure       = MBAR_TO_BAR.convert(Double.parseDouble(values[2]));
957                 final double   temperature    = Double.parseDouble(values[3]);
958                 final double   humidity       = Double.parseDouble(values[4]);
959                 final int      originOfValues = Integer.parseInt(values[5]);
960 
961                 // Initialise a new Range measurement
962                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
963                 // Check rollover
964                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
965                 final MeteorologicalMeasurement meteo = new MeteorologicalMeasurement(epoch, pressure, temperature,
966                         humidity, originOfValues);
967                 pi.dataBlock.addMeteoData(meteo);
968 
969             }
970 
971             /** {@inheritDoc} */
972             @Override
973             public Iterable<LineParser> allowedNext() {
974                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
975                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
976             }
977 
978         },
979 
980         /** Meteorological Supplement record. */
981         METEO_SUPP("21") {
982 
983             /** {@inheritDoc} */
984             @Override
985             public void parse(final String line, final ParseInfo pi) {
986                 // Not implemented yet
987             }
988 
989             /** {@inheritDoc} */
990             @Override
991             public Iterable<LineParser> allowedNext() {
992                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
993                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
994             }
995 
996         },
997 
998         /** Pointing Angle Record. */
999         ANGLES("30") {
1000 
1001             /** {@inheritDoc} */
1002             @Override
1003             public void parse(final String line, final ParseInfo pi) {
1004 
1005                 // Data contained in the line
1006                 final String[] values = SEPARATOR.split(line);
1007 
1008                 // Read data
1009                 final double  secOfDay              = Double.parseDouble(values[1]);
1010                 final double  azmiuth               = FastMath.toRadians(Double.parseDouble(values[2]));
1011                 final double  elevation             = FastMath.toRadians(Double.parseDouble(values[3]));
1012                 final int     directionFlag         = Integer.parseInt(values[4]);
1013                 final int     orginFlag             = Integer.parseInt(values[5]);
1014                 final boolean isRefractionCorrected = readBoolean(values[6]);
1015 
1016 
1017                 // Angles rates
1018                 double azimuthRate   = Double.NaN;
1019                 double elevationRate = Double.NaN;
1020                 if (pi.version == 2) {
1021                     // degrees/second ==> rad/s
1022                     azimuthRate   = FastMath.toRadians(Double.parseDouble(values[7]));
1023                     elevationRate = FastMath.toRadians(Double.parseDouble(values[8]));
1024                 }
1025 
1026                 // Initialise a new angles measurement
1027                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
1028                 // Check rollover
1029                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
1030                 final AnglesMeasurement angles = new AnglesMeasurement(epoch, azmiuth, elevation,
1031                         directionFlag, orginFlag,
1032                         isRefractionCorrected,
1033                         azimuthRate, elevationRate);
1034                 pi.dataBlock.addAnglesData(angles);
1035 
1036             }
1037 
1038             /** {@inheritDoc} */
1039             @Override
1040             public Iterable<LineParser> allowedNext() {
1041                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1042                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1043             }
1044 
1045         },
1046 
1047         /** Calibration Record. */
1048         CALIB("40") {
1049 
1050             /** {@inheritDoc} */
1051             @Override
1052             public void parse(final String line, final ParseInfo pi) {
1053 
1054                 // Data contained in the line
1055                 final String[] values = SEPARATOR.split(line);
1056 
1057                 // Read data
1058                 final double   secOfDay               = Double.parseDouble(values[1]);
1059                 final int      typeOfData             = Integer.parseInt(values[2]);
1060                 final String   systemConfigId         = values[3];
1061                 final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
1062                 final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
1063                 final double   oneWayDistance         = Double.parseDouble(values[6]);
1064                 final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
1065                 final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
1066                 final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
1067                 final double   skew                   = Double.parseDouble(values[10]);
1068                 final double   kurtosis               = Double.parseDouble(values[11]);
1069                 final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
1070                 final int      typeIndicator          = Integer.parseInt(values[13]);
1071                 final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
1072                 final int      detectorChannel        = Integer.parseInt(values[15]);
1073 
1074                 // Check file version for additional data
1075                 int    span       = 0;
1076                 double returnRate = Double.NaN;
1077                 if (pi.version == 2) {
1078                     // fixed 20230321
1079                     // the span may be "na"
1080                     span       = readIntegerWithNaN(values[16], -1);
1081                     returnRate = Double.parseDouble(values[17]);
1082                 }
1083 
1084                 // Initialise a new angles measurement
1085                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
1086                 // Check rollover
1087                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
1088                 final Calibration cal = new Calibration(epoch, typeOfData, systemConfigId, numberOfPointsRecorded,
1089                         numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew, kurtosis, peakMinusMean,
1090                         typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
1091                 pi.dataBlock.addCalibrationData(cal);
1092 
1093             }
1094 
1095             /** {@inheritDoc} */
1096             @Override
1097             public Iterable<LineParser> allowedNext() {
1098                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1099                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1100             }
1101 
1102         },
1103 
1104         /** Calibration Details Record. */
1105         CALIB_DETAILS("41") {
1106 
1107             /** {@inheritDoc} */
1108             @Override
1109             public void parse(final String line, final ParseInfo pi) {
1110 
1111                 // Data contained in the line
1112                 final String[] values = SEPARATOR.split(line);
1113 
1114                 // Read data
1115                 final double   secOfDay               = Double.parseDouble(values[1]);
1116                 final int      typeOfData             = Integer.parseInt(values[2]);
1117                 final String   systemConfigId         = values[3];
1118                 final int      numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
1119                 final int      numberOfPointsUsed     = readIntegerWithNaN(values[5], -1);
1120                 final double   oneWayDistance         = Double.parseDouble(values[6]);
1121                 final double   systemDelay            = PS.toSI(Double.parseDouble(values[7]));
1122                 final double   delayShift             = PS.toSI(Double.parseDouble(values[8]));
1123                 final double   rms                    = PS.toSI(Double.parseDouble(values[9]));
1124                 final double   skew                   = Double.parseDouble(values[10]);
1125                 final double   kurtosis               = Double.parseDouble(values[11]);
1126                 final double   peakMinusMean          = PS.toSI(Double.parseDouble(values[12]));
1127                 final int      typeIndicator          = Integer.parseInt(values[13]);
1128                 final int      shiftTypeIndicator     = Integer.parseInt(values[14]);
1129                 final int      detectorChannel        = Integer.parseInt(values[15]);
1130 
1131                 // Check file version for additional data
1132                 int    span       = 0;
1133                 double returnRate = Double.NaN;
1134                 if (pi.version == 2) {
1135                     span       = Integer.parseInt(values[16]);
1136                     returnRate = Double.parseDouble(values[17]);
1137                 }
1138 
1139                 // Initialise a new angles measurement
1140                 AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
1141                 // Check rollover
1142                 epoch = checkRollover(epoch, pi.header.getStartEpoch());
1143                 final CalibrationDetail cal = new CalibrationDetail(epoch, typeOfData, systemConfigId,
1144                         numberOfPointsRecorded, numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew,
1145                         kurtosis, peakMinusMean, typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
1146                 pi.dataBlock.addCalibrationDetailData(cal);
1147 
1148             }
1149 
1150             /** {@inheritDoc} */
1151             @Override
1152             public Iterable<LineParser> allowedNext() {
1153                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1154                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1155             }
1156 
1157         },
1158 
1159         /** Calibration "Shot" Record. */
1160         CALIB_SHOT("42") {
1161 
1162             /** {@inheritDoc} */
1163             @Override
1164             public void parse(final String line, final ParseInfo pi) {
1165                 // Not implemented yet
1166             }
1167 
1168             /** {@inheritDoc} */
1169             @Override
1170             public Iterable<LineParser> allowedNext() {
1171                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1172                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1173             }
1174 
1175         },
1176 
1177         /** Session (Pass) Statistics Record. */
1178         STAT("50") {
1179 
1180             /** {@inheritDoc} */
1181             @Override
1182             public void parse(final String line, final ParseInfo pi) {
1183 
1184                 // Data contained in the line
1185                 final String[] values = SEPARATOR.split(line);
1186 
1187                 // Read data
1188                 final String systemConfigId    = values[1];
1189                 final double rms               = PS.toSI(Double.parseDouble(values[2]));
1190                 final double skewness          = Double.parseDouble(values[3]);
1191                 final double kurtosis          = Double.parseDouble(values[4]);
1192                 //
1193                 // The peak minus mean may be "*"
1194                 // 50 shao     35.0  -0.509   2.221 ****** 0
1195                 final double peakMinusMean = values[5].contains("*") ? Double.NaN : PS.toSI(Double.parseDouble(values[5]));
1196 
1197                 final int dataQualityIndicator = Integer.parseInt(values[6]);
1198 
1199                 final SessionStatistics stat = new SessionStatistics(systemConfigId, rms, skewness, kurtosis, peakMinusMean,
1200                         dataQualityIndicator);
1201                 pi.dataBlock.addSessionStatisticsData(stat);
1202 
1203             }
1204 
1205             /** {@inheritDoc} */
1206             @Override
1207             public Iterable<LineParser> allowedNext() {
1208                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1209                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1210             }
1211 
1212         },
1213 
1214         /** Compatibility record. */
1215         COMPATIBILITY("60") {
1216 
1217             /** {@inheritDoc} */
1218             @Override
1219             public void parse(final String line, final ParseInfo pi) {
1220                 // Not implemented yet
1221             }
1222 
1223             /** {@inheritDoc} */
1224             @Override
1225             public Iterable<LineParser> allowedNext() {
1226                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1227                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1228             }
1229 
1230         },
1231 
1232         /** Comments. */
1233         COMMENTS(COMMENTS_IDENTIFIER) {
1234 
1235             /** {@inheritDoc} */
1236             @Override
1237             public void parse(final String line, final ParseInfo pi) {
1238 
1239                 // Comment
1240                 final String comment = line.substring(2).trim();
1241                 pi.file.getComments().add(comment);
1242 
1243             }
1244 
1245             /** {@inheritDoc} */
1246             @Override
1247             public Iterable<LineParser> allowedNext() {
1248                 return Arrays.asList(H1, H2, H3, H4, H5, H8, H9, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO,
1249                         METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1250 
1251             }
1252 
1253         },
1254 
1255         /** Custom. */
1256         CUSTOM("9\\d") {
1257 
1258             /** {@inheritDoc} */
1259             @Override
1260             public void parse(final String line, final ParseInfo pi) {
1261                 // Not implemented yet
1262             }
1263 
1264             /** {@inheritDoc} */
1265             public Iterable<LineParser> allowedNext() {
1266                 return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
1267                                      STAT, COMPATIBILITY, COMMENTS, CUSTOM);
1268 
1269             }
1270 
1271         },
1272 
1273         /** End of data block. */
1274         H8("H8", "h8") {
1275 
1276             /** {@inheritDoc} */
1277             @Override
1278             public void parse(final String line, final ParseInfo pi) {
1279 
1280                 // fixed 2022-12-12
1281                 // For the case of monthE is -1.
1282                 // Use the date of the last range data as the end epoch.
1283                 if (pi.header.getEndEpoch() == null) {
1284                     final List<RangeMeasurement> rangeData =  pi.dataBlock.getRangeData();
1285                     pi.header.setEndEpoch(rangeData.get(rangeData.size() - 1).getDate());
1286                 }
1287 
1288                 // Fill data block
1289                 pi.dataBlock.setHeader(pi.header);
1290                 pi.dataBlock.setConfigurationRecords(pi.configurationRecords);
1291 
1292                 // Add the data block to the CRD file
1293                 pi.file.addDataBlock(pi.dataBlock);
1294 
1295                 // Initialize a new empty containers
1296                 pi.startEpochDateComponents           = DateComponents.J2000_EPOCH;
1297                 final CRDHeader lastHeader  = pi.header;
1298                 pi.header               = new CRDHeader();
1299                 pi.configurationRecords = new CRDConfiguration();
1300                 pi.dataBlock            = new CRDDataBlock();
1301 
1302                 // fill header with H1 H2 H3 if the file is for many targets, single system
1303                 // configuration (see P31 in crd201)
1304                 pi.header.setFormat(lastHeader.getFormat());
1305                 pi.header.setVersion(lastHeader.getVersion());
1306                 pi.header.setProductionEpoch(lastHeader.getProductionEpoch());
1307                 pi.header.setProductionHour(lastHeader.getProductionHour());
1308 
1309                 pi.header.setStationName(lastHeader.getStationName());
1310                 pi.header.setSystemIdentifier(lastHeader.getSystemIdentifier());
1311                 pi.header.setSystemNumber(lastHeader.getSystemNumber());
1312                 pi.header.setSystemOccupancy(lastHeader.getSystemOccupancy());
1313                 pi.header.setEpochIdentifier(lastHeader.getEpochIdentifier());
1314                 pi.header.setStationNetword(lastHeader.getStationNetword());
1315 
1316                 pi.header.setName(lastHeader.getName());
1317                 pi.header.setIlrsSatelliteId(lastHeader.getIlrsSatelliteId());
1318                 pi.header.setSic(lastHeader.getSic());
1319                 pi.header.setNoradId(lastHeader.getNoradId());
1320                 pi.header.setSpacecraftEpochTimeScale(lastHeader.getSpacecraftEpochTimeScale());
1321                 pi.header.setTargetClass(lastHeader.getTargetClass());
1322                 pi.header.setTargetLocation(lastHeader.getTargetLocation());
1323 
1324             }
1325 
1326             /** {@inheritDoc} */
1327             @Override
1328             public Iterable<LineParser> allowedNext() {
1329                 return Arrays.asList(H1, H4, H9, COMMENTS);
1330             }
1331 
1332         },
1333 
1334         /** Last record in file. */
1335         H9("H9", "h9") {
1336 
1337             /** {@inheritDoc} */
1338             @Override
1339             public void parse(final String line, final ParseInfo pi) {
1340                 pi.done = true;
1341             }
1342 
1343             /** {@inheritDoc} */
1344             @Override
1345             public Iterable<LineParser> allowedNext() {
1346                 return Collections.singleton(H9);
1347             }
1348 
1349         };
1350 
1351         /** Patterns for identifying line. */
1352         private final Pattern[] patterns;
1353 
1354         /** Identifiers. */
1355         private final String[] identifiers;
1356 
1357         /** Simple constructor.
1358          * @param identifier regular expression for identifying line (i.e. first element)
1359          */
1360         LineParser(final String... identifier) {
1361             this.identifiers = identifier;
1362             // Initialise patterns
1363             this.patterns    = new Pattern[identifiers.length];
1364             for (int index = 0; index < patterns.length; index++) {
1365                 patterns[index] = Pattern.compile(identifiers[index]);
1366             }
1367         }
1368 
1369         /** Parse a line.
1370          * @param line line to parse
1371          * @param pi holder for transient data
1372          */
1373         public abstract void parse(String line, ParseInfo pi);
1374 
1375         /** Get the allowed parsers for next line.
1376          * @return allowed parsers for next line
1377          */
1378         public abstract Iterable<LineParser> allowedNext();
1379 
1380         /** Check if parser can handle line.
1381          * @param line line to parse
1382          * @return true if parser can handle the specified line
1383          */
1384         public boolean canHandle(final String line) {
1385             // Line identifier
1386             final String lineId = SEPARATOR.split(line)[0];
1387             // Loop on patterns
1388             for (Pattern pattern : patterns) {
1389                 if (pattern.matcher(lineId).matches()) {
1390                     return true;
1391                 }
1392             }
1393             // No match
1394             return false;
1395         }
1396 
1397         /**
1398          * Read a boolean from a string value.
1399          * @param value input value
1400          * @return the correspondin boolean
1401          */
1402         private static boolean readBoolean(final String value) {
1403             return Integer.parseInt(value) == 1;
1404         }
1405 
1406         /**
1407          * Read an integer value taking into consideration a possible "NaN".
1408          * If the value is "NaN", the defaultValue is returned.
1409          * @param value input string
1410          * @param defaultValue the default value
1411          * @return the corresponding integer value
1412          */
1413         private static int readIntegerWithNaN(final String value, final int defaultValue) {
1414             return CRD.STR_NAN.equalsIgnoreCase(value) ? defaultValue : Integer.parseInt(value);
1415         }
1416     }
1417 
1418 }