HatanakaCompressFilter.java

  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;
  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.data.LineOrientedFilteringReader;
  32. import org.orekit.errors.OrekitException;
  33. import org.orekit.errors.OrekitMessages;
  34. import org.orekit.gnss.SatelliteSystem;

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

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

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

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

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

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

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

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

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

  76.     }

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

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

  81.         /** Simple constructor.
  82.          * @param name file name
  83.          * @param input underlying compressed stream
  84.          * @exception IOException if first lines cannot be read
  85.          */
  86.         HatanakaReader(final String name, final Reader input)
  87.             throws IOException {
  88.             super(name, input);
  89.             format = CompactRinexFormat.getFormat(name, getBufferedReader());
  90.         }

  91.         /** {@inheritDoc} */
  92.         @Override
  93.         protected CharSequence filterLine(final int lineNumber, final String originalLine) throws IOException {
  94.             return format.uncompressSection(originalLine);
  95.         }

  96.     }

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

  99.         /** Length of the uncompressed text field. */
  100.         private final int fieldLength;

  101.         /** Number of decimal places uncompressed text field. */
  102.         private final int decimalPlaces;

  103.         /** State vector. */
  104.         private final long[] state;

  105.         /** Number of components in the state vector. */
  106.         private int nbComponents;

  107.         /** Uncompressed value. */
  108.         private CharSequence uncompressed;

  109.         /** Simple constructor.
  110.          * @param fieldLength length of the uncompressed text field
  111.          * @param decimalPlaces number of decimal places uncompressed text field
  112.          * @param order differential order
  113.          */
  114.         NumericDifferential(final int fieldLength, final int decimalPlaces, final int order) {
  115.             this.fieldLength   = fieldLength;
  116.             this.decimalPlaces = decimalPlaces;
  117.             this.state         = new long[order + 1];
  118.             this.nbComponents  = 0;
  119.         }

  120.         /** Handle a new compressed value.
  121.          * @param sequence sequence containing the value to consider
  122.          */
  123.         public void accept(final CharSequence sequence) {

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

  126.             // update state vector
  127.             for (int i = nbComponents; i > 0; --i) {
  128.                 state[i - 1] += state[i];
  129.             }

  130.             if (++nbComponents == state.length) {
  131.                 // the state vector is full
  132.                 --nbComponents;
  133.             }

  134.             // output uncompressed value
  135.             final String unscaled = Long.toString(FastMath.abs(state[0]));
  136.             final int    length   = unscaled.length();
  137.             final int    digits   = FastMath.max(length, decimalPlaces);
  138.             final int    padding  = fieldLength - (digits + (state[0] < 0 ? 2 : 1));
  139.             final StringBuilder builder = new StringBuilder();
  140.             for (int i = 0; i < padding; ++i) {
  141.                 builder.append(' ');
  142.             }
  143.             if (state[0] < 0) {
  144.                 builder.append('-');
  145.             }
  146.             if (length > decimalPlaces) {
  147.                 builder.append(unscaled, 0, length - decimalPlaces);
  148.             }
  149.             builder.append('.');
  150.             for (int i = decimalPlaces; i > 0; --i) {
  151.                 builder.append(i > length ? '0' : unscaled.charAt(length - i));
  152.             }

  153.             uncompressed = builder;

  154.         }

  155.         /** Get a string representation of the uncompressed value.
  156.          * @return string representation of the uncompressed value
  157.          */
  158.         public CharSequence getUncompressed() {
  159.             return uncompressed;
  160.         }

  161.     }

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

  164.         /** Buffer holding the current state. */
  165.         private CharBuffer state;

  166.         /** Simple constructor.
  167.          * @param fieldLength length of the uncompressed text field
  168.          */
  169.         TextDifferential(final int fieldLength) {
  170.             this.state = CharBuffer.allocate(fieldLength);
  171.             for (int i = 0; i < fieldLength; ++i) {
  172.                 state.put(i, ' ');
  173.             }
  174.         }

  175.         /** Handle a new compressed value.
  176.          * @param sequence sequence containing the value to consider
  177.          */
  178.         public void accept(final CharSequence sequence) {

  179.             // update state
  180.             final int length = FastMath.min(state.capacity(), sequence.length());
  181.             for (int i = 0; i < length; ++i) {
  182.                 final char c = sequence.charAt(i);
  183.                 if (c == '&') {
  184.                     // update state with disappearing character
  185.                     state.put(i, ' ');
  186.                 } else if (c != ' ') {
  187.                     // update state with changed character
  188.                     state.put(i, c);
  189.                 }
  190.             }

  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 state;
  197.         }

  198.     }

  199.     /** Container for combined observations and flags. */
  200.     private static class CombinedDifferentials {

  201.         /** Observation differentials. */
  202.         private NumericDifferential[] observations;

  203.         /** Flags differential. */
  204.         private TextDifferential flags;

  205.         /** Simple constructor.
  206.          * Build an empty container.
  207.          * @param nbObs number of observations
  208.          */
  209.         CombinedDifferentials(final int nbObs) {
  210.             this.observations = new NumericDifferential[nbObs];
  211.             this.flags        = new TextDifferential(2 * nbObs);
  212.         }

  213.     }

  214.     /** Base class for parsing compact RINEX format. */
  215.     private abstract static class CompactRinexFormat {

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

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

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

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

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

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

  228.         /** File name. */
  229.         private final String name;

  230.         /** Line-oriented input. */
  231.         private final BufferedReader reader;

  232.         /** Current line number. */
  233.         private int lineNumber;

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

  236.         /** Number of satellites. */
  237.         private int nbSat;

  238.         /** Indicator for current section type. */
  239.         private Section section;

  240.         /** Satellites observed at current epoch. */
  241.         private List<String> satellites;

  242.         /** Differential engine for epoch. */
  243.         private TextDifferential epochDifferential;

  244.         /** Receiver clock offset differential. */
  245.         private NumericDifferential clockDifferential;

  246.         /** Differential engine for satellites list. */
  247.         private TextDifferential satListDifferential;

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

  250.         /** Simple constructor.
  251.          * @param name file name
  252.          * @param reader line-oriented input
  253.          */
  254.         protected CompactRinexFormat(final String name, final BufferedReader reader) {
  255.             this.name    = name;
  256.             this.reader  = reader;
  257.             this.maxObs  = new HashMap<>();
  258.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  259.                 maxObs.put(system, 0);
  260.             }
  261.             this.nbSat   = DEFAULT_NB_SAT;
  262.             this.section = Section.HEADER;
  263.         }

  264.         /** Uncompress a section.
  265.          * @param firstLine first line of the section
  266.          * @return uncompressed section (contains several lines)
  267.          * @exception IOException if we cannot read lines from underlying stream
  268.          */
  269.         public CharSequence uncompressSection(final String firstLine)
  270.             throws IOException {
  271.             final CharSequence uncompressed;
  272.             switch (section) {

  273.                 case HEADER : {
  274.                     // header lines
  275.                     final StringBuilder builder = new StringBuilder();
  276.                     String line = firstLine;
  277.                     lineNumber = 3; // there are 2 CRINEX lines before the RINEX header line
  278.                     while (section == Section.HEADER) {
  279.                         if (builder.length() > 0) {
  280.                             builder.append('\n');
  281.                             line = readLine();
  282.                         }
  283.                         builder.append(parseHeaderLine(line));
  284.                         trimTrailingSpaces(builder);
  285.                     }
  286.                     uncompressed = builder;
  287.                     section      = Section.EPOCH;
  288.                     break;
  289.                 }

  290.                 case EPOCH : {
  291.                     // epoch and receiver clock offset lines
  292.                     ++lineNumber; // the caller has read one epoch line
  293.                     uncompressed = parseEpochAndClockLines(firstLine, readLine().trim());
  294.                     section      = Section.OBSERVATION;
  295.                     break;
  296.                 }

  297.                 default : {
  298.                     // observation lines
  299.                     final String[] lines = new String[satellites.size()];
  300.                     ++lineNumber; // the caller has read one observation line
  301.                     lines[0] = firstLine;
  302.                     for (int i = 1; i < lines.length; ++i) {
  303.                         lines[i] = readLine();
  304.                     }
  305.                     uncompressed = parseObservationLines(lines);
  306.                     section      = Section.EPOCH;
  307.                 }

  308.             }

  309.             return uncompressed;

  310.         }

  311.         /** Parse a header line.
  312.          * @param line header line
  313.          * @return uncompressed line
  314.          */
  315.         public CharSequence parseHeaderLine(final String line) {

  316.             if (isHeaderLine(NB_OF_SATELLITES, line)) {
  317.                 // number of satellites
  318.                 nbSat = parseInt(line, 0, 6);
  319.             } else if (isHeaderLine(END_OF_HEADER, line)) {
  320.                 // we have reached end of header, prepare parsing of data records
  321.                 section = Section.EPOCH;
  322.             }

  323.             // within header, lines are simply copied
  324.             return line;

  325.         }

  326.         /** Parse epoch and receiver clock offset lines.
  327.          * @param epochLine epoch line
  328.          * @param clockLine receiver clock offset line
  329.          * @return uncompressed line
  330.          * @exception IOException if we cannot read additional special events lines
  331.          */
  332.         public abstract CharSequence parseEpochAndClockLines(String epochLine, String clockLine)
  333.             throws IOException;

  334.         /** Parse epoch and receiver clock offset lines.
  335.          * @param builder builder that may used to copy special event lines
  336.          * @param epochStart start of the epoch field
  337.          * @param epochLength length of epoch field
  338.          * @param eventStart start of the special events field
  339.          * @param nbSatStart start of the number of satellites field
  340.          * @param satListStart start of the satellites list
  341.          * @param clockLength length of receiver clock field
  342.          * @param clockDecimalPlaces number of decimal places for receiver clock offset
  343.          * @param epochLine epoch line
  344.          * @param clockLine receiver clock offset line
  345.          * @param resetChar character indicating differentials reset
  346.          * @exception IOException if we cannot read additional special events lines
  347.          */
  348.         protected void doParseEpochAndClockLines(final StringBuilder builder,
  349.                                                  final int epochStart, final int epochLength,
  350.                                                  final int eventStart, final int nbSatStart, final int satListStart,
  351.                                                  final int clockLength, final int clockDecimalPlaces,
  352.                                                  final String epochLine,
  353.                                                  final String clockLine, final char resetChar)
  354.             throws IOException {

  355.             boolean loop = true;
  356.             String loopEpochLine = epochLine;
  357.             String loopClockLine = clockLine;
  358.             while (loop) {

  359.                 // check if differentials should be reset
  360.                 if (epochDifferential == null || loopEpochLine.charAt(0) == resetChar) {
  361.                     epochDifferential   = new TextDifferential(epochLength);
  362.                     satListDifferential = new TextDifferential(nbSat * 3);
  363.                     differentials       = new HashMap<>();
  364.                 }

  365.                 // check for special events
  366.                 epochDifferential.accept(loopEpochLine.subSequence(epochStart,
  367.                                                                    FastMath.min(loopEpochLine.length(), epochStart + epochLength)));
  368.                 if (parseInt(epochDifferential.getUncompressed(), eventStart, 1) > 1) {
  369.                     // this was not really the epoch, but rather a special event
  370.                     // we just copy the lines and skip to real epoch and clock lines
  371.                     builder.append(epochDifferential.getUncompressed());
  372.                     trimTrailingSpaces(builder);
  373.                     builder.append('\n');
  374.                     final int skippedLines = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
  375.                     for (int i = 0; i < skippedLines; ++i) {
  376.                         builder.append(loopClockLine);
  377.                         trimTrailingSpaces(builder);
  378.                         builder.append('\n');
  379.                         loopClockLine = readLine();
  380.                     }

  381.                     // the epoch and clock are in the next lines
  382.                     loopEpochLine = loopClockLine;
  383.                     loopClockLine = readLine();
  384.                     loop = true;

  385.                 } else {
  386.                     loop = false;
  387.                     final int n = parseInt(epochDifferential.getUncompressed(), nbSatStart, 3);
  388.                     satellites = new ArrayList<>(n);
  389.                     if (satListStart < loopEpochLine.length()) {
  390.                         satListDifferential.accept(loopEpochLine.subSequence(satListStart, loopEpochLine.length()));
  391.                     }
  392.                     final CharSequence satListPart = satListDifferential.getUncompressed();
  393.                     for (int i = 0; i < n; ++i) {
  394.                         satellites.add(satListPart.subSequence(i * 3, (i + 1) * 3).toString());
  395.                     }

  396.                     // parse clock offset
  397.                     if (!loopClockLine.isEmpty()) {
  398.                         if (loopClockLine.length() > 2 && loopClockLine.charAt(1) == '&') {
  399.                             clockDifferential = new NumericDifferential(clockLength, clockDecimalPlaces, parseInt(loopClockLine, 0, 1));
  400.                             clockDifferential.accept(loopClockLine.subSequence(2, loopClockLine.length()));
  401.                         } else if (clockDifferential == null) {
  402.                             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  403.                                                       lineNumber, name, loopClockLine);
  404.                         } else {
  405.                             clockDifferential.accept(loopClockLine);
  406.                         }
  407.                     }
  408.                 }
  409.             }

  410.         }

  411.         /** Get the uncompressed epoch part.
  412.          * @return uncompressed epoch part
  413.          */
  414.         protected CharSequence getEpochPart() {
  415.             return epochDifferential.getUncompressed();
  416.         }

  417.         /** Get the uncompressed clock part.
  418.          * @return uncompressed clock part
  419.          */
  420.         protected CharSequence getClockPart() {
  421.             return clockDifferential == null ? "" : clockDifferential.getUncompressed();
  422.         }

  423.         /** Get the satellites for current observations.
  424.          * @return satellites for current observation
  425.          */
  426.         protected List<String> getSatellites() {
  427.             return satellites;
  428.         }

  429.         /** Get the combined differentials for one satellite.
  430.          * @param sat satellite id
  431.          * @return observationDifferentials
  432.          */
  433.         protected CombinedDifferentials getCombinedDifferentials(final CharSequence sat) {
  434.             return differentials.get(sat);
  435.         }

  436.         /** Parse observation lines.
  437.          * @param observationLines observation lines
  438.          * @return uncompressed lines
  439.          */
  440.         public abstract CharSequence parseObservationLines(String[] observationLines);

  441.         /** Parse observation lines.
  442.          * @param dataLength length of data fields
  443.          * @param dataDecimalPlaces number of decimal places for data fields
  444.          * @param observationLines observation lines
  445.          */
  446.         protected void doParseObservationLines(final int dataLength, final int dataDecimalPlaces,
  447.                                                final String[] observationLines) {

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

  449.                 final CharSequence line = observationLines[i];

  450.                 // get the differentials associated with this observations line
  451.                 final String sat = satellites.get(i);
  452.                 CombinedDifferentials satDiffs = differentials.get(sat);
  453.                 if (satDiffs == null) {
  454.                     final SatelliteSystem system = SatelliteSystem.parseSatelliteSystem(sat.subSequence(0, 1).toString());
  455.                     satDiffs = new CombinedDifferentials(maxObs.get(system));
  456.                     differentials.put(sat, satDiffs);
  457.                 }

  458.                 // parse observations
  459.                 int k = 0;
  460.                 for (int j = 0; j < satDiffs.observations.length; ++j) {

  461.                     if (k >= line.length() || line.charAt(k) == ' ') {
  462.                         // the data field is missing
  463.                         satDiffs.observations[j] = null;
  464.                     } else {
  465.                         // the data field is present

  466.                         if (k + 1 < line.length() &&
  467.                             Character.isDigit(line.charAt(k)) &&
  468.                             line.charAt(k + 1) == '&') {
  469.                             // reinitialize differentials
  470.                             satDiffs.observations[j] = new NumericDifferential(dataLength, dataDecimalPlaces,
  471.                                                                                Character.digit(line.charAt(k), 10));
  472.                             k += 2;
  473.                         }

  474.                         // extract the compressed differenced value
  475.                         final int start = k;
  476.                         while (k < line.length() && line.charAt(k) != ' ') {
  477.                             ++k;
  478.                         }
  479.                         try {
  480.                             satDiffs.observations[j].accept(line.subSequence(start, k));
  481.                         } catch (NumberFormatException nfe) {
  482.                             throw new OrekitException(nfe,
  483.                                                       OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  484.                                                       lineNumber + i - (observationLines.length - 1),
  485.                                                       name, observationLines[i]);
  486.                         }

  487.                     }

  488.                     // skip blank separator
  489.                     ++k;

  490.                 }

  491.                 if (k < line.length()) {
  492.                     satDiffs.flags.accept(line.subSequence(k, line.length()));
  493.                 }

  494.             }

  495.         }

  496.         /** Check if a line corresponds to a header.
  497.          * @param label header label
  498.          * @param line header line
  499.          * @return true if line corresponds to header
  500.          */
  501.         protected boolean isHeaderLine(final String label, final String line) {
  502.             return label.equals(parseString(line, LABEL_START, label.length()));
  503.         }

  504.         /** Update the max number of observations.
  505.          * @param system satellite system
  506.          * @param nbObs number of observations
  507.          */
  508.         protected void updateMaxObs(final SatelliteSystem system, final int nbObs) {
  509.             maxObs.put(system, FastMath.max(maxObs.get(system), nbObs));
  510.         }

  511.         /** Read a new line.
  512.          * @return line read
  513.          * @exception IOException if a read error occurs
  514.          */
  515.         private String readLine()
  516.             throws IOException {
  517.             final String line = reader.readLine();
  518.             if (line == null) {
  519.                 throw new OrekitException(OrekitMessages.UNEXPECTED_END_OF_FILE, name);
  520.             }
  521.             lineNumber++;
  522.             return line;
  523.         }

  524.         /** Get the rinex format corresponding to this compact rinex format.
  525.          * @param name file name
  526.          * @param reader line-oriented input
  527.          * @return rinex format associated with this compact rinex format
  528.          * @exception IOException if first lines cannot be read
  529.          */
  530.         public static CompactRinexFormat getFormat(final String name, final BufferedReader reader)
  531.             throws IOException {

  532.             // read the first two lines of the file
  533.             final String line1 = reader.readLine();
  534.             final String line2 = reader.readLine();
  535.             if (line1 == null || line2 == null) {
  536.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  537.             }

  538.             // extract format version
  539.             final int cVersion100 = (int) FastMath.rint(100 * parseDouble(line1, 0, 9));
  540.             if (cVersion100 != 100 && cVersion100 != 300) {
  541.                 throw new OrekitException(OrekitMessages.UNSUPPORTED_FILE_FORMAT, name);
  542.             }
  543.             if (!CRINEX_VERSION_TYPE.equals(parseString(line1, LABEL_START, CRINEX_VERSION_TYPE.length()))) {
  544.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  545.             }
  546.             if (!CRINEX_PROG_DATE.equals(parseString(line2, LABEL_START, CRINEX_PROG_DATE.length()))) {
  547.                 throw new OrekitException(OrekitMessages.NOT_A_SUPPORTED_HATANAKA_COMPRESSED_FILE, name);
  548.             }

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

  551.         }

  552.         /** Extract a string from a line.
  553.          * @param line to parse
  554.          * @param start start index of the string
  555.          * @param length length of the string
  556.          * @return parsed string
  557.          */
  558.         public static String parseString(final CharSequence line, final int start, final int length) {
  559.             if (line.length() > start) {
  560.                 return line.subSequence(start, FastMath.min(line.length(), start + length)).toString().trim();
  561.             } else {
  562.                 return null;
  563.             }
  564.         }

  565.         /** Extract an integer from a line.
  566.          * @param line to parse
  567.          * @param start start index of the integer
  568.          * @param length length of the integer
  569.          * @return parsed integer
  570.          */
  571.         public static int parseInt(final CharSequence line, final int start, final int length) {
  572.             if (line.length() > start && parseString(line, start, length).length() > 0) {
  573.                 return Integer.parseInt(parseString(line, start, length));
  574.             } else {
  575.                 return 0;
  576.             }
  577.         }

  578.         /** Extract a double from a line.
  579.          * @param line to parse
  580.          * @param start start index of the real
  581.          * @param length length of the real
  582.          * @return parsed real, or {@code Double.NaN} if field was empty
  583.          */
  584.         public static double parseDouble(final CharSequence line, final int start, final int length) {
  585.             if (line.length() > start && parseString(line, start, length).length() > 0) {
  586.                 return Double.parseDouble(parseString(line, start, length));
  587.             } else {
  588.                 return Double.NaN;
  589.             }
  590.         }

  591.         /** Trim trailing spaces in a builder.
  592.          * @param builder builder to trim
  593.          */
  594.         public static void trimTrailingSpaces(final StringBuilder builder) {
  595.             for (int i = builder.length() - 1; i >= 0 && builder.charAt(i) == ' '; --i) {
  596.                 builder.deleteCharAt(i);
  597.             }
  598.         }

  599.         /** Enumerate for parsing sections. */
  600.         private enum Section {

  601.             /** Header section. */
  602.             HEADER,

  603.             /** Epoch and receiver clock offset section. */
  604.             EPOCH,

  605.             /** Observation section. */
  606.             OBSERVATION;

  607.         }

  608.     }

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

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

  613.         /** Start of epoch field. */
  614.         private static final int    EPOCH_START          = 0;

  615.         /** Length of epoch field. */
  616.         private static final int    EPOCH_LENGTH         = 32;

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

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

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

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

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

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

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

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

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

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

  637.         /** Simple constructor.
  638.          * @param name file name
  639.          * @param reader line-oriented input
  640.          */
  641.         CompactRinex1(final String name, final BufferedReader reader) {
  642.             super(name, reader);
  643.         }

  644.         @Override
  645.         /** {@inheritDoc} */
  646.         public CharSequence parseHeaderLine(final String line) {
  647.             if (isHeaderLine(NB_TYPES_OF_OBSERV, line)) {
  648.                 for (final SatelliteSystem system : SatelliteSystem.values()) {
  649.                     updateMaxObs(system, parseInt(line, 0, 6));
  650.                 }
  651.                 return line;
  652.             } else {
  653.                 return super.parseHeaderLine(line);
  654.             }
  655.         }

  656.         @Override
  657.         /** {@inheritDoc} */
  658.         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
  659.             throws IOException {

  660.             final StringBuilder builder = new StringBuilder();
  661.             doParseEpochAndClockLines(builder,
  662.                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
  663.                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
  664.                                       clockLine, '&');

  665.             // build uncompressed lines, taking care of clock being put
  666.             // back in line 1 and satellites after 12th put in continuation lines
  667.             final List<String> satellites = getSatellites();
  668.             builder.append(getEpochPart());
  669.             int iSat = 0;
  670.             while (iSat < FastMath.min(satellites.size(), MAX_SAT_EPOCH_LINE)) {
  671.                 builder.append(satellites.get(iSat++));
  672.             }
  673.             if (getClockPart().length() > 0) {
  674.                 while (builder.length() < CLOCK_START) {
  675.                     builder.append(' ');
  676.                 }
  677.                 builder.append(getClockPart());
  678.             }

  679.             while (iSat < satellites.size()) {
  680.                 // add a continuation line
  681.                 trimTrailingSpaces(builder);
  682.                 builder.append('\n');
  683.                 for (int k = 0; k < SAT_LIST_START; ++k) {
  684.                     builder.append(' ');
  685.                 }
  686.                 final int iSatStart = iSat;
  687.                 while (iSat < FastMath.min(satellites.size(), iSatStart + MAX_SAT_EPOCH_LINE)) {
  688.                     builder.append(satellites.get(iSat++));
  689.                 }
  690.             }
  691.             trimTrailingSpaces(builder);
  692.             return builder;

  693.         }

  694.         @Override
  695.         /** {@inheritDoc} */
  696.         public CharSequence parseObservationLines(final String[] observationLines) {

  697.             // parse the observation lines
  698.             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);

  699.             // build uncompressed lines
  700.             final StringBuilder builder = new StringBuilder();
  701.             for (final CharSequence sat : getSatellites()) {
  702.                 if (builder.length() > 0) {
  703.                     trimTrailingSpaces(builder);
  704.                     builder.append('\n');
  705.                 }
  706.                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
  707.                 final CharSequence          flags = cd.flags.getUncompressed();
  708.                 for (int i = 0; i < cd.observations.length; ++i) {
  709.                     if (i > 0 && i % 5 == 0) {
  710.                         trimTrailingSpaces(builder);
  711.                         builder.append('\n');
  712.                     }
  713.                     if (cd.observations[i] == null) {
  714.                         // missing observation
  715.                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
  716.                             builder.append(' ');
  717.                         }
  718.                     } else {
  719.                         builder.append(cd.observations[i].getUncompressed());
  720.                         if (2 * i < flags.length()) {
  721.                             builder.append(flags.charAt(2 * i));
  722.                         }
  723.                         if (2 * i + 1 < flags.length()) {
  724.                             builder.append(flags.charAt(2 * i + 1));
  725.                         }
  726.                     }
  727.                 }
  728.             }
  729.             trimTrailingSpaces(builder);
  730.             return builder;

  731.         }

  732.     }

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

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

  737.         /** Start of epoch field. */
  738.         private static final int    EPOCH_START          = 0;

  739.         /** Length of epoch field. */
  740.         private static final int    EPOCH_LENGTH         = 41;

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

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

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

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

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

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

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

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

  757.         /** Simple constructor.
  758.          * @param name file name
  759.          * @param reader line-oriented input
  760.          */
  761.         CompactRinex3(final String name, final BufferedReader reader) {
  762.             super(name, reader);
  763.         }

  764.         @Override
  765.         /** {@inheritDoc} */
  766.         public CharSequence parseHeaderLine(final String line) {
  767.             if (isHeaderLine(SYS_NB_OBS_TYPES, line)) {
  768.                 if (line.charAt(0) != ' ') {
  769.                     // it is the first line of an observation types description
  770.                     // (continuation lines are ignored here)
  771.                     updateMaxObs(SatelliteSystem.parseSatelliteSystem(parseString(line, 0, 1)),
  772.                                  parseInt(line, 1, 5));
  773.                 }
  774.                 return line;
  775.             } else {
  776.                 return super.parseHeaderLine(line);
  777.             }
  778.         }

  779.         @Override
  780.         /** {@inheritDoc} */
  781.         public CharSequence parseEpochAndClockLines(final String epochLine, final String clockLine)
  782.             throws IOException {

  783.             final StringBuilder builder = new StringBuilder();
  784.             doParseEpochAndClockLines(builder,
  785.                                       EPOCH_START, EPOCH_LENGTH, EVENT_START, NB_SAT_START, SAT_LIST_START,
  786.                                       CLOCK_LENGTH, CLOCK_DECIMAL_PLACES, epochLine,
  787.                                       clockLine, '>');

  788.             // build uncompressed line
  789.             builder.append(getEpochPart());
  790.             if (getClockPart().length() > 0) {
  791.                 while (builder.length() < CLOCK_START) {
  792.                     builder.append(' ');
  793.                 }
  794.                 builder.append(getClockPart());
  795.             }

  796.             trimTrailingSpaces(builder);
  797.             return builder;

  798.         }

  799.         @Override
  800.         /** {@inheritDoc} */
  801.         public CharSequence parseObservationLines(final String[] observationLines) {

  802.             // parse the observation lines
  803.             doParseObservationLines(DATA_LENGTH, DATA_DECIMAL_PLACES, observationLines);

  804.             // build uncompressed lines
  805.             final StringBuilder builder = new StringBuilder();
  806.             for (final CharSequence sat : getSatellites()) {
  807.                 if (builder.length() > 0) {
  808.                     trimTrailingSpaces(builder);
  809.                     builder.append('\n');
  810.                 }
  811.                 builder.append(sat);
  812.                 final CombinedDifferentials cd    = getCombinedDifferentials(sat);
  813.                 final CharSequence          flags = cd.flags.getUncompressed();
  814.                 for (int i = 0; i < cd.observations.length; ++i) {
  815.                     if (cd.observations[i] == null) {
  816.                         // missing observation
  817.                         for (int j = 0; j < DATA_LENGTH + 2; ++j) {
  818.                             builder.append(' ');
  819.                         }
  820.                     } else {
  821.                         builder.append(cd.observations[i].getUncompressed());
  822.                         if (2 * i < flags.length()) {
  823.                             builder.append(flags.charAt(2 * i));
  824.                         }
  825.                         if (2 * i + 1 < flags.length()) {
  826.                             builder.append(flags.charAt(2 * i + 1));
  827.                         }
  828.                     }
  829.                 }
  830.             }
  831.             trimTrailingSpaces(builder);
  832.             return builder;

  833.         }

  834.     }

  835. }