TLESeries.java

/* Copyright 2002-2013 CS Systèmes d'Information
 * Licensed to CS Systèmes d'Information (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.propagation.analytical.tle;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.orekit.data.DataLoader;
import org.orekit.data.DataProvidersManager;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.ChronologicalComparator;
import org.orekit.time.TimeStamped;
import org.orekit.utils.PVCoordinates;

/** This class reads and handles series of TLEs for one space object.
 *  <p>
 *  TLE data is read using the standard Orekit mechanism based on a configured
 *  {@link DataProvidersManager DataProvidersManager}. This means TLE data may
 *  be retrieved from many different storage media (local disk files, remote servers,
 *  database ...).
 *  </p>
 *  <p>
 *  This class provides bounded ephemerides by finding the best initial TLE to
 *  propagate and then handling the propagation.
 *  </p>
 *
 * @see TLE
 * @see DataProvidersManager
 * @author Fabien Maussion
 * @author Luc Maisonobe
 */
public class TLESeries implements DataLoader {

    /** Default supported files name pattern. */
    private static final String DEFAULT_SUPPORTED_NAMES = ".*\\.tle$";

    /** Regular expression for supported files names. */
    private final String supportedNames;

    /** Available satellite numbers. */
    private final Set<Integer> availableSatNums;

    /** Set containing all TLE entries. */
    private final SortedSet<TimeStamped> tles;

    /** Satellite number used for filtering. */
    private int filterSatelliteNumber;

    /** Launch year used for filtering (all digits). */
    private int filterLaunchYear;

    /** Launch number used for filtering. */
    private int filterLaunchNumber;

    /** Launch piece used for filtering. */
    private String filterLaunchPiece;

    /** Previous TLE in the cached selection. */
    private TLE previous;

    /** Next TLE in the cached selection. */
    private TLE next;

    /** Last used TLE. */
    private TLE lastTLE;

    /** Associated propagator. */
    private TLEPropagator lastPropagator;

    /** Date of the first TLE. */
    private AbsoluteDate firstDate;

    /** Date of the last TLE. */
    private AbsoluteDate lastDate;

    /** Indicator for non-TLE extra lines. */
    private final boolean ignoreNonTLELines;

    /** Simple constructor with a TLE file.
     * <p>This constructor does not load any data by itself. Data must be
     * loaded later on by calling one of the {@link #loadTLEData()
     * loadTLEData()} method, the {@link #loadTLEData(int)
     * loadTLEData(filterSatelliteNumber)} method or the {@link #loadTLEData(int,
     * int, String) loadTLEData(filterLaunchYear, filterLaunchNumber, filterLaunchPiece)} method.<p>
     * @param supportedNames regular expression for supported files names
     * (if null, a default pattern matching files with a ".tle" extension will be used)
     * @param ignoreNonTLELines if true, extra non-TLE lines are silently ignored,
     * if false an exception will be generated when such lines are encountered
     * @see #loadTLEData()
     * @see #loadTLEData(int)
     * @see #loadTLEData(int, int, String)
     */
    public TLESeries(final String supportedNames, final boolean ignoreNonTLELines) {

        this.supportedNames    = (supportedNames == null) ? DEFAULT_SUPPORTED_NAMES : supportedNames;
        availableSatNums       = new TreeSet<Integer>();
        this.ignoreNonTLELines = ignoreNonTLELines;
        filterSatelliteNumber  = -1;
        filterLaunchYear       = -1;
        filterLaunchNumber     = -1;
        filterLaunchPiece      = null;

        tles     = new TreeSet<TimeStamped>(new ChronologicalComparator());
        previous = null;
        next     = null;

    }

