HatanakaCompressFilter.java

  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;
  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. import org.hipparchus.util.FastMath;
  29. import org.orekit.data.DataFilter;
  30. import org.orekit.data.DataSource;
  31. import org.orekit.errors.OrekitException;
  32. import org.orekit.errors.OrekitMessages;
  33. import org.orekit.gnss.SatelliteSystem;

  34. /** Decompression filter for Hatanaka compressed RINEX files.
  35.  * @see <a href="http://cedadocs.ceda.ac.uk/1254/1/Hatanaka%5C_compressed%5C_format%5C_help.pdf">A
  36.  * Compression Format and Tools for GNSS Observation Data</a>
  37.  * @since 10.1
  38.  */
  39. public class HatanakaCompressFilter implements DataFilter {

  40.     /** Pattern for rinex 2 observation files. */
  41.     private static final Pattern RINEX_2_PATTERN = Pattern.compile("^(\\w{4}\\d{3}[0a-x](?:\\d{2})?\\.\\d{2})[dD]$");

  42.     /** Pattern for rinex 3 observation files. */
  43.     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$");

  44.     /** Empty constructor.
  45.      * <p>
  46.      * This constructor is not strictly necessary, but it prevents spurious
  47.      * javadoc warnings with JDK 18 and later.
  48.      * </p>
  49.      * @since 12.0
  50.      */
  51.     public HatanakaCompressFilter() {
  52.         // nothing to do
  53.     }

  54.     /** {@inheritDoc} */
  55.     @Override
  56.     public DataSource filter(final DataSource original) {

  57.         final String            oName   = original.getName();
  58.         final DataSource.Opener oOpener = original.getOpener();

  59.         final Matcher rinex2Matcher = RINEX_2_PATTERN.matcher(oName);
  60.         if (rinex2Matcher.matches()) {
  61.             // this is a rinex 2 file compressed with Hatanaka method
  62.             final String                  fName   = rinex2Matcher.group(1) + "o";
  63.             final DataSource.ReaderOpener fOpener = () -> new HatanakaReader(oName, oOpener.openReaderOnce());
  64.             return new DataSource(fName, fOpener);
  65.         }

  66.         final Matcher rinex3Matcher = RINEX_3_PATTERN.matcher(oName);
  67.         if (rinex3Matcher.matches()) {
  68.             // this is a rinex 3 file compressed with Hatanaka method
  69.             final String                  fName   = rinex3Matcher.group(1) + ".rnx";
  70.             final DataSource.ReaderOpener fOpener = () -> new HatanakaReader(oName, oOpener.openReaderOnce());
  71.             return new DataSource(fName, fOpener);
  72.         }

  73.         // it is not an Hatanaka compressed rinex file
  74.         return original;

  75.     }

  76.     /** Filtering of Hatanaka compressed characters stream. */
  77.     private static class HatanakaReader extends Reader {

  78.         /** Format of the current file. */
  79.         private final CompactRinexFormat format;

  80.         /** Line-oriented input. */
  81.         private final BufferedReader reader;

  82.         /** Pending uncompressed output lines. */
  83.         private CharSequence pending;

  84.         /** Number of characters already output in pending lines. */
  85.         private int countOut;

  86.         /** Simple constructor.
  87.          * @param name file name
  88.          * @param input underlying compressed stream
  89.          * @exception IOException if first lines cannot be read
  90.          */
  91.         HatanakaReader(final String name, final Reader input)
  92.             throws IOException {

  93.             reader = new BufferedReader(input);

  94.             // check header
  95.             format = CompactRinexFormat.getFormat(name, reader);

  96.             pending = null;

  97.         }

  98.         /** {@inheritDoc} */
  99.         @Override
  100.         public int read(final char[] b, final int offset, final int len) throws IOException {

  101.             if (pending == null) {
  102.                 // we need to read another section from the underlying characters stream and uncompress it
  103.                 countOut = 0;
  104.                 final String firstLine = reader.readLine();
  105.                 if (firstLine == null) {
  106.                     // there are no lines left
  107.                     return -1;
  108.                 } else {
  109.                     pending = format.uncompressSection(firstLine);
  110.                 }
  111.             }

  112.             // copy as many characters as possible from current line
  113.             int n = FastMath.min(len, pending.length() - countOut);
  114.             for (int i = 0; i < n; ++i) {
  115.                 b[offset + i] = pending.charAt(countOut + i);
  116.             }

  117.             if (n < len) {
  118.                 // line has been completed and we can still output end of line
  119.                 b[offset + n] = '\n';
  120.                 pending       = null;
  121.                 ++n;
  122.             } else {
  123.                 // there are still some pending characters
  124.                 countOut += n;
  125.             }

  126.             return n;

  127.         }

  128.         /** {@inheritDoc} */
  129.         @Override
  130.         public void close() throws IOException {
  131.             reader.close();
  132.         }

  133.     }

  134.     /** Processor handling differential compression for one numerical data field. */
  135.     private static class NumericDifferential {

  136.         /** Length of the uncompressed text field. */
  137.         private final int fieldLength;

  138.         /** Number of decimal places uncompressed text field. */
  139.         private final int decimalPlaces;

  140.         /** State vector. */
  141.         private final long[] state;

  142.         /** Number of components in the state vector. */
  143.         private int nbComponents;

  144.         /** Uncompressed value. */
  145.         private CharSequence uncompressed;

  146.         /** Simple constructor.
  147.          * @param fieldLength length of the uncompressed text field
  148.          * @param decimalPlaces number of decimal places uncompressed text field
  149.          * @param order differential order
  150.          */
  151.         NumericDifferential(final int fieldLength, final int decimalPlaces, final int order) {
  152.             this.fieldLength   = fieldLength;
  153.             this.decimalPlaces = decimalPlaces;
  154.             this.state         = new long[order + 1];
  155.             this.nbComponents  = 0;
  156.         }

  157.         /** Handle a new compressed value.
  158.          * @param sequence sequence containing the value to consider
  159.          */
  160.         public void accept(final CharSequence sequence) {

  161.             // store the value as the last component of state vector
  162.             state[nbComponents] = Long.parseLong(sequence.toString());

  163.             // update state vector
  164.             for (int i = nbComponents; i > 0; --i) {
  165.                 state[i - 1] += state[i];
  166.             }

  167.             if (++nbComponents == state.length) {
  168.                 // the state vector is full
  169.                 --nbComponents;
  170.             }

  171.             // output uncompressed value
  172.             final String unscaled = Long.toString(FastMath.abs(state[0]));
  173.             final int    length   = unscaled.length();
  174.             final int    digits   = FastMath.max(length, decimalPlaces);
  175.             final int    padding  = fieldLength - (digits + (state[0] < 0 ? 2 : 1));
  176.             final StringBuilder builder = new StringBuilder();
  177.             for (int i = 0; i < padding; ++i) {
  178.                 builder.append(' ');
  179.             }
  180.             if (state[0] < 0) {
  181.                 builder.append('-');
  182.             }
  183.             if (length > decimalPlaces) {
  184.                 builder.append(unscaled, 0, length - decimalPlaces);
  185.             }
  186.             builder.append('.');
  187.             for (int i = decimalPlaces; i > 0; --i) {
  188.                 builder.append(i > length ? '0' : unscaled.charAt(length - i));
  189.             }

  190.             uncompressed = builder;

  191.         }

  192.         /** Get a string representation of the uncompressed value.
  193.          * @return string representation of the uncompressed value
  194.          */
  195.         public CharSequence getUncompressed() {
  196.             return uncompressed;
  197.         }

  198.     }

  199.     /** Processor handling text compression for one text data field. */
  200.     private static class TextDifferential {

  201.         /** Buffer holding the current state. */
  202.         private CharBuffer state;

  203.         /** Simple constructor.
  204.          * @param fieldLength length of the uncompressed text field
  205.          */
  206.         TextDifferential(final int fieldLength) {
  207.             this.state = CharBuffer.allocate(fieldLength);
  208.             for (int i = 0; i < fieldLength; ++i) {
  209.                 state.put(i, ' ');
  210.             }
  211.         }

  212.         /** Handle a new compressed value.
  213.          * @param sequence sequence containing the value to consider
  214.          */
  215.         public void accept(final CharSequence sequence) {

  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.         /** Get a string representation of the uncompressed value.
  230.          * @return string representation of the uncompressed value
  231.          */
  232.         public CharSequence getUncompressed() {
  233.             return state;
  234.         }

  235.     }

  236.     /** Container for combined observations and flags. */
  237.     private static class CombinedDifferentials {

  238.         /** Observation differentials. */
  239.         private NumericDifferential[] observations;

  240.         /** Flags differential. */
  241.         private TextDifferential flags;

  242.         /** Simple constructor.
  243.          * Build an empty container.
  244.          * @param nbObs number of observations
  245.          */
  246.         CombinedDifferentials(final int nbObs) {
  247.             this.observations = new NumericDifferential[nbObs];
  248.             this.flags        = new TextDifferential(2 * nbObs);
  249.         }

  250.     }

  251.     /** Base class for parsing compact RINEX format. */
  252.     private abstract static class CompactRinexFormat {

  253.         /** Index of label in data lines. */
  254.         private static final int LABEL_START = 60;

  255.         /** Label for compact Rinex version. */
  256.         private static final String CRINEX_VERSION_TYPE  = "CRINEX VERS   / TYPE";

  257.         /** Label for compact Rinex program. */
  258.         private static final String CRINEX_PROG_DATE     = "CRINEX PROG / DATE";

  259.         /** Label for number of satellites. */
  260.         private static final String NB_OF_SATELLITES = "# OF SATELLITES";

  261.         /** Label for end of header. */
  262.         private static final String END_OF_HEADER    = "END OF HEADER";

  263.         /** Default number of satellites (used if not present in the file). */
  264.         private static final int DEFAULT_NB_SAT = 500;

  265.         /** File name. */
  266.         private final String name;

  267.         /** Line-oriented input. */
  268.         private final BufferedReader reader;

  269.         /** Current line number. */
  270.         private int lineNumber;

  271.         /** Maximum number of observations for one satellite. */
  272.         private final Map<SatelliteSystem, Integer> maxObs;

  273.         /** Number of satellites. */
  274.         private int nbSat;

  275.         /** Indicator for current section type. */
  276.         private Section section;

  277.         /** Satellites observed at current epoch. */
  278.         private List<String> satellites;

  279.         /** Differential engine for epoch. */
  280.         private TextDifferential epochDifferential;

  281.         /** Receiver clock offset differential. */
  282.         private NumericDifferential clockDifferential;

  283.         /** Differential engine for satellites list. */
  284.         private TextDifferential satListDifferential;

  285.         /** Differential engines for each satellite. */
  286.         private Map<String, CombinedDifferentials> differentials;

  287.         /** Simple constructor.
  288.          * @param name file name
  289.          * @param reader line-oriented input
  290.          */
  291.         protected CompactRinexFormat(final String name, final BufferedReader reader) {
  292.             this.name    = name;
  293.             this.reader  = reader;
  294.             this.maxObs  = new HashMap<>();
  295.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  296.                 maxObs.put(system, 0);
  297.             }
  298.             this.nbSat   = DEFAULT_NB_SAT;
  299.             this.section = Section.HEADER;
  300.         }

  301.         /** Uncompress a section.
  302.          * @param firstLine first line of the section
  303.          * @return uncompressed section (contains several lines)
  304.          * @exception IOException if we cannot read lines from underlying stream
  305.          */
  306.         public CharSequence uncompressSection(final String firstLine)
  307.             throws IOException {
  308.             final CharSequence uncompressed;
  309.             switch (section) {

  310.                 case HEADER : {
  311.                     // header lines
  312.                     final StringBuilder builder = new StringBuilder();
  313.                     String line = firstLine;
  314.                     lineNumber = 3; // there are 2 CRINEX lines before the RINEX header line
  315.                     while (section == Section.HEADER) {
  316.                         if (builder.length() > 0) {
  317.                             builder.append('\n');
  318.                             line = readLine();
  319.                         }
  320.                         builder.append(parseHeaderLine(line));
  321.                         trimTrailingSpaces(builder);
  322.                     }
  323.                     uncompressed = builder;
  324.                     section      = Section.EPOCH;
  325.                     break;
  326.                 }

  327.                 case EPOCH : {
  328.                     // epoch and receiver clock offset lines
  329.                     ++lineNumber; // the caller has read one epoch line
  330.                     uncompressed = parseEpochAndClockLines(firstLine, readLine().trim());
  331.                     section      = Section.OBSERVATION;
  332.                     break;
  333.                 }

  334.                 default : {
  335.                     // observation lines
  336.                     final String[] lines = new String[satellites.size()];
  337.                     ++lineNumber; // the caller has read one observation line
  338.                     lines[0] = firstLine;
  339.                     for (int i = 1; i < lines.length; ++i) {
  340.                         lines[i] = readLine();
  341.                     }
  342.                     uncompressed = parseObservationLines(lines);
  343.                     section      = Section.EPOCH;
  344.                 }

  345.             }

  346.             return uncompressed;

  347.         }

  348.         /** Parse a header line.
  349.          * @param line header line
  350.          * @return uncompressed line
  351.          */
  352.         public CharSequence parseHeaderLine(final String line) {

  353.             if (isHeaderLine(NB_OF_SATELLITES, line)) {
  354.                 // number of satellites
  355.                 nbSat = parseInt(line, 0, 6);
  356.             } else if (isHeaderLine(END_OF_HEADER, line)) {
  357.                 // we have reached end of header, prepare parsing of data records
  358.                 section = Section.EPOCH;
  359.             }

  360.             // within header, lines are simply copied
  361.             return line;

  362.         }

  363.         /** Parse epoch and receiver clock offset lines.
  364.          * @param epochLine epoch line
  365.          * @param clockLine receiver clock offset line
  366.          * @return uncompressed line
  367.          * @exception IOException if we cannot read additional special events lines
  368.          */
  369.         public abstract CharSequence parseEpochAndClockLines(String epochLine, String clockLine)
  370.             throws IOException;

  371.         /** Parse epoch and receiver clock offset lines.
  372.          * @param builder builder that may used to copy special event lines
  373.          * @param epochStart start of the epoch field
  374.          * @param epochLength length of epoch field
  375.          * @param eventStart start of the special events field
  376.          * @param nbSatStart start of the number of satellites field
  377.          * @param satListStart start of the satellites list
  378.          * @param clockLength length of receiver clock field
  379.          * @param clockDecimalPlaces number of decimal places for receiver clock offset
  380.          * @param epochLine epoch line
  381.          * @param clockLine receiver clock offset line
  382.          * @param resetChar character indicating differentials reset
  383.          * @exception IOException if we cannot read additional special events lines
  384.          */
  385.         protected void doParseEpochAndClockLines(final StringBuilder builder,
  386.                                                  final int epochStart, final int epochLength,
  387.                                                  final int eventStart, final int nbSatStart, final int satListStart,
  388.                                                  final int clockLength, final int clockDecimalPlaces,
  389.                                                  final String epochLine,
  390.                                                  final String clockLine, final char resetChar)
  391.             throws IOException {

  392.             boolean loop = true;
  393.             String loopEpochLine = epochLine;
  394.             String loopClockLine = clockLine;
  395.             while (loop) {

  396.                 // check if differentials should be reset
  397.                 if (epochDifferential == null || loopEpochLine.charAt(0) == resetChar) {
  398.                     epochDifferential   = new TextDifferential(epochLength);
  399.                     satListDifferential = new TextDifferential(nbSat * 3);
  400.                     differentials       = new HashMap<>();
  401.                 }

  402.                 // check for special events
  403.                 epochDifferential.accept(loopEpochLine.subSequence(epochStart,
  404.                                                                    FastMath.min(loopEpochLine.length(), epochStart + epochLength)));
  405.                 if (parseInt(epochDifferential.getUncompressed(), eventStart, 1) > 1) {
  406.                     // this was not really the epoch, but rather a special event
  407.                     // we just copy the lines and skip to real epoch and clock lines
  408.                     builder.append(epochDifferential.getUncompressed());
  409.                     trimTrailingSpaces(builder);
  410.                     builder.append('\n');
  411.                     final int skippedLines = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
  412.                     for (int i = 0; i < skippedLines; ++i) {
  413.                         builder.append(loopClockLine);
  414.                         trimTrailingSpaces(builder);
  415.                         builder.append('\n');
  416.                         loopClockLine = readLine();
  417.                     }

  418.                     // the epoch and clock are in the next lines
  419.                     loopEpochLine = loopClockLine;
  420.                     loopClockLine = readLine();
  421.                     loop = true;

  422.                 } else {
  423.                     loop = false;
  424.                     final int n = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
  425.                     satellites = new ArrayList<>(n);
  426.                     if (satListStart < loopEpochLine.length()) {
  427.                         satListDifferential.accept(loopEpochLine.subSequence(satListStart, loopEpochLine.length()));
  428.                     }
  429.                     final CharSequence satListPart = satListDifferential.getUncompressed();
  430.                     for (int i = 0; i < n; ++i) {
  431.                         satellites.add(satListPart.subSequence(i * 3, (i + 1) * 3).toString());
  432.                     }

  433.                     // parse clock offset
  434.                     if (!loopClockLine.isEmpty()) {
  435.                         if (loopClockLine.length() > 2 && loopClockLine.charAt(1) == '&') {
  436.                             clockDifferential = new NumericDifferential(clockLength, clockDecimalPlaces, parseInt(loopClockLine, 0, 1));
  437.                             clockDifferential.accept(loopClockLine.subSequence(2, loopClockLine.length()));
  438.                         } else if (clockDifferential == null) {
  439.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  440.                                                       lineNumber, name, loopClockLine);
  441.                         } else {
  442.                             clockDifferential.accept(loopClockLine);
  443.                         }
  444.                     }
  445.                 }
  446.             }

  447.         }

  448.         /** Get the uncompressed epoch part.
  449.          * @return uncompressed epoch part
  450.          */
  451.         protected CharSequence getEpochPart() {
  452.             return epochDifferential.getUncompressed();
  453.         }

  454.         /** Get the uncompressed clock part.
  455.          * @return uncompressed clock part
  456.          */
  457.         protected CharSequence getClockPart() {
  458.             return clockDifferential == null ? "" : clockDifferential.getUncompressed();
  459.         }

  460.         /** Get the satellites for current observations.
  461.          * @return satellites for current observation
  462.          */
  463.         protected List<String> getSatellites() {
  464.             return satellites;
  465.         }

  466.         /** Get the combined differentials for one satellite.
  467.          * @param sat satellite id
  468.          * @return observationDifferentials
  469.          */
  470.         protected CombinedDifferentials getCombinedDifferentials(final CharSequence sat) {
  471.             return differentials.get(sat);
  472.         }

  473.         /** Parse observation lines.
  474.          * @param observationLines observation lines
  475.          * @return uncompressed lines
  476.          */
  477.         public abstract CharSequence parseObservationLines(String[] observationLines);

  478.         /** Parse observation lines.
  479.          * @param dataLength length of data fields
  480.          * @param dataDecimalPlaces number of decimal places for data fields
  481.          * @param observationLines observation lines
  482.          */
  483.         protected void doParseObservationLines(final int dataLength, final int dataDecimalPlaces,
  484.                                                final String[] observationLines) {

  485.             for (int i = 0; i < observationLines.length; ++i) {

  486.                 final CharSequence line = observationLines[i];

  487.                 // get the differentials associated with this observations line
  488.                 final String sat = satellites.get(i);
  489.                 CombinedDifferentials satDiffs = differentials.get(sat);
  490.                 if (satDiffs == null) {
  491.                     final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(sat.subSequence(0, 1).toString());
  492.                     satDiffs = new CombinedDifferentials(maxObs.get(system));
  493.                     differentials.put(sat, satDiffs);
  494.                 }

  495.                 // parse observations
  496.                 int k = 0;
  497.                 for (int j = 0; j < satDiffs.observations.length; ++j) {

  498.                     if (k >= line.length() || line.charAt(k) == ' ') {
  499.                         // the data field is missing
  500.                         satDiffs.observations[j] = null;
  501.                     } else {
  502.                         // the data field is present

  503.                         if (k + 1 < line.length() &&
  504.                             Character.isDigit(line.charAt(k)) &&
  505.                             line.charAt(k + 1) == '&') {
  506.                             // reinitialize differentials
  507.                             satDiffs.observations[j] = new NumericDifferential(dataLength, dataDecimalPlaces,
  508.                                                                                Character.digit(line.charAt(k), 10));
  509.                             k += 2;
  510.                         }

  511.                         // extract the compressed differenced value
  512.                         final int start = k;
  513.                         while (k < line.length() && line.charAt(k) != ' ') {
  514.                             ++k;
  515.                         }
  516.                         try {
  517.                             satDiffs.observations[j].accept(line.subSequence(start, k));
  518.                         } catch (NumberFormatException nfe) {
  519.                             throw new OrekitException(nfe,
  520.                                                       OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  521.                                                       lineNumber + i - (observationLines.length - 1),
  522.                                                       name, observationLines[i]);
  523.                         }

  524.                     }

  525.                     // skip blank separator
  526.                     ++k;

  527.                 }

  528.                 if (k < line.length()) {
  529.                     satDiffs.flags.accept(line.subSequence(k, line.length()));
  530.                 }

  531.             }

  532.         }

  533.         /** Check if a line corresponds to a header.
  534.          * @param label header label
  535.          * @param line header line
  536.          * @return true if line corresponds to header
  537.          */
  538.         protected boolean isHeaderLine(final String label, final String line) {
  539.             return label.equals(parseString(line, LABEL_START, label.length()));
  540.         }

  541.         /** Update the max number of observations.
  542.          * @param system satellite system
  543.          * @param nbObs number of observations
  544.          */
  545.         protected void updateMaxObs(final SatelliteSystem system, final int nbObs) {
  546.             maxObs.put(system, FastMath.max(maxObs.get(system), nbObs));
  547.         }

  548.         /** Read a new line.
  549.          * @return line read
  550.          * @exception IOException if a read error occurs
  551.          */
  552.         private String readLine()
  553.             throws IOException {
  554.             final String line = reader.readLine();
  555.             if (line == null) {
  556.                 throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE, name);
  557.             }
  558.             lineNumber++;
  559.             return line;
  560.         }

  561.         /** Get the rinex format corresponding to this compact rinex format.
  562.          * @param name file name
  563.          * @param reader line-oriented input
  564.          * @return rinex format associated with this compact rinex format
  565.          * @exception IOException if first lines cannot be read
  566.          */
  567.         public static CompactRinexFormat getFormat(final String name, final BufferedReader reader)
  568.             throws IOException {

  569.             // read the first two lines of the file
  570.             final String line1 = reader.readLine();
  571.             final String line2 = reader.readLine();
  572.             if (line1 == null || line2 == null) {
  573.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  574.             }

  575.             // extract format version
  576.             final int cVersion100 = (int) FastMath.rint(100 * parseDouble(line1, 0, 9));
  577.             if (cVersion100 != 100 && cVersion100 != 300) {
  578.                 throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT, name);
  579.             }
  580.             if (!CRINEX_VERSION_TYPE.equals(parseString(line1, LABEL_START, CRINEX_VERSION_TYPE.length()))) {
  581.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  582.             }
  583.             if (!CRINEX_PROG_DATE.equals(parseString(line2, LABEL_START, CRINEX_PROG_DATE.length()))) {
  584.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  585.             }

  586.             // build the appropriate parser
  587.             return cVersion100 < 300 ? new CompactRinex1(name, reader) : new CompactRinex3(name, reader);

  588.         }

  589.         /** Extract a string from a line.
  590.          * @param line to parse
  591.          * @param start start index of the string
  592.          * @param length length of the string
  593.          * @return parsed string
  594.          */
  595.         public static String parseString(final CharSequence line, final int start, final int length) {
  596.             if (line.length() > start) {
  597.                 return line.subSequence(start, FastMath.min(line.length(), start + length)).toString().trim();
  598.             } else {
  599.                 return null;
  600.             }
  601.         }

  602.         /** Extract an integer from a line.
  603.          * @param line to parse
  604.          * @param start start index of the integer
  605.          * @param length length of the integer
  606.          * @return parsed integer
  607.          */
  608.         public static int parseInt(final CharSequence line, final int start, final int length) {
  609.             if (line.length() > start && parseString(line, start, length).length() > 0) {
  610.                 return Integer.parseInt(parseString(line, start, length));
  611.             } else {
  612.                 return 0;
  613.             }
  614.         }

  615.         /** Extract a double from a line.
  616.          * @param line to parse
  617.          * @param start start index of the real
  618.          * @param length length of the real
  619.          * @return parsed real, or {@code Double.NaN} if field was empty
  620.          */
  621.         public static double parseDouble(final CharSequence line, final int start, final int length) {
  622.             if (line.length() > start && parseString(line, start, length).length() > 0) {
  623.                 return Double.parseDouble(parseString(line, start, length));
  624.             } else {
  625.                 return Double.NaN;
  626.             }
  627.         }

  628.         /** Trim trailing spaces in a builder.
  629.          * @param builder builder to trim
  630.          */
  631.         public static void trimTrailingSpaces(final StringBuilder builder) {
  632.             for (int i = builder.length() - 1; i >= 0 && builder.charAt(i) == ' '; --i) {
  633.                 builder.deleteCharAt(i);
  634.             }
  635.         }

  636.         /** Enumerate for parsing sections. */
  637.         private enum Section {

  638.             /** Header section. */
  639.             HEADER,

  640.             /** Epoch and receiver clock offset section. */
  641.             EPOCH,

  642.             /** Observation section. */
  643.             OBSERVATION;

  644.         }

  645.     }

  646.     /** Compact RINEX 1 format (for RINEX 2.x). */
  647.     private static class CompactRinex1 extends CompactRinexFormat {

  648.         /** Label for number of observations. */
  649.         private static final String NB_TYPES_OF_OBSERV   = "# / TYPES OF OBSERV";

  650.         /** Start of epoch field. */
  651.         private static final int    EPOCH_START          = 0;

  652.         /** Length of epoch field. */
  653.         private static final int    EPOCH_LENGTH         = 32;

  654.         /** Start of events flag. */
  655.         private static final int    EVENT_START          = EPOCH_START + EPOCH_LENGTH - 4;

  656.         /** Start of number of satellites field. */
  657.         private static final int    NB_SAT_START         = EPOCH_START + EPOCH_LENGTH - 3;

  658.         /** Start of satellites list field. */
  659.         private static final int    SAT_LIST_START       = EPOCH_START + EPOCH_LENGTH;

  660.         /** Length of satellites list field. */
  661.         private static final int    SAT_LIST_LENGTH      = 36;

  662.         /** Maximum number of satellites per epoch line. */
  663.         private static final int    MAX_SAT_EPOCH_LINE   = 12;

  664.         /** Start of receiver clock field. */
  665.         private static final int    CLOCK_START          = SAT_LIST_START + SAT_LIST_LENGTH;

  666.         /** Length of receiver clock field. */
  667.         private static final int    CLOCK_LENGTH         = 12;

  668.         /** Number of decimal places for receiver clock offset. */
  669.         private static final int    CLOCK_DECIMAL_PLACES = 9;

  670.         /** Length of a data field. */
  671.         private static final int    DATA_LENGTH          = 14;

  672.         /** Number of decimal places for data fields. */
  673.         private static final int    DATA_DECIMAL_PLACES  = 3;

  674.         /** Simple constructor.
  675.          * @param name file name
  676.          * @param reader line-oriented input
  677.          */
  678.         CompactRinex1(final String name, final BufferedReader reader) {
  679.             super(name, reader);
  680.         }

  681.         @Override
  682.         /** {@inheritDoc} */
  683.         public CharSequence parseHeaderLine(final String line) {
  684.             if (isHeaderLine(NB_TYPES_OF_OBSERV, line)) {
  685.                 for (final SatelliteSystem system : SatelliteSystem.values()) {
  686.                     updateMaxObs(system, parseInt(line, 0, 6));
  687.                 }
  688.                 return line;
  689.             } else {
  690.                 return super.parseHeaderLine(line);
  691.             }
  692.         }

  693.         @Override
  694.         /** {@inheritDoc} */
  695.         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
  696.             throws IOException {

  697.             final StringBuilder builder = new StringBuilder();
  698.             doParseEpochAndClockLines(builder,
  699.                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
  700.                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
  701.                                       clockLine, '&');

  702.             // build uncompressed lines, taking care of clock being put
  703.             // back in line 1 and satellites after 12th put in continuation lines
  704.             final List<String> satellites = getSatellites();
  705.             builder.append(getEpochPart());
  706.             int iSat = 0;
  707.             while (iSat < FastMath.min(satellites.size(), MAX_SAT_EPOCH_LINE)) {
  708.                 builder.append(satellites.get(iSat++));
  709.             }
  710.             if (getClockPart().length() > 0) {
  711.                 while (builder.length() < CLOCK_START) {
  712.                     builder.append(' ');
  713.                 }
  714.                 builder.append(getClockPart());
  715.             }

  716.             while (iSat < satellites.size()) {
  717.                 // add a continuation line
  718.                 trimTrailingSpaces(builder);
  719.                 builder.append('\n');
  720.                 for (int k = 0; k < SAT_LIST_START; ++k) {
  721.                     builder.append(' ');
  722.                 }
  723.                 final int iSatStart = iSat;
  724.                 while (iSat < FastMath.min(satellites.size(), iSatStart + MAX_SAT_EPOCH_LINE)) {
  725.                     builder.append(satellites.get(iSat++));
  726.                 }
  727.             }
  728.             trimTrailingSpaces(builder);
  729.             return builder;

  730.         }

  731.         @Override
  732.         /** {@inheritDoc} */
  733.         public CharSequence parseObservationLines(final String[] observationLines) {

  734.             // parse the observation lines
  735.             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);

  736.             // build uncompressed lines
  737.             final StringBuilder builder = new StringBuilder();
  738.             for (final CharSequence sat : getSatellites()) {
  739.                 if (builder.length() > 0) {
  740.                     trimTrailingSpaces(builder);
  741.                     builder.append('\n');
  742.                 }
  743.                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
  744.                 final CharSequence          flags = cd.flags.getUncompressed();
  745.                 for (int i = 0; i < cd.observations.length; ++i) {
  746.                     if (i > 0 && i % 5 == 0) {
  747.                         trimTrailingSpaces(builder);
  748.                         builder.append('\n');
  749.                     }
  750.                     if (cd.observations[i] == null) {
  751.                         // missing observation
  752.                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
  753.                             builder.append(' ');
  754.                         }
  755.                     } else {
  756.                         builder.append(cd.observations[i].getUncompressed());
  757.                         if (2 * i < flags.length()) {
  758.                             builder.append(flags.charAt(2 * i));
  759.                         }
  760.                         if (2 * i + 1 < flags.length()) {
  761.                             builder.append(flags.charAt(2 * i + 1));
  762.                         }
  763.                     }
  764.                 }
  765.             }
  766.             trimTrailingSpaces(builder);
  767.             return builder;

  768.         }

  769.     }

  770.     /** Compact RINEX 3 format (for RINEX 3.x). */
  771.     private static class CompactRinex3 extends CompactRinexFormat {

  772.         /** Label for number of observation types. */
  773.         private static final String SYS_NB_OBS_TYPES     = "SYS / # / OBS TYPES";

  774.         /** Start of epoch field. */
  775.         private static final int    EPOCH_START          = 0;

  776.         /** Length of epoch field. */
  777.         private static final int    EPOCH_LENGTH         = 41;

  778.         /** Start of receiver clock field. */
  779.         private static final int    CLOCK_START          = EPOCH_START + EPOCH_LENGTH;

  780.         /** Length of receiver clock field. */
  781.         private static final int    CLOCK_LENGTH         = 15;

  782.         /** Number of decimal places for receiver clock offset. */
  783.         private static final int    CLOCK_DECIMAL_PLACES = 12;

  784.         /** Start of events flag. */
  785.         private static final int    EVENT_START          = EPOCH_START + EPOCH_LENGTH - 10;

  786.         /** Start of number of satellites field. */
  787.         private static final int    NB_SAT_START         = EPOCH_START + EPOCH_LENGTH - 9;

  788.         /** Start of satellites list field (only in the compact rinex). */
  789.         private static final int    SAT_LIST_START       = EPOCH_START + EPOCH_LENGTH;

  790.         /** Length of a data field. */
  791.         private static final int    DATA_LENGTH          = 14;

  792.         /** Number of decimal places for data fields. */
  793.         private static final int    DATA_DECIMAL_PLACES  = 3;

  794.         /** Simple constructor.
  795.          * @param name file name
  796.          * @param reader line-oriented input
  797.          */
  798.         CompactRinex3(final String name, final BufferedReader reader) {
  799.             super(name, reader);
  800.         }

  801.         @Override
  802.         /** {@inheritDoc} */
  803.         public CharSequence parseHeaderLine(final String line) {
  804.             if (isHeaderLine(SYS_NB_OBS_TYPES, line)) {
  805.                 if (line.charAt(0) != ' ') {
  806.                     // it is the first line of an observation types description
  807.                     // (continuation lines are ignored here)
  808.                     updateMaxObs(SatelliteSystem.parseSatelliteSystem(parseString(line, 0, 1)),
  809.                                  parseInt(line, 1, 5));
  810.                 }
  811.                 return line;
  812.             } else {
  813.                 return super.parseHeaderLine(line);
  814.             }
  815.         }

  816.         @Override
  817.         /** {@inheritDoc} */
  818.         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
  819.             throws IOException {

  820.             final StringBuilder builder = new StringBuilder();
  821.             doParseEpochAndClockLines(builder,
  822.                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
  823.                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
  824.                                       clockLine, '>');

  825.             // build uncompressed line
  826.             builder.append(getEpochPart());
  827.             if (getClockPart().length() > 0) {
  828.                 while (builder.length() < CLOCK_START) {
  829.                     builder.append(' ');
  830.                 }
  831.                 builder.append(getClockPart());
  832.             }

  833.             trimTrailingSpaces(builder);
  834.             return builder;

  835.         }

  836.         @Override
  837.         /** {@inheritDoc} */
  838.         public CharSequence parseObservationLines(final String[] observationLines) {

  839.             // parse the observation lines
  840.             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);

  841.             // build uncompressed lines
  842.             final StringBuilder builder = new StringBuilder();
  843.             for (final CharSequence sat : getSatellites()) {
  844.                 if (builder.length() > 0) {
  845.                     trimTrailingSpaces(builder);
  846.                     builder.append('\n');
  847.                 }
  848.                 builder.append(sat);
  849.                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
  850.                 final CharSequence          flags = cd.flags.getUncompressed();
  851.                 for (int i = 0; i < cd.observations.length; ++i) {
  852.                     if (cd.observations[i] == null) {
  853.                         // missing observation
  854.                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
  855.                             builder.append(' ');
  856.                         }
  857.                     } else {
  858.                         builder.append(cd.observations[i].getUncompressed());
  859.                         if (2 * i < flags.length()) {
  860.                             builder.append(flags.charAt(2 * i));
  861.                         }
  862.                         if (2 * i + 1 < flags.length()) {
  863.                             builder.append(flags.charAt(2 * i + 1));
  864.                         }
  865.                     }
  866.                 }
  867.             }
  868.             trimTrailingSpaces(builder);
  869.             return builder;

  870.         }

  871.     }

  872. }