ICGEMFormatReader.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.forces.gravity.potential;

  18. import org.hipparchus.util.FastMath;
  19. import org.hipparchus.util.MathUtils;
  20. import org.hipparchus.util.Precision;
  21. import org.orekit.annotation.DefaultDataContext;
  22. import org.orekit.data.DataContext;
  23. import org.orekit.errors.OrekitException;
  24. import org.orekit.errors.OrekitMessages;
  25. import org.orekit.time.AbsoluteDate;
  26. import org.orekit.time.DateComponents;
  27. import org.orekit.time.TimeComponents;
  28. import org.orekit.time.TimeScale;
  29. import org.orekit.utils.Constants;
  30. import org.orekit.utils.TimeSpanMap;

  31. import java.io.BufferedReader;
  32. import java.io.IOException;
  33. import java.io.InputStream;
  34. import java.io.InputStreamReader;
  35. import java.nio.charset.StandardCharsets;
  36. import java.text.ParseException;
  37. import java.util.ArrayList;
  38. import java.util.List;
  39. import java.util.Locale;
  40. import java.util.regex.Matcher;
  41. import java.util.regex.Pattern;

  42. /** Reader for the ICGEM gravity field format.
  43.  *
  44.  * <p>This format is used to describe the gravity field of EIGEN models
  45.  * published by the GFZ Potsdam since 2004. It is described in Franz
  46.  * Barthelmes and Christoph F&ouml;rste paper: "the ICGEM-format".
  47.  * The 2006-02-28 version of this paper can be found <a
  48.  * href="http://op.gfz-potsdam.de/grace/results/grav/g005_ICGEM-Format.pdf">here</a>
  49.  * and the 2011-06-07 version of this paper can be found <a
  50.  * href="http://icgem.gfz-potsdam.de/ICGEM-Format-2011.pdf">here</a>.
  51.  * These versions differ in time-dependent coefficients, which are linear-only prior
  52.  * to 2011 (up to eigen-5 model) and have also harmonic effects after that date
  53.  * (starting with eigen-6 model). A third (undocumented as of 2018-05-14) version
  54.  * of the file format also adds a time-span for time-dependent coefficients, allowing
  55.  * for piecewise models. All three versions are supported by the class.</p>
  56.  * <p>
  57.  * This reader uses relaxed check on the gravity constant key so any key ending
  58.  * in gravity_constant is accepted and not only earth_gravity_constant as specified
  59.  * in the previous documents. This allows to read also non Earth gravity fields
  60.  * as found in <a href="http://icgem.gfz-potsdam.de/tom_celestial">ICGEM
  61.  * - Gravity Field Models of other Celestial Bodies</a> page to be read.
  62.  * </p>
  63.  *
  64.  * <p> The proper way to use this class is to call the {@link GravityFieldFactory}
  65.  *  which will determine which reader to use with the selected gravity field file.</p>
  66.  *
  67.  * @see GravityFields
  68.  * @author Luc Maisonobe
  69.  */
  70. public class ICGEMFormatReader extends PotentialCoefficientsReader {

  71.     /** Format. */
  72.     private static final String FORMAT                  = "format";

  73.     /** Maximum supported formats. */
  74.     private static final double MAX_FORMAT              = 2.0;

  75.     /** Product type. */
  76.     private static final String PRODUCT_TYPE            = "product_type";

  77.     /** Gravity field product type. */
  78.     private static final String GRAVITY_FIELD           = "gravity_field";

  79.     /** Gravity constant marker. */
  80.     private static final String GRAVITY_CONSTANT        = "gravity_constant";

  81.     /** Reference radius. */
  82.     private static final String REFERENCE_RADIUS        = "radius";

  83.     /** Max degree. */
  84.     private static final String MAX_DEGREE              = "max_degree";

  85.     /** Errors indicator. */
  86.     private static final String ERRORS                  = "errors";

  87.     /** Tide system indicator. */
  88.     private static final String TIDE_SYSTEM_INDICATOR   = "tide_system";

  89.     /** Indicator value for zero-tide system. */
  90.     private static final String ZERO_TIDE               = "zero_tide";

  91.     /** Indicator value for tide-free system. */
  92.     private static final String TIDE_FREE               = "tide_free";

  93.     /** Indicator value for unknown tide system. */
  94.     private static final String TIDE_UNKNOWN            = "unknown";

  95.     /** Normalization indicator. */
  96.     private static final String NORMALIZATION_INDICATOR = "norm";

  97.     /** Indicator value for normalized coefficients. */
  98.     private static final String NORMALIZED              = "fully_normalized";

  99.     /** Indicator value for un-normalized coefficients. */
  100.     private static final String UNNORMALIZED            = "unnormalized";

  101.     /** End of header marker. */
  102.     private static final String END_OF_HEADER           = "end_of_head";

  103.     /** Gravity field coefficient. */
  104.     private static final String GFC                     = "gfc";

  105.     /** Time stamped gravity field coefficient. */
  106.     private static final String GFCT                    = "gfct";

  107.     /** Gravity field coefficient first time derivative. */
  108.     private static final String DOT                     = "dot";

  109.     /** Gravity field coefficient trend. */
  110.     private static final String TRND                    = "trnd";

  111.     /** Gravity field coefficient sine amplitude. */
  112.     private static final String ASIN                    = "asin";

  113.     /** Gravity field coefficient cosine amplitude. */
  114.     private static final String ACOS                    = "acos";

  115.     /** Name of base coefficients. */
  116.     private static final String BASE_NAMES              = "C/S";

  117.     /** Pattern for delimiting regular expressions. */
  118.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  119.     /** Pattern for supported formats. */
  120.     private static final Pattern SUPPORTED_FORMAT = Pattern.compile("icgem(\\d+\\.\\d+)");

  121.     /** Flag for Gravitational coefficient. */
  122.     private static final int MU = 0x1;

  123.     /** Flag for scaling radius. */
  124.     private static final int AE = 0x2;

  125.     /** Flag for degree/order. */
  126.     private static final int LIMITS = 0x4;

  127.     /** Flag for errors. */
  128.     private static final int ERR = 0x8;

  129.     /** Flag for coefficients. */
  130.     private static final int COEFFS = 0x10;

  131.     /** Indicator for normalized coefficients. */
  132.     private boolean normalized;

  133.     /** Reference dates. */
  134.     private List<AbsoluteDate> referenceDates;

  135.     /** Pulsations. */
  136.     private List<Double>       pulsations;

  137.     /** Time map of the harmonics. */
  138.     private TimeSpanMap<Container> containers;

  139.     /** Simple constructor.
  140.      *
  141.      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
  142.      *
  143.      * @param supportedNames regular expression for supported files names
  144.      * @param missingCoefficientsAllowed if true, allows missing coefficients in the input data
  145.      * @see #ICGEMFormatReader(String, boolean, TimeScale)
  146.      */
  147.     @DefaultDataContext
  148.     public ICGEMFormatReader(final String supportedNames, final boolean missingCoefficientsAllowed) {
  149.         this(supportedNames, missingCoefficientsAllowed,
  150.                 DataContext.getDefault().getTimeScales().getTT());
  151.     }

  152.     /**
  153.      * Simple constructor.
  154.      *
  155.      * @param supportedNames             regular expression for supported files names
  156.      * @param missingCoefficientsAllowed if true, allows missing coefficients in the input
  157.      *                                   data
  158.      * @param timeScale                  to use when parsing dates.
  159.      * @since 10.1
  160.      */
  161.     public ICGEMFormatReader(final String supportedNames,
  162.                              final boolean missingCoefficientsAllowed,
  163.                              final TimeScale timeScale) {
  164.         super(supportedNames, missingCoefficientsAllowed, timeScale);
  165.     }

  166.     /** {@inheritDoc} */
  167.     public void loadData(final InputStream input, final String name)
  168.         throws IOException, ParseException, OrekitException {

  169.         // reset the indicator before loading any data
  170.         setReadComplete(false);
  171.         containers     = null;
  172.         referenceDates = new ArrayList<>();
  173.         pulsations     = new ArrayList<>();

  174.         // by default, the field is normalized with unknown tide system
  175.         // (will be overridden later if non-default)
  176.         normalized            = true;
  177.         TideSystem tideSystem = TideSystem.UNKNOWN;
  178.         Errors     errors     = Errors.NO;

  179.         double    version        = 1.0;
  180.         boolean   inHeader       = true;
  181.         Flattener flattener      = null;
  182.         int       flags          = 0;
  183.         double[]  c0             = null;
  184.         double[]  s0             = null;
  185.         int       lineNumber     = 0;
  186.         String    line           = null;
  187.         try (BufferedReader r = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
  188.             for (line = r.readLine(); line != null; line = r.readLine()) {
  189.                 boolean parseError = false;
  190.                 ++lineNumber;
  191.                 line = line.trim();
  192.                 if (line.isEmpty()) {
  193.                     continue;
  194.                 }
  195.                 final String[] tab = SEPARATOR.split(line);
  196.                 if (inHeader) {
  197.                     if (tab.length >= 2 && FORMAT.equals(tab[0])) {
  198.                         final Matcher matcher = SUPPORTED_FORMAT.matcher(tab[1]);
  199.                         if (matcher.matches()) {
  200.                             version = Double.parseDouble(matcher.group(1));
  201.                             if (version > MAX_FORMAT) {
  202.                                 parseError = true;
  203.                             }
  204.                         } else {
  205.                             parseError = true;
  206.                         }
  207.                     } else if (tab.length >= 2 && PRODUCT_TYPE.equals(tab[0])) {
  208.                         parseError = !GRAVITY_FIELD.equals(tab[1]);
  209.                     } else if (tab.length >= 2 && tab[0].endsWith(GRAVITY_CONSTANT)) {
  210.                         setMu(parseDouble(tab[1]));
  211.                         flags |= MU;
  212.                     } else if (tab.length >= 2 && REFERENCE_RADIUS.equals(tab[0])) {
  213.                         setAe(parseDouble(tab[1]));
  214.                         flags |= AE;
  215.                     } else if (tab.length >= 2 && MAX_DEGREE.equals(tab[0])) {

  216.                         final int degree = FastMath.min(getMaxParseDegree(), Integer.parseInt(tab[1]));
  217.                         final int order  = FastMath.min(getMaxParseOrder(), degree);
  218.                         flattener  = new Flattener(degree, order);
  219.                         c0         = buildFlatArray(flattener, missingCoefficientsAllowed() ? 0.0 : Double.NaN);
  220.                         s0         = buildFlatArray(flattener, missingCoefficientsAllowed() ? 0.0 : Double.NaN);
  221.                         flags     |= LIMITS;

  222.                     } else if (tab.length >= 2 && ERRORS.equals(tab[0])) {
  223.                         try {
  224.                             errors = Errors.valueOf(tab[1].toUpperCase(Locale.US));
  225.                             flags |= ERR;
  226.                         } catch (IllegalArgumentException iae) {
  227.                             parseError = true;
  228.                         }
  229.                     } else if (tab.length >= 2 && TIDE_SYSTEM_INDICATOR.equals(tab[0])) {
  230.                         if (ZERO_TIDE.equals(tab[1])) {
  231.                             tideSystem = TideSystem.ZERO_TIDE;
  232.                         } else if (TIDE_FREE.equals(tab[1])) {
  233.                             tideSystem = TideSystem.TIDE_FREE;
  234.                         } else if (TIDE_UNKNOWN.equals(tab[1])) {
  235.                             tideSystem = TideSystem.UNKNOWN;
  236.                         } else {
  237.                             parseError = true;
  238.                         }
  239.                     } else if (tab.length >= 2 && NORMALIZATION_INDICATOR.equals(tab[0])) {
  240.                         if (NORMALIZED.equals(tab[1])) {
  241.                             normalized = true;
  242.                         } else if (UNNORMALIZED.equals(tab[1])) {
  243.                             normalized = false;
  244.                         } else {
  245.                             parseError = true;
  246.                         }
  247.                     } else if (tab.length >= 1 && END_OF_HEADER.equals(tab[0])) {
  248.                         inHeader   = false;
  249.                     }
  250.                     if (parseError) {
  251.                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  252.                                                   lineNumber, name, line);
  253.                     }
  254.                 } else if (dataKeyKnown(tab) && tab.length > 2) {

  255.                     final int n = Integer.parseInt(tab[1]);
  256.                     final int m = Integer.parseInt(tab[2]);
  257.                     flags |= COEFFS;
  258.                     if (!flattener.withinRange(n, m)) {
  259.                         // just ignore coefficients we don't need
  260.                         continue;
  261.                     }

  262.                     if (tab.length > 4 && GFC.equals(tab[0])) {
  263.                         // fixed coefficient

  264.                         parseCoefficient(tab[3], flattener, c0, n, m, "C", name);
  265.                         parseCoefficient(tab[4], flattener, s0, n, m, "S", name);

  266.                     } else if (version < 2.0 && tab.length > 5 + errors.fields && GFCT.equals(tab[0])) {
  267.                         // base of linear coefficient with infinite time range

  268.                         if (containers == null) {
  269.                             // prepare the single container (it will be populated when next lines are parsed)
  270.                             containers = new TimeSpanMap<>(new Container(flattener));
  271.                         }

  272.                         // set the constant coefficients to 0 as they will be managed by the piecewise model
  273.                         final int globalIndex = flattener.index(n, m);
  274.                         c0[globalIndex]       = 0.0;
  275.                         s0[globalIndex]       = 0.0;

  276.                         // store the single reference date valid for the whole field
  277.                         final AbsoluteDate lineDate = parseDate(tab[5 + errors.fields]);
  278.                         final int referenceIndex    = referenceDateIndex(referenceDates, lineDate);
  279.                         if (referenceIndex != 0) {
  280.                             // we already know the reference date, check this lines does not define a new one
  281.                             throw new OrekitException(OrekitMessages.SEVERAL_REFERENCE_DATES_IN_GRAVITY_FIELD,
  282.                                                       referenceDates.get(0), lineDate, name,
  283.                                                       lineDate.durationFrom(referenceDates.get(0)));
  284.                         }

  285.                         final Container single = containers.getFirstSpan().getData();
  286.                         final int       index  = single.flattener.index(n, m);
  287.                         if (single.components[index] != null) {
  288.                             throw new OrekitException(OrekitMessages.DUPLICATED_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
  289.                                                       BASE_NAMES, n, m, name);
  290.                         }
  291.                         single.components[index] = new TimeDependentHarmonic(referenceIndex, parseDouble(tab[3]), parseDouble(tab[4]));


  292.                     } else if (version >= 2.0 && tab.length > 6 + errors.fields && GFCT.equals(tab[0])) {
  293.                         // base of linear coefficient with limited time range

  294.                         if (containers == null) {
  295.                             // prepare empty map to hold containers as they are parsed
  296.                             containers = new TimeSpanMap<>(null);
  297.                         }

  298.                         // set the constant coefficients to 0 as they will be managed by the piecewise model
  299.                         final int globalIndex = flattener.index(n, m);
  300.                         c0[globalIndex]       = 0.0;
  301.                         s0[globalIndex]       = 0.0;

  302.                         final AbsoluteDate t0 = parseDate(tab[5 + errors.fields]);
  303.                         final AbsoluteDate t1 = parseDate(tab[6 + errors.fields]);

  304.                         // get the containers active for the specified time range
  305.                         final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
  306.                         for (final TimeSpanMap.Span<Container> span : active) {
  307.                             final Container             container = span.getData();
  308.                             final int                   index     = container.flattener.index(n, m);
  309.                             if (container.components[index] != null) {
  310.                                 throw new OrekitException(OrekitMessages.DUPLICATED_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
  311.                                                           BASE_NAMES, n, m, name);
  312.                             }
  313.                             container.components[index] = new TimeDependentHarmonic(referenceDateIndex(referenceDates, t0),
  314.                                                                                     parseDouble(tab[3]), parseDouble(tab[4]));
  315.                         }

  316.                     } else if (version < 2.0 && tab.length > 4 && (DOT.equals(tab[0]) || TRND.equals(tab[0]))) {
  317.                         // slope of linear coefficient with infinite range

  318.                         // store the secular trend coefficients
  319.                         final Container single = containers.getFirstSpan().getData();
  320.                         final TimeDependentHarmonic harmonic = single.components[single.flattener.index(n, m)];
  321.                         if (harmonic == null) {
  322.                             parseError = true;
  323.                         } else {
  324.                             harmonic.setTrend(parseDouble(tab[3]) / Constants.JULIAN_YEAR,
  325.                                               parseDouble(tab[4]) / Constants.JULIAN_YEAR);
  326.                         }

  327.                     } else if (version >= 2.0 && tab.length > 6 + errors.fields && TRND.equals(tab[0])) {
  328.                         // slope of linear coefficient with limited range

  329.                         final AbsoluteDate t0 = parseDate(tab[5 + errors.fields]);
  330.                         final AbsoluteDate t1 = parseDate(tab[6 + errors.fields]);

  331.                         // get the containers active for the specified time range
  332.                         final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
  333.                         for (final TimeSpanMap.Span<Container> span : active) {
  334.                             final Container             container = span.getData();
  335.                             final int                   index     = container.flattener.index(n, m);
  336.                             if (container.components[index] == null) {
  337.                                 parseError = true;
  338.                                 break;
  339.                             } else {
  340.                                 container.components[index].setTrend(parseDouble(tab[3]) / Constants.JULIAN_YEAR,
  341.                                                                      parseDouble(tab[4]) / Constants.JULIAN_YEAR);
  342.                             }
  343.                         }

  344.                     } else if (version < 2.0 && tab.length > 5 + errors.fields && (ASIN.equals(tab[0]) || ACOS.equals(tab[0]))) {
  345.                         // periodic coefficient with infinite range

  346.                         final double period = parseDouble(tab[5 + errors.fields]) * Constants.JULIAN_YEAR;
  347.                         final int    pIndex = pulsationIndex(pulsations, MathUtils.TWO_PI / period);

  348.                         // store the periodic effects coefficients
  349.                         final Container single = containers.getFirstSpan().getData();
  350.                         final TimeDependentHarmonic harmonic = single.components[single.flattener.index(n, m)];
  351.                         if (harmonic == null) {
  352.                             parseError = true;
  353.                         } else {
  354.                             if (ACOS.equals(tab[0])) {
  355.                                 harmonic.addCosine(-1, pIndex, parseDouble(tab[3]), parseDouble(tab[4]));
  356.                             } else {
  357.                                 harmonic.addSine(-1, pIndex, parseDouble(tab[3]), parseDouble(tab[4]));
  358.                             }
  359.                         }

  360.                     } else if (version >= 2.0 && tab.length > 7 + errors.fields && (ASIN.equals(tab[0]) || ACOS.equals(tab[0]))) {
  361.                         // periodic coefficient with limited range

  362.                         final AbsoluteDate t0      = parseDate(tab[5 + errors.fields]);
  363.                         final AbsoluteDate t1      = parseDate(tab[6 + errors.fields]);
  364.                         final int          tIndex  = referenceDateIndex(referenceDates, t0);
  365.                         final double       period  = parseDouble(tab[7 + errors.fields]) * Constants.JULIAN_YEAR;
  366.                         final int          pIndex  = pulsationIndex(pulsations, MathUtils.TWO_PI / period);

  367.                         // get the containers active for the specified time range
  368.                         final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
  369.                         for (final TimeSpanMap.Span<Container> span : active) {
  370.                             final Container             container = span.getData();
  371.                             final int                   index     = container.flattener.index(n, m);
  372.                             if (container.components[index] == null) {
  373.                                 parseError = true;
  374.                                 break;
  375.                             } else {
  376.                                 if (ASIN.equals(tab[0])) {
  377.                                     container.components[index].addSine(tIndex, pIndex,
  378.                                                                         parseDouble(tab[3]), parseDouble(tab[4]));
  379.                                 } else {
  380.                                     container.components[index].addCosine(tIndex, pIndex,
  381.                                                                           parseDouble(tab[3]), parseDouble(tab[4]));
  382.                                 }
  383.                             }
  384.                         }

  385.                     } else {
  386.                         parseError = true;
  387.                     }

  388.                 } else if (dataKeyKnown(tab)) {
  389.                     // this was an expected data key, but the line is truncated
  390.                     parseError = true;
  391.                 }

  392.                 if (parseError) {
  393.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  394.                                               lineNumber, name, line);
  395.                 }

  396.             }

  397.         } catch (NumberFormatException nfe) {
  398.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  399.                                       lineNumber, name, line);
  400.         }

  401.         if (flags != (MU | AE | LIMITS | ERR | COEFFS)) {
  402.             String loaderName = getClass().getName();
  403.             loaderName = loaderName.substring(loaderName.lastIndexOf('.') + 1);
  404.             throw new OrekitException(OrekitMessages.UNEXPECTED_FILE_FORMAT_ERROR_FOR_LOADER,
  405.                                       name, loaderName);
  406.         }

  407.         if (missingCoefficientsAllowed()) {
  408.             // ensure at least the (0, 0) element is properly set
  409.             if (Precision.equals(c0[flattener.index(0, 0)], 0.0, 0)) {
  410.                 c0[flattener.index(0, 0)] = 1.0;
  411.             }
  412.         }

  413.         setRawCoefficients(normalized, flattener, c0, s0, name);
  414.         setTideSystem(tideSystem);
  415.         setReadComplete(true);

  416.     }

  417.     /** Check if a line starts with a known data key.
  418.      * @param tab line fields
  419.      * @return true if line starts with a known data key
  420.      * @since 11.1
  421.      */
  422.     private boolean dataKeyKnown(final String[] tab) {
  423.         return tab.length > 0 &&
  424.                (GFC.equals(tab[0])  || GFCT.equals(tab[0]) ||
  425.                 DOT.equals(tab[0])  || TRND.equals(tab[0]) ||
  426.                 ASIN.equals(tab[0]) || ACOS.equals(tab[0]));
  427.     }

  428.     /** Get the spans with containers active over a time range.
  429.      * @param flattener converter from triangular form to flat form
  430.      * @param t0 start of time span
  431.      * @param t1 end of time span
  432.      * @return span active from {@code t0} to {@code t1}
  433.      * @since 11.1
  434.      */
  435.     private List<TimeSpanMap.Span<Container>> getActive(final Flattener flattener,
  436.                                                         final AbsoluteDate t0, final AbsoluteDate t1) {

  437.         final List<TimeSpanMap.Span<Container>> active = new ArrayList<>();

  438.         TimeSpanMap.Span<Container> span = containers.getSpan(t0);
  439.         if (span.getStart().isBefore(t0)) {
  440.             if (span.getEnd().isAfterOrEqualTo(t1)) {
  441.                 if (span.getData() == null) {
  442.                     // the specified time range lies on an empty range
  443.                     span = containers.addValidBetween(new Container(flattener), t0, t1);
  444.                 } else {
  445.                     // the specified time range splits an existing container in three parts
  446.                     containers.addValidAfter(copyContainer(span.getData(), flattener), t1, false);
  447.                     span = containers.addValidAfter(copyContainer(span.getData(), flattener), t0, false);
  448.                 }
  449.             } else {
  450.                 span = containers.addValidAfter(copyContainer(span.getData(), flattener), t0, false);
  451.             }
  452.         }

  453.         while (span.getStart().isBefore(t1)) {
  454.             if (span.getEnd().isAfter(t1)) {
  455.                 // this span extends past t1, we must split it
  456.                 span = containers.addValidBefore(copyContainer(span.getData(), flattener), t1, false);
  457.             }
  458.             active.add(span);
  459.             span = span.next();
  460.         }

  461.         return active;

  462.     }

  463.     /** Copy a container.
  464.      * @param original time span to copy (may be null)
  465.      * @param flattener converter between triangular and flat forms
  466.      * @return fresh copy
  467.      */
  468.     private Container copyContainer(final Container original, final Flattener flattener) {
  469.         return original == null ?
  470.                new Container(flattener) :
  471.                original.resize(flattener.getDegree(), flattener.getOrder());
  472.     }

  473.     /** Get the index of a reference date, adding it if needed.
  474.      * @param known known reference dates
  475.      * @param referenceDate reference date to select
  476.      * @return index of the reference date in the {@code known} list
  477.      * @since 11.1
  478.      */
  479.     private int referenceDateIndex(final List<AbsoluteDate> known, final AbsoluteDate referenceDate) {
  480.         for (int i = 0; i < known.size(); ++i) {
  481.             if (known.get(i).equals(referenceDate)) {
  482.                 return i;
  483.             }
  484.         }
  485.         known.add(referenceDate);
  486.         return known.size() - 1;
  487.     }

  488.     /** Get the index of a pulsation, adding it if needed.
  489.      * @param known known pulsations
  490.      * @param pulsation pulsation to select
  491.      * @return index of the pulsation in the {@code known} list
  492.      * @since 11.1
  493.      */
  494.     private int pulsationIndex(final List<Double> known, final double pulsation) {
  495.         for (int i = 0; i < known.size(); ++i) {
  496.             if (Precision.equals(known.get(i), pulsation, 1)) {
  497.                 return i;
  498.             }
  499.         }
  500.         known.add(pulsation);
  501.         return known.size() - 1;
  502.     }

  503.     /** {@inheritDoc} */
  504.     public RawSphericalHarmonicsProvider getProvider(final boolean wantNormalized,
  505.                                                      final int degree, final int order) {

  506.         // get the constant part of the field
  507.         final ConstantSphericalHarmonics constant = getBaseProvider(wantNormalized, degree, order);
  508.         if (containers == null) {
  509.             // there are no time-dependent parts in the field
  510.             return constant;
  511.         }

  512.         // create the shared parts of the model
  513.         final AbsoluteDate[] dates = new AbsoluteDate[referenceDates.size()];
  514.         for (int i = 0; i < dates.length; ++i) {
  515.             dates[i] = referenceDates.get(i);
  516.         }
  517.         final double[] puls = new double[pulsations.size()];
  518.         for (int i = 0; i < puls.length; ++i) {
  519.             puls[i] = pulsations.get(i);
  520.         }

  521.         // convert the mutable containers to piecewise parts with desired normalization
  522.         final TimeSpanMap<PiecewisePart> pieces = new TimeSpanMap<>(null);
  523.         for (TimeSpanMap.Span<Container> span = containers.getFirstSpan(); span != null; span = span.next()) {
  524.             if (span.getData() != null) {
  525.                 final Flattener spanFlattener = span.getData().flattener;
  526.                 final Flattener rescaledFlattener = new Flattener(FastMath.min(degree, spanFlattener.getDegree()),
  527.                                                                   FastMath.min(order, spanFlattener.getOrder()));
  528.                 pieces.addValidBetween(new PiecewisePart(rescaledFlattener,
  529.                                                          rescale(wantNormalized, rescaledFlattener, span.getData().flattener,
  530.                                                                  span.getData().components)),
  531.                                        span.getStart(), span.getEnd());
  532.             }
  533.         }

  534.         return new PiecewiseSphericalHarmonics(constant, dates, puls, pieces);

  535.     }

  536.     /** Parse a reference date.
  537.      * <p>
  538.      * The reference dates have either the format yyyymmdd (for 2011 format)
  539.      * or the format yyyymmdd.xxxx (for format version 2.0).
  540.      * </p>
  541.      * <p>
  542.      * The documentation for 2011 format does not specify the time scales,
  543.      * but on of the example reads "The reference time t0 is: t0 = 2005.0 y"
  544.      * when the corresponding field in the data section reads "20050101",
  545.      * so we assume the dates are consistent with astronomical conventions
  546.      * and 2005.0 is 2005-01-01T12:00:00 (i.e. noon).
  547.      * </p>
  548.      * <p>
  549.      * The 2.0 format is not described anywhere (at least I did not find any
  550.      * references), but the .xxxx fractional part seems to be hours and minutes chosen
  551.      * close to some strong earthquakes looking at the dates in Eigen 6S4 file
  552.      * with non-zero fractional part and the corresponding earthquakes hours
  553.      * (19850109.1751 vs. 1985-01-09T19:28:21, but it was not really a big quake,
  554.      * maybe there is a confusion with the 1985 Mexico earthquake at 1985-09-19T13:17:47,
  555.      * 20020815.0817 vs 2002-08-15:05:30:26, 20041226.0060 vs. 2004-12-26T00:58:53,
  556.      * 20100227.0735 vs. 2010-02-27T06:34:11, and 20110311.0515 vs. 2011-03-11T05:46:24).
  557.      * We guess the .0060 fractional part for the 2004 Sumatra-Andaman islands
  558.      * earthquake results from sloppy rounding when writing the file.
  559.      * </p>
  560.      * @param field text field containing the date
  561.      * @return parsed date
  562.      * @since 11.1
  563.      */
  564.     private AbsoluteDate parseDate(final String field) {

  565.         // check the date part (format yyyymmdd)
  566.         final DateComponents dc = new DateComponents(Integer.parseInt(field.substring(0, 4)),
  567.                                                      Integer.parseInt(field.substring(4, 6)),
  568.                                                      Integer.parseInt(field.substring(6, 8)));

  569.         // check the hour part (format .hhmm, working around checks on minutes)
  570.         final TimeComponents tc;
  571.         if (field.length() > 8) {
  572.             // we convert from hours and minutes here in order to allow
  573.             // the strange special case found in Eigen 6S4 file with date 20041226.0060
  574.             tc = new TimeComponents(Integer.parseInt(field.substring(9, 11)) * 3600 +
  575.                                     Integer.parseInt(field.substring(11, 13)) * 60);
  576.         } else {
  577.             // assume astronomical convention for hour
  578.             tc = TimeComponents.H12;
  579.         }

  580.         return toDate(dc, tc);

  581.     }

  582.     /** Temporary container for reading coefficients.
  583.      * @since 11.1
  584.      */
  585.     private static class Container {

  586.         /** Converter between (degree, order) indices and flatten array. */
  587.         private final Flattener flattener;

  588.         /** Components of the spherical harmonics. */
  589.         private final TimeDependentHarmonic[] components;

  590.         /** Build a container with given degree and order.
  591.          * @param flattener converter between (degree, order) indices and flatten array
  592.          */
  593.         Container(final Flattener flattener) {
  594.             this.flattener  = flattener;
  595.             this.components = new TimeDependentHarmonic[flattener.arraySize()];
  596.         }

  597.         /** Build a resized container.
  598.          * @param degree degree of the container
  599.          * @param order order of the container
  600.          * @return resized container
  601.          */
  602.         Container resize(final int degree, final int order) {

  603.             // create new instance
  604.             final Container resized = new Container(new Flattener(degree, order));

  605.             // copy harmonics
  606.             for (int n = 0; n <= degree; ++n) {
  607.                 for (int m = 0; m <= FastMath.min(n, order); ++m) {
  608.                     resized.components[resized.flattener.index(n, m)] = components[flattener.index(n, m)];
  609.                 }
  610.             }

  611.             return resized;

  612.         }

  613.     }

  614.     /** Errors present in the gravity field.
  615.      * @since 11.1
  616.      */
  617.     private enum Errors {

  618.         /** No errors. */
  619.         NO(0),

  620.         /** Calibrated errors. */
  621.         CALIBRATED(2),

  622.         /** Formal errors. */
  623.         FORMAL(2),

  624.         /** Both calibrated and formal. */
  625.         CALIBRATED_AND_FORMAL(4);

  626.         /** Number of error fields in data lines. */
  627.         private final int fields;

  628.         /** Simple constructor.
  629.          * @param fields umber of error fields in data lines
  630.          */
  631.         Errors(final int fields) {
  632.             this.fields = fields;
  633.         }

  634.     }

  635. }