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  
19  import java.io.BufferedInputStream;
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.nio.charset.StandardCharsets;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.regex.Pattern;
32  
33  import org.hipparchus.exception.DummyLocalizable;
34  import org.hipparchus.geometry.euclidean.threed.Vector3D;
35  import org.hipparchus.util.FastMath;
36  import org.orekit.annotation.DefaultDataContext;
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.OrekitIllegalArgumentException;
43  import org.orekit.errors.OrekitMessages;
44  import org.orekit.gnss.PredefinedGnssSignal;
45  import org.orekit.gnss.RadioWave;
46  import org.orekit.gnss.SatInSystem;
47  import org.orekit.gnss.SatelliteSystem;
48  import org.orekit.time.AbsoluteDate;
49  import org.orekit.time.TimeScale;
50  import org.orekit.utils.TimeSpanMap;
51  
52  /**
53   * Factory for GNSS antennas (both receiver and satellite).
54   * <p>
55   * The factory creates antennas by parsing an
56   * <a href="ftp://www.igs.org/pub/station/general/antex14.txt">ANTEX</a> file.
57   * </p>
58   *
59   * @author Luc Maisonobe
60   * @since 9.2
61   */
62  public class AntexLoader {
63  
64      /** Default supported files name pattern for antex files. */
65      public static final String DEFAULT_ANTEX_SUPPORTED_NAMES = "^\\w{5}(?:_\\d{4})?\\.atx$";
66  
67      /** Pattern for delimiting regular expressions. */
68      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
69  
70      /** Satellites antennas. */
71      private final List<TimeSpanMap<SatelliteAntenna>> satellitesAntennas;
72  
73      /** Receivers antennas. */
74      private final List<ReceiverAntenna> receiversAntennas;
75  
76      /** GPS time scale. */
77      private final TimeScale gps;
78  
79      /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
80       * default data context}.
81       *
82       * @param supportedNames regular expression for supported files names
83       * @see #AntexLoader(String, DataProvidersManager, TimeScale)
84       */
85      @DefaultDataContext
86      public AntexLoader(final String supportedNames) {
87          this(supportedNames, DataContext.getDefault().getDataProvidersManager(),
88                  DataContext.getDefault().getTimeScales().getGPS());
89      }
90  
91      /**
92       * Construct a loader by specifying a {@link DataProvidersManager}.
93       *
94       * @param supportedNames regular expression for supported files names
95       * @param dataProvidersManager provides access to auxiliary data.
96       * @param gps the GPS time scale to use when loading the ANTEX files.
97       * @since 10.1
98       */
99      public AntexLoader(final String supportedNames,
100                        final DataProvidersManager dataProvidersManager,
101                        final TimeScale gps) {
102         this.gps = gps;
103         satellitesAntennas = new ArrayList<>();
104         receiversAntennas  = new ArrayList<>();
105         dataProvidersManager.feed(supportedNames, new Parser());
106     }
107 
108     /**
109      * Construct a loader by specifying the source of ANTEX auxiliary data files.
110      *
111      * @param source source for the ANTEX data
112      * @param gps the GPS time scale to use when loading the ANTEX files.
113      * @since 12.0
114      */
115     public AntexLoader(final DataSource source, final TimeScale gps) {
116         try {
117             this.gps = gps;
118             satellitesAntennas = new ArrayList<>();
119             receiversAntennas  = new ArrayList<>();
120             try (InputStream         is  = source.getOpener().openStreamOnce();
121                  BufferedInputStream bis = new BufferedInputStream(is)) {
122                 new Parser().loadData(bis, source.getName());
123             }
124         } catch (IOException ioe) {
125             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
126         }
127     }
128 
129     /** Add a satellite antenna.
130      * @param antenna satellite antenna to add
131      */
132     private void addSatelliteAntenna(final SatelliteAntenna antenna) {
133         try {
134             final TimeSpanMap<SatelliteAntenna> existing = findSatelliteAntenna(antenna.getSatInSystem());
135             // this is an update for a satellite antenna, with new time span
136             existing.addValidAfter(antenna, antenna.getValidFrom(), false);
137         } catch (OrekitException oe) {
138             // this is a new satellite antenna
139             satellitesAntennas.add(new TimeSpanMap<>(antenna));
140         }
141     }
142 
143     /** Get parsed satellites antennas.
144      * @return unmodifiable view of parsed satellites antennas
145      */
146     public List<TimeSpanMap<SatelliteAntenna>> getSatellitesAntennas() {
147         return Collections.unmodifiableList(satellitesAntennas);
148     }
149 
150     /** Find the time map for a specific satellite antenna.
151      * @param satInSystem satellite in system
152      * @return time map for the antenna
153      */
154     public TimeSpanMap<SatelliteAntenna> findSatelliteAntenna(final SatInSystem satInSystem) {
155         final Optional<TimeSpanMap<SatelliteAntenna>> existing =
156                         satellitesAntennas.
157                         stream().
158                         filter(m -> m.getFirstSpan().getData().getSatInSystem().equals(satInSystem)).
159                         findFirst();
160         if (existing.isPresent()) {
161             return existing.get();
162         } else {
163             throw new OrekitException(OrekitMessages.CANNOT_FIND_SATELLITE_IN_SYSTEM,
164                                       satInSystem.getPRN(), satInSystem.getSystem());
165         }
166     }
167 
168     /** Add a receiver antenna.
169      * @param antenna receiver antenna to add
170      */
171     private void addReceiverAntenna(final ReceiverAntenna antenna) {
172         receiversAntennas.add(antenna);
173     }
174 
175     /** Get parsed receivers antennas.
176      * @return unmodifiable view of parsed receivers antennas
177      */
178     public List<ReceiverAntenna> getReceiversAntennas() {
179         return Collections.unmodifiableList(receiversAntennas);
180     }
181 
182     /** Parser for antex files.
183      * @see <a href="ftp://www.igs.org/pub/station/general/antex14.txt">ANTEX: The Antenna Exchange Format, Version 1.4</a>
184      */
185     private class Parser implements DataLoader {
186 
187         /** Index of label in data lines. */
188         private static final int LABEL_START = 60;
189 
190         /** Supported format version. */
191         private static final double FORMAT_VERSION = 1.4;
192 
193         /** Phase center eccentricities conversion factor. */
194         private static final double MM_TO_M = 0.001;
195 
196         /** {@inheritDoc} */
197         @Override
198         public boolean stillAcceptsData() {
199             // we load all antex files we can find
200             return true;
201         }
202 
203         /** {@inheritDoc} */
204         @Override
205         public void loadData(final InputStream input, final String name)
206             throws IOException, OrekitException {
207 
208             int                              lineNumber           = 0;
209             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
210 
211                 // placeholders for parsed data
212                 SatInSystem                      satInSystem          = null;
213                 String                           antennaType          = null;
214                 SatelliteType                    satelliteType        = null;
215                 String                           serialNumber         = null;
216                 int                              satelliteCode        = -1;
217                 String                           cosparID             = null;
218                 AbsoluteDate                     validFrom            = AbsoluteDate.PAST_INFINITY;
219                 AbsoluteDate                     validUntil           = AbsoluteDate.FUTURE_INFINITY;
220                 String                           sinexCode            = null;
221                 double                           azimuthStep          = Double.NaN;
222                 double                           polarStart           = Double.NaN;
223                 double                           polarStop            = Double.NaN;
224                 double                           polarStep            = Double.NaN;
225                 double[]                         grid1D               = null;
226                 double[][]                       grid2D               = null;
227                 Vector3D                         eccentricities       = Vector3D.ZERO;
228                 int                              nbFrequencies        = -1;
229                 PredefinedGnssSignal             predefinedGnssSignal = null;
230                 Map<RadioWave, FrequencyPattern> patterns             = null;
231                 boolean                          inFrequency          = false;
232                 boolean                          inRMS                = false;
233 
234                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
235                     ++lineNumber;
236                     switch (line.substring(LABEL_START).trim()) {
237                         case "COMMENT" :
238                             // nothing to do
239                             break;
240                         case "ANTEX VERSION / SYST" :
241                             if (FastMath.abs(parseDouble(line, 0, 8) - FORMAT_VERSION) > 0.001) {
242                                 throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT, name);
243                             }
244                             // we parse the general setting for satellite system to check for format errors,
245                             // but otherwise ignore it
246                             SatelliteSystem.parseSatelliteSystem(parseString(line, 20, 1));
247                             break;
248                         case "PCV TYPE / REFANT" :
249                             // TODO
250                             break;
251                         case "END OF HEADER" :
252                             // nothing to do
253                             break;
254                         case "START OF ANTENNA" :
255                             // reset antenna data
256                             satInSystem          = null;
257                             antennaType          = null;
258                             satelliteType        = null;
259                             serialNumber         = null;
260                             satelliteCode        = -1;
261                             cosparID             = null;
262                             validFrom            = AbsoluteDate.PAST_INFINITY;
263                             validUntil           = AbsoluteDate.FUTURE_INFINITY;
264                             sinexCode            = null;
265                             azimuthStep          = Double.NaN;
266                             polarStart           = Double.NaN;
267                             polarStop            = Double.NaN;
268                             polarStep            = Double.NaN;
269                             grid1D               = null;
270                             grid2D               = null;
271                             eccentricities       = Vector3D.ZERO;
272                             predefinedGnssSignal = null;
273                             patterns             = null;
274                             inFrequency          = false;
275                             inRMS                = false;
276                             break;
277                         case "TYPE / SERIAL NO" :
278                             antennaType = parseString(line, 0, 20);
279                             try {
280                                 satelliteType = SatelliteType.parseSatelliteType(antennaType);
281                                 final String satField = parseString(line, 20, 20);
282                                 if (!satField.isEmpty()) {
283                                     satInSystem = new SatInSystem(satField);
284                                     if (satInSystem.getSystem() == SatelliteSystem.MIXED) {
285                                         // MIXED satellite system is not allowed here
286                                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
287                                                                   lineNumber, name, line);
288                                     }
289                                     satelliteCode = parseInt(line, 41, 9); // we drop the system type
290                                     cosparID      = parseString(line, 50, 10);
291                                 }
292                             } catch (OrekitIllegalArgumentException oiae) {
293                                 // this is a receiver antenna, not a satellite antenna
294                                 serialNumber = parseString(line, 20, 20);
295                             }
296                             break;
297                         case "METH / BY / # / DATE" :
298                             // ignoreds
299                             break;
300                         case "DAZI" :
301                             azimuthStep = FastMath.toRadians(parseDouble(line,  2, 6));
302                             break;
303                         case "ZEN1 / ZEN2 / DZEN" :
304                             polarStart = FastMath.toRadians(parseDouble(line,  2, 6));
305                             polarStop  = FastMath.toRadians(parseDouble(line,  8, 6));
306                             polarStep  = FastMath.toRadians(parseDouble(line, 14, 6));
307                             break;
308                         case "# OF FREQUENCIES" :
309                             nbFrequencies = parseInt(line, 0, 6);
310                             patterns      = new HashMap<>(nbFrequencies);
311                             break;
312                         case "VALID FROM" :
313                             validFrom = new AbsoluteDate(parseInt(line,     0,  6),
314                                                          parseInt(line,     6,  6),
315                                                          parseInt(line,    12,  6),
316                                                          parseInt(line,    18,  6),
317                                                          parseInt(line,    24,  6),
318                                                          parseDouble(line, 30, 13),
319                                                          gps);
320                             break;
321                         case "VALID UNTIL" :
322                             validUntil = new AbsoluteDate(parseInt(line,     0,  6),
323                                                           parseInt(line,     6,  6),
324                                                           parseInt(line,    12,  6),
325                                                           parseInt(line,    18,  6),
326                                                           parseInt(line,    24,  6),
327                                                           parseDouble(line, 30, 13),
328                                                           gps);
329                             break;
330                         case "SINEX CODE" :
331                             sinexCode = parseString(line, 0, 10);
332                             break;
333                         case "START OF FREQUENCY" :
334                             try {
335                                 predefinedGnssSignal = PredefinedGnssSignal.valueOf(parseString(line, 3, 3));
336                                 grid1D    = new double[1 + (int) FastMath.round((polarStop - polarStart) / polarStep)];
337                                 if (azimuthStep > 0.001) {
338                                     grid2D = new double[1 + (int) FastMath.round(2 * FastMath.PI / azimuthStep)][grid1D.length];
339                                 }
340                             } catch (IllegalArgumentException iae) {
341                                 throw new OrekitException(iae, OrekitMessages.UNKNOWN_RINEX_FREQUENCY,
342                                                           parseString(line, 3, 3), name, lineNumber);
343                             }
344                             inFrequency = true;
345                             break;
346                         case "NORTH / EAST / UP" :
347                             if (!inRMS) {
348                                 eccentricities = new Vector3D(parseDouble(line,  0, 10) * MM_TO_M,
349                                                               parseDouble(line, 10, 10) * MM_TO_M,
350                                                               parseDouble(line, 20, 10) * MM_TO_M);
351                             }
352                             break;
353                         case "END OF FREQUENCY" : {
354                             final String endFrequency = parseString(line, 3, 3);
355                             if (predefinedGnssSignal == null || !predefinedGnssSignal.toString().equals(endFrequency)) {
356                                 throw new OrekitException(OrekitMessages.MISMATCHED_FREQUENCIES,
357                                                           name, lineNumber, predefinedGnssSignal, endFrequency);
358 
359                             }
360 
361                             // Check if the number of frequencies has been parsed
362                             if (patterns == null) {
363                                 // null object, an OrekitException is thrown
364                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
365                                                           lineNumber, name, line);
366                             }
367 
368                             final PhaseCenterVariationFunction phaseCenterVariation;
369                             if (grid2D == null) {
370                                 double max = 0;
371                                 for (final double v : grid1D) {
372                                     max = FastMath.max(max, FastMath.abs(v));
373                                 }
374                                 if (max == 0.0) {
375                                     // there are no known variations for this pattern
376                                     phaseCenterVariation = (polarAngle, azimuthAngle) -> 0.0;
377                                 } else {
378                                     phaseCenterVariation = new OneDVariation(polarStart, polarStep, grid1D);
379                                 }
380                             } else {
381                                 phaseCenterVariation = new TwoDVariation(polarStart, polarStep, azimuthStep, grid2D);
382                             }
383                             patterns.put(predefinedGnssSignal, new FrequencyPattern(eccentricities, phaseCenterVariation));
384                             predefinedGnssSignal = null;
385                             grid1D               = null;
386                             grid2D               = null;
387                             inFrequency          = false;
388                             break;
389                         }
390                         case "START OF FREQ RMS" :
391                             inRMS = true;
392                             break;
393                         case "END OF FREQ RMS" :
394                             inRMS = false;
395                             break;
396                         case "END OF ANTENNA" :
397                             if (satelliteType == null) {
398                                 addReceiverAntenna(new ReceiverAntenna(antennaType, sinexCode, patterns, serialNumber));
399                             } else {
400                                 addSatelliteAntenna(new SatelliteAntenna(antennaType, sinexCode, patterns,
401                                                                          satInSystem, satelliteType, satelliteCode,
402                                                                          cosparID, validFrom, validUntil));
403                             }
404                             break;
405                         default :
406                             if (inFrequency) {
407                                 final String[] fields = SEPARATOR.split(line.trim());
408                                 if (fields.length != grid1D.length + 1) {
409                                     throw new OrekitException(OrekitMessages.WRONG_COLUMNS_NUMBER,
410                                                               name, lineNumber, grid1D.length + 1, fields.length);
411                                 }
412                                 if ("NOAZI".equals(fields[0])) {
413                                     // azimuth-independent phase
414                                     for (int i = 0; i < grid1D.length; ++i) {
415                                         grid1D[i] = Double.parseDouble(fields[i + 1]) * MM_TO_M;
416                                     }
417 
418                                 } else {
419                                     // azimuth-dependent phase
420                                     // azimuth is counted from Y/North to X/East in Antex files
421                                     // we will interpolate using phase angle in right-handed frame,
422                                     // so we have to change azimuth to 90-α and renormalize to have
423                                     // a 0° to 360° range with same values at both ends, closing the circle
424                                     final double antexAziDeg      = Double.parseDouble(fields[0]);
425                                     final double normalizedAziDeg = (antexAziDeg <= 90.0 ? 90.0 : 450.0) - antexAziDeg;
426                                     final int k = (int) FastMath.round(FastMath.toRadians(normalizedAziDeg) / azimuthStep);
427                                     for (int i = 0; i < grid2D[k].length; ++i) {
428                                         grid2D[k][i] = Double.parseDouble(fields[i + 1]) * MM_TO_M;
429                                     }
430                                     if (k == 0) {
431                                         // copy X/East azimuth values to close the circle
432                                         System.arraycopy(grid2D[0], 0, grid2D[grid2D.length - 1], 0, grid2D[0].length);
433                                     }
434                                 }
435                             } else if (inRMS) {
436                                 // RMS section is ignored (furthermore there are no RMS sections in both igs08.atx and igs14.atx)
437                             } else {
438                                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
439                                                           lineNumber, name, line);
440                             }
441                     }
442                 }
443 
444             } catch (NumberFormatException nfe) {
445                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
446                                           lineNumber, name, "tot");
447             }
448 
449         }
450 
451         /** Extract a string from a line.
452          * @param line to parse
453          * @param start start index of the string
454          * @param length length of the string
455          * @return parsed string
456          */
457         private String parseString(final String line, final int start, final int length) {
458             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
459         }
460 
461         /** Extract an integer from a line.
462          * @param line to parse
463          * @param start start index of the integer
464          * @param length length of the integer
465          * @return parsed integer
466          */
467         private int parseInt(final String line, final int start, final int length) {
468             return Integer.parseInt(parseString(line, start, length));
469         }
470 
471         /** Extract a double from a line.
472          * @param line to parse
473          * @param start start index of the real
474          * @param length length of the real
475          * @return parsed real
476          */
477         private double parseDouble(final String line, final int start, final int length) {
478             return Double.parseDouble(parseString(line, start, length));
479         }
480 
481     }
482 
483 }