RinexObservationWriter.java

  1. /* Copyright 2022-2025 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 java.util.function.BiFunction;

  25. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  26. import org.hipparchus.util.FastMath;
  27. import org.orekit.annotation.DefaultDataContext;
  28. import org.orekit.data.DataContext;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.files.rinex.AppliedDCBS;
  32. import org.orekit.files.rinex.AppliedPCVS;
  33. import org.orekit.files.rinex.section.RinexComment;
  34. import org.orekit.files.rinex.section.RinexLabels;
  35. import org.orekit.gnss.ObservationTimeScale;
  36. import org.orekit.gnss.ObservationType;
  37. import org.orekit.gnss.PredefinedObservationType;
  38. import org.orekit.gnss.SatInSystem;
  39. import org.orekit.gnss.SatelliteSystem;
  40. import org.orekit.time.AbsoluteDate;
  41. import org.orekit.time.ClockModel;
  42. import org.orekit.time.ClockTimeScale;
  43. import org.orekit.time.DateTimeComponents;
  44. import org.orekit.time.TimeScale;
  45. import org.orekit.time.TimeScales;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  101.     /** Destination of generated output. */
  102.     private final Appendable output;

  103.     /** Output name for error messages. */
  104.     private final String outputName;

  105.     /** Receiver clock offset model. */
  106.     private ClockModel receiverClockModel;

  107.     /** Time scale for writing dates. */
  108.     private TimeScale timeScale;

  109.     /** Saved header. */
  110.     private RinexObservationHeader savedHeader;

  111.     /** Saved comments. */
  112.     private List<RinexComment> savedComments;

  113.     /** Pending observations. */
  114.     private final List<ObservationDataSet> pending;

  115.     /** Line number. */
  116.     private int lineNumber;

  117.     /** Column number. */
  118.     private int column;

  119.     /** Set of time scales.
  120.      * @since 13.0
  121.      */
  122.     private final TimeScales timeScales;

  123.     /** Mapper from satellite system to time scales.
  124.      * @since 13.0
  125.      */
  126.     private final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder;

  127.     /** Simple constructor.
  128.      * <p>
  129.      * This constructor uses the {@link DataContext#getDefault() default data context}
  130.      * and recognizes only {@link PredefinedObservationType} and {@link SatelliteSystem}
  131.      * with non-null {@link SatelliteSystem#getObservationTimeScale() time scales}
  132.      * (i.e. neither user-defined, nor {@link SatelliteSystem#SBAS}, nor {@link SatelliteSystem#MIXED}).
  133.      * </p>
  134.      * @param output destination of generated output
  135.      * @param outputName output name for error messages
  136.      */
  137.     @DefaultDataContext
  138.     public RinexObservationWriter(final Appendable output, final String outputName) {
  139.         this(output, outputName,
  140.              (system, ts) -> system.getObservationTimeScale() == null ?
  141.                              null :
  142.                              system.getObservationTimeScale().getTimeScale(ts),
  143.              DataContext.getDefault().getTimeScales());
  144.     }

  145.     /** Simple constructor.
  146.      * @param output destination of generated output
  147.      * @param outputName output name for error messages
  148.      * @param timeScaleBuilder mapper from satellite system to time scales (useful for user-defined satellite systems)
  149.      * @param timeScales the set of time scales to use when parsing dates
  150.      * @since 13.0
  151.      */
  152.     public RinexObservationWriter(final Appendable output, final String outputName,
  153.                                   final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
  154.                                   final TimeScales timeScales) {
  155.         this.output           = output;
  156.         this.outputName       = outputName;
  157.         this.savedHeader      = null;
  158.         this.savedComments    = Collections.emptyList();
  159.         this.pending          = new ArrayList<>();
  160.         this.lineNumber       = 0;
  161.         this.column           = 0;
  162.         this.timeScaleBuilder = timeScaleBuilder;
  163.         this.timeScales       = timeScales;
  164.     }

  165.     /** {@inheritDoc} */
  166.     @Override
  167.     public void close() throws IOException {
  168.         processPending();
  169.     }

  170.     /** Set receiver clock model.
  171.      * @param receiverClockModel receiver clock model
  172.      * @since 12.1
  173.      */
  174.     public void setReceiverClockModel(final ClockModel receiverClockModel) {
  175.         this.receiverClockModel = receiverClockModel;
  176.     }

  177.     /** Write a complete observation file.
  178.      * <p>
  179.      * This method calls {@link #prepareComments(List)} and
  180.      * {@link #writeHeader(RinexObservationHeader)} once and then loops on
  181.      * calling {@link #writeObservationDataSet(ObservationDataSet)}
  182.      * for all observation data sets in the file
  183.      * </p>
  184.      * @param rinexObservation Rinex observation file to write
  185.      * @see #writeHeader(RinexObservationHeader)
  186.      * @see #writeObservationDataSet(ObservationDataSet)
  187.      * @exception IOException if an I/O error occurs.
  188.      */
  189.     @DefaultDataContext
  190.     public void writeCompleteFile(final RinexObservation rinexObservation)
  191.         throws IOException {
  192.         prepareComments(rinexObservation.getComments());
  193.         writeHeader(rinexObservation.getHeader());
  194.         for (final ObservationDataSet observationDataSet : rinexObservation.getObservationDataSets()) {
  195.             writeObservationDataSet(observationDataSet);
  196.         }
  197.     }

  198.     /** Prepare comments to be emitted at specified lines.
  199.      * @param comments comments to be emitted
  200.      */
  201.     public void prepareComments(final List<RinexComment> comments) {
  202.         savedComments = comments;
  203.     }

  204.     /** Write header.
  205.      * <p>
  206.      * This method must be called exactly once at the beginning
  207.      * (directly or by {@link #writeCompleteFile(RinexObservation)})
  208.      * </p>
  209.      * @param header header to write
  210.      * @exception IOException if an I/O error occurs.
  211.      */
  212.     @DefaultDataContext
  213.     public void writeHeader(final RinexObservationHeader header)
  214.         throws IOException {

  215.         // check header is written exactly once
  216.         if (savedHeader != null) {
  217.             throw new OrekitException(OrekitMessages.HEADER_ALREADY_WRITTEN, outputName);
  218.         }
  219.         savedHeader = header;
  220.         lineNumber  = 1;

  221.         final String timeScaleName;
  222.         if (timeScaleBuilder.apply(header.getSatelliteSystem(), timeScales) != null) {
  223.             timeScale     = timeScaleBuilder.apply(header.getSatelliteSystem(), timeScales);
  224.             timeScaleName = "   ";
  225.         } else {
  226.             timeScale     = ObservationTimeScale.GPS.getTimeScale(timeScales);
  227.             timeScaleName = timeScale.getName();
  228.         }
  229.         if (!header.getClockOffsetApplied() && receiverClockModel != null) {
  230.             // getClockOffsetApplied returned false, which means the measurements
  231.             // should *NOT* be put in system time scale, and the receiver has a clock model
  232.             // we have to set up a time scale corresponding to this receiver clock
  233.             // (but we keep the name set earlier despite it is not really relevant anymore)
  234.             timeScale = new ClockTimeScale(timeScale.getName(), timeScale, receiverClockModel);
  235.         }

  236.         // RINEX VERSION / TYPE
  237.         outputField("%9.2f", header.getFormatVersion(), 9);
  238.         outputField("",                 20, true);
  239.         outputField("OBSERVATION DATA", 40, true);
  240.         outputField(header.getSatelliteSystem().getKey(), 41);
  241.         finishHeaderLine(RinexLabels.VERSION);

  242.         // PGM / RUN BY / DATE
  243.         outputField(header.getProgramName(), 20, true);
  244.         outputField(header.getRunByName(),   40, true);
  245.         final DateTimeComponents dtc = header.getCreationDateComponents();
  246.         if (header.getFormatVersion() < 3.0 && dtc.getTime().getSecond() < 0.5) {
  247.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 42);
  248.             outputField('-', 43);
  249.             outputField(dtc.getDate().getMonthEnum().getUpperCaseAbbreviation(), 46,  true);
  250.             outputField('-', 47);
  251.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 49);
  252.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 52);
  253.             outputField(':', 53);
  254.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 55);
  255.             outputField(header.getCreationTimeZone(), 58, true);
  256.         } else {
  257.             outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 44);
  258.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 46);
  259.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 48);
  260.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 51);
  261.             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 53);
  262.             outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 55);
  263.             outputField(header.getCreationTimeZone(), 59, false);
  264.         }
  265.         finishHeaderLine(RinexLabels.PROGRAM);

  266.         // MARKER NAME
  267.         outputField(header.getMarkerName(), 60, true);
  268.         finishHeaderLine(RinexLabels.MARKER_NAME);

  269.         // MARKER NUMBER
  270.         if (header.getMarkerNumber() != null) {
  271.             outputField(header.getMarkerNumber(), 20, true);
  272.             finishHeaderLine(RinexLabels.MARKER_NUMBER);
  273.         }

  274.         // MARKER TYPE
  275.         if (header.getFormatVersion() >= 2.20) {
  276.             outputField(header.getMarkerType(), 20, true);
  277.             finishHeaderLine(RinexLabels.MARKER_TYPE);
  278.         }

  279.         // OBSERVER / AGENCY
  280.         outputField(header.getObserverName(), 20, true);
  281.         outputField(header.getAgencyName(),   60, true);
  282.         finishHeaderLine(RinexLabels.OBSERVER_AGENCY);

  283.         // REC # / TYPE / VERS
  284.         outputField(header.getReceiverNumber(),  20, true);
  285.         outputField(header.getReceiverType(),    40, true);
  286.         outputField(header.getReceiverVersion(), 60, true);
  287.         finishHeaderLine(RinexLabels.REC_NB_TYPE_VERS);

  288.         // ANT # / TYPE
  289.         outputField(header.getAntennaNumber(), 20, true);
  290.         outputField(header.getAntennaType(),   40, true);
  291.         finishHeaderLine(RinexLabels.ANT_NB_TYPE);

  292.         // APPROX POSITION XYZ
  293.         writeHeaderLine(header.getApproxPos(), RinexLabels.APPROX_POSITION_XYZ);

  294.         // ANTENNA: DELTA H/E/N
  295.         if (!Double.isNaN(header.getAntennaHeight())) {
  296.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaHeight(),         14);
  297.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getX(), 28);
  298.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getY(), 42);
  299.             finishHeaderLine(RinexLabels.ANTENNA_DELTA_H_E_N);
  300.         }

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

  303.         // ANTENNA: PHASECENTER
  304.         if (header.getAntennaPhaseCenter() != null) {
  305.             outputField(header.getPhaseCenterSystem().getKey(), 1);
  306.             outputField("", 2, true);
  307.             outputField(header.getObservationCode(), 5, true);
  308.             outputField(NINE_FOUR_DIGITS_FLOAT,     header.getAntennaPhaseCenter().getX(), 14);
  309.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getY(), 28);
  310.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getZ(), 42);
  311.             finishHeaderLine(RinexLabels.ANTENNA_PHASE_CENTER);
  312.         }

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

  315.         // ANTENNA: ZERODIR AZI
  316.         if (!Double.isNaN(header.getAntennaAzimuth())) {
  317.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, FastMath.toDegrees(header.getAntennaAzimuth()), 14);
  318.             finishHeaderLine(RinexLabels.ANTENNA_ZERODIR_AZI);
  319.         }

  320.         // ANTENNA: ZERODIR XYZ
  321.         writeHeaderLine(header.getAntennaZeroDirection(), RinexLabels.ANTENNA_ZERODIR_XYZ);

  322.         // OBS SCALE FACTOR
  323.         if (FastMath.abs(header.getFormatVersion() - 2.20) < 0.001) {
  324.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  325.                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
  326.                     if (sfc != null) {
  327.                         outputField(SIX_DIGITS_INTEGER, (int) FastMath.round(sfc.getCorrection()), 6);
  328.                         outputField(SIX_DIGITS_INTEGER, sfc.getTypesObsScaled().size(), 12);
  329.                         for (int i = 0; i < sfc.getTypesObsScaled().size(); ++i) {
  330.                             outputField(sfc.getTypesObsScaled().get(i).getName(), 18 + 6 * i, false);
  331.                         }
  332.                         finishHeaderLine(RinexLabels.OBS_SCALE_FACTOR);
  333.                     }
  334.                 }
  335.             }
  336.         }

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

  339.         // DOI
  340.         writeHeaderLine(header.getDoi(), RinexLabels.DOI);

  341.         // LICENSE OF USE
  342.         writeHeaderLine(header.getLicense(), RinexLabels.LICENSE);

  343.         // STATION INFORMATION
  344.         writeHeaderLine(header.getStationInformation(), RinexLabels.STATION_INFORMATION);

  345.         // SYS / # / OBS TYPES
  346.         for (Map.Entry<SatelliteSystem, List<ObservationType>> entry : header.getTypeObs().entrySet()) {
  347.             if (header.getFormatVersion() < 3.0) {
  348.                 outputField(SIX_DIGITS_INTEGER, entry.getValue().size(), 6);
  349.             } else {
  350.                 outputField(entry.getKey().getKey(), 1);
  351.                 outputField(THREE_DIGITS_INTEGER, entry.getValue().size(), 6);
  352.             }
  353.             for (final ObservationType observationType : entry.getValue()) {
  354.                 int next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
  355.                 if (next > LABEL_INDEX) {
  356.                     // we need to set up a continuation line
  357.                     finishHeaderLine(header.getFormatVersion() < 3.0 ?
  358.                                      RinexLabels.NB_TYPES_OF_OBSERV :
  359.                                      RinexLabels.SYS_NB_TYPES_OF_OBSERV);
  360.                     outputField("", 6, true);
  361.                     next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
  362.                 }
  363.                 outputField(observationType.getName(), next, false);
  364.             }
  365.             finishHeaderLine(header.getFormatVersion() < 3.0 ?
  366.                              RinexLabels.NB_TYPES_OF_OBSERV :
  367.                              RinexLabels.SYS_NB_TYPES_OF_OBSERV);
  368.         }

  369.         // SIGNAL STRENGTH UNIT
  370.         writeHeaderLine(header.getSignalStrengthUnit(), RinexLabels.SIGNAL_STRENGTH_UNIT);

  371.         // INTERVAL
  372.         if (!Double.isNaN(header.getInterval())) {
  373.             outputField(TEN_THREE_DIGITS_FLOAT, header.getInterval(), 10);
  374.             finishHeaderLine(RinexLabels.INTERVAL);
  375.         }

  376.         // TIME OF FIRST OBS
  377.         final DateTimeComponents dtcFirst = header.getTFirstObs().getComponents(timeScale).roundIfNeeded(60, 7);
  378.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getYear(), 6);
  379.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getMonth(), 12);
  380.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getDay(), 18);
  381.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getHour(), 24);
  382.         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getMinute(), 30);
  383.         outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcFirst.getTime().getSecond(), 43);
  384.         outputField(timeScaleName, 51, false);
  385.         finishHeaderLine(RinexLabels.TIME_OF_FIRST_OBS);

  386.         // TIME OF LAST OBS
  387.         if (!header.getTLastObs().equals(AbsoluteDate.FUTURE_INFINITY)) {
  388.             final DateTimeComponents dtcLast = header.getTLastObs().getComponents(timeScale).roundIfNeeded(60, 7);
  389.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getYear(), 6);
  390.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getMonth(), 12);
  391.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getDay(), 18);
  392.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getHour(), 24);
  393.             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getMinute(), 30);
  394.             outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcLast.getTime().getSecond(), 43);
  395.             outputField(timeScaleName, 51, false);
  396.             finishHeaderLine(RinexLabels.TIME_OF_LAST_OBS);
  397.         }

  398.         // RCV CLOCK OFFS APPL
  399.         outputField(SIX_DIGITS_INTEGER, header.getClockOffsetApplied() ? 1 : 0, 6);
  400.         finishHeaderLine(RinexLabels.RCV_CLOCK_OFFS_APPL);

  401.         // SYS / DCBS APPLIED
  402.         for (final AppliedDCBS appliedDCBS : header.getListAppliedDCBS()) {
  403.             outputField(appliedDCBS.getSatelliteSystem().getKey(),  1);
  404.             outputField("",                                         2, true);
  405.             outputField(appliedDCBS.getProgDCBS(),                 20, true);
  406.             outputField(appliedDCBS.getSourceDCBS(),               60, true);
  407.             finishHeaderLine(RinexLabels.SYS_DCBS_APPLIED);
  408.         }

  409.         // SYS / PCVS APPLIED
  410.         for (final AppliedPCVS appliedPCVS : header.getListAppliedPCVS()) {
  411.             outputField(appliedPCVS.getSatelliteSystem().getKey(),  1);
  412.             outputField("",                                         2, true);
  413.             outputField(appliedPCVS.getProgPCVS(),                 20, true);
  414.             outputField(appliedPCVS.getSourcePCVS(),               60, true);
  415.             finishHeaderLine(RinexLabels.SYS_PCVS_APPLIED);
  416.         }

  417.         // SYS / SCALE FACTOR
  418.         if (header.getFormatVersion() >= 3.0) {
  419.             for (final SatelliteSystem system : SatelliteSystem.values()) {
  420.                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
  421.                     if (sfc != null) {
  422.                         outputField(system.getKey(), 1);
  423.                         outputField("", 2, true);
  424.                         outputField(FOUR_DIGITS_INTEGER, (int) FastMath.rint(sfc.getCorrection()), 6);
  425.                         if (sfc.getTypesObsScaled().size() < header.getTypeObs().get(system).size()) {
  426.                             outputField("", 8, true);
  427.                             outputField(TWO_DIGITS_INTEGER,  sfc.getTypesObsScaled().size(), 10);
  428.                             for (ObservationType observationType : sfc.getTypesObsScaled()) {
  429.                                 int next = column + 4;
  430.                                 if (next > LABEL_INDEX) {
  431.                                     // we need to set up a continuation line
  432.                                     finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
  433.                                     outputField("", 10, true);
  434.                                     next = column + 4;
  435.                                 }
  436.                                 outputField("", next - 3, true);
  437.                                 outputField(observationType.getName(), next, true);
  438.                             }
  439.                         }
  440.                         finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
  441.                     }
  442.                 }
  443.             }
  444.         }

  445.         // SYS / PHASE SHIFT
  446.         for (final PhaseShiftCorrection psc : header.getPhaseShiftCorrections()) {
  447.             outputField(psc.getSatelliteSystem().getKey(), 1);
  448.             outputField(psc.getTypeObs().getName(), 5, false);
  449.             outputField(EIGHT_FIVE_DIGITS_FLOAT, psc.getCorrection(), 14);
  450.             if (!psc.getSatsCorrected().isEmpty()) {
  451.                 outputField(TWO_DIGITS_INTEGER, psc.getSatsCorrected().size(), 18);
  452.                 for (final SatInSystem sis : psc.getSatsCorrected()) {
  453.                     int next = column + 4;
  454.                     if (next > LABEL_INDEX) {
  455.                         // we need to set up a continuation line
  456.                         finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
  457.                         outputField("", 18, true);
  458.                         next = column + 4;
  459.                     }
  460.                     outputField(sis.toString(), next, false);
  461.                 }
  462.             }
  463.             finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
  464.         }

  465.         if (header.getFormatVersion() >= 3.01) {
  466.             if (!header.getGlonassChannels().isEmpty()) {
  467.                 // GLONASS SLOT / FRQ #
  468.                 outputField(THREE_DIGITS_INTEGER, header.getGlonassChannels().size(), 3);
  469.                 outputField("", 4, true);
  470.                 for (final GlonassSatelliteChannel channel : header.getGlonassChannels()) {
  471.                     int next = column + 7;
  472.                     if (next > LABEL_INDEX) {
  473.                         // we need to set up a continuation line
  474.                         finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
  475.                         outputField("", 4, true);
  476.                         next = column + 7;
  477.                     }
  478.                     outputField(channel.getSatellite().getSystem().getKey(), next - 6);
  479.                     outputField(PADDED_TWO_DIGITS_INTEGER, channel.getSatellite().getPRN(), next - 4);
  480.                     outputField(TWO_DIGITS_INTEGER, channel.getK(), next - 1);
  481.                     outputField("", next, true);
  482.                 }
  483.             }
  484.             finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
  485.         }

  486.         if (header.getFormatVersion() >= 3.0) {
  487.             // GLONASS COD/PHS/BIS
  488.             if (Double.isNaN(header.getC1cCodePhaseBias())) {
  489.                 outputField("", 13, true);
  490.             } else {
  491.                 outputField(PredefinedObservationType.C1C.getName(), 4, false);
  492.                 outputField("", 5, true);
  493.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1cCodePhaseBias(), 13);
  494.             }
  495.             if (Double.isNaN(header.getC1pCodePhaseBias())) {
  496.                 outputField("", 26, true);
  497.             } else {
  498.                 outputField(PredefinedObservationType.C1P.getName(), 17, false);
  499.                 outputField("", 18, true);
  500.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1pCodePhaseBias(), 26);
  501.             }
  502.             if (Double.isNaN(header.getC2cCodePhaseBias())) {
  503.                 outputField("", 39, true);
  504.             } else {
  505.                 outputField(PredefinedObservationType.C2C.getName(), 30, false);
  506.                 outputField("", 31, true);
  507.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2cCodePhaseBias(), 39);
  508.             }
  509.             if (Double.isNaN(header.getC2pCodePhaseBias())) {
  510.                 outputField("", 52, true);
  511.             } else {
  512.                 outputField(PredefinedObservationType.C2P.getName(), 43, false);
  513.                 outputField("", 44, true);
  514.                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2pCodePhaseBias(), 52);
  515.             }
  516.             finishHeaderLine(RinexLabels.GLONASS_COD_PHS_BIS);
  517.         }

  518.         // LEAP SECONDS
  519.         if (header.getLeapSeconds() > 0) {
  520.             outputField(SIX_DIGITS_INTEGER, header.getLeapSeconds(), 6);
  521.             if (header.getFormatVersion() >= 3.0) {
  522.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsFuture(),  12);
  523.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsWeekNum(), 18);
  524.                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsDayNum(),  24);
  525.             }
  526.             finishHeaderLine(RinexLabels.LEAP_SECONDS);
  527.         }

  528.         // # OF SATELLITES
  529.         if (header.getNbSat() >= 0) {
  530.             outputField(SIX_DIGITS_INTEGER, header.getNbSat(), 6);
  531.             finishHeaderLine(RinexLabels.NB_OF_SATELLITES);
  532.         }

  533.         // PRN / # OF OBS
  534.         for (final Map.Entry<SatInSystem, Map<ObservationType, Integer>> entry1 : header.getNbObsPerSat().entrySet()) {
  535.             final SatInSystem sis = entry1.getKey();
  536.             outputField(sis.toString(), 6, false);
  537.             for (final Map.Entry<ObservationType, Integer> entry2 : entry1.getValue().entrySet()) {
  538.                 int next = column + 6;
  539.                 if (next > LABEL_INDEX) {
  540.                     // we need to set up a continuation line
  541.                     finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
  542.                     outputField("", 6, true);
  543.                     next = column + 6;
  544.                 }
  545.                 outputField(SIX_DIGITS_INTEGER, entry2.getValue(), next);
  546.             }
  547.             finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
  548.         }

  549.         // END OF HEADER
  550.         writeHeaderLine("", RinexLabels.END);

  551.     }

  552.     /** Write one observation data set.
  553.      * <p>
  554.      * Note that this writers output only regular observations, so
  555.      * the event flag is always set to 0
  556.      * </p>
  557.      * @param observationDataSet observation data set to write
  558.      * @exception IOException if an I/O error occurs.
  559.      */
  560.     public void writeObservationDataSet(final ObservationDataSet observationDataSet)
  561.         throws IOException {

  562.         // check header has already been written
  563.         if (savedHeader == null) {
  564.             throw new OrekitException(OrekitMessages.HEADER_NOT_WRITTEN, outputName);
  565.         }

  566.         if (!pending.isEmpty() && observationDataSet.durationFrom(pending.get(0).getDate()) > EPS_DATE) {
  567.             // the specified observation belongs to the next batch
  568.             // we must process the current batch of pending observations
  569.             processPending();
  570.         }

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

  573.     }

  574.     /** Process all pending measurements.
  575.      * @exception IOException if an I/O error occurs.
  576.      */
  577.     private void processPending() throws IOException {

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

  579.             // write the batch of pending observations
  580.             if (savedHeader.getFormatVersion() < 3.0) {
  581.                 writePendingRinex2Observations();
  582.             } else {
  583.                 writePendingRinex34Observations();
  584.             }

  585.             // prepare for next batch
  586.             pending.clear();

  587.         }

  588.     }

  589.     /** Write one observation data set in RINEX 2 format.
  590.      * @exception IOException if an I/O error occurs.
  591.      */
  592.     public void writePendingRinex2Observations() throws IOException {

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

  594.         // EPOCH/SAT
  595.         final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
  596.         outputField("",  1, true);
  597.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getYear() % 100,    3);
  598.         outputField("",  4, true);
  599.         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getMonth(),         6);
  600.         outputField("",  7, true);
  601.         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getDay(),           9);
  602.         outputField("", 10, true);
  603.         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getHour(),         12);
  604.         outputField("", 13, true);
  605.         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getMinute(),       15);
  606.         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(),       26);

  607.         // event flag
  608.         outputField("", 28, true);
  609.         if (first.getEventFlag() == 0) {
  610.             outputField("", 29, true);
  611.         } else {
  612.             outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 29);
  613.         }

  614.         // list of satellites and receiver clock offset
  615.         outputField(THREE_DIGITS_INTEGER, pending.size(), 32);
  616.         boolean offsetWritten = false;
  617.         final double  clockOffset   = first.getRcvrClkOffset();
  618.         for (final ObservationDataSet ods : pending) {
  619.             int next = column + 3;
  620.             if (next > 68) {
  621.                 // we need to set up a continuation line
  622.                 if (clockOffset != 0.0) {
  623.                     outputField(TWELVE_NINE_DIGITS_FLOAT, clockOffset, 80);
  624.                 }
  625.                 offsetWritten = true;
  626.                 finishLine();
  627.                 outputField("", 32, true);
  628.                 next = column + 3;
  629.             }
  630.             outputField(ods.getSatellite().toString(), next, false);
  631.         }
  632.         if (!offsetWritten && clockOffset != 0.0) {
  633.             outputField("", 68, true);
  634.             outputField(TWELVE_NINE_DIGITS_FLOAT, first.getRcvrClkOffset(), 80);
  635.         }
  636.         finishLine();

  637.         // observations per se
  638.         for (final ObservationDataSet ods : pending) {
  639.             for (final ObservationData od : ods.getObservationData()) {
  640.                 int next = column + 16;
  641.                 if (next > 80) {
  642.                     // we need to set up a continuation line
  643.                     finishLine();
  644.                     next = column + 16;
  645.                 }
  646.                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
  647.                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
  648.                 if (od.getLossOfLockIndicator() == 0) {
  649.                     outputField("", next - 1, true);
  650.                 } else {
  651.                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
  652.                 }
  653.                 if (od.getSignalStrength() == 0) {
  654.                     outputField("", next, true);
  655.                 } else {
  656.                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
  657.                 }
  658.             }
  659.             finishLine();
  660.         }

  661.     }

  662.     /** Write one observation data set in RINEX 3/4 format.
  663.      * @exception IOException if an I/O error occurs.
  664.      */
  665.     public void writePendingRinex34Observations()
  666.         throws IOException {

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

  668.         // EPOCH/SAT
  669.         final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
  670.         outputField(">",  2, true);
  671.         outputField(FOUR_DIGITS_INTEGER,         dtc.getDate().getYear(),    6);
  672.         outputField("",   7, true);
  673.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getMonth(),   9);
  674.         outputField("",  10, true);
  675.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getDay(),    12);
  676.         outputField("", 13, true);
  677.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getHour(),   15);
  678.         outputField("", 16, true);
  679.         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getMinute(), 18);
  680.         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(), 29);

  681.         // event flag
  682.         outputField("", 31, true);
  683.         if (first.getEventFlag() == 0) {
  684.             outputField("", 32, true);
  685.         } else {
  686.             outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 32);
  687.         }

  688.         // number of satellites and receiver clock offset
  689.         outputField(THREE_DIGITS_INTEGER, pending.size(), 35);
  690.         if (first.getRcvrClkOffset() != 0.0) {
  691.             outputField("", 41, true);
  692.             outputField(FIFTEEN_TWELVE_DIGITS_FLOAT, first.getRcvrClkOffset(), 56);
  693.         }
  694.         finishLine();

  695.         // observations per se
  696.         for (final ObservationDataSet ods : pending) {
  697.             outputField(ods.getSatellite().toString(), 3, false);
  698.             for (final ObservationData od : ods.getObservationData()) {
  699.                 final int next = column + 16;
  700.                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
  701.                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
  702.                 if (od.getLossOfLockIndicator() == 0) {
  703.                     outputField("", next - 1, true);
  704.                 } else {
  705.                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
  706.                 }
  707.                 if (od.getSignalStrength() == 0) {
  708.                     outputField("", next, true);
  709.                 } else {
  710.                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
  711.                 }
  712.             }
  713.             finishLine();
  714.         }

  715.     }

  716.     /** Write one header string.
  717.      * @param s string data (may be null)
  718.      * @param label line label
  719.      * @throws IOException if an I/O error occurs.
  720.      */
  721.     private void writeHeaderLine(final String s, final RinexLabels label) throws IOException {
  722.         if (s != null) {
  723.             outputField(s, s.length(), true);
  724.             finishHeaderLine(label);
  725.         }
  726.     }

  727.     /** Write one header vector.
  728.      * @param vector vector data (may be null)
  729.      * @param label line label
  730.      * @throws IOException if an I/O error occurs.
  731.      */
  732.     private void writeHeaderLine(final Vector3D vector, final RinexLabels label) throws IOException {
  733.         if (vector != null) {
  734.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getX(), 14);
  735.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getY(), 28);
  736.             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getZ(), 42);
  737.             finishHeaderLine(label);
  738.         }
  739.     }

  740.     /** Finish one header line.
  741.      * @param label line label
  742.      * @throws IOException if an I/O error occurs.
  743.      */
  744.     private void finishHeaderLine(final RinexLabels label) throws IOException {
  745.         for (int i = column; i < LABEL_INDEX; ++i) {
  746.             output.append(' ');
  747.         }
  748.         output.append(label.getLabel());
  749.         finishLine();
  750.     }

  751.     /** Finish one line.
  752.      * @throws IOException if an I/O error occurs.
  753.      */
  754.     private void finishLine() throws IOException {

  755.         // pending line
  756.         output.append(System.lineSeparator());
  757.         lineNumber++;
  758.         column = 0;

  759.         // emit comments that should be placed at next lines
  760.         for (final RinexComment comment : savedComments) {
  761.             if (comment.getLineNumber() == lineNumber) {
  762.                 outputField(comment.getText(), LABEL_INDEX, true);
  763.                 output.append(RinexLabels.COMMENT.getLabel());
  764.                 output.append(System.lineSeparator());
  765.                 lineNumber++;
  766.                 column = 0;
  767.             } else if (comment.getLineNumber() > lineNumber) {
  768.                 break;
  769.             }
  770.         }

  771.     }

  772.     /** Output one single character field.
  773.      * @param c field value
  774.      * @param next target column for next field
  775.      * @throws IOException if an I/O error occurs.
  776.      */
  777.     private void outputField(final char c, final int next) throws IOException {
  778.         outputField(Character.toString(c), next, false);
  779.     }

  780.     /** Output one integer field.
  781.      * @param format format to use
  782.      * @param value field value
  783.      * @param next target column for next field
  784.      * @throws IOException if an I/O error occurs.
  785.      */
  786.     private void outputField(final String format, final int value, final int next) throws IOException {
  787.         outputField(String.format(Locale.US, format, value), next, false);
  788.     }

  789.     /** Output one double field.
  790.      * @param format format to use
  791.      * @param value field value
  792.      * @param next target column for next field
  793.      * @throws IOException if an I/O error occurs.
  794.      */
  795.     private void outputField(final String format, final double value, final int next) throws IOException {
  796.         if (Double.isNaN(value)) {
  797.             // NaN values are replaced by blank fields
  798.             outputField("", next, true);
  799.         } else {
  800.             outputField(String.format(Locale.US, format, value), next, false);
  801.         }
  802.     }

  803.     /** Output one field.
  804.      * @param field field to output
  805.      * @param next target column for next field
  806.      * @param leftJustified if true, field is left-justified
  807.      * @throws IOException if an I/O error occurs.
  808.      */
  809.     private void outputField(final String field, final int next, final boolean leftJustified) throws IOException {
  810.         final int padding = next - (field == null ? 0 : field.length()) - column;
  811.         if (padding < 0) {
  812.             throw new OrekitException(OrekitMessages.FIELD_TOO_LONG, field, next - column);
  813.         }
  814.         if (leftJustified && field != null) {
  815.             output.append(field);
  816.         }
  817.         for (int i = 0; i < padding; ++i) {
  818.             output.append(' ');
  819.         }
  820.         if (!leftJustified && field != null) {
  821.             output.append(field);
  822.         }
  823.         column = next;
  824.     }

  825.     /** Get the scaling factor for an observation.
  826.      * @param type type of observation
  827.      * @param system satellite system for the observation
  828.      * @return scaling factor
  829.      */
  830.     private double getScaling(final ObservationType type, final SatelliteSystem system) {

  831.         for (final ScaleFactorCorrection scaleFactorCorrection : savedHeader.getScaleFactorCorrections(system)) {
  832.             // check if the next Observation Type to read needs to be scaled
  833.             if (scaleFactorCorrection.getTypesObsScaled().contains(type)) {
  834.                 return scaleFactorCorrection.getCorrection();
  835.             }
  836.         }

  837.         // no scaling
  838.         return 1.0;

  839.     }

  840. }