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