ODMParser.java

/* Copyright 2002-2015 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.files.ccsds;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.math3.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.general.OrbitFile;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeScalesFactory;
import org.orekit.utils.Constants;
import org.orekit.utils.IERSConventions;

/** Base class for all CCSDS Orbit Data Message parsers.
 * <p>
 * This base class is immutable, and hence thread safe. When parts
 * must be changed, such as reference date for Mission Elapsed Time or
 * Mission Relative Time time systems, or the gravitational coefficient or
 * the IERS conventions, the various {@code withXxx} methods must be called,
 * which create a new immutable instance with the new parameters. This
 * is a combination of the <a href="">builder design pattern</a> and
 * a <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
 * interface</a>.
 * </p>
 * @author Luc Maisonobe
 * @since 6.1
 */
public abstract class ODMParser {

    /** Pattern for international designator. */
    private static final Pattern INTERNATIONAL_DESIGNATOR = Pattern.compile("(\\p{Digit}{4})-(\\p{Digit}{3})(\\p{Upper}{1,3})");

    /** Reference date for Mission Elapsed Time or Mission Relative Time time systems. */
    private final AbsoluteDate missionReferenceDate;

    /** Gravitational coefficient. */
    private final  double mu;

    /** IERS Conventions. */
    private final  IERSConventions conventions;

    /** Indicator for simple or accurate EOP interpolation. */
    private final  boolean simpleEOP;

    /** Launch Year. */
    private int launchYear;

    /** Launch number. */
    private int launchNumber;

    /** Piece of launch (from "A" to "ZZZ"). */
    private String launchPiece;

    /** Complete constructor.
     * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
     * @param mu gravitational coefficient
     * @param conventions IERS Conventions
     * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
     * @param launchYear launch year for TLEs
     * @param launchNumber launch number for TLEs
     * @param launchPiece piece of launch (from "A" to "ZZZ") for TLEs
     */
    protected ODMParser(final AbsoluteDate missionReferenceDate, final double mu,
                        final IERSConventions conventions, final boolean simpleEOP,
                        final int launchYear, final int launchNumber, final String launchPiece) {
        this.missionReferenceDate = missionReferenceDate;
        this.mu                   = mu;
        this.conventions          = conventions;
        this.simpleEOP            = simpleEOP;
        this.launchYear           = launchYear;
        this.launchNumber         = launchNumber;
        this.launchPiece          = launchPiece;
    }

    /** Set initial date.
     * @param newMissionReferenceDate mission reference date to use while parsing
     * @return a new instance, with mission reference date replaced
     * @see #getMissionReferenceDate()
     */
    public abstract ODMParser withMissionReferenceDate(final AbsoluteDate newMissionReferenceDate);

    /** Get initial date.
     * @return mission reference date to use while parsing
     * @see #withMissionReferenceDate(AbsoluteDate)
     */
    public AbsoluteDate getMissionReferenceDate() {
        return missionReferenceDate;
    }

    /** Set gravitational coefficient.
     * @param newMu gravitational coefficient to use while parsing
     * @return a new instance, with gravitational coefficient date replaced
     * @see #getMu()
     */
    public abstract ODMParser withMu(final double newMu);

    /** Get gravitational coefficient.
     * @return gravitational coefficient to use while parsing
     * @see #withMu(double)
     */
    public double getMu() {
        return mu;
    }

    /** Set IERS conventions.
     * @param newConventions IERS conventions to use while parsing
     * @return a new instance, with IERS conventions replaced
     * @see #getConventions()
     */
    public abstract ODMParser withConventions(final IERSConventions newConventions);

    /** Get IERS conventions.
     * @return IERS conventions to use while parsing
     * @see #withConventions(IERSConventions)
     */
    public IERSConventions getConventions() {
        return conventions;
    }

