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