RinexObservationWriter.java

  1. /* Copyright 2023 Thales Alenia Space
  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.observation;

  18. import java.io.IOException;
  19. import java.util.ArrayList;
  20. import java.util.Collections;
  21. import java.util.List;
  22. import java.util.Locale;
  23. import java.util.Map;

  24. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  25. import org.hipparchus.util.FastMath;
  26. import org.orekit.annotation.DefaultDataContext;
  27. import org.orekit.errors.OrekitException;
  28. import org.orekit.errors.OrekitMessages;
  29. import org.orekit.files.rinex.AppliedDCBS;
  30. import org.orekit.files.rinex.AppliedPCVS;
  31. import org.orekit.files.rinex.section.RinexComment;
  32. import org.orekit.files.rinex.section.RinexLabels;
  33. import org.orekit.gnss.ObservationTimeScale;
  34. import org.orekit.gnss.ObservationType;
  35. import org.orekit.gnss.SatInSystem;
  36. import org.orekit.gnss.SatelliteSystem;
  37. import org.orekit.time.AbsoluteDate;
  38. import org.orekit.time.DateTimeComponents;
  39. import org.orekit.time.TimeScale;
  40. import org.orekit.time.TimeScalesFactory;

  41. /** Writer for Rinex observation file.
  42.  * <p>
  43.  * As RINEX file are organized in batches of observations at some dates,
  44.  * these observations are cached and a new batch is output only when
  45.  * a new date appears when calling {@link #writeObservationDataSet(ObservationDataSet)}
  46.  * or when the file is closed by calling the {@link #close() close} method.
  47.  * Failing to call {@link #close() close} would imply the last batch
  48.  * of measurements is not written. This is the reason why this class implements
  49.  * {@link AutoCloseable}, so the {@link #close() close} method can be called automatically in
  50.  * a {@code try-with-resources} statement.
  51.  * </p>
  52.  * @author Luc Maisonobe
  53.  * @since 12.0
  54.  */
  55. public class RinexObservationWriter implements AutoCloseable {

  56.     /** Index of label in header lines. */
  57.     private static final int LABEL_INDEX = 60;

  58.     /** Format for one 1 digit integer field. */
  59.     private static final String ONE_DIGIT_INTEGER = "%1d";

  60.     /** Format for one 2 digits integer field. */
  61.     private static final String PADDED_TWO_DIGITS_INTEGER = "%02d";

  62.     /** Format for one 2 digits integer field. */
  63.     private static final String TWO_DIGITS_INTEGER = "%2d";

  64.     /** Format for one 4 digits integer field. */
  65.     private static final String PADDED_FOUR_DIGITS_INTEGER = "%04d";

  66.     /** Format for one 3 digits integer field. */
  67.     private static final String THREE_DIGITS_INTEGER = "%3d";

  68.     /** Format for one 4 digits integer field. */
  69.     private static final String FOUR_DIGITS_INTEGER = "%4d";

  70.     /** Format for one 6 digits integer field. */
  71.     private static final String SIX_DIGITS_INTEGER = "%6d";

  72.     /** Format for one 8.3 digits float field. */
  73.     private static final String EIGHT_THREE_DIGITS_FLOAT = "%8.3f";

  74.     /** Format for one 8.5 digits float field. */
  75.     private static final String EIGHT_FIVE_DIGITS_FLOAT = "%8.5f";

  76.     /** Format for one 9.4 digits float field. */
  77.     private static final String NINE_FOUR_DIGITS_FLOAT = "%9.4f";

  78.     /** Format for one 10.3 digits float field. */
  79.     private static final String TEN_THREE_DIGITS_FLOAT = "%10.3f";

  80.     /** Format for one 11.7 digits float field. */
  81.     private static final String ELEVEN_SEVEN_DIGITS_FLOAT = "%11.7f";

  82.     /** Format for one 12.9 digits float field. */
  83.     private static final String TWELVE_NINE_DIGITS_FLOAT = "%12.9f";

  84.     /** Format for one 13.7 digits float field. */
  85.     private static final String THIRTEEN_SEVEN_DIGITS_FLOAT = "%13.7f";

  86.     /** Format for one 14.3 digits float field. */
  87.     private static final String FOURTEEN_THREE_DIGITS_FLOAT = "%14.3f";

  88.     /** Format for one 14.4 digits float field. */
  89.     private static final String FOURTEEN_FOUR_DIGITS_FLOAT = "%14.4f";

  90.     /** Format for one 15.12 digits float field. */
  91.     private static final String FIFTEEN_TWELVE_DIGITS_FLOAT = "%15.12f";

  92.     /** Threshold for considering measurements are at the sate time.
  93.      * (we know the RINEX files encode dates with a resolution of 0.1µs)
  94.      */
  95.     private static final double EPS_DATE = 1.0e-8;

  96.     /** Destination of generated output. */
  97.     private final Appendable output;

  98.     /** Output name for error messages. */
  99.     private final String outputName;

  100.     /** Time scale for writing dates. */
  101.     private TimeScale timeScale;

  102.     /** Saved header. */
  103.     private RinexObservationHeader savedHeader;

  104.     /** Saved comments. */
  105.     private List<RinexComment> savedComments;

  106.     /** Pending observations. */
  107.     private final List<ObservationDataSet> pending;

  108.     /** Line number. */
  109.     private int lineNumber;

  110.     /** Column number. */
  111.     private int column;

  112.     /** Simple constructor.
  113.      * @param output destination of generated output
  114.      * @param outputName output name for error messages
  115.      */
  116.     public RinexObservationWriter(final Appendable output, final String outputName) {
  117.         this.output        = output;
  118.         this.outputName    = outputName;
  119.         this.savedHeader   = null;
  120.         this.savedComments = Collections.emptyList();
  121.         this.pending       = new ArrayList<>();
  122.         this.lineNumber    = 0;
  123.         this.column        = 0;
  124.     }

  125.     /** {@inheritDoc} */
  126.     @Override
  127.     public void close() throws IOException {
  128.         processPending();
  129.     }

  130.     /** Write a complete observation file.
  131.      * <p>
  132.      * This method calls {@link #prepareComments(List)} and
  133.      * {@link #writeHeader(RinexObservationHeader)} once and then loops on
  134.      * calling {@link #writeObservationDataSet(ObservationDataSet)}
  135.      * for all observation data sets in the file
  136.      * </p>
  137.      * @param rinexObservation Rinex observation file to write
  138.      * @see #writeHeader(RinexObservationHeader)
  139.      * @see #writeObservationDataSet(ObservationDataSet)
  140.      * @exception IOException if an I/O error occurs.
  141.      */
  142.     @DefaultDataContext
  143.     public void writeCompleteFile(final RinexObservation rinexObservation)
  144.         throws IOException {
  145.         prepareComments(rinexObservation.getComments());
  146.         writeHeader(rinexObservation.getHeader());
  147.         for (final ObservationDataSet observationDataSet : rinexObservation.getObservationDataSets()) {
  148.             writeObservationDataSet(observationDataSet);
  149.         }
  150.     }

  151.     /** Prepare comments to be emitted at specified lines.
  152.      * @param comments comments to be emitted
  153.      */
  154.     public void prepareComments(final List<RinexComment> comments) {
  155.         savedComments = comments;
  156.     }

  157.     /** Write header.
  158.      * <p>
  159.      * This method must be called exactly once at the beginning
  160.      * (directly or by {@link #writeCompleteFile(RinexObservation)})
  161.      * </p>
  162.      * @param header header to write
  163.      * @exception IOException if an I/O error occurs.
  164.      */
  165.     @DefaultDataContext
  166.     public void writeHeader(final RinexObservationHeader header)
  167.         throws IOException {

  168.         // check header is written exactly once
  169.         if (savedHeader != null) {
  170.             throw new OrekitException(OrekitMessages.HEADER_ALREADY_WRITTEN, outputName);
  171.         }
  172.         savedHeader = header;
  173.         lineNumber  = 1;

  174.         final ObservationTimeScale observationTimeScale = header.getSatelliteSystem().getObservationTimeScale() != null ?
  175.                                                           header.getSatelliteSystem().getObservationTimeScale() :
  176.                                                           ObservationTimeScale.GPS;
  177.         timeScale = observationTimeScale.getTimeScale(TimeScalesFactory.getTimeScales());

  178.         // RINEX VERSION / TYPE
  179.         outputField("%9.2f", header.getFormatVersion(), 9);
  180.         outputField("",                 20, true);
  181.         outputField("OBSERVATION DATA", 40, true);
  182.         outputField(header.getSatelliteSystem().getKey(), 41);
  183.         finishHeaderLine(RinexLabels.VERSION);

  184.         // PGM / RUN BY / DATE
  185.         outputField(header.getProgramName(), 20, true);
  186.         outputField(header.getRunByName(),   40, true);
  187.         final DateTimeComponents dtc = header.getCreationDateComponents();
  188.         if (header.getFormatVersion() < 3.0 && dtc.getTime().getSecond() < 0.5) {
  189.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 42);
  190.             outputField('-', 43);
  191.             outputField(dtc.getDate().getMonthEnum().getUpperCaseAbbreviation(), 46,  true);
  192.             outputField('-', 47);
  193.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 49);
  194.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 52);
  195.             outputField(':', 53);
  196.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 55);
  197.             outputField(header.getCreationTimeZone(), 58, true);
  198.         } else {
  199.             outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 44);
  200.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 46);
  201.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 48);
  202.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 51);
  203.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 53);
  204.             outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 55);
  205.             outputField(header.getCreationTimeZone(), 59, false);
  206.         }
  207.         finishHeaderLine(RinexLabels.PROGRAM);

  208.         // MARKER NAME
  209.         outputField(header.getMarkerName(), 60, true);
  210.         finishHeaderLine(RinexLabels.MARKER_NAME);

  211.         // MARKER NUMBER
  212.         if (header.getMarkerNumber() != null) {
  213.             outputField(header.getMarkerNumber(), 20, true);
  214.             finishHeaderLine(RinexLabels.MARKER_NUMBER);
  215.         }

  216.         // MARKER TYPE
  217.         if (header.getFormatVersion() >= 2.20) {
  218.             outputField(header.getMarkerType(), 20, true);
  219.             finishHeaderLine(RinexLabels.MARKER_TYPE);
  220.         }

  221.         // OBSERVER / AGENCY
  222.         outputField(header.getObserverName(), 20, true);
  223.         outputField(header.getAgencyName(),   40, true);
  224.         finishHeaderLine(RinexLabels.OBSERVER_AGENCY);

  225.         // REC # / TYPE / VERS
  226.         outputField(header.getReceiverNumber(),  20, true);
  227.         outputField(header.getReceiverType(),    40, true);
  228.         outputField(header.getReceiverVersion(), 60, true);
  229.         finishHeaderLine(RinexLabels.REC_NB_TYPE_VERS);

  230.         // ANT # / TYPE
  231.         outputField(header.getAntennaNumber(), 20, true);
  232.         outputField(header.getAntennaType(),   40, true);
  233.         finishHeaderLine(RinexLabels.ANT_NB_TYPE);

  234.         // APPROX POSITION XYZ
  235.         writeHeaderLine(header.getApproxPos(), RinexLabels.APPROX_POSITION_XYZ);

  236.         // ANTENNA: DELTA H/E/N
  237.         if (!Double.isNaN(header.getAntennaHeight())) {
  238.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaHeight(),         14);
  239.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getX(), 28);
  240.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getY(), 42);
  241.             finishHeaderLine(RinexLabels.ANTENNA_DELTA_H_E_N);
  242.         }

  243.         // ANTENNA: DELTA X/Y/Z
  244.         writeHeaderLine(header.getAntennaReferencePoint(), RinexLabels.ANTENNA_DELTA_X_Y_Z);

  245.         // ANTENNA: PHASECENTER
  246.         if (header.getAntennaPhaseCenter() != null) {
  247.             outputField(header.getPhaseCenterSystem().getKey(), 1);
  248.             outputField("", 2, true);
  249.             outputField(header.getObservationCode(), 5, true);
  250.             outputField(NINE_FOUR_DIGITS_FLOAT,     header.getAntennaPhaseCenter().getX(), 14);
  251.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getY(), 28);
  252.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getZ(), 42);
  253.             finishHeaderLine(RinexLabels.ANTENNA_PHASE_CENTER);
  254.         }

  255.         // ANTENNA: B.SIGHT XY
  256.         writeHeaderLine(header.getAntennaBSight(), RinexLabels.ANTENNA_B_SIGHT_XYZ);

  257.         // ANTENNA: ZERODIR AZI
  258.         if (!Double.isNaN(header.getAntennaAzimuth())) {
  259.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, FastMath.toDegrees(header.getAntennaAzimuth()), 14);
  260.             finishHeaderLine(RinexLabels.ANTENNA_ZERODIR_AZI);
  261.         }

  262.         // ANTENNA: ZERODIR XYZ
  263.         writeHeaderLine(header.getAntennaZeroDirection(), RinexLabels.ANTENNA_ZERODIR_XYZ);

  264.         // OBS SCALE FACTOR
  265.         if (FastMath.abs(header.getFormatVersion() - 2.20) < 0.001) {
  266.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  267.                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
  268.                     if (sfc != null) {
  269.                         outputField(SIX_DIGITS_INTEGER, (int) FastMath.round(sfc.getCorrection()), 6);
  270.                         outputField(SIX_DIGITS_INTEGER, sfc.getTypesObsScaled().size(), 12);
  271.                         for (int i = 0; i < sfc.getTypesObsScaled().size(); ++i) {
  272.                             outputField(sfc.getTypesObsScaled().get(i).name(), 18 + 6 * i, false);
  273.                         }
  274.                         finishHeaderLine(RinexLabels.OBS_SCALE_FACTOR);
  275.                     }
  276.                 }
  277.             }
  278.         }

  279.         // CENTER OF MASS: XYZ
  280.         writeHeaderLine(header.getCenterMass(), RinexLabels.CENTER_OF_MASS_XYZ);

  281.         // DOI
  282.         writeHeaderLine(header.getDoi(), RinexLabels.DOI);

  283.         // LICENSE OF USE
  284.         writeHeaderLine(header.getLicense(), RinexLabels.LICENSE);

  285.         // STATION INFORMATION
  286.         writeHeaderLine(header.getStationInformation(), RinexLabels.STATION_INFORMATION);

  287.         // SYS / # / OBS TYPES
  288.         for (Map.Entry<SatelliteSystem, List<ObservationType>> entry : header.getTypeObs().entrySet()) {
  289.             if (header.getFormatVersion() < 3.0) {
  290.                 outputField(SIX_DIGITS_INTEGER, entry.getValue().size(), 6);
  291.             } else {
  292.                 outputField(entry.getKey().getKey(), 1);
  293.                 outputField(THREE_DIGITS_INTEGER, entry.getValue().size(), 6);
  294.             }
  295.             for (final ObservationType observationType : entry.getValue()) {
  296.                 int next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
  297.                 if (next > LABEL_INDEX) {
  298.                     // we need to set up a continuation line
  299.                     finishHeaderLine(header.getFormatVersion() < 3.0 ?
  300.                                      RinexLabels.NB_TYPES_OF_OBSERV :
  301.                                      RinexLabels.SYS_NB_TYPES_OF_OBSERV);
  302.                     outputField("", 6, true);
  303.                     next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
  304.                 }
  305.                 outputField(observationType.name(), next, false);
  306.             }
  307.             finishHeaderLine(header.getFormatVersion() < 3.0 ?
  308.                              RinexLabels.NB_TYPES_OF_OBSERV :
  309.                              RinexLabels.SYS_NB_TYPES_OF_OBSERV);
  310.         }

  311.         // SIGNAL STRENGTH UNIT
  312.         writeHeaderLine(header.getSignalStrengthUnit(), RinexLabels.SIGNAL_STRENGTH_UNIT);

  313.         // INTERVAL
  314.         if (!Double.isNaN(header.getInterval())) {
  315.             outputField(TEN_THREE_DIGITS_FLOAT, header.getInterval(), 10);
  316.             finishHeaderLine(RinexLabels.INTERVAL);
  317.         }

  318.         // TIME OF FIRST OBS
  319.         final DateTimeComponents dtcFirst = header.getTFirstObs().getComponents(timeScale);
  320.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getYear(), 6);
  321.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getMonth(), 12);
  322.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getDay(), 18);
  323.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getHour(), 24);
  324.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getMinute(), 30);
  325.         outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcFirst.getTime().getSecond(), 43);
  326.         outputField(observationTimeScale.name(), 51, false);
  327.         finishHeaderLine(RinexLabels.TIME_OF_FIRST_OBS);

  328.         // TIME OF LAST OBS
  329.         if (!header.getTLastObs().equals(AbsoluteDate.FUTURE_INFINITY)) {
  330.             final DateTimeComponents dtcLast = header.getTLastObs().getComponents(timeScale);
  331.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getYear(), 6);
  332.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getMonth(), 12);
  333.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getDay(), 18);
  334.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getHour(), 24);
  335.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getMinute(), 30);
  336.             outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcLast.getTime().getSecond(), 43);
  337.             outputField(observationTimeScale.name(), 51, false);
  338.             finishHeaderLine(RinexLabels.TIME_OF_LAST_OBS);
  339.         }

  340.         // RCV CLOCK OFFS APPL
  341.         if (header.getClkOffset() >= 0) {
  342.             outputField(SIX_DIGITS_INTEGER, header.getClkOffset(), 6);
  343.             finishHeaderLine(RinexLabels.RCV_CLOCK_OFFS_APPL);
  344.         }

  345.         // SYS / DCBS APPLIED
  346.         for (final AppliedDCBS appliedDCBS : header.getListAppliedDCBS()) {
  347.             outputField(appliedDCBS.getSatelliteSystem().getKey(),  1);
  348.             outputField("",                                         2, true);
  349.             outputField(appliedDCBS.getProgDCBS(),                 20, true);
  350.             outputField(appliedDCBS.getSourceDCBS(),               60, true);
  351.             finishHeaderLine(RinexLabels.SYS_DCBS_APPLIED);
  352.         }

  353.         // SYS / PCVS APPLIED
  354.         for (final AppliedPCVS appliedPCVS : header.getListAppliedPCVS()) {
  355.             outputField(appliedPCVS.getSatelliteSystem().getKey(),  1);
  356.             outputField("",                                         2, true);
  357.             outputField(appliedPCVS.getProgPCVS(),                 20, true);
  358.             outputField(appliedPCVS.getSourcePCVS(),               60, true);
  359.             finishHeaderLine(RinexLabels.SYS_PCVS_APPLIED);
  360.         }

  361.         // SYS / SCALE FACTOR
  362.         if (header.getFormatVersion() >= 3.0) {
  363.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  364.                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
  365.                     if (sfc != null) {
  366.                         outputField(system.getKey(), 1);
  367.                         outputField("", 2, true);
  368.                         outputField(FOUR_DIGITS_INTEGER, (int) FastMath.rint(sfc.getCorrection()), 6);
  369.                         if (sfc.getTypesObsScaled().size() < header.getTypeObs().get(system).size()) {
  370.                             outputField("", 8, true);
  371.                             outputField(TWO_DIGITS_INTEGER,  sfc.getTypesObsScaled().size(), 10);
  372.                             for (ObservationType observationType : sfc.getTypesObsScaled()) {
  373.                                 int next = column + 4;
  374.                                 if (next > LABEL_INDEX) {
  375.                                     // we need to set up a continuation line
  376.                                     finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
  377.                                     outputField("", 10, true);
  378.                                     next = column + 4;
  379.                                 }
  380.                                 outputField("", next - 3, true);
  381.                                 outputField(observationType.name(), next, true);
  382.                             }
  383.                         }
  384.                         finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
  385.                     }
  386.                 }
  387.             }
  388.         }

  389.         // SYS / PHASE SHIFT
  390.         for (final PhaseShiftCorrection psc : header.getPhaseShiftCorrections()) {
  391.             outputField(psc.getSatelliteSystem().getKey(), 1);
  392.             outputField(psc.getTypeObs().name(), 5, false);
  393.             outputField(EIGHT_FIVE_DIGITS_FLOAT, psc.getCorrection(), 14);
  394.             if (!psc.getSatsCorrected().isEmpty()) {
  395.                 outputField(TWO_DIGITS_INTEGER, psc.getSatsCorrected().size(), 18);
  396.                 for (final SatInSystem sis : psc.getSatsCorrected()) {
  397.                     int next = column + 4;
  398.                     if (next > LABEL_INDEX) {
  399.                         // we need to set up a continuation line
  400.                         finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
  401.                         outputField("", 18, true);
  402.                         next = column + 4;
  403.                     }
  404.                     outputField(sis.getSystem().getKey(), next - 2);
  405.                     outputField(PADDED_TWO_DIGITS_INTEGER, sis.getTwoDigitsRinexPRN(), next);
  406.                 }
  407.             }
  408.             finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
  409.         }

  410.         if (header.getFormatVersion() >= 3.01) {
  411.             if (!header.getGlonassChannels().isEmpty()) {
  412.                 // GLONASS SLOT / FRQ #
  413.                 outputField(THREE_DIGITS_INTEGER, header.getGlonassChannels().size(), 3);
  414.                 outputField("", 4, true);
  415.                 for (final GlonassSatelliteChannel channel : header.getGlonassChannels()) {
  416.                     int next = column + 7;
  417.                     if (next > LABEL_INDEX) {
  418.                         // we need to set up a continuation line
  419.                         finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
  420.                         outputField("", 4, true);
  421.                         next = column + 7;
  422.                     }
  423.                     outputField(channel.getSatellite().getSystem().getKey(), next - 6);
  424.                     outputField(PADDED_TWO_DIGITS_INTEGER, channel.getSatellite().getPRN(), next - 4);
  425.                     outputField(TWO_DIGITS_INTEGER, channel.getK(), next - 1);
  426.                     outputField("", next, true);
  427.                 }
  428.             }
  429.             finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
  430.         }

  431.         if (header.getFormatVersion() >= 3.0) {
  432.             // GLONASS COD/PHS/BIS
  433.             if (Double.isNaN(header.getC1cCodePhaseBias())) {
  434.                 outputField("", 13, true);
  435.             } else {
  436.                 outputField(ObservationType.C1C.name(), 4, false);
  437.                 outputField("", 5, true);
  438.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1cCodePhaseBias(), 13);
  439.             }
  440.             if (Double.isNaN(header.getC1pCodePhaseBias())) {
  441.                 outputField("", 26, true);
  442.             } else {
  443.                 outputField(ObservationType.C1P.name(), 17, false);
  444.                 outputField("", 18, true);
  445.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1pCodePhaseBias(), 26);
  446.             }
  447.             if (Double.isNaN(header.getC2cCodePhaseBias())) {
  448.                 outputField("", 39, true);
  449.             } else {
  450.                 outputField(ObservationType.C2C.name(), 30, false);
  451.                 outputField("", 31, true);
  452.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2cCodePhaseBias(), 39);
  453.             }
  454.             if (Double.isNaN(header.getC2pCodePhaseBias())) {
  455.                 outputField("", 52, true);
  456.             } else {
  457.                 outputField(ObservationType.C2P.name(), 43, false);
  458.                 outputField("", 44, true);
  459.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2pCodePhaseBias(), 52);
  460.             }
  461.             finishHeaderLine(RinexLabels.GLONASS_COD_PHS_BIS);
  462.         }

  463.         // LEAP SECONDS
  464.         if (header.getLeapSeconds() > 0) {
  465.             outputField(SIX_DIGITS_INTEGER, header.getLeapSeconds(), 6);
  466.             if (header.getFormatVersion() >= 3.0) {
  467.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsFuture(),  12);
  468.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsWeekNum(), 18);
  469.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsDayNum(),  24);
  470.             }
  471.             finishHeaderLine(RinexLabels.LEAP_SECONDS);
  472.         }

  473.         // # OF SATELLITES
  474.         if (header.getNbSat() >= 0) {
  475.             outputField(SIX_DIGITS_INTEGER, header.getNbSat(), 6);
  476.             finishHeaderLine(RinexLabels.NB_OF_SATELLITES);
  477.         }

  478.         // PRN / # OF OBS
  479.         for (final Map.Entry<SatInSystem, Map<ObservationType, Integer>> entry1 : header.getNbObsPerSat().entrySet()) {
  480.             final SatInSystem sis = entry1.getKey();
  481.             outputField(sis.getSystem().getKey(), 4);
  482.             outputField(PADDED_TWO_DIGITS_INTEGER, sis.getTwoDigitsRinexPRN(), 6);
  483.             for (final Map.Entry<ObservationType, Integer> entry2 : entry1.getValue().entrySet()) {
  484.                 int next = column + 6;
  485.                 if (next > LABEL_INDEX) {
  486.                     // we need to set up a continuation line
  487.                     finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
  488.                     outputField("", 6, true);
  489.                     next = column + 6;
  490.                 }
  491.                 outputField(SIX_DIGITS_INTEGER, entry2.getValue(), next);
  492.             }
  493.             finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
  494.         }

  495.         // END OF HEADER
  496.         writeHeaderLine("", RinexLabels.END);

  497.     }

  498.     /** Write one observation data set.
  499.      * <p>
  500.      * Note that this writers output only regular observations, so
  501.      * the event flag is always set to 0
  502.      * </p>
  503.      * @param observationDataSet observation data set to write
  504.      * @exception IOException if an I/O error occurs.
  505.      */
  506.     public void writeObservationDataSet(final ObservationDataSet observationDataSet)
  507.         throws IOException {

  508.         // check header has already been written
  509.         if (savedHeader == null) {
  510.             throw new OrekitException(OrekitMessages.HEADER_NOT_WRITTEN, outputName);
  511.         }

  512.         if (!pending.isEmpty() && observationDataSet.durationFrom(pending.get(0).getDate()) > EPS_DATE) {
  513.             // the specified observation belongs to the next batch
  514.             // we must process the current batch of pending observations
  515.             processPending();
  516.         }

  517.         // add the observation to the pending list, so it is written later on
  518.         pending.add(observationDataSet);

  519.     }

  520.     /** Process all pending measurements.
  521.      * @exception IOException if an I/O error occurs.
  522.      */
  523.     private void processPending() throws IOException {

  524.         if (!pending.isEmpty()) {

  525.             // write the batch of pending observations
  526.             if (savedHeader.getFormatVersion() < 3.0) {
  527.                 writePendingRinex2Observations();
  528.             } else {
  529.                 writePendingRinex34Observations();
  530.             }

  531.             // prepare for next batch
  532.             pending.clear();

  533.         }

  534.     }

  535.     /** Write one observation data set in RINEX 2 format.
  536.      * @exception IOException if an I/O error occurs.
  537.      */
  538.     public void writePendingRinex2Observations() throws IOException {

  539.         final ObservationDataSet first = pending.get(0);

  540.         // EPOCH/SAT
  541.         final DateTimeComponents dtc = first.getDate().getComponents(timeScale);
  542.         outputField("",  1, true);
  543.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getYear() % 100,    3);
  544.         outputField("",  4, true);
  545.         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getMonth(),         6);
  546.         outputField("",  7, true);
  547.         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getDay(),           9);
  548.         outputField("", 10, true);
  549.         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getHour(),         12);
  550.         outputField("", 13, true);
  551.         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getMinute(),       15);
  552.         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(),       26);

  553.         // event flag
  554.         outputField("", 28, true);
  555.         if (first.getEventFlag() == 0) {
  556.             outputField("", 29, true);
  557.         } else {
  558.             outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 29);
  559.         }

  560.         // list of satellites and receiver clock offset
  561.         outputField(THREE_DIGITS_INTEGER, pending.size(), 32);
  562.         boolean offsetWritten = false;
  563.         final double  clockOffset   = first.getRcvrClkOffset();
  564.         for (final ObservationDataSet ods : pending) {
  565.             int next = column + 3;
  566.             if (next > 68) {
  567.                 // we need to set up a continuation line
  568.                 if (clockOffset != 0.0) {
  569.                     outputField(TWELVE_NINE_DIGITS_FLOAT, clockOffset, 80);
  570.                 }
  571.                 offsetWritten = true;
  572.                 finishLine();
  573.                 outputField("", 32, true);
  574.                 next = column + 3;
  575.             }
  576.             outputField(ods.getSatellite().getSystem().getKey(), next - 2);
  577.             outputField(PADDED_TWO_DIGITS_INTEGER, ods.getSatellite().getTwoDigitsRinexPRN(), next);
  578.         }
  579.         if (!offsetWritten && clockOffset != 0.0) {
  580.             outputField("", 68, true);
  581.             outputField(TWELVE_NINE_DIGITS_FLOAT, first.getRcvrClkOffset(), 80);
  582.         }
  583.         finishLine();

  584.         // observations per se
  585.         for (final ObservationDataSet ods : pending) {
  586.             for (final ObservationData od : ods.getObservationData()) {
  587.                 int next = column + 16;
  588.                 if (next > 80) {
  589.                     // we need to set up a continuation line
  590.                     finishLine();
  591.                     next = column + 16;
  592.                 }
  593.                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
  594.                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
  595.                 if (od.getLossOfLockIndicator() == 0) {
  596.                     outputField("", next - 1, true);
  597.                 } else {
  598.                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
  599.                 }
  600.                 if (od.getSignalStrength() == 0) {
  601.                     outputField("", next, true);
  602.                 } else {
  603.                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
  604.                 }
  605.             }
  606.             finishLine();
  607.         }

  608.     }

  609.     /** Write one observation data set in RINEX 3/4 format.
  610.      * @exception IOException if an I/O error occurs.
  611.      */
  612.     public void writePendingRinex34Observations()
  613.         throws IOException {

  614.         final ObservationDataSet first = pending.get(0);

  615.         // EPOCH/SAT
  616.         final DateTimeComponents dtc = first.getDate().getComponents(timeScale);
  617.         outputField(">",  2, true);
  618.         outputField(FOUR_DIGITS_INTEGER,         dtc.getDate().getYear(),    6);
  619.         outputField("",   7, true);
  620.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getMonth(),   9);
  621.         outputField("",  10, true);
  622.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getDay(),    12);
  623.         outputField("", 13, true);
  624.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getHour(),   15);
  625.         outputField("", 16, true);
  626.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getMinute(), 18);
  627.         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(), 29);

  628.         // event flag
  629.         outputField("", 31, true);
  630.         if (first.getEventFlag() == 0) {
  631.             outputField("", 32, true);
  632.         } else {
  633.             outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 32);
  634.         }

  635.         // number of satellites and receiver clock offset
  636.         outputField(THREE_DIGITS_INTEGER, pending.size(), 35);
  637.         if (first.getRcvrClkOffset() != 0.0) {
  638.             outputField("", 41, true);
  639.             outputField(FIFTEEN_TWELVE_DIGITS_FLOAT, first.getRcvrClkOffset(), 56);
  640.         }
  641.         finishLine();

  642.         // observations per se
  643.         for (final ObservationDataSet ods : pending) {
  644.             outputField(ods.getSatellite().getSystem().getKey(), 1);
  645.             outputField(PADDED_TWO_DIGITS_INTEGER, ods.getSatellite().getTwoDigitsRinexPRN(), 3);
  646.             for (final ObservationData od : ods.getObservationData()) {
  647.                 final int next = column + 16;
  648.                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
  649.                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
  650.                 if (od.getLossOfLockIndicator() == 0) {
  651.                     outputField("", next - 1, true);
  652.                 } else {
  653.                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
  654.                 }
  655.                 if (od.getSignalStrength() == 0) {
  656.                     outputField("", next, true);
  657.                 } else {
  658.                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
  659.                 }
  660.             }
  661.             finishLine();
  662.         }

  663.     }

  664.     /** Write one header string.
  665.      * @param s string data (may be null)
  666.      * @param label line label
  667.      * @throws IOException if an I/O error occurs.
  668.      */
  669.     private void writeHeaderLine(final String s, final RinexLabels label) throws IOException {
  670.         if (s != null) {
  671.             outputField(s, s.length(), true);
  672.             finishHeaderLine(label);
  673.         }
  674.     }

  675.     /** Write one header vector.
  676.      * @param vector vector data (may be null)
  677.      * @param label line label
  678.      * @throws IOException if an I/O error occurs.
  679.      */
  680.     private void writeHeaderLine(final Vector3D vector, final RinexLabels label) throws IOException {
  681.         if (vector != null) {
  682.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getX(), 14);
  683.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getY(), 28);
  684.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getZ(), 42);
  685.             finishHeaderLine(label);
  686.         }
  687.     }

  688.     /** Finish one header line.
  689.      * @param label line label
  690.      * @throws IOException if an I/O error occurs.
  691.      */
  692.     private void finishHeaderLine(final RinexLabels label) throws IOException {
  693.         for (int i = column; i < LABEL_INDEX; ++i) {
  694.             output.append(' ');
  695.         }
  696.         output.append(label.getLabel());
  697.         finishLine();
  698.     }

  699.     /** Finish one line.
  700.      * @throws IOException if an I/O error occurs.
  701.      */
  702.     private void finishLine() throws IOException {

  703.         // pending line
  704.         output.append(System.lineSeparator());
  705.         lineNumber++;
  706.         column = 0;

  707.         // emit comments that should be placed at next lines
  708.         for (final RinexComment comment : savedComments) {
  709.             if (comment.getLineNumber() == lineNumber) {
  710.                 outputField(comment.getText(), LABEL_INDEX, true);
  711.                 output.append(RinexLabels.COMMENT.getLabel());
  712.                 output.append(System.lineSeparator());
  713.                 lineNumber++;
  714.                 column = 0;
  715.             } else if (comment.getLineNumber() > lineNumber) {
  716.                 break;
  717.             }
  718.         }

  719.     }

  720.     /** Output one single character field.
  721.      * @param c field value
  722.      * @param next target column for next field
  723.      * @throws IOException if an I/O error occurs.
  724.      */
  725.     private void outputField(final char c, final int next) throws IOException {
  726.         outputField(Character.toString(c), next, false);
  727.     }

  728.     /** Output one integer field.
  729.      * @param format format to use
  730.      * @param value field value
  731.      * @param next target column for next field
  732.      * @throws IOException if an I/O error occurs.
  733.      */
  734.     private void outputField(final String format, final int value, final int next) throws IOException {
  735.         outputField(String.format(Locale.US, format, value), next, false);
  736.     }

  737.     /** Output one double field.
  738.      * @param format format to use
  739.      * @param value field value
  740.      * @param next target column for next field
  741.      * @throws IOException if an I/O error occurs.
  742.      */
  743.     private void outputField(final String format, final double value, final int next) throws IOException {
  744.         if (Double.isNaN(value)) {
  745.             // NaN values are replaced by blank fields
  746.             outputField("", next, true);
  747.         } else {
  748.             outputField(String.format(Locale.US, format, value), next, false);
  749.         }
  750.     }

  751.     /** Output one field.
  752.      * @param field field to output
  753.      * @param next target column for next field
  754.      * @param leftJustified if true, field is left-justified
  755.      * @throws IOException if an I/O error occurs.
  756.      */
  757.     private void outputField(final String field, final int next, final boolean leftJustified) throws IOException {
  758.         final int padding = next - (field == null ? 0 : field.length()) - column;
  759.         if (leftJustified && field != null) {
  760.             output.append(field);
  761.         }
  762.         for (int i = 0; i < padding; ++i) {
  763.             output.append(' ');
  764.         }
  765.         if (!leftJustified && field != null) {
  766.             output.append(field);
  767.         }
  768.         column = next;
  769.     }

  770.     /** Get the scaling factor for an observation.
  771.      * @param type type of observation
  772.      * @param system satellite system for the observation
  773.      * @return scaling factor
  774.      */
  775.     private double getScaling(final ObservationType type, final SatelliteSystem system) {

  776.         for (final ScaleFactorCorrection scaleFactorCorrection : savedHeader.getScaleFactorCorrections(system)) {
  777.             // check if the next Observation Type to read needs to be scaled
  778.             if (scaleFactorCorrection.getTypesObsScaled().contains(type)) {
  779.                 return scaleFactorCorrection.getCorrection();
  780.             }
  781.         }

  782.         // no scaling
  783.         return 1.0;

  784.     }

  785. }