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