CRDParser.java
/* Copyright 2002-2024 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.files.ilrs;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ilrs.CRD.AnglesMeasurement;
import org.orekit.files.ilrs.CRD.CRDDataBlock;
import org.orekit.files.ilrs.CRD.Calibration;
import org.orekit.files.ilrs.CRD.CalibrationDetail;
import org.orekit.files.ilrs.CRD.FrRangeMeasurement;
import org.orekit.files.ilrs.CRD.MeteorologicalMeasurement;
import org.orekit.files.ilrs.CRD.NptRangeMeasurement;
import org.orekit.files.ilrs.CRD.RangeMeasurement;
import org.orekit.files.ilrs.CRD.RangeSupplement;
import org.orekit.files.ilrs.CRD.SessionStatistics;
import org.orekit.files.ilrs.CRDConfiguration.CalibrationTargetConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.DetectorConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.LaserConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.MeteorologicalConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.SoftwareConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.SystemConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.TimingSystemConfiguration;
import org.orekit.files.ilrs.CRDConfiguration.TransponderConfiguration;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.Constants;
import org.orekit.utils.units.Unit;
import org.orekit.utils.units.UnitsConverter;
/**
* A parser for the CRD data file format.
* <p>
* It supports both 1.0 and 2.0 versions
* <p>
* <b>Note</b>: Not all the records are read by the parser. Only the most significants are parsed.
* Contributions are welcome to support more fields in the format.
* @see <a href="https://ilrs.gsfc.nasa.gov/docs/2009/crd_v1.01.pdf">1.0 file format</a>
* @see <a href="https://ilrs.gsfc.nasa.gov/docs/2021/crd_v2.01e2.pdf">2.0 file format</a>
* @author Bryan Cazabonne
* @author Rongwang Li
* @since 10.3
*/
public class CRDParser {
/** Default supported files name pattern for CRD files. */
public static final String DEFAULT_CRD_SUPPORTED_NAMES = "^(?!0+$)\\w{1,12}\\_\\d{6,8}.\\w{3}$";
/** Nanometers units. */
private static final Unit NM = Unit.parse("nm");
/** Kilohertz units. */
private static final Unit KHZ = Unit.parse("kHz");
/** Microseconds units. */
private static final Unit US = Unit.parse("µs");
/** Nanoseconds units. */
private static final Unit NS = Unit.parse("ns");
/** Picoseconds units. */
private static final Unit PS = Unit.parse("ps");
/** mbar to bar converter. */
private static final UnitsConverter MBAR_TO_BAR = new UnitsConverter(Unit.parse("mbar"), Unit.parse("bar"));
/** File format. */
private static final String FILE_FORMAT = "CRD";
/** Pattern for delimiting regular expressions. */
private static final Pattern SEPARATOR = Pattern.compile("\\s+");
/** Pattern for delimiting expressions with comma. */
private static final Pattern COMMA = Pattern.compile(",");
/** Identifier of comment record. */
private static final String COMMENTS_IDENTIFIER = "00";
/** Pattern of " [-]?(na)". */
private static final Pattern PATTERN_NA = Pattern.compile(" [-]?(na)");
/** Time scale used to define epochs in CPF file. */
private final TimeScale timeScale;
/**
* Default constructor.
* <p>
* This constructor uses the {@link DataContext#getDefault() default data context}.
*/
@DefaultDataContext
public CRDParser() {
this(DataContext.getDefault().getTimeScales().getUTC());
}
/**
* Constructor.
* @param utc utc time scale to read epochs
*/
public CRDParser(final TimeScale utc) {
this.timeScale = utc;
}
/**
* Get the time scale used to read the file.
* @return the time scale used to read the file
*/
public TimeScale getTimeScale() {
return timeScale;
}
/**
* Parse a CRD file.
* @param source data source containing the CRD file.
* @return a parsed CRD file.
* @throws IOException if {@code reader} throws one.
*/
public CRD parse(final DataSource source) throws IOException {
// Initialize internal data structures
final ParseInfo pi = new ParseInfo();
int lineNumber = 0;
Iterable<LineParser> crdParsers = Collections.singleton(LineParser.H1);
try (BufferedReader reader = new BufferedReader(source.getOpener().openReaderOnce())) {
nextLine:
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
++lineNumber;
if (line.startsWith(COMMENTS_IDENTIFIER)) {
// Comment is in the beginning of the file.
crdParsers = Arrays.asList(LineParser.COMMENTS);
}
for (final LineParser candidate : crdParsers) {
if (candidate.canHandle(line)) {
try {
// Note: since crd v2.01.
// The literal “na” is used instead of “-1” for fields that are not applicable or not avaiable.
// And there may be "-na".
// note: "analog" --> "aNaNlog"
line = PATTERN_NA.matcher(line).replaceAll(" " + CRD.STR_NAN);
candidate.parse(line, pi);
if (pi.done) {
// Return file
return pi.file;
}
crdParsers = candidate.allowedNext();
continue nextLine;
} catch (StringIndexOutOfBoundsException | NumberFormatException e) {
throw new OrekitException(e,
OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, source.getName(), line);
}
}
}
}
// We never reached the EOF marker
throw new OrekitException(OrekitMessages.CRD_UNEXPECTED_END_OF_FILE, lineNumber);
} catch (IOException ioe) {
throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
}
}
/**
* Make sure the epoch is 'right' by doing a day shift if it is required by comparing the current and session start epoch.
* According to the CRD document, the duration of a session must be less than one day.
* @param epoch current epoch
* @param startEpoch start epoch of session
* @return epoch with rollover is handled.
*/
private static AbsoluteDate checkRollover(final AbsoluteDate epoch, final AbsoluteDate startEpoch) {
// If the current epoch is before the start epoch of a session, the epoch should be shifted by 1 day.
// For METEO(20) data, the epoch may be a 'little' (10 hours?) before the session start epoch.
// And also for CALIB(40) and CALIB_DETAILS(41)
return epoch.durationFrom(startEpoch) < -36000 ? epoch.shiftedBy(Constants.JULIAN_DAY) : epoch;
}
/** Transient data used for parsing a CRD file. The data is kept in a
* separate data structure to make the parser thread-safe.
* <p><b>Note</b>: The class intentionally does not provide accessor
* methods, as it is only used internally for parsing a CRD file.</p>
*/
private class ParseInfo {
/** The corresponding CDR file. */
private CRD file;
/** Version. */
private int version;
/** The current data block. */
private CRDDataBlock dataBlock;
/** Data block header. */
private CRDHeader header;
/** Cofiguration records. */
private CRDConfiguration configurationRecords;
/** Time scale. */
private TimeScale timeScale;
/** Current data block start epoch, DateComponents only. */
private DateComponents startEpochDateComponents;
/** End Of File reached indicator. */
private boolean done;
/**
* Constructor.
*/
protected ParseInfo() {
// Initialise default values
this.done = false;
this.version = 1;
this.startEpochDateComponents = DateComponents.J2000_EPOCH;
// Initialise empty object
this.file = new CRD();
this.header = new CRDHeader();
this.configurationRecords = new CRDConfiguration();
this.dataBlock = new CRDDataBlock();
// Time scale
this.timeScale = CRDParser.this.timeScale;
}
}
/** Parsers for specific lines. */
private enum LineParser {
/** Format header. */
H1("H1", "h1") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Format and version
final String format = values[1];
pi.version = Integer.parseInt(values[2]);
// Throw an exception if format is not equal to "CRD"
if (!format.equalsIgnoreCase(FILE_FORMAT)) {
throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
}
// Fill first elements
pi.header.setFormat(format);
pi.header.setVersion(pi.version);
// Epoch of ephemeris production
final int year = Integer.parseInt(values[3]);
final int month = Integer.parseInt(values[4]);
final int day = Integer.parseInt(values[5]);
pi.header.setProductionEpoch(new DateComponents(year, month, day));
// Hour of ephemeris production
pi.header.setProductionHour(Integer.parseInt(values[6]));
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H2, COMMENTS);
}
},
/** Station header. */
H2("H2", "h2") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Station name
pi.header.setStationName(values[1]);
// Crustal Dynamics Project keys
pi.header.setSystemIdentifier(Integer.parseInt(values[2]));
pi.header.setSystemNumber(Integer.parseInt(values[3]));
pi.header.setSystemOccupancy(Integer.parseInt(values[4]));
// Station epoch time scale
pi.header.setEpochIdentifier(Integer.parseInt(values[5]));
// Station network
if (pi.version == 2) {
pi.header.setStationNetword(values[6]);
} else {
pi.header.setStationNetword(CRD.STR_VALUE_NOT_AVAILABLE);
}
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
}
},
/** Target header. */
H3("H3", "h3") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Target name
pi.header.setName(values[1]);
// Identifiers
pi.header.setIlrsSatelliteId(values[2]);
pi.header.setSic(values[3]);
pi.header.setNoradId(values[4]);
// Spacecraft Epoch Time Scale
pi.header.setSpacecraftEpochTimeScale(Integer.parseInt(values[5]));
// Target class and location (if needed)
pi.header.setTargetClass(Integer.parseInt(values[6]));
if (pi.version == 2) {
// na=unknown (for use when tracking a transponder using a Version 1 CPF)
// treated it as -1
pi.header.setTargetLocation(readIntegerWithNaN(values[7], -1));
}
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H4, C0, C1, C2, C3, C4, C5, C6, C7, COMMENTS);
}
},
/** Session (Pass/Pass segment) header. */
H4("H4", "h4") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Data type
pi.header.setDataType(Integer.parseInt(values[1]));
// Start epoch
final int yearS = Integer.parseInt(values[2]);
final int monthS = Integer.parseInt(values[3]);
final int dayS = Integer.parseInt(values[4]);
final int hourS = Integer.parseInt(values[5]);
final int minuteS = Integer.parseInt(values[6]);
final double secondS = Integer.parseInt(values[7]);
pi.startEpochDateComponents = new DateComponents(yearS, monthS, dayS);
pi.header.setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
hourS, minuteS, secondS,
pi.timeScale));
// End epoch
// since crd v2.01
// Set the ending date and time fields to “na” if not available.
if (pi.version == 2 && values[8].equalsIgnoreCase("")) {
pi.header.setEndEpoch(null);
} else {
final int yearE = Integer.parseInt(values[8]);
final int monthE = Integer.parseInt(values[9]);
final int dayE = Integer.parseInt(values[10]);
final int hourE = Integer.parseInt(values[11]);
final int minuteE = Integer.parseInt(values[12]);
final double secondE = Integer.parseInt(values[13]);
// fixed 2022-12-12
// if yearE or monthE is -1.
if (monthE == -1) {
pi.header.setEndEpoch(null);
} else {
pi.header.setEndEpoch(new AbsoluteDate(yearE, monthE, dayE, hourE, minuteE, secondE, pi.timeScale));
}
}
// Data release
pi.header.setDataReleaseFlag(Integer.parseInt(values[14]));
// Correction flags
pi.header.setIsTroposphericRefractionApplied(readBoolean(values[15]));
pi.header.setIsCenterOfMassCorrectionApplied(readBoolean(values[16]));
pi.header.setIsReceiveAmplitudeCorrectionApplied(readBoolean(values[17]));
pi.header.setIsStationSystemDelayApplied(readBoolean(values[18]));
pi.header.setIsTransponderDelayApplied(readBoolean(values[19]));
// Range type indicator
pi.header.setRangeType(Integer.parseInt(values[20]));
// Data quality indicator
pi.header.setQualityIndicator(Integer.parseInt(values[21]));
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES,
CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Prediction header. */
H5("H5", "h5") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill data
pi.header.setPredictionType(Integer.parseInt(values[1]));
pi.header.setYearOfCentury(Integer.parseInt(values[2]));
pi.header.setDateAndTime(values[3]);
pi.header.setPredictionProvider(values[4]);
pi.header.setSequenceNumber(Integer.parseInt(values[5]));
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB,
CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** System configuration record. */
C0("C0", "c0") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty system configuration record
final SystemConfiguration systemRecord = new SystemConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Wavelength
systemRecord.setWavelength(NM.toSI(Double.parseDouble(values[2])));
// System ID
systemRecord.setSystemId(values[3]);
// Components, A B C D E F G
systemRecord.setComponents(Arrays.copyOfRange(values, 4, values.length));
// Add the system configuration record
pi.configurationRecords.addConfigurationRecord(systemRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Laser configuration record. */
C1("C1", "c1") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty laser configuration record
final LaserConfiguration laserRecord = new LaserConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
laserRecord.setLaserId(values[2]);
laserRecord.setLaserType(values[3]);
laserRecord.setPrimaryWavelength(NM.toSI(Double.parseDouble(values[4])));
laserRecord.setNominalFireRate(Double.parseDouble(values[5]));
laserRecord.setPulseEnergy(Double.parseDouble(values[6]));
laserRecord.setPulseWidth(Double.parseDouble(values[7]));
laserRecord.setBeamDivergence(Double.parseDouble(values[8]));
laserRecord.setPulseInOutgoingSemiTrain(readIntegerWithNaN(values[9], 1));
// Add the laser configuration record
pi.configurationRecords.addConfigurationRecord(laserRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(C2, C3, C4, C5, C6, C7, TEN, ELEVEN, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
}
},
/** Detector configuration record. */
C2("C2", "c2") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty detector configuration record
final DetectorConfiguration detectorRecord = new DetectorConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
detectorRecord.setDetectorId(values[2]);
detectorRecord.setDetectorType(values[3]);
detectorRecord.setApplicableWavelength(NM.toSI(Double.parseDouble(values[4])));
detectorRecord.setQuantumEfficiency(Double.parseDouble(values[5]));
detectorRecord.setAppliedVoltage(Double.parseDouble(values[6]));
detectorRecord.setDarkCount(KHZ.toSI(Double.parseDouble(values[7])));
detectorRecord.setOutputPulseType(values[8]);
detectorRecord.setOutputPulseWidth(Double.parseDouble(values[9]));
detectorRecord.setSpectralFilter(NM.toSI(Double.parseDouble(values[10])));
detectorRecord.setTransmissionOfSpectralFilter(Double.parseDouble(values[11]));
detectorRecord.setSpatialFilter(Double.parseDouble(values[12]));
detectorRecord.setExternalSignalProcessing(values[13]);
// Check file version for additional data
if (pi.version == 2) {
detectorRecord.setAmplifierGain(Double.parseDouble(values[14]));
detectorRecord.setAmplifierBandwidth(KHZ.toSI(Double.parseDouble(values[15])));
detectorRecord.setAmplifierInUse(values[16]);
} else {
detectorRecord.setAmplifierGain(Double.NaN);
detectorRecord.setAmplifierBandwidth(Double.NaN);
detectorRecord.setAmplifierInUse(CRD.STR_VALUE_NOT_AVAILABLE);
}
// Add the detector configuration record
pi.configurationRecords.addConfigurationRecord(detectorRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Timing system configuration record. */
C3("C3", "c3") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty timing system configuration record
final TimingSystemConfiguration timingRecord = new TimingSystemConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
timingRecord.setLocalTimingId(values[2]);
timingRecord.setTimeSource(values[3]);
timingRecord.setFrequencySource(values[4]);
timingRecord.setTimer(values[5]);
final String timerSerialNumber = values[6];
if (CRD.STR_NAN.equalsIgnoreCase(timerSerialNumber)) {
// The timer serial number may be "na"
timingRecord.setTimerSerialNumber(CRD.STR_VALUE_NOT_AVAILABLE);
} else {
timingRecord.setTimerSerialNumber(timerSerialNumber);
}
timingRecord.setEpochDelayCorrection(US.toSI(Double.parseDouble(values[7])));
// Add the timing system configuration record
pi.configurationRecords.addConfigurationRecord(timingRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Transponder configuration record. */
C4("C4", "c4") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty transponder configuration record
final TransponderConfiguration transponderRecord = new TransponderConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Estimated offsets and drifts
transponderRecord.setTransponderId(values[2]);
transponderRecord.setStationUTCOffset(NS.toSI(Double.parseDouble(values[3])));
transponderRecord.setStationOscDrift(Double.parseDouble(values[4]));
transponderRecord.setTranspUTCOffset(NS.toSI(Double.parseDouble(values[5])));
transponderRecord.setTranspOscDrift(Double.parseDouble(values[6]));
// Transponder clock reference time
transponderRecord.setTranspClkRefTime(Double.parseDouble(values[7]));
// Clock and drift indicators
transponderRecord.setStationClockAndDriftApplied(Integer.parseInt(values[8]));
transponderRecord.setSpacecraftClockAndDriftApplied(Integer.parseInt(values[9]));
// Spacecraft time simplified
transponderRecord.setIsSpacecraftTimeSimplified(readBoolean(values[10]));
// Add the transponder configuration record
pi.configurationRecords.addConfigurationRecord(transponderRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Software configuration record. */
C5("C5", "c5") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty software configuration record
final SoftwareConfiguration softwareRecord = new SoftwareConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
softwareRecord.setSoftwareId(values[2]);
softwareRecord.setTrackingSoftwares(COMMA.split(values[3]));
softwareRecord.setTrackingSoftwareVersions(COMMA.split(values[4]));
softwareRecord.setProcessingSoftwares(COMMA.split(values[5]));
softwareRecord.setProcessingSoftwareVersions(COMMA.split(values[6]));
// Add the software configuration record
pi.configurationRecords.addConfigurationRecord(softwareRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Meteorological instrumentation configuration record. */
C6("C6", "c6") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty meteorological configuration record
final MeteorologicalConfiguration meteoRecord = new MeteorologicalConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
meteoRecord.setMeteorologicalId(values[2]);
meteoRecord.setPressSensorManufacturer(values[3]);
meteoRecord.setPressSensorModel(values[4]);
meteoRecord.setPressSensorSerialNumber(values[5]);
meteoRecord.setTempSensorManufacturer(values[6]);
meteoRecord.setTempSensorModel(values[7]);
meteoRecord.setTempSensorSerialNumber(values[8]);
meteoRecord.setHumiSensorManufacturer(values[9]);
meteoRecord.setHumiSensorModel(values[10]);
meteoRecord.setHumiSensorSerialNumber(values[11]);
// Add the meteorological configuration record
pi.configurationRecords.addConfigurationRecord(meteoRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Calibration Target configuration record. */
C7("C7", "c7") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Initialise an empty calibration target configuration record
final CalibrationTargetConfiguration calibRecord = new CalibrationTargetConfiguration();
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Fill values
calibRecord.setConfigurationId(values[2]);
calibRecord.setTargetName(values[3]);
calibRecord.setSurveyedTargetDistance(Double.parseDouble(values[4]));
calibRecord.setSurveyError(Double.parseDouble(values[5]) * 1e-3); // mm --> m
calibRecord.setSumOfAllConstantDelays(Double.parseDouble(values[6]));
calibRecord.setPulseEnergy(Double.parseDouble(values[7]));
calibRecord.setProcessingSoftwareName(values[8]);
calibRecord.setProcessingSoftwareVersion(values[9]);
// Add the calibration target configuration record
pi.configurationRecords.addConfigurationRecord(calibRecord);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H3, H4, H5, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP,
ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Range Record (Full rate, Sampled Engineering/Quicklook). */
TEN("10") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final double timeOfFlight = Double.parseDouble(values[2]);
final String systemConfigId = values[3];
final int epochEvent = Integer.parseInt(values[4]);
final int filterFlag = Integer.parseInt(values[5]);
final int detectorChannel = Integer.parseInt(values[6]);
final int stopNumber = Integer.parseInt(values[7]);
final int receiveAmplitude = readIntegerWithNaN(values[8], -1);
int transmitAmplitude = -1;
if (pi.version == 2) {
transmitAmplitude = readIntegerWithNaN(values[9], -1);
}
// Initialise a new Range measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final RangeMeasurement range = new FrRangeMeasurement(epoch, timeOfFlight, epochEvent, systemConfigId,
filterFlag, detectorChannel, stopNumber, receiveAmplitude, transmitAmplitude);
pi.dataBlock.addRangeData(range);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Range Record (Normal point). */
ELEVEN("11") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final double timeOfFlight = Double.parseDouble(values[2]);
final String systemConfigId = values[3];
final int epochEvent = Integer.parseInt(values[4]);
final double windowLength = Double.parseDouble(values[5]);
final int numberOfRawRanges = Integer.parseInt(values[6]);
final double binRms = PS.toSI(Double.parseDouble(values[7]));
final double binSkew = Double.parseDouble(values[8]);
final double binKurtosis = Double.parseDouble(values[9]);
final double binPeakMinusMean = PS.toSI(Double.parseDouble(values[10]));
final double returnRate = Double.parseDouble(values[11]);
final int detectorChannel = Integer.parseInt(values[12]);
double snr = Double.NaN;
if (pi.version == 2) {
snr = Double.parseDouble(values[13]);
}
// Initialise a new Range measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final RangeMeasurement range = new NptRangeMeasurement(epoch, timeOfFlight, epochEvent, snr,
systemConfigId, windowLength, numberOfRawRanges, binRms, binSkew, binKurtosis, binPeakMinusMean,
returnRate, detectorChannel);
pi.dataBlock.addRangeData(range);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT,
COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Range Supplement Record. */
TWELVE("12") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final String systemConfigId = values[2];
final double troposphericRefractionCorr = PS.toSI(Double.parseDouble(values[3]));
final double centerOfMassCorr = Double.parseDouble(values[4]);
final double ndFilterValue = Double.parseDouble(values[5]);
final double timeBiasApplied = Double.parseDouble(values[6]);
double rangeRate = Double.NaN;
if (pi.version == 2) {
rangeRate = Double.parseDouble(values[7]);
}
// Initialise a new Range measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final RangeSupplement rangeSup = new RangeSupplement(epoch, systemConfigId, troposphericRefractionCorr,
centerOfMassCorr, ndFilterValue, timeBiasApplied, rangeRate);
pi.dataBlock.addRangeSupplementData(rangeSup);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, ANGLES, CALIB, STAT, COMPATIBILITY, COMMENTS);
}
},
/** Meteorological record. */
METEO("20") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final double pressure = MBAR_TO_BAR.convert(Double.parseDouble(values[2]));
final double temperature = Double.parseDouble(values[3]);
final double humidity = Double.parseDouble(values[4]);
final int originOfValues = Integer.parseInt(values[5]);
// Initialise a new Range measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final MeteorologicalMeasurement meteo = new MeteorologicalMeasurement(epoch, pressure, temperature,
humidity, originOfValues);
pi.dataBlock.addMeteoData(meteo);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Meteorological Supplement record. */
METEO_SUPP("21") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Not implemented yet
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Pointing Angle Record. */
ANGLES("30") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final double azmiuth = FastMath.toRadians(Double.parseDouble(values[2]));
final double elevation = FastMath.toRadians(Double.parseDouble(values[3]));
final int directionFlag = Integer.parseInt(values[4]);
final int orginFlag = Integer.parseInt(values[5]);
final boolean isRefractionCorrected = readBoolean(values[6]);
// Angles rates
double azimuthRate = Double.NaN;
double elevationRate = Double.NaN;
if (pi.version == 2) {
// degrees/second ==> rad/s
azimuthRate = FastMath.toRadians(Double.parseDouble(values[7]));
elevationRate = FastMath.toRadians(Double.parseDouble(values[8]));
}
// Initialise a new angles measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final AnglesMeasurement angles = new AnglesMeasurement(epoch, azmiuth, elevation,
directionFlag, orginFlag,
isRefractionCorrected,
azimuthRate, elevationRate);
pi.dataBlock.addAnglesData(angles);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Calibration Record. */
CALIB("40") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final int typeOfData = Integer.parseInt(values[2]);
final String systemConfigId = values[3];
final int numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
final int numberOfPointsUsed = readIntegerWithNaN(values[5], -1);
final double oneWayDistance = Double.parseDouble(values[6]);
final double systemDelay = PS.toSI(Double.parseDouble(values[7]));
final double delayShift = PS.toSI(Double.parseDouble(values[8]));
final double rms = PS.toSI(Double.parseDouble(values[9]));
final double skew = Double.parseDouble(values[10]);
final double kurtosis = Double.parseDouble(values[11]);
final double peakMinusMean = PS.toSI(Double.parseDouble(values[12]));
final int typeIndicator = Integer.parseInt(values[13]);
final int shiftTypeIndicator = Integer.parseInt(values[14]);
final int detectorChannel = Integer.parseInt(values[15]);
// Check file version for additional data
int span = 0;
double returnRate = Double.NaN;
if (pi.version == 2) {
// fixed 20230321
// the span may be "na"
span = readIntegerWithNaN(values[16], -1);
returnRate = Double.parseDouble(values[17]);
}
// Initialise a new angles measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final Calibration cal = new Calibration(epoch, typeOfData, systemConfigId, numberOfPointsRecorded,
numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew, kurtosis, peakMinusMean,
typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
pi.dataBlock.addCalibrationData(cal);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Calibration Details Record. */
CALIB_DETAILS("41") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final double secOfDay = Double.parseDouble(values[1]);
final int typeOfData = Integer.parseInt(values[2]);
final String systemConfigId = values[3];
final int numberOfPointsRecorded = readIntegerWithNaN(values[4], -1);
final int numberOfPointsUsed = readIntegerWithNaN(values[5], -1);
final double oneWayDistance = Double.parseDouble(values[6]);
final double systemDelay = PS.toSI(Double.parseDouble(values[7]));
final double delayShift = PS.toSI(Double.parseDouble(values[8]));
final double rms = PS.toSI(Double.parseDouble(values[9]));
final double skew = Double.parseDouble(values[10]);
final double kurtosis = Double.parseDouble(values[11]);
final double peakMinusMean = PS.toSI(Double.parseDouble(values[12]));
final int typeIndicator = Integer.parseInt(values[13]);
final int shiftTypeIndicator = Integer.parseInt(values[14]);
final int detectorChannel = Integer.parseInt(values[15]);
// Check file version for additional data
int span = 0;
double returnRate = Double.NaN;
if (pi.version == 2) {
span = Integer.parseInt(values[16]);
returnRate = Double.parseDouble(values[17]);
}
// Initialise a new angles measurement
AbsoluteDate epoch = new AbsoluteDate(pi.startEpochDateComponents, new TimeComponents(secOfDay), pi.timeScale);
// Check rollover
epoch = checkRollover(epoch, pi.header.getStartEpoch());
final CalibrationDetail cal = new CalibrationDetail(epoch, typeOfData, systemConfigId,
numberOfPointsRecorded, numberOfPointsUsed, oneWayDistance, systemDelay, delayShift, rms, skew,
kurtosis, peakMinusMean, typeIndicator, shiftTypeIndicator, detectorChannel, span, returnRate);
pi.dataBlock.addCalibrationDetailData(cal);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Calibration "Shot" Record. */
CALIB_SHOT("42") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Not implemented yet
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Session (Pass) Statistics Record. */
STAT("50") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Data contained in the line
final String[] values = SEPARATOR.split(line);
// Read data
final String systemConfigId = values[1];
final double rms = PS.toSI(Double.parseDouble(values[2]));
final double skewness = Double.parseDouble(values[3]);
final double kurtosis = Double.parseDouble(values[4]);
//
// The peak minus mean may be "*"
// 50 shao 35.0 -0.509 2.221 ****** 0
final double peakMinusMean = values[5].contains("*") ? Double.NaN : PS.toSI(Double.parseDouble(values[5]));
final int dataQualityIndicator = Integer.parseInt(values[6]);
final SessionStatistics stat = new SessionStatistics(systemConfigId, rms, skewness, kurtosis, peakMinusMean,
dataQualityIndicator);
pi.dataBlock.addSessionStatisticsData(stat);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Compatibility record. */
COMPATIBILITY("60") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Not implemented yet
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Comments. */
COMMENTS(COMMENTS_IDENTIFIER) {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Comment
final String comment = line.substring(2).trim();
pi.file.getComments().add(comment);
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H1, H2, H3, H4, H5, H8, H9, C0, C1, C2, C3, C4, C5, C6, C7, TEN, ELEVEN, TWELVE, METEO,
METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT, STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** Custom. */
CUSTOM("9\\d") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// Not implemented yet
}
/** {@inheritDoc} */
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H8, TEN, ELEVEN, TWELVE, METEO, METEO_SUPP, ANGLES, CALIB, CALIB_DETAILS, CALIB_SHOT,
STAT, COMPATIBILITY, COMMENTS, CUSTOM);
}
},
/** End of data block. */
H8("H8", "h8") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
// fixed 2022-12-12
// For the case of monthE is -1.
// Use the date of the last range data as the end epoch.
if (pi.header.getEndEpoch() == null) {
final List<RangeMeasurement> rangeData = pi.dataBlock.getRangeData();
pi.header.setEndEpoch(rangeData.get(rangeData.size() - 1).getDate());
}
// Fill data block
pi.dataBlock.setHeader(pi.header);
pi.dataBlock.setConfigurationRecords(pi.configurationRecords);
// Add the data block to the CRD file
pi.file.addDataBlock(pi.dataBlock);
// Initialize a new empty containers
pi.startEpochDateComponents = DateComponents.J2000_EPOCH;
final CRDHeader lastHeader = pi.header;
pi.header = new CRDHeader();
pi.configurationRecords = new CRDConfiguration();
pi.dataBlock = new CRDDataBlock();
// fill header with H1 H2 H3 if the file is for many targets, single system
// configuration (see P31 in crd201)
pi.header.setFormat(lastHeader.getFormat());
pi.header.setVersion(lastHeader.getVersion());
pi.header.setProductionEpoch(lastHeader.getProductionEpoch());
pi.header.setProductionHour(lastHeader.getProductionHour());
pi.header.setStationName(lastHeader.getStationName());
pi.header.setSystemIdentifier(lastHeader.getSystemIdentifier());
pi.header.setSystemNumber(lastHeader.getSystemNumber());
pi.header.setSystemOccupancy(lastHeader.getSystemOccupancy());
pi.header.setEpochIdentifier(lastHeader.getEpochIdentifier());
pi.header.setStationNetword(lastHeader.getStationNetword());
pi.header.setName(lastHeader.getName());
pi.header.setIlrsSatelliteId(lastHeader.getIlrsSatelliteId());
pi.header.setSic(lastHeader.getSic());
pi.header.setNoradId(lastHeader.getNoradId());
pi.header.setSpacecraftEpochTimeScale(lastHeader.getSpacecraftEpochTimeScale());
pi.header.setTargetClass(lastHeader.getTargetClass());
pi.header.setTargetLocation(lastHeader.getTargetLocation());
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Arrays.asList(H1, H4, H9, COMMENTS);
}
},
/** Last record in file. */
H9("H9", "h9") {
/** {@inheritDoc} */
@Override
public void parse(final String line, final ParseInfo pi) {
pi.done = true;
}
/** {@inheritDoc} */
@Override
public Iterable<LineParser> allowedNext() {
return Collections.singleton(H9);
}
};
/** Patterns for identifying line. */
private final Pattern[] patterns;
/** Identifiers. */
private final String[] identifiers;
/** Simple constructor.
* @param identifier regular expression for identifying line (i.e. first element)
*/
LineParser(final String... identifier) {
this.identifiers = identifier;
// Initialise patterns
this.patterns = new Pattern[identifiers.length];
for (int index = 0; index < patterns.length; index++) {
patterns[index] = Pattern.compile(identifiers[index]);
}
}
/** Parse a line.
* @param line line to parse
* @param pi holder for transient data
*/
public abstract void parse(String line, ParseInfo pi);
/** Get the allowed parsers for next line.
* @return allowed parsers for next line
*/
public abstract Iterable<LineParser> allowedNext();
/** Check if parser can handle line.
* @param line line to parse
* @return true if parser can handle the specified line
*/
public boolean canHandle(final String line) {
// Line identifier
final String lineId = SEPARATOR.split(line)[0];
// Loop on patterns
for (Pattern pattern : patterns) {
if (pattern.matcher(lineId).matches()) {
return true;
}
}
// No match
return false;
}
/**
* Read a boolean from a string value.
* @param value input value
* @return the correspondin boolean
*/
private static boolean readBoolean(final String value) {
return Integer.parseInt(value) == 1;
}
/**
* Read an integer value taking into consideration a possible "NaN".
* If the value is "NaN", the defaultValue is returned.
* @param value input string
* @param defaultValue the default value
* @return the corresponding integer value
*/
private static int readIntegerWithNaN(final String value, final int defaultValue) {
return CRD.STR_NAN.equalsIgnoreCase(value) ? defaultValue : Integer.parseInt(value);
}
}
}