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  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.function.BiFunction;
25  
26  import org.hipparchus.geometry.euclidean.threed.Vector3D;
27  import org.hipparchus.util.FastMath;
28  import org.orekit.annotation.DefaultDataContext;
29  import org.orekit.data.DataContext;
30  import org.orekit.errors.OrekitException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.files.rinex.AppliedDCBS;
33  import org.orekit.files.rinex.AppliedPCVS;
34  import org.orekit.files.rinex.section.RinexComment;
35  import org.orekit.files.rinex.section.RinexLabels;
36  import org.orekit.gnss.ObservationTimeScale;
37  import org.orekit.gnss.ObservationType;
38  import org.orekit.gnss.PredefinedObservationType;
39  import org.orekit.gnss.SatInSystem;
40  import org.orekit.gnss.SatelliteSystem;
41  import org.orekit.time.AbsoluteDate;
42  import org.orekit.time.ClockModel;
43  import org.orekit.time.ClockTimeScale;
44  import org.orekit.time.DateTimeComponents;
45  import org.orekit.time.TimeScale;
46  import org.orekit.time.TimeScales;
47  import org.orekit.utils.formatting.FastDoubleFormatter;
48  import org.orekit.utils.formatting.FastLongFormatter;
49  
50  /** Writer for Rinex observation file.
51   * <p>
52   * As RINEX file are organized in batches of observations at some dates,
53   * these observations are cached and a new batch is output only when
54   * a new date appears when calling {@link #writeObservationDataSet(ObservationDataSet)}
55   * or when the file is closed by calling the {@link #close() close} method.
56   * Failing to call {@link #close() close} would imply the last batch
57   * of measurements is not written. This is the reason why this class implements
58   * {@link AutoCloseable}, so the {@link #close() close} method can be called automatically in
59   * a {@code try-with-resources} statement.
60   * </p>
61   * @author Luc Maisonobe
62   * @since 12.0
63   */
64  public class RinexObservationWriter implements AutoCloseable {
65  
66      /** Index of label in header lines. */
67      private static final int LABEL_INDEX = 60;
68  
69      /** Format for one 1 digit integer field. */
70      private static final FastLongFormatter ONE_DIGIT_INTEGER = new FastLongFormatter(1, false);
71  
72      /** Format for one 2 digits integer field. */
73      private static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);
74  
75      /** Format for one 2 digits integer field. */
76      private static final FastLongFormatter TWO_DIGITS_INTEGER = new FastLongFormatter(2, false);
77  
78      /** Format for one 4 digits integer field. */
79      private static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);
80  
81      /** Format for one 3 digits integer field. */
82      private static final FastLongFormatter THREE_DIGITS_INTEGER = new FastLongFormatter(3, false);
83  
84      /** Format for one 4 digits integer field. */
85      private static final FastLongFormatter FOUR_DIGITS_INTEGER = new FastLongFormatter(4, false);
86  
87      /** Format for one 6 digits integer field. */
88      private static final FastLongFormatter SIX_DIGITS_INTEGER = new FastLongFormatter(6, false);
89  
90      /** Format for one 8.3 digits float field. */
91      private static final FastDoubleFormatter EIGHT_THREE_DIGITS_FLOAT = new FastDoubleFormatter(8, 3);
92  
93      /** Format for one 8.5 digits float field. */
94      private static final FastDoubleFormatter EIGHT_FIVE_DIGITS_FLOAT = new FastDoubleFormatter(8, 5);
95  
96      /** Format for one 9.2 digits float field. */
97      private static final FastDoubleFormatter NINE_TWO_DIGITS_FLOAT = new FastDoubleFormatter(9, 2);
98  
99      /** Format for one 9.4 digits float field. */
100     private static final FastDoubleFormatter NINE_FOUR_DIGITS_FLOAT = new FastDoubleFormatter(9, 4);
101 
102     /** Format for one 10.3 digits float field. */
103     private static final FastDoubleFormatter TEN_THREE_DIGITS_FLOAT = new FastDoubleFormatter(10, 3);
104 
105     /** Format for one 11.7 digits float field. */
106     private static final FastDoubleFormatter ELEVEN_SEVEN_DIGITS_FLOAT = new FastDoubleFormatter(11, 7);
107 
108     /** Format for one 12.9 digits float field. */
109     private static final FastDoubleFormatter TWELVE_NINE_DIGITS_FLOAT = new FastDoubleFormatter(12, 9);
110 
111     /** Format for one 13.7 digits float field. */
112     private static final FastDoubleFormatter THIRTEEN_SEVEN_DIGITS_FLOAT = new FastDoubleFormatter(13, 7);
113 
114     /** Format for one 14.3 digits float field. */
115     private static final FastDoubleFormatter FOURTEEN_THREE_DIGITS_FLOAT = new FastDoubleFormatter(14, 3);
116 
117     /** Format for one 14.4 digits float field. */
118     private static final FastDoubleFormatter FOURTEEN_FOUR_DIGITS_FLOAT = new FastDoubleFormatter(14, 4);
119 
120     /** Format for one 15.12 digits float field. */
121     private static final FastDoubleFormatter FIFTEEN_TWELVE_DIGITS_FLOAT = new FastDoubleFormatter(15, 12);
122 
123     /** Threshold for considering measurements are at the sate time.
124      * (we know the RINEX files encode dates with a resolution of 0.1µs)
125      */
126     private static final double EPS_DATE = 1.0e-8;
127 
128     /** Destination of generated output. */
129     private final Appendable output;
130 
131     /** Output name for error messages. */
132     private final String outputName;
133 
134     /** Receiver clock offset model. */
135     private ClockModel receiverClockModel;
136 
137     /** Time scale for writing dates. */
138     private TimeScale timeScale;
139 
140     /** Saved header. */
141     private RinexObservationHeader savedHeader;
142 
143     /** Saved comments. */
144     private List<RinexComment> savedComments;
145 
146     /** Pending observations. */
147     private final List<ObservationDataSet> pending;
148 
149     /** Line number. */
150     private int lineNumber;
151 
152     /** Column number. */
153     private int column;
154 
155     /** Set of time scales.
156      * @since 13.0
157      */
158     private final TimeScales timeScales;
159 
160     /** Mapper from satellite system to time scales.
161      * @since 13.0
162      */
163     private final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder;
164 
165     /** Simple constructor.
166      * <p>
167      * This constructor uses the {@link DataContext#getDefault() default data context}
168      * and recognizes only {@link PredefinedObservationType} and {@link SatelliteSystem}
169      * with non-null {@link SatelliteSystem#getObservationTimeScale() time scales}
170      * (i.e. neither user-defined, nor {@link SatelliteSystem#SBAS}, nor {@link SatelliteSystem#MIXED}).
171      * </p>
172      * @param output destination of generated output
173      * @param outputName output name for error messages
174      */
175     @DefaultDataContext
176     public RinexObservationWriter(final Appendable output, final String outputName) {
177         this(output, outputName,
178              (system, ts) -> system.getObservationTimeScale() == null ?
179                              null :
180                              system.getObservationTimeScale().getTimeScale(ts),
181              DataContext.getDefault().getTimeScales());
182     }
183 
184     /** Simple constructor.
185      * @param output destination of generated output
186      * @param outputName output name for error messages
187      * @param timeScaleBuilder mapper from satellite system to time scales (useful for user-defined satellite systems)
188      * @param timeScales the set of time scales to use when parsing dates
189      * @since 13.0
190      */
191     public RinexObservationWriter(final Appendable output, final String outputName,
192                                   final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
193                                   final TimeScales timeScales) {
194         this.output           = output;
195         this.outputName       = outputName;
196         this.savedHeader      = null;
197         this.savedComments    = Collections.emptyList();
198         this.pending          = new ArrayList<>();
199         this.lineNumber       = 0;
200         this.column           = 0;
201         this.timeScaleBuilder = timeScaleBuilder;
202         this.timeScales       = timeScales;
203     }
204 
205     /** {@inheritDoc} */
206     @Override
207     public void close() throws IOException {
208         processPending();
209     }
210 
211     /** Set receiver clock model.
212      * @param receiverClockModel receiver clock model
213      * @since 12.1
214      */
215     public void setReceiverClockModel(final ClockModel receiverClockModel) {
216         this.receiverClockModel = receiverClockModel;
217     }
218 
219     /** Write a complete observation file.
220      * <p>
221      * This method calls {@link #prepareComments(List)} and
222      * {@link #writeHeader(RinexObservationHeader)} once and then loops on
223      * calling {@link #writeObservationDataSet(ObservationDataSet)}
224      * for all observation data sets in the file
225      * </p>
226      * @param rinexObservation Rinex observation file to write
227      * @see #writeHeader(RinexObservationHeader)
228      * @see #writeObservationDataSet(ObservationDataSet)
229      * @exception IOException if an I/O error occurs.
230      */
231     @DefaultDataContext
232     public void writeCompleteFile(final RinexObservation rinexObservation)
233         throws IOException {
234         prepareComments(rinexObservation.getComments());
235         writeHeader(rinexObservation.getHeader());
236         for (final ObservationDataSet observationDataSet : rinexObservation.getObservationDataSets()) {
237             writeObservationDataSet(observationDataSet);
238         }
239     }
240 
241     /** Prepare comments to be emitted at specified lines.
242      * @param comments comments to be emitted
243      */
244     public void prepareComments(final List<RinexComment> comments) {
245         savedComments = comments;
246     }
247 
248     /** Write header.
249      * <p>
250      * This method must be called exactly once at the beginning
251      * (directly or by {@link #writeCompleteFile(RinexObservation)})
252      * </p>
253      * @param header header to write
254      * @exception IOException if an I/O error occurs.
255      */
256     @DefaultDataContext
257     public void writeHeader(final RinexObservationHeader header)
258         throws IOException {
259 
260         // check header is written exactly once
261         if (savedHeader != null) {
262             throw new OrekitException(OrekitMessages.HEADER_ALREADY_WRITTEN, outputName);
263         }
264         savedHeader = header;
265         lineNumber  = 1;
266 
267         final String timeScaleName;
268         if (timeScaleBuilder.apply(header.getSatelliteSystem(), timeScales) != null) {
269             timeScale     = timeScaleBuilder.apply(header.getSatelliteSystem(), timeScales);
270             timeScaleName = "   ";
271         } else {
272             timeScale     = ObservationTimeScale.GPS.getTimeScale(timeScales);
273             timeScaleName = timeScale.getName();
274         }
275         if (!header.getClockOffsetApplied() && receiverClockModel != null) {
276             // getClockOffsetApplied returned false, which means the measurements
277             // should *NOT* be put in system time scale, and the receiver has a clock model
278             // we have to set up a time scale corresponding to this receiver clock
279             // (but we keep the name set earlier despite it is not really relevant anymore)
280             timeScale = new ClockTimeScale(timeScale.getName(), timeScale, receiverClockModel);
281         }
282 
283         // RINEX VERSION / TYPE
284         outputField(NINE_TWO_DIGITS_FLOAT, header.getFormatVersion(), 9);
285         outputField("",                 20, true);
286         outputField("OBSERVATION DATA", 40, true);
287         outputField(header.getSatelliteSystem().getKey(), 41);
288         finishHeaderLine(RinexLabels.VERSION);
289 
290         // PGM / RUN BY / DATE
291         outputField(header.getProgramName(), 20, true);
292         outputField(header.getRunByName(),   40, true);
293         final DateTimeComponents dtc = header.getCreationDateComponents();
294         if (header.getFormatVersion() < 3.0 && dtc.getTime().getSecond() < 0.5) {
295             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 42);
296             outputField('-', 43);
297             outputField(dtc.getDate().getMonthEnum().getUpperCaseAbbreviation(), 46,  true);
298             outputField('-', 47);
299             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 49);
300             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 52);
301             outputField(':', 53);
302             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 55);
303             outputField(header.getCreationTimeZone(), 58, true);
304         } else {
305             outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 44);
306             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 46);
307             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 48);
308             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 51);
309             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 53);
310             outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 55);
311             outputField(header.getCreationTimeZone(), 59, false);
312         }
313         finishHeaderLine(RinexLabels.PROGRAM);
314 
315         // MARKER NAME
316         outputField(header.getMarkerName(), 60, true);
317         finishHeaderLine(RinexLabels.MARKER_NAME);
318 
319         // MARKER NUMBER
320         if (header.getMarkerNumber() != null) {
321             outputField(header.getMarkerNumber(), 20, true);
322             finishHeaderLine(RinexLabels.MARKER_NUMBER);
323         }
324 
325         // MARKER TYPE
326         if (header.getFormatVersion() >= 2.20) {
327             outputField(header.getMarkerType(), 20, true);
328             finishHeaderLine(RinexLabels.MARKER_TYPE);
329         }
330 
331         // OBSERVER / AGENCY
332         outputField(header.getObserverName(), 20, true);
333         outputField(header.getAgencyName(),   60, true);
334         finishHeaderLine(RinexLabels.OBSERVER_AGENCY);
335 
336         // REC # / TYPE / VERS
337         outputField(header.getReceiverNumber(),  20, true);
338         outputField(header.getReceiverType(),    40, true);
339         outputField(header.getReceiverVersion(), 60, true);
340         finishHeaderLine(RinexLabels.REC_NB_TYPE_VERS);
341 
342         // ANT # / TYPE
343         outputField(header.getAntennaNumber(), 20, true);
344         outputField(header.getAntennaType(),   40, true);
345         finishHeaderLine(RinexLabels.ANT_NB_TYPE);
346 
347         // APPROX POSITION XYZ
348         writeHeaderLine(header.getApproxPos(), RinexLabels.APPROX_POSITION_XYZ);
349 
350         // ANTENNA: DELTA H/E/N
351         if (!Double.isNaN(header.getAntennaHeight())) {
352             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaHeight(),         14);
353             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getX(), 28);
354             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getY(), 42);
355             finishHeaderLine(RinexLabels.ANTENNA_DELTA_H_E_N);
356         }
357 
358         // ANTENNA: DELTA X/Y/Z
359         writeHeaderLine(header.getAntennaReferencePoint(), RinexLabels.ANTENNA_DELTA_X_Y_Z);
360 
361         // ANTENNA: PHASECENTER
362         if (header.getAntennaPhaseCenter() != null) {
363             outputField(header.getPhaseCenterSystem().getKey(), 1);
364             outputField("", 2, true);
365             outputField(header.getObservationCode(), 5, true);
366             outputField(NINE_FOUR_DIGITS_FLOAT,     header.getAntennaPhaseCenter().getX(), 14);
367             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getY(), 28);
368             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getZ(), 42);
369             finishHeaderLine(RinexLabels.ANTENNA_PHASE_CENTER);
370         }
371 
372         // ANTENNA: B.SIGHT XY
373         writeHeaderLine(header.getAntennaBSight(), RinexLabels.ANTENNA_B_SIGHT_XYZ);
374 
375         // ANTENNA: ZERODIR AZI
376         if (!Double.isNaN(header.getAntennaAzimuth())) {
377             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, FastMath.toDegrees(header.getAntennaAzimuth()), 14);
378             finishHeaderLine(RinexLabels.ANTENNA_ZERODIR_AZI);
379         }
380 
381         // ANTENNA: ZERODIR XYZ
382         writeHeaderLine(header.getAntennaZeroDirection(), RinexLabels.ANTENNA_ZERODIR_XYZ);
383 
384         // OBS SCALE FACTOR
385         if (FastMath.abs(header.getFormatVersion() - 2.20) < 0.001) {
386             for (final SatelliteSystem system : SatelliteSystem.values()) {
387                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
388                     if (sfc != null) {
389                         outputField(SIX_DIGITS_INTEGER, (int) FastMath.round(sfc.getCorrection()), 6);
390                         outputField(SIX_DIGITS_INTEGER, sfc.getTypesObsScaled().size(), 12);
391                         for (int i = 0; i < sfc.getTypesObsScaled().size(); ++i) {
392                             outputField(sfc.getTypesObsScaled().get(i).getName(), 18 + 6 * i, false);
393                         }
394                         finishHeaderLine(RinexLabels.OBS_SCALE_FACTOR);
395                     }
396                 }
397             }
398         }
399 
400         // CENTER OF MASS: XYZ
401         writeHeaderLine(header.getCenterMass(), RinexLabels.CENTER_OF_MASS_XYZ);
402 
403         // DOI
404         writeHeaderLine(header.getDoi(), RinexLabels.DOI);
405 
406         // LICENSE OF USE
407         writeHeaderLine(header.getLicense(), RinexLabels.LICENSE);
408 
409         // STATION INFORMATION
410         writeHeaderLine(header.getStationInformation(), RinexLabels.STATION_INFORMATION);
411 
412         // SYS / # / OBS TYPES
413         for (Map.Entry<SatelliteSystem, List<ObservationType>> entry : header.getTypeObs().entrySet()) {
414             if (header.getFormatVersion() < 3.0) {
415                 outputField(SIX_DIGITS_INTEGER, entry.getValue().size(), 6);
416             } else {
417                 outputField(entry.getKey().getKey(), 1);
418                 outputField(THREE_DIGITS_INTEGER, entry.getValue().size(), 6);
419             }
420             for (final ObservationType observationType : entry.getValue()) {
421                 int next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
422                 if (next > LABEL_INDEX) {
423                     // we need to set up a continuation line
424                     finishHeaderLine(header.getFormatVersion() < 3.0 ?
425                                      RinexLabels.NB_TYPES_OF_OBSERV :
426                                      RinexLabels.SYS_NB_TYPES_OF_OBSERV);
427                     outputField("", 6, true);
428                     next = column + (header.getFormatVersion() < 3.0 ? 6 : 4);
429                 }
430                 outputField(observationType.getName(), next, false);
431             }
432             finishHeaderLine(header.getFormatVersion() < 3.0 ?
433                              RinexLabels.NB_TYPES_OF_OBSERV :
434                              RinexLabels.SYS_NB_TYPES_OF_OBSERV);
435         }
436 
437         // SIGNAL STRENGTH UNIT
438         writeHeaderLine(header.getSignalStrengthUnit(), RinexLabels.SIGNAL_STRENGTH_UNIT);
439 
440         // INTERVAL
441         if (!Double.isNaN(header.getInterval())) {
442             outputField(TEN_THREE_DIGITS_FLOAT, header.getInterval(), 10);
443             finishHeaderLine(RinexLabels.INTERVAL);
444         }
445 
446         // TIME OF FIRST OBS
447         final DateTimeComponents dtcFirst = header.getTFirstObs().getComponents(timeScale).roundIfNeeded(60, 7);
448         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getYear(), 6);
449         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getMonth(), 12);
450         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getDate().getDay(), 18);
451         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getHour(), 24);
452         outputField(SIX_DIGITS_INTEGER,          dtcFirst.getTime().getMinute(), 30);
453         outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcFirst.getTime().getSecond(), 43);
454         outputField(timeScaleName, 51, false);
455         finishHeaderLine(RinexLabels.TIME_OF_FIRST_OBS);
456 
457         // TIME OF LAST OBS
458         if (!header.getTLastObs().equals(AbsoluteDate.FUTURE_INFINITY)) {
459             final DateTimeComponents dtcLast = header.getTLastObs().getComponents(timeScale).roundIfNeeded(60, 7);
460             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getYear(), 6);
461             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getMonth(), 12);
462             outputField(SIX_DIGITS_INTEGER,          dtcLast.getDate().getDay(), 18);
463             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getHour(), 24);
464             outputField(SIX_DIGITS_INTEGER,          dtcLast.getTime().getMinute(), 30);
465             outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcLast.getTime().getSecond(), 43);
466             outputField(timeScaleName, 51, false);
467             finishHeaderLine(RinexLabels.TIME_OF_LAST_OBS);
468         }
469 
470         // RCV CLOCK OFFS APPL
471         outputField(SIX_DIGITS_INTEGER, header.getClockOffsetApplied() ? 1 : 0, 6);
472         finishHeaderLine(RinexLabels.RCV_CLOCK_OFFS_APPL);
473 
474         // SYS / DCBS APPLIED
475         for (final AppliedDCBS appliedDCBS : header.getListAppliedDCBS()) {
476             outputField(appliedDCBS.getSatelliteSystem().getKey(),  1);
477             outputField("",                                         2, true);
478             outputField(appliedDCBS.getProgDCBS(),                 20, true);
479             outputField(appliedDCBS.getSourceDCBS(),               60, true);
480             finishHeaderLine(RinexLabels.SYS_DCBS_APPLIED);
481         }
482 
483         // SYS / PCVS APPLIED
484         for (final AppliedPCVS appliedPCVS : header.getListAppliedPCVS()) {
485             outputField(appliedPCVS.getSatelliteSystem().getKey(),  1);
486             outputField("",                                         2, true);
487             outputField(appliedPCVS.getProgPCVS(),                 20, true);
488             outputField(appliedPCVS.getSourcePCVS(),               60, true);
489             finishHeaderLine(RinexLabels.SYS_PCVS_APPLIED);
490         }
491 
492         // SYS / SCALE FACTOR
493         if (header.getFormatVersion() >= 3.0) {
494             for (final SatelliteSystem system : SatelliteSystem.values()) {
495                 for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
496                     if (sfc != null) {
497                         outputField(system.getKey(), 1);
498                         outputField("", 2, true);
499                         outputField(FOUR_DIGITS_INTEGER, (int) FastMath.rint(sfc.getCorrection()), 6);
500                         if (sfc.getTypesObsScaled().size() < header.getTypeObs().get(system).size()) {
501                             outputField("", 8, true);
502                             outputField(TWO_DIGITS_INTEGER,  sfc.getTypesObsScaled().size(), 10);
503                             for (ObservationType observationType : sfc.getTypesObsScaled()) {
504                                 int next = column + 4;
505                                 if (next > LABEL_INDEX) {
506                                     // we need to set up a continuation line
507                                     finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
508                                     outputField("", 10, true);
509                                     next = column + 4;
510                                 }
511                                 outputField("", next - 3, true);
512                                 outputField(observationType.getName(), next, true);
513                             }
514                         }
515                         finishHeaderLine(RinexLabels.SYS_SCALE_FACTOR);
516                     }
517                 }
518             }
519         }
520 
521         // SYS / PHASE SHIFT
522         for (final PhaseShiftCorrection psc : header.getPhaseShiftCorrections()) {
523             outputField(psc.getSatelliteSystem().getKey(), 1);
524             outputField(psc.getTypeObs().getName(), 5, false);
525             outputField(EIGHT_FIVE_DIGITS_FLOAT, psc.getCorrection(), 14);
526             if (!psc.getSatsCorrected().isEmpty()) {
527                 outputField(TWO_DIGITS_INTEGER, psc.getSatsCorrected().size(), 18);
528                 for (final SatInSystem sis : psc.getSatsCorrected()) {
529                     int next = column + 4;
530                     if (next > LABEL_INDEX) {
531                         // we need to set up a continuation line
532                         finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
533                         outputField("", 18, true);
534                         next = column + 4;
535                     }
536                     outputField(sis.toString(), next, false);
537                 }
538             }
539             finishHeaderLine(RinexLabels.SYS_PHASE_SHIFT);
540         }
541 
542         if (header.getFormatVersion() >= 3.01) {
543             if (!header.getGlonassChannels().isEmpty()) {
544                 // GLONASS SLOT / FRQ #
545                 outputField(THREE_DIGITS_INTEGER, header.getGlonassChannels().size(), 3);
546                 outputField("", 4, true);
547                 for (final GlonassSatelliteChannel channel : header.getGlonassChannels()) {
548                     int next = column + 7;
549                     if (next > LABEL_INDEX) {
550                         // we need to set up a continuation line
551                         finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
552                         outputField("", 4, true);
553                         next = column + 7;
554                     }
555                     outputField(channel.getSatellite().getSystem().getKey(), next - 6);
556                     outputField(PADDED_TWO_DIGITS_INTEGER, channel.getSatellite().getPRN(), next - 4);
557                     outputField(TWO_DIGITS_INTEGER, channel.getK(), next - 1);
558                     outputField("", next, true);
559                 }
560             }
561             finishHeaderLine(RinexLabels.GLONASS_SLOT_FRQ_NB);
562         }
563 
564         if (header.getFormatVersion() >= 3.0) {
565             // GLONASS COD/PHS/BIS
566             if (Double.isNaN(header.getC1cCodePhaseBias())) {
567                 outputField("", 13, true);
568             } else {
569                 outputField(PredefinedObservationType.C1C.getName(), 4, false);
570                 outputField("", 5, true);
571                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1cCodePhaseBias(), 13);
572             }
573             if (Double.isNaN(header.getC1pCodePhaseBias())) {
574                 outputField("", 26, true);
575             } else {
576                 outputField(PredefinedObservationType.C1P.getName(), 17, false);
577                 outputField("", 18, true);
578                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1pCodePhaseBias(), 26);
579             }
580             if (Double.isNaN(header.getC2cCodePhaseBias())) {
581                 outputField("", 39, true);
582             } else {
583                 outputField(PredefinedObservationType.C2C.getName(), 30, false);
584                 outputField("", 31, true);
585                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2cCodePhaseBias(), 39);
586             }
587             if (Double.isNaN(header.getC2pCodePhaseBias())) {
588                 outputField("", 52, true);
589             } else {
590                 outputField(PredefinedObservationType.C2P.getName(), 43, false);
591                 outputField("", 44, true);
592                 outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2pCodePhaseBias(), 52);
593             }
594             finishHeaderLine(RinexLabels.GLONASS_COD_PHS_BIS);
595         }
596 
597         // LEAP SECONDS
598         if (header.getLeapSeconds() > 0) {
599             outputField(SIX_DIGITS_INTEGER, header.getLeapSeconds(), 6);
600             if (header.getFormatVersion() >= 3.0) {
601                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsFuture(),  12);
602                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsWeekNum(), 18);
603                 outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsDayNum(),  24);
604             }
605             finishHeaderLine(RinexLabels.LEAP_SECONDS);
606         }
607 
608         // # OF SATELLITES
609         if (header.getNbSat() >= 0) {
610             outputField(SIX_DIGITS_INTEGER, header.getNbSat(), 6);
611             finishHeaderLine(RinexLabels.NB_OF_SATELLITES);
612         }
613 
614         // PRN / # OF OBS
615         for (final Map.Entry<SatInSystem, Map<ObservationType, Integer>> entry1 : header.getNbObsPerSat().entrySet()) {
616             final SatInSystem sis = entry1.getKey();
617             outputField(sis.toString(), 6, false);
618             // list the entries in the order specified in SYS / # / OBS TYPES
619             for (final ObservationType obsType : header.getTypeObs().get(sis.getSystem())) {
620                 final Integer nbObs = entry1.getValue().get(obsType);
621                 int next = column + 6;
622                 if (next > LABEL_INDEX) {
623                     // we need to set up a continuation line
624                     finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
625                     outputField("", 6, true);
626                     next = column + 6;
627                 }
628                 outputField(SIX_DIGITS_INTEGER, nbObs == null ? 0 : nbObs, next);
629             }
630             finishHeaderLine(RinexLabels.PRN_NB_OF_OBS);
631         }
632 
633         // END OF HEADER
634         writeHeaderLine("", RinexLabels.END);
635 
636     }
637 
638     /** Write one observation data set.
639      * <p>
640      * Note that this writers output only regular observations, so
641      * the event flag is always set to 0
642      * </p>
643      * @param observationDataSet observation data set to write
644      * @exception IOException if an I/O error occurs.
645      */
646     public void writeObservationDataSet(final ObservationDataSet observationDataSet)
647         throws IOException {
648 
649         // check header has already been written
650         if (savedHeader == null) {
651             throw new OrekitException(OrekitMessages.HEADER_NOT_WRITTEN, outputName);
652         }
653 
654         if (!pending.isEmpty() && observationDataSet.durationFrom(pending.get(0).getDate()) > EPS_DATE) {
655             // the specified observation belongs to the next batch
656             // we must process the current batch of pending observations
657             processPending();
658         }
659 
660         // add the observation to the pending list, so it is written later on
661         pending.add(observationDataSet);
662 
663     }
664 
665     /** Process all pending measurements.
666      * @exception IOException if an I/O error occurs.
667      */
668     private void processPending() throws IOException {
669 
670         if (!pending.isEmpty()) {
671 
672             // write the batch of pending observations
673             if (savedHeader.getFormatVersion() < 3.0) {
674                 writePendingRinex2Observations();
675             } else {
676                 writePendingRinex34Observations();
677             }
678 
679             // prepare for next batch
680             pending.clear();
681 
682         }
683 
684     }
685 
686     /** Write one observation data set in RINEX 2 format.
687      * @exception IOException if an I/O error occurs.
688      */
689     public void writePendingRinex2Observations() throws IOException {
690 
691         final ObservationDataSet first = pending.get(0);
692 
693         // EPOCH/SAT
694         final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
695         outputField("",  1, true);
696         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getYear() % 100,    3);
697         outputField("",  4, true);
698         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getMonth(),         6);
699         outputField("",  7, true);
700         outputField(TWO_DIGITS_INTEGER,          dtc.getDate().getDay(),           9);
701         outputField("", 10, true);
702         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getHour(),         12);
703         outputField("", 13, true);
704         outputField(TWO_DIGITS_INTEGER,          dtc.getTime().getMinute(),       15);
705         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(),       26);
706 
707         // event flag
708         outputField("", 28, true);
709         outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 29);
710 
711         // list of satellites and receiver clock offset
712         outputField(THREE_DIGITS_INTEGER, pending.size(), 32);
713         boolean offsetWritten = false;
714         final double  clockOffset   = first.getRcvrClkOffset();
715         for (final ObservationDataSet ods : pending) {
716             int next = column + 3;
717             if (next > 68) {
718                 // we need to set up a continuation line
719                 if (clockOffset != 0.0) {
720                     outputField(TWELVE_NINE_DIGITS_FLOAT, clockOffset, 80);
721                 }
722                 offsetWritten = true;
723                 finishLine();
724                 outputField("", 32, true);
725                 next = column + 3;
726             }
727             outputField(ods.getSatellite().toString(), next, false);
728         }
729         if (!offsetWritten && clockOffset != 0.0) {
730             outputField("", 68, true);
731             outputField(TWELVE_NINE_DIGITS_FLOAT, first.getRcvrClkOffset(), 80);
732         }
733         finishLine();
734 
735         // observations per se
736         for (final ObservationDataSet ods : pending) {
737             for (final ObservationData od : ods.getObservationData()) {
738                 int next = column + 16;
739                 if (next > 80) {
740                     // we need to set up a continuation line
741                     finishLine();
742                     next = column + 16;
743                 }
744                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
745                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
746                 if (od.getLossOfLockIndicator() == 0) {
747                     outputField("", next - 1, true);
748                 } else {
749                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
750                 }
751                 if (od.getSignalStrength() == 0) {
752                     outputField("", next, true);
753                 } else {
754                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
755                 }
756             }
757             finishLine();
758         }
759 
760     }
761 
762     /** Write one observation data set in RINEX 3/4 format.
763      * @exception IOException if an I/O error occurs.
764      */
765     public void writePendingRinex34Observations()
766         throws IOException {
767 
768         final ObservationDataSet first = pending.get(0);
769 
770         // EPOCH/SAT
771         final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
772         outputField(">",  2, true);
773         outputField(FOUR_DIGITS_INTEGER,         dtc.getDate().getYear(),    6);
774         outputField("",   7, true);
775         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getMonth(),   9);
776         outputField("",  10, true);
777         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getDate().getDay(),    12);
778         outputField("", 13, true);
779         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getHour(),   15);
780         outputField("", 16, true);
781         outputField(PADDED_TWO_DIGITS_INTEGER,   dtc.getTime().getMinute(), 18);
782         outputField(ELEVEN_SEVEN_DIGITS_FLOAT,   dtc.getTime().getSecond(), 29);
783 
784         // event flag
785         outputField("", 31, true);
786         outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 32);
787 
788         // number of satellites and receiver clock offset
789         outputField(THREE_DIGITS_INTEGER, pending.size(), 35);
790         if (first.getRcvrClkOffset() != 0.0) {
791             outputField("", 41, true);
792             outputField(FIFTEEN_TWELVE_DIGITS_FLOAT, first.getRcvrClkOffset(), 56);
793         }
794         finishLine();
795 
796         // observations per se
797         for (final ObservationDataSet ods : pending) {
798             outputField(ods.getSatellite().toString(), 3, false);
799             for (final ObservationData od : ods.getObservationData()) {
800                 final int next = column + 16;
801                 final double scaling = getScaling(od.getObservationType(), ods.getSatellite().getSystem());
802                 outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
803                 if (od.getLossOfLockIndicator() == 0) {
804                     outputField("", next - 1, true);
805                 } else {
806                     outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
807                 }
808                 if (od.getSignalStrength() == 0) {
809                     outputField("", next, true);
810                 } else {
811                     outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
812                 }
813             }
814             finishLine();
815         }
816 
817     }
818 
819     /** Write one header string.
820      * @param s string data (may be null)
821      * @param label line label
822      * @throws IOException if an I/O error occurs.
823      */
824     private void writeHeaderLine(final String s, final RinexLabels label) throws IOException {
825         if (s != null) {
826             outputField(s, s.length(), true);
827             finishHeaderLine(label);
828         }
829     }
830 
831     /** Write one header vector.
832      * @param vector vector data (may be null)
833      * @param label line label
834      * @throws IOException if an I/O error occurs.
835      */
836     private void writeHeaderLine(final Vector3D vector, final RinexLabels label) throws IOException {
837         if (vector != null) {
838             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getX(), 14);
839             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getY(), 28);
840             outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getZ(), 42);
841             finishHeaderLine(label);
842         }
843     }
844 
845     /** Finish one header line.
846      * @param label line label
847      * @throws IOException if an I/O error occurs.
848      */
849     private void finishHeaderLine(final RinexLabels label) throws IOException {
850         for (int i = column; i < LABEL_INDEX; ++i) {
851             output.append(' ');
852         }
853         output.append(label.getLabel());
854         finishLine();
855     }
856 
857     /** Finish one line.
858      * @throws IOException if an I/O error occurs.
859      */
860     private void finishLine() throws IOException {
861 
862         // pending line
863         output.append(System.lineSeparator());
864         lineNumber++;
865         column = 0;
866 
867         // emit comments that should be placed at next lines
868         for (final RinexComment comment : savedComments) {
869             if (comment.getLineNumber() == lineNumber) {
870                 outputField(comment.getText(), LABEL_INDEX, true);
871                 output.append(RinexLabels.COMMENT.getLabel());
872                 output.append(System.lineSeparator());
873                 lineNumber++;
874                 column = 0;
875             } else if (comment.getLineNumber() > lineNumber) {
876                 break;
877             }
878         }
879 
880     }
881 
882     /** Output one single character field.
883      * @param c field value
884      * @param next target column for next field
885      * @throws IOException if an I/O error occurs.
886      */
887     private void outputField(final char c, final int next) throws IOException {
888         outputField(Character.toString(c), next, false);
889     }
890 
891     /** Output one integer field.
892      * @param formatter formatter to use
893      * @param value field value
894      * @param next target column for next field
895      * @throws IOException if an I/O error occurs.
896      */
897     private void outputField(final FastLongFormatter formatter, final int value, final int next) throws IOException {
898         outputField(formatter.toString(value), next, false);
899     }
900 
901     /** Output one double field.
902      * @param formatter formatter to use
903      * @param value field value
904      * @param next target column for next field
905      * @throws IOException if an I/O error occurs.
906      */
907     private void outputField(final FastDoubleFormatter formatter, final double value, final int next) throws IOException {
908         if (Double.isNaN(value)) {
909             // NaN values are replaced by blank fields
910             outputField("", next, true);
911         } else {
912             outputField(formatter.toString(value), next, false);
913         }
914     }
915 
916     /** Output one field.
917      * @param field field to output
918      * @param next target column for next field
919      * @param leftJustified if true, field is left-justified
920      * @throws IOException if an I/O error occurs.
921      */
922     private void outputField(final String field, final int next, final boolean leftJustified) throws IOException {
923         final int padding = next - (field == null ? 0 : field.length()) - column;
924         if (padding < 0) {
925             throw new OrekitException(OrekitMessages.FIELD_TOO_LONG, field, next - column);
926         }
927         if (leftJustified && field != null) {
928             output.append(field);
929         }
930         for (int i = 0; i < padding; ++i) {
931             output.append(' ');
932         }
933         if (!leftJustified && field != null) {
934             output.append(field);
935         }
936         column = next;
937     }
938 
939     /** Get the scaling factor for an observation.
940      * @param type type of observation
941      * @param system satellite system for the observation
942      * @return scaling factor
943      */
944     private double getScaling(final ObservationType type, final SatelliteSystem system) {
945 
946         for (final ScaleFactorCorrection scaleFactorCorrection : savedHeader.getScaleFactorCorrections(system)) {
947             // check if the next Observation Type to read needs to be scaled
948             if (scaleFactorCorrection.getTypesObsScaled().contains(type)) {
949                 return scaleFactorCorrection.getCorrection();
950             }
951         }
952 
953         // no scaling
954         return 1.0;
955 
956     }
957 
958 }