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