1   /* Copyright 2022-2026 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.utils;
18  
19  import org.hipparchus.util.FastMath;
20  import org.orekit.errors.OrekitException;
21  import org.orekit.errors.OrekitMessages;
22  import org.orekit.files.rinex.section.CommonLabel;
23  import org.orekit.files.rinex.section.Label;
24  import org.orekit.files.rinex.section.RinexBaseHeader;
25  import org.orekit.files.rinex.section.RinexComment;
26  import org.orekit.time.DateTimeComponents;
27  import org.orekit.utils.formatting.FastDecimalFormatter;
28  import org.orekit.utils.formatting.FastDoubleFormatter;
29  import org.orekit.utils.formatting.FastLongFormatter;
30  import org.orekit.utils.formatting.FastScientificFormatter;
31  
32  import java.io.IOException;
33  import java.util.Collections;
34  import java.util.List;
35  
36  /** Base write for Rinex files.
37   * @param <T> type of the header
38   * @author Luc Maisonobe
39   * @since 14.0
40   */
41  public abstract class BaseRinexWriter<T extends RinexBaseHeader> implements AutoCloseable {
42  
43      /** Format for one 2 digits integer field. */
44      public static final FastLongFormatter TWO_DIGITS_INTEGER = new FastLongFormatter(2, false);
45  
46      /** Format for one 2 digits integer field. */
47      public static final FastLongFormatter PADDED_TWO_DIGITS_INTEGER = new FastLongFormatter(2, true);
48  
49      /** Format for one 3 digits integer field. */
50      public static final FastLongFormatter THREE_DIGITS_INTEGER = new FastLongFormatter(3, false);
51  
52      /** Format for one 4 digits integer field. */
53      public static final FastLongFormatter FOUR_DIGITS_INTEGER = new FastLongFormatter(4, false);
54  
55      /** Format for one 4 digits integer field. */
56      public static final FastLongFormatter PADDED_FOUR_DIGITS_INTEGER = new FastLongFormatter(4, true);
57  
58      /** Format for one 6 digits integer field. */
59      public static final FastLongFormatter SIX_DIGITS_INTEGER = new FastLongFormatter(6, false);
60  
61      /** Format for one 9.2 digits float field. */
62      public static final FastDoubleFormatter NINE_TWO_DIGITS_FLOAT = new FastDecimalFormatter(9, 2);
63  
64      /** Format for one 19 characters wide field in scientific notation. */
65      public static final FastDoubleFormatter NINETEEN_SCIENTIFIC_FLOAT = new FastScientificFormatter(19);
66  
67      /** Destination of generated output. */
68      private final Appendable output;
69  
70      /** Output name for error messages. */
71      private final String outputName;
72  
73      /** Indicator of closed output. */
74      private boolean closed;
75  
76      /** Saved header. */
77      private T savedHeader;
78  
79      /** Index of the labels in header lines. */
80      private int savedLabelIndex;
81  
82      /** Saved comments. */
83      private List<RinexComment> savedComments;
84  
85      /** Line number. */
86      private int lineNumber;
87  
88      /** Column number. */
89      private int column;
90  
91      /** Simple constructor.
92       * @param output destination of generated output
93       * @param outputName output name for error messages
94       */
95      protected BaseRinexWriter(final Appendable output, final String outputName) {
96          this.output        = output;
97          this.outputName    = outputName;
98          this.savedComments = Collections.emptyList();
99          this.closed        = false;
100     }
101 
102     /** {@inheritDoc} */
103     @Override
104     public void close() throws IOException {
105         try {
106             if (!closed && output instanceof AutoCloseable closeable) {
107                 closeable.close();
108             }
109             closed = true;
110         // CHECKSTYLE: stop IllegalCatch check
111         } catch (Exception ex) {
112         // CHECKSTYLE: resume IllegalCatch check
113             throw new IOException(ex);
114         }
115     }
116 
117     /** Prepare comments to be emitted at specified lines.
118      * @param comments comments to be emitted
119      */
120     public void prepareComments(final List<RinexComment> comments) {
121         savedComments = comments;
122     }
123 
124     /** Write header.
125      * @param header     header to write
126      * @param labelIndex index of the label in header
127      * @exception IOException if an I/O error occurs.
128      */
129     protected void writeHeader(final T header, final int labelIndex) throws IOException {
130 
131         // check header is written exactly once
132         if (savedHeader != null) {
133             throw new OrekitException(OrekitMessages.HEADER_ALREADY_WRITTEN, outputName);
134         }
135         savedHeader     = header;
136         savedLabelIndex = labelIndex;
137         lineNumber      = 1;
138 
139     }
140 
141     /** Get the header.
142      * @return header
143      */
144     protected T getHeader() {
145         return savedHeader;
146     }
147 
148     /** Get column number.
149      * @return column number
150      */
151     protected int getColumn() {
152         return column;
153     }
154 
155     /** Finish one header line.
156      * @param label line label
157      * @throws IOException if an I/O error occurs.
158      */
159     protected void finishHeaderLine(final Label label) throws IOException {
160         checkOutputNotClosed();
161         for (int i = column; i < savedLabelIndex; ++i) {
162             output.append(' ');
163         }
164         output.append(label.getLabel());
165         finishLine();
166     }
167 
168     /** Finish one line.
169      * @throws IOException if an I/O error occurs.
170      */
171     public void finishLine() throws IOException {
172 
173         checkOutputNotClosed();
174 
175         // pending line
176         output.append(System.lineSeparator());
177         lineNumber++;
178         column = 0;
179 
180         // emit comments that should be placed at next lines
181         for (final RinexComment comment : savedComments) {
182             if (comment.getLineNumber() == lineNumber) {
183                 outputField(comment.getText(), savedLabelIndex, true);
184                 output.append(CommonLabel.COMMENT.getLabel());
185                 output.append(System.lineSeparator());
186                 lineNumber++;
187                 column = 0;
188             } else if (comment.getLineNumber() > lineNumber) {
189                 break;
190             }
191         }
192 
193     }
194 
195     /** Write one header string.
196      * @param s string data (may be null)
197      * @param label line label
198      * @throws IOException if an I/O error occurs.
199      */
200     protected void writeHeaderLine(final String s, final Label label) throws IOException {
201         if (s != null) {
202             outputField(s, s.length(), true);
203             finishHeaderLine(label);
204         }
205     }
206 
207     /** Check header has been written.
208      */
209     protected void checkHeaderWritten() {
210         if (savedHeader == null) {
211             throw new OrekitException(OrekitMessages.HEADER_NOT_WRITTEN, outputName);
212         }
213     }
214 
215     /** Check if column exceeds header line length.
216      * @param tentative tentative column number
217      * @return true if tentative column number exceeds header line length
218      */
219     protected boolean exceedsHeaderLength(final int tentative) {
220         return tentative > savedLabelIndex;
221     }
222 
223     /** Write the PGM / RUN BY / DATE header line.
224      * @param header header to write
225      * @throws IOException if an I/O error occurs.
226      */
227     protected void writeProgramRunByDate(final RinexBaseHeader header)
228         throws IOException {
229         outputField(header.getProgramName(), 20, true);
230         outputField(header.getRunByName(),   40, true);
231         final DateTimeComponents dtc = header.getCreationDateComponents();
232         if (header.getFormatVersion() < 3.0 && dtc.getTime().getSecond() < 0.5) {
233             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 42);
234             outputField('-', 43);
235             outputField(dtc.getDate().getMonthEnum().getUpperCaseAbbreviation(), 46,  true);
236             outputField('-', 47);
237             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 49);
238             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 52);
239             outputField(':', 53);
240             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 55);
241             outputField(header.getCreationTimeZone(), 58, true);
242         } else {
243             outputField(PADDED_FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 44);
244             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 46);
245             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 48);
246             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 51);
247             outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 53);
248             outputField(PADDED_TWO_DIGITS_INTEGER, (int) FastMath.rint(dtc.getTime().getSecond()), 55);
249             outputField(header.getCreationTimeZone(), 59, false);
250         }
251         finishHeaderLine(CommonLabel.PROGRAM);
252     }
253 
254     /** Output one single character field.
255      * @param c field value
256      * @param next target column for next field
257      * @throws IOException if an I/O error occurs.
258      */
259     public void outputField(final char c, final int next) throws IOException {
260         outputField(Character.toString(c), next, false);
261     }
262 
263     /** Output one integer field.
264      * @param formatter formatter to use
265      * @param value field value
266      * @param next target column for next field
267      * @throws IOException if an I/O error occurs.
268      */
269     public void outputField(final FastLongFormatter formatter, final int value, final int next) throws IOException {
270         outputField(formatter.toString(value), next, false);
271     }
272 
273     /** Output one long integer field.
274      * @param formatter formatter to use
275      * @param value field value
276      * @param next target column for next field
277      * @throws IOException if an I/O error occurs.
278      */
279     public void outputField(final FastLongFormatter formatter, final long value, final int next) throws IOException {
280         outputField(formatter.toString(value), next, false);
281     }
282 
283     /** Output one double field.
284      * @param formatter formatter to use
285      * @param value field value
286      * @param next target column for next field
287      * @throws IOException if an I/O error occurs.
288      */
289     public void outputField(final FastDoubleFormatter formatter, final double value, final int next) throws IOException {
290         if (Double.isNaN(value)) {
291             // NaN values are replaced by blank fields
292             outputField("", next, true);
293         } else {
294             outputField(formatter.toString(value), next, false);
295         }
296     }
297 
298     /** Output one field.
299      * @param field field to output
300      * @param next target column for next field
301      * @param leftJustified if true, field is left-justified
302      * @throws IOException if an I/O error occurs.
303      */
304     public void outputField(final String field, final int next, final boolean leftJustified) throws IOException {
305         final int padding = next - (field == null ? 0 : field.length()) - column;
306         if (padding < 0) {
307             throw new OrekitException(OrekitMessages.FIELD_TOO_LONG, field, next - column);
308         }
309         checkOutputNotClosed();
310         if (leftJustified && field != null) {
311             output.append(field);
312         }
313         for (int i = 0; i < padding; ++i) {
314             output.append(' ');
315         }
316         if (!leftJustified && field != null) {
317             output.append(field);
318         }
319         column = next;
320     }
321 
322     /** Check that output has not been closed.
323      */
324     private void checkOutputNotClosed() {
325         if (closed) {
326             throw new OrekitException(OrekitMessages.OUTPUT_ALREADY_CLOSED, outputName);
327         }
328     }
329 
330 }