EopCsvFilesLoader.java
/* Copyright 2023 Luc Maisonobe
* 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.frames;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.function.Supplier;
import org.orekit.data.DataProvidersManager;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
import org.orekit.utils.units.Unit;
/** Loader for EOP csv files (can be bulletin A, bulletin B, EOP C04…).
* <p>
* This class is immutable and hence thread-safe
* </p>
* @author Luc Maisonobe
* @since 12.0
*/
class EopCsvFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
/** Separator. */
private static final String SEPARATOR = ";";
/** Header for MJD. */
private static final String MJD = "MJD";
/** Header for Year. */
private static final String YEAR = "Year";
/** Header for Month. */
private static final String MONTH = "Month";
/** Header for Day. */
private static final String DAY = "Day";
/** Header for x_pole. */
private static final String X_POLE = "x_pole";
/** Header for y_pole. */
private static final String Y_POLE = "y_pole";
/** Header for x_rate. */
private static final String X_RATE = "x_rate";
/** Header for y_rate. */
private static final String Y_RATE = "y_rate";
/** Header for UT1-UTC. */
private static final String UT1_UTC = "UT1-UTC";
/** Header for LOD. */
private static final String LOD = "LOD";
/** Header for dPsi. */
private static final String DPSI = "dPsi";
/** Header for dEpsilon. */
private static final String DEPSILON = "dEpsilon";
/** Header for dX. */
private static final String DX = "dX";
/** Header for dY. */
private static final String DY = "dY";
/** Converter for milliarcseconds. */
private static final Unit MAS = Unit.parse("mas");
/** Converter for milliarcseconds per day. */
private static final Unit MAS_D = Unit.parse("mas/day");
/** Converter for milliseconds. */
private static final Unit MS = Unit.parse("ms");
/** Build a loader for IERS EOP csv files.
* @param supportedNames regular expression for supported files names
* @param manager provides access to the EOP C04 files.
* @param utcSupplier UTC time scale.
*/
EopCsvFilesLoader(final String supportedNames,
final DataProvidersManager manager,
final Supplier<TimeScale> utcSupplier) {
super(supportedNames, manager, utcSupplier);
}
/** {@inheritDoc} */
public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
final SortedSet<EOPEntry> history) {
final Parser parser = new Parser(converter, getUtc());
final EopParserLoader loader = new EopParserLoader(parser);
this.feed(loader);
history.addAll(loader.getEop());
}
/** Internal class performing the parsing. */
class Parser extends AbstractEopParser {
/** Configuration for ITRF versions. */
private final ItrfVersionProvider itrfVersionProvider;
/** Column number for MJD field. */
private int mjdColumn;
/** Column number for year field. */
private int yearColumn;
/** Column number for month field. */
private int monthColumn;
/** Column number for day field. */
private int dayColumn;
/** Column number for X pole field. */
private int xPoleColumn;
/** Column number for Y pole field. */
private int yPoleColumn;
/** Column number for X rate pole field. */
private int xRatePoleColumn;
/** Column number for Y rate pole field. */
private int yRatePoleColumn;
/** Column number for UT1-UTC field. */
private int ut1Column;
/** Column number for LOD field. */
private int lodColumn;
/** Column number for dX field. */
private int dxColumn;
/** Column number for dY field. */
private int dyColumn;
/** Column number for dPsi field. */
private int dPsiColumn;
/** Column number for dEpsilon field. */
private int dEpsilonColumn;
/** ITRF version configuration. */
private ITRFVersionLoader.ITRFVersionConfiguration configuration;
/** Simple constructor.
* @param converter converter to use
* @param utc time scale for parsing dates.
*/
Parser(final NutationCorrectionConverter converter,
final TimeScale utc) {
super(converter, null, utc);
this.itrfVersionProvider = new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES,
getDataProvidersManager());
}
/** {@inheritDoc} */
public Collection<EOPEntry> parse(final InputStream input, final String name)
throws IOException, OrekitException {
final List<EOPEntry> history = new ArrayList<>();
// set up a reader for line-oriented csv files
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
// reset parse info to start new file (do not clear history!)
int lineNumber = 0;
configuration = null;
// read all file
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
++lineNumber;
final boolean parsed;
if (lineNumber == 1) {
parsed = parseHeaderLine(line);
} else {
history.add(parseDataLine(line, name));
parsed = true;
}
if (!parsed) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
}
// check if we have read something
if (lineNumber < 2) {
throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
}
}
return history;
}
/** Parse the header line.
* @param headerLine header line
* @return true if line was parsed correctly
*/
private boolean parseHeaderLine(final String headerLine) {
// reset columns numbers
mjdColumn = -1;
yearColumn = -1;
monthColumn = -1;
dayColumn = -1;
xPoleColumn = -1;
yPoleColumn = -1;
xRatePoleColumn = -1;
yRatePoleColumn = -1;
ut1Column = -1;
lodColumn = -1;
dxColumn = -1;
dyColumn = -1;
dPsiColumn = -1;
dEpsilonColumn = -1;
// split header fields
final String[] fields = headerLine.split(SEPARATOR);
// affect column numbers according to header fields
for (int column = 0; column < fields.length; ++column) {
switch (fields[column]) {
case MJD :
mjdColumn = column;
break;
case YEAR :
yearColumn = column;
break;
case MONTH :
monthColumn = column;
break;
case DAY :
dayColumn = column;
break;
case X_POLE :
xPoleColumn = column;
break;
case Y_POLE :
yPoleColumn = column;
break;
case X_RATE :
xRatePoleColumn = column;
break;
case Y_RATE :
yRatePoleColumn = column;
break;
case UT1_UTC :
ut1Column = column;
break;
case LOD :
lodColumn = column;
break;
case DX :
dxColumn = column;
break;
case DY :
dyColumn = column;
break;
case DPSI :
dPsiColumn = column;
break;
case DEPSILON :
dEpsilonColumn = column;
break;
default :
// ignored column
}
}
// check all required files are present (we just allow pole rates to be missing)
return mjdColumn >= 0 && yearColumn >= 0 && monthColumn >= 0 && dayColumn >= 0 &&
xPoleColumn >= 0 && yPoleColumn >= 0 && ut1Column >= 0 && lodColumn >= 0 &&
(dxColumn >= 0 && dyColumn >= 0 || dPsiColumn >= 0 && dEpsilonColumn >= 0);
}
/** Parse a data line.
* @param line line to parse
* @param name file name (for error messages)
* @return parsed entry
*/
private EOPEntry parseDataLine(final String line, final String name) {
final String[] fields = line.split(SEPARATOR);
// check date
final DateComponents dc = new DateComponents(Integer.parseInt(fields[yearColumn]),
Integer.parseInt(fields[monthColumn]),
Integer.parseInt(fields[dayColumn]));
final int mjd = Integer.parseInt(fields[mjdColumn]);
if (dc.getMJD() != mjd) {
throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
}
final AbsoluteDate date = new AbsoluteDate(dc, getUtc());
if (configuration == null || !configuration.isValid(mjd)) {
// get a configuration for current name and date range
configuration = itrfVersionProvider.getConfiguration(name, mjd);
}
final double x = parseField(fields, xPoleColumn, MAS);
final double y = parseField(fields, yPoleColumn, MAS);
final double xRate = parseField(fields, xRatePoleColumn, MAS_D);
final double yRate = parseField(fields, yRatePoleColumn, MAS_D);
final double dtu1 = parseField(fields, ut1Column, MS);
final double lod = parseField(fields, lodColumn, MS);
if (dxColumn >= 0) {
// non-rotatin origin paradigm
final double dx = parseField(fields, dxColumn, MAS);
final double dy = parseField(fields, dyColumn, MAS);
final double[] equinox = getConverter().toEquinox(date, dx, dy);
return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
equinox[0], equinox[1], dx, dy,
configuration.getVersion(), date);
} else {
// equinox paradigm
final double ddPsi = parseField(fields, dPsiColumn, MAS);
final double dddEpsilon = parseField(fields, dEpsilonColumn, MAS);
final double[] nro = getConverter().toNonRotating(date, ddPsi, dddEpsilon);
return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
ddPsi, dddEpsilon, nro[0], nro[1],
configuration.getVersion(), date);
}
}
/** Parse one field.
* @param fields fields array to parse
* @param index index in the field array (negative for ignored fields)
* @param unit field unit
* @return parsed and converted field
*/
private double parseField(final String[] fields, final int index, final Unit unit) {
return (index < 0 || index >= fields.length || fields[index].isEmpty()) ?
Double.NaN :
unit.toSI(Double.parseDouble(fields[index]));
}
}
}