PotentialCoefficientsReader.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.forces.gravity.potential;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Locale;

import org.hipparchus.util.FastMath;
import org.hipparchus.util.Precision;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.data.DataLoader;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateComponents;
import org.orekit.time.TimeComponents;
import org.orekit.time.TimeScale;

/**This abstract class represents a Gravitational Potential Coefficients file reader.
 *
 * <p> As it exits many different coefficients models and containers this
 *  interface represents all the methods that should be implemented by a reader.
 *  The proper way to use this interface is to call the {@link GravityFieldFactory}
 *  which will determine which reader to use with the selected potential
 *  coefficients file.</p>
 *
 * @see GravityFields
 * @author Fabien Maussion
 */
public abstract class PotentialCoefficientsReader implements DataLoader {

    /** Maximal degree to parse. */
    private int maxParseDegree;

    /** Maximal order to parse. */
    private int maxParseOrder;

    /** Regular expression for supported files names. */
    private final String supportedNames;

    /** Allow missing coefficients in the input data. */
    private final boolean missingCoefficientsAllowed;

    /** Time scale for parsing dates. */
    private final TimeScale timeScale;

    /** Indicator for complete read. */
    private boolean readComplete;

    /** Central body reference radius. */
    private double ae;

    /** Central body attraction coefficient. */
    private double mu;

    /** Converter from triangular to flat form. */
    private Flattener flattener;

    /** Raw tesseral-sectorial coefficients matrix. */
    private double[] rawC;

    /** Raw tesseral-sectorial coefficients matrix. */
    private double[] rawS;

    /** Indicator for normalized raw coefficients. */
    private boolean normalized;

    /** Tide system. */
    private TideSystem tideSystem;

    /** Simple constructor.
     * <p>Build an uninitialized reader.</p>
     *
     * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
     *
     * @param supportedNames regular expression for supported files names
     * @param missingCoefficientsAllowed allow missing coefficients in the input data
     * @see #PotentialCoefficientsReader(String, boolean, TimeScale)
     */
    @DefaultDataContext
    protected PotentialCoefficientsReader(final String supportedNames,
                                          final boolean missingCoefficientsAllowed) {
        this(supportedNames, missingCoefficientsAllowed,
                DataContext.getDefault().getTimeScales().getTT());
    }

    /** Simple constructor.
     * <p>Build an uninitialized reader.</p>
     * @param supportedNames regular expression for supported files names
     * @param missingCoefficientsAllowed allow missing coefficients in the input data
     * @param timeScale to use when parsing dates.
     * @since 10.1
     */
    protected PotentialCoefficientsReader(final String supportedNames,
                                          final boolean missingCoefficientsAllowed,
                                          final TimeScale timeScale) {
        this.supportedNames             = supportedNames;
        this.missingCoefficientsAllowed = missingCoefficientsAllowed;
        this.maxParseDegree             = Integer.MAX_VALUE;
        this.maxParseOrder              = Integer.MAX_VALUE;
        this.readComplete               = false;
        this.ae                         = Double.NaN;
        this.mu                         = Double.NaN;
        this.flattener                  = null;
        this.rawC                       = null;
        this.rawS                       = null;
        this.normalized                 = false;
        this.tideSystem                 = TideSystem.UNKNOWN;
        this.timeScale                  = timeScale;
    }

    /** Get the regular expression for supported files names.
     * @return regular expression for supported files names
     */
    public String getSupportedNames() {
        return supportedNames;
    }

    /** Check if missing coefficients are allowed in the input data.
     * @return true if missing coefficients are allowed in the input data
     */
    public boolean missingCoefficientsAllowed() {
        return missingCoefficientsAllowed;
    }

    /** Set the degree limit for the next file parsing.
     * @param maxParseDegree maximal degree to parse (may be safely
     * set to {@link Integer#MAX_VALUE} to parse all available coefficients)
     * @since 6.0
     */
    public void setMaxParseDegree(final int maxParseDegree) {
        this.maxParseDegree = maxParseDegree;
    }

    /** Get the degree limit for the next file parsing.
     * @return degree limit for the next file parsing
     * @since 6.0
     */
    public int getMaxParseDegree() {
        return maxParseDegree;
    }

    /** Set the order limit for the next file parsing.
     * @param maxParseOrder maximal order to parse (may be safely
     * set to {@link Integer#MAX_VALUE} to parse all available coefficients)
     * @since 6.0
     */
    public void setMaxParseOrder(final int maxParseOrder) {
        this.maxParseOrder = maxParseOrder;
    }

    /** Get the order limit for the next file parsing.
     * @return order limit for the next file parsing
     * @since 6.0
     */
    public int getMaxParseOrder() {
        return maxParseOrder;
    }

