- /* Copyright 2002-2025 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.models.earth.ionosphere.nequick;
- import org.hipparchus.CalculusFieldElement;
- import org.hipparchus.util.FastMath;
- import org.hipparchus.util.MathUtils;
- import org.orekit.data.DataSource;
- import org.orekit.errors.OrekitException;
- import org.orekit.errors.OrekitMessages;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.Reader;
- import java.util.regex.Pattern;
- /** Modified Dip Latitude grid.
- * <p>
- * The modip grid allows to estimate modip μ [deg] at a given point (φ,λ)
- * by interpolation of the relevant values contained in the support file.
- * </p>
- * <p>
- * The data is parsed from files sampling Earth in latitude and longitude. In
- * the case of Galileo-specific modip file, sampling step is 5° in latitude and
- * 10° in longitude, and extra rows/columns have been added to wrap around
- * anti-meridian and longitude and also "past" pole (adding extra points with
- * 180° longitude phasing offset) in latitude. In the case of ITU original
- * NeQuick 2 model, sampling step is 1° in latitude and 2° in longitude, without
- * any extra rows/columns. We therefore add the extra rows/columns upon parsing
- * the ITU file.
- * </p>
- * @author Bryan Cazabonne
- * @author Luc Maisonobe
- * @since 13.0
- */
- class ModipGrid {
- /** Pattern for delimiting regular expressions. */
- private static final Pattern SEPARATOR = Pattern.compile("\\s+");
- /** Number of cells of modip grid in longitude (without wrapping). */
- private final int nbCellsLon;
- /** Cell size in longitude. */
- private final double sizeLon;
- /** Number of cells of modip grid in latitude (without wrapping). */
- private final int nbCellsLat;
- /** Cell size in latitude. */
- private final double sizeLat;
- /** Modip grid. */
- private final double[][] grid;
- /** Build a new modip grid.
- * @param nbCellsLon number of cells of modip grid in longitude (without wrapping)
- * @param nbCellsLat number of cells of modip grid in latitude (without wrapping)
- * @param source source of the grid file
- * @param wrappingAlreadyIncluded indicator for already included modip grid wrapping in resource file
- */
- ModipGrid(final int nbCellsLon, final int nbCellsLat, final DataSource source,
- final boolean wrappingAlreadyIncluded) {
- this.nbCellsLon = nbCellsLon;
- this.sizeLon = MathUtils.TWO_PI / nbCellsLon;
- this.nbCellsLat = nbCellsLat;
- this.sizeLat = FastMath.PI / nbCellsLat;
- this.grid = new double[nbCellsLat + 3][nbCellsLon + 3];
- loadData(source, !wrappingAlreadyIncluded);
- }
- /** Compute modip for a location.
- * @param latitude latitude
- * @param longitude longitude
- * @return modip at specified location, in degrees
- */
- public double computeMODIP(final double latitude, final double longitude) {
- // index in latitude (note that Galileo document uses x for interpolation in latitude)
- // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
- final double x = (latitude + MathUtils.SEMI_PI) / sizeLat + 1;
- if (x < 1) {
- return -90;
- }
- if (x >= nbCellsLat + 1) {
- return 90;
- }
- final int i = (int) FastMath.floor(x);
- final double deltaX = x - i;
- // index in longitude (note that Galileo document uses y for interpolation in longitude)
- // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
- double y = (longitude + FastMath.PI) / sizeLon + 1;
- while (y < 1) {
- y += nbCellsLon;
- }
- while (y >= nbCellsLon + 1) {
- y -= nbCellsLon;
- }
- final int j = (int) FastMath.floor(y);
- final double deltaY = y - j;
- // zi coefficients (Eq. 12 and 13)
- final double z1 = interpolate(grid[i - 1][j - 1], grid[i][j - 1], grid[i + 1][j - 1], grid[i + 2][j - 1], deltaX);
- final double z2 = interpolate(grid[i - 1][j], grid[i][j], grid[i + 1][j], grid[i + 2][j], deltaX);
- final double z3 = interpolate(grid[i - 1][j + 1], grid[i][j + 1], grid[i + 1][j + 1], grid[i + 2][j + 1], deltaX);
- final double z4 = interpolate(grid[i - 1][j + 2], grid[i][j + 2], grid[i + 1][j + 2], grid[i + 2][j + 2], deltaX);
- // Modip (Ref Eq. 16)
- return interpolate(z1, z2, z3, z4, deltaY);
- }
- /**
- * This method provides a third order interpolation function
- * as recommended in the reference document (Ref Eq. 128 to Eq. 138)
- *
- * @param z1 z1 coefficient
- * @param z2 z2 coefficient
- * @param z3 z3 coefficient
- * @param z4 z4 coefficient
- * @param x position
- * @return a third order interpolation
- */
- private double interpolate(final double z1, final double z2, final double z3, final double z4,
- final double x) {
- if (x < 5e-11) {
- return z2;
- }
- final double delta = 2.0 * x - 1.0;
- final double g1 = z3 + z2;
- final double g2 = z3 - z2;
- final double g3 = z4 + z1;
- final double g4 = (z4 - z1) / 3.0;
- final double a0 = 9.0 * g1 - g3;
- final double a1 = 9.0 * g2 - g4;
- final double a2 = g3 - g1;
- final double a3 = g4 - g2;
- return 0.0625 * (a0 + delta * (a1 + delta * (a2 + delta * a3)));
- }
- /** Compute modip for a location.
- * @param <T> type of the field elements
- * @param latitude latitude
- * @param longitude longitude
- * @return modip at specified location
- */
- public <T extends CalculusFieldElement<T>> T computeMODIP(final T latitude, final T longitude) {
- // index in latitude (note that Galileo document uses x for interpolation in latitude)
- // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
- final T x = latitude.add(MathUtils.SEMI_PI).divide(sizeLat).add(1);
- if (x.getReal() < 1) {
- return latitude.newInstance(-90);
- }
- if (x.getReal() >= nbCellsLat + 1) {
- return latitude.newInstance(90);
- }
- final int i = (int) FastMath.floor(x.getReal());
- final T deltaX = x.subtract(i);
- // index in longitude (note that Galileo document uses y for interpolation in longitude)
- // ensuring we have two points before (or at) index and two points after index for 3rd order interpolation
- T y = longitude.add(FastMath.PI).divide(sizeLon).add(1);
- while (y.getReal() < 1) {
- y = y.add(nbCellsLon);
- }
- while (y.getReal() >= nbCellsLon + 1) {
- y = y.subtract(nbCellsLon);
- }
- final int j = (int) FastMath.floor(y.getReal());
- final T deltaY = y.subtract(j);
- // zi coefficients (Eq. 12 and 13)
- final T z1 = interpolate(x.newInstance(grid[i - 1][j - 1]), x.newInstance(grid[i][j - 1]),
- x.newInstance(grid[i + 1][j - 1]), x.newInstance(grid[i + 2][j - 1]),
- deltaX);
- final T z2 = interpolate(x.newInstance(grid[i - 1][j]), x.newInstance(grid[i][j]),
- x.newInstance(grid[i + 1][j]), x.newInstance(grid[i + 2][j]),
- deltaX);
- final T z3 = interpolate(x.newInstance(grid[i - 1][j + 1]), x.newInstance(grid[i][j + 1]),
- x.newInstance(grid[i + 1][j + 1]), x.newInstance(grid[i + 2][j + 1]),
- deltaX);
- final T z4 = interpolate(x.newInstance(grid[i - 1][j + 2]), x.newInstance(grid[i][j + 2]),
- x.newInstance(grid[i + 1][j + 2]), x.newInstance(grid[i + 2][j + 2]),
- deltaX);
- // Modip (Ref Eq. 16)
- return interpolate(z1, z2, z3, z4, deltaY);
- }
- /**
- * This method provides a third order interpolation function
- * as recommended in the reference document (Ref Eq. 128 to Eq. 138)
- *
- * @param <T> type of the field elements
- * @param z1 z1 coefficient
- * @param z2 z2 coefficient
- * @param z3 z3 coefficient
- * @param z4 z4 coefficient
- * @param x position
- * @return a third order interpolation
- */
- private <T extends CalculusFieldElement<T>> T interpolate(final T z1, final T z2, final T z3, final T z4,
- final T x) {
- if (x.getReal() < 5e-11) {
- return z2;
- }
- final T delta = x.multiply(2.0).subtract(1.0);
- final T g1 = z3.add(z2);
- final T g2 = z3.subtract(z2);
- final T g3 = z4.add(z1);
- final T g4 = z4.subtract(z1).divide(3.0);
- final T a0 = g1.multiply(9.0).subtract(g3);
- final T a1 = g2.multiply(9.0).subtract(g4);
- final T a2 = g3.subtract(g1);
- final T a3 = g4.subtract(g2);
- return delta.multiply(a3).add(a2).multiply(delta).add(a1).multiply(delta).add(a0).multiply(0.0625);
- }
- /**
- * Load data from a stream.
- *
- * @param source grid source
- * @param addWrapping if true, wrapping should be added to loaded data
- */
- private void loadData(final DataSource source, final boolean addWrapping) {
- // if we must add wrapping, we must keep some empty rows and columns that will be filled later on
- final int first = addWrapping ? 1 : 0;
- // Open stream and parse data
- int lineNumber = 0;
- String line = null;
- int row = first;
- try (Reader r = source.getOpener().openReaderOnce();
- BufferedReader br = new BufferedReader(r)) {
- for (line = br.readLine(); line != null; line = br.readLine()) {
- ++lineNumber;
- // Read grid data
- final String[] fields = SEPARATOR.split(line.trim());
- if (fields.length == (addWrapping ? grid[row].length - 2 : grid[row].length)) {
- // this should be a regular data line (i.e. not a header line)
- for (int i = 0; i < fields.length; i++) {
- grid[row][first + i] = Double.parseDouble(fields[i]);
- }
- ++row;
- }
- }
- } catch (IOException | NumberFormatException e) {
- throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, source.getName(), line);
- }
- if (addWrapping) {
- // we must add rows phased 180° in longitude after poles, for the sake of interpolation
- final double[] extraNorth = grid[0];
- final double[] refNorth = grid[2];
- final double[] extraSouth = grid[grid.length - 1];
- final double[] refSouth = grid[grid.length - 3];
- final int deltaPhase = nbCellsLon / 2;
- for (int j = 1; j <= deltaPhase; j++) {
- extraNorth[j] = refNorth[j + deltaPhase];
- extraNorth[j + deltaPhase] = refNorth[j];
- extraSouth[j] = refSouth[j + deltaPhase];
- extraSouth[j + deltaPhase] = refSouth[j];
- }
- extraNorth[nbCellsLon + 1] = extraNorth[1];
- extraSouth[nbCellsLon + 1] = extraSouth[1];
- // we must copy columns to wrap around Earth in longitude
- // the first three columns must be identical to the last three columns
- // the columns 1 and gi.length - 2 (i.e. anti-meridian) are already
- // identical in the original resource file
- for (final double[] gi : grid) {
- gi[0] = gi[gi.length - 3];
- gi[gi.length - 1] = gi[2];
- }
- row++;
- }
- // Throw an exception if modip grid was not loaded properly
- if (row != grid.length) {
- throw new OrekitException(OrekitMessages.MODIP_GRID_NOT_LOADED, source.getName());
- }
- }
- }