1   /* Copyright 2022-2025 Luc Maisonobe
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.odm.ocm;
18  
19  import java.io.BufferedReader;
20  import java.io.BufferedWriter;
21  import java.io.ByteArrayInputStream;
22  import java.io.CharArrayWriter;
23  import java.io.IOException;
24  import java.io.InputStreamReader;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  
32  import org.hipparchus.geometry.euclidean.threed.Vector3D;
33  import org.hipparchus.util.Precision;
34  import org.junit.jupiter.api.Assertions;
35  import org.junit.jupiter.api.BeforeEach;
36  import org.junit.jupiter.api.Test;
37  import org.orekit.Utils;
38  import org.orekit.bodies.CelestialBodyFactory;
39  import org.orekit.data.DataContext;
40  import org.orekit.data.DataSource;
41  import org.orekit.errors.OrekitIllegalArgumentException;
42  import org.orekit.errors.OrekitMessages;
43  import org.orekit.files.ccsds.definitions.BodyFacade;
44  import org.orekit.files.ccsds.definitions.CenterName;
45  import org.orekit.files.ccsds.definitions.FrameFacade;
46  import org.orekit.files.ccsds.definitions.TimeSystem;
47  import org.orekit.files.ccsds.ndm.ParserBuilder;
48  import org.orekit.files.ccsds.ndm.WriterBuilder;
49  import org.orekit.files.ccsds.ndm.odm.oem.InterpolationMethod;
50  import org.orekit.files.ccsds.utils.FileFormat;
51  import org.orekit.files.ccsds.utils.generation.Generator;
52  import org.orekit.files.ccsds.utils.generation.KvnGenerator;
53  import org.orekit.files.ccsds.utils.generation.XmlGenerator;
54  import org.orekit.files.general.EphemerisFile;
55  import org.orekit.frames.Frame;
56  import org.orekit.frames.FramesFactory;
57  import org.orekit.time.AbsoluteDate;
58  import org.orekit.time.TimeScalesFactory;
59  import org.orekit.utils.Constants;
60  import org.orekit.utils.IERSConventions;
61  import org.orekit.utils.TimeStampedPVCoordinates;
62  
63  public class EphemerisOcmWriterTest {
64  
65      @BeforeEach
66      public void setUp() throws Exception {
67          Utils.setDataRoot("regular-data");
68      }
69  
70      @Test
71      public void testOCMWriter() {
72          Assertions.assertNotNull(new WriterBuilder().buildOcmWriter());
73      }
74  
75      @Test
76      public void testWriteOCM3Kvn() throws IOException {
77          final CharArrayWriter caw = new CharArrayWriter();
78          final Generator generator = new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60);
79          doTestWriteOCM3(caw, generator);
80      }
81  
82      @Test
83      public void testWriteOCM3Xml() throws IOException {
84          final CharArrayWriter caw = new CharArrayWriter();
85          final Generator generator = new XmlGenerator(caw, 2, "", Constants.JULIAN_DAY, true, XmlGenerator.NDM_XML_V3_SCHEMA_LOCATION);
86          doTestWriteOCM3(caw, generator);
87      }
88  
89      private void doTestWriteOCM3(final CharArrayWriter caw, Generator generator) throws IOException {
90          final String ex = "/ccsds/odm/ocm/OCMExample3.txt";
91          final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
92          final OcmParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getMars().getGM()).buildOcmParser();
93          final Ocm ocm = parser.parseMessage(source);
94  
95          OcmWriter writer = new WriterBuilder().
96                          withConventions(IERSConventions.IERS_2010).
97                          withDataContext(DataContext.getDefault()).
98                          buildOcmWriter();
99          writer.writeMessage(generator, ocm);
100         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
101 
102         final Ocm generatedOcm = new ParserBuilder().
103                                          withConventions(IERSConventions.IERS_2010).
104                                          withSimpleEOP(true).
105                                          withDataContext(DataContext.getDefault()).
106                                          withMu(Constants.EIGEN5C_EARTH_MU).
107                                          withDefaultInterpolationDegree(1).
108                                          buildOcmParser().
109                                          parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
110         compareOcms(ocm, generatedOcm);
111     }
112 
113     @Test
114     public void testUnfoundSpaceId() throws IOException {
115         final String ex = "/ccsds/odm/ocm/OCMExample1.txt";
116         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
117         final OcmParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOcmParser();
118         final Ocm ocm = parser.parseMessage(source);
119 
120         EphemerisOcmWriter writer = new EphemerisOcmWriter(new WriterBuilder().buildOcmWriter(),
121                                                            ocm.getHeader(), dummyMetadata(),
122                                                            dummyTrajectoryMetadata(), FileFormat.KVN, "",
123                                                            Constants.JULIAN_DAY, 0);
124         try {
125             writer.write(new CharArrayWriter(), ocm);
126             Assertions.fail("an exception should have been thrown");
127         } catch (OrekitIllegalArgumentException oiae) {
128             Assertions.assertEquals(OrekitMessages.VALUE_NOT_FOUND, oiae.getSpecifier());
129             Assertions.assertEquals(dummyMetadata().getInternationalDesignator(), oiae.getParts()[0]);
130         }
131 
132     }
133 
134     @Test
135     public void testNullFile() throws IOException {
136         final String ex = "/ccsds/odm/ocm/OCMExample1.txt";
137         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
138         final OcmParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOcmParser();
139         final Ocm ocm = parser.parseMessage(source);
140         EphemerisOcmWriter writer = new EphemerisOcmWriter(new WriterBuilder().buildOcmWriter(),
141                                                            ocm.getHeader(),
142                                                            ocm.getSegments().get(0).getMetadata(),
143                                                            ocm.getSegments().get(0).getData().getTrajectoryBlocks().get(0).getMetadata(),
144                                                            FileFormat.KVN, "dummy",
145                                                            Constants.JULIAN_DAY, 0);
146         try {
147             writer.write((BufferedWriter) null, ocm);
148             Assertions.fail("an exception should have been thrown");
149         } catch (OrekitIllegalArgumentException oiae) {
150             Assertions.assertEquals(OrekitMessages.NULL_ARGUMENT, oiae.getSpecifier());
151             Assertions.assertEquals("writer", oiae.getParts()[0]);
152         }
153     }
154 
155     @Test
156     public void testNullEphemeris() throws IOException {
157         EphemerisOcmWriter writer = new EphemerisOcmWriter(new WriterBuilder().buildOcmWriter(),
158                                                            null, dummyMetadata(), dummyTrajectoryMetadata(),
159                                                            FileFormat.KVN, "nullEphemeris",
160                                                            Constants.JULIAN_DAY, 60);
161         CharArrayWriter caw = new CharArrayWriter();
162         writer.write(caw, null);
163         Assertions.assertEquals(0, caw.size());
164     }
165 
166     @Test
167     public void testGenerateKVN() throws IOException {
168         doTestGenerate(FileFormat.KVN, 45);
169     }
170 
171     @Test
172     public void testGenerateXML() throws IOException {
173         doTestGenerate(FileFormat.XML, 55);
174     }
175 
176     private void doTestGenerate(FileFormat format, int expectedLines) throws IOException {
177 
178         final DataContext context = DataContext.getDefault();
179         final String id = "1999-012A";
180         StandAloneEphemerisFile file = new StandAloneEphemerisFile();
181         file.generate(id, OrbitElementsType.CARTPV, context.getFrames().getEME2000(),
182                       new TimeStampedPVCoordinates(AbsoluteDate.GALILEO_EPOCH,
183                                                    new Vector3D(1.0e6, 2.0e6, 3.0e6),
184                                                    new Vector3D(-300, -200, -100)),
185                       900.0, 60.0);
186 
187 
188         OcmMetadata metadata = dummyMetadata();
189         metadata.setEpochT0(AbsoluteDate.GALILEO_EPOCH);
190         metadata.setInternationalDesignator(id);
191         EphemerisOcmWriter writer = new EphemerisOcmWriter(new WriterBuilder().withDataContext(context).buildOcmWriter(),
192                                                            null, metadata, dummyTrajectoryMetadata(), format, "",
193                                                            Constants.JULIAN_DAY, -1);
194         final CharArrayWriter caw = new CharArrayWriter();
195         writer.write(caw, file);
196         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
197 
198         int count = 0;
199         try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
200              InputStreamReader    isr  = new InputStreamReader(bais, StandardCharsets.UTF_8);
201              BufferedReader       br   = new BufferedReader(isr)) {
202             for (String line = br.readLine(); line != null; line = br.readLine()) {
203                 ++count;
204             }
205         }
206         Assertions.assertEquals(expectedLines, count);
207 
208     }
209 
210     private static void compareOcmEphemerisBlocks(TrajectoryStateHistory block1, TrajectoryStateHistory block2) {
211         compareOcmEphemerisBlocksMetadata(block1.getMetadata(), block2.getMetadata());
212         Assertions.assertEquals(0.0, block1.getStart().durationFrom(block2.getStart()), 1.0e-12);
213         Assertions.assertEquals(0.0, block1.getStop().durationFrom(block2.getStop()), 1.0e-12);
214         Assertions.assertEquals(block1.getMetadata().getInterpolationDegree(), block2.getMetadata().getInterpolationDegree());
215         Assertions.assertEquals(block1.getMetadata().getInterpolationMethod(), block2.getMetadata().getInterpolationMethod());
216         Assertions.assertEquals(block1.getTrajectoryStates().size(), block2.getTrajectoryStates().size());
217         for (int i = 0; i < block1.getTrajectoryStates().size(); i++) {
218             TrajectoryState c1 = block1.getTrajectoryStates().get(i);
219             TrajectoryState c2 = block2.getTrajectoryStates().get(i);
220             Assertions.assertEquals(0.0, c1.getDate().durationFrom(c2.getDate()), 1.0e-12);
221             Assertions.assertEquals(c1.getType(), c2.getType());
222             Assertions.assertEquals(c1.getElements().length, c2.getElements().length);
223             for (int j = 0; j < c1.getElements().length; ++j) {
224             Assertions.assertTrue(Precision.equals(c1.getElements()[j], c2.getElements()[j], 2));
225             }
226         }
227     }
228 
229     private static void compareOcmEphemerisBlocksMetadata(TrajectoryStateHistoryMetadata meta1, TrajectoryStateHistoryMetadata meta2) {
230         Assertions.assertEquals(meta1.getTrajID(),                                 meta2.getTrajID());
231         Assertions.assertEquals(meta1.getTrajPrevID(),                             meta2.getTrajPrevID());
232         Assertions.assertEquals(meta1.getTrajNextID(),                             meta2.getTrajNextID());
233         Assertions.assertEquals(meta1.getTrajBasis(),                              meta2.getTrajBasis());
234         Assertions.assertEquals(meta1.getTrajBasisID(),                            meta2.getTrajBasisID());
235         Assertions.assertEquals(meta1.getInterpolationMethod(),                    meta2.getInterpolationMethod());
236         Assertions.assertEquals(meta1.getInterpolationDegree(),                    meta2.getInterpolationDegree());
237         Assertions.assertEquals(meta1.getPropagator(),                             meta2.getPropagator());
238         Assertions.assertEquals(meta1.getCenter().getName(),                       meta2.getCenter().getName());
239         Assertions.assertEquals(meta1.getTrajReferenceFrame().getName(),           meta2.getTrajReferenceFrame().getName());
240         Assertions.assertEquals(meta1.getTrajFrameEpoch(),                         meta2.getTrajFrameEpoch());
241         Assertions.assertEquals(meta1.getUseableStartTime(),                       meta2.getUseableStartTime());
242         Assertions.assertEquals(meta1.getUseableStopTime(),                        meta2.getUseableStopTime());
243         Assertions.assertEquals(meta1.getOrbRevNum(),                              meta2.getOrbRevNum());
244         Assertions.assertEquals(meta1.getOrbRevNumBasis(),                         meta2.getOrbRevNumBasis());
245         Assertions.assertEquals(meta1.getOrbAveraging(),                           meta2.getOrbAveraging());
246         Assertions.assertEquals(meta1.getTrajType(),                               meta2.getTrajType());
247         Assertions.assertEquals(meta1.getTrajUnits().size(),                       meta2.getTrajUnits().size());
248         for (int i = 0; i < meta1.getTrajUnits().size(); ++i) {
249             Assertions.assertEquals(meta1.getTrajUnits().get(i), meta2.getTrajUnits().get(i));
250         }
251     }
252 
253     static void compareOcms(Ocm file1, Ocm file2) {
254         Assertions.assertEquals(file1.getHeader().getOriginator(), file2.getHeader().getOriginator());
255         Assertions.assertEquals(file1.getSegments().get(0).getData().getTrajectoryBlocks().size(),
256                                 file2.getSegments().get(0).getData().getTrajectoryBlocks().size());
257         for (int i = 0; i < file1.getSegments().get(0).getData().getTrajectoryBlocks().size(); i++) {
258             compareOcmEphemerisBlocks(file1.getSegments().get(0).getData().getTrajectoryBlocks().get(i),
259                                       file2.getSegments().get(0).getData().getTrajectoryBlocks().get(i));
260         }
261     }
262 
263     private class StandAloneEphemerisFile
264         implements EphemerisFile<TimeStampedPVCoordinates, TrajectoryStateHistory> {
265         private final Map<String, OcmSatelliteEphemeris> satEphem;
266 
267         /** Simple constructor.
268          */
269         public StandAloneEphemerisFile() {
270             this.satEphem = new HashMap<>();
271         }
272 
273         private void generate(final String internationalDesignator, final OrbitElementsType type,
274                               final Frame referenceFrame, final TimeStampedPVCoordinates pv0,
275                               final double duration, final double step) {
276 
277             TrajectoryStateHistoryMetadata metadata = dummyTrajectoryMetadata();
278             metadata.addComment("generated for " + internationalDesignator);
279             metadata.setUseableStartTime(pv0.getDate());
280             metadata.setUseableStopTime(pv0.getDate().shiftedBy(duration));
281 
282             List<TrajectoryState> states = new ArrayList<>();
283             for (double dt = 0; dt < duration; dt += step) {
284                 TimeStampedPVCoordinates pv = pv0.shiftedBy(dt);
285                 double[] elements = type.toRawElements(pv, referenceFrame, null, Constants.EIGEN5C_EARTH_MU);
286                 states.add(new TrajectoryState(type, pv.getDate(), elements));
287             }
288 
289             if (!satEphem.containsKey(internationalDesignator)) {
290                 satEphem.put(internationalDesignator,
291                              new OcmSatelliteEphemeris(internationalDesignator, Constants.EIGEN5C_EARTH_MU, Collections.emptyList()));
292             }
293 
294             List<TrajectoryStateHistory> history = new ArrayList<>(satEphem.get(internationalDesignator).getSegments());
295             history.add(new TrajectoryStateHistory(metadata, states, null, Constants.EIGEN5C_EARTH_MU));
296             satEphem.put(internationalDesignator, new OcmSatelliteEphemeris(internationalDesignator, Constants.EIGEN5C_EARTH_MU, history));
297 
298         }
299 
300         @Override
301         public Map<String, OcmSatelliteEphemeris> getSatellites() {
302             return satEphem;
303         }
304 
305     }
306 
307     private OcmMetadata dummyMetadata() {
308         OcmMetadata metadata = new OcmMetadata(DataContext.getDefault());
309         metadata.addComment("dummy metadata comment");
310         metadata.setTimeSystem(TimeSystem.TT);
311         metadata.setInternationalDesignator("9999-999ZZZ");
312         metadata.setObjectName("transgalactic");
313         metadata.setEpochT0(AbsoluteDate.J2000_EPOCH.shiftedBy(80 * Constants.JULIAN_CENTURY));
314         metadata.setStartTime(metadata.getEpochT0());
315         metadata.setStopTime(metadata.getStartTime().shiftedBy(Constants.JULIAN_YEAR));
316         return metadata;
317     }
318 
319     private TrajectoryStateHistoryMetadata dummyTrajectoryMetadata() {
320         final AbsoluteDate t0 = new AbsoluteDate(2003, 5, 7, 19, 43, 56.75, TimeScalesFactory.getUTC());
321         TrajectoryStateHistoryMetadata metadata = new TrajectoryStateHistoryMetadata(t0, DataContext.getDefault());
322         metadata.addComment("dummy trajectory comment");
323         metadata.setTrajID("traj 17");
324         metadata.setTrajBasis("PREDICTED");
325         metadata.setTrajBasisID("simulation 22");
326         metadata.setInterpolationMethod(InterpolationMethod.HERMITE);
327         metadata.setInterpolationDegree(4);
328         metadata.setPropagator("Orekit");
329         metadata.setCenter(BodyFacade.create(CenterName.EARTH));
330         metadata.setTrajReferenceFrame(FrameFacade.map(FramesFactory.getTOD(IERSConventions.IERS_2010, false)));
331         metadata.setTrajFrameEpoch(new AbsoluteDate(2003, 5, 7, TimeScalesFactory.getUTC()));
332         metadata.setUseableStartTime(t0);
333         metadata.setUseableStopTime(t0.shiftedBy(3600.0));
334         metadata.setOrbRevNum(12);
335         metadata.setOrbRevNumBasis(0);
336         metadata.setOrbAveraging("OSCULATING");
337         metadata.setTrajType(OrbitElementsType.KEPLERIAN);
338         metadata.setTrajUnits(metadata.getTrajType().getUnits());
339         return metadata;
340     }
341 
342 }