    /** Load TLE data for a specified object.
     * <p>The TLE data already loaded in the instance will be discarded
     * and replaced by the newly loaded data.</p>
     * <p>The filtering values will be automatically set to the first loaded
     * satellite. This feature is useful when the satellite selection is
     * already set up by either the instance configuration (supported file
     * names) or by the {@link DataProvidersManager data providers manager}
     * configuration and the local filtering feature provided here can be ignored.</p>
     * @exception OrekitException if some data can't be read, some
     * file content is corrupted or no TLE data is available
     * @see #loadTLEData(int)
     * @see #loadTLEData(int, int, String)
     */
    public void loadTLEData() throws OrekitException {

        availableSatNums.clear();

        // set the filtering parameters
        filterSatelliteNumber = -1;
        filterLaunchYear      = -1;
        filterLaunchNumber    = -1;
        filterLaunchPiece     = null;

        // load the data from the configured data providers
        tles.clear();
        DataProvidersManager.getInstance().feed(supportedNames, this);
        if (tles.isEmpty()) {
            throw new OrekitException(OrekitMessages.NO_TLE_DATA_AVAILABLE);
        }

    }

    /** Get the available satellite numbers.
     * @return available satellite numbers
     * @throws OrekitException if some data can't be read, some
     * file content is corrupted or no TLE data is available
     */
    public Set<Integer> getAvailableSatelliteNumbers() throws OrekitException {
        if (availableSatNums.isEmpty()) {
            loadTLEData();
        }
        return availableSatNums;
    }

    /** Load TLE data for a specified object.
     * <p>The TLE data already loaded in the instance will be discarded
     * and replaced by the newly loaded data.</p>
     * <p>Calling this method with the satellite number set to a negative value,
     * is equivalent to call {@link #loadTLEData()}.</p>
     * @param satelliteNumber satellite number
     * @exception OrekitException if some data can't be read, some
     * file content is corrupted or no TLE data is available for the selected object
     * @see #loadTLEData()
     * @see #loadTLEData(int, int, String)
     */
    public void loadTLEData(final int satelliteNumber) throws OrekitException {

        if (satelliteNumber < 0) {
            // no filtering at all
            loadTLEData();
        } else {
            // set the filtering parameters
            filterSatelliteNumber = satelliteNumber;
            filterLaunchYear      = -1;
            filterLaunchNumber    = -1;
            filterLaunchPiece     = null;

            // load the data from the configured data providers
            tles.clear();
            DataProvidersManager.getInstance().feed(supportedNames, this);
            if (tles.isEmpty()) {
                throw new OrekitException(OrekitMessages.NO_TLE_FOR_OBJECT, satelliteNumber);
            }
        }

    }

    /** Load TLE data for a specified object.
     * <p>The TLE data already loaded in the instance will be discarded
     * and replaced by the newly loaded data.</p>
     * <p>Calling this method with either the launch year or the launch number
     * set to a negative value, or the launch piece set to null or an empty
     * string are all equivalent to call {@link #loadTLEData()}.</p>
     * @param launchYear launch year (all digits)
     * @param launchNumber launch number
     * @param launchPiece launch piece
     * @exception OrekitException if some data can't be read, some
     * file content is corrupted or no TLE data is available for the selected object
     * @see #loadTLEData()
     * @see #loadTLEData(int)
     */
    public void loadTLEData(final int launchYear, final int launchNumber,
                            final String launchPiece) throws OrekitException {

        if ((launchYear < 0) || (launchNumber < 0) ||
            (launchPiece == null) || (launchPiece.length() == 0)) {
            // no filtering at all
            loadTLEData();
        } else {
            // set the filtering parameters
            filterSatelliteNumber = -1;
            filterLaunchYear      = launchYear;
            filterLaunchNumber    = launchNumber;
            filterLaunchPiece     = launchPiece;

            // load the data from the configured data providers
            tles.clear();
            DataProvidersManager.getInstance().feed(supportedNames, this);
            if (tles.isEmpty()) {
                throw new OrekitException(OrekitMessages.NO_TLE_FOR_LAUNCH_YEAR_NUMBER_PIECE,
                                          launchYear, launchNumber, launchPiece);
            }
        }

    }

    /** {@inheritDoc} */
    public boolean stillAcceptsData() {
        return tles.isEmpty();
    }

