1   /* Copyright 2002-2026 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.ccsds.ndm.adm.aem;
18  
19  import java.io.IOException;
20  
21  import org.hipparchus.exception.LocalizedCoreFormats;
22  import org.orekit.errors.OrekitException;
23  import org.orekit.errors.OrekitMessages;
24  import org.orekit.files.ccsds.definitions.FrameFacade;
25  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
26  import org.orekit.files.ccsds.section.Header;
27  import org.orekit.files.ccsds.utils.generation.Generator;
28  import org.orekit.propagation.Propagator;
29  import org.orekit.propagation.SpacecraftState;
30  import org.orekit.propagation.sampling.OrekitFixedStepHandler;
31  import org.orekit.time.AbsoluteDate;
32  
33  /**
34   * A writer for AEM files.
35   *
36   * <p> Each instance corresponds to a single AEM file.
37   *
38   * <p> This class can be used as a step handler for a {@link Propagator}.
39   *
40   * <pre>{@code
41   * Propagator propagator = ...; // pre-configured propagator
42   * AEMWriter  aemWriter  = ...; // pre-configured writer
43   *   try (Generator out = ...;  // set-up output stream
44   *        StreamingAemWriter sw = new StreamingAemWriter(out, aemWriter)) { // set-up streaming writer
45   *
46   *     // write segment 1
47   *     propagator.getMultiplexer().add(step, sw.newSegment());
48   *     propagator.propagate(startDate1, stopDate1);
49   *
50   *     ...
51   *
52   *     // write segment n
53   *     propagator.getMultiplexer().clear();
54   *     propagator.getMultiplexer().add(step, sw.newSegment());
55   *     propagator.propagate(startDateN, stopDateN);
56   *
57   *   }
58   * }</pre>
59   * @author Bryan Cazabonne
60   * @author Luc Maisonobe
61   * @see <a href="https://public.ccsds.org/Pubs/504x0b1c1.pdf">CCSDS 504.0-B-1 Attitude Data Messages</a>
62   * @see AemWriter
63   * @since 10.2
64   */
65  public class StreamingAemWriter implements AutoCloseable {
66  
67      /** Generator for AEM output. */
68      private final Generator generator;
69  
70      /** Writer for the AEM message format. */
71      private final AemWriter writer;
72  
73      /** Header. */
74      private final AdmHeader header;
75  
76      /** Current metadata. */
77      private final AemMetadata metadata;
78  
79      /** Indicator for writing header. */
80      private boolean headerWritePending;
81  
82      /** Simple constructor.
83       * @param generator generator for AEM output
84       * @param writer writer for the AEM message format
85       * @param header file header (may be null)
86       * @param template template for metadata
87       * @since 11.0
88       */
89      public StreamingAemWriter(final Generator generator, final AemWriter writer,
90                                final AdmHeader header, final AemMetadata template) {
91          this.generator          = generator;
92          this.writer             = writer;
93          this.header             = header;
94          this.metadata           = template.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
95          this.headerWritePending = true;
96      }
97  
98      /**
99       * Create a writer for a new AEM attitude ephemeris segment.
100      * <p> The returned writer can only write a single attitude ephemeris segment in an AEM.
101      * This method must be called to create a writer for each attitude ephemeris segment.
102      * @return a new AEM segment writer, ready for use.
103      */
104     public SegmentWriter newSegment() {
105         return new SegmentWriter();
106     }
107 
108     /** {@inheritDoc} */
109     @Override
110     public void close() throws IOException {
111         writer.writeFooter(generator);
112     }
113 
114     /** A writer for a segment of an AEM. */
115     public class SegmentWriter implements OrekitFixedStepHandler {
116 
117         /** Empty constructor.
118          * <p>
119          * This constructor is not strictly necessary, but it prevents spurious
120          * javadoc warnings with JDK 18 and later.
121          * </p>
122          * @since 12.0
123          */
124         public SegmentWriter() {
125             // nothing to do
126         }
127 
128         /**
129          * {@inheritDoc}
130          *
131          * <p> Sets the {@link AemMetadataKey#START_TIME} and {@link AemMetadataKey#STOP_TIME} in this
132          * segment's metadata if not already set by the user. Then calls
133          * {@link org.orekit.files.ccsds.utils.generation.AbstractMessageWriter#writeHeader(Generator, Header)
134          * writeHeader} if it is the first segment) and {@link AemWriter#writeMetadata(Generator, double, AemMetadata)} to start the segment.
135          */
136         @Override
137         public void init(final SpacecraftState s0, final AbsoluteDate t, final double step) {
138             try {
139                 final AbsoluteDate date = s0.getDate();
140                 if (t.isBefore(date)) {
141                     throw new OrekitException(OrekitMessages.NON_CHRONOLOGICALLY_SORTED_ENTRIES,
142                             date, t, date.durationFrom(t));
143                 }
144 
145                 if (headerWritePending) {
146                     // we write the header only for the first segment
147                     writer.writeHeader(generator, header);
148                     headerWritePending = false;
149                 }
150 
151                 metadata.setStartTime(date);
152                 metadata.setUseableStartTime(null);
153                 metadata.setUseableStopTime(null);
154                 metadata.setStopTime(t);
155                 if (metadata.getEndpoints().getFrameA() == null ||
156                     metadata.getEndpoints().getFrameA().asSpacecraftBodyFrame().isEmpty()) {
157                     // the external frame must be frame A
158                     metadata.getEndpoints().setFrameA(FrameFacade.map(s0.getAttitude().getReferenceFrame()));
159                 } else {
160                     // the external frame must be frame B
161                     metadata.getEndpoints().setFrameB(FrameFacade.map(s0.getAttitude().getReferenceFrame()));
162                 }
163                 writer.writeMetadata(generator,
164                                      header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
165                                      metadata);
166                 writer.startAttitudeBlock(generator);
167             } catch (IOException e) {
168                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE, e.getLocalizedMessage());
169             }
170         }
171 
172         /** {@inheritDoc}. */
173         @Override
174         public void handleStep(final SpacecraftState currentState) {
175             try {
176                 writer.writeAttitudeEphemerisLine(generator,
177                                                   header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
178                                                   metadata, currentState.getAttitude().getOrientation());
179             } catch (IOException e) {
180                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE, e.getLocalizedMessage());
181             }
182         }
183 
184         /** {@inheritDoc}. */
185         @Override
186         public void finish(final SpacecraftState finalState) {
187             try {
188                 writer.endAttitudeBlock(generator);
189             } catch (IOException e) {
190                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE, e.getLocalizedMessage());
191             }
192         }
193 
194     }
195 
196 }