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.odm.oem;
18  
19  import org.hipparchus.geometry.euclidean.threed.Vector3D;
20  import org.junit.jupiter.api.Assertions;
21  import org.junit.jupiter.api.BeforeEach;
22  import org.junit.jupiter.api.Test;
23  import org.orekit.Utils;
24  import org.orekit.bodies.CelestialBodyFactory;
25  import org.orekit.data.DataContext;
26  import org.orekit.data.DataSource;
27  import org.orekit.errors.OrekitIllegalArgumentException;
28  import org.orekit.errors.OrekitMessages;
29  import org.orekit.files.ccsds.definitions.BodyFacade;
30  import org.orekit.files.ccsds.definitions.FrameFacade;
31  import org.orekit.files.ccsds.definitions.TimeSystem;
32  import org.orekit.files.ccsds.ndm.ParserBuilder;
33  import org.orekit.files.ccsds.ndm.WriterBuilder;
34  import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
35  import org.orekit.files.ccsds.utils.FileFormat;
36  import org.orekit.files.ccsds.utils.generation.Generator;
37  import org.orekit.files.ccsds.utils.generation.KvnGenerator;
38  import org.orekit.files.ccsds.utils.generation.XmlGenerator;
39  import org.orekit.files.general.EphemerisFile;
40  import org.orekit.frames.Frame;
41  import org.orekit.frames.FramesFactory;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.utils.Constants;
44  import org.orekit.utils.IERSConventions;
45  import org.orekit.utils.TimeStampedPVCoordinates;
46  
47  import java.io.BufferedReader;
48  import java.io.BufferedWriter;
49  import java.io.ByteArrayInputStream;
50  import java.io.CharArrayWriter;
51  import java.io.IOException;
52  import java.io.InputStreamReader;
53  import java.nio.charset.StandardCharsets;
54  import java.util.ArrayList;
55  import java.util.Collections;
56  import java.util.HashMap;
57  import java.util.List;
58  import java.util.Map;
59  
60  public class EphemerisOemWriterTest {
61  
62      // As the default format for position is 3 digits after decimal point in km the max precision in m is 1
63      private static final double POSITION_PRECISION = 1; // in m
64      // As the default format for velocity is 5 digits after decimal point in km/s the max precision in m/s is 1e-2
65      private static final double VELOCITY_PRECISION = 1e-2; //in m/s
66  
67      @BeforeEach
68      public void setUp() throws Exception {
69          Utils.setDataRoot("regular-data");
70      }
71  
72      @Test
73      public void testOEMWriter() {
74          Assertions.assertNotNull(new WriterBuilder().buildOemWriter());
75      }
76  
77      @Test
78      public void testWriteOEM1Kvn() throws IOException {
79          final CharArrayWriter caw = new CharArrayWriter();
80          final Generator generator = new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60);
81          doTestWriteOEM1(caw, generator);
82      }
83  
84      @Test
85      public void testWriteOEM1Xml() throws IOException {
86          final CharArrayWriter caw = new CharArrayWriter();
87          final Generator generator = new XmlGenerator(caw, 2, "", Constants.JULIAN_DAY, true, XmlGenerator.NDM_XML_V3_SCHEMA_LOCATION);
88          doTestWriteOEM1(caw, generator);
89      }
90  
91      private void doTestWriteOEM1(final CharArrayWriter caw, Generator generator) throws IOException {
92          final String ex = "/ccsds/odm/oem/OEMExample1.txt";
93          final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
94          final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getMars().getGM()).buildOemParser();
95          final Oem oem = parser.parseMessage(source);
96  
97          OemWriter writer = new WriterBuilder().
98                          withConventions(IERSConventions.IERS_2010).
99                          withDataContext(DataContext.getDefault()).
100                         buildOemWriter();
101         writer.writeMessage(generator, oem);
102         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
103 
104         final Oem generatedOem = new ParserBuilder().
105                                          withConventions(IERSConventions.IERS_2010).
106                                          withSimpleEOP(true).
107                                          withDataContext(DataContext.getDefault()).
108                                          withMu(CelestialBodyFactory.getMars().getGM()).
109                                          withDefaultInterpolationDegree(1).
110                                          buildOemParser().
111                                          parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
112         compareOems(oem, generatedOem);
113     }
114 
115     @Test
116     public void testUnfoundSpaceId() throws IOException {
117         final String ex = "/ccsds/odm/oem/OEMExample1.txt";
118         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
119         final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOemParser();
120         final Oem oem = parser.parseMessage(source);
121 
122         EphemerisOemWriter writer = new EphemerisOemWriter(new WriterBuilder().buildOemWriter(),
123                                                      oem.getHeader(), dummyMetadata(), FileFormat.KVN, "",
124                                                      Constants.JULIAN_DAY, 0);
125         try {
126             writer.write(new CharArrayWriter(), oem);
127             Assertions.fail("an exception should have been thrown");
128         } catch (OrekitIllegalArgumentException oiae) {
129             Assertions.assertEquals(OrekitMessages.VALUE_NOT_FOUND, oiae.getSpecifier());
130             Assertions.assertEquals(dummyMetadata().getObjectID(), oiae.getParts()[0]);
131         }
132 
133     }
134 
135     @Test
136     public void testNullFile() throws IOException {
137         final String ex = "/ccsds/odm/oem/OEMExample1.txt";
138         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
139         final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOemParser();
140         final Oem oem = parser.parseMessage(source);
141         EphemerisOemWriter writer = new EphemerisOemWriter(new WriterBuilder().buildOemWriter(),
142                                                      oem.getHeader(),
143                                                      oem.getSegments().get(0).getMetadata(),
144                                                      FileFormat.KVN, "dummy",
145                                                      Constants.JULIAN_DAY, 0);
146         try {
147             writer.write((BufferedWriter) null, oem);
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         EphemerisOemWriter writer = new EphemerisOemWriter(new WriterBuilder().buildOemWriter(),
158                                                      null, dummyMetadata(), FileFormat.KVN, "nullEphemeris",
159                                                      Constants.JULIAN_DAY, 60);
160         CharArrayWriter caw = new CharArrayWriter();
161         writer.write(caw, null);
162         Assertions.assertEquals(0, caw.size());
163     }
164 
165     @Test
166     public void testUnisatelliteFileWithDefault() throws IOException {
167         final String ex = "/ccsds/odm/oem/OEMExample1.txt";
168         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
169         final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOemParser();
170         final Oem oem = parser.parseMessage(source);
171 
172         OemWriter writer = new WriterBuilder().buildOemWriter();
173         final CharArrayWriter caw = new CharArrayWriter();
174         writer.writeMessage(new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60), oem);
175         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
176 
177         final Oem generatedOem = new ParserBuilder().
178                                          withMu(CelestialBodyFactory.getEarth().getGM()).
179                                          buildOemParser().
180                                          parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
181         Assertions.assertEquals(oem.getSegments().get(0).getMetadata().getObjectID(),
182                 generatedOem.getSegments().get(0).getMetadata().getObjectID());
183     }
184 
185     @Test
186     public void testIssue723() throws IOException {
187         final String ex = "/ccsds/odm/oem/OEMExampleWithHeaderComment.txt";
188         final DataSource source =  new DataSource(ex, () -> getClass().getResourceAsStream(ex));
189         final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOemParser();
190         final Oem oem = parser.parseMessage(source);
191 
192         EphemerisOemWriter writer = new EphemerisOemWriter(new WriterBuilder().buildOemWriter(),
193                                                      oem.getHeader(),
194                                                      oem.getSegments().get(0).getMetadata(),
195                                                      FileFormat.KVN, "TestOEMIssue723.aem",
196                                                      Constants.JULIAN_DAY, 0);
197         final CharArrayWriter caw = new CharArrayWriter();
198         writer.write(caw, oem);
199         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
200 
201         final Oem generatedOem = new ParserBuilder().
202                                          withMu(CelestialBodyFactory.getEarth().getGM()).
203                                          buildOemParser().
204                                          parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
205         Assertions.assertEquals(oem.getHeader().getComments().get(0), generatedOem.getHeader().getComments().get(0));
206     }
207 
208     /**
209      * Check writing an OEM with format parameters for orbit.
210      *
211      * @throws IOException on error
212      */
213     @Test
214     public void testWriteOemFormat() throws IOException {
215         // setup
216         String exampleFile = "/ccsds/odm/oem/OEMExample4.txt";
217         final DataSource source =  new DataSource(exampleFile, () -> getClass().getResourceAsStream(exampleFile));
218         final OemParser parser  = new ParserBuilder().withMu(CelestialBodyFactory.getEarth().getGM()).buildOemParser();
219         Oem oem = parser.parseMessage(source);
220 
221         OemWriter writer = new WriterBuilder().buildOemWriter();
222         final CharArrayWriter caw = new CharArrayWriter();
223         writer.writeMessage(new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60), oem);
224 
225         String[] lines2 = caw.toString().split("\n");
226         Assertions.assertEquals("2002-12-18T12:00:00.331 2789.619 -280.045 -1746.755 4.73372 -2.49586 -1.0419499999999997", lines2[21]);
227         Assertions.assertEquals("2002-12-18T12:01:00.331 2783.419 -308.143 -1877.071 5.18604 -2.42124 -1.99608", lines2[22]);
228         Assertions.assertEquals("2002-12-18T12:02:00.331 2776.033 -336.859 -2008.682 5.63678 -2.33951 -1.94687", lines2[23]);
229 
230     }
231 
232     @Test
233     public void testMultisatelliteFile() throws IOException {
234 
235         final DataContext context = DataContext.getDefault();
236         final String id1 = "1999-012A";
237         final String id2 = "1999-012B";
238         StandAloneEphemerisFile file = new StandAloneEphemerisFile();
239         file.generate(id1, id1 + "-name", context.getFrames().getEME2000(),
240                       new TimeStampedPVCoordinates(AbsoluteDate.GALILEO_EPOCH,
241                                                    new Vector3D(1.0e6, 2.0e6, 3.0e6),
242                                                    new Vector3D(-300, -200, -100)),
243                       900.0, 60.0);
244         file.generate(id2, id2 + "-name", context.getFrames().getEME2000(),
245                       new TimeStampedPVCoordinates(AbsoluteDate.GALILEO_EPOCH,
246                                                    new Vector3D(3.0e6, 2.0e6, -1.0e6),
247                                                    new Vector3D(-17, -20, 150)),
248                       600.0, 10.0);
249 
250 
251         OemMetadata metadata = dummyMetadata();
252         metadata.setObjectID(id2);
253         EphemerisOemWriter writer = new EphemerisOemWriter(new WriterBuilder().withDataContext(context).buildOemWriter(),
254                                                      null, metadata, FileFormat.KVN, "",
255                                                      Constants.JULIAN_DAY, -1);
256         final CharArrayWriter caw = new CharArrayWriter();
257         writer.write(caw, file);
258         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
259 
260         int count = 0;
261         try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
262              InputStreamReader    isr  = new InputStreamReader(bais, StandardCharsets.UTF_8);
263              BufferedReader       br   = new BufferedReader(isr)) {
264             for (String line = br.readLine(); line != null; line = br.readLine()) {
265                 ++count;
266             }
267         }
268         Assertions.assertEquals(80, count);
269 
270     }
271 
272     private static void compareOemEphemerisBlocks(OemSegment block1, OemSegment block2) {
273         compareOemEphemerisBlocksMetadata(block1.getMetadata(), block2.getMetadata());
274         Assertions.assertEquals(block1.getStart(), block2.getStart());
275         Assertions.assertEquals(block1.getStop(), block2.getStop());
276         Assertions.assertEquals(block1.getMetadata().getInterpolationDegree(), block2.getMetadata().getInterpolationDegree());
277         Assertions.assertEquals(block1.getMetadata().getInterpolationMethod(), block2.getMetadata().getInterpolationMethod());
278         Assertions.assertEquals(block1.getData().getEphemeridesDataLines().size(), block2.getData().getEphemeridesDataLines().size());
279         for (int i = 0; i < block1.getData().getEphemeridesDataLines().size(); i++) {
280             TimeStampedPVCoordinates c1 = block1.getData().getEphemeridesDataLines().get(i);
281             TimeStampedPVCoordinates c2 = block2.getData().getEphemeridesDataLines().get(i);
282             Assertions.assertEquals(c1.getDate(), c2.getDate());
283             Assertions.assertEquals( 0.0,
284                     Vector3D.distance(c1.getPosition(), c2.getPosition()), POSITION_PRECISION,c1.getPosition() + " -> " + c2.getPosition());
285             Assertions.assertEquals( 0.0,
286                     Vector3D.distance(c1.getVelocity(), c2.getVelocity()), VELOCITY_PRECISION,c1.getVelocity() + " -> " + c2.getVelocity());
287         }
288         Assertions.assertEquals(block1.getCovarianceMatrices().size(), block2.getCovarianceMatrices().size());
289         for (int j = 0; j < block1.getCovarianceMatrices().size(); j++) {
290             CartesianCovariance covMat1 = block1.getCovarianceMatrices().get(j);
291             CartesianCovariance covMat2 = block2.getCovarianceMatrices().get(j);
292             Assertions.assertEquals(covMat1.getEpoch(), covMat2.getEpoch());
293             Assertions.assertEquals(covMat1.getReferenceFrame().asFrame(),               covMat2.getReferenceFrame().asFrame());
294             Assertions.assertEquals(covMat1.getReferenceFrame().asCelestialBodyFrame(),  covMat2.getReferenceFrame().asCelestialBodyFrame());
295             Assertions.assertEquals(covMat1.getReferenceFrame().asOrbitRelativeFrame(),  covMat2.getReferenceFrame().asOrbitRelativeFrame());
296             Assertions.assertEquals(covMat1.getReferenceFrame().asSpacecraftBodyFrame(), covMat2.getReferenceFrame().asSpacecraftBodyFrame());
297             Assertions.assertEquals(covMat1.getCovarianceMatrix(),covMat2.getCovarianceMatrix());
298         }
299     }
300 
301     private static void compareOemEphemerisBlocksMetadata(OemMetadata meta1, OemMetadata meta2) {
302         Assertions.assertEquals(meta1.getObjectID(),                               meta2.getObjectID());
303         Assertions.assertEquals(meta1.getObjectName(),                             meta2.getObjectName());
304         Assertions.assertEquals(meta1.getCenter().getName(),                       meta2.getCenter().getName());
305         Assertions.assertEquals(meta1.getReferenceFrame().asFrame(),               meta2.getReferenceFrame().asFrame());
306         Assertions.assertEquals(meta1.getReferenceFrame().asCelestialBodyFrame(),  meta2.getReferenceFrame().asCelestialBodyFrame());
307         Assertions.assertEquals(meta1.getReferenceFrame().asOrbitRelativeFrame(),  meta2.getReferenceFrame().asOrbitRelativeFrame());
308         Assertions.assertEquals(meta1.getReferenceFrame().asSpacecraftBodyFrame(), meta2.getReferenceFrame().asSpacecraftBodyFrame());
309         Assertions.assertEquals(meta1.getTimeSystem().name(),    meta2.getTimeSystem().name());
310     }
311 
312     static void compareOems(Oem file1, Oem file2) {
313         Assertions.assertEquals(file1.getHeader().getOriginator(), file2.getHeader().getOriginator());
314         Assertions.assertEquals(file1.getSegments().size(), file2.getSegments().size());
315         for (int i = 0; i < file1.getSegments().size(); i++) {
316             compareOemEphemerisBlocks(file1.getSegments().get(i), file2.getSegments().get(i));
317         }
318     }
319 
320     private class StandAloneEphemerisFile
321         implements EphemerisFile<TimeStampedPVCoordinates, OemSegment> {
322         private final Map<String, OemSatelliteEphemeris> satEphem;
323 
324         /** Simple constructor.
325          */
326         public StandAloneEphemerisFile() {
327             this.satEphem    = new HashMap<String, OemSatelliteEphemeris>();
328         }
329 
330         private void generate(final String objectID, final String objectName,
331                               final Frame referenceFrame, final TimeStampedPVCoordinates pv0,
332                               final double duration, final double step) {
333 
334             OemMetadata metadata = dummyMetadata();
335             metadata.addComment("metadata for " + objectName);
336             metadata.setObjectID(objectID);
337             metadata.setObjectName(objectName);
338             metadata.setStartTime(pv0.getDate());
339             metadata.setStopTime(pv0.getDate().shiftedBy(duration));
340             metadata.setUseableStartTime(metadata.getStartTime().shiftedBy(step));
341             metadata.setUseableStartTime(metadata.getStopTime().shiftedBy(-step));
342 
343             OemData data = new OemData();
344             data.addComment("generated data for " + objectName);
345             data.addComment("duration was set to " + duration + " s");
346             data.addComment("step was set to " + step + " s");
347             for (double dt = 0; dt < duration; dt += step) {
348                 data.addData(pv0.shiftedBy(dt), false);
349             }
350 
351             if (!satEphem.containsKey(objectID)) {
352                 satEphem.put(objectID,
353                              new OemSatelliteEphemeris(objectID, Constants.EIGEN5C_EARTH_MU, Collections.emptyList()));
354             }
355 
356             List<OemSegment> segments =
357                             new ArrayList<>(satEphem.get(objectID).getSegments());
358             segments.add(new OemSegment(metadata, data, Constants.EIGEN5C_EARTH_MU));
359             satEphem.put(objectID, new OemSatelliteEphemeris(objectID, Constants.EIGEN5C_EARTH_MU, segments));
360 
361         }
362 
363         @Override
364         public Map<String, OemSatelliteEphemeris> getSatellites() {
365             return satEphem;
366         }
367 
368     }
369 
370     private OemMetadata dummyMetadata() {
371         OemMetadata metadata = new OemMetadata(4);
372         metadata.addComment("dummy comment");
373         metadata.setTimeSystem(TimeSystem.TT);
374         metadata.setObjectID("9999-999ZZZ");
375         metadata.setObjectName("transgalactic");
376         metadata.setCenter(new BodyFacade("EARTH", CelestialBodyFactory.getCelestialBodies().getEarth()));
377         metadata.setReferenceFrame(FrameFacade.map(FramesFactory.getEME2000()));
378         metadata.setStartTime(AbsoluteDate.J2000_EPOCH.shiftedBy(80 * Constants.JULIAN_CENTURY));
379         metadata.setStopTime(metadata.getStartTime().shiftedBy(Constants.JULIAN_YEAR));
380         return metadata;
381     }
382 
383 }