BulletinBFilesLoader.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.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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hipparchus.util.FastMath;
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.Month;
import org.orekit.time.TimeScale;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
import org.orekit.utils.units.UnitsConverter;
/** Loader for bulletin B files.
* <p>Bulletin B files contain {@link EOPEntry
* Earth Orientation Parameters} for a few months periods.
* They correspond to finalized data, suitable for long term
* a posteriori analysis.</p>
* <p>The bulletin B files are recognized thanks to their base names,
* which must match one of the patterns <code>bulletinb_IAU2000-###.txt</code>,
* <code>bulletinb_IAU2000.###</code>, <code>bulletinb-###.txt</code> or
* <code>bulletinb.###</code> (or the same ending with <code>.gz</code>
* for gzip-compressed files) where # stands for a digit character.</p>
* <p>
* Starting with bulletin B 252 published in February 2009, buletins B are
* written in a format containing nutation corrections for both the
* new IAU2000 nutation model as dx, dy entries in its section 1 and nutation
* corrections for the old IAU1976 nutation model as dPsi, dEpsilon entries in
* its section 2. These bulletins are available from IERS <a
* href="ftp://ftp.iers.org/products/eop/bulletinb/format_2009/">
* FTP site</a>. They are also available with exactly the same content
* (but a different naming convention) from <a
* href="http://hpiers.obspm.fr/eoppc/bul/bulb_new/">Paris-Meudon
* observatory site</a>.
* </p>
* <p>
* Ending with bulletin B 263 published in January 2010, bulletins B were
* written in a format containing only one type of nutation corrections in its
* section 1, either for new IAU2000 nutation model as dx, dy entries or the old
* IAU1976 nutation model as dPsi, dEpsilon entries, depending on the file (a pair of
* files with different name was published each month between March 2003 and January 2010).
* </p>
* <p>
* Bulletin B in csv format must be read using {@link EopCsvFilesLoader} rather
* than using this loader. Bulletin B in xml format must be read using {@link EopXmlLoader}
* rather than using this loader.
* </p>
* <p>
* This class handles both the old and the new format.
* </p>
* <p>
* This class is immutable and hence thread-safe
* </p>
* @author Luc Maisonobe
* @see EopCsvFilesLoader
* @see EopXmlLoader
*/
class BulletinBFilesLoader extends AbstractEopLoader implements EopHistoryLoader {
/** Section 1 header pattern. */
private static final Pattern SECTION_1_HEADER;
/** Section 2 header pattern for old format. */
private static final Pattern SECTION_2_HEADER_OLD;
/** Section 3 header pattern. */
private static final Pattern SECTION_3_HEADER;
/** Pattern for line introducing the final bulletin B values. */
private static final Pattern FINAL_VALUES_START;
/** Pattern for line introducing the bulletin B preliminary extension. */
private static final Pattern FINAL_VALUES_END;
/** Data line pattern in section 1 (old format). */
private static final Pattern SECTION_1_DATA_OLD_FORMAT;
/** Data line pattern in section 2. */
private static final Pattern SECTION_2_DATA_OLD_FORMAT;
/** Data line pattern in section 1 (new format). */
private static final Pattern SECTION_1_DATA_NEW_FORMAT;
/** Data line pattern in section 3 (new format). */
private static final Pattern SECTION_3_DATA_NEW_FORMAT;
static {
// the section headers lines in the old bulletin B monthly data files have
// the following form (the indentation discrepancy for section 6 is really
// present in the available files):
// 1 - EARTH ORIENTATION PARAMETERS (IERS evaluation).
// either
// 2 - SMOOTHED VALUES OF x, y, UT1, D, DPSI, DEPSILON (IERS EVALUATION)
// or
// 2 - SMOOTHED VALUES OF x, y, UT1, D, dX, dY (IERS EVALUATION)
// 3 - NORMAL VALUES OF THE EARTH ORIENTATION PARAMETERS AT FIVE-DAY INTERVALS
// 4 - DURATION OF THE DAY AND ANGULAR VELOCITY OF THE EARTH (IERS evaluation).
// 5 - INFORMATION ON TIME SCALES
// 6 - SUMMARY OF CONTRIBUTED EARTH ORIENTATION PARAMETERS SERIES
//
// the section headers lines in the new bulletin B monthly data files have
// the following form:
// 1 - DAILY FINAL VALUES OF x, y, UT1-UTC, dX, dY
// 2 - DAILY FINAL VALUES OF CELESTIAL POLE OFFSETS dPsi1980 & dEps1980
// 3 - EARTH ANGULAR VELOCITY : DAILY FINAL VALUES OF LOD, OMEGA AT 0hUTC
// 4 - INFORMATION ON TIME SCALES
// 5 - SUMMARY OF CONTRIBUTED EARTH ORIENTATION PARAMETERS SERIES
SECTION_1_HEADER = Pattern.compile("^ +1 - (\\p{Upper}+) \\p{Upper}+ \\p{Upper}+.*");
SECTION_2_HEADER_OLD = Pattern.compile("^ +2 - SMOOTHED \\p{Upper}+ \\p{Upper}+.*((?:DPSI, DEPSILON)|(?:dX, dY)).*");
SECTION_3_HEADER = Pattern.compile("^ +3 - \\p{Upper}+ \\p{Upper}+ \\p{Upper}+.*");
// the markers bracketing the final values in section 1 in the old bulletin B
// monthly data files have the following form:
//
// Final Bulletin B values.
// ...
// Preliminary extension, to be updated weekly in Bulletin A and monthly
// in Bulletin B.
//
// the markers bracketing the final values in section 1 in the new bulletin B
// monthly data files have the following form:
//
// Final values
// ...
// Preliminary extension
//
FINAL_VALUES_START = Pattern.compile("^\\p{Blank}+Final( Bulletin B)? values.*");
FINAL_VALUES_END = Pattern.compile("^\\p{Blank}+Preliminary extension.*");
// the data lines in the old bulletin B monthly data files have the following form:
// in section 1:
// AUG 1 55044 0.22176 0.49302 0.231416 -33.768584 -69.1 -8.9
// AUG 6 55049 0.23202 0.48003 0.230263 -33.769737 -69.5 -8.5
// in section 2:
// AUG 1 55044 0.22176 0.49302 0.230581 -0.835 -0.310 -69.1 -8.9
// AUG 2 55045 0.22395 0.49041 0.230928 -0.296 -0.328 -69.5 -8.9
//
// the data lines in the new bulletin B monthly data files have the following form:
// in section 1:
// 2009 8 2 55045 223.954 490.410 230.9277 0.214 -0.056 0.008 0.009 0.0641 0.048 0.121
// 2009 8 3 55046 225.925 487.700 231.2186 0.300 -0.138 0.010 0.012 0.0466 0.099 0.248
// 2009 8 4 55047 227.931 485.078 231.3929 0.347 -0.231 0.019 0.023 0.0360 0.099 0.249
// 2009 8 5 55048 230.016 482.445 231.4601 0.321 -0.291 0.025 0.028 0.0441 0.095 0.240
// 2009 8 6 55049 232.017 480.026 231.3619 0.267 -0.273 0.025 0.029 0.0477 0.038 0.095
// in section 2:
// 2009 8 2 55045 -69.474 -8.929 0.199 0.121
// 2009 8 3 55046 -69.459 -9.016 0.250 0.248
// 2009 8 4 55047 -69.401 -9.039 0.250 0.249
// 2009 8 5 55048 -69.425 -8.864 0.247 0.240
// 2009 8 6 55049 -69.510 -8.539 0.153 0.095
// in section 3:
// 2009 8 2 55045 -0.3284 0.0013 15.04106723584 0.00000000023
// 2009 8 3 55046 -0.2438 0.0013 15.04106722111 0.00000000023
// 2009 8 4 55047 -0.1233 0.0013 15.04106720014 0.00000000023
// 2009 8 5 55048 0.0119 0.0013 15.04106717660 0.00000000023
// 2009 8 6 55049 0.1914 0.0013 15.04106714535 0.00000000023
final StringBuilder builder = new StringBuilder("^\\p{Blank}+(?:");
for (final Month month : Month.values()) {
builder.append(month.getUpperCaseAbbreviation());
builder.append('|');
}
builder.delete(builder.length() - 1, builder.length());
builder.append(")");
final String integerPattern = "[-+]?\\p{Digit}+";
final String realPattern = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";
final String monthNameField = builder.toString();
final String ignoredIntegerField = "\\p{Blank}*" + integerPattern;
final String storedIntegerField = "\\p{Blank}*(" + integerPattern + ")";
final String mjdField = "\\p{Blank}+(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})";
final String storedRealField = "\\p{Blank}+(" + realPattern + ")";
final String ignoredRealField = "\\p{Blank}+" + realPattern;
final String finalBlanks = "\\p{Blank}*$";
SECTION_1_DATA_OLD_FORMAT = Pattern.compile(monthNameField + ignoredIntegerField + mjdField +
ignoredRealField + ignoredRealField + ignoredRealField +
ignoredRealField + ignoredRealField + ignoredRealField +
finalBlanks);
SECTION_2_DATA_OLD_FORMAT = Pattern.compile(monthNameField + ignoredIntegerField + mjdField +
storedRealField + storedRealField + storedRealField +
ignoredRealField +
storedRealField + storedRealField + storedRealField +
finalBlanks);
SECTION_1_DATA_NEW_FORMAT = Pattern.compile(storedIntegerField + storedIntegerField + storedIntegerField + mjdField +
storedRealField + storedRealField + storedRealField +
storedRealField + storedRealField + ignoredRealField + ignoredRealField +
ignoredRealField + ignoredRealField + ignoredRealField +
finalBlanks);
SECTION_3_DATA_NEW_FORMAT = Pattern.compile(ignoredIntegerField + ignoredIntegerField + ignoredIntegerField + mjdField +
storedRealField +
ignoredRealField + ignoredRealField + ignoredRealField +
finalBlanks);
}
/** Build a loader for IERS bulletins B files.
* @param supportedNames regular expression for supported files names
* @param manager provides access to the bulletin B files.
* @param utcSupplier UTC time scale.
*/
BulletinBFilesLoader(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 ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
ITRFVersionLoader.SUPPORTED_NAMES,
getDataProvidersManager());
final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
final EopParserLoader loader = new EopParserLoader(parser);
this.feed(loader);
history.addAll(loader.getEop());
}
/** Internal class performing the parsing. */
static class Parser extends AbstractEopParser {
/** ITRF version configuration. */
private ITRFVersionLoader.ITRFVersionConfiguration configuration;
/** History entries. */
private List<EOPEntry> history;
/** Map for fields read in different sections. */
private final Map<Integer, double[]> fieldsMap;
/** Current line number. */
private int lineNumber;
/** Current line. */
private String line;
/** Start of final data. */
private int mjdMin;
/** End of final data. */
private int mjdMax;
/**
* Simple constructor.
*
* @param converter converter to use
* @param itrfVersionProvider to use for determining the ITRF version of the EOP.
* @param utc time scale for parsing dates.
*/
Parser(final NutationCorrectionConverter converter,
final ItrfVersionProvider itrfVersionProvider,
final TimeScale utc) {
super(converter, itrfVersionProvider, utc);
this.fieldsMap = new HashMap<>();
this.lineNumber = 0;
this.mjdMin = Integer.MAX_VALUE;
this.mjdMax = Integer.MIN_VALUE;
}
/** {@inheritDoc} */
@Override
public Collection<EOPEntry> parse(final InputStream input, final String name)
throws IOException {
// set up a reader for line-oriented bulletin B files
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
// reset parse info to start new file
fieldsMap.clear();
lineNumber = 0;
mjdMin = Integer.MAX_VALUE;
mjdMax = Integer.MIN_VALUE;
history = new ArrayList<>();
configuration = null;
// skip header up to section 1 and check if we are parsing an old or new format file
final Matcher section1Matcher = seekToLine(SECTION_1_HEADER, reader, name);
final boolean isOldFormat = "EARTH".equals(section1Matcher.group(1));
if (isOldFormat) {
// extract MJD bounds for final data from section 1
loadMJDBoundsOldFormat(reader, name);
final Matcher section2Matcher = seekToLine(SECTION_2_HEADER_OLD, reader, name);
final boolean isNonRotatingOrigin = section2Matcher.group(1).startsWith("dX");
loadEOPOldFormat(isNonRotatingOrigin, reader, name);
} else {
// extract x, y, UT1-UTC, dx, dy from section 1
loadXYDTDxDyNewFormat(reader, name);
// skip to section 3
seekToLine(SECTION_3_HEADER, reader, name);
// extract LOD data from section 3
loadLODNewFormat(reader, name);
// set up the EOP entries
for (Map.Entry<Integer, double[]> entry : fieldsMap.entrySet()) {
final int mjd = entry.getKey();
final double[] array = entry.getValue();
if (Double.isNaN(array[0] + array[1] + array[2] + array[3] + array[4] + array[5])) {
throw notifyUnexpectedErrorEncountered(name);
}
final AbsoluteDate mjdDate =
new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
getUtc());
final double[] equinox = getConverter().toEquinox(mjdDate, array[4], array[5]);
if (configuration == null || !configuration.isValid(mjd)) {
// get a configuration for current name and date range
configuration = getItrfVersionProvider().getConfiguration(name, mjd);
}
history.add(new EOPEntry(mjd, array[0], array[1], array[2], array[3],
Double.NaN, Double.NaN,
equinox[0], equinox[1], array[4], array[5],
configuration.getVersion(), mjdDate));
}
}
}
return history;
}
/** Read until a line matching a pattern is found.
* @param pattern pattern to look for
* @param reader reader from where file content is obtained
* @param name name of the file (or zip entry)
* @return the matching matcher for the line
* @exception IOException if data can't be read
*/
private Matcher seekToLine(final Pattern pattern, final BufferedReader reader, final String name)
throws IOException {
for (line = reader.readLine(); line != null; line = reader.readLine()) {
++lineNumber;
final Matcher matcher = pattern.matcher(line);
if (matcher.matches()) {
return matcher;
}
}
// we have reached end of file and not found a matching line
throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
name, lineNumber);
}
/** Read MJD bounds of the final data part from section 1 in the old bulletin B format.
* @param reader reader from where file content is obtained
* @param name name of the file (or zip entry)
* @exception IOException if data can't be read
*/
private void loadMJDBoundsOldFormat(final BufferedReader reader, final String name)
throws IOException {
boolean inFinalValuesPart = false;
for (line = reader.readLine(); line != null; line = reader.readLine()) {
lineNumber++;
Matcher matcher = FINAL_VALUES_START.matcher(line);
if (matcher.matches()) {
// we are entering final values part (in section 1)
inFinalValuesPart = true;
} else if (inFinalValuesPart) {
matcher = SECTION_1_DATA_OLD_FORMAT.matcher(line);
if (matcher.matches()) {
// this is a data line, build an entry from the extracted fields
final int mjd = Integer.parseInt(matcher.group(1));
mjdMin = FastMath.min(mjdMin, mjd);
mjdMax = FastMath.max(mjdMax, mjd);
} else {
matcher = FINAL_VALUES_END.matcher(line);
if (matcher.matches()) {
// we leave final values part
return;
}
}
}
}
throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE_AFTER_LINE,
name, lineNumber);
}
/** Read EOP data from section 2 in the old bulletin B format.
* @param isNonRotatingOrigin if true, the file contain Non-Rotating Origin nutation corrections
* @param reader reader from where file content is obtained
* @param name name of the file (or zip entry)
* @exception IOException if data can't be read
*/
private void loadEOPOldFormat(final boolean isNonRotatingOrigin,
final BufferedReader reader, final String name)
throws IOException {
// read the data lines in the final values part inside section 2
line = reader.readLine();
while (line != null) {
lineNumber++;
final Matcher matcher = SECTION_2_DATA_OLD_FORMAT.matcher(line);
if (matcher.matches()) {
// this is a data line, build an entry from the extracted fields
final int mjd = Integer.parseInt(matcher.group(1));
final double x = Double.parseDouble(matcher.group(2)) * Constants.ARC_SECONDS_TO_RADIANS;
final double y = Double.parseDouble(matcher.group(3)) * Constants.ARC_SECONDS_TO_RADIANS;
final double dtu1 = Double.parseDouble(matcher.group(4));
final double lod = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(5)));
if (mjd >= mjdMin) {
final AbsoluteDate mjdDate =
new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
getUtc());
final double[] equinox;
final double[] nro;
if (isNonRotatingOrigin) {
nro = new double[] {
UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6))),
UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(7)))
};
equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
} else {
equinox = new double[] {
UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6))),
UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(7)))
};
nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
}
if (configuration == null || !configuration.isValid(mjd)) {
// get a configuration for current name and date range
configuration = getItrfVersionProvider().getConfiguration(name, mjd);
}
history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
equinox[0], equinox[1], nro[0], nro[1],
configuration.getVersion(), mjdDate));
line = mjd < mjdMax ? reader.readLine() : null;
} else {
line = reader.readLine();
}
} else {
line = reader.readLine();
}
}
}
/** Read X, Y, UT1-UTC, dx, dy from section 1 in the new bulletin B format.
* @param reader reader from where file content is obtained
* @param name name of the file (or zip entry)
* @exception IOException if data can't be read
*/
private void loadXYDTDxDyNewFormat(final BufferedReader reader, final String name)
throws IOException {
boolean inFinalValuesPart = false;
line = reader.readLine();
while (line != null) {
lineNumber++;
Matcher matcher = FINAL_VALUES_START.matcher(line);
if (matcher.matches()) {
// we are entering final values part (in section 1)
inFinalValuesPart = true;
line = reader.readLine();
} else if (inFinalValuesPart) {
matcher = SECTION_1_DATA_NEW_FORMAT.matcher(line);
if (matcher.matches()) {
// this is a data line, build an entry from the extracted fields
final int year = Integer.parseInt(matcher.group(1));
final int month = Integer.parseInt(matcher.group(2));
final int day = Integer.parseInt(matcher.group(3));
final int mjd = Integer.parseInt(matcher.group(4));
if (new DateComponents(year, month, day).getMJD() != mjd) {
throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
name, year, month, day, mjd);
}
mjdMin = FastMath.min(mjdMin, mjd);
mjdMax = FastMath.max(mjdMax, mjd);
final double x = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(5)));
final double y = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(6)));
final double dtu1 = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(7)));
final double dx = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(8)));
final double dy = UnitsConverter.MILLI_ARC_SECONDS_TO_RADIANS.convert(Double.parseDouble(matcher.group(9)));
fieldsMap.put(mjd,
new double[] {
dtu1, Double.NaN, x, y, dx, dy
});
line = reader.readLine();
} else {
matcher = FINAL_VALUES_END.matcher(line);
line = matcher.matches() ? null : reader.readLine();
}
} else {
line = reader.readLine();
}
}
}
/** Read LOD from section 3 in the new bulletin B format.
* @param reader reader from where file content is obtained
* @param name name of the file (or zip entry)
* @exception IOException if data can't be read
*/
private void loadLODNewFormat(final BufferedReader reader, final String name)
throws IOException {
line = reader.readLine();
while (line != null) {
lineNumber++;
final Matcher matcher = SECTION_3_DATA_NEW_FORMAT.matcher(line);
if (matcher.matches()) {
// this is a data line, build an entry from the extracted fields
final int mjd = Integer.parseInt(matcher.group(1));
if (mjd >= mjdMin) {
final double lod = UnitsConverter.MILLI_SECONDS_TO_SECONDS.convert(Double.parseDouble(matcher.group(2)));
final double[] array = fieldsMap.get(mjd);
if (array == null) {
throw notifyUnexpectedErrorEncountered(name);
}
array[1] = lod;
line = mjd >= mjdMax ? null : reader.readLine();
} else {
line = reader.readLine();
}
} else {
line = reader.readLine();
}
}
}
/** Create an exception to be thrown.
* @param name name of the file (or zip entry)
* @return OrekitException always thrown to notify an unexpected error has been
* encountered by the caller
*/
private OrekitException notifyUnexpectedErrorEncountered(final String name) {
String loaderName = BulletinBFilesLoader.class.getName();
loaderName = loaderName.substring(loaderName.lastIndexOf('.') + 1);
return new OrekitException(OrekitMessages.UNEXPECTED_FILE_FORMAT_ERROR_FOR_LOADER,
name, loaderName);
}
}
}