    /** {@inheritDoc} */
    public boolean stillAcceptsData() {
        return !(readComplete &&
                 getMaxAvailableDegree() >= getMaxParseDegree() &&
                 getMaxAvailableOrder()  >= getMaxParseOrder());
    }

    /** Set the indicator for completed read.
     * @param readComplete if true, a gravity field has been completely read
     */
    protected void setReadComplete(final boolean readComplete) {
        this.readComplete = readComplete;
    }

    /** Set the central body reference radius.
     * @param ae central body reference radius
     */
    protected void setAe(final double ae) {
        this.ae = ae;
    }

    /** Get the central body reference radius.
     * @return central body reference radius
     */
    protected double getAe() {
        return ae;
    }

    /** Set the central body attraction coefficient.
     * @param mu central body attraction coefficient
     */
    protected void setMu(final double mu) {
        this.mu = mu;
    }

    /** Get the central body attraction coefficient.
     * @return central body attraction coefficient
     */
    protected double getMu() {
        return mu;
    }

    /** Set the {@link TideSystem} used in the gravity field.
     * @param tideSystem tide system used in the gravity field
     */
    protected void setTideSystem(final TideSystem tideSystem) {
        this.tideSystem = tideSystem;
    }

    /** Get the {@link TideSystem} used in the gravity field.
     * @return tide system used in the gravity field
     */
    protected TideSystem getTideSystem() {
        return tideSystem;
    }

    /** Set the tesseral-sectorial coefficients matrix.
     * @param rawNormalized if true, raw coefficients are normalized
     * @param f converter from triangular to flat form
     * @param c raw tesseral-sectorial coefficients matrix
     * @param s raw tesseral-sectorial coefficients matrix
     * @param name name of the file (or zip entry)
     * @since 11.1
     */
    protected void setRawCoefficients(final boolean rawNormalized, final Flattener f,
                                      final double[] c, final double[] s,
                                      final String name) {

        this.flattener = f;

        // normalization indicator
        normalized = rawNormalized;

        // set known constant values, if they were not defined in the file.
        // See Hofmann-Wellenhof and Moritz, "Physical Geodesy",
        // section 2.6 Harmonics of Lower Degree.
        // All S_i,0 are irrelevant because they are multiplied by zero.
        // C0,0 is 1, the central part, since all coefficients are normalized by GM.
        setIfUnset(c, flattener.index(0, 0), 1);
        setIfUnset(s, flattener.index(0, 0), 0);

        if (flattener.getDegree() >= 1) {
            // C1,0, C1,1, and S1,1 are the x,y,z coordinates of the center of mass,
            // which are 0 since all coefficients are given in an Earth centered frame
            setIfUnset(c, flattener.index(1, 0), 0);
            setIfUnset(s, flattener.index(1, 0), 0);
            if (flattener.getOrder() >= 1) {
                setIfUnset(c, flattener.index(1, 1), 0);
                setIfUnset(s, flattener.index(1, 1), 0);
            }
        }

        // cosine part
        for (int i = 0; i <= flattener.getDegree(); ++i) {
            for (int j = 0; j <= FastMath.min(i, flattener.getOrder()); ++j) {
                if (Double.isNaN(c[flattener.index(i, j)])) {
                    throw new OrekitException(OrekitMessages.MISSING_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
                                              'C', i, j, name);
                }
            }
        }
        rawC = c.clone();

        // sine part
        for (int i = 0; i <= flattener.getDegree(); ++i) {
            for (int j = 0; j <= FastMath.min(i, flattener.getOrder()); ++j) {
                if (Double.isNaN(s[flattener.index(i, j)])) {
                    throw new OrekitException(OrekitMessages.MISSING_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
                                              'S', i, j, name);
                }
            }
        }
        rawS = s.clone();

    }

    /**
     * Set a coefficient if it has not been set already.
     * <p>
     * If {@code array[i]} is 0 or NaN this method sets it to {@code value} and returns
     * {@code true}. Otherwise the original value of {@code array[i]} is preserved and
     * this method return {@code false}.
     * <p>
     * If {@code array[i]} does not exist then this method returns {@code false}.
     *
     * @param array the coefficient array.
     * @param i     index in array.
     * @param value the new value to set.
     * @return {@code true} if the coefficient was set to {@code value}, {@code false} if
     * the coefficient was not set to {@code value}. A {@code false} return indicates the
     * coefficient has previously been set to a non-NaN, non-zero value.
     */
    private boolean setIfUnset(final double[] array, final int i, final double value) {
        if (array.length > i && (Double.isNaN(array[i]) || Precision.equals(array[i], 0.0, 0))) {
            // the coefficient was not already initialized
            array[i] = value;
            return true;
        } else {
            return false;
        }
    }

    /** Get the maximal degree available in the last file parsed.
     * @return maximal degree available in the last file parsed
     * @since 6.0
     */
    public int getMaxAvailableDegree() {
        return flattener.getDegree();
    }

