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