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.ccsds.ndm.adm.aem;
18  
19  import java.io.IOException;
20  import java.util.List;
21  
22  import org.orekit.errors.OrekitIllegalArgumentException;
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.utils.FileFormat;
27  import org.orekit.files.ccsds.utils.generation.Generator;
28  import org.orekit.files.ccsds.utils.generation.KvnGenerator;
29  import org.orekit.files.ccsds.utils.generation.XmlGenerator;
30  import org.orekit.files.general.AttitudeEphemerisFile;
31  import org.orekit.files.general.AttitudeEphemerisFile.SatelliteAttitudeEphemeris;
32  import org.orekit.files.general.AttitudeEphemerisFileWriter;
33  import org.orekit.utils.AccurateFormatter;
34  import org.orekit.utils.Formatter;
35  import org.orekit.utils.TimeStampedAngularCoordinates;
36  
37  /** An {@link AttitudeEphemerisFileWriter} generating {@link Aem AEM} files.
38   * @author Bryan Cazabonne
39   * @since 11.0
40   */
41  public class AttitudeWriter implements AttitudeEphemerisFileWriter {
42  
43      /** Underlying writer. */
44      private final AemWriter writer;
45  
46      /** Header. */
47      private final AdmHeader header;
48  
49      /** Current metadata. */
50      private final AemMetadata metadata;
51  
52      /** File format to use. */
53      private final FileFormat fileFormat;
54  
55      /** Output name for error messages. */
56      private final String outputName;
57  
58      /** Maximum offset for relative dates.
59       * @since 12.0
60       */
61      private final double maxRelativeOffset;
62  
63      /** Column number for aligning units. */
64      private final int unitsColumn;
65  
66      /** Used to format dates and doubles to string. */
67      private final Formatter formatter;
68  
69      /**
70       * Constructor used to create a new AEM writer configured with the necessary parameters
71       * to successfully fill in all required fields that aren't part of a standard object.
72       * <p>
73       * If the mandatory header entries are not present (or if header is null),
74       * built-in defaults will be used
75       * </p>
76       * <p>
77       * The writer is built from the complete header and partial metadata. The template
78       * metadata is used to initialize and independent local copy, that will be updated
79       * as new segments are written (with at least the segment start and stop will change,
80       * but some other parts may change too). The {@code template} argument itself is not
81       * changed.
82       * </p>
83       * <p>
84       * Calling this constructor directly is not recommended. Users should rather use
85       * {@link org.orekit.files.ccsds.ndm.WriterBuilder#buildAemWriter()}.
86       * </p>
87       * @param writer underlying writer
88       * @param header file header (may be null)
89       * @param template template for metadata
90       * @param fileFormat file format to use
91       * @param outputName output name for error messages
92       * @param maxRelativeOffset maximum offset in seconds to use relative dates
93       * (if a date is too far from reference, it will be displayed as calendar elements)
94       * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
95       * @param formatter how to format date and double to string.
96       * @since 13.0
97       */
98      public AttitudeWriter(final AemWriter writer,
99                            final AdmHeader header, final AemMetadata template,
100                           final FileFormat fileFormat, final String outputName,
101                           final double maxRelativeOffset, final int unitsColumn, final Formatter formatter) {
102         this.writer            = writer;
103         this.header            = header;
104         this.metadata          = template.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
105         this.fileFormat        = fileFormat;
106         this.outputName        = outputName;
107         this.maxRelativeOffset = maxRelativeOffset;
108         this.unitsColumn       = unitsColumn;
109         this.formatter = formatter;
110     }
111 
112     /**
113      * Constructor used to create a new AEM writer configured with the necessary parameters
114      * to successfully fill in all required fields that aren't part of a standard object.
115      * <p>
116      * If the mandatory header entries are not present (or if header is null),
117      * built-in defaults will be used
118      * </p>
119      * <p>
120      * The writer is built from the complete header and partial metadata. The template
121      * metadata is used to initialize and independent local copy, that will be updated
122      * as new segments are written (with at least the segment start and stop will change,
123      * but some other parts may change too). The {@code template} argument itself is not
124      * changed.
125      * </p>
126      * <p>
127      * Calling this constructor directly is not recommended. Users should rather use
128      * {@link org.orekit.files.ccsds.ndm.WriterBuilder#buildAemWriter()}.
129      * </p>
130      * @param writer underlying writer
131      * @param header file header (may be null)
132      * @param template template for metadata
133      * @param fileFormat file format to use
134      * @param outputName output name for error messages
135      * @param maxRelativeOffset maximum offset in seconds to use relative dates
136      * (if a date is too far from reference, it will be displayed as calendar elements)
137      * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
138      * @since 12.0
139      */
140     public AttitudeWriter(final AemWriter writer,
141                           final AdmHeader header, final AemMetadata template,
142                           final FileFormat fileFormat, final String outputName,
143                           final double maxRelativeOffset, final int unitsColumn) {
144         this(writer, header, template, fileFormat, outputName, maxRelativeOffset, unitsColumn, new AccurateFormatter());
145     }
146 
147     /** {@inheritDoc}
148      * <p>
149      * As {@code AttitudeEphemerisFile.SatelliteAttitudeEphemeris} does not have all the entries
150      * from {@link AemMetadata}, the only values that will be extracted from the
151      * {@code ephemerisFile} will be the start time, stop time, reference frame, interpolation
152      * method and interpolation degree. The missing values (like object name, local spacecraft
153      * body frame, attitude type...) will be inherited from the template  metadata set at writer
154      * {@link #AttitudeWriter(AemWriter, AdmHeader, AemMetadata, FileFormat, String, double, int) construction}.
155      * </p>
156      */
157     @Override
158     public <C extends TimeStampedAngularCoordinates, S extends AttitudeEphemerisFile.AttitudeEphemerisSegment<C>>
159         void write(final Appendable appendable, final AttitudeEphemerisFile<C, S> ephemerisFile)
160         throws IOException {
161 
162         if (appendable == null) {
163             throw new OrekitIllegalArgumentException(OrekitMessages.NULL_ARGUMENT, "writer");
164         }
165 
166         if (ephemerisFile == null) {
167             return;
168         }
169 
170         final SatelliteAttitudeEphemeris<C, S> satEphem =
171                         ephemerisFile.getSatellites().get(metadata.getObjectID());
172         if (satEphem == null) {
173             throw new OrekitIllegalArgumentException(OrekitMessages.VALUE_NOT_FOUND,
174                                                      metadata.getObjectID(), "ephemerisFile");
175         }
176 
177         // Get attitude ephemeris segments to output.
178         final List<S> segments = satEphem.getSegments();
179         if (segments.isEmpty()) {
180             // No data -> No output
181             return;
182         }
183 
184         try (Generator generator = fileFormat == FileFormat.KVN ?
185              new KvnGenerator(appendable, AemWriter.KVN_PADDING_WIDTH, outputName,
186                               maxRelativeOffset, unitsColumn, formatter) :
187              new XmlGenerator(appendable, XmlGenerator.DEFAULT_INDENT, outputName,
188                               maxRelativeOffset, unitsColumn > 0, null, formatter)) {
189 
190             writer.writeHeader(generator, header);
191 
192             // Loop on segments
193             for (final S segment : segments) {
194                 writeSegment(generator, segment);
195             }
196 
197             writer.writeFooter(generator);
198 
199         }
200 
201     }
202 
203     /** Write one segment.
204      * @param generator generator to use for producing output
205      * @param segment segment to write
206      * @param <C> type of the angular coordinates
207      * @param <S> type of the segment
208      * @throws IOException if any buffer writing operations fails
209      */
210     private <C extends TimeStampedAngularCoordinates, S extends AttitudeEphemerisFile.AttitudeEphemerisSegment<C>>
211         void writeSegment(final Generator generator, final S segment) throws IOException {
212 
213         // override template metadata with segment values
214         metadata.setStartTime(segment.getStart());
215         metadata.setStopTime(segment.getStop());
216         if (metadata.getEndpoints().getFrameA() == null ||
217             metadata.getEndpoints().getFrameA().asSpacecraftBodyFrame() == null) {
218             // the external frame must be frame A
219             metadata.getEndpoints().setFrameA(FrameFacade.map(segment.getReferenceFrame()));
220         } else {
221             // the external frame must be frame B
222             metadata.getEndpoints().setFrameB(FrameFacade.map(segment.getReferenceFrame()));
223         }
224         metadata.setInterpolationMethod(segment.getInterpolationMethod());
225         metadata.setInterpolationDegree(segment.getInterpolationSamples() - 1);
226         metadata.validate(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
227         writer.writeMetadata(generator,
228                              header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
229                              metadata);
230 
231         // Loop on attitude data
232         writer.startAttitudeBlock(generator);
233         if (segment instanceof AemSegment) {
234             generator.writeComments(((AemSegment) segment).getData().getComments());
235         }
236         for (final TimeStampedAngularCoordinates coordinates : segment.getAngularCoordinates()) {
237             writer.writeAttitudeEphemerisLine(generator,
238                                               header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
239                                               metadata, coordinates);
240         }
241         writer.endAttitudeBlock(generator);
242 
243     }
244 
245 }