    /** Get the maximal order available in the last file parsed.
     * @return maximal order available in the last file parsed
     * @since 6.0
     */
    public int getMaxAvailableOrder() {
        return flattener.getOrder();
    }

    /** {@inheritDoc} */
    public abstract void loadData(InputStream input, String name)
        throws IOException, ParseException, OrekitException;

    /** Get a provider for read spherical harmonics coefficients.
     * @param wantNormalized if true, the provider will provide normalized coefficients,
     * otherwise it will provide un-normalized coefficients
     * @param degree maximal degree
     * @param order maximal order
     * @return a new provider
     * @since 6.0
     */
    public abstract RawSphericalHarmonicsProvider getProvider(boolean wantNormalized, int degree, int order);

    /** Get a time-independent provider containing base harmonics coefficients.
     * <p>
     * Beware that some coeefficients may be missing here, if they are managed as time-dependent
     * piecewise models (as in ICGEM V2.0).
     * </p>
     * @param wantNormalized if true, the raw provider must provide normalized coefficients,
     * otherwise it will provide un-normalized coefficients
     * @param degree maximal degree
     * @param order maximal order
     * @return a new provider, with no time-dependent parts
     * @see #getProvider(boolean, int, int)
     * @since 11.1
     */
    protected ConstantSphericalHarmonics getBaseProvider(final boolean wantNormalized,
                                                         final int degree, final int order) {

        if (!readComplete) {
            throw new OrekitException(OrekitMessages.NO_GRAVITY_FIELD_DATA_LOADED);
        }

        final Flattener truncatedFlattener = new Flattener(degree, order);
        return new ConstantSphericalHarmonics(ae, mu, tideSystem, truncatedFlattener,
                                              rescale(1.0, wantNormalized, truncatedFlattener, flattener, rawC),
                                              rescale(1.0, wantNormalized, truncatedFlattener, flattener, rawS));

    }

    /** Build a coefficients array in flat form.
     * @param flattener converter from triangular to flat form
     * @param value initial value to put in array elements
     * @return built array
     * @since 11.1
     */
    protected static double[] buildFlatArray(final Flattener flattener, final double value) {
        final double[] array = new double[flattener.arraySize()];
        Arrays.fill(array, value);
        return array;
    }

    /**
     * Parse a double from a string. Accept the Fortran convention of using a 'D' or
     * 'd' instead of an 'E' or 'e'.
     *
     * @param string to be parsed.
     * @return the double value of {@code string}.
     */
    protected static double parseDouble(final String string) {
        return Double.parseDouble(string.toUpperCase(Locale.ENGLISH).replace('D', 'E'));
    }

    /** Build a coefficients row.
     * @param degree row degree
     * @param order row order
     * @param value initial value to put in array elements
     * @return built row
     */
    protected static double[] buildRow(final int degree, final int order, final double value) {
        final double[] row = new double[FastMath.min(order, degree) + 1];
        Arrays.fill(row, value);
        return row;
    }

    /** Parse a coefficient.
     * @param field text field to parse
     * @param f converter from triangular to flat form
     * @param array array where to put the coefficient
     * @param i first index in the list
     * @param j second index in the list
     * @param cName name of the coefficient
     * @param name name of the file
     * @since 11.1
     */
    protected void parseCoefficient(final String field, final Flattener f,
                                    final double[] array, final int i, final int j,
                                    final String cName, final String name) {
        final int    index    = f.index(i, j);
        final double value    = parseDouble(field);
        final double oldValue = array[index];
        if (Double.isNaN(oldValue) || Precision.equals(oldValue, 0.0, 0)) {
            // the coefficient was not already initialized
            array[index] = value;
        } else {
            throw new OrekitException(OrekitMessages.DUPLICATED_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
                                      name, i, j, name);
        }
    }

