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