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 }