1   /* Copyright 2022-2025 Thales Alenia Space
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.rinex.observation;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.CharArrayWriter;
21  import java.io.IOException;
22  import java.nio.charset.StandardCharsets;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.function.BiFunction;
26  import java.util.function.Function;
27  
28  import org.hipparchus.geometry.euclidean.threed.Vector3D;
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.Precision;
31  import org.junit.jupiter.api.Assertions;
32  import org.junit.jupiter.api.BeforeEach;
33  import org.junit.jupiter.api.Test;
34  import org.orekit.Utils;
35  import org.orekit.annotation.DefaultDataContext;
36  import org.orekit.data.DataContext;
37  import org.orekit.data.DataSource;
38  import org.orekit.errors.OrekitException;
39  import org.orekit.errors.OrekitMessages;
40  import org.orekit.estimation.measurements.QuadraticClockModel;
41  import org.orekit.files.rinex.AppliedDCBS;
42  import org.orekit.files.rinex.AppliedPCVS;
43  import org.orekit.files.rinex.section.RinexComment;
44  import org.orekit.gnss.ObservationTimeScale;
45  import org.orekit.gnss.ObservationType;
46  import org.orekit.gnss.PredefinedObservationType;
47  import org.orekit.gnss.SatInSystem;
48  import org.orekit.gnss.SatelliteSystem;
49  import org.orekit.time.AbsoluteDate;
50  import org.orekit.time.ConstantOffsetTimeScale;
51  import org.orekit.time.TimeOffset;
52  import org.orekit.time.TimeScale;
53  import org.orekit.time.TimeScales;
54  
55  public class RinexObservationWriterTest {
56  
57      @BeforeEach
58      public void setUp() {
59          Utils.setDataRoot("regular-data");
60      }
61  
62      @DefaultDataContext
63      @Test
64      public void testWriteHeaderTwice() throws IOException {
65          final RinexObservation robs = load("rinex/bbbb0000.00o",
66                                             PredefinedObservationType::valueOf,
67                                             (system, timeScales) -> system.getObservationTimeScale().getTimeScale(timeScales),
68                                             DataContext.getDefault().getTimeScales());
69          final CharArrayWriter  caw  = new CharArrayWriter();
70          try (RinexObservationWriter writer = new RinexObservationWriter(caw, "dummy")) {
71              writer.setReceiverClockModel(robs.extractClockModel(2));
72              writer.prepareComments(robs.getComments());
73              writer.writeHeader(robs.getHeader());
74              writer.writeHeader(robs.getHeader());
75              Assertions.fail("an exception should have been thrown");
76          } catch (OrekitException oe) {
77              Assertions.assertEquals(OrekitMessages.HEADER_ALREADY_WRITTEN, oe.getSpecifier());
78              Assertions.assertEquals("dummy", oe.getParts()[0]);
79          }
80      }
81  
82      @DefaultDataContext
83      @Test
84      public void testTooLongAgency() throws IOException {
85          final RinexObservation robs = load("rinex/bbbb0000.00o",
86                                             PredefinedObservationType::valueOf,
87                                             (system, timeScales) -> system.getObservationTimeScale().getTimeScale(timeScales),
88                                             DataContext.getDefault().getTimeScales());
89          robs.getHeader().setAgencyName("much too long agency name exceeding 40 characters");
90          final CharArrayWriter  caw  = new CharArrayWriter();
91          try (RinexObservationWriter writer = new RinexObservationWriter(caw, "dummy")) {
92              writer.setReceiverClockModel(robs.extractClockModel(2));
93              writer.prepareComments(robs.getComments());
94              writer.writeHeader(robs.getHeader());
95              Assertions.fail("an exception should have been thrown");
96          } catch (OrekitException oe) {
97              Assertions.assertEquals(OrekitMessages.FIELD_TOO_LONG, oe.getSpecifier());
98              Assertions.assertEquals("much too long agency name exceeding 40 characters", oe.getParts()[0]);
99              Assertions.assertEquals(40, (Integer) oe.getParts()[1]);
100         }
101     }
102 
103     @DefaultDataContext
104     @Test
105     public void testNoWriteHeader() throws IOException {
106         final RinexObservation robs = load("rinex/aiub0000.00o",
107                                            PredefinedObservationType::valueOf,
108                                            (system, timeScales) -> system.getObservationTimeScale().getTimeScale(timeScales),
109                                            DataContext.getDefault().getTimeScales());
110         final CharArrayWriter  caw  = new CharArrayWriter();
111         try (RinexObservationWriter writer = new RinexObservationWriter(caw, "dummy")) {
112             writer.setReceiverClockModel(robs.extractClockModel(2));
113             writer.writeObservationDataSet(robs.getObservationDataSets().get(0));
114             Assertions.fail("an exception should have been thrown");
115         } catch (OrekitException oe) {
116             Assertions.assertEquals(OrekitMessages.HEADER_NOT_WRITTEN, oe.getSpecifier());
117             Assertions.assertEquals("dummy", oe.getParts()[0]);
118         }
119     }
120 
121     @DefaultDataContext
122     @Test
123     public void testRoundTripRinex2A() throws IOException {
124         doTestRoundTrip("rinex/aiub0000.00o", 0.0);
125     }
126 
127     @DefaultDataContext
128     @Test
129     public void testRoundTripRinex2B() throws IOException {
130         doTestRoundTrip("rinex/cccc0000.07o", 0.0);
131     }
132 
133     @DefaultDataContext
134     @Test
135     public void testRoundTripRinex3A() throws IOException {
136         doTestRoundTrip("rinex/bbbb0000.00o", 0.0);
137     }
138 
139     @DefaultDataContext
140     @Test
141     public void testRoundTripRinex3B() throws IOException {
142         doTestRoundTrip("rinex/dddd0000.01o", 0.0);
143     }
144 
145     @DefaultDataContext
146     @Test
147     public void testRoundTripDcbs() throws IOException {
148         doTestRoundTrip("rinex/dcbs.00o", 0.0);
149     }
150 
151     @DefaultDataContext
152     @Test
153     public void testRoundTripPcvs() throws IOException {
154         doTestRoundTrip("rinex/pcvs.00o", 0.0);
155     }
156 
157     @DefaultDataContext
158     @Test
159     public void testRoundTripScaleFactor() throws IOException {
160         doTestRoundTrip("rinex/bbbb0000.00o", 0.0);
161     }
162 
163     @DefaultDataContext
164     @Test
165     public void testRoundTripObsScaleFactor() throws IOException {
166         doTestRoundTrip("rinex/ice12720-scaled.07o", 0.0);
167     }
168 
169     @DefaultDataContext
170     @Test
171     public void testRoundTripLeapSecond() throws IOException {
172         doTestRoundTrip("rinex/jnu10110.17o", 0.0);
173     }
174 
175     @DefaultDataContext
176     @Test
177     public void testContinuationPhaseShift() throws IOException {
178         doTestRoundTrip("rinex/continuation-phase-shift.23o", 0.0);
179     }
180 
181     @DefaultDataContext
182     @Test
183     public void testCustomSystem() throws IOException {
184         doTestRoundTrip("rinex/custom-system.01o", 0.0,
185                         CustomType::new,
186                         (system, timeScales) -> {
187                            final ObservationTimeScale ots = system.getObservationTimeScale();
188                            return ots != null ?
189                                   ots.getTimeScale(timeScales) :
190                                   new ConstantOffsetTimeScale(system.name(), new TimeOffset(45, TimeOffset.SECOND));
191                         },
192                         DataContext.getDefault().getTimeScales());
193     }
194 
195     private RinexObservation load(final String name,
196                                   final Function<? super String, ? extends ObservationType> typeBuilder,
197                                   final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
198                                   final TimeScales timeScales) {
199         final DataSource dataSource = new DataSource(name, () -> Utils.class.getClassLoader().getResourceAsStream(name));
200         return new RinexObservationParser(typeBuilder, timeScaleBuilder, timeScales).parse(dataSource);
201      }
202 
203     @DefaultDataContext
204     private void doTestRoundTrip(final String resourceName, double expectedDt) throws IOException {
205         doTestRoundTrip(resourceName, expectedDt,
206                         PredefinedObservationType::valueOf,
207                         (system, timeScales) -> system.getObservationTimeScale() == null ?
208                                                 null :
209                                                 system.getObservationTimeScale().getTimeScale(timeScales),
210                         DataContext.getDefault().getTimeScales());
211      }
212 
213     private void doTestRoundTrip(final String resourceName, double expectedDt,
214                                  final Function<? super String, ? extends ObservationType> typeBuilder,
215                                  final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
216                                  final TimeScales timeScales) throws IOException {
217 
218         final RinexObservation robs = load(resourceName, typeBuilder, timeScaleBuilder, timeScales);
219         final CharArrayWriter  caw  = new CharArrayWriter();
220         try (RinexObservationWriter writer = new RinexObservationWriter(caw, "dummy", timeScaleBuilder, timeScales)) {
221             writer.setReceiverClockModel(robs.extractClockModel(2));
222             RinexObservation patched = load(resourceName, typeBuilder, timeScaleBuilder, timeScales);
223             patched.getHeader().setClockOffsetApplied(robs.getHeader().getClockOffsetApplied());
224             if (FastMath.abs(expectedDt) > 1.0e-15) {
225                 writer.setReceiverClockModel(new QuadraticClockModel(robs.getHeader().getTFirstObs(),
226                                                                      expectedDt, 0.0, 0.0));
227             }
228             writer.writeCompleteFile(patched);
229         }
230 
231         // reparse the written file
232         final byte[]           bytes   = caw.toString().getBytes(StandardCharsets.UTF_8);
233         final DataSource       source  = new DataSource("", () -> new ByteArrayInputStream(bytes));
234         final RinexObservation rebuilt = new RinexObservationParser(typeBuilder, timeScaleBuilder, timeScales).parse(source);
235 
236         checkRinexFile(robs, rebuilt, expectedDt);
237 
238     }
239 
240     private void checkRinexFile(final RinexObservation first, final RinexObservation second,
241                                 final double expectedDt) {
242         checkRinexHeader(first.getHeader(), second.getHeader(), expectedDt);
243         // we may have lost comments in events observations
244         Assertions.assertTrue(first.getComments().size() >= second.getComments().size());
245         for (int i = 0; i < second.getComments().size(); ++i) {
246             checkRinexComments(first.getComments().get(i), second.getComments().get(i));
247         }
248         Assertions.assertEquals(first.getObservationDataSets().size(), second.getObservationDataSets().size());
249         for (int i = 0; i < first.getObservationDataSets().size(); ++i) {
250             checkRinexObs(first.getObservationDataSets().get(i), second.getObservationDataSets().get(i), expectedDt);
251         }
252     }
253 
254     private void checkRinexHeader(final RinexObservationHeader first, final RinexObservationHeader second,
255                                   final double expectedDt) {
256         Assertions.assertEquals(first.getFormatVersion(),          second.getFormatVersion(), 0.001);
257         Assertions.assertEquals(first.getSatelliteSystem(),        second.getSatelliteSystem());
258         Assertions.assertEquals(first.getProgramName(),            second.getProgramName());
259         Assertions.assertEquals(first.getRunByName(),              second.getRunByName());
260         Assertions.assertEquals(first.getCreationDateComponents(), second.getCreationDateComponents());
261         Assertions.assertEquals(first.getCreationTimeZone(),       second.getCreationTimeZone());
262         checkDate(first.getCreationDate(), second.getCreationDate(), 0.0);
263         Assertions.assertEquals(first.getDoi(),                    second.getDoi());
264         Assertions.assertEquals(first.getLicense(),                second.getLicense());
265         Assertions.assertEquals(first.getStationInformation(),     second.getStationInformation());
266         Assertions.assertEquals(first.getMarkerName(),             second.getMarkerName());
267         Assertions.assertEquals(first.getMarkerNumber(),           second.getMarkerNumber());
268         Assertions.assertEquals(first.getObserverName(),           second.getObserverName());
269         Assertions.assertEquals(first.getAgencyName(),             second.getAgencyName());
270         Assertions.assertEquals(first.getReceiverNumber(),         second.getReceiverNumber());
271         Assertions.assertEquals(first.getReceiverType(),           second.getReceiverType());
272         Assertions.assertEquals(first.getReceiverVersion(),        second.getReceiverVersion());
273         Assertions.assertEquals(first.getAntennaNumber(),          second.getAntennaNumber());
274         Assertions.assertEquals(first.getAntennaType(),            second.getAntennaType());
275         checkVector(first.getApproxPos(), second.getApproxPos());
276         Assertions.assertEquals(first.getAntennaHeight(),          second.getAntennaHeight(),         1.0e-12);
277         Assertions.assertEquals(first.getEccentricities().getX(),  second.getEccentricities().getX(), 1.0e-12);
278         Assertions.assertEquals(first.getEccentricities().getY(),  second.getEccentricities().getY(), 1.0e-12);
279         Assertions.assertEquals(first.getClockOffsetApplied(),     second.getClockOffsetApplied());
280         Assertions.assertEquals(first.getInterval(),               second.getInterval(),              1.0e-12);
281         checkDate(first.getTFirstObs(), second.getTFirstObs(), expectedDt);
282         checkDate(first.getTLastObs(),  second.getTLastObs(), expectedDt);
283         Assertions.assertEquals(first.getLeapSeconds(),            second.getLeapSeconds());
284         Assertions.assertEquals(first.getMarkerType(),             second.getMarkerType());
285         checkVector(first.getAntennaReferencePoint(),              second.getAntennaReferencePoint());
286         Assertions.assertEquals(first.getObservationCode(),        second.getObservationCode());
287         checkVector(first.getAntennaPhaseCenter(),                 second.getAntennaPhaseCenter());
288         checkVector(first.getAntennaBSight(),                      second.getAntennaBSight());
289         Assertions.assertEquals(first.getAntennaAzimuth(),         second.getAntennaAzimuth(), 1.0e-12);
290         checkVector(first.getAntennaZeroDirection(),               second.getAntennaZeroDirection());
291         checkVector(first.getCenterMass(),                         second.getCenterMass());
292         Assertions.assertEquals(first.getSignalStrengthUnit(),     second.getSignalStrengthUnit());
293         Assertions.assertEquals(first.getLeapSecondsFuture(),      second.getLeapSecondsFuture());
294         Assertions.assertEquals(first.getLeapSecondsWeekNum(),     second.getLeapSecondsWeekNum());
295         Assertions.assertEquals(first.getLeapSecondsDayNum(),      second.getLeapSecondsDayNum());
296         Assertions.assertEquals(first.getListAppliedDCBS().size(), second.getListAppliedDCBS().size());
297         for (int i = 0; i < first.getListAppliedDCBS().size(); ++i) {
298             checkDCB(first.getListAppliedDCBS().get(i), second.getListAppliedDCBS().get(i));
299         }
300         Assertions.assertEquals(first.getListAppliedPCVS().size(), second.getListAppliedPCVS().size());
301         for (int i = 0; i < first.getListAppliedPCVS().size(); ++i) {
302             checkPCV(first.getListAppliedPCVS().get(i), second.getListAppliedPCVS().get(i));
303         }
304         Assertions.assertEquals(first.getPhaseShiftCorrections().size(), second.getPhaseShiftCorrections().size());
305         for (int i = 0; i < first.getPhaseShiftCorrections().size(); ++i) {
306             checkPhaseShiftCorrection(first.getPhaseShiftCorrections().get(i), second.getPhaseShiftCorrections().get(i));
307         }
308         for (SatelliteSystem system : SatelliteSystem.values()) {
309             Assertions.assertEquals(first.getScaleFactorCorrections(system).size(), second.getScaleFactorCorrections(system).size());
310             for (int i = 0; i < first.getScaleFactorCorrections(system).size(); ++i) {
311                 checkScaleFactorCorrection(first.getScaleFactorCorrections(system).get(i),
312                                            second.getScaleFactorCorrections(system).get(i));
313             }
314         }
315         Assertions.assertEquals(first.getGlonassChannels().size(), second.getGlonassChannels().size());
316         for (int i = 0; i < first.getGlonassChannels().size(); ++i) {
317             checkGlonassChannel(first.getGlonassChannels().get(i), second.getGlonassChannels().get(i));
318         }
319         Assertions.assertEquals(first.getNbSat(),      second.getNbSat());
320         Assertions.assertEquals(first.getNbObsPerSat().size(), second.getNbObsPerSat().size());
321         for (final Map.Entry<SatInSystem, Map<ObservationType, Integer>> firstE : first.getNbObsPerSat().entrySet()) {
322             Map<ObservationType, Integer> firstV  = firstE.getValue();
323             Map<ObservationType, Integer> secondV = second.getNbObsPerSat().get(firstE.getKey());
324             Assertions.assertEquals(firstV.size(), secondV.size());
325             for (final Map.Entry<ObservationType, Integer> firstF : firstV.entrySet()) {
326                 Assertions.assertEquals(firstF.getValue(), secondV.get(firstF.getKey()));
327             }
328         }
329         Assertions.assertEquals(first.getTypeObs().size(), second.getTypeObs().size());
330         for (final Map.Entry<SatelliteSystem, List<ObservationType>> firstE : first.getTypeObs().entrySet()) {
331             List<ObservationType> firstT  = firstE.getValue();
332             List<ObservationType> secondT = second.getTypeObs().get(firstE.getKey());
333             Assertions.assertEquals(firstT.size(), secondT.size());
334             for (int i = 0; i < firstT.size(); ++i) {
335                 Assertions.assertEquals(firstT.get(i), secondT.get(i));
336             }
337         }
338         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC1cCodePhaseBias(), second.getC1cCodePhaseBias(), 1.0e-12));
339         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC1pCodePhaseBias(), second.getC1pCodePhaseBias(), 1.0e-12));
340         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC2cCodePhaseBias(), second.getC2cCodePhaseBias(), 1.0e-12));
341         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC2pCodePhaseBias(), second.getC2pCodePhaseBias(), 1.0e-12));
342 
343     }
344 
345     private void checkRinexComments(final RinexComment first, final RinexComment second) {
346         Assertions.assertEquals(first.getLineNumber(), second.getLineNumber());
347         Assertions.assertEquals(first.getText(),       second.getText());
348     }
349 
350     private void checkRinexObs(final ObservationDataSet first, final ObservationDataSet second,
351                                final double expectedDt) {
352         Assertions.assertEquals(first.getSatellite().getSystem(), second.getSatellite().getSystem());
353         Assertions.assertEquals(first.getSatellite().getPRN(),    second.getSatellite().getPRN());
354         checkDate(first.getDate(), second.getDate(), expectedDt);
355         Assertions.assertEquals(first.getEventFlag(),           second.getEventFlag());
356         Assertions.assertEquals(first.getObservationData().size(), second.getObservationData().size());
357         for (int i = 0; i < first.getObservationData().size(); ++i) {
358             final ObservationData firstO  = first.getObservationData().get(i);
359             final ObservationData secondO = second.getObservationData().get(i);
360             Assertions.assertEquals(firstO.getValue(),               secondO.getValue(), 1.0e-12);
361             Assertions.assertEquals(firstO.getLossOfLockIndicator(), secondO.getLossOfLockIndicator());
362             Assertions.assertEquals(firstO.getSignalStrength(),      secondO.getSignalStrength());
363         }
364         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getRcvrClkOffset(), second.getRcvrClkOffset(), 1.0e-12));
365     }
366 
367     private void checkDCB(final AppliedDCBS first, final AppliedDCBS second) {
368         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
369         Assertions.assertEquals(first.getProgDCBS(),        second.getProgDCBS());
370         Assertions.assertEquals(first.getSourceDCBS(),      second.getSourceDCBS());
371     }
372 
373     private void checkPCV(final AppliedPCVS first, final AppliedPCVS second) {
374         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
375         Assertions.assertEquals(first.getProgPCVS(),        second.getProgPCVS());
376         Assertions.assertEquals(first.getSourcePCVS(),      second.getSourcePCVS());
377     }
378 
379     private void checkPhaseShiftCorrection(final PhaseShiftCorrection first, final PhaseShiftCorrection second) {
380         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
381         Assertions.assertEquals(first.getTypeObs(),         second.getTypeObs());
382         Assertions.assertEquals(first.getCorrection(),      second.getCorrection(), 1.0e-12);
383         Assertions.assertEquals(first.getSatsCorrected().size(), second.getSatsCorrected().size());
384         for (int i = 0; i < first.getSatsCorrected().size(); ++i) {
385             Assertions.assertEquals(first.getSatsCorrected().get(i).getSystem(), second.getSatsCorrected().get(i).getSystem());
386             Assertions.assertEquals(first.getSatsCorrected().get(i).getPRN(),    second.getSatsCorrected().get(i).getPRN());
387         }
388     }
389 
390     private void checkScaleFactorCorrection(final ScaleFactorCorrection first, final ScaleFactorCorrection second) {
391         Assertions.assertEquals(first.getCorrection(),      second.getCorrection(), 1.0e-12);
392         Assertions.assertEquals(first.getTypesObsScaled().size(), second.getTypesObsScaled().size());
393         for (int i = 0; i < first.getTypesObsScaled().size(); ++i) {
394             Assertions.assertEquals(first.getTypesObsScaled().get(i), second.getTypesObsScaled().get(i));
395         }
396     }
397 
398     private void checkGlonassChannel(final GlonassSatelliteChannel first, final GlonassSatelliteChannel second) {
399         Assertions.assertEquals(first.getSatellite().getSystem(), second.getSatellite().getSystem());
400         Assertions.assertEquals(first.getSatellite().getPRN(),    second.getSatellite().getPRN());
401         Assertions.assertEquals(first.getK(),                     second.getK());
402     }
403 
404     private void checkDate(final AbsoluteDate first, final AbsoluteDate second,
405                            final double expectedDt) {
406         if (first == null) {
407             Assertions.assertNull(second);
408         } else if (Double.isInfinite(first.durationFrom(AbsoluteDate.ARBITRARY_EPOCH))) {
409             Assertions.assertEquals(first, second);
410         } else {
411             Assertions.assertEquals(expectedDt, second.durationFrom(first), 1.0e-6);
412         }
413     }
414 
415     private void checkVector(final Vector3D first, final Vector3D second) {
416         if (first == null) {
417             Assertions.assertNull(second);
418         } else {
419             Assertions.assertEquals(0.0, Vector3D.distance(first, second), 1.0e-12 * first.getNorm());
420         }
421     }
422 
423 }