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