ICGEMFormatReader.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.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
import org.hipparchus.util.Precision;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
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;
import org.orekit.utils.Constants;
import org.orekit.utils.TimeSpanMap;
/** Reader for the ICGEM gravity field format.
*
* <p>This format is used to describe the gravity field of EIGEN models
* published by the GFZ Potsdam since 2004. It is described in Franz
* Barthelmes and Christoph Förste paper: "the ICGEM-format".
* The 2006-02-28 version of this paper can be found <a
* href="http://op.gfz-potsdam.de/grace/results/grav/g005_ICGEM-Format.pdf">here</a>
* and the 2011-06-07 version of this paper can be found <a
* href="http://icgem.gfz-potsdam.de/ICGEM-Format-2011.pdf">here</a>.
* These versions differ in time-dependent coefficients, which are linear-only prior
* to 2011 (up to eigen-5 model) and have also harmonic effects after that date
* (starting with eigen-6 model). A third (undocumented as of 2018-05-14) version
* of the file format also adds a time-span for time-dependent coefficients, allowing
* for piecewise models. All three versions are supported by the class.</p>
* <p>
* This reader uses relaxed check on the gravity constant key so any key ending
* in gravity_constant is accepted and not only earth_gravity_constant as specified
* in the previous documents. This allows to read also non Earth gravity fields
* as found in <a href="http://icgem.gfz-potsdam.de/tom_celestial">ICGEM
* - Gravity Field Models of other Celestial Bodies</a> page to be read.
* </p>
*
* <p> The proper way to use this class is to call the {@link GravityFieldFactory}
* which will determine which reader to use with the selected gravity field file.</p>
*
* @see GravityFields
* @author Luc Maisonobe
*/
public class ICGEMFormatReader extends PotentialCoefficientsReader {
/** Format. */
private static final String FORMAT = "format";
/** Maximum supported formats. */
private static final double MAX_FORMAT = 2.0;
/** Product type. */
private static final String PRODUCT_TYPE = "product_type";
/** Gravity field product type. */
private static final String GRAVITY_FIELD = "gravity_field";
/** Gravity constant marker. */
private static final String GRAVITY_CONSTANT = "gravity_constant";
/** Reference radius. */
private static final String REFERENCE_RADIUS = "radius";
/** Max degree. */
private static final String MAX_DEGREE = "max_degree";
/** Errors indicator. */
private static final String ERRORS = "errors";
/** Tide system indicator. */
private static final String TIDE_SYSTEM_INDICATOR = "tide_system";
/** Indicator value for zero-tide system. */
private static final String ZERO_TIDE = "zero_tide";
/** Indicator value for tide-free system. */
private static final String TIDE_FREE = "tide_free";
/** Indicator value for unknown tide system. */
private static final String TIDE_UNKNOWN = "unknown";
/** Normalization indicator. */
private static final String NORMALIZATION_INDICATOR = "norm";
/** Indicator value for normalized coefficients. */
private static final String NORMALIZED = "fully_normalized";
/** Indicator value for un-normalized coefficients. */
private static final String UNNORMALIZED = "unnormalized";
/** End of header marker. */
private static final String END_OF_HEADER = "end_of_head";
/** Gravity field coefficient. */
private static final String GFC = "gfc";
/** Time stamped gravity field coefficient. */
private static final String GFCT = "gfct";
/** Gravity field coefficient first time derivative. */
private static final String DOT = "dot";
/** Gravity field coefficient trend. */
private static final String TRND = "trnd";
/** Gravity field coefficient sine amplitude. */
private static final String ASIN = "asin";
/** Gravity field coefficient cosine amplitude. */
private static final String ACOS = "acos";
/** Name of base coefficients. */
private static final String BASE_NAMES = "C/S";
/** Pattern for delimiting regular expressions. */
private static final Pattern SEPARATOR = Pattern.compile("\\s+");
/** Pattern for supported formats. */
private static final Pattern SUPPORTED_FORMAT = Pattern.compile("icgem(\\d+\\.\\d+)");
/** Flag for Gravitational coefficient. */
private static final int MU = 0x1;
/** Flag for scaling radius. */
private static final int AE = 0x2;
/** Flag for degree/order. */
private static final int LIMITS = 0x4;
/** Flag for errors. */
private static final int ERR = 0x8;
/** Flag for coefficients. */
private static final int COEFFS = 0x10;
/** Indicator for normalized coefficients. */
private boolean normalized;
/** Reference dates. */
private List<AbsoluteDate> referenceDates;
/** Pulsations. */
private List<Double> pulsations;
/** Time map of the harmonics. */
private TimeSpanMap<Container> containers;
/** Simple constructor.
*
* <p>This constructor uses the {@link DataContext#getDefault() default data context}.
*
* @param supportedNames regular expression for supported files names
* @param missingCoefficientsAllowed if true, allows missing coefficients in the input data
* @see #ICGEMFormatReader(String, boolean, TimeScale)
*/
@DefaultDataContext
public ICGEMFormatReader(final String supportedNames, final boolean missingCoefficientsAllowed) {
this(supportedNames, missingCoefficientsAllowed,
DataContext.getDefault().getTimeScales().getTT());
}
/**
* Simple constructor.
*
* @param supportedNames regular expression for supported files names
* @param missingCoefficientsAllowed if true, allows missing coefficients in the input
* data
* @param timeScale to use when parsing dates.
* @since 10.1
*/
public ICGEMFormatReader(final String supportedNames,
final boolean missingCoefficientsAllowed,
final TimeScale timeScale) {
super(supportedNames, missingCoefficientsAllowed, timeScale);
}
/** {@inheritDoc} */
public void loadData(final InputStream input, final String name)
throws IOException, ParseException, OrekitException {
// reset the indicator before loading any data
setReadComplete(false);
containers = null;
referenceDates = new ArrayList<>();
pulsations = new ArrayList<>();
// by default, the field is normalized with unknown tide system
// (will be overridden later if non-default)
normalized = true;
TideSystem tideSystem = TideSystem.UNKNOWN;
Errors errors = Errors.NO;
double version = 1.0;
boolean inHeader = true;
Flattener flattener = null;
int flags = 0;
double[] c0 = null;
double[] s0 = null;
int lineNumber = 0;
String line = null;
try (BufferedReader r = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
for (line = r.readLine(); line != null; line = r.readLine()) {
boolean parseError = false;
++lineNumber;
line = line.trim();
if (line.length() == 0) {
continue;
}
final String[] tab = SEPARATOR.split(line);
if (inHeader) {
if (tab.length >= 2 && FORMAT.equals(tab[0])) {
final Matcher matcher = SUPPORTED_FORMAT.matcher(tab[1]);
if (matcher.matches()) {
version = Double.parseDouble(matcher.group(1));
if (version > MAX_FORMAT) {
parseError = true;
}
} else {
parseError = true;
}
} else if (tab.length >= 2 && PRODUCT_TYPE.equals(tab[0])) {
parseError = !GRAVITY_FIELD.equals(tab[1]);
} else if (tab.length >= 2 && tab[0].endsWith(GRAVITY_CONSTANT)) {
setMu(parseDouble(tab[1]));
flags |= MU;
} else if (tab.length >= 2 && REFERENCE_RADIUS.equals(tab[0])) {
setAe(parseDouble(tab[1]));
flags |= AE;
} else if (tab.length >= 2 && MAX_DEGREE.equals(tab[0])) {
final int degree = FastMath.min(getMaxParseDegree(), Integer.parseInt(tab[1]));
final int order = FastMath.min(getMaxParseOrder(), degree);
flattener = new Flattener(degree, order);
c0 = buildFlatArray(flattener, missingCoefficientsAllowed() ? 0.0 : Double.NaN);
s0 = buildFlatArray(flattener, missingCoefficientsAllowed() ? 0.0 : Double.NaN);
flags |= LIMITS;
} else if (tab.length >= 2 && ERRORS.equals(tab[0])) {
try {
errors = Errors.valueOf(tab[1].toUpperCase(Locale.US));
flags |= ERR;
} catch (IllegalArgumentException iae) {
parseError = true;
}
} else if (tab.length >= 2 && TIDE_SYSTEM_INDICATOR.equals(tab[0])) {
if (ZERO_TIDE.equals(tab[1])) {
tideSystem = TideSystem.ZERO_TIDE;
} else if (TIDE_FREE.equals(tab[1])) {
tideSystem = TideSystem.TIDE_FREE;
} else if (TIDE_UNKNOWN.equals(tab[1])) {
tideSystem = TideSystem.UNKNOWN;
} else {
parseError = true;
}
} else if (tab.length >= 2 && NORMALIZATION_INDICATOR.equals(tab[0])) {
if (NORMALIZED.equals(tab[1])) {
normalized = true;
} else if (UNNORMALIZED.equals(tab[1])) {
normalized = false;
} else {
parseError = true;
}
} else if (tab.length >= 1 && END_OF_HEADER.equals(tab[0])) {
inHeader = false;
}
if (parseError) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
} else if (dataKeyKnown(tab) && tab.length > 2) {
final int n = Integer.parseInt(tab[1]);
final int m = Integer.parseInt(tab[2]);
flags |= COEFFS;
if (!flattener.withinRange(n, m)) {
// just ignore coefficients we don't need
continue;
}
if (tab.length > 4 && GFC.equals(tab[0])) {
// fixed coefficient
parseCoefficient(tab[3], flattener, c0, n, m, "C", name);
parseCoefficient(tab[4], flattener, s0, n, m, "S", name);
} else if (version < 2.0 && tab.length > 5 + errors.fields && GFCT.equals(tab[0])) {
// base of linear coefficient with infinite time range
if (containers == null) {
// prepare the single container (it will be populated when next lines are parsed)
containers = new TimeSpanMap<>(new Container(flattener));
}
// set the constant coefficients to 0 as they will be managed by the piecewise model
final int globalIndex = flattener.index(n, m);
c0[globalIndex] = 0.0;
s0[globalIndex] = 0.0;
// store the single reference date valid for the whole field
final AbsoluteDate lineDate = parseDate(tab[5 + errors.fields]);
final int referenceIndex = referenceDateIndex(referenceDates, lineDate);
if (referenceIndex != 0) {
// we already know the reference date, check this lines does not define a new one
throw new OrekitException(OrekitMessages.SEVERAL_REFERENCE_DATES_IN_GRAVITY_FIELD,
referenceDates.get(0), lineDate, name,
lineDate.durationFrom(referenceDates.get(0)));
}
final Container single = containers.getFirstSpan().getData();
final int index = single.flattener.index(n, m);
if (single.components[index] != null) {
throw new OrekitException(OrekitMessages.DUPLICATED_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
BASE_NAMES, n, m, name);
}
single.components[index] = new TimeDependentHarmonic(referenceIndex, parseDouble(tab[3]), parseDouble(tab[4]));
} else if (version >= 2.0 && tab.length > 6 + errors.fields && GFCT.equals(tab[0])) {
// base of linear coefficient with limited time range
if (containers == null) {
// prepare empty map to hold containers as they are parsed
containers = new TimeSpanMap<>(null);
}
// set the constant coefficients to 0 as they will be managed by the piecewise model
final int globalIndex = flattener.index(n, m);
c0[globalIndex] = 0.0;
s0[globalIndex] = 0.0;
final AbsoluteDate t0 = parseDate(tab[5 + errors.fields]);
final AbsoluteDate t1 = parseDate(tab[6 + errors.fields]);
// get the containers active for the specified time range
final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
for (final TimeSpanMap.Span<Container> span : active) {
final Container container = span.getData();
final int index = container.flattener.index(n, m);
if (container.components[index] != null) {
throw new OrekitException(OrekitMessages.DUPLICATED_GRAVITY_FIELD_COEFFICIENT_IN_FILE,
BASE_NAMES, n, m, name);
}
container.components[index] = new TimeDependentHarmonic(referenceDateIndex(referenceDates, t0),
parseDouble(tab[3]), parseDouble(tab[4]));
}
} else if (version < 2.0 && tab.length > 4 && (DOT.equals(tab[0]) || TRND.equals(tab[0]))) {
// slope of linear coefficient with infinite range
// store the secular trend coefficients
final Container single = containers.getFirstSpan().getData();
final TimeDependentHarmonic harmonic = single.components[single.flattener.index(n, m)];
if (harmonic == null) {
parseError = true;
} else {
harmonic.setTrend(parseDouble(tab[3]) / Constants.JULIAN_YEAR,
parseDouble(tab[4]) / Constants.JULIAN_YEAR);
}
} else if (version >= 2.0 && tab.length > 6 + errors.fields && TRND.equals(tab[0])) {
// slope of linear coefficient with limited range
final AbsoluteDate t0 = parseDate(tab[5 + errors.fields]);
final AbsoluteDate t1 = parseDate(tab[6 + errors.fields]);
// get the containers active for the specified time range
final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
for (final TimeSpanMap.Span<Container> span : active) {
final Container container = span.getData();
final int index = container.flattener.index(n, m);
if (container.components[index] == null) {
parseError = true;
break;
} else {
container.components[index].setTrend(parseDouble(tab[3]) / Constants.JULIAN_YEAR,
parseDouble(tab[4]) / Constants.JULIAN_YEAR);
}
}
} else if (version < 2.0 && tab.length > 5 + errors.fields && (ASIN.equals(tab[0]) || ACOS.equals(tab[0]))) {
// periodic coefficient with infinite range
final double period = parseDouble(tab[5 + errors.fields]) * Constants.JULIAN_YEAR;
final int pIndex = pulsationIndex(pulsations, MathUtils.TWO_PI / period);
// store the periodic effects coefficients
final Container single = containers.getFirstSpan().getData();
final TimeDependentHarmonic harmonic = single.components[single.flattener.index(n, m)];
if (harmonic == null) {
parseError = true;
} else {
if (ACOS.equals(tab[0])) {
harmonic.addCosine(-1, pIndex, parseDouble(tab[3]), parseDouble(tab[4]));
} else {
harmonic.addSine(-1, pIndex, parseDouble(tab[3]), parseDouble(tab[4]));
}
}
} else if (version >= 2.0 && tab.length > 7 + errors.fields && (ASIN.equals(tab[0]) || ACOS.equals(tab[0]))) {
// periodic coefficient with limited range
final AbsoluteDate t0 = parseDate(tab[5 + errors.fields]);
final AbsoluteDate t1 = parseDate(tab[6 + errors.fields]);
final int tIndex = referenceDateIndex(referenceDates, t0);
final double period = parseDouble(tab[7 + errors.fields]) * Constants.JULIAN_YEAR;
final int pIndex = pulsationIndex(pulsations, MathUtils.TWO_PI / period);
// get the containers active for the specified time range
final List<TimeSpanMap.Span<Container>> active = getActive(flattener, t0, t1);
for (final TimeSpanMap.Span<Container> span : active) {
final Container container = span.getData();
final int index = container.flattener.index(n, m);
if (container.components[index] == null) {
parseError = true;
break;
} else {
if (ASIN.equals(tab[0])) {
container.components[index].addSine(tIndex, pIndex,
parseDouble(tab[3]), parseDouble(tab[4]));
} else {
container.components[index].addCosine(tIndex, pIndex,
parseDouble(tab[3]), parseDouble(tab[4]));
}
}
}
} else {
parseError = true;
}
} else if (dataKeyKnown(tab)) {
// this was an expected data key, but the line is truncated
parseError = true;
}
if (parseError) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
}
} catch (NumberFormatException nfe) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
if (flags != (MU | AE | LIMITS | ERR | COEFFS)) {
String loaderName = getClass().getName();
loaderName = loaderName.substring(loaderName.lastIndexOf('.') + 1);
throw new OrekitException(OrekitMessages.UNEXPECTED_FILE_FORMAT_ERROR_FOR_LOADER,
name, loaderName);
}
if (missingCoefficientsAllowed()) {
// ensure at least the (0, 0) element is properly set
if (Precision.equals(c0[flattener.index(0, 0)], 0.0, 0)) {
c0[flattener.index(0, 0)] = 1.0;
}
}
setRawCoefficients(normalized, flattener, c0, s0, name);
setTideSystem(tideSystem);
setReadComplete(true);
}
/** Check if a line starts with a known data key.
* @param tab line fields
* @return true if line starts with a known data key
* @since 11.1
*/
private boolean dataKeyKnown(final String[] tab) {
return tab.length > 0 &&
(GFC.equals(tab[0]) || GFCT.equals(tab[0]) ||
DOT.equals(tab[0]) || TRND.equals(tab[0]) ||
ASIN.equals(tab[0]) || ACOS.equals(tab[0]));
}
/** Get the spans with containers active over a time range.
* @param flattener converter from triangular form to flat form
* @param t0 start of time span
* @param t1 end of time span
* @return span active from {@code t0} to {@code t1}
* @since 11.1
*/
private List<TimeSpanMap.Span<Container>> getActive(final Flattener flattener,
final AbsoluteDate t0, final AbsoluteDate t1) {
final List<TimeSpanMap.Span<Container>> active = new ArrayList<>();
TimeSpanMap.Span<Container> span = containers.getSpan(t0);
if (span.getStart().isBefore(t0)) {
if (span.getEnd().isAfterOrEqualTo(t1)) {
if (span.getData() == null) {
// the specified time range lies on an empty range
span = containers.addValidBetween(new Container(flattener), t0, t1);
} else {
// the specified time range splits an existing container in three parts
containers.addValidAfter(copyContainer(span.getData(), flattener), t1, false);
span = containers.addValidAfter(copyContainer(span.getData(), flattener), t0, false);
}
} else {
span = containers.addValidAfter(copyContainer(span.getData(), flattener), t0, false);
}
}
while (span.getStart().isBefore(t1)) {
if (span.getEnd().isAfter(t1)) {
// this span extends past t1, we must split it
span = containers.addValidBefore(copyContainer(span.getData(), flattener), t1, false);
}
active.add(span);
span = span.next();
}
return active;
}
/** Copy a container.
* @param original time span to copy (may be null)
* @param flattener converter between triangular and flat forms
* @return fresh copy
*/
private Container copyContainer(final Container original, final Flattener flattener) {
return original == null ?
new Container(flattener) :
original.resize(flattener.getDegree(), flattener.getOrder());
}
/** Get the index of a reference date, adding it if needed.
* @param known known reference dates
* @param referenceDate reference date to select
* @return index of the reference date in the {@code known} list
* @since 11.1
*/
private int referenceDateIndex(final List<AbsoluteDate> known, final AbsoluteDate referenceDate) {
for (int i = 0; i < known.size(); ++i) {
if (known.get(i).equals(referenceDate)) {
return i;
}
}
known.add(referenceDate);
return known.size() - 1;
}
/** Get the index of a pulsation, adding it if needed.
* @param known known pulsations
* @param pulsation pulsation to select
* @return index of the pulsation in the {@code known} list
* @since 11.1
*/
private int pulsationIndex(final List<Double> known, final double pulsation) {
for (int i = 0; i < known.size(); ++i) {
if (Precision.equals(known.get(i), pulsation, 1)) {
return i;
}
}
known.add(pulsation);
return known.size() - 1;
}
/** {@inheritDoc} */
public RawSphericalHarmonicsProvider getProvider(final boolean wantNormalized,
final int degree, final int order) {
// get the constant part of the field
final ConstantSphericalHarmonics constant = getBaseProvider(wantNormalized, degree, order);
if (containers == null) {
// there are no time-dependent parts in the field
return constant;
}
// create the shared parts of the model
final AbsoluteDate[] dates = new AbsoluteDate[referenceDates.size()];
for (int i = 0; i < dates.length; ++i) {
dates[i] = referenceDates.get(i);
}
final double[] puls = new double[pulsations.size()];
for (int i = 0; i < puls.length; ++i) {
puls[i] = pulsations.get(i);
}
// convert the mutable containers to piecewise parts with desired normalization
final TimeSpanMap<PiecewisePart> pieces = new TimeSpanMap<>(null);
for (TimeSpanMap.Span<Container> span = containers.getFirstSpan(); span != null; span = span.next()) {
if (span.getData() != null) {
final Flattener spanFlattener = span.getData().flattener;
final Flattener rescaledFlattener = new Flattener(FastMath.min(degree, spanFlattener.getDegree()),
FastMath.min(order, spanFlattener.getOrder()));
pieces.addValidBetween(new PiecewisePart(rescaledFlattener,
rescale(wantNormalized, rescaledFlattener, span.getData().flattener,
span.getData().components)),
span.getStart(), span.getEnd());
}
}
return new PiecewiseSphericalHarmonics(constant, dates, puls, pieces);
}
/** Parse a reference date.
* <p>
* The reference dates have either the format yyyymmdd (for 2011 format)
* or the format yyyymmdd.xxxx (for format version 2.0).
* </p>
* <p>
* The documentation for 2011 format does not specify the time scales,
* but on of the example reads "The reference time t0 is: t0 = 2005.0 y"
* when the corresponding field in the data section reads "20050101",
* so we assume the dates are consistent with astronomical conventions
* and 2005.0 is 2005-01-01T12:00:00 (i.e. noon).
* </p>
* <p>
* The 2.0 format is not described anywhere (at least I did not find any
* references), but the .xxxx fractional part seems to be hours and minutes chosen
* close to some strong earthquakes looking at the dates in Eigen 6S4 file
* with non-zero fractional part and the corresponding earthquakes hours
* (19850109.1751 vs. 1985-01-09T19:28:21, but it was not really a big quake,
* maybe there is a confusion with the 1985 Mexico earthquake at 1985-09-19T13:17:47,
* 20020815.0817 vs 2002-08-15:05:30:26, 20041226.0060 vs. 2004-12-26T00:58:53,
* 20100227.0735 vs. 2010-02-27T06:34:11, and 20110311.0515 vs. 2011-03-11T05:46:24).
* We guess the .0060 fractional part for the 2004 Sumatra-Andaman islands
* earthquake results from sloppy rounding when writing the file.
* </p>
* @param field text field containing the date
* @return parsed date
* @since 11.1
*/
private AbsoluteDate parseDate(final String field) {
// check the date part (format yyyymmdd)
final DateComponents dc = new DateComponents(Integer.parseInt(field.substring(0, 4)),
Integer.parseInt(field.substring(4, 6)),
Integer.parseInt(field.substring(6, 8)));
// check the hour part (format .hhmm, working around checks on minutes)
final TimeComponents tc;
if (field.length() > 8) {
// we convert from hours and minutes here in order to allow
// the strange special case found in Eigen 6S4 file with date 20041226.0060
tc = new TimeComponents(Integer.parseInt(field.substring(9, 11)) * 3600 +
Integer.parseInt(field.substring(11, 13)) * 60);
} else {
// assume astronomical convention for hour
tc = TimeComponents.H12;
}
return toDate(dc, tc);
}
/** Temporary container for reading coefficients.
* @since 11.1
*/
private static class Container {
/** Converter between (degree, order) indices and flatten array. */
private final Flattener flattener;
/** Components of the spherical harmonics. */
private final TimeDependentHarmonic[] components;
/** Build a container with given degree and order.
* @param flattener converter between (degree, order) indices and flatten array
*/
Container(final Flattener flattener) {
this.flattener = flattener;
this.components = new TimeDependentHarmonic[flattener.arraySize()];
}
/** Build a resized container.
* @param degree degree of the container
* @param order order of the container
* @return resized container
*/
Container resize(final int degree, final int order) {
// create new instance
final Container resized = new Container(new Flattener(degree, order));
// copy harmonics
for (int n = 0; n <= degree; ++n) {
for (int m = 0; m <= FastMath.min(n, order); ++m) {
resized.components[resized.flattener.index(n, m)] = components[flattener.index(n, m)];
}
}
return resized;
}
}
/** Errors present in the gravity field.
* @since 11.1
*/
private enum Errors {
/** No errors. */
NO(0),
/** Calibrated errors. */
CALIBRATED(2),
/** Formal errors. */
FORMAL(2),
/** Both calibrated and formal. */
CALIBRATED_AND_FORMAL(4);
/** Number of error fields in data lines. */
private final int fields;
/** Simple constructor.
* @param fields umber of error fields in data lines
*/
Errors(final int fields) {
this.fields = fields;
}
}
}