    /** Set EOP interpolation method.
     * @param newSimpleEOP if true, tidal effects are ignored when interpolating EOP
     * @return a new instance, with EOP interpolation method replaced
     * @see #isSimpleEOP()
     */
    public abstract ODMParser withSimpleEOP(final boolean newSimpleEOP);

    /** Get EOP interpolation method.
     * @return true if tidal effects are ignored when interpolating EOP
     * @see #withSimpleEOP(boolean)
     */
    public boolean isSimpleEOP() {
        return simpleEOP;
    }

    /** Set international designator.
     * <p>
     * This method may be used to ensure the launch year number and pieces are
     * correctly set if they are not present in the CCSDS file header in the
     * OBJECT_ID in the form YYYY-NNN-P{PP}. If they are already in the header,
     * they will be parsed automatically regardless of this method being called
     * or not (i.e. header information override information set here).
     * </p>
     * @param newLaunchYear launch year
     * @param newLaunchNumber launch number
     * @param newLaunchPiece piece of launch (from "A" to "ZZZ")
     * @return a new instance, with TLE settings replaced
     */
    public abstract ODMParser withInternationalDesignator(final int newLaunchYear,
                                                          final int newLaunchNumber,
                                                          final String newLaunchPiece);

    /** Get the launch year.
     * @return launch year
     */
    public int getLaunchYear() {
        return launchYear;
    }

    /** Get the launch number.
     * @return launch number
     */
    public int getLaunchNumber() {
        return launchNumber;
    }

    /** Get the piece of launch.
     * @return piece of launch
     */
    public String getLaunchPiece() {
        return launchPiece;
    }

