SP3.java

  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. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.Collections;
  21. import java.util.Iterator;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.SortedSet;
  26. import java.util.TreeSet;

  27. import org.hipparchus.util.FastMath;
  28. import org.hipparchus.util.Precision;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.files.general.EphemerisFile;
  32. import org.orekit.frames.Frame;
  33. import org.orekit.time.AbsoluteDate;
  34. import org.orekit.time.ChronologicalComparator;

  35. /**
  36.  * Represents a parsed SP3 orbit file.
  37.  * @author Thomas Neidhart
  38.  * @author Evan Ward
  39.  */
  40. public class SP3 implements EphemerisFile<SP3Coordinate, SP3Segment> {

  41.     /** Header.
  42.      * @since 12.0
  43.      */
  44.     private final SP3Header header;

  45.     /** Standard gravitational parameter in m³ / s². */
  46.     private final double mu;

  47.     /** Number of samples to use when interpolating. */
  48.     private final int interpolationSamples;

  49.     /** Reference frame. */
  50.     private final Frame frame;

  51.     /** A map containing satellite information. */
  52.     private Map<String, SP3Ephemeris> satellites;

  53.     /**
  54.      * Create a new SP3 file object.
  55.      *
  56.      * @param mu                   is the standard gravitational parameter in m³ / s².
  57.      * @param interpolationSamples number of samples to use in interpolation.
  58.      * @param frame                reference frame
  59.      */
  60.     public SP3(final double mu, final int interpolationSamples, final Frame frame) {
  61.         this.header               = new SP3Header();
  62.         this.mu                   = mu;
  63.         this.interpolationSamples = interpolationSamples;
  64.         this.frame                = frame;
  65.         this.satellites           = new LinkedHashMap<>(); // must be linked hash map to preserve order of satellites in the file
  66.     }

  67.     /** Check file is valid.
  68.      * @param parsing if true, we are parsing an existing file, and are more lenient
  69.      * in order to accept some common errors (like between 86 and 99 satellites
  70.      * in SP3a, SP3b or SP3c files)
  71.      * @param fileName file name to generate the error message
  72.      * @exception OrekitException if file is not valid
  73.      */
  74.     public void validate(final boolean parsing, final String fileName) throws OrekitException {

  75.         // check available data
  76.         final SortedSet<AbsoluteDate> epochs = new TreeSet<>(new ChronologicalComparator());
  77.         boolean hasAccuracy = false;
  78.         for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
  79.             SP3Coordinate previous = null;
  80.             for (final SP3Segment segment : entry.getValue().getSegments()) {
  81.                 for (final SP3Coordinate coordinate : segment.getCoordinates()) {
  82.                     final AbsoluteDate previousDate = previous == null ? header.getEpoch() : previous.getDate();
  83.                     final double       nbSteps      = coordinate.getDate().durationFrom(previousDate) / header.getEpochInterval();
  84.                     if (FastMath.abs(nbSteps - FastMath.rint(nbSteps)) > 0.001) {
  85.                         // not an integral number of steps
  86.                         throw new OrekitException(OrekitMessages.INCONSISTENT_SAMPLING_DATE,
  87.                                                   previousDate.shiftedBy(FastMath.rint(nbSteps) * header.getEpochInterval()),
  88.                                                   coordinate.getDate());
  89.                     }
  90.                     epochs.add(coordinate.getDate());
  91.                     previous = coordinate;
  92.                     hasAccuracy |= !(coordinate.getPositionAccuracy() == null &&
  93.                                     coordinate.getVelocityAccuracy() == null &&
  94.                                     Double.isNaN(coordinate.getClockAccuracy()) &&
  95.                                     Double.isNaN(coordinate.getClockRateAccuracy()));
  96.                 }
  97.             }
  98.         }

  99.         // check versions limitations
  100.         if (getSatelliteCount() > getMaxAllowedSatCount(parsing)) {
  101.             throw new OrekitException(OrekitMessages.SP3_TOO_MANY_SATELLITES_FOR_VERSION,
  102.                                       header.getVersion(), getMaxAllowedSatCount(parsing), getSatelliteCount(),
  103.                                       fileName);
  104.         }

  105.         header.validate(parsing, hasAccuracy, fileName);

  106.         // check epochs
  107.         if (epochs.size() != header.getNumberOfEpochs()) {
  108.             throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
  109.                                       epochs.size(), fileName, header.getNumberOfEpochs());
  110.         }

  111.     }

  112.     /** Get the header.
  113.      * @return header
  114.      * @since 12.0
  115.      */
  116.     public SP3Header getHeader() {
  117.         return header;
  118.     }

  119.     /** Get maximum number of satellites allowed for format version.
  120.      * @param parsing if true, we are parsing an existing file, and are more lenient
  121.      * in order to accept some common errors (like between 86 and 99 satellites
  122.      * in SP3a, SP3b or SP3c files)
  123.      * @return maximum number of satellites allowed for format version
  124.      * @since 12.0
  125.      */
  126.     private int getMaxAllowedSatCount(final boolean parsing) {
  127.         return header.getVersion() < 'd' ? (parsing ? 99 : 85) : 999;
  128.     }

  129.     /** Splice several SP3 files together.
  130.      * <p>
  131.      * Splicing SP3 files is intended to be used when continuous computation
  132.      * covering more than one file is needed. The files should all have the exact same
  133.      * metadata: {@link SP3Header#getType() type}, {@link SP3Header#getTimeSystem() time system},
  134.      * {@link SP3Header#getCoordinateSystem() coordinate system}, except for satellite accuracy
  135.      * which can be different from one file to the next one, and some satellites may
  136.      * be missing in some files… Once sorted (which is done internally), if the gap between
  137.      * segments from two file is at most {@link SP3Header#getEpochInterval() epoch interval},
  138.      * then the segments are merged as one segment, otherwise the segments are kept separated.
  139.      * </p>
  140.      * <p>
  141.      * The spliced file only contains the satellites that were present in all files.
  142.      * Satellites present in some files and absent from other files are silently
  143.      * dropped.
  144.      * </p>
  145.      * <p>
  146.      * Depending on producer, successive SP3 files either have a gap between the last
  147.      * entry of one file and the first entry of the next file (for example files with
  148.      * a 5 minutes epoch interval may end at 23:55 and the next file start at 00:00),
  149.      * or both files have one point exactly at the splicing date (i.e. 24:00 one day
  150.      * and 00:00 next day). In the later case, the last point of the early file is dropped
  151.      * and the first point of the late file takes precedence, hence only one point remains
  152.      * in the spliced file ; this design choice is made to enforce continuity and
  153.      * regular interpolation.
  154.      * </p>
  155.      * @param sp3 SP3 files to merge
  156.      * @return merged SP3
  157.      * @since 12.0
  158.      */
  159.     public static SP3 splice(final Collection<SP3> sp3) {

  160.         // sort the files
  161.         final ChronologicalComparator comparator = new ChronologicalComparator();
  162.         final SortedSet<SP3> sorted = new TreeSet<>((s1, s2) -> comparator.compare(s1.header.getEpoch(), s2.header.getEpoch()));
  163.         sorted.addAll(sp3);

  164.         // prepare spliced file
  165.         final SP3 first   = sorted.first();
  166.         final SP3 spliced = new SP3(first.mu, first.interpolationSamples, first.frame);
  167.         spliced.header.setFilter(first.header.getFilter());
  168.         spliced.header.setType(first.header.getType());
  169.         spliced.header.setTimeSystem(first.header.getTimeSystem());
  170.         spliced.header.setDataUsed(first.header.getDataUsed());
  171.         spliced.header.setEpoch(first.header.getEpoch());
  172.         spliced.header.setGpsWeek(first.header.getGpsWeek());
  173.         spliced.header.setSecondsOfWeek(first.header.getSecondsOfWeek());
  174.         spliced.header.setModifiedJulianDay(first.header.getModifiedJulianDay());
  175.         spliced.header.setDayFraction(first.header.getDayFraction());
  176.         spliced.header.setEpochInterval(first.header.getEpochInterval());
  177.         spliced.header.setCoordinateSystem(first.header.getCoordinateSystem());
  178.         spliced.header.setOrbitTypeKey(first.header.getOrbitTypeKey());
  179.         spliced.header.setAgency(first.header.getAgency());
  180.         spliced.header.setPosVelBase(first.header.getPosVelBase());
  181.         spliced.header.setClockBase(first.header.getClockBase());

  182.         // identify the satellites that are present in all files
  183.         final List<String> commonSats = new ArrayList<>(first.header.getSatIds());
  184.         for (final SP3 current : sorted) {
  185.             for (final Iterator<String> iter = commonSats.iterator(); iter.hasNext();) {
  186.                 final String sat = iter.next();
  187.                 if (!current.containsSatellite(sat)) {
  188.                     iter.remove();
  189.                     break;
  190.                 }
  191.             }
  192.         }

  193.         // create the spliced list
  194.         for (final String sat : commonSats) {
  195.             spliced.addSatellite(sat);
  196.         }

  197.         // in order to be conservative, we keep the worst accuracy from all SP3 files for this satellite
  198.         for (int i = 0; i < commonSats.size(); ++i) {
  199.             final String sat = commonSats.get(i);
  200.             double accuracy = Double.POSITIVE_INFINITY;
  201.             for (final SP3 current : sorted) {
  202.                 accuracy = FastMath.max(accuracy, current.header.getAccuracy(sat));
  203.             }
  204.             spliced.header.setAccuracy(i, accuracy);
  205.         }

  206.         // splice files
  207.         SP3 previous = null;
  208.         int epochCount = 0;
  209.         for (final SP3 current : sorted) {

  210.             epochCount += current.header.getNumberOfEpochs();
  211.             if (previous != null) {

  212.                 // check metadata and check if we should drop the last entry of previous file
  213.                 final boolean dropLast = current.checkSplice(previous);
  214.                 if (dropLast) {
  215.                     --epochCount;
  216.                 }

  217.                 // append the pending data from previous file
  218.                 for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  219.                     if (commonSats.contains(entry.getKey())) {
  220.                         final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
  221.                         for (final SP3Segment segment : entry.getValue().getSegments()) {
  222.                             final List<SP3Coordinate> coordinates = segment.getCoordinates();
  223.                             for (int i = 0; i < coordinates.size() - (dropLast ? 1 : 0); ++i) {
  224.                                 splicedEphemeris.addCoordinate(coordinates.get(i), spliced.header.getEpochInterval());
  225.                             }
  226.                         }
  227.                     }
  228.                 }

  229.             }

  230.             previous = current;

  231.         }
  232.         spliced.header.setNumberOfEpochs(epochCount);

  233.         // append the pending data from last file
  234.         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  235.             if (commonSats.contains(entry.getKey())) {
  236.                 final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
  237.                 for (final SP3Segment segment : entry.getValue().getSegments()) {
  238.                     for (final SP3Coordinate coordinate : segment.getCoordinates()) {
  239.                         splicedEphemeris.addCoordinate(coordinate, spliced.header.getEpochInterval());
  240.                     }
  241.                 }
  242.             }
  243.         }

  244.         return spliced;

  245.     }

  246.     /** Check if instance can be spliced after previous one.
  247.      * @param previous SP3 file (should already be sorted to be before current instance), can be null
  248.      * @return true if last entry of previous file should be dropped as first entry of current file
  249.      * is at very close date and will take precedence
  250.      * @exception OrekitException if metadata are incompatible
  251.      * @since 12.0
  252.      */
  253.     private boolean checkSplice(final SP3 previous) throws OrekitException {

  254.         if (!(previous.header.getType()             == header.getType()                  &&
  255.               previous.header.getTimeSystem()       == header.getTimeSystem()            &&
  256.               previous.header.getOrbitType()        == header.getOrbitType()             &&
  257.               previous.header.getCoordinateSystem().equals(header.getCoordinateSystem()) &&
  258.               previous.header.getDataUsed().equals(header.getDataUsed())                 &&
  259.               previous.header.getAgency().equals(header.getAgency()))) {
  260.             throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_FILE_METADATA);
  261.         }

  262.         boolean dropLast = false;
  263.         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  264.             final SP3Ephemeris previousEphem = entry.getValue();
  265.             final SP3Ephemeris currentEphem  = satellites.get(entry.getKey());
  266.             if (currentEphem != null) {
  267.                 if (!(previousEphem.getAvailableDerivatives()    == currentEphem.getAvailableDerivatives() &&
  268.                       previousEphem.getFrame()                   == currentEphem.getFrame()                &&
  269.                       previousEphem.getInterpolationSamples()    == currentEphem.getInterpolationSamples() &&
  270.                       Precision.equals(previousEphem.getMu(),       currentEphem.getMu(), 2))) {
  271.                     throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_SATELLITE_MEDATADA,
  272.                                               entry.getKey());
  273.                 } else {
  274.                     final double dt = currentEphem.getStart().durationFrom(previousEphem.getStop());
  275.                     dropLast = dt < 0.001 * header.getEpochInterval();
  276.                 }
  277.             }
  278.         }

  279.         return dropLast;

  280.     }

  281.     /** Add a new satellite with a given identifier to the list of
  282.      * stored satellites.
  283.      * @param satId the satellite identifier
  284.      */
  285.     public void addSatellite(final String satId) {
  286.         header.addSatId(satId);
  287.         satellites.putIfAbsent(satId, new SP3Ephemeris(satId, mu, frame, interpolationSamples, header.getFilter()));
  288.     }

  289.     @Override
  290.     public Map<String, SP3Ephemeris> getSatellites() {
  291.         return Collections.unmodifiableMap(satellites);
  292.     }

  293.     /** Get an ephemeris.
  294.      * @param index index of the satellite
  295.      * @return satellite ephemeris
  296.      * @since 12.0
  297.      */
  298.     public SP3Ephemeris getEphemeris(final int index) {
  299.         int n = index;
  300.         for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
  301.             if (n == 0) {
  302.                 return entry.getValue();
  303.             }
  304.             n--;
  305.         }

  306.         // satellite not found
  307.         throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, index);

  308.     }

  309.     /** Get an ephemeris.
  310.      * @param satId satellite identifier
  311.      * @return satellite ephemeris, or null if not found
  312.      * @since 12.0
  313.      */
  314.     public SP3Ephemeris getEphemeris(final String satId) {
  315.         final SP3Ephemeris ephemeris = satellites.get(satId);
  316.         if (ephemeris == null) {
  317.             throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, satId);
  318.         } else {
  319.             return ephemeris;
  320.         }
  321.     }

  322.     /** Get the number of satellites contained in this orbit file.
  323.      * @return the number of satellites
  324.      */
  325.     public int getSatelliteCount() {
  326.         return satellites.size();
  327.     }

  328.     /** Tests whether a satellite with the given id is contained in this orbit
  329.      * file.
  330.      * @param satId the satellite id
  331.      * @return {@code true} if the satellite is contained in the file,
  332.      *         {@code false} otherwise
  333.      */
  334.     public boolean containsSatellite(final String satId) {
  335.         return header.getSatIds().contains(satId);
  336.     }

  337. }