    /** Rescale coefficients arrays.
     * <p>
     * The normalized/unnormalized nature of original coefficients is inherited from previous parsing.
     * </p>
     * @param scale general scaling factor to apply to all elements
     * @param wantNormalized if true, the rescaled coefficients must be normalized,
     * otherwise they must be un-normalized
     * @param rescaledFlattener converter from triangular to flat form
     * @param originalFlattener converter from triangular to flat form
     * @param original original coefficients
     * @return rescaled coefficients
     * @since 11.1
     */
    protected double[] rescale(final double scale, final boolean wantNormalized, final Flattener rescaledFlattener,
                               final Flattener originalFlattener, final double[] original) {

        if (rescaledFlattener.getDegree() > originalFlattener.getDegree()) {
            throw new OrekitException(OrekitMessages.TOO_LARGE_DEGREE_FOR_GRAVITY_FIELD,
                                      rescaledFlattener.getDegree(), flattener.getDegree());
        }

        if (rescaledFlattener.getOrder() > originalFlattener.getOrder()) {
            throw new OrekitException(OrekitMessages.TOO_LARGE_ORDER_FOR_GRAVITY_FIELD,
                                      rescaledFlattener.getOrder(), flattener.getOrder());
        }

        // scaling and normalization factors
        final FactorsGenerator generator;
        if (wantNormalized == normalized) {
            // the parsed coefficients already match the specified normalization
            generator = (n, m) -> scale;
        } else {
            // we need to normalize/unnormalize parsed coefficients
            final double[][] unnormalizationFactors =
                            GravityFieldFactory.getUnnormalizationFactors(rescaledFlattener.getDegree(),
                                                                          rescaledFlattener.getOrder());
            generator = wantNormalized ?
                (n, m) -> scale / unnormalizationFactors[n][m] :
                (n, m) -> scale * unnormalizationFactors[n][m];
        }

        // perform rescaling
        final double[] rescaled = buildFlatArray(rescaledFlattener, 0.0);
        for (int n = 0; n <= rescaledFlattener.getDegree(); ++n) {
            for (int m = 0; m <= FastMath.min(n, rescaledFlattener.getOrder()); ++m) {
                final int    rescaledndex  = rescaledFlattener.index(n, m);
                final int    originalndex  = originalFlattener.index(n, m);
                rescaled[rescaledndex] = original[originalndex] * generator.factor(n, m);
            }
        }

        return rescaled;

    }

    /** Rescale coefficients arrays.
     * <p>
     * The normalized/unnormalized nature of original coefficients is inherited from previous parsing.
     * </p>
     * @param wantNormalized if true, the rescaled coefficients must be normalized,
     * otherwise they must be un-normalized
     * @param rescaledFlattener converter from triangular to flat form
     * @param originalFlattener converter from triangular to flat form
     * @param original original coefficients
     * @return rescaled coefficients
     * @since 11.1
     */
    protected TimeDependentHarmonic[] rescale(final boolean wantNormalized, final Flattener rescaledFlattener,
                                              final Flattener originalFlattener, final TimeDependentHarmonic[] original) {

        if (rescaledFlattener.getDegree() > originalFlattener.getDegree()) {
            throw new OrekitException(OrekitMessages.TOO_LARGE_DEGREE_FOR_GRAVITY_FIELD,
                                      rescaledFlattener.getDegree(), flattener.getDegree());
        }

        if (rescaledFlattener.getOrder() > originalFlattener.getOrder()) {
            throw new OrekitException(OrekitMessages.TOO_LARGE_ORDER_FOR_GRAVITY_FIELD,
                                      rescaledFlattener.getOrder(), flattener.getOrder());
        }

        // scaling and normalization factors
        final FactorsGenerator generator;
        if (wantNormalized == normalized) {
            // the parsed coefficients already match the specified normalization
            generator = (n, m) -> 1.0;
        } else {
            // we need to normalize/unnormalize parsed coefficients
            final double[][] unnormalizationFactors =
                            GravityFieldFactory.getUnnormalizationFactors(rescaledFlattener.getDegree(),
                                                                          rescaledFlattener.getOrder());
            generator = wantNormalized ?
                (n, m) -> 1.0 / unnormalizationFactors[n][m] :
                (n, m) -> unnormalizationFactors[n][m];
        }

        // perform rescaling
        final TimeDependentHarmonic[] rescaled = new TimeDependentHarmonic[rescaledFlattener.arraySize()];
        for (int n = 0; n <= rescaledFlattener.getDegree(); ++n) {
            for (int m = 0; m <= FastMath.min(n, rescaledFlattener.getOrder()); ++m) {
                final int originalndex = originalFlattener.index(n, m);
                if (original[originalndex] != null) {
                    final int    rescaledndex = rescaledFlattener.index(n, m);
                    final double factor       = generator.factor(n, m);
                    rescaled[rescaledndex]    = new TimeDependentHarmonic(factor, original[originalndex]);
                }
            }
        }

        return rescaled;

    }

    /**
     * Create a date from components. Assumes the time part is noon.
     *
     * @param components year, month, day.
     * @return date.
     */
    protected AbsoluteDate toDate(final DateComponents components) {
        return toDate(components, TimeComponents.H12);
    }

    /**
     * Create a date from components.
     *
     * @param dc dates components.
     * @param tc time components
     * @return date.
     * @since 11.1
     */
    protected AbsoluteDate toDate(final DateComponents dc, final TimeComponents tc) {
        return new AbsoluteDate(dc, tc, timeScale);
    }

    /** Generator for normalization/unnormalization factors.
     * @since 11.1
     */
    private interface FactorsGenerator {
        /** Generator the normalization/unnormalization factors.
         * @param n degree of the gravity field component
         * @param m order of the gravity field component
         * @return factor to apply to term
         */
        double factor(int n, int m);
    }

}