1 /* Copyright 2002-2012 Space Applications Services
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.files.sp3;
18
19 import java.io.BufferedReader;
20 import java.io.IOException;
21 import java.io.Reader;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Scanner;
28 import java.util.function.Function;
29 import java.util.regex.Pattern;
30
31 import org.hipparchus.exception.LocalizedCoreFormats;
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.DataSource;
37 import org.orekit.errors.OrekitException;
38 import org.orekit.errors.OrekitIllegalArgumentException;
39 import org.orekit.errors.OrekitMessages;
40 import org.orekit.files.general.EphemerisFileParser;
41 import org.orekit.frames.Frame;
42 import org.orekit.gnss.IGSUtils;
43 import org.orekit.gnss.TimeSystem;
44 import org.orekit.time.AbsoluteDate;
45 import org.orekit.time.DateComponents;
46 import org.orekit.time.DateTimeComponents;
47 import org.orekit.time.TimeComponents;
48 import org.orekit.time.TimeScale;
49 import org.orekit.time.TimeScales;
50 import org.orekit.utils.CartesianDerivativesFilter;
51 import org.orekit.utils.Constants;
52
53 /** A parser for the SP3 orbit file format. It supports all formats from sp3-a
54 * to sp3-d.
55 * <p>
56 * <b>Note:</b> this parser is thread-safe, so calling {@link #parse} from
57 * different threads is allowed.
58 * </p>
59 * @see <a href="https://files.igs.org/pub/data/format/sp3_docu.txt">SP3-a file format</a>
60 * @see <a href="https://files.igs.org/pub/data/format/sp3c.txt">SP3-c file format</a>
61 * @see <a href="https://files.igs.org/pub/data/format/sp3d.pdf">SP3-d file format</a>
62 * @author Thomas Neidhart
63 * @author Luc Maisonobe
64 */
65 public class SP3Parser implements EphemerisFileParser<SP3> {
66
67 /** Default number of samples to use when interpolating SP3 coordinates. */
68 public static final int DEFAULT_INTERPOLATION_SAMPLES = 7;
69
70 /** Spaces delimiters. */
71 private static final String SPACES = "\\s+";
72
73 /** Standard gravitational parameter in m³/s². */
74 private final double mu;
75
76 /** Number of data points to use in interpolation. */
77 private final int interpolationSamples;
78
79 /** Mapping from frame identifier in the file to a {@link Frame}. */
80 private final Function<? super String, ? extends Frame> frameBuilder;
81
82 /** Set of time scales. */
83 private final TimeScales timeScales;
84
85 /**
86 * Create an SP3 parser using default values.
87 *
88 * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
89 * It also uses a {@link #DEFAULT_INTERPOLATION_SAMPLES default number of samples} to
90 * interpolate coordinates.
91 *
92 * @see #SP3Parser(double, int, Function)
93 * @see IGSUtils#guessFrame(String)
94 */
95 @DefaultDataContext
96 public SP3Parser() {
97 this(Constants.EIGEN5C_EARTH_MU, DEFAULT_INTERPOLATION_SAMPLES, IGSUtils::guessFrame);
98 }
99
100 /**
101 * Create an SP3 parser and specify the extra information needed to create a {@link
102 * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
103 *
104 * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
105 *
106 * @param mu is the standard gravitational parameter to use for
107 * creating {@link org.orekit.orbits.Orbit Orbits} from
108 * the ephemeris data. See {@link Constants}.
109 * @param interpolationSamples is the number of samples to use when interpolating.
110 * @param frameBuilder is a function that can construct a frame from an SP3
111 * coordinate system string. The coordinate system can be
112 * any 5 character string e.g. ITR92, IGb08.
113 * @see #SP3Parser(double, int, Function, TimeScales)
114 * @see IGSUtils#guessFrame(String)
115 */
116 @DefaultDataContext
117 public SP3Parser(final double mu,
118 final int interpolationSamples,
119 final Function<? super String, ? extends Frame> frameBuilder) {
120 this(mu, interpolationSamples, frameBuilder,
121 DataContext.getDefault().getTimeScales());
122 }
123
124 /**
125 * Create an SP3 parser and specify the extra information needed to create a {@link
126 * org.orekit.propagation.Propagator Propagator} from the ephemeris data.
127 *
128 * @param mu is the standard gravitational parameter to use for
129 * creating {@link org.orekit.orbits.Orbit Orbits} from
130 * the ephemeris data. See {@link Constants}.
131 * @param interpolationSamples is the number of samples to use when interpolating.
132 * @param frameBuilder is a function that can construct a frame from an SP3
133 * coordinate system string. The coordinate system can be
134 * @param timeScales the set of time scales used for parsing dates.
135 * @since 10.1
136 */
137 public SP3Parser(final double mu,
138 final int interpolationSamples,
139 final Function<? super String, ? extends Frame> frameBuilder,
140 final TimeScales timeScales) {
141 this.mu = mu;
142 this.interpolationSamples = interpolationSamples;
143 this.frameBuilder = frameBuilder;
144 this.timeScales = timeScales;
145 }
146
147 @Override
148 public SP3 parse(final DataSource source) {
149
150 try (Reader reader = source.getOpener().openReaderOnce();
151 BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {
152
153 if (br == null) {
154 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
155 }
156
157 // initialize internal data structures
158 final ParseInfo pi = new ParseInfo(source.getName(), this);
159
160 int lineNumber = 0;
161 Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.HEADER_VERSION);
162 nextLine:
163 for (String line = br.readLine(); line != null; line = br.readLine()) {
164 ++lineNumber;
165 for (final LineParser candidate : candidateParsers) {
166 if (candidate.canHandle(line)) {
167 try {
168 candidate.parse(line, pi);
169 if (pi.done) {
170 break nextLine;
171 }
172 candidateParsers = candidate.allowedNext();
173 continue nextLine;
174 } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
175 throw new OrekitException(e,
176 OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
177 lineNumber, pi.fileName, line);
178 }
179 }
180 }
181
182 // no parsers found for this line
183 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
184 lineNumber, pi.fileName, line);
185
186 }
187
188 pi.file.validate(true, pi.fileName);
189 return pi.file;
190
191 } catch (IOException ioe) {
192 throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
193 }
194
195 }
196
197 /** Transient data used for parsing a sp3 file. The data is kept in a
198 * separate data structure to make the parser thread-safe.
199 * <p><b>Note</b>: The class intentionally does not provide accessor
200 * methods, as it is only used internally for parsing a SP3 file.</p>
201 */
202 private static class ParseInfo {
203
204 /** File name.
205 * @since 12.0
206 */
207 private final String fileName;
208
209 /** Englobing parser. */
210 private final SP3Parser parser;
211
212 /** The corresponding SP3File object. */
213 private SP3 file;
214
215 /** The latest epoch as read from the SP3 file. */
216 private AbsoluteDate latestEpoch;
217
218 /** The latest position as read from the SP3 file. */
219 private Vector3D latestPosition;
220
221 /** The latest position accuracy as read from the SP3 file.
222 * @since 12.0
223 */
224 private Vector3D latestPositionAccuracy;
225
226 /** The latest clock value as read from the SP3 file. */
227 private double latestClock;
228
229 /** The latest clock value as read from the SP3 file.
230 * @since 12.0
231 */
232 private double latestClockAccuracy;
233
234 /** The latest clock event flag as read from the SP3 file.
235 * @since 12.0
236 */
237 private boolean latestClockEvent;
238
239 /** The latest clock prediction flag as read from the SP3 file.
240 * @since 12.0
241 */
242 private boolean latestClockPrediction;
243
244 /** The latest orbit maneuver event flag as read from the SP3 file.
245 * @since 12.0
246 */
247 private boolean latestOrbitManeuverEvent;
248
249 /** The latest orbit prediction flag as read from the SP3 file.
250 * @since 12.0
251 */
252 private boolean latestOrbitPrediction;
253
254 /** Indicates if the SP3 file has velocity entries. */
255 private boolean hasVelocityEntries;
256
257 /** The timescale used in the SP3 file. */
258 private TimeScale timeScale;
259
260 /** Date and time of the file. */
261 private DateTimeComponents epoch;
262
263 /** The number of satellites as contained in the SP3 file. */
264 private int maxSatellites;
265
266 /** The number of satellites accuracies already seen. */
267 private int nbAccuracies;
268
269 /** End Of File reached indicator. */
270 private boolean done;
271
272 /** Create a new {@link ParseInfo} object.
273 * @param fileName file name
274 * @param parser englobing parser
275 */
276 protected ParseInfo(final String fileName,
277 final SP3Parser parser) {
278 this.fileName = fileName;
279 this.parser = parser;
280 latestEpoch = null;
281 latestPosition = null;
282 latestClock = 0.0;
283 hasVelocityEntries = false;
284 epoch = DateTimeComponents.JULIAN_EPOCH;
285 timeScale = parser.timeScales.getGPS();
286 maxSatellites = 0;
287 nbAccuracies = 0;
288 done = false;
289 }
290 }
291
292 /** Parsers for specific lines. */
293 private enum LineParser {
294
295 /** Parser for version, epoch, data used and agency information. */
296 HEADER_VERSION("^#[a-z].*") {
297
298 /** {@inheritDoc} */
299 @Override
300 public void parse(final String line, final ParseInfo pi) {
301 try (Scanner s1 = new Scanner(line);
302 Scanner s2 = s1.useDelimiter(SPACES);
303 Scanner scanner = s2.useLocale(Locale.US)) {
304 scanner.skip("#");
305 final String v = scanner.next();
306
307 final SP3Header header = new SP3Header();
308 header.setVersion(v.substring(0, 1).toLowerCase().charAt(0));
309
310 pi.hasVelocityEntries = "V".equals(v.substring(1, 2));
311 header.setFilter(pi.hasVelocityEntries ?
312 CartesianDerivativesFilter.USE_PV :
313 CartesianDerivativesFilter.USE_P);
314
315 final int year = Integer.parseInt(v.substring(2));
316 final int month = scanner.nextInt();
317 final int day = scanner.nextInt();
318 final int hour = scanner.nextInt();
319 final int minute = scanner.nextInt();
320 final double second = scanner.nextDouble();
321
322 pi.epoch = new DateTimeComponents(year, month, day,
323 hour, minute, second);
324
325 final int numEpochs = scanner.nextInt();
326 header.setNumberOfEpochs(numEpochs);
327
328 // data used indicator
329 final String fullSpec = scanner.next();
330 final List<DataUsed> dataUsed = new ArrayList<>();
331 for (final String specifier : fullSpec.split("\\+")) {
332 dataUsed.add(DataUsed.parse(specifier, pi.fileName, header.getVersion()));
333 }
334 header.setDataUsed(dataUsed);
335
336 header.setCoordinateSystem(scanner.next());
337 header.setOrbitTypeKey(scanner.next());
338 header.setAgency(scanner.hasNext() ? scanner.next() : "");
339 pi.file = new SP3(header, pi.parser.mu, pi.parser.interpolationSamples,
340 pi.parser.frameBuilder.apply(header.getCoordinateSystem()));
341 }
342 }
343
344 /** {@inheritDoc} */
345 @Override
346 public Iterable<LineParser> allowedNext() {
347 return Collections.singleton(HEADER_DATE_TIME_REFERENCE);
348 }
349
350 },
351
352 /** Parser for additional date/time references in gps/julian day notation. */
353 HEADER_DATE_TIME_REFERENCE("^##.*") {
354
355 /** {@inheritDoc} */
356 @Override
357 public void parse(final String line, final ParseInfo pi) {
358 try (Scanner s1 = new Scanner(line);
359 Scanner s2 = s1.useDelimiter(SPACES);
360 Scanner scanner = s2.useLocale(Locale.US)) {
361 scanner.skip("##");
362
363 // gps week
364 pi.file.getHeader().setGpsWeek(scanner.nextInt());
365 // seconds of week
366 pi.file.getHeader().setSecondsOfWeek(scanner.nextDouble());
367 // epoch interval
368 pi.file.getHeader().setEpochInterval(scanner.nextDouble());
369 // modified julian day
370 pi.file.getHeader().setModifiedJulianDay(scanner.nextInt());
371 // day fraction
372 pi.file.getHeader().setDayFraction(scanner.nextDouble());
373 }
374 }
375
376 /** {@inheritDoc} */
377 @Override
378 public Iterable<LineParser> allowedNext() {
379 return Collections.singleton(HEADER_SAT_IDS);
380 }
381
382 },
383
384 /** Parser for satellites identifiers. */
385 HEADER_SAT_IDS("^\\+ .*") {
386
387 /** {@inheritDoc} */
388 @Override
389 public void parse(final String line, final ParseInfo pi) {
390
391 if (pi.maxSatellites == 0) {
392 // this is the first ids line, it also contains the number of satellites
393 pi.maxSatellites = Integer.parseInt(line.substring(3, 6).trim());
394 }
395
396 final int lineLength = line.length();
397 int count = pi.file.getSatelliteCount();
398 int startIdx = 9;
399 while (count++ < pi.maxSatellites && (startIdx + 3) <= lineLength) {
400 final String satId = line.substring(startIdx, startIdx + 3).trim();
401 if (!satId.isEmpty()) {
402 pi.file.addSatellite(satId);
403 }
404 startIdx += 3;
405 }
406 }
407
408 /** {@inheritDoc} */
409 @Override
410 public Iterable<LineParser> allowedNext() {
411 return Arrays.asList(HEADER_SAT_IDS, HEADER_ACCURACY);
412 }
413
414 },
415
416 /** Parser for general accuracy information for each satellite. */
417 HEADER_ACCURACY("^\\+\\+.*") {
418
419 /** {@inheritDoc} */
420 @Override
421 public void parse(final String line, final ParseInfo pi) {
422 final int lineLength = line.length();
423 int startIdx = 9;
424 while (pi.nbAccuracies < pi.maxSatellites && (startIdx + 3) <= lineLength) {
425 final String sub = line.substring(startIdx, startIdx + 3).trim();
426 if (!sub.isEmpty()) {
427 final int exponent = Integer.parseInt(sub);
428 // the accuracy is calculated as 2**exp (in mm)
429 pi.file.getHeader().setAccuracy(pi.nbAccuracies++,
430 SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
431 SP3Utils.POS_VEL_BASE_ACCURACY,
432 exponent));
433 }
434 startIdx += 3;
435 }
436 }
437
438 /** {@inheritDoc} */
439 @Override
440 public Iterable<LineParser> allowedNext() {
441 return Arrays.asList(HEADER_ACCURACY, HEADER_TIME_SYSTEM);
442 }
443
444 },
445
446 /** Parser for time system. */
447 HEADER_TIME_SYSTEM("^%c.*") {
448
449 /** {@inheritDoc} */
450 @Override
451 public void parse(final String line, final ParseInfo pi) {
452
453 if (pi.file.getHeader().getType() == null) {
454 // this the first custom fields line, the only one really used
455 pi.file.getHeader().setType(SP3FileType.parse(line.substring(3, 5).trim()));
456
457 // now identify the time system in use
458 final String tsStr = line.substring(9, 12).trim();
459 final TimeSystem ts;
460 if (tsStr.equalsIgnoreCase("ccc")) {
461 ts = TimeSystem.GPS;
462 } else {
463 ts = TimeSystem.parseTimeSystem(tsStr);
464 }
465 pi.file.getHeader().setTimeSystem(ts);
466 pi.timeScale = ts.getTimeScale(pi.parser.timeScales);
467
468 // now we know the time scale used, we can set the file epoch
469 pi.file.getHeader().setEpoch(new AbsoluteDate(pi.epoch, pi.timeScale));
470 }
471
472 }
473
474 /** {@inheritDoc} */
475 @Override
476 public Iterable<LineParser> allowedNext() {
477 return Arrays.asList(HEADER_TIME_SYSTEM, HEADER_STANDARD_DEVIATIONS);
478 }
479
480 },
481
482 /** Parser for standard deviations of position/velocity/clock components. */
483 HEADER_STANDARD_DEVIATIONS("^%f.*") {
484
485 /** {@inheritDoc} */
486 @Override
487 public void parse(final String line, final ParseInfo pi) {
488 final double posVelBase = Double.parseDouble(line.substring(3, 13).trim());
489 if (posVelBase != 0.0) {
490 // (mm or 10⁻⁴ mm/s)
491 pi.file.getHeader().setPosVelBase(posVelBase);
492 }
493
494 final double clockBase = Double.parseDouble(line.substring(14, 26).trim());
495 if (clockBase != 0.0) {
496 // (ps or 10⁻⁴ ps/s)
497 pi.file.getHeader().setClockBase(clockBase);
498 }
499 }
500
501 /** {@inheritDoc} */
502 @Override
503 public Iterable<LineParser> allowedNext() {
504 return Arrays.asList(HEADER_STANDARD_DEVIATIONS, HEADER_CUSTOM_PARAMETERS);
505 }
506
507 },
508
509 /** Parser for custom parameters. */
510 HEADER_CUSTOM_PARAMETERS("^%i.*") {
511
512 /** {@inheritDoc} */
513 @Override
514 public void parse(final String line, final ParseInfo pi) {
515 // ignore additional custom parameters
516 }
517
518 /** {@inheritDoc} */
519 @Override
520 public Iterable<LineParser> allowedNext() {
521 return Arrays.asList(HEADER_CUSTOM_PARAMETERS, HEADER_COMMENTS);
522 }
523
524 },
525
526 /** Parser for comments. */
527 HEADER_COMMENTS("^[%]?/\\*.*|") {
528
529 /** {@inheritDoc} */
530 @Override
531 public void parse(final String line, final ParseInfo pi) {
532 pi.file.getHeader().addComment(line.substring(line.indexOf('*') + 1).trim());
533 }
534
535 /** {@inheritDoc} */
536 @Override
537 public Iterable<LineParser> allowedNext() {
538 return Arrays.asList(HEADER_COMMENTS, DATA_EPOCH);
539 }
540
541 },
542
543 /** Parser for epoch. */
544 DATA_EPOCH("^\\* .*") {
545
546 /** {@inheritDoc} */
547 @Override
548 public void parse(final String line, final ParseInfo pi) {
549 final int year;
550 final int month;
551 final int day;
552 final int hour;
553 final int minute;
554 final double second;
555 try (Scanner s1 = new Scanner(line);
556 Scanner s2 = s1.useDelimiter(SPACES);
557 Scanner scanner = s2.useLocale(Locale.US)) {
558 scanner.skip("\\*");
559 year = scanner.nextInt();
560 month = scanner.nextInt();
561 day = scanner.nextInt();
562 hour = scanner.nextInt();
563 minute = scanner.nextInt();
564 second = scanner.nextDouble();
565 }
566
567 // some SP3 files have weird epochs as in the following three examples, where
568 // the middle dates are wrong
569 //
570 // * 2016 7 6 16 58 0.00000000
571 // PL51 11872.234459 3316.551981 101.400098 999999.999999
572 // VL51 8054.606014 -27076.640110 -53372.762255 999999.999999
573 // * 2016 7 6 16 60 0.00000000
574 // PL51 11948.228978 2986.113872 -538.901114 999999.999999
575 // VL51 4605.419303 -27972.588048 -53316.820671 999999.999999
576 // * 2016 7 6 17 2 0.00000000
577 // PL51 11982.652569 2645.786926 -1177.549463 999999.999999
578 // VL51 1128.248622 -28724.293303 -53097.358387 999999.999999
579 //
580 // * 2016 7 6 23 58 0.00000000
581 // PL51 3215.382310 -7958.586164 8812.395707
582 // VL51 -18058.659942 -45834.335707 -34496.540437
583 // * 2016 7 7 24 0 0.00000000
584 // PL51 2989.229334 -8494.421415 8385.068555
585 // VL51 -19617.027447 -43444.824985 -36706.159070
586 // * 2016 7 7 0 2 0.00000000
587 // PL51 2744.983592 -9000.639164 7931.904779
588 // VL51 -21072.925764 -40899.633288 -38801.567078
589 //
590 // * 2021 12 31 0 0 0.00000000
591 // PL51 6578.459330 5572.231927 -8703.502054
592 // VL51 -5356.007694 -48869.881161 -35036.676469
593 // * 2022 1 0 0 2 0.00000000
594 // PL51 6499.035610 4978.263048 -9110.135595
595 // VL51 -7881.633197 -50092.564035 -32717.740919
596 // * 2022 1 0 0 4 0.00000000
597 // PL51 6389.313975 4370.794537 -9488.314264
598 // VL51 -10403.797055 -51119.231402 -30295.421935
599 // In the first case, the date should really be 2016 7 6 17 0 0.00000000,
600 // i.e as the minutes field overflows, the hours field should be incremented
601 // In the second case, the date should really be 2016 7 7 0 0 0.00000000,
602 // i.e. as the hours field overflows, the day field should be kept as is
603 // we cannot be sure how carry was managed when these bogus files were written
604 // so we try different options, incrementing or not previous field, and selecting
605 // the closest one to expected date
606 // In the third case, there are two different errors: the date is globally
607 // shifted to the left by one character, and the day is 0 instead of 1
608 DateComponents dc = day == 0 ?
609 new DateComponents(new DateComponents(year, month, 1), -1) :
610 new DateComponents(year, month, day);
611 final List<AbsoluteDate> candidates = new ArrayList<>();
612 int h = hour;
613 int m = minute;
614 double s = second;
615 if (s >= 60.0) {
616 s -= 60;
617 addCandidate(candidates, dc, h, m, s, pi.timeScale);
618 m++;
619 }
620 if (m > 59) {
621 m = 0;
622 addCandidate(candidates, dc, h, m, s, pi.timeScale);
623 h++;
624 }
625 if (h > 23) {
626 h = 0;
627 addCandidate(candidates, dc, h, m, s, pi.timeScale);
628 dc = new DateComponents(dc, 1);
629 }
630 addCandidate(candidates, dc, h, m, s, pi.timeScale);
631 final AbsoluteDate expected = pi.latestEpoch == null ?
632 pi.file.getHeader().getEpoch() :
633 pi.latestEpoch.shiftedBy(pi.file.getHeader().getEpochInterval());
634 pi.latestEpoch = null;
635 for (final AbsoluteDate candidate : candidates) {
636 if (FastMath.abs(candidate.durationFrom(expected)) < 0.01 * pi.file.getHeader().getEpochInterval()) {
637 pi.latestEpoch = candidate;
638 }
639 }
640 if (pi.latestEpoch == null) {
641 // no date recognized, just parse again the initial fields
642 // in order to generate again an exception
643 pi.latestEpoch = new AbsoluteDate(year, month, day, hour, minute, second, pi.timeScale);
644 }
645
646 }
647
648 /** Add an epoch candidate to a list.
649 * @param candidates list of candidates
650 * @param dc date components
651 * @param hour hour number from 0 to 23
652 * @param minute minute number from 0 to 59
653 * @param second second number from 0.0 to 60.0 (excluded)
654 * @param timeScale time scale
655 * @since 11.1.1
656 */
657 private void addCandidate(final List<AbsoluteDate> candidates, final DateComponents dc,
658 final int hour, final int minute, final double second,
659 final TimeScale timeScale) {
660 try {
661 candidates.add(new AbsoluteDate(dc, new TimeComponents(hour, minute, second), timeScale));
662 } catch (OrekitIllegalArgumentException oiae) {
663 // ignored
664 }
665 }
666
667 /** {@inheritDoc} */
668 @Override
669 public Iterable<LineParser> allowedNext() {
670 return Collections.singleton(DATA_POSITION);
671 }
672
673 },
674
675 /** Parser for position. */
676 DATA_POSITION("^P.*") {
677
678 /** {@inheritDoc} */
679 @Override
680 public void parse(final String line, final ParseInfo pi) {
681 final String satelliteId = line.substring(1, 4).trim();
682
683 if (!pi.file.containsSatellite(satelliteId)) {
684 pi.latestPosition = Vector3D.ZERO;
685 } else {
686
687 final SP3Header header = pi.file.getHeader();
688
689 // the position values are in km and have to be converted to m
690 pi.latestPosition = new Vector3D(SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(4, 18).trim())),
691 SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(18, 32).trim())),
692 SP3Utils.POSITION_UNIT.toSI(Double.parseDouble(line.substring(32, 46).trim())));
693
694 // clock (microsec)
695 final double clockField = line.trim().length() <= 46 ?
696 SP3Utils.DEFAULT_CLOCK_VALUE :
697 Double.parseDouble(line.substring(46, 60).trim());
698 pi.latestClock = FastMath.abs(clockField - SP3Utils.DEFAULT_CLOCK_VALUE) < 1.0e-6 ?
699 Double.NaN : SP3Utils.CLOCK_UNIT.toSI(clockField);
700
701 if (pi.latestPosition.getNorm() > 0) {
702
703 if (line.length() < 69 ||
704 line.substring(61, 63).trim().isEmpty() ||
705 line.substring(64, 66).trim().isEmpty() ||
706 line.substring(67, 69).trim().isEmpty()) {
707 pi.latestPositionAccuracy = null;
708 } else {
709 pi.latestPositionAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
710 header.getPosVelBase(),
711 Integer.parseInt(line.substring(61, 63).trim())),
712 SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
713 header.getPosVelBase(),
714 Integer.parseInt(line.substring(64, 66).trim())),
715 SP3Utils.siAccuracy(SP3Utils.POSITION_ACCURACY_UNIT,
716 header.getPosVelBase(),
717 Integer.parseInt(line.substring(67, 69).trim())));
718 }
719
720 if (line.length() < 73 || line.substring(70, 73).trim().isEmpty()) {
721 pi.latestClockAccuracy = Double.NaN;
722 } else {
723 pi.latestClockAccuracy = SP3Utils.siAccuracy(SP3Utils.CLOCK_ACCURACY_UNIT,
724 header.getClockBase(),
725 Integer.parseInt(line.substring(70, 73).trim()));
726 }
727
728 pi.latestClockEvent = line.length() >= 75 && line.charAt(74) == 'E';
729 pi.latestClockPrediction = line.length() >= 76 && line.charAt(75) == 'P';
730 pi.latestOrbitManeuverEvent = line.length() >= 79 && line.charAt(78) == 'M';
731 pi.latestOrbitPrediction = line.length() >= 80 && line.charAt(79) == 'P';
732
733 if (!pi.hasVelocityEntries) {
734 final SP3Coordinate coord =
735 new SP3Coordinate(pi.latestEpoch,
736 pi.latestPosition, pi.latestPositionAccuracy,
737 Vector3D.ZERO, null,
738 pi.latestClock, pi.latestClockAccuracy,
739 0.0, Double.NaN,
740 pi.latestClockEvent, pi.latestClockPrediction,
741 pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
742 pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
743 }
744 }
745 }
746 }
747
748 /** {@inheritDoc} */
749 @Override
750 public Iterable<LineParser> allowedNext() {
751 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_POSITION_CORRELATION, DATA_VELOCITY, EOF);
752 }
753
754 },
755
756 /** Parser for position correlation. */
757 DATA_POSITION_CORRELATION("^EP.*") {
758
759 /** {@inheritDoc} */
760 @Override
761 public void parse(final String line, final ParseInfo pi) {
762 // ignored for now
763 }
764
765 /** {@inheritDoc} */
766 @Override
767 public Iterable<LineParser> allowedNext() {
768 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY, EOF);
769 }
770
771 },
772
773 /** Parser for velocity. */
774 DATA_VELOCITY("^V.*") {
775
776 /** {@inheritDoc} */
777 @Override
778 public void parse(final String line, final ParseInfo pi) {
779 final String satelliteId = line.substring(1, 4).trim();
780
781 if (pi.file.containsSatellite(satelliteId) && pi.latestPosition.getNorm() > 0) {
782
783 final SP3Header header = pi.file.getHeader();
784
785 // the velocity values are in dm/s and have to be converted to m/s
786 final Vector3D velocity = new Vector3D(SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(4, 18).trim())),
787 SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(18, 32).trim())),
788 SP3Utils.VELOCITY_UNIT.toSI(Double.parseDouble(line.substring(32, 46).trim())));
789
790 // clock rate in file is 1e-4 us / s
791 final double clockRateField = line.trim().length() <= 46 ?
792 SP3Utils.DEFAULT_CLOCK_RATE_VALUE :
793 Double.parseDouble(line.substring(46, 60).trim());
794 final double clockRateChange = FastMath.abs(clockRateField - SP3Utils.DEFAULT_CLOCK_RATE_VALUE) < 1.0e-6 ?
795 Double.NaN : SP3Utils.CLOCK_RATE_UNIT.toSI(clockRateField);
796
797 final Vector3D velocityAccuracy;
798 if (line.length() < 69 ||
799 line.substring(61, 63).trim().isEmpty() ||
800 line.substring(64, 66).trim().isEmpty() ||
801 line.substring(67, 69).trim().isEmpty()) {
802 velocityAccuracy = null;
803 } else {
804 velocityAccuracy = new Vector3D(SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
805 header.getPosVelBase(),
806 Integer.parseInt(line.substring(61, 63).trim())),
807 SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
808 header.getPosVelBase(),
809 Integer.parseInt(line.substring(64, 66).trim())),
810 SP3Utils.siAccuracy(SP3Utils.VELOCITY_ACCURACY_UNIT,
811 header.getPosVelBase(),
812 Integer.parseInt(line.substring(67, 69).trim())));
813 }
814
815 final double clockRateAccuracy;
816 if (line.length() < 73 || line.substring(70, 73).trim().isEmpty()) {
817 clockRateAccuracy = Double.NaN;
818 } else {
819 clockRateAccuracy = SP3Utils.siAccuracy(SP3Utils.CLOCK_RATE_ACCURACY_UNIT,
820 header.getClockBase(),
821 Integer.parseInt(line.substring(70, 73).trim()));
822 }
823
824 final SP3Coordinate coord =
825 new SP3Coordinate(pi.latestEpoch,
826 pi.latestPosition, pi.latestPositionAccuracy,
827 velocity, velocityAccuracy,
828 pi.latestClock, pi.latestClockAccuracy,
829 clockRateChange, clockRateAccuracy,
830 pi.latestClockEvent, pi.latestClockPrediction,
831 pi.latestOrbitManeuverEvent, pi.latestOrbitPrediction);
832 pi.file.getEphemeris(satelliteId).addCoordinate(coord, header.getEpochInterval());
833 }
834 }
835
836 /** {@inheritDoc} */
837 @Override
838 public Iterable<LineParser> allowedNext() {
839 return Arrays.asList(DATA_EPOCH, DATA_POSITION, DATA_VELOCITY_CORRELATION, EOF);
840 }
841
842 },
843
844 /** Parser for velocity correlation. */
845 DATA_VELOCITY_CORRELATION("^EV.*") {
846
847 /** {@inheritDoc} */
848 @Override
849 public void parse(final String line, final ParseInfo pi) {
850 // ignored for now
851 }
852
853 /** {@inheritDoc} */
854 @Override
855 public Iterable<LineParser> allowedNext() {
856 return Arrays.asList(DATA_EPOCH, DATA_POSITION, EOF);
857 }
858
859 },
860
861 /** Parser for End Of File marker. */
862 EOF("^[eE][oO][fF]\\s*$") {
863
864 /** {@inheritDoc} */
865 @Override
866 public void parse(final String line, final ParseInfo pi) {
867 pi.done = true;
868 }
869
870 /** {@inheritDoc} */
871 @Override
872 public Iterable<LineParser> allowedNext() {
873 return Collections.singleton(EOF);
874 }
875
876 };
877
878 /** Pattern for identifying line. */
879 private final Pattern pattern;
880
881 /** Simple constructor.
882 * @param lineRegexp regular expression for identifying line
883 */
884 LineParser(final String lineRegexp) {
885 pattern = Pattern.compile(lineRegexp);
886 }
887
888 /** Parse a line.
889 * @param line line to parse
890 * @param pi holder for transient data
891 */
892 public abstract void parse(String line, ParseInfo pi);
893
894 /** Get the allowed parsers for next line.
895 * @return allowed parsers for next line
896 */
897 public abstract Iterable<LineParser> allowedNext();
898
899 /** Check if parser can handle line.
900 * @param line line to parse
901 * @return true if parser can handle the specified line
902 */
903 public boolean canHandle(final String line) {
904 return pattern.matcher(line).matches();
905 }
906
907 }
908
909 }