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