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.util.ArrayList;
20  import java.util.Collections;
21  import java.util.Iterator;
22  import java.util.List;
23  
24  import org.hipparchus.util.FastMath;
25  import org.orekit.errors.OrekitIllegalArgumentException;
26  import org.orekit.errors.OrekitMessages;
27  import org.orekit.files.rinex.RinexFile;
28  import org.orekit.time.AbsoluteDate;
29  import org.orekit.time.ClockOffset;
30  import org.orekit.time.SampledClockModel;
31  
32  /** Container for Rinex observation file.
33   * @author Luc Maisonobe
34   * @since 12.0
35   */
36  public class RinexObservation extends RinexFile<RinexObservationHeader> {
37  
38      /** Observations. */
39      private final List<ObservationDataSet> observations;
40  
41      /** Simple constructor.
42       */
43      public RinexObservation() {
44          super(new RinexObservationHeader());
45          this.observations = new ArrayList<>();
46      }
47  
48      /** Get an unmodifiable view of the observations.
49       * @return unmodifiable view of the observations
50       * @see #bundleByDates()
51       */
52      public List<ObservationDataSet> getObservationDataSets() {
53          return Collections.unmodifiableList(observations);
54      }
55  
56      /** Get an iterable view of observations bundled by common date.
57       * <p>
58       * The observations are the same as the ones provided by {@link #getObservationDataSets()},
59       * but instead of one single list covering the whole Rinex file, several lists
60       * are made available, all observations withing each list sharing a common date
61       * </p>
62       * @return an iterable view of observations bundled by common date
63       * @see #getObservationDataSets()
64       * @since 13.0
65       */
66      public Iterable<List<ObservationDataSet>> bundleByDates() {
67          return BundlingIterator::new;
68      }
69  
70      /** Add an observations data set.
71       * <p>
72       * Observations must be added chronologically, within header date range, and separated
73       * by an integer multiple of the {@link RinexObservationHeader#getInterval() interval}
74       * (ideally one interval, but entries at same dates and missing entries are allowed so
75       * any non-negative integer is allowed).
76       * </p>
77       * @param observationsDataSet observations data set
78       */
79      public void addObservationDataSet(final ObservationDataSet observationsDataSet) {
80  
81          final RinexObservationHeader header  = getHeader();
82          final AbsoluteDate           current = observationsDataSet.getDate();
83  
84          // check interval from previous observation
85          if (!observations.isEmpty()) {
86              final AbsoluteDate previous   = observations.get(observations.size() - 1).getDate();
87              final double       factor     = current.durationFrom(previous) / header.getInterval();
88              final double       acceptable = FastMath.max(0.0, FastMath.rint(factor));
89              if (FastMath.abs(factor - acceptable) > 0.01) {
90                  throw new OrekitIllegalArgumentException(OrekitMessages.INCONSISTENT_SAMPLING_DATE,
91                                                           previous.shiftedBy(acceptable * header.getInterval()),
92                                                           current);
93              }
94          }
95  
96          // check global range
97          final AbsoluteDate first = header.getTFirstObs();
98          final AbsoluteDate last  = header.getTLastObs();
99          if (!current.isBetweenOrEqualTo(first, last)) {
100             throw new OrekitIllegalArgumentException(OrekitMessages.OUT_OF_RANGE_DATE,
101                                                      current, first, last);
102         }
103 
104         observations.add(observationsDataSet);
105 
106     }
107 
108     /** Extract the receiver clock model.
109      * @param nbInterpolationPoints number of points to use in interpolation
110      * @return extracted clock model or null if all {@link
111      * ObservationDataSet#getRcvrClkOffset() clock offsets} are zero
112      * @since 12.1
113      */
114     public SampledClockModel extractClockModel(final int nbInterpolationPoints) {
115         final List<ClockOffset> sample = new ArrayList<>();
116         boolean someNonZero = false;
117         AbsoluteDate previous = null;
118         for (final ObservationDataSet ods : observations) {
119             if (previous == null || ods.getDate().durationFrom(previous) > 0.5 * getHeader().getInterval()) {
120                 // this is a new date
121                 sample.add(new ClockOffset(ods.getDate(), ods.getRcvrClkOffset(),
122                                            Double.NaN, Double.NaN));
123                 someNonZero |= ods.getRcvrClkOffset() != 0;
124             }
125             previous = ods.getDate();
126         }
127 
128         // build a clock model only if at least some non-zero offsets have been found
129         return someNonZero ?
130                new SampledClockModel(sample, nbInterpolationPoints) :
131                null;
132 
133     }
134 
135     /** Iterator providing {@link ObservationDataSet} bundled by dates.
136      * @since 13.0
137      */
138     private class BundlingIterator implements Iterator<List<ObservationDataSet>> {
139 
140         /** Ratio for dates comparisons tolerance. */
141         private static final double RATIO = 0.01;
142 
143         /** Tolerance for dates comparisons. */
144         private final double tolerance;
145 
146         /** Index of next bundle. */
147         private int next;
148 
149         /** Build an iterator starting at first observations data set.
150          */
151         BundlingIterator() {
152             this.tolerance = RATIO * getHeader().getInterval();
153             this.next = 0;
154         }
155 
156         /** {@inheritDoc} */
157         @Override
158         public boolean hasNext() {
159             return next < observations.size();
160         }
161 
162         /** {@inheritDoc} */
163         @Override
164         public List<ObservationDataSet> next() {
165 
166             // common date for all observation data sets in this bundle
167             final AbsoluteDate bundleDate = observations.get(next).getDate();
168 
169             final int start = next;
170             while (next < observations.size() &&
171                    FastMath.abs(observations.get(next).getDate().durationFrom(bundleDate)) <= tolerance) {
172                 // we can include next observation in the current bundle
173                 ++next;
174             }
175 
176             // return the bundle of observations that share the same date
177             return observations.subList(start, next);
178 
179         }
180 
181     }
182 
183 }