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.BufferedReader;
20  import java.io.BufferedWriter;
21  import java.io.ByteArrayInputStream;
22  import java.io.CharArrayWriter;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStreamReader;
26  import java.nio.charset.StandardCharsets;
27  import java.nio.file.Path;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.hipparchus.geometry.euclidean.threed.Rotation;
35  import org.hipparchus.geometry.euclidean.threed.Vector3D;
36  import org.junit.jupiter.api.Assertions;
37  import org.junit.jupiter.api.BeforeEach;
38  import org.junit.jupiter.api.Test;
39  import org.junit.jupiter.api.io.TempDir;
40  import org.orekit.Utils;
41  import org.orekit.data.DataContext;
42  import org.orekit.data.DataSource;
43  import org.orekit.errors.OrekitIllegalArgumentException;
44  import org.orekit.errors.OrekitMessages;
45  import org.orekit.files.ccsds.definitions.CelestialBodyFrame;
46  import org.orekit.files.ccsds.definitions.FrameFacade;
47  import org.orekit.files.ccsds.definitions.SpacecraftBodyFrame;
48  import org.orekit.files.ccsds.definitions.TimeSystem;
49  import org.orekit.files.ccsds.ndm.ParserBuilder;
50  import org.orekit.files.ccsds.ndm.WriterBuilder;
51  import org.orekit.files.ccsds.ndm.adm.AdmHeader;
52  import org.orekit.files.ccsds.ndm.adm.AttitudeType;
53  import org.orekit.files.ccsds.utils.FileFormat;
54  import org.orekit.files.ccsds.utils.generation.KvnGenerator;
55  import org.orekit.files.general.AttitudeEphemerisFile;
56  import org.orekit.frames.Frame;
57  import org.orekit.frames.FramesFactory;
58  import org.orekit.time.AbsoluteDate;
59  import org.orekit.utils.Constants;
60  import org.orekit.utils.IERSConventions;
61  import org.orekit.utils.TimeStampedAngularCoordinates;
62  
63  public class AttitudeWriterTest {
64  
65      // The default format writes 5O digits after the decimal point hence the quaternion precision
66      private static final double QUATERNION_PRECISION = 1e-5;
67      private static final double DATE_PRECISION = 1e-3;
68      
69      @TempDir
70      public Path temporaryFolderPath;
71  
72      @BeforeEach
73      public void setUp() throws Exception {
74          Utils.setDataRoot("regular-data");
75      }
76  
77      @Test
78      public void testAEMWriter() {
79          Assertions.assertNotNull(new WriterBuilder().buildAemWriter());
80      }
81  
82      @Test
83      public void testWriteAEM1() throws IOException {
84          final String ex = "/ccsds/adm/aem/AEMExample01.txt";
85          final DataSource source = new DataSource(ex, () -> getClass().getResourceAsStream(ex));
86          final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
87  
88          AdmHeader header = new AdmHeader();
89          header.setFormatVersion(aem.getHeader().getFormatVersion());
90          header.setCreationDate(aem.getHeader().getCreationDate());
91          header.setOriginator(aem.getHeader().getOriginator());
92  
93          final AemSegment s0 = aem.getSegments().get(0);
94          AemMetadata metadata = new AemMetadata(s0.getInterpolationSamples() - 1);
95          metadata.setObjectName(s0.getMetadata().getObjectName());
96          metadata.setObjectID(s0.getMetadata().getObjectID());
97          metadata.getEndpoints().setFrameA(s0.getMetadata().getEndpoints().getFrameA());
98          metadata.getEndpoints().setFrameB(s0.getMetadata().getEndpoints().getFrameB());
99          metadata.getEndpoints().setA2b(s0.getMetadata().getEndpoints().isA2b());
100         metadata.setTimeSystem(s0.getMetadata().getTimeSystem());
101         metadata.setStartTime(s0.getMetadata().getStart());
102         metadata.setStopTime(s0.getMetadata().getStop());
103         metadata.setAttitudeType(s0.getMetadata().getAttitudeType());
104         metadata.setIsFirst(s0.getMetadata().isFirst());
105         metadata.setCenter(s0.getMetadata().getCenter());
106         metadata.setInterpolationMethod(s0.getMetadata().getInterpolationMethod());
107         AemWriter writer = new WriterBuilder().
108                            withConventions(IERSConventions.IERS_2010).
109                            withDataContext(DataContext.getDefault()).
110                            buildAemWriter();
111         final CharArrayWriter caw = new CharArrayWriter();
112         writer.writeMessage(new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60), aem);
113         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
114 
115         final Aem generatedOem = new ParserBuilder().buildAemParser().
116                         parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
117         compareAems(aem, generatedOem);
118     }
119 
120     @Test
121     public void testUnfoundSpaceId() throws IOException {
122         final String ex = "/ccsds/adm/aem/AEMExample01.txt";
123         final DataSource source = new DataSource(ex, () -> getClass().getResourceAsStream(ex));
124         final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
125 
126         AemMetadata metadata = dummyMetadata();
127         metadata.setObjectID("12345");
128         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().buildAemWriter(), null, metadata,
129                                                    FileFormat.KVN, "", Constants.JULIAN_DAY, 60);
130         try {
131             writer.write(new CharArrayWriter(), aem);
132             Assertions.fail("an exception should have been thrown");
133         } catch (OrekitIllegalArgumentException oiae) {
134             Assertions.assertEquals(OrekitMessages.VALUE_NOT_FOUND, oiae.getSpecifier());
135             Assertions.assertEquals(metadata.getObjectID(), oiae.getParts()[0]);
136         }
137     }
138 
139     @Test
140     public void testNullFile() throws IOException {
141         final String ex = "/ccsds/adm/aem/AEMExample01.txt";
142         final DataSource source = new DataSource(ex, () -> getClass().getResourceAsStream(ex));
143         final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
144         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().
145                                                    withConventions(aem.getConventions()).
146                                                    withDataContext(aem.getDataContext()).
147                                                    buildAemWriter(),
148                                                    aem.getHeader(),
149                                                    aem.getSegments().get(0).getMetadata(),
150                                                    FileFormat.KVN,
151                                                    "dummy", Constants.JULIAN_DAY, 0);
152         try {
153             writer.write((BufferedWriter) null, aem);
154             Assertions.fail("an exception should have been thrown");
155         } catch (OrekitIllegalArgumentException oiae) {
156             Assertions.assertEquals(OrekitMessages.NULL_ARGUMENT, oiae.getSpecifier());
157             Assertions.assertEquals("writer", oiae.getParts()[0]);
158         }
159     }
160 
161     @Test
162     public void testNullEphemeris() throws IOException {
163         AdmHeader header = new AdmHeader();
164         header.setOriginator("NASA/JPL");
165         AemMetadata metadata = dummyMetadata();
166         metadata.setObjectID("1996-062A");
167         metadata.setObjectName("MARS GLOBAL SURVEYOR");
168         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().buildAemWriter(),
169                                                    header, metadata, FileFormat.KVN, "TestNullEphemeris.aem",
170                                                    Constants.JULIAN_DAY, 0);
171         CharArrayWriter caw = new CharArrayWriter();
172         writer.write(caw, null);
173         Assertions.assertEquals(0, caw.size());
174     }
175 
176     @Test
177     public void testUnisatelliteFileWithDefault() throws IOException {
178         final String ex = "/ccsds/adm/aem/AEMExample01.txt";
179         final DataSource source = new DataSource(ex, () -> getClass().getResourceAsStream(ex));
180         final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
181 
182         final File temp = temporaryFolderPath.resolve("writeAEMExample01.xml").toFile();
183         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().buildAemWriter(),
184                                                    aem.getHeader(), aem.getSegments().get(0).getMetadata(),
185                                                    FileFormat.XML, temp.getName(), Constants.JULIAN_DAY, 1);
186         writer.write(temp.getAbsolutePath(), aem);
187         final Aem generatedAem = new ParserBuilder().buildAemParser().parseMessage(new DataSource(temp));
188         Assertions.assertEquals(aem.getSegments().get(0).getMetadata().getObjectID(),
189                      generatedAem.getSegments().get(0).getMetadata().getObjectID());
190     }
191 
192     @Test
193     public void testMultisatelliteFile() throws IOException {
194 
195         final DataContext context = DataContext.getDefault();
196         final String id1 = "1999-012A";
197         final String id2 = "1999-012B";
198         StandAloneEphemerisFile file = new StandAloneEphemerisFile();
199         file.generate(id1, id1 + "-name", AttitudeType.QUATERNION_ANGVEL,
200                       context.getFrames().getEME2000(),
201                       new TimeStampedAngularCoordinates(AbsoluteDate.GALILEO_EPOCH,
202                                                         Rotation.IDENTITY,
203                                                         new Vector3D(0.000, 0.010, 0.000),
204                                                         new Vector3D(0.000, 0.000, 0.001)),
205                       900.0, 60.0);
206         file.generate(id2, id2 + "-name", AttitudeType.QUATERNION_ANGVEL,
207                       context.getFrames().getEME2000(),
208                       new TimeStampedAngularCoordinates(AbsoluteDate.GALILEO_EPOCH,
209                                                         Rotation.IDENTITY,
210                                                         new Vector3D(0.000, -0.010, 0.000),
211                                                         new Vector3D(0.000, 0.000, 0.003)),
212                       600.0, 10.0);
213 
214         AemMetadata metadata = dummyMetadata();
215         metadata.setObjectID(id2);
216         AdmHeader header = new AdmHeader();
217         header.setFormatVersion(1.0);
218         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().buildAemWriter(),
219                                                    header, metadata, FileFormat.KVN, "",
220                                                    Constants.JULIAN_DAY, 60);
221         final CharArrayWriter caw = new CharArrayWriter();
222         writer.write(caw, file);
223         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
224 
225         int count = 0;
226         try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
227              InputStreamReader    isr  = new InputStreamReader(bais, StandardCharsets.UTF_8);
228              BufferedReader       br   = new BufferedReader(isr)) {
229             for (String line = br.readLine(); line != null; line = br.readLine()) {
230                 ++count;
231             }
232         }
233         Assertions.assertEquals(81, count);
234 
235     }
236 
237     @Test
238     public void testIssue723() throws IOException {
239         final String ex = "/ccsds/adm/aem/AEMExample02.txt";
240         final DataSource source = new DataSource(ex, () -> getClass().getResourceAsStream(ex));
241         final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
242 
243         AttitudeWriter writer = new AttitudeWriter(new WriterBuilder().buildAemWriter(),
244                                                    aem.getHeader(), aem.getSegments().get(0).getMetadata(),
245                                                    FileFormat.KVN, "TestAEMIssue723.aem",
246                                                    Constants.JULIAN_DAY, 0);
247         final CharArrayWriter caw = new CharArrayWriter();
248         writer.write(caw, aem);
249         final byte[] bytes = caw.toString().getBytes(StandardCharsets.UTF_8);
250 
251         final Aem generatedAem = new ParserBuilder().buildAemParser().
252                         parseMessage(new DataSource("", () -> new ByteArrayInputStream(bytes)));
253         Assertions.assertEquals(aem.getHeader().getComments().get(0), generatedAem.getHeader().getComments().get(0));
254     }
255 
256     @Test
257     public void testWriteAemFormat() throws IOException {
258         // setup
259         String exampleFile = "/ccsds/adm/aem/AEMExample07.txt";
260         final DataSource source = new DataSource(exampleFile, () -> getClass().getResourceAsStream(exampleFile));
261         final Aem aem = new ParserBuilder().buildAemParser().parseMessage(source);
262 
263         AemWriter writer = new WriterBuilder().buildAemWriter();
264         final CharArrayWriter caw = new CharArrayWriter();
265         writer.writeMessage(new KvnGenerator(caw, 0, "", Constants.JULIAN_DAY, 60), aem);
266 
267         String[] lines2 = caw.toString().split("\n");
268 
269         Assertions.assertEquals("2002-12-18T12:00:00.331 0.5674807981623039 0.031460044248583355 0.4568906426171408 0.6842709624277855", lines2[26]);
270         Assertions.assertEquals("2002-12-18T12:01:00.331 0.4231908397172568 -0.4569709067454213 0.23784047193542462 0.7453314789254544", lines2[27]);
271         Assertions.assertEquals("2002-12-18T12:02:00.331 -0.8453188238242068 0.2697396246845473 -0.0653199091139417 0.4565193647993977", lines2[28]);
272     }
273 
274     private static void compareAemAttitudeBlocks(AemSegment segment1, AemSegment segment2) {
275 
276         // compare metadata
277         AemMetadata meta1 = segment1.getMetadata();
278         AemMetadata meta2 = segment2.getMetadata();
279         Assertions.assertEquals(meta1.getObjectID(),                            meta2.getObjectID());
280         Assertions.assertEquals(meta1.getObjectName(),                          meta2.getObjectName());
281         Assertions.assertEquals(meta1.getCenter().getName(),                    meta2.getCenter().getName());
282         Assertions.assertEquals(meta1.getTimeSystem().name(), meta2.getTimeSystem().name());
283         Assertions.assertEquals(meta1.getLaunchYear(),                          meta2.getLaunchYear());
284         Assertions.assertEquals(meta1.getLaunchNumber(),                        meta2.getLaunchNumber());
285         Assertions.assertEquals(meta1.getLaunchPiece(),                         meta2.getLaunchPiece());
286         Assertions.assertEquals(meta1.getHasCreatableBody(),                    meta2.getHasCreatableBody());
287         Assertions.assertEquals(meta1.getInterpolationDegree(),                 meta2.getInterpolationDegree());
288 
289         // compare data
290         Assertions.assertEquals(0.0, segment1.getStart().durationFrom(segment2.getStart()), DATE_PRECISION);
291         Assertions.assertEquals(0.0, segment1.getStop().durationFrom(segment2.getStop()),   DATE_PRECISION);
292         Assertions.assertEquals(segment1.getInterpolationMethod(), segment2.getInterpolationMethod());
293         Assertions.assertEquals(segment1.getAngularCoordinates().size(), segment2.getAngularCoordinates().size());
294         for (int i = 0; i < segment1.getAngularCoordinates().size(); i++) {
295             TimeStampedAngularCoordinates c1 = segment1.getAngularCoordinates().get(i);
296             Rotation rot1 = c1.getRotation();
297             TimeStampedAngularCoordinates c2 = segment2.getAngularCoordinates().get(i);
298             Rotation rot2 = c2.getRotation();
299             Assertions.assertEquals(0.0, c1.getDate().durationFrom(c2.getDate()), DATE_PRECISION);
300             Assertions.assertEquals(rot1.getQ0(), rot2.getQ0(), QUATERNION_PRECISION);
301             Assertions.assertEquals(rot1.getQ1(), rot2.getQ1(), QUATERNION_PRECISION);
302             Assertions.assertEquals(rot1.getQ2(), rot2.getQ2(), QUATERNION_PRECISION);
303             Assertions.assertEquals(rot1.getQ3(), rot2.getQ3(), QUATERNION_PRECISION);
304         }
305     }
306 
307     static void compareAems(Aem file1, Aem file2) {
308         Assertions.assertEquals(file1.getHeader().getOriginator(), file2.getHeader().getOriginator());
309         Assertions.assertEquals(file1.getSegments().size(), file2.getSegments().size());
310         for (int i = 0; i < file1.getSegments().size(); i++) {
311             compareAemAttitudeBlocks(file1.getSegments().get(i), file2.getSegments().get(i));
312         }
313     }
314 
315     private class StandAloneEphemerisFile
316         implements AttitudeEphemerisFile<TimeStampedAngularCoordinates, AemSegment> {
317         private final Map<String, AemSatelliteEphemeris> satEphem;
318 
319         public StandAloneEphemerisFile() {
320             this.satEphem = new HashMap<>();
321         }
322 
323         private void generate(final String objectID, final String objectName,
324                               final AttitudeType type, final Frame referenceFrame,
325                               final TimeStampedAngularCoordinates ac0,
326                               final double duration, final double step) {
327 
328             AemMetadata metadata = dummyMetadata();
329             metadata.addComment("metadata for " + objectName);
330             metadata.setObjectID(objectID);
331             metadata.setObjectName(objectName);
332             metadata.getEndpoints().setFrameA(FrameFacade.map(referenceFrame));
333             metadata.setAttitudeType(type);
334             metadata.setStartTime(ac0.getDate());
335             metadata.setStopTime(ac0.getDate().shiftedBy(duration));
336             metadata.setUseableStartTime(metadata.getStartTime().shiftedBy(step));
337             metadata.setUseableStartTime(metadata.getStopTime().shiftedBy(-step));
338 
339             AemData data = new AemData();
340             data.addComment("generated data for " + objectName);
341             data.addComment("duration was set to " + duration + " s");
342             data.addComment("step was set to " + step + " s");
343             for (double dt = 0; dt < duration; dt += step) {
344                 data.addData(ac0.shiftedBy(dt));
345             }
346 
347             if (!satEphem.containsKey(objectID)) {
348                 satEphem.put(objectID, new AemSatelliteEphemeris(objectID, Collections.emptyList()));
349             }
350 
351             List<AemSegment> segments = new ArrayList<>(satEphem.get(objectID).getSegments());
352             segments.add(new AemSegment(metadata, data));
353             satEphem.put(objectID, new AemSatelliteEphemeris(objectID, segments));
354 
355         }
356 
357         @Override
358         public Map<String, AemSatelliteEphemeris> getSatellites() {
359             return satEphem;
360         }
361 
362     }
363 
364     private AemMetadata dummyMetadata() {
365         AemMetadata metadata = new AemMetadata(4);
366         metadata.setTimeSystem(TimeSystem.TT);
367         metadata.setObjectID("9999-999ZZZ");
368         metadata.setObjectName("transgalactic");
369         metadata.getEndpoints().setFrameA(new FrameFacade(FramesFactory.getGCRF(), CelestialBodyFrame.GCRF,
370                                                           null, null, "GCRF"));
371         metadata.getEndpoints().setFrameB(new FrameFacade(null, null, null,
372                                                           new SpacecraftBodyFrame(SpacecraftBodyFrame.BaseEquipment.GYRO_FRAME, "1"),
373                                                           "GYRO 1"));
374         metadata.getEndpoints().setA2b(true);
375         metadata.setStartTime(AbsoluteDate.J2000_EPOCH.shiftedBy(80 * Constants.JULIAN_CENTURY));
376         metadata.setStopTime(metadata.getStartTime().shiftedBy(Constants.JULIAN_YEAR));
377         metadata.setAttitudeType(AttitudeType.QUATERNION_DERIVATIVE);
378         metadata.setIsFirst(true);
379         return metadata;
380     }
381 
382 }