EopCsvFilesLoader.java

  1. /* Copyright 2022-2025 Luc Maisonobe
  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.frames;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.ArrayList;
  24. import java.util.Collection;
  25. import java.util.List;
  26. import java.util.SortedSet;
  27. import java.util.function.Supplier;

  28. import org.orekit.data.DataProvidersManager;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.time.AbsoluteDate;
  32. import org.orekit.time.DateComponents;
  33. import org.orekit.time.TimeScale;
  34. import org.orekit.utils.IERSConventions;
  35. import org.orekit.utils.IERSConventions.NutationCorrectionConverter;
  36. import org.orekit.utils.units.Unit;

  37. /** Loader for EOP csv files (can be bulletin A, bulletin B, EOP C04…).
  38.  * <p>
  39.  * This class is immutable and hence thread-safe
  40.  * </p>
  41.  * @author Luc Maisonobe
  42.  * @since 12.0
  43.  */
  44. class EopCsvFilesLoader extends AbstractEopLoader implements EopHistoryLoader {

  45.     /** Separator. */
  46.     private static final String SEPARATOR = ";";

  47.     /** Header for MJD. */
  48.     private static final String MJD = "MJD";

  49.     /** Header for Year. */
  50.     private static final String YEAR = "Year";

  51.     /** Header for Month. */
  52.     private static final String MONTH = "Month";

  53.     /** Header for Day. */
  54.     private static final String DAY = "Day";

  55.     /** Header for x_pole. */
  56.     private static final String X_POLE = "x_pole";

  57.     /** Header for y_pole. */
  58.     private static final String Y_POLE = "y_pole";

  59.     /** Header for x_rate. */
  60.     private static final String X_RATE = "x_rate";

  61.     /** Header for y_rate. */
  62.     private static final String Y_RATE = "y_rate";

  63.     /** Header for UT1-UTC. */
  64.     private static final String UT1_UTC = "UT1-UTC";

  65.     /** Header for LOD. */
  66.     private static final String LOD = "LOD";

  67.     /** Header for dPsi. */
  68.     private static final String DPSI = "dPsi";

  69.     /** Header for dEpsilon. */
  70.     private static final String DEPSILON = "dEpsilon";

  71.     /** Header for dX. */
  72.     private static final String DX = "dX";

  73.     /** Header for dY. */
  74.     private static final String DY = "dY";

  75.     /** Converter for milliarcseconds. */
  76.     private static final Unit MAS = Unit.parse("mas");

  77.     /** Converter for milliarcseconds per day. */
  78.     private static final Unit MAS_D = Unit.parse("mas/day");

  79.     /** Converter for milliseconds. */
  80.     private static final Unit MS = Unit.parse("ms");

  81.     /** Build a loader for IERS EOP csv files.
  82.      * @param supportedNames regular expression for supported files names
  83.      * @param manager provides access to the EOP C04 files.
  84.      * @param utcSupplier UTC time scale.
  85.      */
  86.     EopCsvFilesLoader(final String supportedNames,
  87.                       final DataProvidersManager manager,
  88.                       final Supplier<TimeScale> utcSupplier) {
  89.         super(supportedNames, manager, utcSupplier);
  90.     }

  91.     /** {@inheritDoc} */
  92.     public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
  93.                             final SortedSet<EOPEntry> history) {
  94.         final Parser parser = new Parser(converter, getUtc());
  95.         final EopParserLoader loader = new EopParserLoader(parser);
  96.         this.feed(loader);
  97.         history.addAll(loader.getEop());
  98.     }

  99.     /** Internal class performing the parsing. */
  100.     class Parser extends AbstractEopParser {

  101.         /** Configuration for ITRF versions. */
  102.         private final ItrfVersionProvider itrfVersionProvider;

  103.         /** Column number for MJD field. */
  104.         private int mjdColumn;

  105.         /** Column number for year field. */
  106.         private int yearColumn;

  107.         /** Column number for month field. */
  108.         private int monthColumn;

  109.         /** Column number for day field. */
  110.         private int dayColumn;

  111.         /** Column number for X pole field. */
  112.         private int xPoleColumn;

  113.         /** Column number for Y pole field. */
  114.         private int yPoleColumn;

  115.         /** Column number for X rate pole field. */
  116.         private int xRatePoleColumn;

  117.         /** Column number for Y rate pole field. */
  118.         private int yRatePoleColumn;

  119.         /** Column number for UT1-UTC field. */
  120.         private int ut1Column;

  121.         /** Column number for LOD field. */
  122.         private int lodColumn;

  123.         /** Column number for dX field. */
  124.         private int dxColumn;

  125.         /** Column number for dY field. */
  126.         private int dyColumn;

  127.         /** Column number for dPsi field. */
  128.         private int dPsiColumn;

  129.         /** Column number for dEpsilon field. */
  130.         private int dEpsilonColumn;

  131.         /** ITRF version configuration. */
  132.         private ITRFVersionLoader.ITRFVersionConfiguration configuration;

  133.         /** Simple constructor.
  134.          * @param converter converter to use
  135.          * @param utc       time scale for parsing dates.
  136.          */
  137.         Parser(final NutationCorrectionConverter converter,
  138.                final TimeScale utc) {
  139.             super(converter, null, utc);
  140.             this.itrfVersionProvider = new ITRFVersionLoader(ITRFVersionLoader.SUPPORTED_NAMES,
  141.                                                              getDataProvidersManager());
  142.         }

  143.         /** {@inheritDoc} */
  144.         public Collection<EOPEntry> parse(final InputStream input, final String name)
  145.             throws IOException, OrekitException {

  146.             final List<EOPEntry> history = new ArrayList<>();

  147.             // set up a reader for line-oriented csv files
  148.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
  149.                 // reset parse info to start new file (do not clear history!)
  150.                 int lineNumber = 0;
  151.                 configuration  = null;

  152.                 // read all file
  153.                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  154.                     ++lineNumber;

  155.                     final boolean parsed;
  156.                     if (lineNumber == 1) {
  157.                         parsed = parseHeaderLine(line);
  158.                     } else {
  159.                         history.add(parseDataLine(line, name));
  160.                         parsed = true;
  161.                     }

  162.                     if (!parsed) {
  163.                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  164.                                 lineNumber, name, line);
  165.                     }
  166.                 }

  167.                 // check if we have read something
  168.                 if (lineNumber < 2) {
  169.                     throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  170.                 }
  171.             }

  172.             return history;
  173.         }

  174.         /** Parse the header line.
  175.          * @param headerLine header line
  176.          * @return true if line was parsed correctly
  177.          */
  178.         private boolean parseHeaderLine(final String headerLine) {

  179.             // reset columns numbers
  180.             mjdColumn       = -1;
  181.             yearColumn      = -1;
  182.             monthColumn     = -1;
  183.             dayColumn       = -1;
  184.             xPoleColumn     = -1;
  185.             yPoleColumn     = -1;
  186.             xRatePoleColumn = -1;
  187.             yRatePoleColumn = -1;
  188.             ut1Column       = -1;
  189.             lodColumn       = -1;
  190.             dxColumn        = -1;
  191.             dyColumn        = -1;
  192.             dPsiColumn      = -1;
  193.             dEpsilonColumn  = -1;

  194.             // split header fields
  195.             final String[] fields = headerLine.split(SEPARATOR);

  196.             // affect column numbers according to header fields
  197.             for (int column = 0; column < fields.length; ++column) {
  198.                 switch (fields[column]) {
  199.                     case MJD :
  200.                         mjdColumn = column;
  201.                         break;
  202.                     case YEAR :
  203.                         yearColumn = column;
  204.                         break;
  205.                     case MONTH :
  206.                         monthColumn = column;
  207.                         break;
  208.                     case DAY :
  209.                         dayColumn = column;
  210.                         break;
  211.                     case X_POLE :
  212.                         xPoleColumn = column;
  213.                         break;
  214.                     case Y_POLE :
  215.                         yPoleColumn = column;
  216.                         break;
  217.                     case X_RATE :
  218.                         xRatePoleColumn = column;
  219.                         break;
  220.                     case Y_RATE :
  221.                         yRatePoleColumn = column;
  222.                         break;
  223.                     case UT1_UTC :
  224.                         ut1Column = column;
  225.                         break;
  226.                     case LOD :
  227.                         lodColumn = column;
  228.                         break;
  229.                     case DX :
  230.                         dxColumn = column;
  231.                         break;
  232.                     case DY :
  233.                         dyColumn = column;
  234.                         break;
  235.                     case DPSI :
  236.                         dPsiColumn = column;
  237.                         break;
  238.                     case DEPSILON :
  239.                         dEpsilonColumn = column;
  240.                         break;
  241.                     default :
  242.                         // ignored column
  243.                 }
  244.             }

  245.             // check all required files are present (we just allow pole rates to be missing)
  246.             return mjdColumn >= 0 && yearColumn >= 0 && monthColumn >= 0 && dayColumn >= 0 &&
  247.                    xPoleColumn >= 0 && yPoleColumn >= 0 && ut1Column >= 0 && lodColumn >= 0 &&
  248.                    (dxColumn >= 0 && dyColumn >= 0 || dPsiColumn >= 0 && dEpsilonColumn >= 0);

  249.         }

  250.         /** Parse a data line.
  251.          * @param line line to parse
  252.          * @param name file name (for error messages)
  253.          * @return parsed entry
  254.          */
  255.         private EOPEntry parseDataLine(final String line, final String name) {

  256.             final String[] fields = line.split(SEPARATOR);

  257.             // check date
  258.             final DateComponents dc = new DateComponents(Integer.parseInt(fields[yearColumn]),
  259.                                                          Integer.parseInt(fields[monthColumn]),
  260.                                                          Integer.parseInt(fields[dayColumn]));
  261.             final int    mjd   = Integer.parseInt(fields[mjdColumn]);
  262.             if (dc.getMJD() != mjd) {
  263.                 throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
  264.                                           name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
  265.             }
  266.             final AbsoluteDate date = new AbsoluteDate(dc, getUtc());

  267.             if (configuration == null || !configuration.isValid(mjd)) {
  268.                 // get a configuration for current name and date range
  269.                 configuration = itrfVersionProvider.getConfiguration(name, mjd);
  270.             }

  271.             final double x     = parseField(fields, xPoleColumn,     MAS);
  272.             final double y     = parseField(fields, yPoleColumn,     MAS);
  273.             final double xRate = parseField(fields, xRatePoleColumn, MAS_D);
  274.             final double yRate = parseField(fields, yRatePoleColumn, MAS_D);
  275.             final double dtu1  = parseField(fields, ut1Column,       MS);
  276.             final double lod   = parseField(fields, lodColumn,       MS);

  277.             if (dxColumn >= 0) {
  278.                 // non-rotatin origin paradigm
  279.                 final double dx = parseField(fields, dxColumn, MAS);
  280.                 final double dy = parseField(fields, dyColumn, MAS);
  281.                 final double[] equinox = getConverter().toEquinox(date, dx, dy);
  282.                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
  283.                                     equinox[0], equinox[1], dx, dy,
  284.                                     configuration.getVersion(), date);
  285.             } else {
  286.                 // equinox paradigm
  287.                 final double ddPsi      = parseField(fields, dPsiColumn,     MAS);
  288.                 final double dddEpsilon = parseField(fields, dEpsilonColumn, MAS);
  289.                 final double[] nro = getConverter().toNonRotating(date, ddPsi, dddEpsilon);
  290.                 return new EOPEntry(dc.getMJD(), dtu1, lod, x, y, xRate, yRate,
  291.                                     ddPsi, dddEpsilon, nro[0], nro[1],
  292.                                     configuration.getVersion(), date);
  293.             }


  294.         }

  295.         /** Parse one field.
  296.          * @param fields fields array to parse
  297.          * @param index index in the field array (negative for ignored fields)
  298.          * @param unit field unit
  299.          * @return parsed and converted field
  300.          */
  301.         private double parseField(final String[] fields, final int index, final Unit unit) {
  302.             return (index < 0 || index >= fields.length || fields[index].isEmpty()) ?
  303.                    Double.NaN :
  304.                    unit.toSI(Double.parseDouble(fields[index]));
  305.         }

  306.     }

  307. }