1   /* Copyright 2002-2026 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;
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.Reader;
21  import java.nio.CharBuffer;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.hipparchus.util.FastMath;
30  import org.orekit.data.DataFilter;
31  import org.orekit.data.DataSource;
32  import org.orekit.data.LineOrientedFilteringReader;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitMessages;
35  import org.orekit.gnss.SatelliteSystem;
36  
37  /** Decompression filter for Hatanaka compressed RINEX files.
38   * @see <a href="http://cedadocs.ceda.ac.uk/1254/1/Hatanaka%5C_compressed%5C_format%5C_help.pdf">A
39   * Compression Format and Tools for GNSS Observation Data</a>
40   * @since 10.1
41   */
42  public class HatanakaCompressFilter implements DataFilter {
43  
44      /** Pattern for rinex 2 observation files. */
45      private static final Pattern RINEX_2_PATTERN = Pattern.compile("^(\\w{4}\\d{3}[0a-x](?:\\d{2})?\\.\\d{2})[dD]$");
46  
47      /** Pattern for rinex 3 observation files. */
48      private static final Pattern RINEX_3_PATTERN = Pattern.compile("^(\\w{9}_\\w{1}_\\d{11}_\\d{2}\\w_\\d{2}\\w{1}_\\w{2})\\.crx$");
49  
50      /** Empty constructor.
51       * <p>
52       * This constructor is not strictly necessary, but it prevents spurious
53       * javadoc warnings with JDK 18 and later.
54       * </p>
55       * @since 12.0
56       */
57      public HatanakaCompressFilter() {
58          // nothing to do
59      }
60  
61      /** {@inheritDoc} */
62      @Override
63      public DataSource filter(final DataSource original) {
64  
65          final String            oName   = original.getName();
66          final DataSource.Opener oOpener = original.getOpener();
67  
68          final Matcher rinex2Matcher = RINEX_2_PATTERN.matcher(oName);
69          if (rinex2Matcher.matches()) {
70              // this is a rinex 2 file compressed with Hatanaka method
71              final String                  fName   = rinex2Matcher.group(1) + "o";
72              final DataSource.ReaderOpener fOpener = () -> new HatanakaReader(oName, oOpener.openReaderOnce());
73              return new DataSource(fName, fOpener);
74          }
75  
76          final Matcher rinex3Matcher = RINEX_3_PATTERN.matcher(oName);
77          if (rinex3Matcher.matches()) {
78              // this is a rinex 3 file compressed with Hatanaka method
79              final String                  fName   = rinex3Matcher.group(1) + ".rnx";
80              final DataSource.ReaderOpener fOpener = () -> new HatanakaReader(oName, oOpener.openReaderOnce());
81              return new DataSource(fName, fOpener);
82          }
83  
84          // it is not an Hatanaka compressed rinex file
85          return original;
86  
87      }
88  
89      /** Filtering of Hatanaka compressed characters stream. */
90      private static class HatanakaReader extends LineOrientedFilteringReader {
91  
92          /** Format of the current file. */
93          private final CompactRinexFormat format;
94  
95          /** Simple constructor.
96           * @param name file name
97           * @param input underlying compressed stream
98           * @exception IOException if first lines cannot be read
99           */
100         HatanakaReader(final String name, final Reader input)
101             throws IOException {
102             super(name, input);
103             format = CompactRinexFormat.getFormat(name, getBufferedReader());
104         }
105 
106         /** {@inheritDoc} */
107         @Override
108         protected CharSequence filterLine(final int lineNumber, final String originalLine) throws IOException {
109             return format.uncompressSection(originalLine);
110         }
111 
112     }
113 
114     /** Processor handling differential compression for one numerical data field. */
115     private static class NumericDifferential {
116 
117         /** Length of the uncompressed text field. */
118         private final int fieldLength;
119 
120         /** Number of decimal places uncompressed text field. */
121         private final int decimalPlaces;
122 
123         /** State vector. */
124         private final long[] state;
125 
126         /** Number of components in the state vector. */
127         private int nbComponents;
128 
129         /** Uncompressed value. */
130         private CharSequence uncompressed;
131 
132         /** Simple constructor.
133          * @param fieldLength length of the uncompressed text field
134          * @param decimalPlaces number of decimal places uncompressed text field
135          * @param order differential order
136          */
137         NumericDifferential(final int fieldLength, final int decimalPlaces, final int order) {
138             this.fieldLength   = fieldLength;
139             this.decimalPlaces = decimalPlaces;
140             this.state         = new long[order + 1];
141             this.nbComponents  = 0;
142         }
143 
144         /** Handle a new compressed value.
145          * @param sequence sequence containing the value to consider
146          */
147         public void accept(final CharSequence sequence) {
148 
149             // store the value as the last component of state vector
150             state[nbComponents] = Long.parseLong(sequence.toString());
151 
152             // update state vector
153             for (int i = nbComponents; i > 0; --i) {
154                 state[i - 1] += state[i];
155             }
156 
157             if (++nbComponents == state.length) {
158                 // the state vector is full
159                 --nbComponents;
160             }
161 
162             // output uncompressed value
163             final String unscaled = Long.toString(FastMath.abs(state[0]));
164             final int    length   = unscaled.length();
165             final int    digits   = FastMath.max(length, decimalPlaces);
166             final int    padding  = fieldLength - (digits + (state[0] < 0 ? 2 : 1));
167             final StringBuilder builder = new StringBuilder();
168             for (int i = 0; i < padding; ++i) {
169                 builder.append(' ');
170             }
171             if (state[0] < 0) {
172                 builder.append('-');
173             }
174             if (length > decimalPlaces) {
175                 builder.append(unscaled, 0, length - decimalPlaces);
176             }
177             builder.append('.');
178             for (int i = decimalPlaces; i > 0; --i) {
179                 builder.append(i > length ? '0' : unscaled.charAt(length - i));
180             }
181 
182             uncompressed = builder;
183 
184         }
185 
186         /** Get a string representation of the uncompressed value.
187          * @return string representation of the uncompressed value
188          */
189         public CharSequence getUncompressed() {
190             return uncompressed;
191         }
192 
193     }
194 
195     /** Processor handling text compression for one text data field. */
196     private static class TextDifferential {
197 
198         /** Buffer holding the current state. */
199         private final CharBuffer state;
200 
201         /** Simple constructor.
202          * @param fieldLength length of the uncompressed text field
203          */
204         TextDifferential(final int fieldLength) {
205             this.state = CharBuffer.allocate(fieldLength);
206             for (int i = 0; i < fieldLength; ++i) {
207                 state.put(i, ' ');
208             }
209         }
210 
211         /** Handle a new compressed value.
212          * @param sequence sequence containing the value to consider
213          */
214         public void accept(final CharSequence sequence) {
215 
216             // update state
217             final int length = FastMath.min(state.capacity(), sequence.length());
218             for (int i = 0; i < length; ++i) {
219                 final char c = sequence.charAt(i);
220                 if (c == '&') {
221                     // update state with disappearing character
222                     state.put(i, ' ');
223                 } else if (c != ' ') {
224                     // update state with changed character
225                     state.put(i, c);
226                 }
227             }
228 
229         }
230 
231         /** Get a string representation of the uncompressed value.
232          * @return string representation of the uncompressed value
233          */
234         public CharSequence getUncompressed() {
235             return state;
236         }
237 
238     }
239 
240     /** Container for combined observations and flags. */
241     private static class CombinedDifferentials {
242 
243         /** Observation differentials. */
244         private final NumericDifferential[] observations;
245 
246         /** Flags differential. */
247         private final TextDifferential flags;
248 
249         /** Simple constructor.
250          * Build an empty container.
251          * @param nbObs number of observations
252          */
253         CombinedDifferentials(final int nbObs) {
254             this.observations = new NumericDifferential[nbObs];
255             this.flags        = new TextDifferential(2 * nbObs);
256         }
257 
258     }
259 
260     /** Base class for parsing compact RINEX format. */
261     private abstract static class CompactRinexFormat {
262 
263         /** Index of label in data lines. */
264         private static final int LABEL_START = 60;
265 
266         /** Label for compact Rinex version. */
267         private static final String CRINEX_VERSION_TYPE  = "CRINEX VERS   / TYPE";
268 
269         /** Label for compact Rinex program. */
270         private static final String CRINEX_PROG_DATE     = "CRINEX PROG / DATE";
271 
272         /** Label for number of satellites. */
273         private static final String NB_OF_SATELLITES = "# OF SATELLITES";
274 
275         /** Label for end of header. */
276         private static final String END_OF_HEADER    = "END OF HEADER";
277 
278         /** Default number of satellites (used if not present in the file). */
279         private static final int DEFAULT_NB_SAT = 500;
280 
281         /** File name. */
282         private final String name;
283 
284         /** Line-oriented input. */
285         private final BufferedReader reader;
286 
287         /** Current line number. */
288         private int lineNumber;
289 
290         /** Maximum number of observations for one satellite. */
291         private final Map<SatelliteSystem, Integer> maxObs;
292 
293         /** Number of satellites. */
294         private int nbSat;
295 
296         /** Indicator for current section type. */
297         private Section section;
298 
299         /** Satellites observed at current epoch. */
300         private List<String> satellites;
301 
302         /** Differential engine for epoch. */
303         private TextDifferential epochDifferential;
304 
305         /** Receiver clock offset differential. */
306         private NumericDifferential clockDifferential;
307 
308         /** Differential engine for satellites list. */
309         private TextDifferential satListDifferential;
310 
311         /** Differential engines for each satellite. */
312         private Map<String, CombinedDifferentials> differentials;
313 
314         /** Simple constructor.
315          * @param name file name
316          * @param reader line-oriented input
317          */
318         protected CompactRinexFormat(final String name, final BufferedReader reader) {
319             this.name    = name;
320             this.reader  = reader;
321             this.maxObs  = new HashMap<>();
322             for (final SatelliteSystem system : SatelliteSystem.values()) {
323                 maxObs.put(system, 0);
324             }
325             this.nbSat   = DEFAULT_NB_SAT;
326             this.section = Section.HEADER;
327         }
328 
329         /** Uncompress a section.
330          * @param firstLine first line of the section
331          * @return uncompressed section (contains several lines)
332          * @exception IOException if we cannot read lines from underlying stream
333          */
334         public CharSequence uncompressSection(final String firstLine)
335             throws IOException {
336             final CharSequence uncompressed;
337             switch (section) {
338 
339                 case HEADER : {
340                     // header lines
341                     final StringBuilder builder = new StringBuilder();
342                     String line = firstLine;
343                     lineNumber = 3; // there are 2 CRINEX lines before the RINEX header line
344                     while (section == Section.HEADER) {
345                         if (builder.length() > 0) {
346                             builder.append('\n');
347                             line = readLine();
348                         }
349                         builder.append(parseHeaderLine(line));
350                         trimTrailingSpaces(builder);
351                     }
352                     uncompressed = builder;
353                     section      = Section.EPOCH;
354                     break;
355                 }
356 
357                 case EPOCH : {
358                     // epoch and receiver clock offset lines
359                     ++lineNumber; // the caller has read one epoch line
360                     uncompressed = parseEpochAndClockLines(firstLine, readLine().trim());
361                     section      = Section.OBSERVATION;
362                     break;
363                 }
364 
365                 default : {
366                     // observation lines
367                     final String[] lines = new String[satellites.size()];
368                     ++lineNumber; // the caller has read one observation line
369                     lines[0] = firstLine;
370                     for (int i = 1; i < lines.length; ++i) {
371                         lines[i] = readLine();
372                     }
373                     uncompressed = parseObservationLines(lines);
374                     section      = Section.EPOCH;
375                 }
376 
377             }
378 
379             return uncompressed;
380 
381         }
382 
383         /** Parse a header line.
384          * @param line header line
385          * @return uncompressed line
386          */
387         public CharSequence parseHeaderLine(final String line) {
388 
389             if (isHeaderLine(NB_OF_SATELLITES, line)) {
390                 // number of satellites
391                 nbSat = parseInt(line, 0, 6);
392             } else if (isHeaderLine(END_OF_HEADER, line)) {
393                 // we have reached end of header, prepare parsing of data records
394                 section = Section.EPOCH;
395             }
396 
397             // within header, lines are simply copied
398             return line;
399 
400         }
401 
402         /** Parse epoch and receiver clock offset lines.
403          * @param epochLine epoch line
404          * @param clockLine receiver clock offset line
405          * @return uncompressed line
406          * @exception IOException if we cannot read additional special events lines
407          */
408         public abstract CharSequence parseEpochAndClockLines(String epochLine, String clockLine)
409             throws IOException;
410 
411         /** Parse epoch and receiver clock offset lines.
412          * @param builder builder that may used to copy special event lines
413          * @param epochStart start of the epoch field
414          * @param epochLength length of epoch field
415          * @param eventStart start of the special events field
416          * @param nbSatStart start of the number of satellites field
417          * @param satListStart start of the satellites list
418          * @param clockLength length of receiver clock field
419          * @param clockDecimalPlaces number of decimal places for receiver clock offset
420          * @param epochLine epoch line
421          * @param clockLine receiver clock offset line
422          * @param resetChar character indicating differentials reset
423          * @exception IOException if we cannot read additional special events lines
424          */
425         protected void doParseEpochAndClockLines(final StringBuilder builder,
426                                                  final int epochStart, final int epochLength,
427                                                  final int eventStart, final int nbSatStart, final int satListStart,
428                                                  final int clockLength, final int clockDecimalPlaces,
429                                                  final String epochLine,
430                                                  final String clockLine, final char resetChar)
431             throws IOException {
432 
433             boolean loop = true;
434             String loopEpochLine = epochLine;
435             String loopClockLine = clockLine;
436             while (loop) {
437 
438                 // check if differentials should be reset
439                 if (epochDifferential == null || loopEpochLine.charAt(0) == resetChar) {
440                     epochDifferential   = new TextDifferential(epochLength);
441                     satListDifferential = new TextDifferential(nbSat * 3);
442                     differentials       = new HashMap<>();
443                 }
444 
445                 // check for special events
446                 epochDifferential.accept(loopEpochLine.subSequence(epochStart,
447                                                                    FastMath.min(loopEpochLine.length(), epochStart + epochLength)));
448                 if (parseInt(epochDifferential.getUncompressed(), eventStart, 1) > 1) {
449                     // this was not really the epoch, but rather a special event
450                     // we just copy the lines and skip to real epoch and clock lines
451                     builder.append(epochDifferential.getUncompressed());
452                     trimTrailingSpaces(builder);
453                     builder.append('\n');
454                     final int skippedLines = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
455                     for (int i = 0; i < skippedLines; ++i) {
456                         builder.append(loopClockLine);
457                         trimTrailingSpaces(builder);
458                         builder.append('\n');
459                         loopClockLine = readLine();
460                     }
461 
462                     // the epoch and clock are in the next lines
463                     loopEpochLine = loopClockLine;
464                     loopClockLine = readLine();
465                     loop = true;
466 
467                 } else {
468                     loop = false;
469                     final int n = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
470                     satellites = new ArrayList<>(n);
471                     if (satListStart < loopEpochLine.length()) {
472                         satListDifferential.accept(loopEpochLine.subSequence(satListStart, loopEpochLine.length()));
473                     }
474                     final CharSequence satListPart = satListDifferential.getUncompressed();
475                     for (int i = 0; i < n; ++i) {
476                         satellites.add(satListPart.subSequence(i * 3, (i + 1) * 3).toString());
477                     }
478 
479                     // parse clock offset
480                     if (!loopClockLine.isEmpty()) {
481                         if (loopClockLine.length() > 2 && loopClockLine.charAt(1) == '&') {
482                             clockDifferential = new NumericDifferential(clockLength, clockDecimalPlaces, parseInt(loopClockLine, 0, 1));
483                             clockDifferential.accept(loopClockLine.subSequence(2, loopClockLine.length()));
484                         } else if (clockDifferential == null) {
485                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
486                                                       lineNumber, name, loopClockLine);
487                         } else {
488                             clockDifferential.accept(loopClockLine);
489                         }
490                     }
491                 }
492             }
493 
494         }
495 
496         /** Get the uncompressed epoch part.
497          * @return uncompressed epoch part
498          */
499         protected CharSequence getEpochPart() {
500             return epochDifferential.getUncompressed();
501         }
502 
503         /** Get the uncompressed clock part.
504          * @return uncompressed clock part
505          */
506         protected CharSequence getClockPart() {
507             return clockDifferential == null ? "" : clockDifferential.getUncompressed();
508         }
509 
510         /** Get the satellites for current observations.
511          * @return satellites for current observation
512          */
513         protected List<String> getSatellites() {
514             return satellites;
515         }
516 
517         /** Get the combined differentials for one satellite.
518          * @param sat satellite id
519          * @return observationDifferentials
520          */
521         protected CombinedDifferentials getCombinedDifferentials(final CharSequence sat) {
522             return differentials.get(sat);
523         }
524 
525         /** Parse observation lines.
526          * @param observationLines observation lines
527          * @return uncompressed lines
528          */
529         public abstract CharSequence parseObservationLines(String[] observationLines);
530 
531         /** Parse observation lines.
532          * @param dataLength length of data fields
533          * @param dataDecimalPlaces number of decimal places for data fields
534          * @param observationLines observation lines
535          */
536         protected void doParseObservationLines(final int dataLength, final int dataDecimalPlaces,
537                                                final String[] observationLines) {
538 
539             for (int i = 0; i < observationLines.length; ++i) {
540 
541                 final CharSequence line = observationLines[i];
542 
543                 // get the differentials associated with this observations line
544                 final String sat = satellites.get(i);
545                 CombinedDifferentials satDiffs = differentials.get(sat);
546                 if (satDiffs == null) {
547                     final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(sat.subSequence(0, 1).toString());
548                     satDiffs = new CombinedDifferentials(maxObs.get(system));
549                     differentials.put(sat, satDiffs);
550                 }
551 
552                 // parse observations
553                 int k = 0;
554                 for (int j = 0; j < satDiffs.observations.length; ++j) {
555 
556                     if (k >= line.length() || line.charAt(k) == ' ') {
557                         // the data field is missing
558                         satDiffs.observations[j] = null;
559                     } else {
560                         // the data field is present
561 
562                         if (k + 1 < line.length() &&
563                             Character.isDigit(line.charAt(k)) &&
564                             line.charAt(k + 1) == '&') {
565                             // reinitialize differentials
566                             satDiffs.observations[j] = new NumericDifferential(dataLength, dataDecimalPlaces,
567                                                                                Character.digit(line.charAt(k), 10));
568                             k += 2;
569                         }
570 
571                         // extract the compressed differenced value
572                         final int start = k;
573                         while (k < line.length() && line.charAt(k) != ' ') {
574                             ++k;
575                         }
576                         try {
577                             satDiffs.observations[j].accept(line.subSequence(start, k));
578                         } catch (NumberFormatException nfe) {
579                             throw new OrekitException(nfe,
580                                                       OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
581                                                       lineNumber + i - (observationLines.length - 1),
582                                                       name, observationLines[i]);
583                         }
584 
585                     }
586 
587                     // skip blank separator
588                     ++k;
589 
590                 }
591 
592                 if (k < line.length()) {
593                     satDiffs.flags.accept(line.subSequence(k, line.length()));
594                 }
595 
596             }
597 
598         }
599 
600         /** Check if a line corresponds to a header.
601          * @param label header label
602          * @param line header line
603          * @return true if line corresponds to header
604          */
605         protected boolean isHeaderLine(final String label, final String line) {
606             return label.equals(parseString(line, LABEL_START, label.length()));
607         }
608 
609         /** Update the max number of observations.
610          * @param system satellite system
611          * @param nbObs number of observations
612          */
613         protected void updateMaxObs(final SatelliteSystem system, final int nbObs) {
614             maxObs.put(system, FastMath.max(maxObs.get(system), nbObs));
615         }
616 
617         /** Read a new line.
618          * @return line read
619          * @exception IOException if a read error occurs
620          */
621         private String readLine()
622             throws IOException {
623             final String line = reader.readLine();
624             if (line == null) {
625                 throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE, name);
626             }
627             lineNumber++;
628             return line;
629         }
630 
631         /** Get the rinex format corresponding to this compact rinex format.
632          * @param name file name
633          * @param reader line-oriented input
634          * @return rinex format associated with this compact rinex format
635          * @exception IOException if first lines cannot be read
636          */
637         public static CompactRinexFormat getFormat(final String name, final BufferedReader reader)
638             throws IOException {
639 
640             // read the first two lines of the file
641             final String line1 = reader.readLine();
642             final String line2 = reader.readLine();
643             if (line1 == null || line2 == null) {
644                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
645             }
646 
647             // extract format version
648             final int cVersion100 = (int) FastMath.rint(100 * parseDouble(line1, 0, 9));
649             if (cVersion100 != 100 && cVersion100 != 300) {
650                 throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT, name);
651             }
652             if (!CRINEX_VERSION_TYPE.equals(parseString(line1, LABEL_START, CRINEX_VERSION_TYPE.length()))) {
653                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
654             }
655             if (!CRINEX_PROG_DATE.equals(parseString(line2, LABEL_START, CRINEX_PROG_DATE.length()))) {
656                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
657             }
658 
659             // build the appropriate parser
660             return cVersion100 < 300 ? new CompactRinex1(name, reader) : new CompactRinex3(name, reader);
661 
662         }
663 
664         /** Extract a string from a line.
665          * @param line to parse
666          * @param start start index of the string
667          * @param length length of the string
668          * @return parsed string
669          */
670         public static String parseString(final CharSequence line, final int start, final int length) {
671             if (line.length() > start) {
672                 return line.subSequence(start, FastMath.min(line.length(), start + length)).toString().trim();
673             } else {
674                 return null;
675             }
676         }
677 
678         /** Extract an integer from a line.
679          * @param line to parse
680          * @param start start index of the integer
681          * @param length length of the integer
682          * @return parsed integer
683          */
684         public static int parseInt(final CharSequence line, final int start, final int length) {
685             if (line.length() > start && parseString(line, start, length).length() > 0) {
686                 return Integer.parseInt(parseString(line, start, length));
687             } else {
688                 return 0;
689             }
690         }
691 
692         /** Extract a double from a line.
693          * @param line to parse
694          * @param start start index of the real
695          * @param length length of the real
696          * @return parsed real, or {@code Double.NaN} if field was empty
697          */
698         public static double parseDouble(final CharSequence line, final int start, final int length) {
699             if (line.length() > start && parseString(line, start, length).length() > 0) {
700                 return Double.parseDouble(parseString(line, start, length));
701             } else {
702                 return Double.NaN;
703             }
704         }
705 
706         /** Trim trailing spaces in a builder.
707          * @param builder builder to trim
708          */
709         public static void trimTrailingSpaces(final StringBuilder builder) {
710             for (int i = builder.length() - 1; i >= 0 && builder.charAt(i) == ' '; --i) {
711                 builder.deleteCharAt(i);
712             }
713         }
714 
715         /** Enumerate for parsing sections. */
716         private enum Section {
717 
718             /** Header section. */
719             HEADER,
720 
721             /** Epoch and receiver clock offset section. */
722             EPOCH,
723 
724             /** Observation section. */
725             OBSERVATION
726 
727         }
728 
729     }
730 
731     /** Compact RINEX 1 format (for RINEX 2.x). */
732     private static class CompactRinex1 extends CompactRinexFormat {
733 
734         /** Label for number of observations. */
735         private static final String NB_TYPES_OF_OBSERV   = "# / TYPES OF OBSERV";
736 
737         /** Start of epoch field. */
738         private static final int    EPOCH_START          = 0;
739 
740         /** Length of epoch field. */
741         private static final int    EPOCH_LENGTH         = 32;
742 
743         /** Start of events flag. */
744         private static final int    EVENT_START          = EPOCH_START + EPOCH_LENGTH - 4;
745 
746         /** Start of number of satellites field. */
747         private static final int    NB_SAT_START         = EPOCH_START + EPOCH_LENGTH - 3;
748 
749         /** Start of satellites list field. */
750         private static final int    SAT_LIST_START       = EPOCH_START + EPOCH_LENGTH;
751 
752         /** Length of satellites list field. */
753         private static final int    SAT_LIST_LENGTH      = 36;
754 
755         /** Maximum number of satellites per epoch line. */
756         private static final int    MAX_SAT_EPOCH_LINE   = 12;
757 
758         /** Start of receiver clock field. */
759         private static final int    CLOCK_START          = SAT_LIST_START + SAT_LIST_LENGTH;
760 
761         /** Length of receiver clock field. */
762         private static final int    CLOCK_LENGTH         = 12;
763 
764         /** Number of decimal places for receiver clock offset. */
765         private static final int    CLOCK_DECIMAL_PLACES = 9;
766 
767         /** Length of a data field. */
768         private static final int    DATA_LENGTH          = 14;
769 
770         /** Number of decimal places for data fields. */
771         private static final int    DATA_DECIMAL_PLACES  = 3;
772 
773         /** Simple constructor.
774          * @param name file name
775          * @param reader line-oriented input
776          */
777         CompactRinex1(final String name, final BufferedReader reader) {
778             super(name, reader);
779         }
780 
781         @Override
782         /** {@inheritDoc} */
783         public CharSequence parseHeaderLine(final String line) {
784             if (isHeaderLine(NB_TYPES_OF_OBSERV, line)) {
785                 for (final SatelliteSystem system : SatelliteSystem.values()) {
786                     updateMaxObs(system, parseInt(line, 0, 6));
787                 }
788                 return line;
789             } else {
790                 return super.parseHeaderLine(line);
791             }
792         }
793 
794         @Override
795         /** {@inheritDoc} */
796         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
797             throws IOException {
798 
799             final StringBuilder builder = new StringBuilder();
800             doParseEpochAndClockLines(builder,
801                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
802                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
803                                       clockLine, '&');
804 
805             // build uncompressed lines, taking care of clock being put
806             // back in line 1 and satellites after 12th put in continuation lines
807             final List<String> satellites = getSatellites();
808             builder.append(getEpochPart());
809             int iSat = 0;
810             while (iSat < FastMath.min(satellites.size(), MAX_SAT_EPOCH_LINE)) {
811                 builder.append(satellites.get(iSat++));
812             }
813             if (getClockPart().length() > 0) {
814                 while (builder.length() < CLOCK_START) {
815                     builder.append(' ');
816                 }
817                 builder.append(getClockPart());
818             }
819 
820             while (iSat < satellites.size()) {
821                 // add a continuation line
822                 trimTrailingSpaces(builder);
823                 builder.append('\n');
824                 for (int k = 0; k < SAT_LIST_START; ++k) {
825                     builder.append(' ');
826                 }
827                 final int iSatStart = iSat;
828                 while (iSat < FastMath.min(satellites.size(), iSatStart + MAX_SAT_EPOCH_LINE)) {
829                     builder.append(satellites.get(iSat++));
830                 }
831             }
832             trimTrailingSpaces(builder);
833             return builder;
834 
835         }
836 
837         @Override
838         /** {@inheritDoc} */
839         public CharSequence parseObservationLines(final String[] observationLines) {
840 
841             // parse the observation lines
842             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);
843 
844             // build uncompressed lines
845             final StringBuilder builder = new StringBuilder();
846             for (final CharSequence sat : getSatellites()) {
847                 if (builder.length() > 0) {
848                     trimTrailingSpaces(builder);
849                     builder.append('\n');
850                 }
851                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
852                 final CharSequence          flags = cd.flags.getUncompressed();
853                 for (int i = 0; i < cd.observations.length; ++i) {
854                     if (i > 0 && i % 5 == 0) {
855                         trimTrailingSpaces(builder);
856                         builder.append('\n');
857                     }
858                     if (cd.observations[i] == null) {
859                         // missing observation
860                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
861                             builder.append(' ');
862                         }
863                     } else {
864                         builder.append(cd.observations[i].getUncompressed());
865                         if (2 * i < flags.length()) {
866                             builder.append(flags.charAt(2 * i));
867                         }
868                         if (2 * i + 1 < flags.length()) {
869                             builder.append(flags.charAt(2 * i + 1));
870                         }
871                     }
872                 }
873             }
874             trimTrailingSpaces(builder);
875             return builder;
876 
877         }
878 
879     }
880 
881     /** Compact RINEX 3 format (for RINEX 3.x). */
882     private static class CompactRinex3 extends CompactRinexFormat {
883 
884         /** Label for number of observation types. */
885         private static final String SYS_NB_OBS_TYPES     = "SYS / # / OBS TYPES";
886 
887         /** Start of epoch field. */
888         private static final int    EPOCH_START          = 0;
889 
890         /** Length of epoch field. */
891         private static final int    EPOCH_LENGTH         = 41;
892 
893         /** Start of receiver clock field. */
894         private static final int    CLOCK_START          = EPOCH_START + EPOCH_LENGTH;
895 
896         /** Length of receiver clock field. */
897         private static final int    CLOCK_LENGTH         = 15;
898 
899         /** Number of decimal places for receiver clock offset. */
900         private static final int    CLOCK_DECIMAL_PLACES = 12;
901 
902         /** Start of events flag. */
903         private static final int    EVENT_START          = EPOCH_START + EPOCH_LENGTH - 10;
904 
905         /** Start of number of satellites field. */
906         private static final int    NB_SAT_START         = EPOCH_START + EPOCH_LENGTH - 9;
907 
908         /** Start of satellites list field (only in the compact rinex). */
909         private static final int    SAT_LIST_START       = EPOCH_START + EPOCH_LENGTH;
910 
911         /** Length of a data field. */
912         private static final int    DATA_LENGTH          = 14;
913 
914         /** Number of decimal places for data fields. */
915         private static final int    DATA_DECIMAL_PLACES  = 3;
916 
917         /** Simple constructor.
918          * @param name file name
919          * @param reader line-oriented input
920          */
921         CompactRinex3(final String name, final BufferedReader reader) {
922             super(name, reader);
923         }
924 
925         @Override
926         /** {@inheritDoc} */
927         public CharSequence parseHeaderLine(final String line) {
928             if (isHeaderLine(SYS_NB_OBS_TYPES, line)) {
929                 if (line.charAt(0) != ' ') {
930                     // it is the first line of an observation types description
931                     // (continuation lines are ignored here)
932                     updateMaxObs(SatelliteSystem.parseSatelliteSystem(parseString(line, 0, 1)),
933                                  parseInt(line, 1, 5));
934                 }
935                 return line;
936             } else {
937                 return super.parseHeaderLine(line);
938             }
939         }
940 
941         @Override
942         /** {@inheritDoc} */
943         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
944             throws IOException {
945 
946             final StringBuilder builder = new StringBuilder();
947             doParseEpochAndClockLines(builder,
948                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
949                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
950                                       clockLine, '>');
951 
952             // build uncompressed line
953             builder.append(getEpochPart());
954             if (getClockPart().length() > 0) {
955                 while (builder.length() < CLOCK_START) {
956                     builder.append(' ');
957                 }
958                 builder.append(getClockPart());
959             }
960 
961             trimTrailingSpaces(builder);
962             return builder;
963 
964         }
965 
966         @Override
967         /** {@inheritDoc} */
968         public CharSequence parseObservationLines(final String[] observationLines) {
969 
970             // parse the observation lines
971             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);
972 
973             // build uncompressed lines
974             final StringBuilder builder = new StringBuilder();
975             for (final CharSequence sat : getSatellites()) {
976                 if (builder.length() > 0) {
977                     trimTrailingSpaces(builder);
978                     builder.append('\n');
979                 }
980                 builder.append(sat);
981                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
982                 final CharSequence          flags = cd.flags.getUncompressed();
983                 for (int i = 0; i < cd.observations.length; ++i) {
984                     if (cd.observations[i] == null) {
985                         // missing observation
986                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
987                             builder.append(' ');
988                         }
989                     } else {
990                         builder.append(cd.observations[i].getUncompressed());
991                         if (2 * i < flags.length()) {
992                             builder.append(flags.charAt(2 * i));
993                         }
994                         if (2 * i + 1 < flags.length()) {
995                             builder.append(flags.charAt(2 * i + 1));
996                         }
997                     }
998                 }
999             }
1000             trimTrailingSpaces(builder);
1001             return builder;
1002 
1003         }
1004 
1005     }
1006 
1007 }