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.Map;
  23. import java.util.function.BiFunction;

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

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

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

  64.     /** Format for one 1 digit integer field. */
  65.     private static final FastLongFormatter ONE_DIGIT_INTEGER = new FastLongFormatter(1, false);

  66.     /** Format for one 2 digits integer field. */
  67.     private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);

  68.     /** Format for one 2 digits integer field. */
  69.     private static final FastLongFormatter TWO_DIGITS_INTEGER = new FastLongFormatter(2, false);

  70.     /** Format for one 4 digits integer field. */
  71.     private static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);

  72.     /** Format for one 3 digits integer field. */
  73.     private static final FastLongFormatter THREE_DIGITS_INTEGER = new FastLongFormatter(3, false);

  74.     /** Format for one 4 digits integer field. */
  75.     private static final FastLongFormatter FOUR_DIGITS_INTEGER = new FastLongFormatter(4, false);

  76.     /** Format for one 6 digits integer field. */
  77.     private static final FastLongFormatter SIX_DIGITS_INTEGER = new FastLongFormatter(6, false);

  78.     /** Format for one 8.3 digits float field. */
  79.     private static final FastDoubleFormatter EIGHT_THREE_DIGITS_FLOAT = new FastDoubleFormatter(8, 3);

  80.     /** Format for one 8.5 digits float field. */
  81.     private static final FastDoubleFormatter EIGHT_FIVE_DIGITS_FLOAT = new FastDoubleFormatter(8, 5);

  82.     /** Format for one 9.2 digits float field. */
  83.     private static final FastDoubleFormatter NINE_TWO_DIGITS_FLOAT = new FastDoubleFormatter(9, 2);

  84.     /** Format for one 9.4 digits float field. */
  85.     private static final FastDoubleFormatter NINE_FOUR_DIGITS_FLOAT = new FastDoubleFormatter(9, 4);

  86.     /** Format for one 10.3 digits float field. */
  87.     private static final FastDoubleFormatter TEN_THREE_DIGITS_FLOAT = new FastDoubleFormatter(10, 3);

  88.     /** Format for one 11.7 digits float field. */
  89.     private static final FastDoubleFormatter ELEVEN_SEVEN_DIGITS_FLOAT = new FastDoubleFormatter(11, 7);

  90.     /** Format for one 12.9 digits float field. */
  91.     private static final FastDoubleFormatter TWELVE_NINE_DIGITS_FLOAT = new FastDoubleFormatter(12, 9);

  92.     /** Format for one 13.7 digits float field. */
  93.     private static final FastDoubleFormatter THIRTEEN_SEVEN_DIGITS_FLOAT = new FastDoubleFormatter(13, 7);

  94.     /** Format for one 14.3 digits float field. */
  95.     private static final FastDoubleFormatter FOURTEEN_THREE_DIGITS_FLOAT = new FastDoubleFormatter(14, 3);

  96.     /** Format for one 14.4 digits float field. */
  97.     private static final FastDoubleFormatter FOURTEEN_FOUR_DIGITS_FLOAT = new FastDoubleFormatter(14, 4);

  98.     /** Format for one 15.12 digits float field. */
  99.     private static final FastDoubleFormatter FIFTEEN_TWELVE_DIGITS_FLOAT = new FastDoubleFormatter(15, 12);

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

  104.     /** Destination of generated output. */
  105.     private final Appendable output;

  106.     /** Output name for error messages. */
  107.     private final String outputName;

  108.     /** Receiver clock offset model. */
  109.     private ClockModel receiverClockModel;

  110.     /** Time scale for writing dates. */
  111.     private TimeScale timeScale;

  112.     /** Saved header. */
  113.     private RinexObservationHeader savedHeader;

  114.     /** Saved comments. */
  115.     private List<RinexComment> savedComments;

  116.     /** Pending observations. */
  117.     private final List<ObservationDataSet> pending;

  118.     /** Line number. */
  119.     private int lineNumber;

  120.     /** Column number. */
  121.     private int column;

  122.     /** Set of time scales.
  123.      * @since 13.0
  124.      */
  125.     private final TimeScales timeScales;

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

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

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

  168.     /** {@inheritDoc} */
  169.     @Override
  170.     public void close() throws IOException {
  171.         processPending();
  172.     }

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

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

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

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

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

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

  239.         // RINEX VERSION / TYPE
  240.         outputField(NINE_TWO_DIGITS_FLOAT, header.getFormatVersion(), 9);
  241.         outputField("",                 20, true);
  242.         outputField("OBSERVATION DATA", 40, true);
  243.         outputField(header.getSatelliteSystem().getKey(), 41);
  244.         finishHeaderLine(RinexLabels.VERSION);

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

  269.         // MARKER NAME
  270.         outputField(header.getMarkerName(), 60, true);
  271.         finishHeaderLine(RinexLabels.MARKER_NAME);

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

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

  282.         // OBSERVER / AGENCY
  283.         outputField(header.getObserverName(), 20, true);
  284.         outputField(header.getAgencyName(),   60, true);
  285.         finishHeaderLine(RinexLabels.OBSERVER_AGENCY);

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

  291.         // ANT # / TYPE
  292.         outputField(header.getAntennaNumber(), 20, true);
  293.         outputField(header.getAntennaType(),   40, true);
  294.         finishHeaderLine(RinexLabels.ANT_NB_TYPE);

  295.         // APPROX POSITION XYZ
  296.         writeHeaderLine(header.getApproxPos(), RinexLabels.APPROX_POSITION_XYZ);

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

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

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

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

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

  323.         // ANTENNA: ZERODIR XYZ
  324.         writeHeaderLine(header.getAntennaZeroDirection(), RinexLabels.ANTENNA_ZERODIR_XYZ);

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

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

  342.         // DOI
  343.         writeHeaderLine(header.getDoi(), RinexLabels.DOI);

  344.         // LICENSE OF USE
  345.         writeHeaderLine(header.getLicense(), RinexLabels.LICENSE);

  346.         // STATION INFORMATION
  347.         writeHeaderLine(header.getStationInformation(), RinexLabels.STATION_INFORMATION);

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

  372.         // SIGNAL STRENGTH UNIT
  373.         writeHeaderLine(header.getSignalStrengthUnit(), RinexLabels.SIGNAL_STRENGTH_UNIT);

  374.         // INTERVAL
  375.         if (!Double.isNaN(header.getInterval())) {
  376.             outputField(TEN_THREE_DIGITS_FLOAT, header.getInterval(), 10);
  377.             finishHeaderLine(RinexLabels.INTERVAL);
  378.         }

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

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

  401.         // RCV CLOCK OFFS APPL
  402.         outputField(SIX_DIGITS_INTEGER, header.getClockOffsetApplied() ? 1 : 0, 6);
  403.         finishHeaderLine(RinexLabels.RCV_CLOCK_OFFS_APPL);

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

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

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

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

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

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

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

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

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

  552.         // END OF HEADER
  553.         writeHeaderLine("", RinexLabels.END);

  554.     }

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

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

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

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

  576.     }

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

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

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

  588.             // prepare for next batch
  589.             pending.clear();

  590.         }

  591.     }

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

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

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

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

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

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

  664.     }

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

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

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

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

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

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

  718.     }

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

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

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

  754.     /** Finish one line.
  755.      * @throws IOException if an I/O error occurs.
  756.      */
  757.     private void finishLine() throws IOException {

  758.         // pending line
  759.         output.append(System.lineSeparator());
  760.         lineNumber++;
  761.         column = 0;

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

  774.     }

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

  783.     /** Output one integer field.
  784.      * @param formatter formatter to use
  785.      * @param value field value
  786.      * @param next target column for next field
  787.      * @throws IOException if an I/O error occurs.
  788.      */
  789.     private void outputField(final FastLongFormatter formatter, final int value, final int next) throws IOException {
  790.         outputField(formatter.toString(value), next, false);
  791.     }

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

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

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

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

  840.         // no scaling
  841.         return 1.0;

  842.     }

  843. }