AbstractGrid.java

  1. /* Copyright 2022-2025 Thales Alenia Space
  2.  * Licensed to CS Communication & Systèmes (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.models.earth.troposphere.iturp834;

  18. import org.hipparchus.CalculusFieldElement;
  19. import org.hipparchus.analysis.interpolation.GridAxis;
  20. import org.hipparchus.util.FastMath;
  21. import org.orekit.bodies.FieldGeodeticPoint;
  22. import org.orekit.bodies.GeodeticPoint;
  23. import org.orekit.errors.OrekitException;
  24. import org.orekit.errors.OrekitMessages;
  25. import org.orekit.utils.units.Unit;

  26. import java.io.BufferedReader;
  27. import java.io.IOException;
  28. import java.io.InputStream;
  29. import java.io.InputStreamReader;
  30. import java.nio.charset.StandardCharsets;
  31. import java.util.regex.Pattern;

  32. /** Container for grid data.
  33.  * @author Luc Maisonobe
  34.  * @since 13.0
  35.  */
  36. abstract class AbstractGrid {

  37.     /** ITU-R P.834 data resources directory. */
  38.     private static final String ITU_R_P_834 = "/assets/org/orekit/ITU-R-P.834/";

  39.     /** Pattern for splitting fields. */
  40.     private static final Pattern SPLITTER = Pattern.compile("\\s+");

  41.     /** Minimum latitude (degrees). */
  42.     private static final double MIN_LAT = -90.0;

  43.     /** Maximum latitude (degrees). */
  44.     private static final double MAX_LAT =  90.0;

  45.     /** Grid step in latitude (degrees). */
  46.     private static final double STEP_LAT =   1.5;

  47.     /** Grid step in latitude (radians). */
  48.     private static final double STEP_LAT_RAD = FastMath.toRadians(STEP_LAT);

  49.     /** Minimum longitude (degrees). */
  50.     private static final double MIN_LON = -180.0;

  51.     /** Maximum longitude (degrees). */
  52.     private static final double MAX_LON =  180.0;

  53.     /** Grid step in longitude (degrees). */
  54.     private static final double STEP_LON =   1.5;

  55.     /** Grid step in longitude (radians). */
  56.     private static final double STEP_LON_RAD = FastMath.toRadians(STEP_LON);

  57.     /** Latitude grid axis. */
  58.     private final GridAxis latitudeAxis;

  59.     /** Longitude grid axis. */
  60.     private final GridAxis longitudeAxis;

  61.     /** Simple constructor.
  62.      */
  63.     protected AbstractGrid() {
  64.         latitudeAxis  = buildAxis(MIN_LAT, MAX_LAT, STEP_LAT);
  65.         longitudeAxis = buildAxis(MIN_LON, MAX_LON, STEP_LON);
  66.     }

  67.     /** Get latitude axis.
  68.      * @return latitude axis
  69.      */
  70.     public GridAxis getLatitudeAxis() {
  71.         return latitudeAxis;
  72.     }

  73.     /** Get longitude axis.
  74.      * @return longitude axis
  75.      */
  76.     public GridAxis getLongitudeAxis() {
  77.         return longitudeAxis;
  78.     }

  79.     /** Get cell size in latitude.
  80.      * @return cell size in latitude
  81.      */
  82.     public double getSizeLat() {
  83.         return STEP_LAT_RAD;
  84.     }

  85.     /** Get cell size in longitude.
  86.      * @return cell size in longitude
  87.      */
  88.     public double getSizeLon() {
  89.         return STEP_LON_RAD;
  90.     }

  91.     /** Get one raw cell.
  92.      * @param location point location on Earth
  93.      * @param rawData raww grid data
  94.      * @return raw cell
  95.      */
  96.     protected GridCell getRawCell(final GeodeticPoint location, final double[][] rawData) {

  97.         // locate the point
  98.         final int    southIndex    = latitudeAxis.interpolationIndex(location.getLatitude());
  99.         final double southLatitude = latitudeAxis.node(southIndex);
  100.         final int    westIndex     = longitudeAxis.interpolationIndex(location.getLongitude());
  101.         final double westLongitude = longitudeAxis.node(westIndex);

  102.         // build the cell
  103.         return new GridCell(location.getLatitude() - southLatitude,
  104.                             location.getLongitude() - westLongitude,
  105.                             STEP_LAT_RAD, STEP_LON_RAD,
  106.                             rawData[southIndex + 1][westIndex],
  107.                             rawData[southIndex][westIndex],
  108.                             rawData[southIndex][westIndex + 1],
  109.                             rawData[southIndex + 1][westIndex + 1]);

  110.     }

  111.     /** Get one raw cell.
  112.      * @param <T> type of the field elements
  113.      * @param location point location on Earth
  114.      * @param rawData raww grid data
  115.      * @return raw cell
  116.      */
  117.     protected <T extends CalculusFieldElement<T>> FieldGridCell<T> getRawCell(final FieldGeodeticPoint<T> location,
  118.                                                                               final double[][] rawData) {

  119.         // locate the point
  120.         final int    southIndex    = latitudeAxis.interpolationIndex(location.getLatitude().getReal());
  121.         final double southLatitude = latitudeAxis.node(southIndex);
  122.         final int    westIndex     = longitudeAxis.interpolationIndex(location.getLongitude().getReal());
  123.         final double westLongitude = longitudeAxis.node(westIndex);

  124.         // build the cell
  125.         final T zero = location.getAltitude().getField().getZero();
  126.         return new FieldGridCell<>(location.getLatitude().subtract(southLatitude),
  127.                                    location.getLongitude().subtract(westLongitude),
  128.                                    STEP_LAT_RAD, STEP_LON_RAD,
  129.                                    zero.newInstance(rawData[southIndex + 1][westIndex]),
  130.                                    zero.newInstance(rawData[southIndex][westIndex]),
  131.                                    zero.newInstance(rawData[southIndex][westIndex + 1]),
  132.                                    zero.newInstance(rawData[southIndex + 1][westIndex + 1]));

  133.     }

  134.     /** Get one cell.
  135.      * @param location     point location on Earth
  136.      * @param secondOfYear second of year
  137.      * @return cell at location
  138.      */
  139.     public abstract GridCell getCell(GeodeticPoint location, double secondOfYear);

  140.     /** Get one cell.
  141.      * @param <T>          type of the field elements
  142.      * @param location     point location on Earth
  143.      * @param secondOfYear second of year
  144.      * @return cell at location
  145.      */
  146.     public abstract <T extends CalculusFieldElement<T>> FieldGridCell<T> getCell(FieldGeodeticPoint<T> location,
  147.                                                                                  T secondOfYear);

  148.     /** Build a grid axis for interpolating within a table.
  149.      * @param min min angle in degrees (included)
  150.      * @param max max angle in degrees (included)
  151.      * @param step step between points
  152.      * @return grid axis
  153.      */
  154.     private static GridAxis buildAxis(final double min, final double max, final double step) {
  155.         final double[] grid = new double[(int) FastMath.rint((max - min) / step) + 1];
  156.         for (int i = 0; i < grid.length; i++) {
  157.             grid[i] = FastMath.toRadians(min + i * step);
  158.         }
  159.         return new GridAxis(grid, 2);
  160.     }

  161.     /** Parse interpolation table from a resource file.
  162.      * @param unit unit of values in resource file
  163.      * @param name name of the resource to parse
  164.      * @return parsed interpolation function
  165.      */
  166.     protected double[][] parse(final Unit unit, final String name) {

  167.         // parse the file
  168.         final double[][] values = new double[latitudeAxis.size()][longitudeAxis.size()];
  169.         try (InputStream       is     = ITURP834WeatherParametersProvider.class.getResourceAsStream(ITU_R_P_834 + name);
  170.              InputStreamReader isr    = is  == null ? null : new InputStreamReader(is, StandardCharsets.UTF_8);
  171.              BufferedReader    reader = isr == null ? null : new BufferedReader(isr)) {
  172.             if (reader == null) {
  173.                 // this should never happen with embedded data
  174.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, name);
  175.             }
  176.             for (int row = 0; row < latitudeAxis.size(); ++row) {

  177.                 // the ITU-files are sorted in decreasing latitude order, from +90° to -90°…
  178.                 final int latitudeIndex = latitudeAxis.size() - 1 - row;

  179.                 final String   line   = reader.readLine();
  180.                 final String[] fields = line == null ? new String[0] : SPLITTER.split(line.trim());
  181.                 if (fields.length != longitudeAxis.size()) {
  182.                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, row + 1, name, line);
  183.                 }

  184.                 // distribute points in longitude
  185.                 for (int col = 0; col < longitudeAxis.size(); ++col) {
  186.                     // files are between 0° and 360° in longitude, with last column (360°) equal to first column (0°)
  187.                     // our tables, on the other hand, use canonical longitudes between -180° and +180°
  188.                     // we have to redistribute indices
  189.                     // col =   0 → base longitude =   0.0° → fixed longitude =    0.0° → longitudeIndex = 120
  190.                     // col =   1 → base longitude =   1.5° → fixed longitude =    1.5° → longitudeIndex = 121
  191.                     // …
  192.                     // col = 119 → base longitude = 178.5° → fixed longitude =  178.5° → longitudeIndex = 239
  193.                     // col = 120 → base longitude = 180.0° → fixed longitude =  180.0° → longitudeIndex = 240
  194.                     // col = 121 → base longitude = 181.5° → fixed longitude = -178.5° → longitudeIndex =   1
  195.                     // …
  196.                     // col = 239 → base longitude = 358.5° → fixed longitude =   -1.5° → longitudeIndex = 119
  197.                     // col = 240 → base longitude = 360.0° → fixed longitude =    0.0° → longitudeIndex = 120
  198.                     final int longitudeIndex = col < 121 ? col + 120 : col - 120;
  199.                     values[latitudeIndex][longitudeIndex] = unit.toSI(Double.parseDouble(fields[col]));
  200.                 }

  201.                 // the loop above stored longitude 180° at index 240, but longitude -180° is missing
  202.                 values[latitudeIndex][0] = values[latitudeIndex][longitudeAxis.size() - 1];

  203.             }
  204.         } catch (IOException ioe) {
  205.             // this should never happen with the embedded data
  206.             throw new OrekitException(OrekitMessages.INTERNAL_ERROR, ioe);
  207.         }

  208.         // build the interpolating function
  209.         return values;

  210.     }

  211. }