1   /* Copyright 2016 Applied Defense Solutions (ADS)
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    * ADS 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.odm.ocm;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import org.orekit.bodies.OneAxisEllipsoid;
24  import org.orekit.errors.OrekitIllegalArgumentException;
25  import org.orekit.errors.OrekitMessages;
26  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
27  import org.orekit.files.ccsds.section.XmlStructureKey;
28  import org.orekit.files.ccsds.utils.FileFormat;
29  import org.orekit.files.ccsds.utils.generation.Generator;
30  import org.orekit.files.ccsds.utils.generation.KvnGenerator;
31  import org.orekit.files.ccsds.utils.generation.XmlGenerator;
32  import org.orekit.files.general.EphemerisFile;
33  import org.orekit.files.general.EphemerisFile.SatelliteEphemeris;
34  import org.orekit.files.general.EphemerisFileWriter;
35  import org.orekit.frames.Frame;
36  import org.orekit.utils.AccurateFormatter;
37  import org.orekit.utils.Formatter;
38  import org.orekit.utils.TimeStampedPVCoordinates;
39  
40  /** An {@link EphemerisFileWriter} generating {@link Ocm OCM} files.
41   * <p>
42   * This writer is intended to write only trajectory state history blocks.
43   * It does not writes physical properties, covariance data, maneuver data,
44   * perturbations parameters, orbit determination or user-defined parameters.
45   * If these blocks are needed, then {@link OcmWriter OcmWriter} must be
46   * used as it handles all OCM data blocks.
47   * </p>
48   * <p>
49   * The trajectory blocks metadata identifiers ({@code TRAJ_ID},
50   * {@code TRAJ_PREV_ID}, {@code TRAJ_NEXT_ID}) are updated automatically
51   * using {@link TrajectoryStateHistoryMetadata#incrementTrajID(String)},
52   * so users should generally only set {@link TrajectoryStateHistoryMetadata#setTrajID(String)}
53   * in the template.
54   * </p>
55   * @author Luc Maisonobe
56   * @since 12.0
57   * @see OcmWriter
58   * @see StreamingOcmWriter
59   */
60  public class EphemerisOcmWriter implements EphemerisFileWriter {
61  
62      /** Underlying writer. */
63      private final OcmWriter writer;
64  
65      /** Header. */
66      private final OdmHeader header;
67  
68      /** File metadata. */
69      private final OcmMetadata metadata;
70  
71      /** Current trajectory metadata. */
72      private final TrajectoryStateHistoryMetadata trajectoryMetadata;
73  
74      /** File format to use. */
75      private final FileFormat fileFormat;
76  
77      /** Output name for error messages. */
78      private final String outputName;
79  
80      /** Column number for aligning units. */
81      private final int unitsColumn;
82  
83      /** Maximum offset for relative dates. */
84      private final double maxRelativeOffset;
85  
86      /** Used to format dates and doubles to string. */
87      private final Formatter formatter;
88  
89      /** Central body.
90       * @since 12.0
91       */
92      private final OneAxisEllipsoid body;
93  
94      /**
95       * Constructor used to create a new OCM writer configured with the necessary parameters
96       * to successfully fill in all required fields that aren't part of a standard object.
97       * <p>
98       * If the mandatory header entries are not present (or if header is null),
99       * built-in defaults will be used
100      * </p>
101      * <p>
102      * The writer is built from the complete header and partial metadata. The template
103      * metadata is used to initialize and independent local copy, that will be updated
104      * as new segments are written (with at least the segment start and stop will change,
105      * but some other parts may change too). The {@code template} argument itself is not
106      * changed.
107      * </p>
108      * @param writer underlying writer
109      * @param header file header (may be null)
110      * @param metadata  file metadata
111      * @param template  template for trajectory metadata
112      * @param fileFormat file format to use
113      * @param outputName output name for error messages
114      * @param maxRelativeOffset maximum offset in seconds to use relative dates
115      * @param formatter used to format date and double to string.
116      * (if a date is too far from reference, it will be displayed as calendar elements)
117      * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
118      */
119     public EphemerisOcmWriter(final OcmWriter writer,
120                               final OdmHeader header, final OcmMetadata metadata,
121                               final TrajectoryStateHistoryMetadata template,
122                               final FileFormat fileFormat, final String outputName,
123                               final double maxRelativeOffset, final int unitsColumn, final Formatter formatter) {
124         this.writer             = writer;
125         this.header             = header;
126         this.metadata           = metadata.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
127         this.trajectoryMetadata = template.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
128         this.fileFormat         = fileFormat;
129         this.outputName         = outputName;
130         this.maxRelativeOffset  = maxRelativeOffset;
131         this.unitsColumn        = unitsColumn;
132         this.body               = Double.isNaN(writer.getEquatorialRadius()) ?
133                                   null :
134                                   new OneAxisEllipsoid(writer.getEquatorialRadius(),
135                                                        writer.getFlattening(),
136                                                        template.getTrajReferenceFrame().asFrame());
137         this.formatter = formatter;
138     }
139 
140     /**
141      * Constructor used to create a new OCM writer configured with the necessary parameters
142      * to successfully fill in all required fields that aren't part of a standard object.
143      * <p>
144      * If the mandatory header entries are not present (or if header is null),
145      * built-in defaults will be used
146      * </p>
147      * <p>
148      * The writer is built from the complete header and partial metadata. The template
149      * metadata is used to initialize and independent local copy, that will be updated
150      * as new segments are written (with at least the segment start and stop will change,
151      * but some other parts may change too). The {@code template} argument itself is not
152      * changed.
153      * </p>
154      * @param writer underlying writer
155      * @param header file header (may be null)
156      * @param metadata  file metadata
157      * @param template  template for trajectory metadata
158      * @param fileFormat file format to use
159      * @param outputName output name for error messages
160      * @param maxRelativeOffset maximum offset in seconds to use relative dates
161      * (if a date is too far from reference, it will be displayed as calendar elements)
162      * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
163      */
164     public EphemerisOcmWriter(final OcmWriter writer,
165                               final OdmHeader header, final OcmMetadata metadata,
166                               final TrajectoryStateHistoryMetadata template,
167                               final FileFormat fileFormat, final String outputName,
168                               final double maxRelativeOffset, final int unitsColumn) {
169         this(writer, header, metadata, template, fileFormat, outputName, maxRelativeOffset, unitsColumn, new AccurateFormatter());
170     }
171 
172     /** {@inheritDoc}
173      * <p>
174      * As {@code EphemerisFile.SatelliteEphemeris} does not have all the entries
175      * from {@link OcmMetadata}, the only values that will be extracted from the
176      * {@code ephemerisFile} will be the start time, stop time, reference frame, interpolation
177      * method and interpolation degree. The missing values (like object name, local spacecraft
178      * body frame...) will be inherited from the template  metadata set at writer
179      * {@link #EphemerisOcmWriter(OcmWriter, OdmHeader, OcmMetadata, TrajectoryStateHistoryMetadata,
180      * FileFormat, String, double, int) construction}.
181      * </p>
182      */
183     @Override
184     public <C extends TimeStampedPVCoordinates, S extends EphemerisFile.EphemerisSegment<C>>
185         void write(final Appendable appendable, final EphemerisFile<C, S> ephemerisFile)
186         throws IOException {
187 
188         if (appendable == null) {
189             throw new OrekitIllegalArgumentException(OrekitMessages.NULL_ARGUMENT, "writer");
190         }
191 
192         if (ephemerisFile == null) {
193             return;
194         }
195 
196         final String name;
197         if (metadata.getObjectName() != null) {
198             name = metadata.getObjectName();
199         } else if (metadata.getInternationalDesignator() != null) {
200             name = metadata.getInternationalDesignator();
201         } else if (metadata.getObjectDesignator() != null) {
202             name = metadata.getObjectDesignator();
203         } else {
204             name = Ocm.UNKNOWN_OBJECT;
205         }
206         final SatelliteEphemeris<C, S> satEphem = ephemerisFile.getSatellites().get(name);
207         if (satEphem == null) {
208             throw new OrekitIllegalArgumentException(OrekitMessages.VALUE_NOT_FOUND,
209                                                      name, "ephemerisFile");
210         }
211 
212         // Get trajectory blocks to output.
213         final List<S> blocks = satEphem.getSegments();
214         if (blocks.isEmpty()) {
215             // No data -> No output
216             return;
217         }
218 
219         try (Generator generator = fileFormat == FileFormat.KVN ?
220                                    new KvnGenerator(appendable, OcmWriter.KVN_PADDING_WIDTH, outputName,
221                                                     maxRelativeOffset, unitsColumn, formatter) :
222                                    new XmlGenerator(appendable, XmlGenerator.DEFAULT_INDENT, outputName,
223                                                     maxRelativeOffset, unitsColumn > 0, null, formatter)) {
224 
225             writer.writeHeader(generator, header);
226 
227             if (generator.getFormat() == FileFormat.XML) {
228                 generator.enterSection(XmlStructureKey.segment.name());
229             }
230 
231             // write single segment metadata
232             metadata.setStartTime(blocks.get(0).getStart());
233             metadata.setStopTime(blocks.get(blocks.size() - 1).getStop());
234             new OcmMetadataWriter(metadata, writer.getTimeConverter()).write(generator);
235 
236             if (generator.getFormat() == FileFormat.XML) {
237                 generator.enterSection(XmlStructureKey.data.name());
238             }
239 
240             // Loop on trajectory blocks
241             double lastZ = Double.NaN;
242             for (final S block : blocks) {
243 
244                 // prepare metadata
245                 trajectoryMetadata.setTrajNextID(TrajectoryStateHistoryMetadata.incrementTrajID(trajectoryMetadata.getTrajID()));
246                 trajectoryMetadata.setUseableStartTime(block.getStart());
247                 trajectoryMetadata.setUseableStopTime(block.getStop());
248                 trajectoryMetadata.setInterpolationDegree(block.getInterpolationSamples() - 1);
249 
250                 // prepare data
251                 final OrbitElementsType type      = trajectoryMetadata.getTrajType();
252                 final Frame             frame     = trajectoryMetadata.getTrajReferenceFrame().asFrame();
253                 int                     crossings = 0;
254                 final List<TrajectoryState> states = new ArrayList<>(block.getCoordinates().size());
255                 for (final C pv : block.getCoordinates()) {
256                     if (lastZ < 0.0 && pv.getPosition().getZ() >= 0.0) {
257                         // we crossed ascending node
258                         ++crossings;
259                     }
260                     lastZ = pv.getPosition().getZ();
261                     states.add(new TrajectoryState(type, pv.getDate(), type.toRawElements(pv, frame, body, block.getMu())));
262                 }
263                 final TrajectoryStateHistory history = new TrajectoryStateHistory(trajectoryMetadata, states,
264                                                                                   body, block.getMu());
265 
266                 // write trajectory block
267                 final TrajectoryStateHistoryWriter trajectoryWriter =
268                                 new TrajectoryStateHistoryWriter(history, writer.getTimeConverter());
269                 trajectoryWriter.write(generator);
270 
271                 // update the trajectory IDs
272                 trajectoryMetadata.setTrajPrevID(trajectoryMetadata.getTrajID());
273                 trajectoryMetadata.setTrajID(trajectoryMetadata.getTrajNextID());
274 
275                 if (trajectoryMetadata.getOrbRevNum() >= 0) {
276                     // update the orbits revolution number
277                     trajectoryMetadata.setOrbRevNum(trajectoryMetadata.getOrbRevNum() + crossings);
278                 }
279 
280             }
281 
282             if (generator.getFormat() == FileFormat.XML) {
283                 generator.exitSection(); // exit data
284                 generator.exitSection(); // exit segment
285             }
286 
287             writer.writeFooter(generator);
288 
289         }
290 
291     }
292 
293 }