PoissonSeriesParser.java

  1. /* Copyright 2002-2025 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.data;

  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.Arrays;
  24. import java.util.HashMap;
  25. import java.util.Map;
  26. import java.util.regex.Matcher;
  27. import java.util.regex.Pattern;

  28. import org.hipparchus.exception.DummyLocalizable;
  29. import org.hipparchus.util.FastMath;
  30. import org.hipparchus.util.Precision;
  31. import org.orekit.errors.OrekitException;
  32. import org.orekit.errors.OrekitMessages;

  33. /**
  34.  * Parser for {@link PoissonSeries Poisson series} files.
  35.  * <p>
  36.  * A Poisson series is composed of a time polynomial part and a non-polynomial
  37.  * part which consist in summation series. The {@link SeriesTerm series terms}
  38.  * are harmonic functions (combination of sines and cosines) of polynomial
  39.  * <em>arguments</em>. The polynomial arguments are combinations of luni-solar or
  40.  * planetary {@link BodiesElements elements}.
  41.  * </p>
  42.  * <p>
  43.  * The Poisson series files from IERS have various formats, with or without
  44.  * polynomial part, with or without planetary components, with or without
  45.  * period column, with terms of increasing degrees either in dedicated columns
  46.  * or in successive sections of the file ... This class attempts to read all the
  47.  * commonly found formats, by specifying the columns of interest.
  48.  * </p>
  49.  * <p>
  50.  * The handling of increasing degrees terms (i.e. sin, cos, t sin, t cos, t^2 sin,
  51.  * t^2 cos ...) is done as follows.
  52.  * </p>
  53.  * <ul>
  54.  *   <li>user must specify pairs of columns to be extracted at each line,
  55.  *       in increasing degree order</li>
  56.  *   <li>negative columns indices correspond to inexistent values that will be
  57.  *       replaced by 0.0)</li>
  58.  *   <li>file may provide section headers to specify a degree, which is added
  59.  *       to the current column degree</li>
  60.  * </ul>
  61.  * <p>
  62.  * A file from an old convention, like table 5.1 in IERS conventions 1996, uses
  63.  * separate columns for degree 0 and degree 1, and uses only sine for nutation in
  64.  * longitude and cosine for nutation in obliquity. It reads as follows:
  65.  * </p>
  66.  * <pre>
  67.  * ∆ψ = Σ (Ai+A'it) sin(ARGUMENT), ∆ε = Σ (Bi+B'it) cos(ARGUMENT)
  68.  *
  69.  *      MULTIPLIERS OF      PERIOD           LONGITUDE         OBLIQUITY
  70.  *  l    l'   F    D   Om     days         Ai       A'i       Bi       B'i
  71.  *
  72.  *  0    0    0    0    1   -6798.4    -171996    -174.2    92025      8.9
  73.  *  0    0    2   -2    2     182.6     -13187      -1.6     5736     -3.1
  74.  *  0    0    2    0    2      13.7      -2274      -0.2      977     -0.5
  75.  *  0    0    0    0    2   -3399.2       2062       0.2     -895      0.5
  76.  * </pre>
  77.  * <p>
  78.  * In order to parse the nutation in longitude from the previous table, the
  79.  * following settings should be used:
  80.  * </p>
  81.  * <ul>
  82.  *   <li>totalColumns   = 10 (see {@link #PoissonSeriesParser(int)})</li>
  83.  *   <li>firstDelaunay  =  1 (see {@link #withFirstDelaunay(int)})</li>
  84.  *   <li>no calls to {@link #withFirstPlanetary(int)} as there are no planetary columns in this table</li>
  85.  *   <li>sinCosColumns  =  7, -1 for degree 0 for Ai (see {@link #withSinCos(int, int, double, int, double)})</li>
  86.  *   <li>sinCosColumns  =  8, -1 for degree 1 for A'i (see {@link #withSinCos(int, int, double, int, double)})</li>
  87.  * </ul>
  88.  * <p>
  89.  * In order to parse the nutation in obliquity from the previous table, the
  90.  * following settings should be used:
  91.  * </p>
  92.  * <ul>
  93.  *   <li>totalColumns   = 10 (see {@link #PoissonSeriesParser(int)})</li>
  94.  *   <li>firstDelaunay  =  1 (see {@link #withFirstDelaunay(int)})</li>
  95.  *   <li>no calls to {@link #withFirstPlanetary(int)} as there are no planetary columns in this table</li>
  96.  *   <li>sinCosColumns  =  -1, 9 for degree 0 for Bi (see {@link #withSinCos(int, int, double, int, double)})</li>
  97.  *   <li>sinCosColumns  =  -1, 10 for degree 1 for B'i (see {@link #withSinCos(int, int, double, int, double)})</li>
  98.  * </ul>
  99.  * <p>
  100.  * A file from a recent convention, like table 5.3a in IERS conventions 2010, uses
  101.  * only two columns for sin and cos, and separate degrees in successive sections with
  102.  * dedicated headers. It reads as follows:
  103.  * </p>
  104.  * <pre>
  105.  * ---------------------------------------------------------------------------------------------------
  106.  *
  107.  * (unit microarcsecond; cut-off: 0.1 microarcsecond)
  108.  * (ARG being for various combination of the fundamental arguments of the nutation theory)
  109.  *
  110.  *   Sum_i[A_i * sin(ARG) + A"_i * cos(ARG)]
  111.  *
  112.  * + Sum_i[A'_i * sin(ARG) + A"'_i * cos(ARG)] * t           (see Chapter 5, Eq. (35))
  113.  *
  114.  * The Table below provides the values for A_i and A"_i (j=0) and then A'_i and A"'_i (j=1)
  115.  *
  116.  * The expressions for the fundamental arguments appearing in columns 4 to 8 (luni-solar part)
  117.  * and in columns 9 to 17 (planetary part) are those of the IERS Conventions 2003
  118.  *
  119.  * ----------------------------------------------------------------------------------------------------------
  120.  * j = 0  Number of terms = 1320
  121.  * ----------------------------------------------------------------------------------------------------------
  122.  *     i        A_i             A"_i     l    l'   F    D    Om  L_Me L_Ve  L_E L_Ma  L_J L_Sa  L_U L_Ne  p_A
  123.  * ----------------------------------------------------------------------------------------------------------
  124.  *     1   -17206424.18        3338.60    0    0    0    0    1    0    0    0    0    0    0    0    0    0
  125.  *     2    -1317091.22       -1369.60    0    0    2   -2    2    0    0    0    0    0    0    0    0    0
  126.  *     3     -227641.81         279.60    0    0    2    0    2    0    0    0    0    0    0    0    0    0
  127.  *     4      207455.40         -69.80    0    0    0    0    2    0    0    0    0    0    0    0    0    0
  128.  *     5      147587.70        1181.70    0    1    0    0    0    0    0    0    0    0    0    0    0    0
  129.  *
  130.  * ...
  131.  *
  132.  *  1319          -0.10           0.00    0    0    0    0    0    1    0   -3    0    0    0    0    0   -2
  133.  *  1320          -0.10           0.00    0    0    0    0    0    0    0    1    0    1   -2    0    0    0
  134.  *
  135.  * --------------------------------------------------------------------------------------------------------------
  136.  * j = 1  Number of terms = 38
  137.  * --------------------------------------------------------------------------------------------------------------
  138.  *    i          A'_i            A"'_i    l    l'   F    D   Om L_Me L_Ve  L_E L_Ma  L_J L_Sa  L_U L_Ne  p_A
  139.  * --------------------------------------------------------------------------------------------------------------
  140.  *  1321      -17418.82           2.89    0    0    0    0    1    0    0    0    0    0    0    0    0    0
  141.  *  1322        -363.71          -1.50    0    1    0    0    0    0    0    0    0    0    0    0    0    0
  142.  *  1323        -163.84           1.20    0    0    2   -2    2    0    0    0    0    0    0    0    0    0
  143.  *  1324         122.74           0.20    0    1    2   -2    2    0    0    0    0    0    0    0    0    0
  144.  * </pre>
  145.  * <p>
  146.  * In order to parse the nutation in longitude from the previous table, the
  147.  * following settings should be used:
  148.  * </p>
  149.  * <ul>
  150.  *   <li>totalColumns   = 17 (see {@link #PoissonSeriesParser(int)})</li>
  151.  *   <li>firstDelaunay  =  4 (see {@link #withFirstDelaunay(int)})</li>
  152.  *   <li>firstPlanetary =  9 (see {@link #withFirstPlanetary(int)})</li>
  153.  *   <li>sinCosColumns  =  2,3 (we specify only degree 0, so when we read
  154.  *       section j = 0 we read degree 0, when we read section j = 1 we read
  155.  *       degree 1, see {@link #withSinCos(int, int, double, int, double)} ...)</li>
  156.  * </ul>
  157.  * <p>
  158.  * A file from a recent convention, like table 6.5a in IERS conventions 2010, contains
  159.  * both Doodson arguments (τ, s, h, p, N', ps), Doodson numbers and Delaunay parameters.
  160.  * In this case, the coefficients for the Delaunay parameters must be <em>subtracted</em>
  161.  * from the τ = GMST + π tide parameter, so the signs in the files must be reversed
  162.  * in order to match the Doodson arguments and Doodson numbers. This is done automatically
  163.  * (and consistency is checked) only when the {@link #withDoodson(int, int)} method is
  164.  * called at parser configuration time. Some other files use the γ = GMST + π tide parameter
  165.  * rather than Doodson τ argument and the coefficients for the Delaunay parameters must be
  166.  * <em>added</em> to the γ parameter, so no sign reversal is performed. In order to avoid
  167.  * ambiguity as the two cases are incompatible with each other, trying to add a configuration
  168.  * for τ by calling {@link #withDoodson(int, int)} and to also add a configuration for γ by
  169.  * calling {@link #withGamma(int)} triggers an exception.
  170.  * </p>
  171.  * <p>The table 6.5a file also contains a column for the waves names (the Darwin's symbol)
  172.  * which may be empty, so it must be identified explicitly by calling {@link
  173.  * #withOptionalColumn(int)}. The 6.5a table reads as follows:
  174.  * </p>
  175.  * <pre>
  176.  * The in-phase (ip) amplitudes (A₁ δkfR Hf) and the out-of-phase (op) amplitudes (A₁ δkfI Hf)
  177.  * of the corrections for frequency dependence of k₂₁⁽⁰⁾, taking the nominal value k₂₁ for the
  178.  * diurnal tides as (0.29830 − i 0.00144). Units: 10⁻¹² . The entries for δkfR and δkfI are in
  179.  * units of 10⁻⁵. Multipliers of the Doodson arguments identifying the tidal terms are given,
  180.  * as also those of the Delaunay variables characterizing the nutations produced by these
  181.  * terms.
  182.  *
  183.  * Name   deg/hr    Doodson  τ  s  h  p  N' ps   l  l' F  D  Ω  δkfR  δkfI     Amp.    Amp.
  184.  *                    No.                                       /10−5 /10−5    (ip)    (op)
  185.  *   2Q₁ 12.85429   125,755  1 -3  0  2   0  0   2  0  2  0  2    -29     3    -0.1     0.0
  186.  *    σ₁ 12.92714   127,555  1 -3  2  0   0  0   0  0  2  2  2    -30     3    -0.1     0.0
  187.  *       13.39645   135,645  1 -2  0  1  -1  0   1  0  2  0  1    -45     5    -0.1     0.0
  188.  *    Q₁ 13.39866   135,655  1 -2  0  1   0  0   1  0  2  0  2    -46     5    -0.7     0.1
  189.  *    ρ₁ 13.47151   137,455  1 -2  2 -1   0  0  -1  0  2  2  2    -49     5    -0.1     0.0
  190.  * </pre>
  191.  * <ul>
  192.  *   <li>totalColumns   = 18 (see {@link #PoissonSeriesParser(int)})</li>
  193.  *   <li>optionalColumn =  1 (see {@link #withOptionalColumn(int)})</li>
  194.  *   <li>firstDoodson, Doodson number = 4, 3 (see {@link #withDoodson(int, int)})</li>
  195.  *   <li>firstDelaunay  =  10 (see {@link #withFirstDelaunay(int)})</li>
  196.  *   <li>sinCosColumns  =  17, 18, see {@link #withSinCos(int, int, double, int, double)} ...)</li>
  197.  * </ul>
  198.  * <p>
  199.  * Our parsing algorithm involves adding the section degree from the "j = 0, 1, 2 ..." header
  200.  * to the column degree. A side effect of this algorithm is that it is theoretically possible
  201.  * to mix both formats and have for example degree two term appear as degree 2 column in section
  202.  * j=0 and as degree 1 column in section j=1 and as degree 0 column in section j=2. This case
  203.  * is not expected to be encountered in practice. The real files use either several columns
  204.  * <em>or</em> several sections, but not both at the same time.
  205.  * </p>
  206.  *
  207.  * @author Luc Maisonobe
  208.  * @see SeriesTerm
  209.  * @see PolynomialNutation
  210.  * @since 6.1
  211.  */
  212. public class PoissonSeriesParser {

  213.     /** Default pattern for fields with unknown type (non-space characters). */
  214.     private static final String  UNKNOWN_TYPE_PATTERN = "\\S+";

  215.     /** Pattern for optional fields (either nothing or non-space characters). */
  216.     private static final String  OPTIONAL_FIELD_PATTERN = "\\S*";

  217.     /** Pattern for fields with integer type. */
  218.     private static final String  INTEGER_TYPE_PATTERN = "[-+]?\\p{Digit}+";

  219.     /** Pattern for fields with real type. */
  220.     private static final String  REAL_TYPE_PATTERN = "[-+]?(?:(?:\\p{Digit}+(?:\\.\\p{Digit}*)?)|(?:\\.\\p{Digit}+))(?:[eE][-+]?\\p{Digit}+)?";

  221.     /** Pattern for fields with Doodson number. */
  222.     private static final String  DOODSON_TYPE_PATTERN = "\\p{Digit}{2,3}[.,]\\p{Digit}{3}";

  223.     /** Pattern for String replacement. */
  224.     private static final Pattern PATTERN = Pattern.compile("[.,]");

  225.     /** Parser for the polynomial part. */
  226.     private final PolynomialParser polynomialParser;

  227.     /** Fields patterns. */
  228.     private final String[] fieldsPatterns;

  229.     /** Optional column (counting from 1). */
  230.     private final int optional;

  231.     /** Column of the γ = GMST + π tide multiplier (counting from 1). */
  232.     private final int gamma;

  233.     /** Column of the first Doodson multiplier (counting from 1). */
  234.     private final int firstDoodson;

  235.     /** Column of the Doodson number (counting from 1). */
  236.     private final int doodson;

  237.     /** Column of the first Delaunay multiplier (counting from 1). */
  238.     private final int firstDelaunay;

  239.     /** Column of the first planetary multiplier (counting from 1). */
  240.     private final int firstPlanetary;

  241.     /** columns of the sine and cosine coefficients for successive degrees.
  242.      * <p>
  243.      * The ordering is: sin, cos, t sin, t cos, t^2 sin, t^2 cos ...
  244.      * </p>
  245.      */
  246.     private final int[] sinCosColumns;

  247.     /** Multiplicative factors to use for various columns. */
  248.     private final double[] sinCosFactors;

  249.     /** Build a parser for a Poisson series from an IERS table file.
  250.      * @param polynomialParser polynomial parser to use
  251.      * @param fieldsPatterns patterns for fields
  252.      * @param optional optional column
  253.      * @param gamma column of the GMST tide multiplier
  254.      * @param firstDoodson column of the first Doodson multiplier
  255.      * @param doodson column of the Doodson number
  256.      * @param firstDelaunay column of the first Delaunay multiplier
  257.      * @param firstPlanetary column of the first planetary multiplier
  258.      * @param sinCosColumns columns of the sine and cosine coefficients
  259.      * @param factors multiplicative factors to use for various columns
  260.      */
  261.     private PoissonSeriesParser(final PolynomialParser polynomialParser,
  262.                                 final String[] fieldsPatterns, final int optional,
  263.                                 final int gamma, final int firstDoodson,
  264.                                 final int doodson, final int firstDelaunay,
  265.                                 final int firstPlanetary, final int[] sinCosColumns,
  266.                                 final double[] factors) {
  267.         this.polynomialParser = polynomialParser;
  268.         this.fieldsPatterns   = fieldsPatterns;
  269.         this.optional         = optional;
  270.         this.gamma            = gamma;
  271.         this.firstDoodson     = firstDoodson;
  272.         this.doodson          = doodson;
  273.         this.firstDelaunay    = firstDelaunay;
  274.         this.firstPlanetary   = firstPlanetary;
  275.         this.sinCosColumns    = sinCosColumns;
  276.         this.sinCosFactors    = factors;
  277.     }

  278.     /** Build a parser for a Poisson series from an IERS table file.
  279.      * @param totalColumns total number of columns in the non-polynomial sections
  280.      */
  281.     public PoissonSeriesParser(final int totalColumns) {
  282.         this(null, createInitialFieldsPattern(totalColumns), -1,
  283.              -1, -1, -1, -1, -1, new int[0], new double[0]);
  284.     }

  285.     /** Create an array with only non-space fields patterns.
  286.      * @param totalColumns total number of columns
  287.      * @return a new fields pattern array
  288.      */
  289.     private static String[] createInitialFieldsPattern(final int totalColumns) {
  290.         final String[] patterns = new String[totalColumns];
  291.         setPatterns(patterns, 1, totalColumns, UNKNOWN_TYPE_PATTERN);
  292.         return patterns;
  293.     }

  294.     /** Set fields patterns.
  295.      * @param array fields pattern array to modify
  296.      * @param first first column to set (counting from 1), do nothing if non-positive
  297.      * @param count number of columns to set
  298.      * @param pattern pattern to use
  299.      */
  300.     private static void setPatterns(final String[] array, final int first, final int count,
  301.                                     final String pattern) {
  302.         if (first > 0) {
  303.             Arrays.fill(array, first - 1, first - 1 + count, pattern);
  304.         }
  305.     }

  306.     /** Set up polynomial part parsing.
  307.      * @param freeVariable name of the free variable in the polynomial part
  308.      * @param unit default unit for polynomial, if not explicit within the file
  309.      * @return a new parser, with polynomial parser updated
  310.      */
  311.     public PoissonSeriesParser withPolynomialPart(final char freeVariable, final PolynomialParser.Unit unit) {
  312.         return new PoissonSeriesParser(new PolynomialParser(freeVariable, unit), fieldsPatterns, optional,
  313.                                        gamma, firstDoodson, doodson, firstDelaunay,
  314.                                        firstPlanetary, sinCosColumns, sinCosFactors);
  315.     }

  316.     /** Set up optional column.
  317.      * <p>
  318.      * Optional columns typically appears in tides-related files, as some waves have
  319.      * specific names (χ₁, M₂, ...) and other waves don't have names and hence are
  320.      * replaced by spaces in the corresponding file line.
  321.      * </p>
  322.      * <p>
  323.      * At most one column may be optional.
  324.      * </p>
  325.      * @param column optional column (counting from 1)
  326.      * @return a new parser, with updated columns settings
  327.      */
  328.     public PoissonSeriesParser withOptionalColumn(final int column) {

  329.         // update the fields pattern to expect 1 optional field at the right index
  330.         final String[] newFieldsPatterns = fieldsPatterns.clone();
  331.         setPatterns(newFieldsPatterns, optional, 1, UNKNOWN_TYPE_PATTERN);
  332.         setPatterns(newFieldsPatterns, column,   1, OPTIONAL_FIELD_PATTERN);

  333.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, column,
  334.                                        gamma, firstDoodson, doodson, firstDelaunay,
  335.                                        firstPlanetary, sinCosColumns, sinCosFactors);

  336.     }

  337.     /** Set up column of GMST tide multiplier.
  338.      * @param column column of the GMST tide multiplier (counting from 1)
  339.      * @return a new parser, with updated columns settings
  340.      * @see #withDoodson(int, int)
  341.      */
  342.     public PoissonSeriesParser withGamma(final int column) {

  343.         // check we don't try to have both τ and γ configured at the same time
  344.         if (firstDoodson > 0 && column > 0) {
  345.             throw new OrekitException(OrekitMessages.CANNOT_PARSE_BOTH_TAU_AND_GAMMA);
  346.         }

  347.         // update the fields pattern to expect 1 integer at the right index
  348.         final String[] newFieldsPatterns = fieldsPatterns.clone();
  349.         setPatterns(newFieldsPatterns, gamma,  1, UNKNOWN_TYPE_PATTERN);
  350.         setPatterns(newFieldsPatterns, column, 1, INTEGER_TYPE_PATTERN);

  351.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, optional,
  352.                                        column, firstDoodson, doodson, firstDelaunay,
  353.                                        firstPlanetary, sinCosColumns, sinCosFactors);

  354.     }

  355.     /** Set up columns for Doodson multipliers and Doodson number.
  356.      * @param firstMultiplierColumn column of the first Doodson multiplier which
  357.      * corresponds to τ (counting from 1)
  358.      * @param numberColumn column of the Doodson number (counting from 1)
  359.      * @return a new parser, with updated columns settings
  360.      * @see #withGamma(int)
  361.      * @see #withFirstDelaunay(int)
  362.      */
  363.     public PoissonSeriesParser withDoodson(final int firstMultiplierColumn, final int numberColumn) {

  364.         // check we don't try to have both τ and γ configured at the same time
  365.         if (gamma > 0 && firstMultiplierColumn > 0) {
  366.             throw new OrekitException(OrekitMessages.CANNOT_PARSE_BOTH_TAU_AND_GAMMA);
  367.         }

  368.         final String[] newFieldsPatterns = fieldsPatterns.clone();

  369.         // update the fields pattern to expect 6 integers at the right indices
  370.         setPatterns(newFieldsPatterns, firstDoodson,          6, UNKNOWN_TYPE_PATTERN);
  371.         setPatterns(newFieldsPatterns, firstMultiplierColumn, 6, INTEGER_TYPE_PATTERN);

  372.         // update the fields pattern to expect 1 Doodson number at the right index
  373.         setPatterns(newFieldsPatterns, doodson,      1, UNKNOWN_TYPE_PATTERN);
  374.         setPatterns(newFieldsPatterns, numberColumn, 1, DOODSON_TYPE_PATTERN);

  375.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, optional,
  376.                                        gamma, firstMultiplierColumn, numberColumn, firstDelaunay,
  377.                                        firstPlanetary, sinCosColumns, sinCosFactors);

  378.     }

  379.     /** Set up first column of Delaunay multiplier.
  380.      * @param firstColumn column of the first Delaunay multiplier (counting from 1)
  381.      * @return a new parser, with updated columns settings
  382.      */
  383.     public PoissonSeriesParser withFirstDelaunay(final int firstColumn) {

  384.         // update the fields pattern to expect 5 integers at the right indices
  385.         final String[] newFieldsPatterns = fieldsPatterns.clone();
  386.         setPatterns(newFieldsPatterns, firstDelaunay, 5, UNKNOWN_TYPE_PATTERN);
  387.         setPatterns(newFieldsPatterns, firstColumn,   5, INTEGER_TYPE_PATTERN);

  388.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, optional,
  389.                                        gamma, firstDoodson, doodson, firstColumn,
  390.                                        firstPlanetary, sinCosColumns, sinCosFactors);

  391.     }

  392.     /** Set up first column of planetary multiplier.
  393.      * @param firstColumn column of the first planetary multiplier (counting from 1)
  394.      * @return a new parser, with updated columns settings
  395.      */
  396.     public PoissonSeriesParser withFirstPlanetary(final int firstColumn) {

  397.         // update the fields pattern to expect 9 integers at the right indices
  398.         final String[] newFieldsPatterns = fieldsPatterns.clone();
  399.         setPatterns(newFieldsPatterns, firstPlanetary, 9, UNKNOWN_TYPE_PATTERN);
  400.         setPatterns(newFieldsPatterns, firstColumn,    9, INTEGER_TYPE_PATTERN);

  401.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, optional,
  402.                                        gamma, firstDoodson, doodson, firstDelaunay,
  403.                                        firstColumn, sinCosColumns, sinCosFactors);

  404.     }

  405.     /** Set up columns of the sine and cosine coefficients.
  406.      * @param degree degree to set up
  407.      * @param sinColumn column of the sine coefficient for t<sup>degree</sup> counting from 1
  408.      * (may be -1 if there are no sine coefficients)
  409.      * @param sinFactor multiplicative factor for the sine coefficient
  410.      * @param cosColumn column of the cosine coefficient for t<sup>degree</sup> counting from 1
  411.      * (may be -1 if there are no cosine coefficients)
  412.      * @param cosFactor multiplicative factor for the cosine coefficient
  413.      * @return a new parser, with updated columns settings
  414.      */
  415.     public PoissonSeriesParser withSinCos(final int degree,
  416.                                           final int sinColumn, final double sinFactor,
  417.                                           final int cosColumn, final double cosFactor) {

  418.         // update the sin/cos columns array
  419.         final int      maxDegree        = FastMath.max(degree, sinCosColumns.length / 2 - 1);
  420.         final int[]    newSinCosColumns = new int[2 * (maxDegree + 1)];
  421.         Arrays.fill(newSinCosColumns, -1);
  422.         System.arraycopy(sinCosColumns, 0, newSinCosColumns, 0, sinCosColumns.length);
  423.         newSinCosColumns[2 * degree]     = sinColumn;
  424.         newSinCosColumns[2 * degree + 1] = cosColumn;

  425.         final double[] newSinCosFactors = new double[2 * (maxDegree + 1)];
  426.         Arrays.fill(newSinCosFactors, Double.NaN);
  427.         System.arraycopy(sinCosFactors, 0, newSinCosFactors, 0, sinCosFactors.length);
  428.         newSinCosFactors[2 * degree]     = sinFactor;
  429.         newSinCosFactors[2 * degree + 1] = cosFactor;

  430.         // update the fields pattern to expect real numbers at the right indices
  431.         final String[] newFieldsPatterns = fieldsPatterns.clone();
  432.         if (2 * degree < sinCosColumns.length) {
  433.             setPatterns(newFieldsPatterns, sinCosColumns[2 * degree], 1, UNKNOWN_TYPE_PATTERN);
  434.         }
  435.         setPatterns(newFieldsPatterns, sinColumn, 1, REAL_TYPE_PATTERN);
  436.         if (2 * degree  + 1 < sinCosColumns.length) {
  437.             setPatterns(newFieldsPatterns, sinCosColumns[2 * degree + 1], 1, UNKNOWN_TYPE_PATTERN);
  438.         }
  439.         setPatterns(newFieldsPatterns, cosColumn, 1, REAL_TYPE_PATTERN);

  440.         return new PoissonSeriesParser(polynomialParser, newFieldsPatterns, optional,
  441.                                        gamma, firstDoodson, doodson, firstDelaunay,
  442.                                        firstPlanetary, newSinCosColumns, newSinCosFactors);

  443.     }

  444.     /** Parse a stream.
  445.      * @param stream stream containing the IERS table
  446.      * @param name name of the resource file (for error messages only)
  447.      * @return parsed Poisson series
  448.      */
  449.     public PoissonSeries parse(final InputStream stream, final String name) {

  450.         if (stream == null) {
  451.             throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, name);
  452.         }

  453.         // the degrees section header should read something like:
  454.         // j = 0  Nb of terms = 1306
  455.         // or something like:
  456.         // j = 0  Number  of terms = 1037
  457.         final Pattern degreeSectionHeaderPattern =
  458.             Pattern.compile("^\\p{Space}*j\\p{Space}*=\\p{Space}*(\\p{Digit}+)" +
  459.                             "[\\p{Alpha}\\p{Space}]+=\\p{Space}*(\\p{Digit}+)\\p{Space}*$");

  460.         // regular lines are simply a space separated list of numbers
  461.         final StringBuilder builder = new StringBuilder("^\\p{Space}*");
  462.         for (int i = 0; i < fieldsPatterns.length; ++i) {
  463.             builder.append("(");
  464.             builder.append(fieldsPatterns[i]);
  465.             builder.append(")");
  466.             builder.append((i < fieldsPatterns.length - 1) ? "\\p{Space}+" : "\\p{Space}*$");
  467.         }
  468.         final Pattern regularLinePattern = Pattern.compile(builder.toString());

  469.         // setup the reader
  470.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {

  471.             int lineNumber    =  0;
  472.             int expectedIndex = -1;
  473.             int nTerms        = -1;
  474.             int count         =  0;
  475.             int degree        =  0;

  476.             // prepare the container for the parsed data
  477.             PolynomialNutation polynomial;
  478.             if (polynomialParser == null) {
  479.                 // we don't expect any polynomial, we directly set the zero polynomial
  480.                 polynomial = new PolynomialNutation(new double[0]);
  481.             } else {
  482.                 // the dedicated parser will fill in the polynomial later
  483.                 polynomial = null;
  484.             }
  485.             final Map<Long, SeriesTerm> series = new HashMap<>();

  486.             for (String line = reader.readLine(); line != null; line = reader.readLine()) {

  487.                 // replace unicode minus sign ('−') by regular hyphen ('-') for parsing
  488.                 // such unicode characters occur in tables that are copy-pasted from PDF files
  489.                 line = line.replace('\u2212', '-');
  490.                 ++lineNumber;

  491.                 final Matcher regularMatcher = regularLinePattern.matcher(line);
  492.                 if (regularMatcher.matches()) {
  493.                     // we have found a regular data line

  494.                     if (expectedIndex > 0) {
  495.                         // we are in a file were terms are numbered, we check the index
  496.                         if (Integer.parseInt(regularMatcher.group(1)) != expectedIndex) {
  497.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  498.                                                       lineNumber, name, regularMatcher.group());
  499.                         }
  500.                     }

  501.                     // get the Doodson multipliers as well as the Doodson number
  502.                     final int cTau     = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson));
  503.                     final int cS       = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson + 1));
  504.                     final int cH       = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson + 2));
  505.                     final int cP       = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson + 3));
  506.                     final int cNprime  = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson + 4));
  507.                     final int cPs      = (firstDoodson < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstDoodson + 5));
  508.                     final int nDoodson = (doodson      < 0) ? 0 : Integer.parseInt(PATTERN.matcher(regularMatcher.group(doodson)).replaceAll(""));

  509.                     // get the tide multiplier
  510.                     int cGamma   = (gamma < 0) ? 0 : Integer.parseInt(regularMatcher.group(gamma));

  511.                     // get the Delaunay multipliers
  512.                     int cL       = Integer.parseInt(regularMatcher.group(firstDelaunay));
  513.                     int cLPrime  = Integer.parseInt(regularMatcher.group(firstDelaunay + 1));
  514.                     int cF       = Integer.parseInt(regularMatcher.group(firstDelaunay + 2));
  515.                     int cD       = Integer.parseInt(regularMatcher.group(firstDelaunay + 3));
  516.                     int cOmega   = Integer.parseInt(regularMatcher.group(firstDelaunay + 4));

  517.                     // get the planetary multipliers
  518.                     final int cMe      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary));
  519.                     final int cVe      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 1));
  520.                     final int cE       = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 2));
  521.                     final int cMa      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 3));
  522.                     final int cJu      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 4));
  523.                     final int cSa      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 5));
  524.                     final int cUr      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 6));
  525.                     final int cNe      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 7));
  526.                     final int cPa      = (firstPlanetary < 0) ? 0 : Integer.parseInt(regularMatcher.group(firstPlanetary + 8));

  527.                     if (nDoodson > 0) {

  528.                         // set up the traditional parameters corresponding to the Doodson arguments
  529.                         cGamma  = cTau;
  530.                         cL      = -cL;
  531.                         cLPrime = -cLPrime;
  532.                         cF      = -cF;
  533.                         cD      = -cD;
  534.                         cOmega  = -cOmega;

  535.                         // check Doodson number, Doodson multipliers and Delaunay multipliers consistency
  536.                         if (nDoodson != doodsonToDoodsonNumber(cTau, cS, cH, cP, cNprime, cPs) ||
  537.                             nDoodson != delaunayToDoodsonNumber(cGamma, cL, cLPrime, cF, cD, cOmega)) {
  538.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  539.                                                       lineNumber, name, regularMatcher.group());
  540.                         }

  541.                     }

  542.                     final long key = NutationCodec.encode(cGamma, cL, cLPrime, cF, cD, cOmega,
  543.                                                           cMe, cVe, cE, cMa, cJu, cSa, cUr, cNe, cPa);

  544.                     // retrieved the term, or build it if it's the first time it is encountered in the file
  545.                     final SeriesTerm term;
  546.                     if (series.containsKey(key)) {
  547.                         // the term was already known, from another degree
  548.                         term = series.get(key);
  549.                     } else {
  550.                         // the term is a new one
  551.                         term = SeriesTerm.buildTerm(cGamma, cL, cLPrime, cF, cD, cOmega,
  552.                                                     cMe, cVe, cE, cMa, cJu, cSa, cUr, cNe, cPa);
  553.                     }

  554.                     boolean nonZero = false;
  555.                     for (int d = 0; d < sinCosColumns.length / 2; ++d) {
  556.                         final double sinCoeff =
  557.                                 parseCoefficient(regularMatcher, sinCosColumns[2 * d],     sinCosFactors[2 * d]);
  558.                         final double cosCoeff =
  559.                                 parseCoefficient(regularMatcher, sinCosColumns[2 * d + 1], sinCosFactors[2 * d + 1]);
  560.                         if (!Precision.equals(sinCoeff, 0.0, 0) || !Precision.equals(cosCoeff, 0.0, 0)) {
  561.                             nonZero = true;
  562.                             term.add(0, degree + d, sinCoeff, cosCoeff);
  563.                             ++count;
  564.                         }
  565.                     }
  566.                     if (nonZero) {
  567.                         series.put(key, term);
  568.                     }

  569.                     if (expectedIndex > 0) {
  570.                         // we are in a file were terms are numbered
  571.                         // we must update the expected value for next term
  572.                         ++expectedIndex;
  573.                     }

  574.                 } else {

  575.                     final Matcher headerMatcher = degreeSectionHeaderPattern.matcher(line);
  576.                     if (headerMatcher.matches()) {

  577.                         // we have found a degree section header
  578.                         final int nextDegree = Integer.parseInt(headerMatcher.group(1));
  579.                         if (nextDegree != degree + 1 && (degree != 0 || nextDegree != 0)) {
  580.                             throw new OrekitException(OrekitMessages.MISSING_SERIE_J_IN_FILE,
  581.                                                       degree + 1, name, lineNumber);
  582.                         }

  583.                         if (nextDegree == 0) {
  584.                             // in IERS files split in sections, all terms are numbered
  585.                             // we can check the indices
  586.                             expectedIndex = 1;
  587.                         }

  588.                         if (nextDegree > 0 && count != nTerms) {
  589.                             // the previous degree does not have the expected number of terms
  590.                             throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  591.                         }

  592.                         // remember the number of terms the upcoming sublist should have
  593.                         nTerms =  Integer.parseInt(headerMatcher.group(2));
  594.                         count  = 0;
  595.                         degree = nextDegree;

  596.                     } else if (polynomial == null) {
  597.                         // look for the polynomial part
  598.                         final double[] coefficients = polynomialParser.parse(line);
  599.                         if (coefficients != null) {
  600.                             polynomial = new PolynomialNutation(coefficients);
  601.                         }
  602.                     }

  603.                 }

  604.             }

  605.             if (polynomial == null || series.isEmpty()) {
  606.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  607.             }

  608.             if (nTerms > 0 && count != nTerms) {
  609.                 // the last degree does not have the expected number of terms
  610.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_IERS_DATA_FILE, name);
  611.             }

  612.             // build the series
  613.             return new PoissonSeries(polynomial, series);

  614.         } catch (IOException ioe) {
  615.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  616.         }

  617.     }

  618.     /** Parse a scaled coefficient.
  619.      * @param matcher line matcher holding the coefficient
  620.      * @param group group number of the coefficient, or -1 if line does not contain coefficient
  621.      * @param scale scaling factor to apply
  622.      * @return scaled factor, or 0.0 if group is -1
  623.      */
  624.     private double parseCoefficient(final Matcher matcher, final int group, final double scale) {
  625.         if (group < 0) {
  626.             return 0.0;
  627.         } else {
  628.             return scale * Double.parseDouble(matcher.group(group));
  629.         }
  630.     }

  631.     /** Compute Doodson number from Delaunay multipliers.
  632.      * @param cGamma coefficient for γ = GMST + π tide parameter
  633.      * @param cL coefficient for mean anomaly of the Moon
  634.      * @param cLPrime coefficient for mean anomaly of the Sun
  635.      * @param cF coefficient for L - Ω where L is the mean longitude of the Moon
  636.      * @param cD coefficient for mean elongation of the Moon from the Sun
  637.      * @param cOmega coefficient for mean longitude of the ascending node of the Moon
  638.      * @return computed Doodson number
  639.      */
  640.     private int delaunayToDoodsonNumber(final int cGamma,
  641.                                         final int cL, final int cLPrime, final int cF,
  642.                                         final int cD, final int cOmega) {

  643.         // reconstruct Doodson multipliers from gamma and Delaunay multipliers
  644.         final int cTau    = cGamma;
  645.         final int cS      = cGamma + (cL + cF + cD);
  646.         final int cH      = cLPrime - cD;
  647.         final int cP      = -cL;
  648.         final int cNprime = cF - cOmega;
  649.         final int cPs     = -cLPrime;

  650.         return doodsonToDoodsonNumber(cTau, cS, cH, cP, cNprime, cPs);

  651.     }

  652.     /** Compute Doodson number from Doodson multipliers.
  653.      * @param cTau coefficient for mean lunar time
  654.      * @param cS coefficient for mean longitude of the Moon
  655.      * @param cH coefficient for mean longitude of the Sun
  656.      * @param cP coefficient for longitude of Moon mean perigee
  657.      * @param cNprime negative of the longitude of the Moon's mean ascending node on the ecliptic
  658.      * @param cPs coefficient for longitude of Sun mean perigee
  659.      * @return computed Doodson number
  660.      */
  661.     private int doodsonToDoodsonNumber(final int cTau,
  662.                                        final int cS, final int cH, final int cP,
  663.                                        final int cNprime, final int cPs) {

  664.         return ((((cTau * 10 + (cS + 5)) * 10 + (cH + 5)) * 10 + (cP + 5)) * 10 + (cNprime + 5)) * 10 + (cPs + 5);

  665.     }

  666. }