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.clock;
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.nio.file.Files;
25 import java.nio.file.Paths;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.InputMismatchException;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Optional;
32 import java.util.Scanner;
33 import java.util.function.Function;
34 import java.util.regex.Pattern;
35 import java.util.stream.Stream;
36
37 import org.orekit.annotation.DefaultDataContext;
38 import org.orekit.data.DataContext;
39 import org.orekit.errors.OrekitException;
40 import org.orekit.errors.OrekitMessages;
41 import org.orekit.frames.Frame;
42 import org.orekit.gnss.AppliedDCBS;
43 import org.orekit.gnss.AppliedPCVS;
44 import org.orekit.gnss.ObservationType;
45 import org.orekit.gnss.SatelliteSystem;
46 import org.orekit.gnss.TimeSystem;
47 import org.orekit.gnss.clock.RinexClock.ClockDataType;
48 import org.orekit.gnss.clock.RinexClock.Receiver;
49 import org.orekit.gnss.clock.RinexClock.ReferenceClock;
50 import org.orekit.time.AbsoluteDate;
51 import org.orekit.time.DateComponents;
52 import org.orekit.time.TimeComponents;
53 import org.orekit.time.TimeScale;
54 import org.orekit.time.TimeScales;
55 import org.orekit.utils.IERSConventions;
56
57 /** A parser for the clock file from the IGS.
58 * This parser handles versions 2.0 to 3.04 of the RINEX clock files.
59 * <p> It is able to manage some mistakes in file writing and format compliance such as wrong date format,
60 * misplaced header blocks or missing information. </p>
61 * <p> A time system should be specified in the file. However, if it is not, default time system will be chosen
62 * regarding the satellite system. If it is mixed or not specified, default time system will be UTC. </p>
63 * <p> Caution, files with missing information in header can lead to wrong data dates and station positions.
64 * It is advised to check the correctness and format compliance of the clock file to be parsed. </p>
65 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock300.txt"> 3.00 clock file format</a>
66 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock302.txt"> 3.02 clock file format</a>
67 * @see <a href="https://files.igs.org/pub/data/format/rinex_clock304.txt"> 3.04 clock file format</a>
68 *
69 * @author Thomas Paulet
70 * @since 11.0
71 */
72 public class RinexClockParser {
73
74 /** Handled clock file format versions. */
75 private static final List<Double> HANDLED_VERSIONS = Arrays.asList(2.00, 3.00, 3.01, 3.02, 3.04);
76
77 /** Pattern for date format yyyy-mm-dd hh:mm. */
78 private static final Pattern DATE_PATTERN_1 = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");;
79
80 /** Pattern for date format yyyymmdd hhmmss zone or YYYYMMDD HHMMSS zone. */
81 private static final Pattern DATE_PATTERN_2 = Pattern.compile("^[0-9]{8}\\s{1,2}[0-9]{6}.*$");
82
83 /** Pattern for date format dd-MONTH-yyyy hh:mm zone or d-MONTH-yyyy hh:mm zone. */
84 private static final Pattern DATE_PATTERN_3 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}.*$");
85
86 /** Pattern for date format dd-MONTH-yy hh:mm zone or d-MONTH-yy hh:mm zone. */
87 private static final Pattern DATE_PATTERN_4 = Pattern.compile("^[0-9]{1,2}-[a-z,A-Z]{3}-[0-9]{2} [0-9]{2}:[0-9]{2}.*$");
88
89 /** Pattern for date format yyyy MONTH dd hh:mm:ss or yyyy MONTH d hh:mm:ss. */
90 private static final Pattern DATE_PATTERN_5 = Pattern.compile("^[0-9]{4} [a-z,A-Z]{3} [0-9]{1,2} [0-9]{2}:[0-9]{2}:[0-9]{2}.*$");
91
92 /** Spaces delimiters. */
93 private static final String SPACES = "\\s+";
94
95 /** SYS string for line browsing stop. */
96 private static final String SYS = "SYS";
97
98 /** One millimeter, in meters. */
99 private static final double MILLIMETER = 1.0e-3;
100
101 /** Mapping from frame identifier in the file to a {@link Frame}. */
102 private final Function<? super String, ? extends Frame> frameBuilder;
103
104 /** Set of time scales. */
105 private final TimeScales timeScales;
106
107 /**
108 * Create an clock file parser using default values.
109 *
110 * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
111 *
112 * @see #RinexClockParser(Function)
113 */
114 @DefaultDataContext
115 public RinexClockParser() {
116 this(RinexClockParser::guessFrame);
117 }
118
119 /**
120 * Create a clock file parser and specify the frame builder.
121 *
122 * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
123 *
124 * @param frameBuilder is a function that can construct a frame from a clock file
125 * coordinate system string. The coordinate system can be
126 * any 5 character string e.g. ITR92, IGb08.
127 * @see #RinexClockParser(Function, TimeScales)
128 */
129 @DefaultDataContext
130 public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder) {
131 this(frameBuilder, DataContext.getDefault().getTimeScales());
132 }
133
134 /** Constructor, build the IGS clock file parser.
135 * @param frameBuilder is a function that can construct a frame from a clock file
136 * coordinate system string. The coordinate system can be
137 * any 5 character string e.g. ITR92, IGb08.
138 * @param timeScales the set of time scales used for parsing dates.
139 */
140 public RinexClockParser(final Function<? super String, ? extends Frame> frameBuilder,
141 final TimeScales timeScales) {
142
143 this.frameBuilder = frameBuilder;
144 this.timeScales = timeScales;
145 }
146
147 /**
148 * Default string to {@link Frame} conversion for {@link #CLockFileParser()}.
149 *
150 * <p>This method uses the {@link DataContext#getDefault() default data context}.
151 *
152 * @param name of the frame.
153 * @return by default, return ITRF based on 2010 conventions,
154 * with tidal effects considered during EOP interpolation.
155 * <p>If String matches to other already recorded frames, it will return the corresponding frame.</p>
156 * Already embedded frames are:
157 * <p> - ITRF96
158 */
159 @DefaultDataContext
160 private static Frame guessFrame(final String name) {
161 if (name.equals("ITRF96")) {
162 return DataContext.getDefault().getFrames()
163 .getITRF(IERSConventions.IERS_1996, false);
164 } else {
165 return DataContext.getDefault().getFrames()
166 .getITRF(IERSConventions.IERS_2010, false);
167 }
168 }
169
170 /**
171 * Parse an IGS clock file from an input stream using the UTF-8 charset.
172 *
173 * <p> This method creates a {@link BufferedReader} from the stream and as such this
174 * method may read more data than necessary from {@code stream} and the additional
175 * data will be lost. The other parse methods do not have this issue.
176 *
177 * @param stream to read the IGS clock file from
178 * @return a parsed IGS clock file
179 * @throws IOException if {@code stream} throws one
180 * @see #parse(String)
181 * @see #parse(BufferedReader, String)
182 */
183 public RinexClock parse(final InputStream stream) throws IOException {
184 try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
185 return parse(reader, stream.toString());
186 }
187 }
188
189 /**
190 * Parse an IGS clock file from a file on the local file system.
191 * @param fileName file name
192 * @return a parsed IGS clock file
193 * @throws IOException if one is thrown while opening or reading from {@code fileName}
194 * @see #parse(InputStream)
195 * @see #parse(BufferedReader, String)
196 */
197 public RinexClock parse(final String fileName) throws IOException {
198 try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
199 StandardCharsets.UTF_8)) {
200 return parse(reader, fileName);
201 }
202 }
203
204 /**
205 * Parse an IGS clock file from a stream.
206 * @param reader containing the clock file
207 * @param fileName file name
208 * @return a parsed IGS clock file
209 * @throws IOException if {@code reader} throws one
210 * @see #parse(InputStream)
211 * @see #parse(String)
212 */
213 public RinexClock parse(final BufferedReader reader,
214 final String fileName) throws IOException {
215
216 // initialize internal data structures
217 final ParseInfo pi = new ParseInfo();
218
219 int lineNumber = 0;
220 Stream<LineParser> candidateParsers = Stream.of(LineParser.HEADER_VERSION);
221 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
222 ++lineNumber;
223 final String l = line;
224 final Optional<LineParser> selected = candidateParsers.filter(p -> p.canHandle(l)).findFirst();
225 if (selected.isPresent()) {
226 try {
227 selected.get().parse(line, pi);
228 } catch (StringIndexOutOfBoundsException | NumberFormatException | InputMismatchException e) {
229 throw new OrekitException(e,
230 OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
231 lineNumber, fileName, line);
232 }
233 candidateParsers = selected.get().allowedNext();
234 } else {
235 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
236 lineNumber, fileName, line);
237 }
238 }
239
240 return pi.file;
241
242 }
243
244 /** Transient data used for parsing a clock file. */
245 private class ParseInfo {
246
247 /** Set of time scales for parsing dates. */
248 private final TimeScales timeScales;
249
250 /** The corresponding clock file object. */
251 private RinexClock file;
252
253 /** Current satellite system for observation type parsing. */
254 private SatelliteSystem currentSatelliteSystem;
255
256 /** Current start date for reference clocks. */
257 private AbsoluteDate referenceClockStartDate;
258
259 /** Current end date for reference clocks. */
260 private AbsoluteDate referenceClockEndDate;
261
262 /** Current reference clock list. */
263 private List<ReferenceClock> currentReferenceClocks;
264
265 /** Current clock data type. */
266 private ClockDataType currentDataType;
267
268 /** Current receiver/satellite name. */
269 private String currentName;
270
271 /** Current data date components. */
272 private DateComponents currentDateComponents;
273
274 /** Current data time components. */
275 private TimeComponents currentTimeComponents;
276
277 /** Current data number of data values to follow. */
278 private int currentNumberOfValues;
279
280 /** Current data values. */
281 private double[] currentDataValues;
282
283 /** Constructor, build the ParseInfo object. */
284 protected ParseInfo () {
285 this.timeScales = RinexClockParser.this.timeScales;
286 this.file = new RinexClock(frameBuilder);
287 }
288 }
289
290
291 /** Parsers for specific lines. */
292 private enum LineParser {
293
294 /** Parser for version, file type and satellite system. */
295 HEADER_VERSION("^.+RINEX VERSION / TYPE( )*$") {
296
297 /** {@inheritDoc} */
298 @Override
299 public void parse(final String line, final ParseInfo pi) {
300 try (Scanner s1 = new Scanner(line);
301 Scanner s2 = s1.useDelimiter(SPACES);
302 Scanner scanner = s2.useLocale(Locale.US)) {
303
304 // First element of the line is format version
305 final double version = scanner.nextDouble();
306
307 // Throw exception if format version is not handled
308 if (!HANDLED_VERSIONS.contains(version)) {
309 throw new OrekitException(OrekitMessages.CLOCK_FILE_UNSUPPORTED_VERSION, version);
310 }
311
312 pi.file.setFormatVersion(version);
313
314 // Second element is clock file indicator, not used here
315
316 // Last element is the satellite system, might be missing
317 final String satelliteSystemString = line.substring(40, 45).trim();
318
319 // Check satellite if system is recorded
320 if (!satelliteSystemString.equals("")) {
321 // Record satellite system and default time system in clock file object
322 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(satelliteSystemString);
323 pi.file.setSatelliteSystem(satelliteSystem);
324 pi.file.setTimeScale(satelliteSystem.getDefaultTimeSystem(pi.timeScales));
325 }
326 // Set time scale to UTC by default
327 if (pi.file.getTimeScale() == null) {
328 pi.file.setTimeScale(pi.timeScales.getUTC());
329 }
330 }
331 }
332
333 },
334
335 /** Parser for generating program and emiting agency. */
336 HEADER_PROGRAM("^.+PGM / RUN BY / DATE( )*$") {
337
338 /** {@inheritDoc} */
339 @Override
340 public void parse(final String line, final ParseInfo pi) {
341
342 // First element of the name of the generating program
343 final String programName = line.substring(0, 20).trim();
344 pi.file.setProgramName(programName);
345
346 // Second element is the name of the emiting agency
347 final String agencyName = line.substring(20, 40).trim();
348 pi.file.setAgencyName(agencyName);
349
350 // Third element is date
351 String dateString = "";
352
353 if (pi.file.getFormatVersion() < 3.04) {
354
355 // Date string location before 3.04 format version
356 dateString = line.substring(40, 60);
357
358 } else {
359
360 // Date string location after 3.04 format version
361 dateString = line.substring(42, 65);
362
363 }
364
365 parseDateTimeZone(dateString, pi);
366
367 }
368
369 },
370
371 /** Parser for comments. */
372 HEADER_COMMENT("^.+COMMENT( )*$") {
373
374 /** {@inheritDoc} */
375 @Override
376 public void parse(final String line, final ParseInfo pi) {
377
378 if (pi.file.getFormatVersion() < 3.04) {
379 pi.file.addComment(line.substring(0, 60).trim());
380 } else {
381 pi.file.addComment(line.substring(0, 65).trim());
382 }
383 }
384
385 },
386
387 /** Parser for satellite system and related observation types. */
388 HEADER_SYSTEM_OBS("^[A-Z] .*SYS / # / OBS TYPES( )*$") {
389
390 /** {@inheritDoc} */
391 @Override
392 public void parse(final String line, final ParseInfo pi) {
393 try (Scanner s1 = new Scanner(line);
394 Scanner s2 = s1.useDelimiter(SPACES);
395 Scanner scanner = s2.useLocale(Locale.US)) {
396
397 // First element of the line is satellite system code
398 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(scanner.next());
399 pi.currentSatelliteSystem = satelliteSystem;
400
401 // Second element is the number of different observation types
402 scanner.nextInt();
403
404 // Parse all observation types
405 String currentObsType = scanner.next();
406 while (!currentObsType.equals(SYS)) {
407 final ObservationType obsType = ObservationType.valueOf(currentObsType);
408 pi.file.addSystemObservationType(satelliteSystem, obsType);
409 currentObsType = scanner.next();
410 }
411 }
412 }
413
414 },
415
416 /** Parser for continuation of satellite system and related observation types. */
417 HEADER_SYSTEM_OBS_CONTINUATION("^ .*SYS / # / OBS TYPES( )*$") {
418
419 /** {@inheritDoc} */
420 @Override
421 public void parse(final String line, final ParseInfo pi) {
422 try (Scanner s1 = new Scanner(line);
423 Scanner s2 = s1.useDelimiter(SPACES);
424 Scanner scanner = s2.useLocale(Locale.US)) {
425
426 // This is a continuation line, there are only observation types
427 // Parse all observation types
428 String currentObsType = scanner.next();
429 while (!currentObsType.equals(SYS)) {
430 final ObservationType obsType = ObservationType.valueOf(currentObsType);
431 pi.file.addSystemObservationType(pi.currentSatelliteSystem, obsType);
432 currentObsType = scanner.next();
433 }
434 }
435 }
436
437 },
438
439 /** Parser for data time system. */
440 HEADER_TIME_SYSTEM("^.+TIME SYSTEM ID( )*$") {
441
442 /** {@inheritDoc} */
443 @Override
444 public void parse(final String line, final ParseInfo pi) {
445 try (Scanner s1 = new Scanner(line);
446 Scanner s2 = s1.useDelimiter(SPACES);
447 Scanner scanner = s2.useLocale(Locale.US)) {
448
449 // Only element is the time system code
450 final TimeSystem timeSystem = TimeSystem.parseTimeSystem(scanner.next());
451 final TimeScale timeScale = timeSystem.getTimeScale(pi.timeScales);
452 pi.file.setTimeSystem(timeSystem);
453 pi.file.setTimeScale(timeScale);
454 }
455 }
456
457 },
458
459 /** Parser for leap seconds. */
460 HEADER_LEAP_SECONDS("^.+LEAP SECONDS( )*$") {
461
462 /** {@inheritDoc} */
463 @Override
464 public void parse(final String line, final ParseInfo pi) {
465 try (Scanner s1 = new Scanner(line);
466 Scanner s2 = s1.useDelimiter(SPACES);
467 Scanner scanner = s2.useLocale(Locale.US)) {
468
469 // Only element is the number of leap seconds
470 final int numberOfLeapSeconds = scanner.nextInt();
471 pi.file.setNumberOfLeapSeconds(numberOfLeapSeconds);
472 }
473 }
474
475 },
476
477 /** Parser for leap seconds GNSS. */
478 HEADER_LEAP_SECONDS_GNSS("^.+LEAP SECONDS GNSS( )*$") {
479
480 /** {@inheritDoc} */
481 @Override
482 public void parse(final String line, final ParseInfo pi) {
483 try (Scanner s1 = new Scanner(line);
484 Scanner s2 = s1.useDelimiter(SPACES);
485 Scanner scanner = s2.useLocale(Locale.US)) {
486
487 // Only element is the number of leap seconds GNSS
488 final int numberOfLeapSecondsGNSS = scanner.nextInt();
489 pi.file.setNumberOfLeapSecondsGNSS(numberOfLeapSecondsGNSS);
490 }
491 }
492
493 },
494
495 /** Parser for applied differencial code bias corrections. */
496 HEADER_DCBS("^.+SYS / DCBS APPLIED( )*$") {
497
498 /** {@inheritDoc} */
499 @Override
500 public void parse(final String line, final ParseInfo pi) {
501
502 // First element is the related satellite system
503 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(line.substring(0, 1));
504
505 // Second element is the program name
506 final String progDCBS = line.substring(2, 20).trim();
507
508 // Third element is the source of the corrections
509 String sourceDCBS = "";
510 if (pi.file.getFormatVersion() < 3.04) {
511 sourceDCBS = line.substring(19, 60).trim();
512 } else {
513 sourceDCBS = line.substring(22, 65).trim();
514 }
515
516 // Check if sought fields were not actually blanks
517 if (!progDCBS.equals("")) {
518 pi.file.addAppliedDCBS(new AppliedDCBS(satelliteSystem, progDCBS, sourceDCBS));
519 }
520 }
521
522 },
523
524 /** Parser for applied phase center variation corrections. */
525 HEADER_PCVS("^.+SYS / PCVS APPLIED( )*$") {
526
527 /** {@inheritDoc} */
528 @Override
529 public void parse(final String line, final ParseInfo pi) {
530
531 // First element is the related satellite system
532 final SatelliteSystem satelliteSystem = SatelliteSystem.parseSatelliteSystem(line.substring(0, 1));
533
534 // Second element is the program name
535 final String progPCVS = line.substring(2, 20).trim();
536
537 // Third element is the source of the corrections
538 String sourcePCVS = "";
539 if (pi.file.getFormatVersion() < 3.04) {
540 sourcePCVS = line.substring(19, 60).trim();
541 } else {
542 sourcePCVS = line.substring(22, 65).trim();
543 }
544
545 // Check if sought fields were not actually blanks
546 if (!progPCVS.equals("") || !sourcePCVS.equals("")) {
547 pi.file.addAppliedPCVS(new AppliedPCVS(satelliteSystem, progPCVS, sourcePCVS));
548 }
549 }
550
551 },
552
553 /** Parser for the different clock data types that are stored in the file. */
554 HEADER_TYPES_OF_DATA("^.+# / TYPES OF DATA( )*$") {
555
556 /** {@inheritDoc} */
557 @Override
558 public void parse(final String line, final ParseInfo pi) {
559 try (Scanner s1 = new Scanner(line);
560 Scanner s2 = s1.useDelimiter(SPACES);
561 Scanner scanner = s2.useLocale(Locale.US)) {
562
563 // First element is the number of different types of data
564 final int numberOfDifferentDataTypes = scanner.nextInt();
565
566 // Loop over data types
567 for (int i = 0; i < numberOfDifferentDataTypes; i++) {
568 final ClockDataType dataType = ClockDataType.parseClockDataType(scanner.next());
569 pi.file.addClockDataType(dataType);
570 }
571 }
572 }
573
574 },
575
576 /** Parser for the station with reference clock. */
577 HEADER_STATIONS_NAME("^.+STATION NAME / NUM( )*$") {
578
579 /** {@inheritDoc} */
580 @Override
581 public void parse(final String line, final ParseInfo pi) {
582 try (Scanner s1 = new Scanner(line);
583 Scanner s2 = s1.useDelimiter(SPACES);
584 Scanner scanner = s2.useLocale(Locale.US)) {
585
586 // First element is the station clock reference ID
587 final String stationName = scanner.next();
588 pi.file.setStationName(stationName);
589
590 // Second element is the station clock reference identifier
591 final String stationIdentifier = scanner.next();
592 pi.file.setStationIdentifier(stationIdentifier);
593 }
594 }
595
596 },
597
598 /** Parser for the reference clock in case of calibration data. */
599 HEADER_STATION_CLOCK_REF("^.+STATION CLK REF( )*$") {
600
601 /** {@inheritDoc} */
602 @Override
603 public void parse(final String line, final ParseInfo pi) {
604 if (pi.file.getFormatVersion() < 3.04) {
605 pi.file.setExternalClockReference(line.substring(0, 60).trim());
606 } else {
607 pi.file.setExternalClockReference(line.substring(0, 65).trim());
608 }
609 }
610
611 },
612
613 /** Parser for the analysis center. */
614 HEADER_ANALYSIS_CENTER("^.+ANALYSIS CENTER( )*$") {
615
616 /** {@inheritDoc} */
617 @Override
618 public void parse(final String line, final ParseInfo pi) {
619
620 // First element is IGS AC designator
621 final String analysisCenterID = line.substring(0, 3).trim();
622 pi.file.setAnalysisCenterID(analysisCenterID);
623
624 // Then, the full name of the analysis center
625 String analysisCenterName = "";
626 if (pi.file.getFormatVersion() < 3.04) {
627 analysisCenterName = line.substring(5, 60).trim();
628 } else {
629 analysisCenterName = line.substring(5, 65).trim();
630 }
631 pi.file.setAnalysisCenterName(analysisCenterName);
632 }
633
634 },
635
636 /** Parser for the number of reference clocks over a period. */
637 HEADER_NUMBER_OF_CLOCK_REF("^.+# OF CLK REF( )*$") {
638
639 /** {@inheritDoc} */
640 @Override
641 public void parse(final String line, final ParseInfo pi) {
642 try (Scanner s1 = new Scanner(line);
643 Scanner s2 = s1.useDelimiter(SPACES);
644 Scanner scanner = s2.useLocale(Locale.US)) {
645
646 // Initialize current reference clock list corresponding to the period
647 pi.currentReferenceClocks = new ArrayList<ReferenceClock>();
648
649 // First element is the number of reference clocks corresponding to the period
650 scanner.nextInt();
651
652 if (scanner.hasNextInt()) {
653 // Second element is the start epoch of the period
654 final int startYear = scanner.nextInt();
655 final int startMonth = scanner.nextInt();
656 final int startDay = scanner.nextInt();
657 final int startHour = scanner.nextInt();
658 final int startMin = scanner.nextInt();
659 final double startSec = scanner.nextDouble();
660 final AbsoluteDate startEpoch = new AbsoluteDate(startYear, startMonth, startDay,
661 startHour, startMin, startSec,
662 pi.file.getTimeScale());
663 pi.referenceClockStartDate = startEpoch;
664
665 // Third element is the end epoch of the period
666 final int endYear = scanner.nextInt();
667 final int endMonth = scanner.nextInt();
668 final int endDay = scanner.nextInt();
669 final int endHour = scanner.nextInt();
670 final int endMin = scanner.nextInt();
671 double endSec = 0.0;
672 if (pi.file.getFormatVersion() < 3.04) {
673 endSec = Double.parseDouble(line.substring(51, 60));
674 } else {
675 endSec = scanner.nextDouble();
676 }
677 final AbsoluteDate endEpoch = new AbsoluteDate(endYear, endMonth, endDay,
678 endHour, endMin, endSec,
679 pi.file.getTimeScale());
680 pi.referenceClockEndDate = endEpoch;
681 } else {
682 pi.referenceClockStartDate = AbsoluteDate.PAST_INFINITY;
683 pi.referenceClockEndDate = AbsoluteDate.FUTURE_INFINITY;
684 }
685 }
686 }
687
688 },
689
690 /** Parser for the reference clock over a period. */
691 HEADER_ANALYSIS_CLOCK_REF("^.+ANALYSIS CLK REF( )*$") {
692
693 /** {@inheritDoc} */
694 @Override
695 public void parse(final String line, final ParseInfo pi) {
696 try (Scanner s1 = new Scanner(line);
697 Scanner s2 = s1.useDelimiter(SPACES);
698 Scanner scanner = s2.useLocale(Locale.US)) {
699
700 // First element is the name of the receiver/satellite embedding the reference clock
701 final String referenceName = scanner.next();
702
703 // Second element is the reference clock ID
704 final String clockID = scanner.next();
705
706 // Optionally, third element is an a priori clock constraint, by default equal to zero
707 double clockConstraint = 0.0;
708 if (scanner.hasNextDouble()) {
709 clockConstraint = scanner.nextDouble();
710 }
711
712 // Add reference clock to current reference clock list
713 final ReferenceClock referenceClock = new ReferenceClock(referenceName, clockID, clockConstraint,
714 pi.referenceClockStartDate, pi.referenceClockEndDate);
715 pi.currentReferenceClocks.add(referenceClock);
716
717 // Modify time span map of the reference clocks to accept the new reference clock
718 pi.file.addReferenceClockList(pi.currentReferenceClocks, pi.referenceClockStartDate);
719 }
720 }
721
722 },
723
724 /** Parser for the number of stations embedded in the file and the related frame. */
725 HEADER_NUMBER_OF_SOLN_STATIONS("^.+SOLN STA / TRF( )*$") {
726
727 /** {@inheritDoc} */
728 @Override
729 public void parse(final String line, final ParseInfo pi) {
730 try (Scanner s1 = new Scanner(line);
731 Scanner s2 = s1.useDelimiter(SPACES);
732 Scanner scanner = s2.useLocale(Locale.US)) {
733
734 // First element is the number of receivers embedded in the file
735 scanner.nextInt();
736
737 // Second element is the frame linked to given receiver positions
738 final String frameString = scanner.next();
739 pi.file.setFrameName(frameString);
740 }
741 }
742
743 },
744
745 /** Parser for the stations embedded in the file and the related positions. */
746 HEADER_SOLN_STATIONS("^.+SOLN STA NAME / NUM( )*$") {
747
748 /** {@inheritDoc} */
749 @Override
750 public void parse(final String line, final ParseInfo pi) {
751
752 // First element is the receiver designator
753 String designator = line.substring(0, 10).trim();
754
755 // Second element is the receiver identifier
756 String receiverIdentifier = line.substring(10, 30).trim();
757
758 // Third element if X coordinates, in millimeters in the file frame.
759 String xString = "";
760
761 // Fourth element if Y coordinates, in millimeters in the file frame.
762 String yString = "";
763
764 // Fifth element if Z coordinates, in millimeters in the file frame.
765 String zString = "";
766
767 if (pi.file.getFormatVersion() < 3.04) {
768 designator = line.substring(0, 4).trim();
769 receiverIdentifier = line.substring(5, 25).trim();
770 xString = line.substring(25, 36).trim();
771 yString = line.substring(37, 48).trim();
772 zString = line.substring(49, 60).trim();
773 } else {
774 designator = line.substring(0, 10).trim();
775 receiverIdentifier = line.substring(10, 30).trim();
776 xString = line.substring(30, 41).trim();
777 yString = line.substring(42, 53).trim();
778 zString = line.substring(54, 65).trim();
779 }
780
781 final double x = MILLIMETER * Double.parseDouble(xString);
782 final double y = MILLIMETER * Double.parseDouble(yString);
783 final double z = MILLIMETER * Double.parseDouble(zString);
784
785 final Receiver receiver = new Receiver(designator, receiverIdentifier, x, y, z);
786 pi.file.addReceiver(receiver);
787
788 }
789
790 },
791
792 /** Parser for the number of satellites embedded in the file. */
793 HEADER_NUMBER_OF_SOLN_SATS("^.+# OF SOLN SATS( )*$") {
794
795 /** {@inheritDoc} */
796 @Override
797 public void parse(final String line, final ParseInfo pi) {
798
799 // Only element in the line is number of satellites, not used here.
800 // Do nothing...
801 }
802
803 },
804
805 /** Parser for the satellites embedded in the file. */
806 HEADER_PRN_LIST("^.+PRN LIST( )*$") {
807
808 /** {@inheritDoc} */
809 @Override
810 public void parse(final String line, final ParseInfo pi) {
811 try (Scanner s1 = new Scanner(line);
812 Scanner s2 = s1.useDelimiter(SPACES);
813 Scanner scanner = s2.useLocale(Locale.US)) {
814
815 // Only PRN numbers are stored in these lines
816 // Initialize first PRN number
817 String prn = scanner.next();
818
819 // Browse the line until its end
820 while (!prn.equals("PRN")) {
821 pi.file.addSatellite(prn);
822 prn = scanner.next();
823 }
824 }
825 }
826
827 },
828
829 /** Parser for the end of header. */
830 HEADER_END("^.+END OF HEADER( )*$") {
831
832 /** {@inheritDoc} */
833 @Override
834 public void parse(final String line, final ParseInfo pi) {
835 // do nothing...
836 }
837
838 /** {@inheritDoc} */
839 @Override
840 public Stream<LineParser> allowedNext() {
841 return Stream.of(CLOCK_DATA);
842 }
843 },
844
845 /** Parser for a clock data line. */
846 CLOCK_DATA("(^AR |^AS |^CR |^DR |^MS ).+$") {
847
848 /** {@inheritDoc} */
849 @Override
850 public void parse(final String line, final ParseInfo pi) {
851 try (Scanner s1 = new Scanner(line);
852 Scanner s2 = s1.useDelimiter(SPACES);
853 Scanner scanner = s2.useLocale(Locale.US)) {
854
855 // Initialise current values
856 pi.currentDataValues = new double[6];
857
858 // First element is clock data type
859 pi.currentDataType = ClockDataType.parseClockDataType(scanner.next());
860
861 // Second element is receiver/satellite name
862 pi.currentName = scanner.next();
863
864 // Third element is data epoch
865 final int year = scanner.nextInt();
866 final int month = scanner.nextInt();
867 final int day = scanner.nextInt();
868 final int hour = scanner.nextInt();
869 final int min = scanner.nextInt();
870 final double sec = scanner.nextDouble();
871 pi.currentDateComponents = new DateComponents(year, month, day);
872 pi.currentTimeComponents = new TimeComponents(hour, min, sec);
873
874 // Fourth element is number of data values
875 pi.currentNumberOfValues = scanner.nextInt();
876
877 // Get the values in this line, there are at most 2.
878 // Some entries claim less values than there actually are.
879 // All values are added to the set, regardless of their claimed number.
880 int i = 0;
881 while (scanner.hasNextDouble()) {
882 pi.currentDataValues[i++] = scanner.nextDouble();
883 }
884
885 // Check if continuation line is required
886 if (pi.currentNumberOfValues <= 2) {
887 // No continuation line is required
888 pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
889 pi.currentName,
890 pi.currentDateComponents,
891 pi.currentTimeComponents,
892 pi.currentNumberOfValues,
893 pi.currentDataValues[0],
894 pi.currentDataValues[1],
895 0.0, 0.0, 0.0, 0.0));
896 }
897 }
898 }
899
900 /** {@inheritDoc} */
901 @Override
902 public Stream<LineParser> allowedNext() {
903 return Stream.of(CLOCK_DATA, CLOCK_DATA_CONTINUATION);
904 }
905 },
906
907 /** Parser for a continuation clock data line. */
908 CLOCK_DATA_CONTINUATION("^ .+") {
909
910 /** {@inheritDoc} */
911 @Override
912 public void parse(final String line, final ParseInfo pi) {
913 try (Scanner s1 = new Scanner(line);
914 Scanner s2 = s1.useDelimiter(SPACES);
915 Scanner scanner = s2.useLocale(Locale.US)) {
916
917 // Get the values in this continuation line.
918 // Some entries claim less values than there actually are.
919 // All values are added to the set, regardless of their claimed number.
920 int i = 2;
921 while (scanner.hasNextDouble()) {
922 pi.currentDataValues[i++] = scanner.nextDouble();
923 }
924
925 // Add clock data line
926 pi.file.addClockData(pi.currentName, pi.file.new ClockDataLine(pi.currentDataType,
927 pi.currentName,
928 pi.currentDateComponents,
929 pi.currentTimeComponents,
930 pi.currentNumberOfValues,
931 pi.currentDataValues[0],
932 pi.currentDataValues[1],
933 pi.currentDataValues[2],
934 pi.currentDataValues[3],
935 pi.currentDataValues[4],
936 pi.currentDataValues[5]));
937
938 }
939 }
940
941 /** {@inheritDoc} */
942 @Override
943 public Stream<LineParser> allowedNext() {
944 return Stream.of(CLOCK_DATA);
945 }
946 };
947
948 /** Pattern for identifying line. */
949 private final Pattern pattern;
950
951 /** Simple constructor.
952 * @param lineRegexp regular expression for identifying line
953 */
954 LineParser(final String lineRegexp) {
955 pattern = Pattern.compile(lineRegexp);
956 }
957
958 /** Parse a line.
959 * @param line line to parse
960 * @param pi holder for transient data
961 */
962 public abstract void parse(String line, ParseInfo pi);
963
964 /** Get the allowed parsers for next line.
965 * <p>
966 * Because the standard only recommends an order for header keys,
967 * the default implementation of the method returns all the
968 * header keys. Specific implementations must overrides the method.
969 * </p>
970 * @return allowed parsers for next line
971 */
972 public Stream<LineParser> allowedNext() {
973 return Stream.of(HEADER_PROGRAM, HEADER_COMMENT, HEADER_SYSTEM_OBS, HEADER_SYSTEM_OBS_CONTINUATION, HEADER_TIME_SYSTEM, HEADER_LEAP_SECONDS,
974 HEADER_LEAP_SECONDS_GNSS, HEADER_DCBS, HEADER_PCVS, HEADER_TYPES_OF_DATA, HEADER_STATIONS_NAME, HEADER_STATION_CLOCK_REF,
975 HEADER_ANALYSIS_CENTER, HEADER_NUMBER_OF_CLOCK_REF, HEADER_ANALYSIS_CLOCK_REF, HEADER_NUMBER_OF_SOLN_STATIONS,
976 HEADER_SOLN_STATIONS, HEADER_NUMBER_OF_SOLN_SATS, HEADER_PRN_LIST, HEADER_END);
977 }
978
979 /** Check if parser can handle line.
980 * @param line line to parse
981 * @return true if parser can handle the specified line
982 */
983 public boolean canHandle(final String line) {
984 return pattern.matcher(line).matches();
985 }
986
987 /** Parse existing date - time - zone formats.
988 * If zone field is not missing, a proper Orekit date can be created and set into clock file object.
989 * This feature depends on the date format.
990 * @param dateString the whole date - time - zone string
991 * @param pi holder for transient data
992 */
993 private static void parseDateTimeZone(final String dateString, final ParseInfo pi) {
994
995 String date = "";
996 String time = "";
997 String zone = "";
998 DateComponents dateComponents = null;
999 TimeComponents timeComponents = null;
1000
1001 if (DATE_PATTERN_1.matcher(dateString).matches()) {
1002
1003 date = dateString.substring(0, 10).trim();
1004 time = dateString.substring(11, 16).trim();
1005 zone = dateString.substring(16).trim();
1006
1007 } else if (DATE_PATTERN_2.matcher(dateString).matches()) {
1008
1009 date = dateString.substring(0, 8).trim();
1010 time = dateString.substring(9, 16).trim();
1011 zone = dateString.substring(16).trim();
1012
1013 if (!zone.equals("")) {
1014 // Get date and time components
1015 dateComponents = new DateComponents(Integer.parseInt(date.substring(0, 4)),
1016 Integer.parseInt(date.substring(4, 6)),
1017 Integer.parseInt(date.substring(6, 8)));
1018 timeComponents = new TimeComponents(Integer.parseInt(time.substring(0, 2)),
1019 Integer.parseInt(time.substring(2, 4)),
1020 Integer.parseInt(time.substring(4, 6)));
1021
1022 }
1023
1024 } else if (DATE_PATTERN_3.matcher(dateString).matches()) {
1025
1026 date = dateString.substring(0, 11).trim();
1027 time = dateString.substring(11, 17).trim();
1028 zone = dateString.substring(17).trim();
1029
1030 } else if (DATE_PATTERN_4.matcher(dateString).matches()) {
1031
1032 date = dateString.substring(0, 9).trim();
1033 time = dateString.substring(9, 15).trim();
1034 zone = dateString.substring(15).trim();
1035
1036 } else if (DATE_PATTERN_5.matcher(dateString).matches()) {
1037
1038 date = dateString.substring(0, 11).trim();
1039 time = dateString.substring(11, 20).trim();
1040
1041 } else {
1042 // Format is not handled or date is missing. Do nothing...
1043 }
1044
1045 pi.file.setCreationDateString(date);
1046 pi.file.setCreationTimeString(time);
1047 pi.file.setCreationTimeZoneString(zone);
1048
1049 if (dateComponents != null) {
1050 pi.file.setCreationDate(new AbsoluteDate(dateComponents,
1051 timeComponents,
1052 TimeSystem.parseTimeSystem(zone).getTimeScale(pi.timeScales)));
1053 }
1054 }
1055 }
1056
1057 }