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 testRoundTripUPC() throws IOException {
178         doTestRoundTrip("gnss/filtering/UPC33510.08O_trunc", 0.0);
179     }
180 
181     @DefaultDataContext
182     @Test
183     public void testContinuationPhaseShift() throws IOException {
184         doTestRoundTrip("rinex/continuation-phase-shift.23o", 0.0);
185     }
186 
187     @DefaultDataContext
188     @Test
189     public void testCustomSystem() throws IOException {
190         doTestRoundTrip("rinex/custom-system.01o", 0.0,
191                         CustomType::new,
192                         (system, timeScales) -> {
193                            final ObservationTimeScale ots = system.getObservationTimeScale();
194                            return ots != null ?
195                                   ots.getTimeScale(timeScales) :
196                                   new ConstantOffsetTimeScale(system.name(), new TimeOffset(45, TimeOffset.SECOND));
197                         },
198                         DataContext.getDefault().getTimeScales());
199     }
200 
201     private RinexObservation load(final String name,
202                                   final Function<? super String, ? extends ObservationType> typeBuilder,
203                                   final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
204                                   final TimeScales timeScales) {
205         final DataSource dataSource = new DataSource(name, () -> Utils.class.getClassLoader().getResourceAsStream(name));
206         return new RinexObservationParser(typeBuilder, timeScaleBuilder, timeScales).parse(dataSource);
207      }
208 
209     @DefaultDataContext
210     private void doTestRoundTrip(final String resourceName, double expectedDt) throws IOException {
211         doTestRoundTrip(resourceName, expectedDt,
212                         PredefinedObservationType::valueOf,
213                         (system, timeScales) -> system.getObservationTimeScale() == null ?
214                                                 null :
215                                                 system.getObservationTimeScale().getTimeScale(timeScales),
216                         DataContext.getDefault().getTimeScales());
217      }
218 
219     private void doTestRoundTrip(final String resourceName, double expectedDt,
220                                  final Function<? super String, ? extends ObservationType> typeBuilder,
221                                  final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
222                                  final TimeScales timeScales) throws IOException {
223 
224         final RinexObservation robs = load(resourceName, typeBuilder, timeScaleBuilder, timeScales);
225         final CharArrayWriter  caw  = new CharArrayWriter();
226         try (RinexObservationWriter writer = new RinexObservationWriter(caw, "dummy", timeScaleBuilder, timeScales)) {
227             writer.setReceiverClockModel(robs.extractClockModel(2));
228             RinexObservation patched = load(resourceName, typeBuilder, timeScaleBuilder, timeScales);
229             patched.getHeader().setClockOffsetApplied(robs.getHeader().getClockOffsetApplied());
230             if (FastMath.abs(expectedDt) > 1.0e-15) {
231                 writer.setReceiverClockModel(new QuadraticClockModel(robs.getHeader().getTFirstObs(),
232                                                                      expectedDt, 0.0, 0.0));
233             }
234             writer.writeCompleteFile(patched);
235         }
236 
237         // reparse the written file
238         final byte[]           bytes   = caw.toString().getBytes(StandardCharsets.UTF_8);
239         final DataSource       source  = new DataSource("", () -> new ByteArrayInputStream(bytes));
240         final RinexObservation rebuilt = new RinexObservationParser(typeBuilder, timeScaleBuilder, timeScales).parse(source);
241 
242         checkRinexFile(robs, rebuilt, expectedDt);
243 
244     }
245 
246     private void checkRinexFile(final RinexObservation first, final RinexObservation second,
247                                 final double expectedDt) {
248         checkRinexHeader(first.getHeader(), second.getHeader(), expectedDt);
249         // we may have lost comments in events observations
250         Assertions.assertTrue(first.getComments().size() >= second.getComments().size());
251         for (int i = 0; i < second.getComments().size(); ++i) {
252             checkRinexComments(first.getComments().get(i), second.getComments().get(i));
253         }
254         Assertions.assertEquals(first.getObservationDataSets().size(), second.getObservationDataSets().size());
255         for (int i = 0; i < first.getObservationDataSets().size(); ++i) {
256             checkRinexObs(first.getObservationDataSets().get(i), second.getObservationDataSets().get(i), expectedDt);
257         }
258     }
259 
260     private void checkRinexHeader(final RinexObservationHeader first, final RinexObservationHeader second,
261                                   final double expectedDt) {
262         Assertions.assertEquals(first.getFormatVersion(),          second.getFormatVersion(), 0.001);
263         Assertions.assertEquals(first.getSatelliteSystem(),        second.getSatelliteSystem());
264         Assertions.assertEquals(first.getProgramName(),            second.getProgramName());
265         Assertions.assertEquals(first.getRunByName(),              second.getRunByName());
266         Assertions.assertEquals(first.getCreationDateComponents(), second.getCreationDateComponents());
267         Assertions.assertEquals(first.getCreationTimeZone(),       second.getCreationTimeZone());
268         checkDate(first.getCreationDate(), second.getCreationDate(), 0.0);
269         Assertions.assertEquals(first.getDoi(),                    second.getDoi());
270         Assertions.assertEquals(first.getLicense(),                second.getLicense());
271         Assertions.assertEquals(first.getStationInformation(),     second.getStationInformation());
272         Assertions.assertEquals(first.getMarkerName(),             second.getMarkerName());
273         Assertions.assertEquals(first.getMarkerNumber(),           second.getMarkerNumber());
274         Assertions.assertEquals(first.getObserverName(),           second.getObserverName());
275         Assertions.assertEquals(first.getAgencyName(),             second.getAgencyName());
276         Assertions.assertEquals(first.getReceiverNumber(),         second.getReceiverNumber());
277         Assertions.assertEquals(first.getReceiverType(),           second.getReceiverType());
278         Assertions.assertEquals(first.getReceiverVersion(),        second.getReceiverVersion());
279         Assertions.assertEquals(first.getAntennaNumber(),          second.getAntennaNumber());
280         Assertions.assertEquals(first.getAntennaType(),            second.getAntennaType());
281         checkVector(first.getApproxPos(), second.getApproxPos());
282         Assertions.assertEquals(first.getAntennaHeight(),          second.getAntennaHeight(),         1.0e-12);
283         Assertions.assertEquals(first.getEccentricities().getX(),  second.getEccentricities().getX(), 1.0e-12);
284         Assertions.assertEquals(first.getEccentricities().getY(),  second.getEccentricities().getY(), 1.0e-12);
285         Assertions.assertEquals(first.getClockOffsetApplied(),     second.getClockOffsetApplied());
286         Assertions.assertEquals(first.getInterval(),               second.getInterval(),              1.0e-12);
287         checkDate(first.getTFirstObs(), second.getTFirstObs(), expectedDt);
288         checkDate(first.getTLastObs(),  second.getTLastObs(), expectedDt);
289         Assertions.assertEquals(first.getLeapSeconds(),            second.getLeapSeconds());
290         Assertions.assertEquals(first.getMarkerType(),             second.getMarkerType());
291         checkVector(first.getAntennaReferencePoint(),              second.getAntennaReferencePoint());
292         Assertions.assertEquals(first.getObservationCode(),        second.getObservationCode());
293         checkVector(first.getAntennaPhaseCenter(),                 second.getAntennaPhaseCenter());
294         checkVector(first.getAntennaBSight(),                      second.getAntennaBSight());
295         Assertions.assertEquals(first.getAntennaAzimuth(),         second.getAntennaAzimuth(), 1.0e-12);
296         checkVector(first.getAntennaZeroDirection(),               second.getAntennaZeroDirection());
297         checkVector(first.getCenterMass(),                         second.getCenterMass());
298         Assertions.assertEquals(first.getSignalStrengthUnit(),     second.getSignalStrengthUnit());
299         Assertions.assertEquals(first.getLeapSecondsFuture(),      second.getLeapSecondsFuture());
300         Assertions.assertEquals(first.getLeapSecondsWeekNum(),     second.getLeapSecondsWeekNum());
301         Assertions.assertEquals(first.getLeapSecondsDayNum(),      second.getLeapSecondsDayNum());
302         Assertions.assertEquals(first.getListAppliedDCBS().size(), second.getListAppliedDCBS().size());
303         for (int i = 0; i < first.getListAppliedDCBS().size(); ++i) {
304             checkDCB(first.getListAppliedDCBS().get(i), second.getListAppliedDCBS().get(i));
305         }
306         Assertions.assertEquals(first.getListAppliedPCVS().size(), second.getListAppliedPCVS().size());
307         for (int i = 0; i < first.getListAppliedPCVS().size(); ++i) {
308             checkPCV(first.getListAppliedPCVS().get(i), second.getListAppliedPCVS().get(i));
309         }
310         Assertions.assertEquals(first.getPhaseShiftCorrections().size(), second.getPhaseShiftCorrections().size());
311         for (int i = 0; i < first.getPhaseShiftCorrections().size(); ++i) {
312             checkPhaseShiftCorrection(first.getPhaseShiftCorrections().get(i), second.getPhaseShiftCorrections().get(i));
313         }
314         for (SatelliteSystem system : SatelliteSystem.values()) {
315             Assertions.assertEquals(first.getScaleFactorCorrections(system).size(), second.getScaleFactorCorrections(system).size());
316             for (int i = 0; i < first.getScaleFactorCorrections(system).size(); ++i) {
317                 checkScaleFactorCorrection(first.getScaleFactorCorrections(system).get(i),
318                                            second.getScaleFactorCorrections(system).get(i));
319             }
320         }
321         Assertions.assertEquals(first.getGlonassChannels().size(), second.getGlonassChannels().size());
322         for (int i = 0; i < first.getGlonassChannels().size(); ++i) {
323             checkGlonassChannel(first.getGlonassChannels().get(i), second.getGlonassChannels().get(i));
324         }
325         Assertions.assertEquals(first.getNbSat(),      second.getNbSat());
326         Assertions.assertEquals(first.getNbObsPerSat().size(), second.getNbObsPerSat().size());
327         for (final Map.Entry<SatInSystem, Map<ObservationType, Integer>> firstE : first.getNbObsPerSat().entrySet()) {
328             Map<ObservationType, Integer> firstV  = firstE.getValue();
329             Map<ObservationType, Integer> secondV = second.getNbObsPerSat().get(firstE.getKey());
330             Assertions.assertEquals(firstV.size(), secondV.size());
331             for (final Map.Entry<ObservationType, Integer> firstF : firstV.entrySet()) {
332                 Assertions.assertEquals(firstF.getValue(), secondV.get(firstF.getKey()));
333             }
334         }
335         Assertions.assertEquals(first.getTypeObs().size(), second.getTypeObs().size());
336         for (final Map.Entry<SatelliteSystem, List<ObservationType>> firstE : first.getTypeObs().entrySet()) {
337             List<ObservationType> firstT  = firstE.getValue();
338             List<ObservationType> secondT = second.getTypeObs().get(firstE.getKey());
339             Assertions.assertEquals(firstT.size(), secondT.size());
340             for (int i = 0; i < firstT.size(); ++i) {
341                 Assertions.assertEquals(firstT.get(i), secondT.get(i));
342             }
343         }
344         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC1cCodePhaseBias(), second.getC1cCodePhaseBias(), 1.0e-12));
345         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC1pCodePhaseBias(), second.getC1pCodePhaseBias(), 1.0e-12));
346         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC2cCodePhaseBias(), second.getC2cCodePhaseBias(), 1.0e-12));
347         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getC2pCodePhaseBias(), second.getC2pCodePhaseBias(), 1.0e-12));
348 
349     }
350 
351     private void checkRinexComments(final RinexComment first, final RinexComment second) {
352         Assertions.assertEquals(first.getLineNumber(), second.getLineNumber());
353         Assertions.assertEquals(first.getText(),       second.getText());
354     }
355 
356     private void checkRinexObs(final ObservationDataSet first, final ObservationDataSet second,
357                                final double expectedDt) {
358         Assertions.assertEquals(first.getSatellite().getSystem(), second.getSatellite().getSystem());
359         Assertions.assertEquals(first.getSatellite().getPRN(),    second.getSatellite().getPRN());
360         checkDate(first.getDate(), second.getDate(), expectedDt);
361         Assertions.assertEquals(first.getEventFlag(),           second.getEventFlag());
362         Assertions.assertEquals(first.getObservationData().size(), second.getObservationData().size());
363         for (int i = 0; i < first.getObservationData().size(); ++i) {
364             final ObservationData firstO  = first.getObservationData().get(i);
365             final ObservationData secondO = second.getObservationData().get(i);
366             Assertions.assertEquals(firstO.getValue(),               secondO.getValue(), 1.0e-12);
367             Assertions.assertEquals(firstO.getLossOfLockIndicator(), secondO.getLossOfLockIndicator());
368             Assertions.assertEquals(firstO.getSignalStrength(),      secondO.getSignalStrength());
369         }
370         Assertions.assertTrue(Precision.equalsIncludingNaN(first.getRcvrClkOffset(), second.getRcvrClkOffset(), 1.0e-12));
371     }
372 
373     private void checkDCB(final AppliedDCBS first, final AppliedDCBS second) {
374         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
375         Assertions.assertEquals(first.getProgDCBS(),        second.getProgDCBS());
376         Assertions.assertEquals(first.getSourceDCBS(),      second.getSourceDCBS());
377     }
378 
379     private void checkPCV(final AppliedPCVS first, final AppliedPCVS second) {
380         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
381         Assertions.assertEquals(first.getProgPCVS(),        second.getProgPCVS());
382         Assertions.assertEquals(first.getSourcePCVS(),      second.getSourcePCVS());
383     }
384 
385     private void checkPhaseShiftCorrection(final PhaseShiftCorrection first, final PhaseShiftCorrection second) {
386         Assertions.assertEquals(first.getSatelliteSystem(), second.getSatelliteSystem());
387         Assertions.assertEquals(first.getTypeObs(),         second.getTypeObs());
388         Assertions.assertEquals(first.getCorrection(),      second.getCorrection(), 1.0e-12);
389         Assertions.assertEquals(first.getSatsCorrected().size(), second.getSatsCorrected().size());
390         for (int i = 0; i < first.getSatsCorrected().size(); ++i) {
391             Assertions.assertEquals(first.getSatsCorrected().get(i).getSystem(), second.getSatsCorrected().get(i).getSystem());
392             Assertions.assertEquals(first.getSatsCorrected().get(i).getPRN(),    second.getSatsCorrected().get(i).getPRN());
393         }
394     }
395 
396     private void checkScaleFactorCorrection(final ScaleFactorCorrection first, final ScaleFactorCorrection second) {
397         Assertions.assertEquals(first.getCorrection(),      second.getCorrection(), 1.0e-12);
398         Assertions.assertEquals(first.getTypesObsScaled().size(), second.getTypesObsScaled().size());
399         for (int i = 0; i < first.getTypesObsScaled().size(); ++i) {
400             Assertions.assertEquals(first.getTypesObsScaled().get(i), second.getTypesObsScaled().get(i));
401         }
402     }
403 
404     private void checkGlonassChannel(final GlonassSatelliteChannel first, final GlonassSatelliteChannel second) {
405         Assertions.assertEquals(first.getSatellite().getSystem(), second.getSatellite().getSystem());
406         Assertions.assertEquals(first.getSatellite().getPRN(),    second.getSatellite().getPRN());
407         Assertions.assertEquals(first.getK(),                     second.getK());
408     }
409 
410     private void checkDate(final AbsoluteDate first, final AbsoluteDate second,
411                            final double expectedDt) {
412         if (first == null) {
413             Assertions.assertNull(second);
414         } else if (Double.isInfinite(first.durationFrom(AbsoluteDate.ARBITRARY_EPOCH))) {
415             Assertions.assertEquals(first, second);
416         } else {
417             Assertions.assertEquals(expectedDt, second.durationFrom(first), 1.0e-6);
418         }
419     }
420 
421     private void checkVector(final Vector3D first, final Vector3D second) {
422         if (first == null) {
423             Assertions.assertNull(second);
424         } else {
425             Assertions.assertEquals(0.0, Vector3D.distance(first, second), 1.0e-12 * first.getNorm());
426         }
427     }
428 
429 }