1   /* Copyright 2002-2012 Space Applications Services
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.sp3;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.SortedSet;
27  import java.util.TreeSet;
28  
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.Precision;
31  import org.orekit.errors.OrekitException;
32  import org.orekit.errors.OrekitMessages;
33  import org.orekit.files.general.EphemerisFile;
34  import org.orekit.frames.Frame;
35  import org.orekit.time.AbsoluteDate;
36  import org.orekit.time.ChronologicalComparator;
37  
38  /**
39   * Represents a parsed SP3 orbit file.
40   * @author Thomas Neidhart
41   * @author Evan Ward
42   */
43  public class SP3 implements EphemerisFile<SP3Coordinate, SP3Segment> {
44  
45      /** Header.
46       * @since 12.0
47       */
48      private final SP3Header header;
49  
50      /** Standard gravitational parameter in m³ / s². */
51      private final double mu;
52  
53      /** Number of samples to use when interpolating. */
54      private final int interpolationSamples;
55  
56      /** Reference frame. */
57      private final Frame frame;
58  
59      /** A map containing satellite information. */
60      private Map<String, SP3Ephemeris> satellites;
61  
62      /**
63       * Create a new SP3 file object.
64       *
65       * @param mu                   is the standard gravitational parameter in m³ / s².
66       * @param interpolationSamples number of samples to use in interpolation.
67       * @param frame                reference frame
68       */
69      public SP3(final double mu, final int interpolationSamples, final Frame frame) {
70          this.header               = new SP3Header();
71          this.mu                   = mu;
72          this.interpolationSamples = interpolationSamples;
73          this.frame                = frame;
74          this.satellites           = new LinkedHashMap<>(); // must be linked hash map to preserve order of satellites in the file
75      }
76  
77      /** Check file is valid.
78       * @param parsing if true, we are parsing an existing file, and are more lenient
79       * in order to accept some common errors (like between 86 and 99 satellites
80       * in SP3a, SP3b or SP3c files)
81       * @param fileName file name to generate the error message
82       * @exception OrekitException if file is not valid
83       */
84      public void validate(final boolean parsing, final String fileName) throws OrekitException {
85  
86          // check available data
87          final SortedSet<AbsoluteDate> epochs = new TreeSet<>(new ChronologicalComparator());
88          boolean hasAccuracy = false;
89          for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
90              SP3Coordinate previous = null;
91              for (final SP3Segment segment : entry.getValue().getSegments()) {
92                  for (final SP3Coordinate coordinate : segment.getCoordinates()) {
93                      final AbsoluteDate previousDate = previous == null ? header.getEpoch() : previous.getDate();
94                      final double       nbSteps      = coordinate.getDate().durationFrom(previousDate) / header.getEpochInterval();
95                      if (FastMath.abs(nbSteps - FastMath.rint(nbSteps)) > 0.001) {
96                          // not an integral number of steps
97                          throw new OrekitException(OrekitMessages.INCONSISTENT_SAMPLING_DATE,
98                                                    previousDate.shiftedBy(FastMath.rint(nbSteps) * header.getEpochInterval()),
99                                                    coordinate.getDate());
100                     }
101                     epochs.add(coordinate.getDate());
102                     previous = coordinate;
103                     hasAccuracy |= !(coordinate.getPositionAccuracy() == null &&
104                                     coordinate.getVelocityAccuracy() == null &&
105                                     Double.isNaN(coordinate.getClockAccuracy()) &&
106                                     Double.isNaN(coordinate.getClockRateAccuracy()));
107                 }
108             }
109         }
110 
111         // check versions limitations
112         if (getSatelliteCount() > getMaxAllowedSatCount(parsing)) {
113             throw new OrekitException(OrekitMessages.SP3_TOO_MANY_SATELLITES_FOR_VERSION,
114                                       header.getVersion(), getMaxAllowedSatCount(parsing), getSatelliteCount(),
115                                       fileName);
116         }
117 
118         header.validate(parsing, hasAccuracy, fileName);
119 
120         // check epochs
121         if (epochs.size() != header.getNumberOfEpochs()) {
122             throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
123                                       epochs.size(), fileName, header.getNumberOfEpochs());
124         }
125 
126     }
127 
128     /** Get the header.
129      * @return header
130      * @since 12.0
131      */
132     public SP3Header getHeader() {
133         return header;
134     }
135 
136     /** Get maximum number of satellites allowed for format version.
137      * @param parsing if true, we are parsing an existing file, and are more lenient
138      * in order to accept some common errors (like between 86 and 99 satellites
139      * in SP3a, SP3b or SP3c files)
140      * @return maximum number of satellites allowed for format version
141      * @since 12.0
142      */
143     private int getMaxAllowedSatCount(final boolean parsing) {
144         return header.getVersion() < 'd' ? (parsing ? 99 : 85) : 999;
145     }
146 
147     /** Splice several SP3 files together.
148      * <p>
149      * Splicing SP3 files is intended to be used when continuous computation
150      * covering more than one file is needed. The files should all have the exact same
151      * metadata: {@link SP3Header#getType() type}, {@link SP3Header#getTimeSystem() time system},
152      * {@link SP3Header#getCoordinateSystem() coordinate system}, except for satellite accuracy
153      * which can be different from one file to the next one, and some satellites may
154      * be missing in some files… Once sorted (which is done internally), if the gap between
155      * segments from two file is at most {@link SP3Header#getEpochInterval() epoch interval},
156      * then the segments are merged as one segment, otherwise the segments are kept separated.
157      * </p>
158      * <p>
159      * The spliced file only contains the satellites that were present in all files.
160      * Satellites present in some files and absent from other files are silently
161      * dropped.
162      * </p>
163      * <p>
164      * Depending on producer, successive SP3 files either have a gap between the last
165      * entry of one file and the first entry of the next file (for example files with
166      * a 5 minutes epoch interval may end at 23:55 and the next file start at 00:00),
167      * or both files have one point exactly at the splicing date (i.e. 24:00 one day
168      * and 00:00 next day). In the later case, the last point of the early file is dropped
169      * and the first point of the late file takes precedence, hence only one point remains
170      * in the spliced file ; this design choice is made to enforce continuity and
171      * regular interpolation.
172      * </p>
173      * @param sp3 SP3 files to merge
174      * @return merged SP3
175      * @since 12.0
176      */
177     public static SP3 splice(final Collection<SP3> sp3) {
178 
179         // sort the files
180         final ChronologicalComparator comparator = new ChronologicalComparator();
181         final SortedSet<SP3> sorted = new TreeSet<>((s1, s2) -> comparator.compare(s1.header.getEpoch(), s2.header.getEpoch()));
182         sorted.addAll(sp3);
183 
184         // prepare spliced file
185         final SP3 first   = sorted.first();
186         final SP3 spliced = new SP3(first.mu, first.interpolationSamples, first.frame);
187         spliced.header.setFilter(first.header.getFilter());
188         spliced.header.setType(first.header.getType());
189         spliced.header.setTimeSystem(first.header.getTimeSystem());
190         spliced.header.setDataUsed(first.header.getDataUsed());
191         spliced.header.setEpoch(first.header.getEpoch());
192         spliced.header.setGpsWeek(first.header.getGpsWeek());
193         spliced.header.setSecondsOfWeek(first.header.getSecondsOfWeek());
194         spliced.header.setModifiedJulianDay(first.header.getModifiedJulianDay());
195         spliced.header.setDayFraction(first.header.getDayFraction());
196         spliced.header.setEpochInterval(first.header.getEpochInterval());
197         spliced.header.setCoordinateSystem(first.header.getCoordinateSystem());
198         spliced.header.setOrbitTypeKey(first.header.getOrbitTypeKey());
199         spliced.header.setAgency(first.header.getAgency());
200         spliced.header.setPosVelBase(first.header.getPosVelBase());
201         spliced.header.setClockBase(first.header.getClockBase());
202 
203         // identify the satellites that are present in all files
204         final List<String> commonSats = new ArrayList<>(first.header.getSatIds());
205         for (final SP3 current : sorted) {
206             for (final Iterator<String> iter = commonSats.iterator(); iter.hasNext();) {
207                 final String sat = iter.next();
208                 if (!current.containsSatellite(sat)) {
209                     iter.remove();
210                     break;
211                 }
212             }
213         }
214 
215         // create the spliced list
216         for (final String sat : commonSats) {
217             spliced.addSatellite(sat);
218         }
219 
220         // in order to be conservative, we keep the worst accuracy from all SP3 files for this satellite
221         for (int i = 0; i < commonSats.size(); ++i) {
222             final String sat = commonSats.get(i);
223             double accuracy = Double.POSITIVE_INFINITY;
224             for (final SP3 current : sorted) {
225                 accuracy = FastMath.max(accuracy, current.header.getAccuracy(sat));
226             }
227             spliced.header.setAccuracy(i, accuracy);
228         }
229 
230         // splice files
231         SP3 previous = null;
232         int epochCount = 0;
233         for (final SP3 current : sorted) {
234 
235             epochCount += current.header.getNumberOfEpochs();
236             if (previous != null) {
237 
238                 // check metadata and check if we should drop the last entry of previous file
239                 final boolean dropLast = current.checkSplice(previous);
240                 if (dropLast) {
241                     --epochCount;
242                 }
243 
244                 // append the pending data from previous file
245                 for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
246                     if (commonSats.contains(entry.getKey())) {
247                         final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
248                         for (final SP3Segment segment : entry.getValue().getSegments()) {
249                             final List<SP3Coordinate> coordinates = segment.getCoordinates();
250                             for (int i = 0; i < coordinates.size() - (dropLast ? 1 : 0); ++i) {
251                                 splicedEphemeris.addCoordinate(coordinates.get(i), spliced.header.getEpochInterval());
252                             }
253                         }
254                     }
255                 }
256 
257             }
258 
259             previous = current;
260 
261         }
262         spliced.header.setNumberOfEpochs(epochCount);
263 
264         // append the pending data from last file
265         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
266             if (commonSats.contains(entry.getKey())) {
267                 final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
268                 for (final SP3Segment segment : entry.getValue().getSegments()) {
269                     for (final SP3Coordinate coordinate : segment.getCoordinates()) {
270                         splicedEphemeris.addCoordinate(coordinate, spliced.header.getEpochInterval());
271                     }
272                 }
273             }
274         }
275 
276         return spliced;
277 
278     }
279 
280     /** Check if instance can be spliced after previous one.
281      * @param previous SP3 file (should already be sorted to be before current instance), can be null
282      * @return true if last entry of previous file should be dropped as first entry of current file
283      * is at very close date and will take precedence
284      * @exception OrekitException if metadata are incompatible
285      * @since 12.0
286      */
287     private boolean checkSplice(final SP3 previous) throws OrekitException {
288 
289         if (!(previous.header.getType()             == header.getType()                  &&
290               previous.header.getTimeSystem()       == header.getTimeSystem()            &&
291               previous.header.getOrbitType()        == header.getOrbitType()             &&
292               previous.header.getCoordinateSystem().equals(header.getCoordinateSystem()) &&
293               previous.header.getDataUsed().equals(header.getDataUsed())                 &&
294               previous.header.getAgency().equals(header.getAgency()))) {
295             throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_FILE_METADATA);
296         }
297 
298         boolean dropLast = false;
299         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
300             final SP3Ephemeris previousEphem = entry.getValue();
301             final SP3Ephemeris currentEphem  = satellites.get(entry.getKey());
302             if (currentEphem != null) {
303                 if (!(previousEphem.getAvailableDerivatives()    == currentEphem.getAvailableDerivatives() &&
304                       previousEphem.getFrame()                   == currentEphem.getFrame()                &&
305                       previousEphem.getInterpolationSamples()    == currentEphem.getInterpolationSamples() &&
306                       Precision.equals(previousEphem.getMu(),       currentEphem.getMu(), 2))) {
307                     throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_SATELLITE_MEDATADA,
308                                               entry.getKey());
309                 } else {
310                     final double dt = currentEphem.getStart().durationFrom(previousEphem.getStop());
311                     dropLast = dt < 0.001 * header.getEpochInterval();
312                 }
313             }
314         }
315 
316         return dropLast;
317 
318     }
319 
320     /** Add a new satellite with a given identifier to the list of
321      * stored satellites.
322      * @param satId the satellite identifier
323      */
324     public void addSatellite(final String satId) {
325         header.addSatId(satId);
326         satellites.putIfAbsent(satId, new SP3Ephemeris(satId, mu, frame, interpolationSamples, header.getFilter()));
327     }
328 
329     @Override
330     public Map<String, SP3Ephemeris> getSatellites() {
331         return Collections.unmodifiableMap(satellites);
332     }
333 
334     /** Get an ephemeris.
335      * @param index index of the satellite
336      * @return satellite ephemeris
337      * @since 12.0
338      */
339     public SP3Ephemeris getEphemeris(final int index) {
340         int n = index;
341         for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
342             if (n == 0) {
343                 return entry.getValue();
344             }
345             n--;
346         }
347 
348         // satellite not found
349         throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, index);
350 
351     }
352 
353     /** Get an ephemeris.
354      * @param satId satellite identifier
355      * @return satellite ephemeris, or null if not found
356      * @since 12.0
357      */
358     public SP3Ephemeris getEphemeris(final String satId) {
359         final SP3Ephemeris ephemeris = satellites.get(satId);
360         if (ephemeris == null) {
361             throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, satId);
362         } else {
363             return ephemeris;
364         }
365     }
366 
367     /** Get the number of satellites contained in this orbit file.
368      * @return the number of satellites
369      */
370     public int getSatelliteCount() {
371         return satellites.size();
372     }
373 
374     /** Tests whether a satellite with the given id is contained in this orbit
375      * file.
376      * @param satId the satellite id
377      * @return {@code true} if the satellite is contained in the file,
378      *         {@code false} otherwise
379      */
380     public boolean containsSatellite(final String satId) {
381         return header.getSatIds().contains(satId);
382     }
383 
384 }