GlobalIonosphereMapModel.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.models.earth.ionosphere;

  18. import java.io.BufferedInputStream;
  19. import java.io.BufferedReader;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.nio.charset.StandardCharsets;
  24. import java.util.ArrayList;
  25. import java.util.Collections;
  26. import java.util.List;

  27. import org.hipparchus.CalculusFieldElement;
  28. import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
  29. import org.hipparchus.exception.DummyLocalizable;
  30. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  31. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  32. import org.hipparchus.util.FastMath;
  33. import org.orekit.annotation.DefaultDataContext;
  34. import org.orekit.bodies.BodyShape;
  35. import org.orekit.bodies.GeodeticPoint;
  36. import org.orekit.data.DataContext;
  37. import org.orekit.data.DataLoader;
  38. import org.orekit.data.DataProvidersManager;
  39. import org.orekit.data.DataSource;
  40. import org.orekit.errors.OrekitException;
  41. import org.orekit.errors.OrekitMessages;
  42. import org.orekit.frames.Frame;
  43. import org.orekit.frames.TopocentricFrame;
  44. import org.orekit.propagation.FieldSpacecraftState;
  45. import org.orekit.propagation.SpacecraftState;
  46. import org.orekit.time.AbsoluteDate;
  47. import org.orekit.time.FieldAbsoluteDate;
  48. import org.orekit.time.TimeScale;
  49. import org.orekit.utils.ParameterDriver;
  50. import org.orekit.utils.TimeSpanMap;

  51. /**
  52.  * Global Ionosphere Map (GIM) model.
  53.  * The ionospheric delay is computed according to the formulas:
  54.  * <pre>
  55.  *           40.3
  56.  *    δ =  --------  *  STEC      with, STEC = VTEC * F(elevation)
  57.  *            f²
  58.  * </pre>
  59.  * With:
  60.  * <ul>
  61.  * <li>f: The frequency of the signal in Hz.</li>
  62.  * <li>STEC: The Slant Total Electron Content in TECUnits.</li>
  63.  * <li>VTEC: The Vertical Total Electron Content in TECUnits.</li>
  64.  * <li>F(elevation): A mapping function which depends on satellite elevation.</li>
  65.  * </ul>
  66.  * The VTEC is read from a IONEX file. A stream contains, for a given day, the values of the TEC for each hour of the day.
  67.  * Values are given on a global 2.5° x 5.0° (latitude x longitude) grid.
  68.  * <p>
  69.  * A bilinear interpolation is performed the case of the user initialize the latitude and the
  70.  * longitude with values that are not contained in the stream.
  71.  * </p><p>
  72.  * A temporal interpolation is also performed to compute the VTEC at the desired date.
  73.  * </p><p>
  74.  * IONEX files are obtained from
  75.  * <a href="ftp://cddis.nasa.gov/gnss/products/ionex/"> The Crustal Dynamics Data Information System</a>.
  76.  * </p><p>
  77.  * The files have to be extracted to UTF-8 text files before being read by this loader.
  78.  * </p><p>
  79.  * Example of file:
  80.  * </p>
  81.  * <pre>
  82.  *      1.0            IONOSPHERE MAPS     GPS                 IONEX VERSION / TYPE
  83.  * BIMINX V5.3         AIUB                16-JAN-19 07:26     PGM / RUN BY / DATE
  84.  * BROADCAST IONOSPHERE MODEL FOR DAY 015, 2019                COMMENT
  85.  *   2019     1    15     0     0     0                        EPOCH OF FIRST MAP
  86.  *   2019     1    16     0     0     0                        EPOCH OF LAST MAP
  87.  *   3600                                                      INTERVAL
  88.  *     25                                                      # OF MAPS IN FILE
  89.  *   NONE                                                      MAPPING FUNCTION
  90.  *      0.0                                                    ELEVATION CUTOFF
  91.  *                                                             OBSERVABLES USED
  92.  *   6371.0                                                    BASE RADIUS
  93.  *      2                                                      MAP DIMENSION
  94.  *    350.0 350.0   0.0                                        HGT1 / HGT2 / DHGT
  95.  *     87.5 -87.5  -2.5                                        LAT1 / LAT2 / DLAT
  96.  *   -180.0 180.0   5.0                                        LON1 / LON2 / DLON
  97.  *     -1                                                      EXPONENT
  98.  * TEC/RMS values in 0.1 TECU; 9999, if no value available     COMMENT
  99.  *                                                             END OF HEADER
  100.  *      1                                                      START OF TEC MAP
  101.  *   2019     1    15     0     0     0                        EPOCH OF CURRENT MAP
  102.  *     87.5-180.0 180.0   5.0 350.0                            LAT/LON1/LON2/DLON/H
  103.  *    92   92   92   92   92   92   92   92   92   92   92   92   92   92   92   92
  104.  *    92   92   92   92   92   92   92   92   92   92   92   92   92   92   92   92
  105.  *    92   92   92   92   92   92   92   92   92   92   92   92   92   92   92   92
  106.  *    92   92   92   92   92   92   92   92   92   92   92   92   92   92   92   92
  107.  *    92   92   92   92   92   92   92   92   92
  108.  *    ...
  109.  * </pre>
  110.  *
  111.  * @see "Schaer, S., W. Gurtner, and J. Feltens, 1998, IONEX: The IONosphere Map EXchange
  112.  *       Format Version 1, February 25, 1998, Proceedings of the IGS AC Workshop
  113.  *       Darmstadt, Germany, February 9–11, 1998"
  114.  *
  115.  * @author Bryan Cazabonne
  116.  *
  117.  */
  118. public class GlobalIonosphereMapModel implements IonosphericModel {

  119.     /** Map of interpolable TEC. */
  120.     private final TimeSpanMap<TECMapPair> tecMap;

  121.     /** UTC time scale. */
  122.     private final TimeScale utc;

  123.     /** Loaded IONEX files.
  124.      * @since 12.0
  125.      */
  126.     private String names;

  127.     /**
  128.      * Constructor with supported names given by user. This constructor uses the {@link
  129.      * DataContext#getDefault() default data context}.
  130.      *
  131.      * @param supportedNames regular expression that matches the names of the IONEX files
  132.      *                       to be loaded. See {@link DataProvidersManager#feed(String,
  133.      *                       DataLoader)}.
  134.      * @see #GlobalIonosphereMapModel(String, DataProvidersManager, TimeScale)
  135.      */
  136.     @DefaultDataContext
  137.     public GlobalIonosphereMapModel(final String supportedNames) {
  138.         this(supportedNames,
  139.                 DataContext.getDefault().getDataProvidersManager(),
  140.                 DataContext.getDefault().getTimeScales().getUTC());
  141.     }

  142.     /**
  143.      * Constructor that uses user defined supported names and data context.
  144.      *
  145.      * @param supportedNames       regular expression that matches the names of the IONEX
  146.      *                             files to be loaded. See {@link DataProvidersManager#feed(String,
  147.      *                             DataLoader)}.
  148.      * @param dataProvidersManager provides access to auxiliary data files.
  149.      * @param utc                  UTC time scale.
  150.      * @since 10.1
  151.      */
  152.     public GlobalIonosphereMapModel(final String supportedNames,
  153.                                     final DataProvidersManager dataProvidersManager,
  154.                                     final TimeScale utc) {
  155.         this.utc    = utc;
  156.         this.tecMap = new TimeSpanMap<>(null);
  157.         this.names  = "";

  158.         // Read files
  159.         dataProvidersManager.feed(supportedNames, new Parser());

  160.     }

  161.     /**
  162.      * Constructor that uses user defined data sources.
  163.      *
  164.      * @param utc   UTC time scale.
  165.      * @param ionex sources for the IONEX files
  166.      * @since 12.0
  167.      */
  168.     public GlobalIonosphereMapModel(final TimeScale utc,
  169.                                     final DataSource... ionex) {
  170.         try {
  171.             this.utc    = utc;
  172.             this.tecMap = new TimeSpanMap<>(null);
  173.             this.names  = "";
  174.             final Parser parser = new Parser();
  175.             for (final DataSource source : ionex) {
  176.                 try (InputStream is  = source.getOpener().openStreamOnce();
  177.                     BufferedInputStream bis = new BufferedInputStream(is)) {
  178.                     parser.loadData(bis, source.getName());
  179.                 }
  180.             }
  181.         } catch (IOException ioe) {
  182.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  183.         }
  184.     }

  185.     /**
  186.      * Calculates the ionospheric path delay for the signal path from a ground
  187.      * station to a satellite traversing ionosphere single layer at some pierce point.
  188.      * <p>
  189.      * The path delay can be computed for any elevation angle.
  190.      * </p>
  191.      * @param date current date
  192.      * @param piercePoint ionospheric pierce point
  193.      * @param elevation elevation of the satellite from receiver point in radians
  194.      * @param frequency frequency of the signal in Hz
  195.      * @return the path delay due to the ionosphere in m
  196.      */
  197.     private double pathDelayAtIPP(final AbsoluteDate date, final GeodeticPoint piercePoint,
  198.                                   final double elevation, final double frequency) {
  199.         // TEC in TECUnits
  200.         final TECMapPair pair = getPairAtDate(date);
  201.         final double tec = pair.getTEC(date, piercePoint);
  202.         // Square of the frequency
  203.         final double freq2 = frequency * frequency;
  204.         // "Slant" Total Electron Content
  205.         final double stec;
  206.         // Check if a mapping factor is needed
  207.         if (pair.mapping) {
  208.             stec = tec;
  209.         } else {
  210.             // Mapping factor
  211.             final double fz = mappingFunction(elevation, pair.r0, pair.h);
  212.             stec = tec * fz;
  213.         }
  214.         // Delay computation
  215.         final double alpha  = 40.3e16 / freq2;
  216.         return alpha * stec;
  217.     }

  218.     @Override
  219.     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
  220.                             final double frequency, final double[] parameters) {

  221.         // Satellite position in body frame
  222.         final Frame    bodyFrame = baseFrame.getParentShape().getBodyFrame();
  223.         final Vector3D satPoint  = state.getPosition(bodyFrame);

  224.         // Elevation in radians
  225.         final double   elevation = bodyFrame.
  226.                                    getStaticTransformTo(baseFrame, state.getDate()).
  227.                                    transformPosition(satPoint).
  228.                                    getDelta();

  229.         // Only consider measures above the horizon
  230.         if (elevation > 0.0) {
  231.             // Normalized Line Of Sight in body frame
  232.             final Vector3D los = satPoint.subtract(baseFrame.getCartesianPoint()).normalize();
  233.             // Ionosphere Pierce Point
  234.             final GeodeticPoint ipp = piercePoint(state.getDate(), baseFrame.getCartesianPoint(), los, baseFrame.getParentShape());
  235.             if (ipp != null) {
  236.                 // Delay
  237.                 return pathDelayAtIPP(state.getDate(), ipp, elevation, frequency);
  238.             }
  239.         }

  240.         return 0.0;

  241.     }

  242.     /**
  243.      * Calculates the ionospheric path delay for the signal path from a ground
  244.      * station to a satellite traversing ionosphere single layer at some pierce point.
  245.      * <p>
  246.      * The path delay can be computed for any elevation angle.
  247.      * </p>
  248.      * @param <T> type of the elements
  249.      * @param date current date
  250.      * @param piercePoint ionospheric pierce point
  251.      * @param elevation elevation of the satellite from receiver point in radians
  252.      * @param frequency frequency of the signal in Hz
  253.      * @return the path delay due to the ionosphere in m
  254.      */
  255.     private <T extends CalculusFieldElement<T>> T pathDelayAtIPP(final FieldAbsoluteDate<T> date,
  256.                                                                  final GeodeticPoint piercePoint,
  257.                                                                  final T elevation, final double frequency) {
  258.         // TEC in TECUnits
  259.         final TECMapPair pair = getPairAtDate(date.toAbsoluteDate());
  260.         final T tec = pair.getTEC(date, piercePoint);
  261.         // Square of the frequency
  262.         final double freq2 = frequency * frequency;
  263.         // "Slant" Total Electron Content
  264.         final T stec;
  265.         // Check if a mapping factor is needed
  266.         if (pair.mapping) {
  267.             stec = tec;
  268.         } else {
  269.             // Mapping factor
  270.             final T fz = mappingFunction(elevation, pair.r0, pair.h);
  271.             stec = tec.multiply(fz);
  272.         }
  273.         // Delay computation
  274.         final double alpha  = 40.3e16 / freq2;
  275.         return stec.multiply(alpha);
  276.     }

  277.     @Override
  278.     public <T extends CalculusFieldElement<T>> T pathDelay(final FieldSpacecraftState<T> state, final TopocentricFrame baseFrame,
  279.                                                            final double frequency, final T[] parameters) {

  280.         // Satellite position in body frame
  281.         final Frame            bodyFrame = baseFrame.getParentShape().getBodyFrame();
  282.         final FieldVector3D<T> satPoint  = state.getPosition(bodyFrame);

  283.         // Elevation in radians
  284.         final T                elevation = bodyFrame.
  285.                                            getStaticTransformTo(baseFrame, state.getDate()).
  286.                                            transformPosition(satPoint).
  287.                                            getDelta();

  288.         // Only consider measures above the horizon
  289.         if (elevation.getReal() > 0.0) {
  290.             // Normalized Line Of Sight in body frame
  291.             final Vector3D los = satPoint.toVector3D().subtract(baseFrame.getCartesianPoint()).normalize();
  292.             // Ionosphere Pierce Point
  293.             final GeodeticPoint ipp = piercePoint(state.getDate().toAbsoluteDate(), baseFrame.getCartesianPoint(), los, baseFrame.getParentShape());
  294.             if (ipp != null) {
  295.                 // Delay
  296.                 return pathDelayAtIPP(state.getDate(), ipp, elevation, frequency);
  297.             }
  298.         }

  299.         return elevation.getField().getZero();

  300.     }

  301.     /** Get the pair valid at date.
  302.      * @param date computation date
  303.      * @return pair valid at date
  304.      * @since 12.0
  305.      */
  306.     private TECMapPair getPairAtDate(final AbsoluteDate date) {
  307.         final TECMapPair pair = tecMap.get(date);
  308.         if (pair == null) {
  309.             throw new OrekitException(OrekitMessages.NO_TEC_DATA_IN_FILES_FOR_DATE,
  310.                                       names, date);
  311.         }
  312.         return pair;
  313.     }

  314.     @Override
  315.     public List<ParameterDriver> getParametersDrivers() {
  316.         return Collections.emptyList();
  317.     }

  318.     /**
  319.      * Computes the ionospheric mapping function.
  320.      * @param elevation the elevation of the satellite in radians
  321.      * @param r0 mean Earth radius
  322.      * @param h height of the ionospheric layer
  323.      * @return the mapping function
  324.      */
  325.     private double mappingFunction(final double elevation,
  326.                                    final double r0, final double h) {
  327.         // Calculate the zenith angle from the elevation
  328.         final double z = FastMath.abs(0.5 * FastMath.PI - elevation);
  329.         // Distance ratio
  330.         final double ratio = r0 / (r0 + h);
  331.         // Mapping function
  332.         final double coef = FastMath.sin(z) * ratio;
  333.         final double fz = 1.0 / FastMath.sqrt(1.0 - coef * coef);
  334.         return fz;
  335.     }

  336.     /**
  337.      * Computes the ionospheric mapping function.
  338.      * @param <T> type of the elements
  339.      * @param elevation the elevation of the satellite in radians
  340.      * @param r0 mean Earth radius
  341.      * @param h height of the ionospheric layer
  342.      * @return the mapping function
  343.      */
  344.     private <T extends CalculusFieldElement<T>> T mappingFunction(final T elevation,
  345.                                                                   final double r0, final double h) {
  346.         // Calculate the zenith angle from the elevation
  347.         final T z = FastMath.abs(elevation.getPi().multiply(0.5).subtract(elevation));
  348.         // Distance ratio
  349.         final double ratio = r0 / (r0 + h);
  350.         // Mapping function
  351.         final T coef = FastMath.sin(z).multiply(ratio);
  352.         final T fz = FastMath.sqrt(coef.multiply(coef).negate().add(1.0)).reciprocal();
  353.         return fz;
  354.     }

  355.     /** Compute Ionospheric Pierce Point.
  356.      * <p>
  357.      * The pierce point is computed assuming a spherical ionospheric shell above mean Earth radius.
  358.      * </p>
  359.      * @param date computation date
  360.      * @param recPoint point at receiver station in body frame
  361.      * @param los normalized line of sight in body frame
  362.      * @param bodyShape shape of the body
  363.      * @return pierce point, or null if recPoint is above ionosphere single layer
  364.      * @since 12.0
  365.      */
  366.     private GeodeticPoint piercePoint(final AbsoluteDate date,
  367.                                       final Vector3D recPoint, final Vector3D los,
  368.                                       final BodyShape bodyShape) {

  369.         final TECMapPair pair = getPairAtDate(date);
  370.         final double     r    = pair.r0 + pair.h;
  371.         final double     r2   = r * r;
  372.         final double     p2   = recPoint.getNormSq();
  373.         if (p2 >= r2) {
  374.             // we are above ionosphere single layer
  375.             return null;
  376.         }

  377.         // compute positive k such that recPoint + k los is on the spherical shell at radius r
  378.         final double dot = Vector3D.dotProduct(recPoint, los);
  379.         final double k   = FastMath.sqrt(dot * dot + r2 - p2) - dot;

  380.         // Ionosphere Pierce Point in body frame
  381.         final Vector3D ipp = new Vector3D(1, recPoint, k, los);

  382.         return bodyShape.transform(ipp, bodyShape.getBodyFrame(), null);

  383.     }

  384.     /** Parser for IONEX files. */
  385.     private class Parser implements DataLoader {

  386.         /** String for the end of a TEC map. */
  387.         private static final String END = "END OF TEC MAP";

  388.         /** String for the epoch of a TEC map. */
  389.         private static final String EPOCH = "EPOCH OF CURRENT MAP";

  390.         /** Index of label in data lines. */
  391.         private static final int LABEL_START = 60;

  392.         /** Kilometers to meters conversion factor. */
  393.         private static final double KM_TO_M = 1000.0;

  394.         /** Header of the IONEX file. */
  395.         private IONEXHeader header;

  396.         /** List of TEC Maps. */
  397.         private List<TECMap> maps;

  398.         @Override
  399.         public boolean stillAcceptsData() {
  400.             return true;
  401.         }

  402.         @Override
  403.         public void loadData(final InputStream input, final String name)
  404.             throws IOException {

  405.             maps = new ArrayList<>();

  406.             // Open stream and parse data
  407.             int   lineNumber = 0;
  408.             String line      = null;
  409.             try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
  410.                  BufferedReader    br  = new BufferedReader(isr)) {

  411.                 // Placeholders for parsed data
  412.                 int               nbOfMaps    = 1;
  413.                 int               exponent    = -1;
  414.                 double            baseRadius  = Double.NaN;
  415.                 double            hIon        = Double.NaN;
  416.                 boolean           mappingF    = false;
  417.                 boolean           inTEC       = false;
  418.                 double[]          latitudes   = null;
  419.                 double[]          longitudes  = null;
  420.                 AbsoluteDate      firstEpoch  = null;
  421.                 AbsoluteDate      lastEpoch   = null;
  422.                 AbsoluteDate      epoch       = firstEpoch;
  423.                 ArrayList<Double> values      = new ArrayList<>();

  424.                 for (line = br.readLine(); line != null; line = br.readLine()) {
  425.                     ++lineNumber;
  426.                     if (line.length() > LABEL_START) {
  427.                         switch (line.substring(LABEL_START).trim()) {
  428.                             case "EPOCH OF FIRST MAP" :
  429.                                 firstEpoch = parseDate(line);
  430.                                 break;
  431.                             case "EPOCH OF LAST MAP" :
  432.                                 lastEpoch = parseDate(line);
  433.                                 break;
  434.                             case "INTERVAL" :
  435.                                 // ignored;
  436.                                 break;
  437.                             case "# OF MAPS IN FILE" :
  438.                                 nbOfMaps = parseInt(line, 2, 4);
  439.                                 break;
  440.                             case "BASE RADIUS" :
  441.                                 // Value is in kilometers
  442.                                 baseRadius = parseDouble(line, 2, 6) * KM_TO_M;
  443.                                 break;
  444.                             case "MAPPING FUNCTION" :
  445.                                 mappingF = !parseString(line, 2, 4).equals("NONE");
  446.                                 break;
  447.                             case "EXPONENT" :
  448.                                 exponent = parseInt(line, 4, 2);
  449.                                 break;
  450.                             case "HGT1 / HGT2 / DHGT" :
  451.                                 if (parseDouble(line, 17, 3) == 0.0) {
  452.                                     // Value is in kilometers
  453.                                     hIon = parseDouble(line, 3, 5) * KM_TO_M;
  454.                                 }
  455.                                 break;
  456.                             case "LAT1 / LAT2 / DLAT" :
  457.                                 latitudes = parseCoordinate(line);
  458.                                 break;
  459.                             case "LON1 / LON2 / DLON" :
  460.                                 longitudes = parseCoordinate(line);
  461.                                 break;
  462.                             case "END OF HEADER" :
  463.                                 // Check that latitude and longitude boundaries were found
  464.                                 if (latitudes == null || longitudes == null) {
  465.                                     throw new OrekitException(OrekitMessages.NO_LATITUDE_LONGITUDE_BOUNDARIES_IN_IONEX_HEADER, name);
  466.                                 }
  467.                                 // Check that first and last epochs were found
  468.                                 if (firstEpoch == null || lastEpoch == null) {
  469.                                     throw new OrekitException(OrekitMessages.NO_EPOCH_IN_IONEX_HEADER, name);
  470.                                 }
  471.                                 // At the end of the header, we build the IONEXHeader object
  472.                                 header = new IONEXHeader(nbOfMaps, baseRadius, hIon, mappingF);
  473.                                 break;
  474.                             case "START OF TEC MAP" :
  475.                                 inTEC = true;
  476.                                 break;
  477.                             case END :
  478.                                 final BilinearInterpolatingFunction tec = interpolateTEC(values, exponent, latitudes, longitudes);
  479.                                 final TECMap map = new TECMap(epoch, tec);
  480.                                 maps.add(map);
  481.                                 // Reset parameters
  482.                                 inTEC  = false;
  483.                                 values = new ArrayList<>();
  484.                                 epoch  = null;
  485.                                 break;
  486.                             default :
  487.                                 if (inTEC) {
  488.                                     // Date
  489.                                     if (line.endsWith(EPOCH)) {
  490.                                         epoch = parseDate(line);
  491.                                     }
  492.                                     // Fill TEC values list
  493.                                     if (!line.endsWith("LAT/LON1/LON2/DLON/H") &&
  494.                                         !line.endsWith(END) &&
  495.                                         !line.endsWith(EPOCH)) {
  496.                                         for (int fieldStart = 0; fieldStart < line.length(); fieldStart += 5) {
  497.                                             values.add((double) Integer.parseInt(line.substring(fieldStart, fieldStart + 5).trim()));
  498.                                         }
  499.                                     }
  500.                                 }
  501.                                 break;
  502.                         }
  503.                     } else {
  504.                         if (inTEC) {
  505.                             // Here, we are parsing the last line of TEC data for a given latitude
  506.                             // The size of this line is lower than 60.
  507.                             for (int fieldStart = 0; fieldStart < line.length(); fieldStart += 5) {
  508.                                 values.add((double) Integer.parseInt(line.substring(fieldStart, fieldStart + 5).trim()));
  509.                             }
  510.                         }
  511.                     }

  512.                 }

  513.                 // Close the stream after reading
  514.                 input.close();

  515.             } catch (NumberFormatException nfe) {
  516.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  517.                                           lineNumber, name, line);
  518.             }

  519.             // TEC map
  520.             if (maps.size() != header.getTECMapsNumer()) {
  521.                 throw new OrekitException(OrekitMessages.INCONSISTENT_NUMBER_OF_TEC_MAPS_IN_FILE,
  522.                                           maps.size(), header.getTECMapsNumer());
  523.             }
  524.             TECMap previous = null;
  525.             for (TECMap current : maps) {
  526.                 if (previous != null) {
  527.                     tecMap.addValidBetween(new TECMapPair(previous, current,
  528.                                                           header.getEarthRadius(), header.getHIon(), header.isMappingFunction()),
  529.                                            previous.date, current.date);
  530.                 }
  531.                 previous = current;
  532.             }

  533.             names = names.isEmpty() ? name : (names + ", " + name);

  534.         }

  535.         /** Extract a string from a line.
  536.          * @param line to parse
  537.          * @param start start index of the string
  538.          * @param length length of the string
  539.          * @return parsed string
  540.          */
  541.         private String parseString(final String line, final int start, final int length) {
  542.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  543.         }

  544.         /** Extract an integer from a line.
  545.          * @param line to parse
  546.          * @param start start index of the integer
  547.          * @param length length of the integer
  548.          * @return parsed integer
  549.          */
  550.         private int parseInt(final String line, final int start, final int length) {
  551.             return Integer.parseInt(parseString(line, start, length));
  552.         }

  553.         /** Extract a double from a line.
  554.          * @param line to parse
  555.          * @param start start index of the real
  556.          * @param length length of the real
  557.          * @return parsed real
  558.          */
  559.         private double parseDouble(final String line, final int start, final int length) {
  560.             return Double.parseDouble(parseString(line, start, length));
  561.         }

  562.         /** Extract a date from a parsed line.
  563.          * @param line to parse
  564.          * @return an absolute date
  565.          */
  566.         private AbsoluteDate parseDate(final String line) {
  567.             return new AbsoluteDate(parseInt(line, 0, 6),
  568.                                     parseInt(line, 6, 6),
  569.                                     parseInt(line, 12, 6),
  570.                                     parseInt(line, 18, 6),
  571.                                     parseInt(line, 24, 6),
  572.                                     parseDouble(line, 30, 13),
  573.                                     utc);
  574.         }

  575.         /** Build the coordinate array from a parsed line.
  576.          * @param line to parse
  577.          * @return an array of coordinates in radians
  578.          */
  579.         private double[] parseCoordinate(final String line) {
  580.             final double a = parseDouble(line, 2, 6);
  581.             final double b = parseDouble(line, 8, 6);
  582.             final double c = parseDouble(line, 14, 6);
  583.             final double[] coordinate = new double[((int) FastMath.abs((a - b) / c)) + 1];
  584.             int i = 0;
  585.             for (double cor = FastMath.min(a, b); cor <= FastMath.max(a, b); cor += FastMath.abs(c)) {
  586.                 coordinate[i] = FastMath.toRadians(cor);
  587.                 i++;
  588.             }
  589.             return coordinate;
  590.         }

  591.         /** Interpolate the TEC in latitude and longitude.
  592.          * @param exponent exponent defining the unit of the values listed in the data blocks
  593.          * @param values TEC values
  594.          * @param latitudes array containing the different latitudes in radians
  595.          * @param longitudes array containing the different latitudes in radians
  596.          * @return the interpolating TEC functiopn in TECUnits
  597.          */
  598.         private BilinearInterpolatingFunction interpolateTEC(final ArrayList<Double> values, final double exponent,
  599.                                                              final double[] latitudes, final double[] longitudes) {
  600.             // Array dimensions
  601.             final int dimLat = latitudes.length;
  602.             final int dimLon = longitudes.length;

  603.             // Build the array of TEC data
  604.             final double factor = FastMath.pow(10.0, exponent);
  605.             final double[][] fvalTEC = new double[dimLat][dimLon];
  606.             int index = dimLon * dimLat;
  607.             for (int x = 0; x < dimLat; x++) {
  608.                 for (int y = dimLon - 1; y >= 0; y--) {
  609.                     index = index - 1;
  610.                     fvalTEC[x][y] = values.get(index) * factor;
  611.                 }
  612.             }

  613.             // Build Bilinear Interpolation function
  614.             return new BilinearInterpolatingFunction(latitudes, longitudes, fvalTEC);

  615.         }

  616.     }

  617.     /**
  618.      * Container for IONEX data.
  619.      * <p>
  620.      * The TEC contained in the map is previously interpolated
  621.      * according to the latitude and the longitude given by the user.
  622.      * </p>
  623.      */
  624.     private static class TECMap {

  625.         /** Date of the TEC Map. */
  626.         private AbsoluteDate date;

  627.         /** Interpolated TEC [TECUnits]. */
  628.         private BilinearInterpolatingFunction tec;

  629.         /**
  630.          * Constructor.
  631.          * @param date date of the TEC map
  632.          * @param tec interpolated tec
  633.          */
  634.         TECMap(final AbsoluteDate date, final BilinearInterpolatingFunction tec) {
  635.             this.date = date;
  636.             this.tec  = tec;
  637.         }

  638.     }

  639.     /** Container for a consecutive pair of TEC maps.
  640.      * @since 12.0
  641.      */
  642.     private static class TECMapPair {

  643.         /** First snapshot. */
  644.         private final TECMap first;

  645.         /** Second snapshot. */
  646.         private final TECMap second;

  647.         /** Mean earth radius [m]. */
  648.         private double r0;

  649.         /** Height of the ionospheric single layer [m]. */
  650.         private double h;

  651.         /** Flag for mapping function computation. */
  652.         private boolean mapping;

  653.         /** Simple constructor.
  654.          * @param first first snapshot
  655.          * @param second second snapshot
  656.          * @param r0 mean Earth radius
  657.          * @param h height of the ionospheric layer
  658.          * @param mapping flag for mapping computation
  659.          */
  660.         TECMapPair(final TECMap first, final TECMap second,
  661.                    final double r0, final double h, final boolean mapping) {
  662.             this.first   = first;
  663.             this.second  = second;
  664.             this.r0      = r0;
  665.             this.h       = h;
  666.             this.mapping = mapping;
  667.         }

  668.         /** Get TEC at pierce point.
  669.          * @param date date
  670.          * @param ipp Ionospheric Pierce Point
  671.          * @return TEC
  672.          */
  673.         public double getTEC(final AbsoluteDate date, final GeodeticPoint ipp) {
  674.             // Get the TEC values at the two closest dates
  675.             final AbsoluteDate t1   = first.date;
  676.             final double       tec1 = first.tec.value(ipp.getLatitude(), ipp.getLongitude());
  677.             final AbsoluteDate t2   = second.date;
  678.             final double       tec2 = second.tec.value(ipp.getLatitude(), ipp.getLongitude());
  679.             final double       dt   = t2.durationFrom(t1);

  680.             // Perform temporal interpolation (Ref, Eq. 2)
  681.             return (t2.durationFrom(date) / dt) * tec1 + (date.durationFrom(t1) / dt) * tec2;

  682.         }

  683.         /** Get TEC at pierce point.
  684.          * @param date date
  685.          * @param ipp Ionospheric Pierce Point
  686.          * @param <T> type of the field elements
  687.          * @return TEC
  688.          */
  689.         public <T extends CalculusFieldElement<T>> T getTEC(final FieldAbsoluteDate<T> date, final GeodeticPoint ipp) {

  690.             // Get the TEC values at the two closest dates
  691.             final AbsoluteDate t1   = first.date;
  692.             final double       tec1 = first.tec.value(ipp.getLatitude(), ipp.getLongitude());
  693.             final AbsoluteDate t2   = second.date;
  694.             final double       tec2 = second.tec.value(ipp.getLatitude(), ipp.getLongitude());
  695.             final double       dt   = t2.durationFrom(t1);

  696.             // Perform temporal interpolation (Ref, Eq. 2)
  697.             return date.durationFrom(t2).negate().divide(dt).multiply(tec1).add(date.durationFrom(t1).divide(dt).multiply(tec2));

  698.         }

  699.     }

  700.     /** Container for IONEX header. */
  701.     private static class IONEXHeader {

  702.         /** Number of maps contained in the IONEX file. */
  703.         private int nbOfMaps;

  704.         /** Mean earth radius [m]. */
  705.         private double baseRadius;

  706.         /** Height of the ionospheric single layer [m]. */
  707.         private double hIon;

  708.         /** Flag for mapping function adopted for TEC determination. */
  709.         private boolean isMappingFunction;

  710.         /**
  711.          * Constructor.
  712.          * @param nbOfMaps number of TEC maps contained in the file
  713.          * @param baseRadius mean earth radius in meters
  714.          * @param hIon height of the ionospheric single layer in meters
  715.          * @param mappingFunction flag for mapping function adopted for TEC determination
  716.          */
  717.         IONEXHeader(final int nbOfMaps,
  718.                     final double baseRadius, final double hIon,
  719.                     final boolean mappingFunction) {
  720.             this.nbOfMaps          = nbOfMaps;
  721.             this.baseRadius        = baseRadius;
  722.             this.hIon              = hIon;
  723.             this.isMappingFunction = mappingFunction;
  724.         }

  725.         /**
  726.          * Get the number of TEC maps contained in the file.
  727.          * @return the number of TEC maps
  728.          */
  729.         public int getTECMapsNumer() {
  730.             return nbOfMaps;
  731.         }

  732.         /**
  733.          * Get the mean earth radius in meters.
  734.          * @return the mean earth radius
  735.          */
  736.         public double getEarthRadius() {
  737.             return baseRadius;
  738.         }

  739.         /**
  740.          * Get the height of the ionospheric single layer in meters.
  741.          * @return the height of the ionospheric single layer
  742.          */
  743.         public double getHIon() {
  744.             return hIon;
  745.         }

  746.         /**
  747.          * Get the mapping function flag.
  748.          * @return false if mapping function computation is needed
  749.          */
  750.         public boolean isMappingFunction() {
  751.             return isMappingFunction;
  752.         }

  753.     }

  754. }