    /** {@inheritDoc} */
    public void loadData(final InputStream input, final String name)
        throws IOException, OrekitException {

        final BufferedReader r = new BufferedReader(new InputStreamReader(input, "UTF-8"));
        try {

            int lineNumber     = 0;
            String pendingLine = null;
            for (String line = r.readLine(); line != null; line = r.readLine()) {

                ++lineNumber;

                if (pendingLine == null) {

                    // we must wait for the second line
                    pendingLine = line;

                } else {

                    // safety checks
                    if (!TLE.isFormatOK(pendingLine, line)) {
                        if (ignoreNonTLELines) {
                            // just shift one line
                            pendingLine = line;
                            continue;
                        } else {
                            throw new OrekitException(OrekitMessages.NOT_TLE_LINES,
                                                      lineNumber - 1, lineNumber, pendingLine, line);
                        }
                    }

                    final TLE tle = new TLE(pendingLine, line);

                    if (filterSatelliteNumber < 0) {
                        if ((filterLaunchYear < 0) ||
                            ((tle.getLaunchYear()   == filterLaunchYear) &&
                             (tle.getLaunchNumber() == filterLaunchNumber) &&
                             tle.getLaunchPiece().equals(filterLaunchPiece))) {
                            // we now know the number of the object to load
                            filterSatelliteNumber = tle.getSatelliteNumber();
                        }
                    }

                    availableSatNums.add(tle.getSatelliteNumber());

                    if (tle.getSatelliteNumber() == filterSatelliteNumber) {
                        // accept this TLE
                        tles.add(tle);
                    }

                    // we need to wait for two new lines
                    pendingLine = null;

                }

            }

            if ((pendingLine != null) && !ignoreNonTLELines) {
                // there is an unexpected last line
                throw new OrekitException(OrekitMessages.MISSING_SECOND_TLE_LINE,
                                          lineNumber, pendingLine);
            }

        } finally {
            r.close();
        }

    }

    /** Get the extrapolated position and velocity from an initial date.
     * For a good precision, this date should not be too far from the range :
     * [{@link #getFirstDate() first date} ; {@link #getLastDate() last date}].
     * @param date the final date
     * @return the final PVCoordinates
     * @exception OrekitException if the underlying propagator cannot be initialized
     */
    public PVCoordinates getPVCoordinates(final AbsoluteDate date)
        throws OrekitException {
        final TLE toExtrapolate = getClosestTLE(date);
        if (toExtrapolate != lastTLE) {
            lastTLE = toExtrapolate;
            lastPropagator = TLEPropagator.selectExtrapolator(lastTLE);
        }
        return lastPropagator.getPVCoordinates(date);
    }

    /** Get the closest TLE to the selected date.
     * @param date the date
     * @return the TLE that will suit the most for propagation.
     */
    public TLE getClosestTLE(final AbsoluteDate date) {

        //  don't search if the cached selection is fine
        if ((previous != null) && (date.durationFrom(previous.getDate()) >= 0) &&
            (next     != null) && (date.durationFrom(next.getDate())     <= 0)) {
            // the current selection is already good
            if (next.getDate().durationFrom(date) > date.durationFrom(previous.getDate())) {
                return previous;
            } else {
                return next;
            }
        }
        // reset the selection before the search phase
        previous  = null;
        next      = null;
        final SortedSet<TimeStamped> headSet = tles.headSet(date);
        final SortedSet<TimeStamped> tailSet = tles.tailSet(date);


        if (headSet.isEmpty()) {
            return (TLE) tailSet.first();
        }
        if (tailSet.isEmpty()) {
            return (TLE) headSet.last();
        }
        previous = (TLE) headSet.last();
        next = (TLE) tailSet.first();

        if (next.getDate().durationFrom(date) > date.durationFrom(previous.getDate())) {
            return previous;
        } else {
            return next;
        }
    }

    /** Get the start date of the series.
     * @return the first date
     */
    public AbsoluteDate getFirstDate() {
        if (firstDate == null) {
            firstDate = tles.first().getDate();
        }
        return firstDate;
    }

    /** Get the last date of the series.
     * @return the end date
     */
    public AbsoluteDate getLastDate() {
        if (lastDate == null) {
            lastDate = tles.last().getDate();
        }
        return lastDate;
    }

    /** Get the first TLE.
     * @return first TLE
     */
    public TLE getFirst() {
        return (TLE) tles.first();
    }

    /** Get the last TLE.
     * @return last TLE
     */
    public TLE getLast() {
        return (TLE) tles.last();
    }

}