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 }