1   /* Copyright 2002-2025 CS GROUP
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.ilrs;
18  
19  import java.io.IOException;
20  import java.util.Locale;
21  
22  import org.hipparchus.exception.LocalizedCoreFormats;
23  import org.hipparchus.geometry.euclidean.threed.Vector3D;
24  import org.orekit.errors.OrekitException;
25  import org.orekit.frames.Frame;
26  import org.orekit.propagation.Propagator;
27  import org.orekit.propagation.SpacecraftState;
28  import org.orekit.propagation.sampling.OrekitFixedStepHandler;
29  import org.orekit.time.AbsoluteDate;
30  import org.orekit.time.DateTimeComponents;
31  import org.orekit.time.TimeScale;
32  import org.orekit.utils.TimeStampedPVCoordinates;
33  
34  /**
35   * A writer for CPF files.
36   *
37   * <p> Each instance corresponds to a single CPF file.
38   *
39   * <p> This class can be used as a step handler for a {@link Propagator}.
40   * The following example shows its use as a step handler.
41   *
42   * <p>
43   * <b>Note:</b> By default, only required header keys are wrote (H1 and H2). Furthermore, only position data can be written.
44   * Other keys (optionals) are simply ignored.
45   * Contributions are welcome to support more fields in the format.
46   *
47   * @author Bryan Cazabonne
48   * @since 10.3
49   */
50  public class StreamingCpfWriter {
51  
52      /** New line separator for output file. */
53      private static final String NEW_LINE = "\n";
54  
55      /** String A2 Format. */
56      private static final String A1 = "%1s";
57  
58      /** String A2 Format. */
59      private static final String A2 = "%2s";
60  
61      /** String A3 Format. */
62      private static final String A3 = "%3s";
63  
64      /** String A4 Format. */
65      private static final String A4 = "%4s";
66  
67      /** String A8 Format. */
68      private static final String A8 = "%8s";
69  
70      /** String A10 Format. */
71      private static final String A10 = "%10s";
72  
73      /** Integer I1 Format. */
74      private static final String I1 = "%1d";
75  
76      /** Integer I2 Format. */
77      private static final String I2 = "%2d";
78  
79      /** Integer I3 Format. */
80      private static final String I3 = "%3d";
81  
82      /** Integer I4 Format. */
83      private static final String I4 = "%4d";
84  
85      /** Integer I5 Format. */
86      private static final String I5 = "%5d";
87  
88      /** Real 13.6 Format. */
89      private static final String F13_6 = "%13.6f";
90  
91      /** Real 17.3 Format. */
92      private static final String F17_3 = "%17.3f";
93  
94      /** Real 19.6 Format. */
95      private static final String F19_6 = "%19.6f";
96  
97      /** Space. */
98      private static final String SPACE = " ";
99  
100     /** Empty string. */
101     private static final String EMPTY_STRING = "";
102 
103     /** File format. */
104     private static final String FORMAT = "CPF";
105 
106     /** Default locale. */
107     private static final Locale STANDARDIZED_LOCALE = Locale.US;
108 
109     /** Default value for direction flag in position record. */
110     private static final int DEFAULT_DIRECTION_FLAG = 0;
111 
112     /** Output stream. */
113     private final Appendable writer;
114 
115     /** Time scale for all dates. */
116     private final TimeScale timeScale;
117 
118     /** Container for header data. */
119     private final CPFHeader header;
120 
121     /** Flag for optional velocity record. */
122     private final boolean velocityFlag;
123 
124     /**
125      * Create a CPF writer than streams data to the given output stream.
126      * <p>
127      * Using this constructor, velocity data are not written.
128      * </p>
129      * @param writer     the output stream for the CPF file.
130      * @param timeScale  for all times in the CPF
131      * @param header     container for header data
132      * @see #StreamingCpfWriter(Appendable, TimeScale, CPFHeader, boolean)
133      */
134     public StreamingCpfWriter(final Appendable writer,
135                               final TimeScale timeScale,
136                               final CPFHeader header) {
137         this(writer, timeScale, header, false);
138     }
139 
140     /**
141      * Create a CPF writer than streams data to the given output stream.
142      *
143      * @param writer       the output stream for the CPF file.
144      * @param timeScale    for all times in the CPF
145      * @param header       container for header data
146      * @param velocityFlag true if velocity must be written
147      * @since 11.2
148      */
149     public StreamingCpfWriter(final Appendable writer,
150                               final TimeScale timeScale,
151                               final CPFHeader header,
152                               final boolean velocityFlag) {
153         this.writer       = writer;
154         this.timeScale    = timeScale;
155         this.header       = header;
156         this.velocityFlag = velocityFlag;
157     }
158 
159     /**
160      * Writes the CPF header for the file.
161      * @throws IOException if the stream cannot write to stream
162      */
163     public void writeHeader() throws IOException {
164 
165         // Write H1
166         HeaderLineWriter.H1.write(header, writer, timeScale);
167         writer.append(NEW_LINE);
168 
169         // Write H2
170         HeaderLineWriter.H2.write(header, writer, timeScale);
171         writer.append(NEW_LINE);
172 
173         // End of header
174         writer.append("H9");
175         writer.append(NEW_LINE);
176 
177     }
178 
179     /**
180      * Write end of file.
181      * @throws IOException if the stream cannot write to stream
182      */
183     public void writeEndOfFile() throws IOException {
184         writer.append("99");
185     }
186 
187     /**
188      * Create a writer for a new CPF ephemeris segment.
189      * <p>
190      * The returned writer can only write a single ephemeris segment in a CPF.
191      * </p>
192      * @param frame the reference frame to use for the segment.
193      * @return a new CPF segment, ready for writing.
194      */
195     public Segment newSegment(final Frame frame) {
196         return new Segment(frame);
197     }
198 
199     /**
200      * Write a String value in the file.
201      * @param cpfWriter writer
202      * @param format format
203      * @param value value
204      * @param withSpace true if a space must be added
205      * @throws IOException if value cannot be written
206      */
207     private static void writeValue(final Appendable cpfWriter, final String format,
208                                    final String value, final boolean withSpace)
209         throws IOException {
210         cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
211     }
212 
213     /**
214      * Write a integer value in the file.
215      * @param cpfWriter writer
216      * @param format format
217      * @param value value
218      * @param withSpace true if a space must be added
219      * @throws IOException if value cannot be written
220      */
221     private static void writeValue(final Appendable cpfWriter, final String format,
222                                    final int value, final boolean withSpace)
223         throws IOException {
224         cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
225     }
226 
227     /**
228      * Write a real value in the file.
229      * @param cpfWriter writer
230      * @param format format
231      * @param value value
232      * @param withSpace true if a space must be added
233      * @throws IOException if value cannot be written
234      */
235     private static void writeValue(final Appendable cpfWriter, final String format,
236                                    final double value, final boolean withSpace)
237         throws IOException {
238         cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
239     }
240 
241     /**
242      * Write a String value in the file.
243      * @param cpfWriter writer
244      * @param format format
245      * @param value value
246      * @param withSpace true if a space must be added
247      * @throws IOException if value cannot be written
248      */
249     private static void writeValue(final Appendable cpfWriter, final String format,
250                                    final boolean value, final boolean withSpace)
251         throws IOException {
252         // Change to an integer value
253         final int intValue = value ? 1 : 0;
254         writeValue(cpfWriter, format, intValue, withSpace);
255     }
256 
257     /** A writer for a segment of a CPF. */
258     public class Segment implements OrekitFixedStepHandler {
259 
260         /** Reference frame of the output states. */
261         private final Frame frame;
262 
263         /**
264          * Create a new segment writer.
265          *
266          * @param frame    for the output states. Used by {@link #handleStep(SpacecraftState)}.
267          */
268         private Segment(final Frame frame) {
269             this.frame = frame;
270         }
271 
272         /** {@inheritDoc}. */
273         @Override
274         public void handleStep(final SpacecraftState currentState) {
275             try {
276 
277                 // Write ephemeris line
278                 writeEphemerisLine(currentState.getPVCoordinates(frame));
279 
280             } catch (IOException e) {
281                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
282                                           e.getLocalizedMessage());
283             }
284 
285         }
286 
287         /** {@inheritDoc}. */
288         @Override
289         public void finish(final SpacecraftState finalState) {
290             try {
291                 // Write ephemeris line
292                 writeEphemerisLine(finalState.getPVCoordinates(frame));
293 
294                 // Write end of file
295                 writeEndOfFile();
296 
297             } catch (IOException e) {
298                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
299                                           e.getLocalizedMessage());
300             }
301 
302         }
303 
304         /**
305          * Write ephemeris lines.
306          * <p>
307          * If <code>velocityFlag</code> is equals to true, both
308          * position and velocity records are written. Otherwise,
309          * only the position data are used.
310          * </p>
311          * @param pv the time, position, and velocity to write.
312          * @throws IOException if the output stream throws one while writing.
313          */
314         public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
315             throws IOException {
316 
317             // Record type and direction flag
318             writeValue(writer, A2, "10",                                    true);
319             writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);
320 
321             // Epoch
322             final AbsoluteDate epoch = pv.getDate();
323             final DateTimeComponents dtc = epoch.getComponents(timeScale).roundIfNeeded(60, 6);
324             writeValue(writer, I5, dtc.getDate().getMJD(),                  true);
325             writeValue(writer, F13_6, dtc.getTime().getSecondsInLocalDay(), true);
326 
327             // Leap second flag (default 0)
328             writeValue(writer, I2, 0, true);
329 
330             // Position
331             final Vector3D position = pv.getPosition();
332             writeValue(writer, F17_3, position.getX(), true);
333             writeValue(writer, F17_3, position.getY(), true);
334             writeValue(writer, F17_3, position.getZ(), false);
335 
336             // New line
337             writer.append(NEW_LINE);
338 
339             // Write the velocity record
340             if (velocityFlag) {
341 
342                 // Record type and direction flag
343                 writeValue(writer, A2, "20",                                    true);
344                 writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);
345 
346                 // Velocity
347                 final Vector3D velocity = pv.getVelocity();
348                 writeValue(writer, F19_6, velocity.getX(), true);
349                 writeValue(writer, F19_6, velocity.getY(), true);
350                 writeValue(writer, F19_6, velocity.getZ(), false);
351 
352                 // New line
353                 writer.append(NEW_LINE);
354 
355             }
356 
357         }
358 
359     }
360 
361     /** Writer for specific header lines. */
362     public enum HeaderLineWriter {
363 
364         /** Header first line. */
365         H1("H1") {
366 
367             /** {@inheritDoc} */
368             @Override
369             public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
370                 throws IOException {
371 
372                 // write first keys
373                 writeValue(cpfWriter, A2, getIdentifier(),                           true);
374                 writeValue(cpfWriter, A3, FORMAT,                                    true);
375                 writeValue(cpfWriter, I2, cpfHeader.getVersion(),                    true);
376                 writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
377                 writeValue(cpfWriter, A3, cpfHeader.getSource(),                     true);
378                 writeValue(cpfWriter, I4, cpfHeader.getProductionEpoch().getYear(),  true);
379                 writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getMonth(), true);
380                 writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getDay(),   true);
381                 writeValue(cpfWriter, I2, cpfHeader.getProductionHour(),             true);
382                 writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
383                 writeValue(cpfWriter, I3, cpfHeader.getSequenceNumber(),             true);
384 
385                 // check file version
386                 if (cpfHeader.getVersion() == 2) {
387                     writeValue(cpfWriter, I2, cpfHeader.getSubDailySequenceNumber(), true);
388                 }
389 
390                 // write target name from official list
391                 writeValue(cpfWriter, A10, cpfHeader.getName(),                      true);
392 
393                 // write notes (not supported yet)
394                 writeValue(cpfWriter, A10, SPACE,                                    false);
395             }
396 
397         },
398 
399         /** Header second line. */
400         H2("H2") {
401 
402             /** {@inheritDoc} */
403             @Override
404             public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
405                 throws IOException {
406 
407                 // write identifiers
408                 writeValue(cpfWriter, A2, getIdentifier(),                                 true);
409                 writeValue(cpfWriter, A8, cpfHeader.getIlrsSatelliteId(),                  true);
410                 writeValue(cpfWriter, A4, cpfHeader.getSic(),                              true);
411                 writeValue(cpfWriter, A8, cpfHeader.getNoradId(),                          true);
412 
413                 // write starting epoch
414                 final AbsoluteDate starting = cpfHeader.getStartEpoch();
415                 final DateTimeComponents dtcStart = starting.getComponents(timescale);
416                 writeValue(cpfWriter, I4, dtcStart.getDate().getYear(),                    true);
417                 writeValue(cpfWriter, I2, dtcStart.getDate().getMonth(),                   true);
418                 writeValue(cpfWriter, I2, dtcStart.getDate().getDay(),                     true);
419                 writeValue(cpfWriter, I2, dtcStart.getTime().getHour(),                    true);
420                 writeValue(cpfWriter, I2, dtcStart.getTime().getMinute(),                  true);
421                 writeValue(cpfWriter, I2, (int) dtcStart.getTime().getSecond(),            true);
422 
423                 // write ending epoch
424                 final AbsoluteDate ending = cpfHeader.getEndEpoch();
425                 final DateTimeComponents dtcEnd = ending.getComponents(timescale);
426                 writeValue(cpfWriter, I4, dtcEnd.getDate().getYear(),                      true);
427                 writeValue(cpfWriter, I2, dtcEnd.getDate().getMonth(),                     true);
428                 writeValue(cpfWriter, I2, dtcEnd.getDate().getDay(),                       true);
429                 writeValue(cpfWriter, I2, dtcEnd.getTime().getHour(),                      true);
430                 writeValue(cpfWriter, I2, dtcEnd.getTime().getMinute(),                    true);
431                 writeValue(cpfWriter, I2, (int)  dtcEnd.getTime().getSecond(),             true);
432 
433                 // write last keys
434                 writeValue(cpfWriter, I5, cpfHeader.getStep(),                             true);
435                 writeValue(cpfWriter, I1, cpfHeader.isCompatibleWithTIVs(),                true);
436                 writeValue(cpfWriter, I1, cpfHeader.getTargetClass(),                      true);
437                 writeValue(cpfWriter, I2, cpfHeader.getRefFrameId(),                       true);
438                 writeValue(cpfWriter, I1, cpfHeader.getRotationalAngleType(),              true);
439                 if (cpfHeader.getVersion() == 1) {
440                     writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), false);
441                 } else {
442                     writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), true);
443                     writeValue(cpfWriter, I2, cpfHeader.getTargetLocation(),               false);
444                 }
445 
446             }
447 
448         };
449 
450         /** Identifier. */
451         private final String identifier;
452 
453         /** Simple constructor.
454          * @param identifier regular expression for identifying line (i.e. first element)
455          */
456         HeaderLineWriter(final String identifier) {
457             this.identifier = identifier;
458         }
459 
460         /** Write a line.
461          * @param cpfHeader container for header data
462          * @param cpfWriter writer
463          * @param timescale time scale for dates
464          * @throws IOException
465          *             if any buffer writing operations fail or if the underlying
466          *             format doesn't support a configuration in the file
467          */
468         public abstract void write(CPFHeader cpfHeader, Appendable cpfWriter, TimeScale timescale)  throws IOException;
469 
470         /**
471          * Get the regular expression for identifying line.
472          * @return the regular expression for identifying line
473          */
474         public String getIdentifier() {
475             return identifier;
476         }
477 
478     }
479 
480 }