AntexLoader.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.gnss.antenna;

  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.HashMap;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Optional;
  30. import java.util.regex.Pattern;

  31. import org.hipparchus.exception.DummyLocalizable;
  32. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  33. import org.hipparchus.util.FastMath;
  34. import org.orekit.annotation.DefaultDataContext;
  35. import org.orekit.data.DataContext;
  36. import org.orekit.data.DataLoader;
  37. import org.orekit.data.DataProvidersManager;
  38. import org.orekit.data.DataSource;
  39. import org.orekit.errors.OrekitException;
  40. import org.orekit.errors.OrekitIllegalArgumentException;
  41. import org.orekit.errors.OrekitMessages;
  42. import org.orekit.gnss.PredefinedGnssSignal;
  43. import org.orekit.gnss.RadioWave;
  44. import org.orekit.gnss.SatInSystem;
  45. import org.orekit.gnss.SatelliteSystem;
  46. import org.orekit.time.AbsoluteDate;
  47. import org.orekit.time.TimeScale;
  48. import org.orekit.utils.TimeSpanMap;

  49. /**
  50.  * Factory for GNSS antennas (both receiver and satellite).
  51.  * <p>
  52.  * The factory creates antennas by parsing an
  53.  * <a href="ftp://www.igs.org/pub/station/general/antex14.txt">ANTEX</a> file.
  54.  * </p>
  55.  *
  56.  * @author Luc Maisonobe
  57.  * @since 9.2
  58.  */
  59. public class AntexLoader {

  60.     /** Default supported files name pattern for antex files. */
  61.     public static final String DEFAULT_ANTEX_SUPPORTED_NAMES = "^\\w{5}(?:_\\d{4})?\\.atx$";

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

  64.     /** Satellites antennas. */
  65.     private final List<TimeSpanMap<SatelliteAntenna>> satellitesAntennas;

  66.     /** Receivers antennas. */
  67.     private final List<ReceiverAntenna> receiversAntennas;

  68.     /** GPS time scale. */
  69.     private final TimeScale gps;

  70.     /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
  71.      * default data context}.
  72.      *
  73.      * @param supportedNames regular expression for supported files names
  74.      * @see #AntexLoader(String, DataProvidersManager, TimeScale)
  75.      */
  76.     @DefaultDataContext
  77.     public AntexLoader(final String supportedNames) {
  78.         this(supportedNames, DataContext.getDefault().getDataProvidersManager(),
  79.                 DataContext.getDefault().getTimeScales().getGPS());
  80.     }

  81.     /**
  82.      * Construct a loader by specifying a {@link DataProvidersManager}.
  83.      *
  84.      * @param supportedNames regular expression for supported files names
  85.      * @param dataProvidersManager provides access to auxiliary data.
  86.      * @param gps the GPS time scale to use when loading the ANTEX files.
  87.      * @since 10.1
  88.      */
  89.     public AntexLoader(final String supportedNames,
  90.                        final DataProvidersManager dataProvidersManager,
  91.                        final TimeScale gps) {
  92.         this.gps = gps;
  93.         satellitesAntennas = new ArrayList<>();
  94.         receiversAntennas  = new ArrayList<>();
  95.         dataProvidersManager.feed(supportedNames, new Parser());
  96.     }

  97.     /**
  98.      * Construct a loader by specifying the source of ANTEX auxiliary data files.
  99.      *
  100.      * @param source source for the ANTEX data
  101.      * @param gps the GPS time scale to use when loading the ANTEX files.
  102.      * @since 12.0
  103.      */
  104.     public AntexLoader(final DataSource source, final TimeScale gps) {
  105.         try {
  106.             this.gps = gps;
  107.             satellitesAntennas = new ArrayList<>();
  108.             receiversAntennas  = new ArrayList<>();
  109.             try (InputStream         is  = source.getOpener().openStreamOnce();
  110.                  BufferedInputStream bis = new BufferedInputStream(is)) {
  111.                 new Parser().loadData(bis, source.getName());
  112.             }
  113.         } catch (IOException ioe) {
  114.             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  115.         }
  116.     }

  117.     /** Add a satellite antenna.
  118.      * @param antenna satellite antenna to add
  119.      */
  120.     private void addSatelliteAntenna(final SatelliteAntenna antenna) {
  121.         try {
  122.             final TimeSpanMap<SatelliteAntenna> existing = findSatelliteAntenna(antenna.getSatInSystem());
  123.             // this is an update for a satellite antenna, with new time span
  124.             existing.addValidAfter(antenna, antenna.getValidFrom(), false);
  125.         } catch (OrekitException oe) {
  126.             // this is a new satellite antenna
  127.             satellitesAntennas.add(new TimeSpanMap<>(antenna));
  128.         }
  129.     }

  130.     /** Get parsed satellites antennas.
  131.      * @return unmodifiable view of parsed satellites antennas
  132.      */
  133.     public List<TimeSpanMap<SatelliteAntenna>> getSatellitesAntennas() {
  134.         return Collections.unmodifiableList(satellitesAntennas);
  135.     }

  136.     /** Find the time map for a specific satellite antenna.
  137.      * @param satInSystem satellite in system
  138.      * @return time map for the antenna
  139.      */
  140.     public TimeSpanMap<SatelliteAntenna> findSatelliteAntenna(final SatInSystem satInSystem) {
  141.         final Optional<TimeSpanMap<SatelliteAntenna>> existing =
  142.                         satellitesAntennas.
  143.                         stream().
  144.                         filter(m -> m.getFirstSpan().getData().getSatInSystem().equals(satInSystem)).
  145.                         findFirst();
  146.         if (existing.isPresent()) {
  147.             return existing.get();
  148.         } else {
  149.             throw new OrekitException(OrekitMessages.CANNOT_FIND_SATELLITE_IN_SYSTEM,
  150.                                       satInSystem.getPRN(), satInSystem.getSystem());
  151.         }
  152.     }

  153.     /** Add a receiver antenna.
  154.      * @param antenna receiver antenna to add
  155.      */
  156.     private void addReceiverAntenna(final ReceiverAntenna antenna) {
  157.         receiversAntennas.add(antenna);
  158.     }

  159.     /** Get parsed receivers antennas.
  160.      * @return unmodifiable view of parsed receivers antennas
  161.      */
  162.     public List<ReceiverAntenna> getReceiversAntennas() {
  163.         return Collections.unmodifiableList(receiversAntennas);
  164.     }

  165.     /** Parser for antex files.
  166.      * @see <a href="ftp://www.igs.org/pub/station/general/antex14.txt">ANTEX: The Antenna Exchange Format, Version 1.4</a>
  167.      */
  168.     private class Parser implements DataLoader {

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

  171.         /** Supported format version. */
  172.         private static final double FORMAT_VERSION = 1.4;

  173.         /** Phase center eccentricities conversion factor. */
  174.         private static final double MM_TO_M = 0.001;

  175.         /** {@inheritDoc} */
  176.         @Override
  177.         public boolean stillAcceptsData() {
  178.             // we load all antex files we can find
  179.             return true;
  180.         }

  181.         /** {@inheritDoc} */
  182.         @Override
  183.         public void loadData(final InputStream input, final String name)
  184.             throws IOException, OrekitException {

  185.             int                              lineNumber           = 0;
  186.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {

  187.                 // placeholders for parsed data
  188.                 SatInSystem                      satInSystem          = null;
  189.                 String                           antennaType          = null;
  190.                 SatelliteType                    satelliteType        = null;
  191.                 String                           serialNumber         = null;
  192.                 int                              satelliteCode        = -1;
  193.                 String                           cosparID             = null;
  194.                 AbsoluteDate                     validFrom            = AbsoluteDate.PAST_INFINITY;
  195.                 AbsoluteDate                     validUntil           = AbsoluteDate.FUTURE_INFINITY;
  196.                 String                           sinexCode            = null;
  197.                 double                           azimuthStep          = Double.NaN;
  198.                 double                           polarStart           = Double.NaN;
  199.                 double                           polarStop            = Double.NaN;
  200.                 double                           polarStep            = Double.NaN;
  201.                 double[]                         grid1D               = null;
  202.                 double[][]                       grid2D               = null;
  203.                 Vector3D                         eccentricities       = Vector3D.ZERO;
  204.                 int                              nbFrequencies        = -1;
  205.                 PredefinedGnssSignal             predefinedGnssSignal = null;
  206.                 Map<RadioWave, FrequencyPattern> patterns             = null;
  207.                 boolean                          inFrequency          = false;
  208.                 boolean                          inRMS                = false;

  209.                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  210.                     ++lineNumber;
  211.                     switch (line.substring(LABEL_START).trim()) {
  212.                         case "COMMENT" :
  213.                             // nothing to do
  214.                             break;
  215.                         case "ANTEX VERSION / SYST" :
  216.                             if (FastMath.abs(parseDouble(line, 0, 8) - FORMAT_VERSION) > 0.001) {
  217.                                 throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT, name);
  218.                             }
  219.                             // we parse the general setting for satellite system to check for format errors,
  220.                             // but otherwise ignore it
  221.                             SatelliteSystem.parseSatelliteSystem(parseString(line, 20, 1));
  222.                             break;
  223.                         case "PCV TYPE / REFANT" :
  224.                             // TODO
  225.                             break;
  226.                         case "END OF HEADER" :
  227.                             // nothing to do
  228.                             break;
  229.                         case "START OF ANTENNA" :
  230.                             // reset antenna data
  231.                             satInSystem          = null;
  232.                             antennaType          = null;
  233.                             satelliteType        = null;
  234.                             serialNumber         = null;
  235.                             satelliteCode        = -1;
  236.                             cosparID             = null;
  237.                             validFrom            = AbsoluteDate.PAST_INFINITY;
  238.                             validUntil           = AbsoluteDate.FUTURE_INFINITY;
  239.                             sinexCode            = null;
  240.                             azimuthStep          = Double.NaN;
  241.                             polarStart           = Double.NaN;
  242.                             polarStop            = Double.NaN;
  243.                             polarStep            = Double.NaN;
  244.                             grid1D               = null;
  245.                             grid2D               = null;
  246.                             eccentricities       = Vector3D.ZERO;
  247.                             predefinedGnssSignal = null;
  248.                             patterns             = null;
  249.                             inFrequency          = false;
  250.                             inRMS                = false;
  251.                             break;
  252.                         case "TYPE / SERIAL NO" :
  253.                             antennaType = parseString(line, 0, 20);
  254.                             try {
  255.                                 satelliteType = SatelliteType.parseSatelliteType(antennaType);
  256.                                 final String satField = parseString(line, 20, 20);
  257.                                 if (!satField.isEmpty()) {
  258.                                     satInSystem = new SatInSystem(satField);
  259.                                     if (satInSystem.getSystem() == SatelliteSystem.MIXED) {
  260.                                         // MIXED satellite system is not allowed here
  261.                                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  262.                                                                   lineNumber, name, line);
  263.                                     }
  264.                                     satelliteCode = parseInt(line, 41, 9); // we drop the system type
  265.                                     cosparID      = parseString(line, 50, 10);
  266.                                 }
  267.                             } catch (OrekitIllegalArgumentException oiae) {
  268.                                 // this is a receiver antenna, not a satellite antenna
  269.                                 serialNumber = parseString(line, 20, 20);
  270.                             }
  271.                             break;
  272.                         case "METH / BY / # / DATE" :
  273.                             // ignoreds
  274.                             break;
  275.                         case "DAZI" :
  276.                             azimuthStep = FastMath.toRadians(parseDouble(line,  2, 6));
  277.                             break;
  278.                         case "ZEN1 / ZEN2 / DZEN" :
  279.                             polarStart = FastMath.toRadians(parseDouble(line,  2, 6));
  280.                             polarStop  = FastMath.toRadians(parseDouble(line,  8, 6));
  281.                             polarStep  = FastMath.toRadians(parseDouble(line, 14, 6));
  282.                             break;
  283.                         case "# OF FREQUENCIES" :
  284.                             nbFrequencies = parseInt(line, 0, 6);
  285.                             patterns      = new HashMap<>(nbFrequencies);
  286.                             break;
  287.                         case "VALID FROM" :
  288.                             validFrom = new AbsoluteDate(parseInt(line,     0,  6),
  289.                                                          parseInt(line,     6,  6),
  290.                                                          parseInt(line,    12,  6),
  291.                                                          parseInt(line,    18,  6),
  292.                                                          parseInt(line,    24,  6),
  293.                                                          parseDouble(line, 30, 13),
  294.                                                          gps);
  295.                             break;
  296.                         case "VALID UNTIL" :
  297.                             validUntil = new AbsoluteDate(parseInt(line,     0,  6),
  298.                                                           parseInt(line,     6,  6),
  299.                                                           parseInt(line,    12,  6),
  300.                                                           parseInt(line,    18,  6),
  301.                                                           parseInt(line,    24,  6),
  302.                                                           parseDouble(line, 30, 13),
  303.                                                           gps);
  304.                             break;
  305.                         case "SINEX CODE" :
  306.                             sinexCode = parseString(line, 0, 10);
  307.                             break;
  308.                         case "START OF FREQUENCY" :
  309.                             try {
  310.                                 predefinedGnssSignal = PredefinedGnssSignal.valueOf(parseString(line, 3, 3));
  311.                                 grid1D    = new double[1 + (int) FastMath.round((polarStop - polarStart) / polarStep)];
  312.                                 if (azimuthStep > 0.001) {
  313.                                     grid2D = new double[1 + (int) FastMath.round(2 * FastMath.PI / azimuthStep)][grid1D.length];
  314.                                 }
  315.                             } catch (IllegalArgumentException iae) {
  316.                                 throw new OrekitException(iae, OrekitMessages.UNKNOWN_RINEX_FREQUENCY,
  317.                                                           parseString(line, 3, 3), name, lineNumber);
  318.                             }
  319.                             inFrequency = true;
  320.                             break;
  321.                         case "NORTH / EAST / UP" :
  322.                             if (!inRMS) {
  323.                                 eccentricities = new Vector3D(parseDouble(line,  0, 10) * MM_TO_M,
  324.                                                               parseDouble(line, 10, 10) * MM_TO_M,
  325.                                                               parseDouble(line, 20, 10) * MM_TO_M);
  326.                             }
  327.                             break;
  328.                         case "END OF FREQUENCY" : {
  329.                             final String endFrequency = parseString(line, 3, 3);
  330.                             if (predefinedGnssSignal == null || !predefinedGnssSignal.toString().equals(endFrequency)) {
  331.                                 throw new OrekitException(OrekitMessages.MISMATCHED_FREQUENCIES,
  332.                                                           name, lineNumber, predefinedGnssSignal, endFrequency);

  333.                             }

  334.                             // Check if the number of frequencies has been parsed
  335.                             if (patterns == null) {
  336.                                 // null object, an OrekitException is thrown
  337.                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  338.                                                           lineNumber, name, line);
  339.                             }

  340.                             final PhaseCenterVariationFunction phaseCenterVariation;
  341.                             if (grid2D == null) {
  342.                                 double max = 0;
  343.                                 for (final double v : grid1D) {
  344.                                     max = FastMath.max(max, FastMath.abs(v));
  345.                                 }
  346.                                 if (max == 0.0) {
  347.                                     // there are no known variations for this pattern
  348.                                     phaseCenterVariation = (polarAngle, azimuthAngle) -> 0.0;
  349.                                 } else {
  350.                                     phaseCenterVariation = new OneDVariation(polarStart, polarStep, grid1D);
  351.                                 }
  352.                             } else {
  353.                                 phaseCenterVariation = new TwoDVariation(polarStart, polarStep, azimuthStep, grid2D);
  354.                             }
  355.                             patterns.put(predefinedGnssSignal, new FrequencyPattern(eccentricities, phaseCenterVariation));
  356.                             predefinedGnssSignal = null;
  357.                             grid1D               = null;
  358.                             grid2D               = null;
  359.                             inFrequency          = false;
  360.                             break;
  361.                         }
  362.                         case "START OF FREQ RMS" :
  363.                             inRMS = true;
  364.                             break;
  365.                         case "END OF FREQ RMS" :
  366.                             inRMS = false;
  367.                             break;
  368.                         case "END OF ANTENNA" :
  369.                             if (satelliteType == null) {
  370.                                 addReceiverAntenna(new ReceiverAntenna(antennaType, sinexCode, patterns, serialNumber));
  371.                             } else {
  372.                                 addSatelliteAntenna(new SatelliteAntenna(antennaType, sinexCode, patterns,
  373.                                                                          satInSystem, satelliteType, satelliteCode,
  374.                                                                          cosparID, validFrom, validUntil));
  375.                             }
  376.                             break;
  377.                         default :
  378.                             if (inFrequency) {
  379.                                 final String[] fields = SEPARATOR.split(line.trim());
  380.                                 if (fields.length != grid1D.length + 1) {
  381.                                     throw new OrekitException(OrekitMessages.WRONG_COLUMNS_NUMBER,
  382.                                                               name, lineNumber, grid1D.length + 1, fields.length);
  383.                                 }
  384.                                 if ("NOAZI".equals(fields[0])) {
  385.                                     // azimuth-independent phase
  386.                                     for (int i = 0; i < grid1D.length; ++i) {
  387.                                         grid1D[i] = Double.parseDouble(fields[i + 1]) * MM_TO_M;
  388.                                     }

  389.                                 } else {
  390.                                     // azimuth-dependent phase
  391.                                     // azimuth is counted from Y/North to X/East in Antex files
  392.                                     // we will interpolate using phase angle in right-handed frame,
  393.                                     // so we have to change azimuth to 90-α and renormalize to have
  394.                                     // a 0° to 360° range with same values at both ends, closing the circle
  395.                                     final double antexAziDeg      = Double.parseDouble(fields[0]);
  396.                                     final double normalizedAziDeg = (antexAziDeg <= 90.0 ? 90.0 : 450.0) - antexAziDeg;
  397.                                     final int k = (int) FastMath.round(FastMath.toRadians(normalizedAziDeg) / azimuthStep);
  398.                                     for (int i = 0; i < grid2D[k].length; ++i) {
  399.                                         grid2D[k][i] = Double.parseDouble(fields[i + 1]) * MM_TO_M;
  400.                                     }
  401.                                     if (k == 0) {
  402.                                         // copy X/East azimuth values to close the circle
  403.                                         System.arraycopy(grid2D[0], 0, grid2D[grid2D.length - 1], 0, grid2D[0].length);
  404.                                     }
  405.                                 }
  406.                             } else if (inRMS) {
  407.                                 // RMS section is ignored (furthermore there are no RMS sections in both igs08.atx and igs14.atx)
  408.                             } else {
  409.                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  410.                                                           lineNumber, name, line);
  411.                             }
  412.                     }
  413.                 }

  414.             } catch (NumberFormatException nfe) {
  415.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  416.                                           lineNumber, name, "tot");
  417.             }

  418.         }

  419.         /** Extract a string from a line.
  420.          * @param line to parse
  421.          * @param start start index of the string
  422.          * @param length length of the string
  423.          * @return parsed string
  424.          */
  425.         private String parseString(final String line, final int start, final int length) {
  426.             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
  427.         }

  428.         /** Extract an integer from a line.
  429.          * @param line to parse
  430.          * @param start start index of the integer
  431.          * @param length length of the integer
  432.          * @return parsed integer
  433.          */
  434.         private int parseInt(final String line, final int start, final int length) {
  435.             return Integer.parseInt(parseString(line, start, length));
  436.         }

  437.         /** Extract a double from a line.
  438.          * @param line to parse
  439.          * @param start start index of the real
  440.          * @param length length of the real
  441.          * @return parsed real
  442.          */
  443.         private double parseDouble(final String line, final int start, final int length) {
  444.             return Double.parseDouble(parseString(line, start, length));
  445.         }

  446.     }

  447. }