    /** Parse a CCSDS Orbit Data Message.
     * @param fileName name of the file containing the message
     * @return parsed orbit
     * @exception OrekitException if orbit message cannot be parsed
     */
    public ODMFile parse(final String fileName)
        throws OrekitException {

        InputStream stream = null;

        try {
            stream = new FileInputStream(fileName);
            return parse(stream, fileName);
        } catch (FileNotFoundException e) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, fileName);
        } finally {
            try {
                if (stream != null) {
                    stream.close();
                }
            } catch (IOException e) {
                // ignore
            }
        }

    }

    /** Parse a CCSDS Orbit Data Message.
     * @param stream stream containing message
     * @return parsed orbit
     * @exception OrekitException if orbit message cannot be parsed
     */
    public ODMFile parse(final InputStream stream)
        throws OrekitException {
        return parse(stream, "<unknown>");
    }

    /** Parse a CCSDS Orbit Data Message.
     * @param stream stream containing message
     * @param fileName name of the file containing the message (for error messages)
     * @return parsed orbit
     * @exception OrekitException if orbit message cannot be parsed
     */
    public abstract ODMFile parse(final InputStream stream, final String fileName)
        throws OrekitException;

    /** Parse a comment line.
     * @param keyValue key=value pair containing the comment
     * @param comment placeholder where the current comment line should be added
     * @return true if the line was a comment line and was parsed
     */
    protected boolean parseComment(final KeyValue keyValue, final List<String> comment) {
        if (keyValue.getKeyword() == Keyword.COMMENT) {
            comment.add(keyValue.getValue());
            return true;
        } else {
            return false;
        }
    }

    /** Parse an entry from the header.
     * @param keyValue key = value pair
     * @param odmFile instance to update with parsed entry
     * @param comment previous comment lines, will be emptied if used by the keyword
     * @return true if the keyword was a header keyword and has been parsed
     * @exception OrekitException if UTC time scale cannot be retrieved to parse creation date
     */
    protected boolean parseHeaderEntry(final KeyValue keyValue,
                                       final ODMFile odmFile, final List<String> comment)
        throws OrekitException {
        switch (keyValue.getKeyword()) {

        case CREATION_DATE:
            if (!comment.isEmpty()) {
                odmFile.setHeaderComment(comment);
                comment.clear();
            }
            odmFile.setCreationDate(new AbsoluteDate(keyValue.getValue(), TimeScalesFactory.getUTC()));
            return true;

        case ORIGINATOR:
            odmFile.setOriginator(keyValue.getValue());
            return true;

        default:
            return false;

        }

    }

    /** Parse a meta-data key = value entry.
     * @param keyValue key = value pair
     * @param metaData instance to update with parsed entry
     * @param comment previous comment lines, will be emptied if used by the keyword
     * @return true if the keyword was a meta-data keyword and has been parsed
     * @exception OrekitException if center body or frame cannot be retrieved
     */
    protected boolean parseMetaDataEntry(final KeyValue keyValue,
                                         final ODMMetaData metaData, final List<String> comment)
        throws OrekitException {
        switch (keyValue.getKeyword()) {
        case OBJECT_NAME:
            if (!comment.isEmpty()) {
                metaData.setComment(comment);
                comment.clear();
            }
            metaData.setObjectName(keyValue.getValue());
            return true;

        case OBJECT_ID: {
            metaData.setObjectID(keyValue.getValue());
            final Matcher matcher = INTERNATIONAL_DESIGNATOR.matcher(keyValue.getValue());
            if (matcher.matches()) {
                metaData.setLaunchYear(Integer.parseInt(matcher.group(1)));
                metaData.setLaunchNumber(Integer.parseInt(matcher.group(2)));
                metaData.setLaunchPiece(matcher.group(3));
            }
            return true;
        }

        case CENTER_NAME:
            metaData.setCenterName(keyValue.getValue());
            final String canonicalValue;
            if (keyValue.getValue().equals("SOLAR SYSTEM BARYCENTER") || keyValue.getValue().equals("SSB")) {
                canonicalValue = "SOLAR_SYSTEM_BARYCENTER";
            } else if (keyValue.getValue().equals("EARTH MOON BARYCENTER") || keyValue.getValue().equals("EARTH-MOON BARYCENTER") ||
                       keyValue.getValue().equals("EARTH BARYCENTER") || keyValue.getValue().equals("EMB")) {
                canonicalValue = "EARTH_MOON";
            } else {
                canonicalValue = keyValue.getValue();
            }
            for (final CenterName c : CenterName.values()) {
                if (c.name().equals(canonicalValue)) {
                    metaData.setHasCreatableBody(true);
                    metaData.setCenterBody(c.getCelestialBody());
                    metaData.getODMFile().setMuCreated(c.getCelestialBody().getGM());
                }
            }
            return true;

        case REF_FRAME:
            metaData.setRefFrame(parseCCSDSFrame(keyValue.getValue()).getFrame(getConventions(), isSimpleEOP()));
            return true;

        case REF_FRAME_EPOCH:
            metaData.setFrameEpochString(keyValue.getValue());
            return true;

        case TIME_SYSTEM:
            final OrbitFile.TimeSystem timeSystem = OrbitFile.TimeSystem.valueOf(keyValue.getValue());
            metaData.setTimeSystem(timeSystem);
            if (metaData.getFrameEpochString() != null) {
                metaData.setFrameEpoch(parseDate(metaData.getFrameEpochString(), timeSystem));
            }
            return true;

        default:
            return false;
        }
    }

    /** Parse a general state data key = value entry.
     * @param keyValue key = value pair
     * @param general instance to update with parsed entry
     * @param comment previous comment lines, will be emptied if used by the keyword
     * @return true if the keyword was a meta-data keyword and has been parsed
     * @exception OrekitException if center body or frame cannot be retrieved
     */
    protected boolean parseGeneralStateDataEntry(final KeyValue keyValue,
                                                 final OGMFile general, final List<String> comment)
        throws OrekitException {
        switch (keyValue.getKeyword()) {

        case EPOCH:
            general.setEpochComment(comment);
            comment.clear();
            general.setEpoch(parseDate(keyValue.getValue(), general.getTimeSystem()));
            return true;

        case SEMI_MAJOR_AXIS:
            general.setKeplerianElementsComment(comment);
            comment.clear();
            general.setA(keyValue.getDoubleValue() * 1000);
            general.setHasKeplerianElements(true);
            return true;

        case ECCENTRICITY:
            general.setE(keyValue.getDoubleValue());
            return true;

        case INCLINATION:
            general.setI(FastMath.toRadians(keyValue.getDoubleValue()));
            return true;

        case RA_OF_ASC_NODE:
            general.setRaan(FastMath.toRadians(keyValue.getDoubleValue()));
            return true;

        case ARG_OF_PERICENTER:
            general.setPa(FastMath.toRadians(keyValue.getDoubleValue()));
            return true;

        case TRUE_ANOMALY:
            general.setAnomalyType("TRUE");
            general.setAnomaly(FastMath.toRadians(keyValue.getDoubleValue()));
            return true;

        case MEAN_ANOMALY:
            general.setAnomalyType("MEAN");
            general.setAnomaly(FastMath.toRadians(keyValue.getDoubleValue()));
            return true;

        case GM:
            general.setMuParsed(keyValue.getDoubleValue() * 1e9);
            return true;

        case MASS:
            comment.addAll(0, general.getSpacecraftComment());
            general.setSpacecraftComment(comment);
            comment.clear();
            general.setMass(keyValue.getDoubleValue());
            return true;

        case SOLAR_RAD_AREA:
            comment.addAll(0, general.getSpacecraftComment());
            general.setSpacecraftComment(comment);
            comment.clear();
            general.setSolarRadArea(keyValue.getDoubleValue());
            return true;

        case SOLAR_RAD_COEFF:
            comment.addAll(0, general.getSpacecraftComment());
            general.setSpacecraftComment(comment);
            comment.clear();
            general.setSolarRadCoeff(keyValue.getDoubleValue());
            return true;

        case DRAG_AREA:
            comment.addAll(0, general.getSpacecraftComment());
            general.setSpacecraftComment(comment);
            comment.clear();
            general.setDragArea(keyValue.getDoubleValue());
            return true;

        case DRAG_COEFF:
            comment.addAll(0, general.getSpacecraftComment());
            general.setSpacecraftComment(comment);
            comment.clear();
            general.setDragCoeff(keyValue.getDoubleValue());
            return true;

        case COV_REF_FRAME:
            general.setCovarianceComment(comment);
            comment.clear();
            final CCSDSFrame covFrame = parseCCSDSFrame(keyValue.getValue());
            if (covFrame.isLof()) {
                general.setCovRefLofType(covFrame.getLofType());
            } else {
                general.setCovRefFrame(covFrame.getFrame(getConventions(), isSimpleEOP()));
            }
            return true;

        case CX_X:
            general.createCovarianceMatrix();
            general.setCovarianceMatrixEntry(0, 0, keyValue.getDoubleValue());
            return true;

        case CY_X:
            general.setCovarianceMatrixEntry(0, 1, keyValue.getDoubleValue());
            return true;

        case CY_Y:
            general.setCovarianceMatrixEntry(1, 1, keyValue.getDoubleValue());
            return true;

        case CZ_X:
            general.setCovarianceMatrixEntry(0, 2, keyValue.getDoubleValue());
            return true;

        case CZ_Y:
            general.setCovarianceMatrixEntry(1, 2, keyValue.getDoubleValue());
            return true;

        case CZ_Z:
            general.setCovarianceMatrixEntry(2, 2, keyValue.getDoubleValue());
            return true;

        case CX_DOT_X:
            general.setCovarianceMatrixEntry(0, 3, keyValue.getDoubleValue());
            return true;

        case CX_DOT_Y:
            general.setCovarianceMatrixEntry(1, 3, keyValue.getDoubleValue());
            return true;

        case CX_DOT_Z:
            general.setCovarianceMatrixEntry(2, 3, keyValue.getDoubleValue());
            return true;

        case CX_DOT_X_DOT:
            general.setCovarianceMatrixEntry(3, 3, keyValue.getDoubleValue());
            return true;

        case CY_DOT_X:
            general.setCovarianceMatrixEntry(0, 4, keyValue.getDoubleValue());
            return true;

        case CY_DOT_Y:
            general.setCovarianceMatrixEntry(1, 4, keyValue.getDoubleValue());
            return true;

        case CY_DOT_Z:
            general.setCovarianceMatrixEntry(2, 4, keyValue.getDoubleValue());
            return true;

        case CY_DOT_X_DOT:
            general.setCovarianceMatrixEntry(3, 4, keyValue.getDoubleValue());
            return true;

        case CY_DOT_Y_DOT:
            general.setCovarianceMatrixEntry(4, 4, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_X:
            general.setCovarianceMatrixEntry(0, 5, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_Y:
            general.setCovarianceMatrixEntry(1, 5, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_Z:
            general.setCovarianceMatrixEntry(2, 5, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_X_DOT:
            general.setCovarianceMatrixEntry(3, 5, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_Y_DOT:
            general.setCovarianceMatrixEntry(4, 5, keyValue.getDoubleValue());
            return true;

        case CZ_DOT_Z_DOT:
            general.setCovarianceMatrixEntry(5, 5, keyValue.getDoubleValue());
            return true;

        case USER_DEFINED_X:
            general.setUserDefinedParameters(keyValue.getKey(), keyValue.getValue());
            return true;

        default:
            return false;
        }
    }

    /** Parse a CCSDS frame.
     * @param frameName name of the frame, as the value of a CCSDS key=value line
     * @return CCSDS frame corresponding to the name
     */
    protected CCSDSFrame parseCCSDSFrame(final String frameName) {
        return CCSDSFrame.valueOf(frameName.replaceAll("-", ""));
    }

    /** Parse a date.
     * @param date date to parse, as the value of a CCSDS key=value line
     * @param timeSystem time system to use
     * @return parsed date
     * @exception OrekitException if some time scale cannot be retrieved
     */
    protected AbsoluteDate parseDate(final String date, final OrbitFile.TimeSystem timeSystem)
        throws OrekitException {
        switch (timeSystem) {
        case GMST:
            return new AbsoluteDate(date, TimeScalesFactory.getGMST(conventions, false));
        case GPS:
            return new AbsoluteDate(date, TimeScalesFactory.getGPS());
        case TAI:
            return new AbsoluteDate(date, TimeScalesFactory.getTAI());
        case TCB:
            return new AbsoluteDate(date, TimeScalesFactory.getTCB());
        case TDB:
            return new AbsoluteDate(date, TimeScalesFactory.getTDB());
        case TCG:
            return new AbsoluteDate(date, TimeScalesFactory.getTCG());
        case TT:
            return new AbsoluteDate(date, TimeScalesFactory.getTT());
        case UT1:
            return new AbsoluteDate(date, TimeScalesFactory.getUT1(conventions, false));
        case UTC:
            return new AbsoluteDate(date, TimeScalesFactory.getUTC());
        case MET: {
            final DateTimeComponents clock = DateTimeComponents.parseDateTime(date);
            final double offset = clock.getDate().getYear() * Constants.JULIAN_YEAR +
                    clock.getDate().getDayOfYear() * Constants.JULIAN_DAY +
                    clock.getTime().getSecondsInDay();
            return missionReferenceDate.shiftedBy(offset);
        }
        case MRT: {
            final DateTimeComponents clock = DateTimeComponents.parseDateTime(date);
            final double offset = clock.getDate().getYear() * Constants.JULIAN_YEAR +
                    clock.getDate().getDayOfYear() * Constants.JULIAN_DAY +
                    clock.getTime().getSecondsInDay();
            return missionReferenceDate.shiftedBy(offset);
        }
        default:
            throw OrekitException.createInternalError(null);
        }
    }

}