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.geometry.euclidean.threed.Vector3D;
  28. import org.hipparchus.util.FastMath;
  29. import org.hipparchus.util.Precision;
  30. import org.orekit.errors.OrekitException;
  31. import org.orekit.errors.OrekitMessages;
  32. import org.orekit.files.general.EphemerisFile;
  33. import org.orekit.frames.Frame;
  34. import org.orekit.frames.Transform;
  35. import org.orekit.gnss.IGSUtils;
  36. import org.orekit.time.AbsoluteDate;
  37. import org.orekit.time.ChronologicalComparator;
  38. import org.orekit.utils.PVCoordinates;

  39. /**
  40.  * Represents a parsed SP3 orbit file.
  41.  * @author Thomas Neidhart
  42.  * @author Evan Ward
  43.  */
  44. public class SP3 implements EphemerisFile<SP3Coordinate, SP3Segment> {

  45.     /** Header.
  46.      * @since 12.0
  47.      */
  48.     private final SP3Header header;

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

  51.     /** Number of samples to use when interpolating. */
  52.     private final int interpolationSamples;

  53.     /** Reference frame. */
  54.     private final Frame frame;

  55.     /** A map containing satellite information. */
  56.     private final Map<String, SP3Ephemeris> satellites;

  57.     /**
  58.      * Create a new SP3 file object.
  59.      *
  60.      * @param mu                   is the standard gravitational parameter in m³ / s².
  61.      * @param interpolationSamples number of samples to use in interpolation.
  62.      * @param frame                reference frame
  63.      */
  64.     public SP3(final double mu, final int interpolationSamples, final Frame frame) {
  65.         this(new SP3Header(), mu, interpolationSamples, frame);
  66.     }

  67.     /**
  68.      * Create a new SP3 file object.
  69.      *
  70.      * @param header header
  71.      * @param mu                   is the standard gravitational parameter in m³ / s².
  72.      * @param interpolationSamples number of samples to use in interpolation.
  73.      * @param frame                reference frame
  74.      * @since 12.1
  75.      */
  76.     public SP3(final SP3Header header,
  77.                final double mu, final int interpolationSamples, final Frame frame) {
  78.         this.header               = header;
  79.         this.mu                   = mu;
  80.         this.interpolationSamples = interpolationSamples;
  81.         this.frame                = frame;
  82.         this.satellites           = new LinkedHashMap<>(); // must be linked hash map to preserve order of satellites in the file
  83.     }

  84.     /** Check file is valid.
  85.      * @param parsing if true, we are parsing an existing file, and are more lenient
  86.      * in order to accept some common errors (like between 86 and 99 satellites
  87.      * in SP3a, SP3b or SP3c files)
  88.      * @param fileName file name to generate the error message
  89.      * @exception OrekitException if file is not valid
  90.      */
  91.     public void validate(final boolean parsing, final String fileName) throws OrekitException {

  92.         // check available data
  93.         final SortedSet<AbsoluteDate> epochs = new TreeSet<>(new ChronologicalComparator());
  94.         boolean hasAccuracy = false;
  95.         for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
  96.             SP3Coordinate previous = null;
  97.             for (final SP3Segment segment : entry.getValue().getSegments()) {
  98.                 for (final SP3Coordinate coordinate : segment.getCoordinates()) {
  99.                     final AbsoluteDate previousDate = previous == null ? header.getEpoch() : previous.getDate();
  100.                     final double       nbSteps      = coordinate.getDate().durationFrom(previousDate) / header.getEpochInterval();
  101.                     if (FastMath.abs(nbSteps - FastMath.rint(nbSteps)) > 0.001) {
  102.                         // not an integral number of steps
  103.                         throw new OrekitException(OrekitMessages.INCONSISTENT_SAMPLING_DATE,
  104.                                                   previousDate.shiftedBy(FastMath.rint(nbSteps) * header.getEpochInterval()),
  105.                                                   coordinate.getDate());
  106.                     }
  107.                     epochs.add(coordinate.getDate());
  108.                     previous = coordinate;
  109.                     hasAccuracy |= !(coordinate.getPositionAccuracy() == null &&
  110.                                     coordinate.getVelocityAccuracy() == null &&
  111.                                     Double.isNaN(coordinate.getClockAccuracy()) &&
  112.                                     Double.isNaN(coordinate.getClockRateAccuracy()));
  113.                 }
  114.             }
  115.         }

  116.         // check versions limitations
  117.         if (getSatelliteCount() > getMaxAllowedSatCount(parsing)) {
  118.             throw new OrekitException(OrekitMessages.SP3_TOO_MANY_SATELLITES_FOR_VERSION,
  119.                                       header.getVersion(), getMaxAllowedSatCount(parsing), getSatelliteCount(),
  120.                                       fileName);
  121.         }

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

  123.         // check epochs
  124.         if (epochs.size() != header.getNumberOfEpochs()) {
  125.             throw new OrekitException(OrekitMessages.SP3_NUMBER_OF_EPOCH_MISMATCH,
  126.                                       epochs.size(), fileName, header.getNumberOfEpochs());
  127.         }

  128.     }

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

  177.         // sort the files
  178.         final ChronologicalComparator comparator = new ChronologicalComparator();
  179.         final SortedSet<SP3> sorted = new TreeSet<>((s1, s2) -> comparator.compare(s1.header.getEpoch(), s2.header.getEpoch()));
  180.         sorted.addAll(sp3);

  181.         // prepare spliced file
  182.         final SP3 first   = sorted.first();
  183.         final SP3 spliced = new SP3(first.mu, first.interpolationSamples, first.frame);
  184.         spliced.header.setVersion(first.header.getVersion());
  185.         spliced.header.setFilter(first.header.getFilter());
  186.         spliced.header.setType(first.header.getType());
  187.         spliced.header.setTimeSystem(first.header.getTimeSystem());
  188.         spliced.header.setDataUsed(first.header.getDataUsed());
  189.         spliced.header.setEpoch(first.header.getEpoch());
  190.         spliced.header.setGpsWeek(first.header.getGpsWeek());
  191.         spliced.header.setSecondsOfWeek(first.header.getSecondsOfWeek());
  192.         spliced.header.setModifiedJulianDay(first.header.getModifiedJulianDay());
  193.         spliced.header.setDayFraction(first.header.getDayFraction());
  194.         spliced.header.setEpochInterval(first.header.getEpochInterval());
  195.         spliced.header.setCoordinateSystem(first.header.getCoordinateSystem());
  196.         spliced.header.setOrbitTypeKey(first.header.getOrbitTypeKey());
  197.         spliced.header.setAgency(first.header.getAgency());
  198.         spliced.header.setPosVelBase(first.header.getPosVelBase());
  199.         spliced.header.setClockBase(first.header.getClockBase());
  200.         first.header.getComments().forEach(spliced.header::addComment);

  201.         // identify the satellites that are present in all files
  202.         final List<String> commonSats = new ArrayList<>(first.header.getSatIds());
  203.         for (final SP3 current : sorted) {
  204.             for (final Iterator<String> iter = commonSats.iterator(); iter.hasNext();) {
  205.                 final String sat = iter.next();
  206.                 if (!current.containsSatellite(sat)) {
  207.                     iter.remove();
  208.                     break;
  209.                 }
  210.             }
  211.         }

  212.         // create the spliced list
  213.         for (final String sat : commonSats) {
  214.             spliced.addSatellite(sat);
  215.         }

  216.         // in order to be conservative, we keep the worst accuracy from all SP3 files for this satellite
  217.         for (int i = 0; i < commonSats.size(); ++i) {
  218.             final String sat = commonSats.get(i);
  219.             double accuracy = 0;
  220.             for (final SP3 current : sorted) {
  221.                 accuracy = FastMath.max(accuracy, current.header.getAccuracy(sat));
  222.             }
  223.             spliced.header.setAccuracy(i, accuracy);
  224.         }

  225.         // splice files
  226.         SP3 previous = null;
  227.         int epochCount = 0;
  228.         for (final SP3 current : sorted) {

  229.             epochCount += current.header.getNumberOfEpochs();
  230.             if (previous != null) {

  231.                 // check metadata and check if we should drop the last entry of previous file
  232.                 final boolean dropLast = current.checkSplice(previous);
  233.                 if (dropLast) {
  234.                     --epochCount;
  235.                 }

  236.                 // append the pending data from previous file
  237.                 for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  238.                     if (commonSats.contains(entry.getKey())) {
  239.                         final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
  240.                         for (final SP3Segment segment : entry.getValue().getSegments()) {
  241.                             final List<SP3Coordinate> coordinates = segment.getCoordinates();
  242.                             for (int i = 0; i < coordinates.size() - (dropLast ? 1 : 0); ++i) {
  243.                                 splicedEphemeris.addCoordinate(coordinates.get(i), spliced.header.getEpochInterval());
  244.                             }
  245.                         }
  246.                     }
  247.                 }

  248.             }

  249.             previous = current;

  250.         }
  251.         spliced.header.setNumberOfEpochs(epochCount);

  252.         // append the pending data from last file
  253.         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  254.             if (commonSats.contains(entry.getKey())) {
  255.                 final SP3Ephemeris splicedEphemeris = spliced.getEphemeris(entry.getKey());
  256.                 for (final SP3Segment segment : entry.getValue().getSegments()) {
  257.                     for (final SP3Coordinate coordinate : segment.getCoordinates()) {
  258.                         splicedEphemeris.addCoordinate(coordinate, spliced.header.getEpochInterval());
  259.                     }
  260.                 }
  261.             }
  262.         }

  263.         return spliced;

  264.     }

  265.     /** Change the frame of an SP3 file.
  266.      * @param original original SP3 file
  267.      * @param newFrame frame to use for the changed SP3 file
  268.      * @return changed SP3 file
  269.      * @since 12.1
  270.      */
  271.     public static SP3 changeFrame(final SP3 original, final Frame newFrame) {

  272.         // copy header
  273.         final SP3Header header         = new SP3Header();
  274.         final SP3Header originalHeader = original.header;
  275.         header.setVersion(originalHeader.getVersion());
  276.         header.setFilter(originalHeader.getFilter());
  277.         header.setType(originalHeader.getType());
  278.         header.setTimeSystem(originalHeader.getTimeSystem());
  279.         header.setDataUsed(originalHeader.getDataUsed());
  280.         header.setEpoch(originalHeader.getEpoch());
  281.         header.setGpsWeek(originalHeader.getGpsWeek());
  282.         header.setSecondsOfWeek(originalHeader.getSecondsOfWeek());
  283.         header.setModifiedJulianDay(originalHeader.getModifiedJulianDay());
  284.         header.setDayFraction(originalHeader.getDayFraction());
  285.         header.setEpochInterval(originalHeader.getEpochInterval());
  286.         header.setOrbitTypeKey(originalHeader.getOrbitTypeKey());
  287.         header.setAgency(originalHeader.getAgency());
  288.         header.setPosVelBase(originalHeader.getPosVelBase());
  289.         header.setClockBase(originalHeader.getClockBase());
  290.         header.setNumberOfEpochs(originalHeader.getNumberOfEpochs());

  291.         originalHeader.getComments().forEach(header::addComment);

  292.         // change the frame in the header
  293.         header.setCoordinateSystem(IGSUtils.frameName(newFrame));

  294.         // prepare file
  295.         final SP3 changed = new SP3(header,
  296.                                     original.mu,
  297.                                     original.interpolationSamples,
  298.                                     newFrame);

  299.         // first loop to add satellite ids only
  300.         final List<String> ids = originalHeader.getSatIds();
  301.         ids.forEach(changed::addSatellite);

  302.         // now that the header knows the number of satellites,
  303.         // it can lazily allocate the accuracies array,
  304.         // so we can perform a second loop to set individual accuracies
  305.         for (int i = 0; i < ids.size(); ++i) {
  306.             header.setAccuracy(i, originalHeader.getAccuracy(ids.get(i)));
  307.         }

  308.         // convert data to new frame
  309.         for (final Map.Entry<String, SP3Ephemeris> entry : original.satellites.entrySet()) {
  310.             final SP3Ephemeris originalEphemeris = original.getEphemeris(entry.getKey());
  311.             final SP3Ephemeris changedEphemeris  = changed.getEphemeris(entry.getKey());
  312.             for (final SP3Segment segment : originalEphemeris.getSegments()) {
  313.                 for (SP3Coordinate c : segment.getCoordinates()) {
  314.                     final Transform t = originalEphemeris.getFrame().getTransformTo(newFrame, c.getDate());
  315.                     final PVCoordinates newPV = t.transformPVCoordinates(c);
  316.                     final Vector3D      newP  = newPV.getPosition();
  317.                     final Vector3D      newPA = c.getPositionAccuracy() == null ?
  318.                                                 null :
  319.                                                 t.transformVector(c.getPositionAccuracy());
  320.                     final Vector3D      newV  = c.getVelocity() == null ? Vector3D.ZERO : newPV.getVelocity();
  321.                     final Vector3D      newVA = c.getVelocityAccuracy() == null ?
  322.                                                 null :
  323.                                                 t.transformVector(c.getVelocityAccuracy());
  324.                     final SP3Coordinate newC  = new SP3Coordinate(c.getDate(),
  325.                                                                   newP, newPA, newV, newVA,
  326.                                                                   c.getClockCorrection(),
  327.                                                                   c.getClockAccuracy(),
  328.                                                                   c.getClockRateChange(),
  329.                                                                   c.getClockRateAccuracy(),
  330.                                                                   c.hasClockEvent(),
  331.                                                                   c.hasClockPrediction(),
  332.                                                                   c.hasOrbitManeuverEvent(),
  333.                                                                   c.hasOrbitPrediction());
  334.                     changedEphemeris.addCoordinate(newC, originalHeader.getEpochInterval());
  335.                 }
  336.             }
  337.         }

  338.         return changed;

  339.     }

  340.     /** Check if instance can be spliced after previous one.
  341.      * @param previous SP3 file (should already be sorted to be before current instance), can be null
  342.      * @return true if last entry of previous file should be dropped as first entry of current file
  343.      * is at very close date and will take precedence
  344.      * @exception OrekitException if metadata are incompatible
  345.      * @since 12.0
  346.      */
  347.     private boolean checkSplice(final SP3 previous) throws OrekitException {

  348.         if (!(previous.header.getType()             == header.getType()                  &&
  349.               previous.header.getTimeSystem()       == header.getTimeSystem()            &&
  350.               previous.header.getOrbitType()        == header.getOrbitType()             &&
  351.               previous.header.getCoordinateSystem().equals(header.getCoordinateSystem()) &&
  352.               previous.header.getDataUsed().equals(header.getDataUsed())                 &&
  353.               previous.header.getAgency().equals(header.getAgency()))) {
  354.             throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_FILE_METADATA);
  355.         }

  356.         boolean dropLast = false;
  357.         for (final Map.Entry<String, SP3Ephemeris> entry : previous.satellites.entrySet()) {
  358.             final SP3Ephemeris previousEphem = entry.getValue();
  359.             final SP3Ephemeris currentEphem  = satellites.get(entry.getKey());
  360.             if (currentEphem != null) {
  361.                 if (!(previousEphem.getAvailableDerivatives()    == currentEphem.getAvailableDerivatives() &&
  362.                       previousEphem.getFrame()                   == currentEphem.getFrame()                &&
  363.                       previousEphem.getInterpolationSamples()    == currentEphem.getInterpolationSamples() &&
  364.                       Precision.equals(previousEphem.getMu(),       currentEphem.getMu(), 2))) {
  365.                     throw new OrekitException(OrekitMessages.SP3_INCOMPATIBLE_SATELLITE_MEDATADA,
  366.                                               entry.getKey());
  367.                 } else {
  368.                     final double dt = currentEphem.getStart().durationFrom(previousEphem.getStop());
  369.                     dropLast = dt < 0.001 * header.getEpochInterval();
  370.                 }
  371.             }
  372.         }

  373.         return dropLast;

  374.     }

  375.     /** Add a new satellite with a given identifier to the list of
  376.      * stored satellites.
  377.      * @param satId the satellite identifier
  378.      */
  379.     public void addSatellite(final String satId) {
  380.         header.addSatId(satId);
  381.         satellites.putIfAbsent(satId, new SP3Ephemeris(satId, mu, frame, interpolationSamples, header.getFilter()));
  382.     }

  383.     @Override
  384.     public Map<String, SP3Ephemeris> getSatellites() {
  385.         return Collections.unmodifiableMap(satellites);
  386.     }

  387.     /** Get an ephemeris.
  388.      * @param index index of the satellite
  389.      * @return satellite ephemeris
  390.      * @since 12.0
  391.      */
  392.     public SP3Ephemeris getEphemeris(final int index) {
  393.         int n = index;
  394.         for (final Map.Entry<String, SP3Ephemeris> entry : satellites.entrySet()) {
  395.             if (n == 0) {
  396.                 return entry.getValue();
  397.             }
  398.             n--;
  399.         }

  400.         // satellite not found
  401.         throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, index);

  402.     }

  403.     /** Get an ephemeris.
  404.      * @param satId satellite identifier
  405.      * @return satellite ephemeris, or null if not found
  406.      * @since 12.0
  407.      */
  408.     public SP3Ephemeris getEphemeris(final String satId) {
  409.         final SP3Ephemeris ephemeris = satellites.get(satId);
  410.         if (ephemeris == null) {
  411.             throw new OrekitException(OrekitMessages.INVALID_SATELLITE_ID, satId);
  412.         } else {
  413.             return ephemeris;
  414.         }
  415.     }

  416.     /** Get the number of satellites contained in this orbit file.
  417.      * @return the number of satellites
  418.      */
  419.     public int getSatelliteCount() {
  420.         return satellites.size();
  421.     }

  422.     /** Tests whether a satellite with the given id is contained in this orbit
  423.      * file.
  424.      * @param satId the satellite id
  425.      * @return {@code true} if the satellite is contained in the file,
  426.      *         {@code false} otherwise
  427.      */
  428.     public boolean containsSatellite(final String satId) {
  429.         return header.getSatIds().contains(satId);
  430.     }

  431. }