1   /* Copyright 2002-2022 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.Optional;
22  import java.util.regex.Pattern;
23  import java.util.stream.Stream;
24  
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.MeteorologicalMeasurement;
34  import org.orekit.files.ilrs.CRD.RangeMeasurement;
35  import org.orekit.files.ilrs.CRDConfiguration.DetectorConfiguration;
36  import org.orekit.files.ilrs.CRDConfiguration.LaserConfiguration;
37  import org.orekit.files.ilrs.CRDConfiguration.MeteorologicalConfiguration;
38  import org.orekit.files.ilrs.CRDConfiguration.SoftwareConfiguration;
39  import org.orekit.files.ilrs.CRDConfiguration.SystemConfiguration;
40  import org.orekit.files.ilrs.CRDConfiguration.TimingSystemConfiguration;
41  import org.orekit.files.ilrs.CRDConfiguration.TransponderConfiguration;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.DateComponents;
44  import org.orekit.time.TimeComponents;
45  import org.orekit.time.TimeScale;
46  import org.orekit.utils.units.Unit;
47  import org.orekit.utils.units.UnitsConverter;
48  
49  /**
50   * A parser for the CRD data file format.
51   * <p>
52   * It supports both 1.0 and 2.0 versions
53   * <p>
54   * <b>Note</b>: Not all the records are read by the parser. Only the most significants are parsed.
55   * Contributions are welcome to support more fields in the format.
56   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2009/crd_v1.01.pdf">1.0 file format</a>
57   * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2019/crd_v2.01.pdf">2.0 file format</a>
58   * @author Bryan Cazabonne
59   * @since 10.3
60   */
61  public class CRDParser {
62  
63      /** Default supported files name pattern for CRD files. */
64      public static final String DEFAULT_CRD_SUPPORTED_NAMES = "^(?!0+$)\\w{1,12}\\_\\d{6,8}.\\w{3}$";
65  
66      /** Nanometers units. */
67      private static final Unit NM = Unit.parse("nm");
68  
69      /** Kilohertz units. */
70      private static final Unit KHZ = Unit.parse("kHz");
71  
72      /** Microseconds units. */
73      private static final Unit US = Unit.parse("µs");
74  
75      /** mbar to bar converter. */
76      private static final UnitsConverter MBAR_TO_BAR = new UnitsConverter(Unit.parse("mbar"), Unit.parse("bar"));
77  
78      /** File format. */
79      private static final String FILE_FORMAT = "CRD";
80  
81      /** Pattern for delimiting regular expressions. */
82      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
83  
84      /** Pattern for delimiting expressions with comma. */
85      private static final Pattern COMMA = Pattern.compile(",");
86  
87      /** Time scale used to define epochs in CPF file. */
88      private final TimeScale timeScale;
89  
90      /**
91       * Default constructor.
92       * <p>
93       * This constructor uses the {@link DataContext#getDefault() default data context}.
94       */
95      @DefaultDataContext
96      public CRDParser() {
97          this(DataContext.getDefault().getTimeScales().getUTC());
98      }
99  
100     /**
101      * Constructor.
102      * @param utc utc time scale to read epochs
103      */
104     public CRDParser(final TimeScale utc) {
105         this.timeScale = utc;
106     }
107 
108     /**
109      * Get the time scale used to read the file.
110      * @return the time scale used to read the file
111      */
112     public TimeScale getTimeScale() {
113         return timeScale;
114     }
115 
116     /**
117      * Parse a CRD file.
118      * @param source data source containing the CRD file.
119      * @return a parsed CRD file.
120      * @throws IOException if {@code reader} throws one.
121      */
122     public CRD parse(final DataSource source) throws IOException {
123 
124         // Initialize internal data structures
125         final ParseInfo pi = new ParseInfo();
126 
127         int lineNumber = 0;
128         Stream<LineParser> crdParsers = Stream.of(LineParser.H1);
129         try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
130             for (String line = reader.readLine(); line != null; line = reader.readLine()) {
131                 ++lineNumber;
132                 final String l = line;
133                 final Optional<LineParser> selected = crdParsers.filter(p -> p.canHandle(l)).findFirst();
134                 if (selected.isPresent()) {
135                     try {
136                         selected.get().parse(line, pi);
137                     } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
138                         throw new OrekitException(e,
139                                 OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
140                                 lineNumber, source.getName(), line);
141                     }
142                     crdParsers = selected.get().allowedNext();
143                 }
144                 if (pi.done) {
145                     // Return file
146                     return pi.file;
147                 }
148             }
149         }
150 
151         // We never reached the EOF marker
152         throw new OrekitException(OrekitMessages.CRD_UNEXPECTED_END_OF_FILE, lineNumber);
153 
154     }
155 
156     /**
157      * Computes if a day shift has happened comparing the current and past epoch, described by seconds in the day.
158      * This is useful as the data is sorted in the chronological order inside the file.
159      *
160      * @param lastSecOfDay
161      * @param secOfDay
162      * @return Boolean true if change in day.
163      */
164     private static int checkRollover(final double lastSecOfDay, final double secOfDay) {
165         return (secOfDay > lastSecOfDay) ? 0 : 1;
166     }
167 
168     /** Transient data used for parsing a CRD file. The data is kept in a
169      * separate data structure to make the parser thread-safe.
170      * <p><b>Note</b>: The class intentionally does not provide accessor
171      * methods, as it is only used internally for parsing a CRD file.</p>
172      */
173     private class ParseInfo {
174 
175         /** The corresponding CDR file. */
176         private CRD file;
177 
178         /** Version. */
179         private int version;
180 
181         /** The current data block. */
182         private CRDDataBlock dataBlock;
183 
184         /** Data block header. */
185         private CRDHeader header;
186 
187         /** Cofiguration records. */
188         private CRDConfiguration configurationRecords;
189 
190         /** Time scale. */
191         private TimeScale timeScale;
192 
193         /** Current data block start epoch. */
194         private DateComponents startEpoch;
195 
196         /** End Of File reached indicator. */
197         private boolean done;
198 
199         /**
200          * Constructor.
201          */
202         protected ParseInfo() {
203 
204             // Initialise default values
205             this.done       = false;
206             this.version    = 1;
207             this.startEpoch = DateComponents.J2000_EPOCH;
208 
209             // Initialise empty object
210             this.file                 = new CRD();
211             this.header               = new CRDHeader();
212             this.configurationRecords = new CRDConfiguration();
213             this.dataBlock            = new CRDDataBlock();
214 
215             // Time scale
216             this.timeScale = CRDParser.this.timeScale;
217 
218         }
219 
220     }
221 
222     /** Parsers for specific lines. */
223     private enum LineParser {
224 
225         /** Format header. */
226         H1("H1", "h1") {
227 
228             /** {@inheritDoc} */
229             @Override
230             public void parse(final String line, final ParseInfo pi) {
231 
232                 // Data contained in the line
233                 final String[] values = SEPARATOR.split(line);
234 
235                 // Format and version
236                 final String format = values[1];
237                 pi.version = Integer.parseInt(values[2]);
238 
239                 // Throw an exception if format is not equal to "CRD"
240                 if (!format.equalsIgnoreCase(FILE_FORMAT)) {
241                     throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
242                 }
243 
244                 // Fill first elements
245                 pi.header.setFormat(format);
246                 pi.header.setVersion(pi.version);
247 
248                 // Epoch of ephemeris production
249                 final int year  = Integer.parseInt(values[3]);
250                 final int month = Integer.parseInt(values[4]);
251                 final int day   = Integer.parseInt(values[5]);
252                 pi.header.setProductionEpoch(new DateComponents(year, month, day));
253 
254                 // Hour of ephemeris production
255                 pi.header.setProductionHour(Integer.parseInt(values[6]));
256 
257             }
258 
259             /** {@inheritDoc} */
260             @Override
261             public Stream<LineParser> allowedNext() {
262                 return Stream.of(H2, COMMENTS);
263             }
264 
265         },
266 
267         /** Format header. */
268         H2("H2", "h2") {
269 
270             /** {@inheritDoc} */
271             @Override
272             public void parse(final String line, final ParseInfo pi) {
273 
274                 // Data contained in the line
275                 final String[] values = SEPARATOR.split(line);
276 
277                 // Station name
278                 pi.header.setStationName(values[1]);
279 
280                 // Crustal Dynamics Project keys
281                 pi.header.setSystemIdentifier(Integer.parseInt(values[2]));
282                 pi.header.setSystemNumber(Integer.parseInt(values[3]));
283                 pi.header.setSystemOccupancy(Integer.parseInt(values[4]));
284 
285                 // Station epoch time scale
286                 pi.header.setEpochIdentifier(Integer.parseInt(values[5]));
287 
288                 // Station network
289                 if (pi.version == 2) {
290                     pi.header.setStationNetword(values[6]);
291                 }
292 
293             }
294 
295             /** {@inheritDoc} */
296             @Override
297             public Stream<LineParser> allowedNext() {
298                 return Stream.of(H3, COMMENTS);
299             }
300 
301         },
302 
303         /** Format header. */
304         H3("H3", "h3") {
305 
306             /** {@inheritDoc} */
307             @Override
308             public void parse(final String line, final ParseInfo pi) {
309 
310                 // Data contained in the line
311                 final String[] values = SEPARATOR.split(line);
312 
313                 // Target name
314                 pi.header.setName(values[1]);
315 
316                 // Identifiers
317                 pi.header.setIlrsSatelliteId(values[2]);
318                 pi.header.setSic(values[3]);
319                 pi.header.setNoradId(values[4]);
320 
321                 // Spacecraft Epoch Time Scale
322                 pi.header.setSpacecraftEpochTimeScale(Integer.parseInt(values[5]));
323 
324                 // Target class and location (if needed)
325                 pi.header.setTargetClass(Integer.parseInt(values[6]));
326                 if (pi.version == 2) {
327                     pi.header.setTargetLocation(Integer.parseInt(values[7]));
328                 }
329 
330             }
331 
332             /** {@inheritDoc} */
333             @Override
334             public Stream<LineParser> allowedNext() {
335                 return Stream.of(H4, COMMENTS);
336             }
337 
338         },
339 
340         /** Format header. */
341         H4("H4", "h4") {
342 
343             /** {@inheritDoc} */
344             @Override
345             public void parse(final String line, final ParseInfo pi) {
346 
347                 // Data contained in the line
348                 final String[] values = SEPARATOR.split(line);
349 
350                 // Data type
351                 pi.header.setDataType(Integer.parseInt(values[1]));
352 
353                 // Start epoch
354                 final int    yearS   = Integer.parseInt(values[2]);
355                 final int    monthS  = Integer.parseInt(values[3]);
356                 final int    dayS    = Integer.parseInt(values[4]);
357                 final int    hourS   = Integer.parseInt(values[5]);
358                 final int    minuteS = Integer.parseInt(values[6]);
359                 final double secondS = Integer.parseInt(values[7]);
360 
361                 pi.startEpoch = new DateComponents(yearS, monthS, dayS);
362 
363                 pi.header.setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
364                         hourS, minuteS, secondS,
365                         pi.timeScale));
366 
367                 // End epoch
368                 final int    yearE   = Integer.parseInt(values[8]);
369                 final int    monthE  = Integer.parseInt(values[9]);
370                 final int    dayE    = Integer.parseInt(values[10]);
371                 final int    hourE   = Integer.parseInt(values[11]);
372                 final int    minuteE = Integer.parseInt(values[12]);
373                 final double secondE = Integer.parseInt(values[13]);
374 
375                 pi.header.setEndEpoch(new AbsoluteDate(yearE, monthE, dayE,
376                         hourE, minuteE, secondE,
377                         pi.timeScale));
378 
379                 // Data release
380                 pi.header.setDataReleaseFlag(Integer.parseInt(values[14]));
381 
382                 // Correction flags
383                 pi.header.setIsTroposphericRefractionApplied(readBoolean(values[15]));
384                 pi.header.setIsCenterOfMassCorrectionApplied(readBoolean(values[16]));
385                 pi.header.setIsReceiveAmplitudeCorrectionApplied(readBoolean(values[17]));
386                 pi.header.setIsStationSystemDelayApplied(readBoolean(values[18]));
387                 pi.header.setIsTransponderDelayApplied(readBoolean(values[19]));
388 
389                 // Range type indicator
390                 pi.header.setRangeType(Integer.parseInt(values[20]));
391 
392                 // Data quality indicator
393                 pi.header.setQualityIndicator(Integer.parseInt(values[21]));
394 
395             }
396 
397             /** {@inheritDoc} */
398             @Override
399             public Stream<LineParser> allowedNext() {
400                 return Stream.of(H5, C0, COMMENTS);
401             }
402 
403         },
404 
405         /** Format header. */
406         H5("H5", "h5") {
407 
408             /** {@inheritDoc} */
409             @Override
410             public void parse(final String line, final ParseInfo pi) {
411 
412                 // Data contained in the line
413                 final String[] values = SEPARATOR.split(line);
414 
415                 // Fill data
416                 pi.header.setPredictionType(Integer.parseInt(values[1]));
417                 pi.header.setYearOfCentury(Integer.parseInt(values[2]));
418                 pi.header.setDateAndTime(values[3]);
419                 pi.header.setPredictionProvider(values[4]);
420                 pi.header.setSequenceNumber(Integer.parseInt(values[5]));
421 
422             }
423 
424             /** {@inheritDoc} */
425             @Override
426             public Stream<LineParser> allowedNext() {
427                 return Stream.of(C0, COMMENTS);
428             }
429 
430         },
431 
432         /** System configuration record. */
433         C0("C0", "c0") {
434 
435             /** {@inheritDoc} */
436             @Override
437             public void parse(final String line, final ParseInfo pi) {
438 
439                 // Initialise an empty system configuration record
440                 final SystemConfiguration systemRecord = new SystemConfiguration();
441 
442                 // Data contained in the line
443                 final String[] values = SEPARATOR.split(line);
444 
445                 // Wavelength
446                 systemRecord.setWavelength(NM.toSI(Double.parseDouble(values[2])));
447 
448                 // System ID
449                 systemRecord.setSystemId(values[3]);
450 
451                 // Set the system configuration record
452                 pi.configurationRecords.setSystemRecord(systemRecord);
453 
454             }
455 
456             /** {@inheritDoc} */
457             @Override
458             public Stream<LineParser> allowedNext() {
459                 return Stream.of(C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
460             }
461 
462         },
463 
464 
465         /** Laser configuration record. */
466         C1("C1", "c1") {
467 
468             /** {@inheritDoc} */
469             @Override
470             public void parse(final String line, final ParseInfo pi) {
471 
472                 // Initialise an empty laser configuration record
473                 final LaserConfiguration laserRecord = new LaserConfiguration();
474 
475                 // Data contained in the line
476                 final String[] values = SEPARATOR.split(line);
477 
478                 // Fill values
479                 laserRecord.setLaserId(values[2]);
480                 laserRecord.setLaserType(values[3]);
481                 laserRecord.setPrimaryWavelength(NM.toSI(Double.parseDouble(values[4])));
482                 laserRecord.setNominalFireRate(Double.parseDouble(values[5]));
483                 laserRecord.setPulseEnergy(Double.parseDouble(values[6]));
484                 laserRecord.setPulseWidth(Double.parseDouble(values[7]));
485                 laserRecord.setBeamDivergence(Double.parseDouble(values[8]));
486                 laserRecord.setPulseInOutgoingSemiTrain(Integer.parseInt(values[9]));
487 
488                 // Set the laser configuration record
489                 pi.configurationRecords.setLaserRecord(laserRecord);
490 
491             }
492 
493             /** {@inheritDoc} */
494             @Override
495             public Stream<LineParser> allowedNext() {
496                 return Stream.of(C2, C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
497             }
498 
499         },
500 
501         /** Detector configuration record. */
502         C2("C2", "c2") {
503 
504             /** {@inheritDoc} */
505             @Override
506             public void parse(final String line, final ParseInfo pi) {
507 
508                 // Initialise an empty detector configuration record
509                 final DetectorConfiguration detectorRecord = new DetectorConfiguration();
510 
511                 // Data contained in the line
512                 final String[] values = SEPARATOR.split(line);
513 
514                 // Fill values
515                 detectorRecord.setDetectorId(values[2]);
516                 detectorRecord.setDetectorType(values[3]);
517                 detectorRecord.setApplicableWavelength(NM.toSI(Double.parseDouble(values[4])));
518                 detectorRecord.setQuantumEfficiency(Double.parseDouble(values[5]));
519                 detectorRecord.setAppliedVoltage(Double.parseDouble(values[6]));
520                 detectorRecord.setDarkCount(KHZ.toSI(Double.parseDouble(values[7])));
521                 detectorRecord.setOutputPulseType(values[8]);
522                 detectorRecord.setOutputPulseWidth(Double.parseDouble(values[9]));
523                 detectorRecord.setSpectralFilter(NM.toSI(Double.parseDouble(values[10])));
524                 detectorRecord.setTransmissionOfSpectralFilter(Double.parseDouble(values[11]));
525                 detectorRecord.setSpatialFilter(Double.parseDouble(values[12]));
526                 detectorRecord.setExternalSignalProcessing(values[13]);
527 
528                 // Check file version for additional data
529                 if (pi.version == 2) {
530                     detectorRecord.setAmplifierGain(Double.parseDouble(values[14]));
531                     detectorRecord.setAmplifierBandwidth(KHZ.toSI(Double.parseDouble(values[15])));
532                     detectorRecord.setAmplifierInUse(values[16]);
533                 }
534 
535                 // Set the detector configuration record
536                 pi.configurationRecords.setDetectorRecord(detectorRecord);
537 
538             }
539 
540             /** {@inheritDoc} */
541             @Override
542             public Stream<LineParser> allowedNext() {
543                 return Stream.of(C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
544             }
545 
546         },
547 
548         /** Timing system configuration record. */
549         C3("C3", "c3") {
550 
551             /** {@inheritDoc} */
552             @Override
553             public void parse(final String line, final ParseInfo pi) {
554 
555                 // Initialise an empty timing system configuration record
556                 final TimingSystemConfiguration timingRecord = new TimingSystemConfiguration();
557 
558                 // Data contained in the line
559                 final String[] values = SEPARATOR.split(line);
560 
561                 // Fill values
562                 timingRecord.setLocalTimingId(values[2]);
563                 timingRecord.setTimeSource(values[3]);
564                 timingRecord.setFrequencySource(values[4]);
565                 timingRecord.setTimer(values[5]);
566                 timingRecord.setTimerSerialNumber(values[6]);
567                 timingRecord.setEpochDelayCorrection(US.toSI(Double.parseDouble(values[7])));
568 
569                 // Set the timing system configuration record
570                 pi.configurationRecords.setTimingRecord(timingRecord);
571 
572             }
573 
574             /** {@inheritDoc} */
575             @Override
576             public Stream<LineParser> allowedNext() {
577                 return Stream.of(C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
578             }
579 
580         },
581 
582         /** Transponder configuration record. */
583         C4("C4", "c4") {
584 
585             /** {@inheritDoc} */
586             @Override
587             public void parse(final String line, final ParseInfo pi) {
588 
589                 // Initialise an empty transponder configuration record
590                 final TransponderConfiguration transponderRecord = new TransponderConfiguration();
591 
592                 // Data contained in the line
593                 final String[] values = SEPARATOR.split(line);
594 
595                 // Estimated offsets and drifts
596                 transponderRecord.setTransponderId(values[2]);
597                 transponderRecord.setStationUTCOffset(NM.toSI(Double.parseDouble(values[3])));
598                 transponderRecord.setStationOscDrift(Double.parseDouble(values[4]));
599                 transponderRecord.setTranspUTCOffset(NM.toSI(Double.parseDouble(values[5])));
600                 transponderRecord.setTranspOscDrift(Double.parseDouble(values[6]));
601 
602                 // Transponder clock reference time
603                 transponderRecord.setTranspClkRefTime(Double.parseDouble(values[7]));
604 
605                 // Clock and drift indicators
606                 transponderRecord.setStationClockAndDriftApplied(Integer.parseInt(values[8]));
607                 transponderRecord.setSpacecraftClockAndDriftApplied(Integer.parseInt(values[9]));
608 
609                 // Spacecraft time simplified
610                 transponderRecord.setIsSpacecraftTimeSimplified(readBoolean(values[10]));
611 
612                 // Set the transponder configuration record
613                 pi.configurationRecords.setTransponderRecord(transponderRecord);
614 
615             }
616 
617             /** {@inheritDoc} */
618             @Override
619             public Stream<LineParser> allowedNext() {
620                 return Stream.of(C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
621             }
622 
623         },
624 
625         /** Software configuration record. */
626         C5("C5", "c5") {
627 
628             /** {@inheritDoc} */
629             @Override
630             public void parse(final String line, final ParseInfo pi) {
631 
632                 // Initialise an empty software configuration record
633                 final SoftwareConfiguration softwareRecord = new SoftwareConfiguration();
634 
635                 // Data contained in the line
636                 final String[] values = SEPARATOR.split(line);
637 
638                 // Fill values
639                 softwareRecord.setSoftwareId(values[2]);
640                 softwareRecord.setTrackingSoftwares(COMMA.split(values[3]));
641                 softwareRecord.setTrackingSoftwareVersions(COMMA.split(values[4]));
642                 softwareRecord.setProcessingSoftwares(COMMA.split(values[5]));
643                 softwareRecord.setProcessingSoftwareVersions(COMMA.split(values[6]));
644 
645                 // Set the software configuration record
646                 pi.configurationRecords.setSoftwareRecord(softwareRecord);
647 
648             }
649 
650             /** {@inheritDoc} */
651             @Override
652             public Stream<LineParser> allowedNext() {
653                 return Stream.of(C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
654             }
655 
656         },
657 
658         /** Meteorological instrumentation configuration record. */
659         C6("C6", "c6") {
660 
661             /** {@inheritDoc} */
662             @Override
663             public void parse(final String line, final ParseInfo pi) {
664 
665                 // Initialise an empty meteorological configuration record
666                 final MeteorologicalConfiguration meteoRecord = new MeteorologicalConfiguration();
667 
668                 // Data contained in the line
669                 final String[] values = SEPARATOR.split(line);
670 
671                 // Fill values
672                 meteoRecord.setMeteorologicalId(values[2]);
673                 meteoRecord.setPressSensorManufacturer(values[3]);
674                 meteoRecord.setPressSensorModel(values[4]);
675                 meteoRecord.setPressSensorSerialNumber(values[5]);
676                 meteoRecord.setTempSensorManufacturer(values[6]);
677                 meteoRecord.setTempSensorModel(values[7]);
678                 meteoRecord.setTempSensorSerialNumber(values[8]);
679                 meteoRecord.setHumiSensorManufacturer(values[9]);
680                 meteoRecord.setHumiSensorModel(values[10]);
681                 meteoRecord.setHumiSensorSerialNumber(values[11]);
682 
683                 // Set the meteorological configuration record
684                 pi.configurationRecords.setMeteorologicalRecord(meteoRecord);
685 
686             }
687 
688             /** {@inheritDoc} */
689             @Override
690             public Stream<LineParser> allowedNext() {
691                 return Stream.of(C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
692             }
693 
694         },
695 
696         /** Calibration Target configuration record. */
697         C7("C7", "c7") {
698 
699             /** {@inheritDoc} */
700             @Override
701             public void parse(final String line, final ParseInfo pi) {
702                 // Not implemented yet
703             }
704 
705             /** {@inheritDoc} */
706             @Override
707             public Stream<LineParser> allowedNext() {
708                 return Stream.of(TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
709             }
710 
711         },
712 
713         /** Range Record (Full rate, Sampled Engineering/Quicklook). */
714         TEN("10") {
715 
716             /** Storage for the last epoch parsed to compare with the current.*/
717             private double lastSecOfDay = 0;
718 
719             /** Shift in days due to the rollover.*/
720             private int dayShift = 0;
721 
722             /** {@inheritDoc} */
723             @Override
724             public void parse(final String line, final ParseInfo pi) {
725 
726                 // Data contained in the line
727                 final String[] values = SEPARATOR.split(line);
728 
729                 // Read data
730                 final double secOfDay     = Double.parseDouble(values[1]);
731                 final double timeOfFlight = Double.parseDouble(values[2]);
732                 final int    epochEvent   = Integer.parseInt(values[4]);
733 
734                 // Check secOfDay for rollover
735                 dayShift = dayShift + checkRollover(lastSecOfDay, secOfDay);
736                 // Initialise a new Range measurement
737                 final AbsoluteDate epoch = new AbsoluteDate(pi.startEpoch, new TimeComponents(secOfDay), pi.timeScale).shiftedBy(dayShift * 86400);
738                 final RangeMeasurement range = new RangeMeasurement(epoch, timeOfFlight, epochEvent);
739                 pi.dataBlock.addRangeData(range);
740 
741                 lastSecOfDay = secOfDay;
742             }
743 
744             /** {@inheritDoc} */
745             @Override
746             public Stream<LineParser> allowedNext() {
747                 return Stream.of(H8, TEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
748             }
749 
750         },
751 
752         /** Range Record (Normal point). */
753         ELEVEN("11") {
754 
755             /** Storage for the last epoch parsed to compare with the current.*/
756             private double lastSecOfDay = 0;
757 
758             /** Shift in days due to the rollover.*/
759             private int dayShift = 0;
760 
761             /** {@inheritDoc} */
762             @Override
763             public void parse(final String line, final ParseInfo pi) {
764 
765                 // Data contained in the line
766                 final String[] values = SEPARATOR.split(line);
767 
768                 // Read data
769                 final double secOfDay     = Double.parseDouble(values[1]);
770                 final double timeOfFlight = Double.parseDouble(values[2]);
771                 final int    epochEvent   = Integer.parseInt(values[4]);
772                 final double snr          = (pi.version == 2) ? Double.parseDouble(values[13]) : Double.NaN;
773 
774                 // Check secOfDay for rollover
775                 dayShift = dayShift + checkRollover(lastSecOfDay, secOfDay);
776                 // Initialise a new Range measurement
777                 final AbsoluteDate epoch = new AbsoluteDate(pi.startEpoch, new TimeComponents(secOfDay), pi.timeScale).shiftedBy(dayShift * 86400);
778                 final RangeMeasurement range = new RangeMeasurement(epoch, timeOfFlight, epochEvent, snr);
779                 pi.dataBlock.addRangeData(range);
780 
781             }
782 
783             /** {@inheritDoc} */
784             @Override
785             public Stream<LineParser> allowedNext() {
786                 return Stream.of(H8, ELEVEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
787             }
788 
789         },
790 
791         /** Range Supplement Record. */
792         TWELVE("12") {
793 
794             /** {@inheritDoc} */
795             @Override
796             public void parse(final String line, final ParseInfo pi) {
797                 // Not implemented yet
798             }
799 
800             /** {@inheritDoc} */
801             @Override
802             public Stream<LineParser> allowedNext() {
803                 return Stream.of(H8, TEN, ELEVEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
804             }
805 
806         },
807 
808         /** Meteorological record. */
809         METEO("20") {
810 
811             /** Storage for the last epoch parsed to compare with the current.*/
812             private double lastSecOfDay = 0;
813 
814             /** Shift in days due to the rollover.*/
815             private int dayShift = 0;
816 
817             /** {@inheritDoc} */
818             @Override
819             public void parse(final String line, final ParseInfo pi) {
820 
821                 // Data contained in the line
822                 final String[] values = SEPARATOR.split(line);
823 
824                 // Read data
825                 final double secOfDay    = Double.parseDouble(values[1]);
826                 final double pressure    = MBAR_TO_BAR.convert(Double.parseDouble(values[2]));
827                 final double temperature = Double.parseDouble(values[3]);
828                 final double humidity    = Double.parseDouble(values[4]);
829 
830                 // Check secOfDay for rollover
831                 dayShift = dayShift + checkRollover(lastSecOfDay, secOfDay);
832                 // Initialise a new Range measurement
833                 final AbsoluteDate epoch = new AbsoluteDate(pi.startEpoch, new TimeComponents(secOfDay), pi.timeScale).shiftedBy(dayShift * 86400);
834                 final MeteorologicalMeasurement meteo = new MeteorologicalMeasurement(epoch, pressure,
835                         temperature, humidity);
836                 pi.dataBlock.addMeteoData(meteo);
837 
838             }
839 
840             /** {@inheritDoc} */
841             @Override
842             public Stream<LineParser> allowedNext() {
843                 return Stream.of(H8, METEO, METEO_SUPP, TEN, ELEVEN, TWELVE, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
844             }
845 
846         },
847 
848         /** Meteorological Supplement record. */
849         METEO_SUPP("21") {
850 
851             /** {@inheritDoc} */
852             @Override
853             public void parse(final String line, final ParseInfo pi) {
854                 // Not implemented yet
855             }
856 
857             /** {@inheritDoc} */
858             @Override
859             public Stream<LineParser> allowedNext() {
860                 return Stream.of(H8, METEO, METEO_SUPP, TEN, ELEVEN, TWELVE, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
861             }
862 
863         },
864 
865         /** Pointing Angle Record. */
866         ANGLES("30") {
867 
868             /** Storage for the last epoch parsed to compare with the current.*/
869             private double lastSecOfDay = 0;
870 
871             /** Shift in days due to the rollover.*/
872             private int dayShift = 0;
873 
874             /** {@inheritDoc} */
875             @Override
876             public void parse(final String line, final ParseInfo pi) {
877 
878                 // Data contained in the line
879                 final String[] values = SEPARATOR.split(line);
880 
881                 // Read data
882                 final double  secOfDay              = Double.parseDouble(values[1]);
883                 final double  azmiuth               = FastMath.toRadians(Double.parseDouble(values[2]));
884                 final double  elevation             = FastMath.toRadians(Double.parseDouble(values[3]));
885                 final int     directionFlag         = Integer.parseInt(values[4]);
886                 final int     orginFlag             = Integer.parseInt(values[5]);
887                 final boolean isRefractionCorrected = readBoolean(values[6]);
888 
889 
890                 // Angles rates
891                 double azimuthRate   = Double.NaN;
892                 double elevationRate = Double.NaN;
893                 if (pi.version == 2) {
894                     azimuthRate   = readDoubleWithNaN(values[7]);
895                     elevationRate = readDoubleWithNaN(values[8]);
896                 }
897 
898                 // Check secOfDay for rollover
899                 dayShift = dayShift + checkRollover(lastSecOfDay, secOfDay);
900                 // Initialise a new angles measurement
901                 final AbsoluteDate epoch = new AbsoluteDate(pi.startEpoch, new TimeComponents(secOfDay), pi.timeScale);
902                 final AnglesMeasurement angles = new AnglesMeasurement(epoch, azmiuth, elevation,
903                         directionFlag, orginFlag,
904                         isRefractionCorrected,
905                         azimuthRate, elevationRate);
906                 pi.dataBlock.addAnglesData(angles);
907 
908             }
909 
910             /** {@inheritDoc} */
911             @Override
912             public Stream<LineParser> allowedNext() {
913                 return Stream.of(H8, METEO, TEN, ELEVEN, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
914             }
915 
916         },
917 
918         /** Calibration Record. */
919         CALIB("40") {
920 
921             /** {@inheritDoc} */
922             @Override
923             public void parse(final String line, final ParseInfo pi) {
924                 // Not implemented yet
925             }
926 
927             /** {@inheritDoc} */
928             @Override
929             public Stream<LineParser> allowedNext() {
930                 return Stream.of(H8, METEO, CALIB, CALIB_DETAILS, CALIB_SHOT, TEN, ELEVEN, TWELVE, ANGLES, STAT, COMPATIBILITY, COMMENTS);
931             }
932 
933         },
934 
935         /** Calibration Details Record. */
936         CALIB_DETAILS("41") {
937 
938             /** {@inheritDoc} */
939             @Override
940             public void parse(final String line, final ParseInfo pi) {
941                 // Not implemented yet
942             }
943 
944             /** {@inheritDoc} */
945             @Override
946             public Stream<LineParser> allowedNext() {
947                 return Stream.of(H8, METEO, CALIB, CALIB_DETAILS, CALIB_SHOT, TEN, ELEVEN, TWELVE, ANGLES, STAT, COMPATIBILITY, COMMENTS);
948             }
949 
950         },
951 
952         /** Calibration "Shot" Record. */
953         CALIB_SHOT("42") {
954 
955             /** {@inheritDoc} */
956             @Override
957             public void parse(final String line, final ParseInfo pi) {
958                 // Not implemented yet
959             }
960 
961             /** {@inheritDoc} */
962             @Override
963             public Stream<LineParser> allowedNext() {
964                 return Stream.of(H8, METEO, CALIB, CALIB_DETAILS, CALIB_SHOT, TEN, ELEVEN, TWELVE, ANGLES, STAT, COMPATIBILITY, COMMENTS);
965             }
966 
967         },
968 
969         /** Session (Pass) Statistics Record. */
970         STAT("50") {
971 
972             /** {@inheritDoc} */
973             @Override
974             public void parse(final String line, final ParseInfo pi) {
975                 // Not implemented yet
976             }
977 
978             /** {@inheritDoc} */
979             @Override
980             public Stream<LineParser> allowedNext() {
981                 return Stream.of(H8, METEO, CALIB, CALIB_DETAILS, CALIB_SHOT, TEN, ELEVEN, TWELVE, ANGLES, STAT, COMPATIBILITY, H8, COMMENTS);
982             }
983 
984         },
985 
986         /** Compatibility record. */
987         COMPATIBILITY("60") {
988 
989             /** {@inheritDoc} */
990             @Override
991             public void parse(final String line, final ParseInfo pi) {
992                 // Not implemented yet
993             }
994 
995             /** {@inheritDoc} */
996             @Override
997             public Stream<LineParser> allowedNext() {
998                 return Stream.of(H8, METEO, CALIB, CALIB_DETAILS, CALIB_SHOT, TEN, ELEVEN, TWELVE, ANGLES, STAT, COMPATIBILITY, COMMENTS);
999             }
1000 
1001         },
1002 
1003         /** Comments. */
1004         COMMENTS("00") {
1005 
1006             /** {@inheritDoc} */
1007             @Override
1008             public void parse(final String line, final ParseInfo pi) {
1009 
1010                 // Comment
1011                 final String comment = line.split(getFirstIdentifier())[1].trim();
1012                 pi.file.getComments().add(comment);
1013 
1014             }
1015 
1016             /** {@inheritDoc} */
1017             @Override
1018             public Stream<LineParser> allowedNext() {
1019                 return Stream.of(H1, H2, H3, H4, H5, H8, H9, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO,
1020                         METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS);
1021 
1022             }
1023 
1024         },
1025 
1026         /** End of data block. */
1027         H8("H8", "h8") {
1028 
1029             /** {@inheritDoc} */
1030             @Override
1031             public void parse(final String line, final ParseInfo pi) {
1032 
1033                 // Fill data block
1034                 pi.dataBlock.setHeader(pi.header);
1035                 pi.dataBlock.setConfigurationRecords(pi.configurationRecords);
1036 
1037                 // Add the data block to the CRD file
1038                 pi.file.addDataBlock(pi.dataBlock);
1039 
1040                 // Initialize a new empty containers
1041                 pi.startEpoch           = DateComponents.J2000_EPOCH;
1042                 pi.header               = new CRDHeader();
1043                 pi.configurationRecords = new CRDConfiguration();
1044                 pi.dataBlock            = new CRDDataBlock();
1045 
1046             }
1047 
1048             /** {@inheritDoc} */
1049             @Override
1050             public Stream<LineParser> allowedNext() {
1051                 return Stream.of(H1, H9, COMMENTS);
1052             }
1053 
1054         },
1055 
1056         /** Last record in file. */
1057         H9("H9", "h9") {
1058 
1059             /** {@inheritDoc} */
1060             @Override
1061             public void parse(final String line, final ParseInfo pi) {
1062                 pi.done = true;
1063             }
1064 
1065             /** {@inheritDoc} */
1066             @Override
1067             public Stream<LineParser> allowedNext() {
1068                 return Stream.of(H9);
1069             }
1070 
1071         };
1072 
1073         /** Patterns for identifying line. */
1074         private final Pattern[] patterns;
1075 
1076         /** Identifiers. */
1077         private final String[] identifiers;
1078 
1079         /** Simple constructor.
1080          * @param identifier regular expression for identifying line (i.e. first element)
1081          */
1082         LineParser(final String... identifier) {
1083             this.identifiers = identifier;
1084             // Initialise patterns
1085             this.patterns    = new Pattern[identifiers.length];
1086             for (int index = 0; index < patterns.length; index++) {
1087                 patterns[index] = Pattern.compile(identifiers[index]);
1088             }
1089         }
1090 
1091         /**
1092          * Get the regular expression for identifying line.
1093          * @return the regular expression for identifying line
1094          */
1095         public String getFirstIdentifier() {
1096             return identifiers[0];
1097         }
1098 
1099         /** Parse a line.
1100          * @param line line to parse
1101          * @param pi holder for transient data
1102          */
1103         public abstract void parse(String line, ParseInfo pi);
1104 
1105         /** Get the allowed parsers for next line.
1106          * @return allowed parsers for next line
1107          */
1108         public abstract Stream<LineParser> allowedNext();
1109 
1110         /** Check if parser can handle line.
1111          * @param line line to parse
1112          * @return true if parser can handle the specified line
1113          */
1114         public boolean canHandle(final String line) {
1115             // Line identifier
1116             final String lineId = SEPARATOR.split(line)[0];
1117             // Loop on patterns
1118             for (Pattern pattern : patterns) {
1119                 if (pattern.matcher(lineId).matches()) {
1120                     return true;
1121                 }
1122             }
1123             // No match
1124             return false;
1125         }
1126 
1127         /**
1128          * Read a boolean from a string value.
1129          * @param value input value
1130          * @return the correspondin boolean
1131          */
1132         private static boolean readBoolean(final String value) {
1133             return Integer.parseInt(value) == 1;
1134         }
1135 
1136         /**
1137          * Read a double value taking into consideration a possible "na".
1138          * @param value input string
1139          * @return the corresponding double value
1140          */
1141         private static double readDoubleWithNaN(final String value) {
1142             return "na".equals(value) ? Double.NaN : Double.parseDouble(value);
1143         }
1144 
1145     }
1146 
1147 }