GlobalIonosphereMapModel.java

  1. /* Copyright 2002-2024 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 java.util.regex.Pattern;

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

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

  120.     /** Pattern for delimiting regular expressions. */
  121.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  122.     /** Map of interpolable TEC. */
  123.     private TimeSpanMap<TECMapPair> tecMap;

  124.     /** UTC time scale. */
  125.     private final TimeScale utc;

  126.     /** Loaded IONEX files.
  127.      * @since 12.0
  128.      */
  129.     private String names;

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

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

  161.         // Read files
  162.         dataProvidersManager.feed(supportedNames, new Parser());

  163.     }

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

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

  221.     @Override
  222.     public double pathDelay(final SpacecraftState state, final TopocentricFrame baseFrame,
  223.                             final double frequency, final double[] parameters) {

  224.         // Satellite position in body frame
  225.         final Frame    bodyFrame = baseFrame.getParentShape().getBodyFrame();
  226.         final Vector3D satPoint  = state.getPosition(bodyFrame);

  227.         // Elevation in radians
  228.         final double   elevation = bodyFrame.
  229.                                    getStaticTransformTo(baseFrame, state.getDate()).
  230.                                    transformPosition(satPoint).
  231.                                    getDelta();

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

  243.         return 0.0;

  244.     }

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

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

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

  286.         // Elevation in radians
  287.         final T                elevation = bodyFrame.
  288.                                            getStaticTransformTo(baseFrame, state.getDate()).
  289.                                            transformPosition(satPoint).
  290.                                            getDelta();

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

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

  303.     }

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

  317.     @Override
  318.     public List<ParameterDriver> getParametersDrivers() {
  319.         return Collections.emptyList();
  320.     }

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

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

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

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

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

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

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

  386.     }

  387.     /** Parser for IONEX files. */
  388.     private class Parser implements DataLoader {

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

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

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

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

  397.         /** Header of the IONEX file. */
  398.         private IONEXHeader header;

  399.         /** List of TEC Maps. */
  400.         private List<TECMap> maps;

  401.         @Override
  402.         public boolean stillAcceptsData() {
  403.             return true;
  404.         }

  405.         @Override
  406.         public void loadData(final InputStream input, final String name)
  407.             throws IOException {

  408.             maps = new ArrayList<>();

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

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

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

  519.                 }

  520.                 // Close the stream after reading
  521.                 input.close();

  522.             } catch (NumberFormatException nfe) {
  523.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  524.                                           lineNumber, name, line);
  525.             }

  526.             // TEC map
  527.             if (maps.size() != header.getTECMapsNumer()) {
  528.                 throw new OrekitException(OrekitMessages.INCONSISTENT_NUMBER_OF_TEC_MAPS_IN_FILE,
  529.                                           maps.size(), header.getTECMapsNumer());
  530.             }
  531.             TECMap previous = null;
  532.             for (TECMap current : maps) {
  533.                 if (previous != null) {
  534.                     tecMap.addValidBetween(new TECMapPair(previous, current,
  535.                                                           header.getEarthRadius(), header.getHIon(), header.isMappingFunction()),
  536.                                            previous.date, current.date);
  537.                 }
  538.                 previous = current;
  539.             }

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

  541.         }

  542.         /** Extract a string from a line.
  543.          * @param line to parse
  544.          * @param start start index of the string
  545.          * @param length length of the string
  546.          * @return parsed string
  547.          */
  548.         private String parseString(final String line, final int start, final int length) {
  549.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  550.         }

  551.         /** Extract an integer from a line.
  552.          * @param line to parse
  553.          * @param start start index of the integer
  554.          * @param length length of the integer
  555.          * @return parsed integer
  556.          */
  557.         private int parseInt(final String line, final int start, final int length) {
  558.             return Integer.parseInt(parseString(line, start, length));
  559.         }

  560.         /** Extract a double from a line.
  561.          * @param line to parse
  562.          * @param start start index of the real
  563.          * @param length length of the real
  564.          * @return parsed real
  565.          */
  566.         private double parseDouble(final String line, final int start, final int length) {
  567.             return Double.parseDouble(parseString(line, start, length));
  568.         }

  569.         /** Extract a date from a parsed line.
  570.          * @param line to parse
  571.          * @return an absolute date
  572.          */
  573.         private AbsoluteDate parseDate(final String line) {
  574.             return new AbsoluteDate(parseInt(line, 0, 6),
  575.                                     parseInt(line, 6, 6),
  576.                                     parseInt(line, 12, 6),
  577.                                     parseInt(line, 18, 6),
  578.                                     parseInt(line, 24, 6),
  579.                                     parseDouble(line, 30, 13),
  580.                                     utc);
  581.         }

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

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

  610.             // Build the array of TEC data
  611.             final double factor = FastMath.pow(10.0, exponent);
  612.             final double[][] fvalTEC = new double[dimLat][dimLon];
  613.             int index = dimLon * dimLat;
  614.             for (int x = 0; x < dimLat; x++) {
  615.                 for (int y = dimLon - 1; y >= 0; y--) {
  616.                     index = index - 1;
  617.                     fvalTEC[x][y] = values.get(index) * factor;
  618.                 }
  619.             }

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

  622.         }

  623.     }

  624.     /**
  625.      * Container for IONEX data.
  626.      * <p>
  627.      * The TEC contained in the map is previously interpolated
  628.      * according to the latitude and the longitude given by the user.
  629.      * </p>
  630.      */
  631.     private static class TECMap {

  632.         /** Date of the TEC Map. */
  633.         private AbsoluteDate date;

  634.         /** Interpolated TEC [TECUnits]. */
  635.         private BilinearInterpolatingFunction tec;

  636.         /**
  637.          * Constructor.
  638.          * @param date date of the TEC map
  639.          * @param tec interpolated tec
  640.          */
  641.         TECMap(final AbsoluteDate date, final BilinearInterpolatingFunction tec) {
  642.             this.date = date;
  643.             this.tec  = tec;
  644.         }

  645.     }

  646.     /** Container for a consecutive pair of TEC maps.
  647.      * @since 12.0
  648.      */
  649.     private static class TECMapPair {

  650.         /** First snapshot. */
  651.         private final TECMap first;

  652.         /** Second snapshot. */
  653.         private final TECMap second;

  654.         /** Mean earth radius [m]. */
  655.         private double r0;

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

  658.         /** Flag for mapping function computation. */
  659.         private boolean mapping;

  660.         /** Simple constructor.
  661.          * @param first first snapshot
  662.          * @param second second snapshot
  663.          * @param r0 mean Earth radius
  664.          * @param h height of the ionospheric layer
  665.          * @param mapping flag for mapping computation
  666.          */
  667.         TECMapPair(final TECMap first, final TECMap second,
  668.                    final double r0, final double h, final boolean mapping) {
  669.             this.first   = first;
  670.             this.second  = second;
  671.             this.r0      = r0;
  672.             this.h       = h;
  673.             this.mapping = mapping;
  674.         }

  675.         /** Get TEC at pierce point.
  676.          * @param date date
  677.          * @param ipp Ionospheric Pierce Point
  678.          * @return TEC
  679.          */
  680.         public double getTEC(final AbsoluteDate date, final GeodeticPoint ipp) {
  681.             // Get the TEC values at the two closest dates
  682.             final AbsoluteDate t1   = first.date;
  683.             final double       tec1 = first.tec.value(ipp.getLatitude(), ipp.getLongitude());
  684.             final AbsoluteDate t2   = second.date;
  685.             final double       tec2 = second.tec.value(ipp.getLatitude(), ipp.getLongitude());
  686.             final double       dt   = t2.durationFrom(t1);

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

  689.         }

  690.         /** Get TEC at pierce point.
  691.          * @param date date
  692.          * @param ipp Ionospheric Pierce Point
  693.          * @param <T> type of the field elements
  694.          * @return TEC
  695.          */
  696.         public <T extends CalculusFieldElement<T>> T getTEC(final FieldAbsoluteDate<T> date, final GeodeticPoint ipp) {

  697.             // Get the TEC values at the two closest dates
  698.             final AbsoluteDate t1   = first.date;
  699.             final double       tec1 = first.tec.value(ipp.getLatitude(), ipp.getLongitude());
  700.             final AbsoluteDate t2   = second.date;
  701.             final double       tec2 = second.tec.value(ipp.getLatitude(), ipp.getLongitude());
  702.             final double       dt   = t2.durationFrom(t1);

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

  705.         }

  706.     }

  707.     /** Container for IONEX header. */
  708.     private static class IONEXHeader {

  709.         /** Number of maps contained in the IONEX file. */
  710.         private int nbOfMaps;

  711.         /** Mean earth radius [m]. */
  712.         private double baseRadius;

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

  715.         /** Flag for mapping function adopted for TEC determination. */
  716.         private boolean isMappingFunction;

  717.         /**
  718.          * Constructor.
  719.          * @param nbOfMaps number of TEC maps contained in the file
  720.          * @param baseRadius mean earth radius in meters
  721.          * @param hIon height of the ionospheric single layer in meters
  722.          * @param mappingFunction flag for mapping function adopted for TEC determination
  723.          */
  724.         IONEXHeader(final int nbOfMaps,
  725.                     final double baseRadius, final double hIon,
  726.                     final boolean mappingFunction) {
  727.             this.nbOfMaps          = nbOfMaps;
  728.             this.baseRadius        = baseRadius;
  729.             this.hIon              = hIon;
  730.             this.isMappingFunction = mappingFunction;
  731.         }

  732.         /**
  733.          * Get the number of TEC maps contained in the file.
  734.          * @return the number of TEC maps
  735.          */
  736.         public int getTECMapsNumer() {
  737.             return nbOfMaps;
  738.         }

  739.         /**
  740.          * Get the mean earth radius in meters.
  741.          * @return the mean earth radius
  742.          */
  743.         public double getEarthRadius() {
  744.             return baseRadius;
  745.         }

  746.         /**
  747.          * Get the height of the ionospheric single layer in meters.
  748.          * @return the height of the ionospheric single layer
  749.          */
  750.         public double getHIon() {
  751.             return hIon;
  752.         }

  753.         /**
  754.          * Get the mapping function flag.
  755.          * @return false if mapping function computation is needed
  756.          */
  757.         public boolean isMappingFunction() {
  758.             return isMappingFunction;
  759.         }

  760.     }

  761. }