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