1   /* Copyright 2002-2024 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          *                 boolean)}.
268          */
269         private Segment(final Frame frame) {
270             this.frame = frame;
271         }
272 
273         /** {@inheritDoc}. */
274         @Override
275         public void handleStep(final SpacecraftState currentState) {
276             try {
277 
278                 // Write ephemeris line
279                 writeEphemerisLine(currentState.getPVCoordinates(frame));
280 
281             } catch (IOException e) {
282                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
283                                           e.getLocalizedMessage());
284             }
285 
286         }
287 
288         /** {@inheritDoc}. */
289         @Override
290         public void finish(final SpacecraftState finalState) {
291             try {
292                 // Write ephemeris line
293                 writeEphemerisLine(finalState.getPVCoordinates(frame));
294 
295                 // Write end of file
296                 writeEndOfFile();
297 
298             } catch (IOException e) {
299                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
300                                           e.getLocalizedMessage());
301             }
302 
303         }
304 
305         /**
306          * Write ephemeris lines.
307          * <p>
308          * If <code>velocityFlag</code> is equals to true, both
309          * position and velocity records are written. Otherwise,
310          * only the position data are used.
311          * </p>
312          * @param pv the time, position, and velocity to write.
313          * @throws IOException if the output stream throws one while writing.
314          */
315         public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
316             throws IOException {
317 
318             // Record type and direction flag
319             writeValue(writer, A2, "10",                                    true);
320             writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);
321 
322             // Epoch
323             final AbsoluteDate epoch = pv.getDate();
324             final DateTimeComponents dtc = epoch.getComponents(timeScale);
325             writeValue(writer, I5, dtc.getDate().getMJD(),                  true);
326             writeValue(writer, F13_6, dtc.getTime().getSecondsInLocalDay(), true);
327 
328             // Leap second flag (default 0)
329             writeValue(writer, I2, 0, true);
330 
331             // Position
332             final Vector3D position = pv.getPosition();
333             writeValue(writer, F17_3, position.getX(), true);
334             writeValue(writer, F17_3, position.getY(), true);
335             writeValue(writer, F17_3, position.getZ(), false);
336 
337             // New line
338             writer.append(NEW_LINE);
339 
340             // Write the velocity record
341             if (velocityFlag) {
342 
343                 // Record type and direction flag
344                 writeValue(writer, A2, "20",                                    true);
345                 writeValue(writer, I1, DEFAULT_DIRECTION_FLAG,                  true);
346 
347                 // Velocity
348                 final Vector3D velocity = pv.getVelocity();
349                 writeValue(writer, F19_6, velocity.getX(), true);
350                 writeValue(writer, F19_6, velocity.getY(), true);
351                 writeValue(writer, F19_6, velocity.getZ(), false);
352 
353                 // New line
354                 writer.append(NEW_LINE);
355 
356             }
357 
358         }
359 
360     }
361 
362     /** Writer for specific header lines. */
363     public enum HeaderLineWriter {
364 
365         /** Header first line. */
366         H1("H1") {
367 
368             /** {@inheritDoc} */
369             @Override
370             public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
371                 throws IOException {
372 
373                 // write first keys
374                 writeValue(cpfWriter, A2, getIdentifier(),                           true);
375                 writeValue(cpfWriter, A3, FORMAT,                                    true);
376                 writeValue(cpfWriter, I2, cpfHeader.getVersion(),                    true);
377                 writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
378                 writeValue(cpfWriter, A3, cpfHeader.getSource(),                     true);
379                 writeValue(cpfWriter, I4, cpfHeader.getProductionEpoch().getYear(),  true);
380                 writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getMonth(), true);
381                 writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getDay(),   true);
382                 writeValue(cpfWriter, I2, cpfHeader.getProductionHour(),             true);
383                 writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
384                 writeValue(cpfWriter, I3, cpfHeader.getSequenceNumber(),             true);
385 
386                 // check file version
387                 if (cpfHeader.getVersion() == 2) {
388                     writeValue(cpfWriter, I2, cpfHeader.getSubDailySequenceNumber(), true);
389                 }
390 
391                 // write target name from official list
392                 writeValue(cpfWriter, A10, cpfHeader.getName(),                      true);
393 
394                 // write notes (not supported yet)
395                 writeValue(cpfWriter, A10, SPACE,                                    false);
396             }
397 
398         },
399 
400         /** Header second line. */
401         H2("H2") {
402 
403             /** {@inheritDoc} */
404             @Override
405             public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
406                 throws IOException {
407 
408                 // write identifiers
409                 writeValue(cpfWriter, A2, getIdentifier(),                                 true);
410                 writeValue(cpfWriter, A8, cpfHeader.getIlrsSatelliteId(),                  true);
411                 writeValue(cpfWriter, A4, cpfHeader.getSic(),                              true);
412                 writeValue(cpfWriter, A8, cpfHeader.getNoradId(),                          true);
413 
414                 // write starting epoch
415                 final AbsoluteDate starting = cpfHeader.getStartEpoch();
416                 final DateTimeComponents dtcStart = starting.getComponents(timescale);
417                 writeValue(cpfWriter, I4, dtcStart.getDate().getYear(),                    true);
418                 writeValue(cpfWriter, I2, dtcStart.getDate().getMonth(),                   true);
419                 writeValue(cpfWriter, I2, dtcStart.getDate().getDay(),                     true);
420                 writeValue(cpfWriter, I2, dtcStart.getTime().getHour(),                    true);
421                 writeValue(cpfWriter, I2, dtcStart.getTime().getMinute(),                  true);
422                 writeValue(cpfWriter, I2, (int) dtcStart.getTime().getSecond(),            true);
423 
424                 // write ending epoch
425                 final AbsoluteDate ending = cpfHeader.getEndEpoch();
426                 final DateTimeComponents dtcEnd = ending.getComponents(timescale);
427                 writeValue(cpfWriter, I4, dtcEnd.getDate().getYear(),                      true);
428                 writeValue(cpfWriter, I2, dtcEnd.getDate().getMonth(),                     true);
429                 writeValue(cpfWriter, I2, dtcEnd.getDate().getDay(),                       true);
430                 writeValue(cpfWriter, I2, dtcEnd.getTime().getHour(),                      true);
431                 writeValue(cpfWriter, I2, dtcEnd.getTime().getMinute(),                    true);
432                 writeValue(cpfWriter, I2, (int)  dtcEnd.getTime().getSecond(),             true);
433 
434                 // write last keys
435                 writeValue(cpfWriter, I5, cpfHeader.getStep(),                             true);
436                 writeValue(cpfWriter, I1, cpfHeader.isCompatibleWithTIVs(),                true);
437                 writeValue(cpfWriter, I1, cpfHeader.getTargetClass(),                      true);
438                 writeValue(cpfWriter, I2, cpfHeader.getRefFrameId(),                       true);
439                 writeValue(cpfWriter, I1, cpfHeader.getRotationalAngleType(),              true);
440                 if (cpfHeader.getVersion() == 1) {
441                     writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), false);
442                 } else {
443                     writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), true);
444                     writeValue(cpfWriter, I2, cpfHeader.getTargetLocation(),               false);
445                 }
446 
447             }
448 
449         };
450 
451         /** Identifier. */
452         private final String identifier;
453 
454         /** Simple constructor.
455          * @param identifier regular expression for identifying line (i.e. first element)
456          */
457         HeaderLineWriter(final String identifier) {
458             this.identifier = identifier;
459         }
460 
461         /** Write a line.
462          * @param cpfHeader container for header data
463          * @param cpfWriter writer
464          * @param timescale time scale for dates
465          * @throws IOException
466          *             if any buffer writing operations fail or if the underlying
467          *             format doesn't support a configuration in the file
468          */
469         public abstract void write(CPFHeader cpfHeader, Appendable cpfWriter, TimeScale timescale)  throws IOException;
470 
471         /**
472          * Get the regular expression for identifying line.
473          * @return the regular expression for identifying line
474          */
475         public String getIdentifier() {
476             return identifier;
477         }
478 
479     }
480 
481 }