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 }