OemParser.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (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.ndm.odm.oem;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;

import org.orekit.data.DataContext;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.definitions.Units;
import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
import org.orekit.files.ccsds.ndm.odm.CartesianCovariance;
import org.orekit.files.ccsds.ndm.odm.CartesianCovarianceKey;
import org.orekit.files.ccsds.ndm.odm.OdmCommonMetadata;
import org.orekit.files.ccsds.ndm.odm.CommonMetadataKey;
import org.orekit.files.ccsds.ndm.odm.OdmHeader;
import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
import org.orekit.files.ccsds.ndm.odm.OdmParser;
import org.orekit.files.ccsds.ndm.odm.StateVector;
import org.orekit.files.ccsds.ndm.odm.StateVectorKey;
import org.orekit.files.ccsds.section.HeaderProcessingState;
import org.orekit.files.ccsds.section.KvnStructureProcessingState;
import org.orekit.files.ccsds.section.MetadataKey;
import org.orekit.files.ccsds.section.XmlStructureProcessingState;
import org.orekit.files.ccsds.utils.ContextBinding;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.lexical.ParseToken;
import org.orekit.files.ccsds.utils.lexical.TokenType;
import org.orekit.files.ccsds.utils.parsing.ProcessingState;
import org.orekit.files.general.EphemerisFileParser;
import org.orekit.time.AbsoluteDate;
import org.orekit.utils.IERSConventions;
import org.orekit.utils.units.Unit;

/**
 * A parser for the CCSDS OEM (Orbit Ephemeris Message).
 * <p>
 * Note than starting with Orekit 11.0, CCSDS message parsers are
 * mutable objects that gather the data being parsed, until the
 * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
 * parseMessage} method has returned. This implies that parsers
 * should <em>not</em> be used in a multi-thread context. The recommended
 * way to use parsers is to either dedicate one parser for each message
 * and drop it afterwards, or to use a single-thread loop.
 * </p>
 * @author sports
 * @since 6.1
 */
public class OemParser extends OdmParser<Oem, OemParser> implements EphemerisFileParser<Oem> {

    /** Comment marker. */
    private static final String COMMENT = "COMMENT";

                    /** Pattern for splitting strings at blanks. */
    private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");

    /** File header. */
    private OdmHeader header;

    /** File segments. */
    private List<OemSegment> segments;

    /** Metadata for current observation block. */
    private OemMetadata metadata;

    /** Context binding valid for current metadata. */
    private ContextBinding context;

    /** Current Ephemerides block being parsed. */
    private OemData currentBlock;

    /** Indicator for covariance parsing. */
    private boolean inCovariance;

    /** Current covariance matrix being parsed. */
    private CartesianCovariance currentCovariance;

    /** Current row number in covariance matrix. */
    private int currentRow;

    /** Default interpolation degree. */
    private int defaultInterpolationDegree;

    /** Processor for global message structure. */
    private ProcessingState structureProcessor;

    /** State vector logical block being read. */
    private StateVector stateVectorBlock;

    /**
     * Complete constructor.
     * <p>
     * Calling this constructor directly is not recommended. Users should rather use
     * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOemParser()
     * parserBuilder.buildOemParser()}.
     * </p>
     * @param conventions IERS Conventions
     * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
     * @param dataContext used to retrieve frames, time scales, etc.
     * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
     * (may be null if time system is absolute)
     * @param mu gravitational coefficient
     * @param defaultInterpolationDegree default interpolation degree
     * @param parsedUnitsBehavior behavior to adopt for handling parsed units
     * @param filters filters to apply to parse tokens
     * @since 12.0
     */
    public OemParser(final IERSConventions conventions, final boolean simpleEOP,
                     final DataContext dataContext,
                     final AbsoluteDate missionReferenceDate, final double mu,
                     final int defaultInterpolationDegree, final ParsedUnitsBehavior parsedUnitsBehavior,
                     final Function<ParseToken, List<ParseToken>>[] filters) {
        super(Oem.ROOT, Oem.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext,
              missionReferenceDate, mu, parsedUnitsBehavior, filters);
        this.defaultInterpolationDegree  = defaultInterpolationDegree;
    }

    /** {@inheritDoc} */
    @Override
    public Oem parse(final DataSource source) {
        return parseMessage(source);
    }

    /** {@inheritDoc} */
    @Override
    public OdmHeader getHeader() {
        return header;
    }

    /** {@inheritDoc} */
    @Override
    public void reset(final FileFormat fileFormat) {
        header            = new OdmHeader();
        segments          = new ArrayList<>();
        metadata          = null;
        context           = null;
        currentBlock      = null;
        inCovariance      = false;
        currentCovariance = null;
        currentRow        = -1;
        if (fileFormat == FileFormat.XML) {
            structureProcessor = new XmlStructureProcessingState(Oem.ROOT, this);
            reset(fileFormat, structureProcessor);
        } else {
            structureProcessor = new KvnStructureProcessingState(this);
            reset(fileFormat, new HeaderProcessingState(this));
        }
    }

    /** {@inheritDoc} */
    @Override
    public boolean prepareHeader() {
        anticipateNext(new HeaderProcessingState(this));
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean inHeader() {
        anticipateNext(structureProcessor);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean finalizeHeader() {
        header.validate(header.getFormatVersion());
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean prepareMetadata() {
        if (currentBlock != null) {
            // we have started a new segment, we need to finalize the previous one
            finalizeData();
        }
        metadata = new OemMetadata(defaultInterpolationDegree);
        context  = new ContextBinding(this::getConventions, this::isSimpleEOP,
                                      this::getDataContext, this::getParsedUnitsBehavior,
                                      this::getMissionReferenceDate,
                                      metadata::getTimeSystem, () -> 0.0, () -> 1.0);
        anticipateNext(this::processMetadataToken);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean inMetadata() {
        anticipateNext(structureProcessor);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean finalizeMetadata() {
        metadata.finalizeMetadata(context);
        metadata.validate(header.getFormatVersion());
        if (metadata.getCenter().getBody() != null) {
            setMuCreated(metadata.getCenter().getBody().getGM());
        }
        anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnDataToken);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean prepareData() {
        currentBlock = new OemData();
        anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean inData() {
        anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processKvnCovarianceToken);
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean finalizeData() {
        if (metadata != null) {
            currentBlock.validate(header.getFormatVersion());
            segments.add(new OemSegment(metadata, currentBlock, getSelectedMu()));
        }
        metadata          = null;
        context           = null;
        currentBlock      = null;
        inCovariance      = false;
        currentCovariance = null;
        currentRow        = -1;
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public Oem build() {
        // OEM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
        // automatically before the end of the file
        finalizeData();
        final Oem file = new Oem(header, segments, getConventions(), getDataContext(), getSelectedMu());
        file.checkTimeSystems();
        return file;
    }

    /** Manage state vector section in a XML message.
     * @param starting if true, parser is entering the section
     * otherwise it is leaving the section
     * @return always return true
     */
    boolean manageXmlStateVectorSection(final boolean starting) {
        if (starting) {
            stateVectorBlock = new StateVector();
            anticipateNext(this::processXmlStateVectorToken);
        } else {
            currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
                                 stateVectorBlock.hasAcceleration());
            stateVectorBlock = null;
            anticipateNext(structureProcessor);
        }
        return true;
    }

    /** Manage covariance matrix section.
     * @param starting if true, parser is entering the section
     * otherwise it is leaving the section
     * @return always return true
     */
    boolean manageCovarianceSection(final boolean starting) {
        if (starting) {
            // save the current metadata for later retrieval of reference frame
            final OdmCommonMetadata savedMetadata = metadata;
            currentCovariance = new CartesianCovariance(() -> savedMetadata.getReferenceFrame());
            anticipateNext(getFileFormat() == FileFormat.XML ?
                        this::processXmlCovarianceToken :
                        this::processKvnCovarianceToken);
        } else {
            currentBlock.addCovarianceMatrix(currentCovariance);
            currentCovariance = null;
            anticipateNext(structureProcessor);
        }
        return true;
    }

    /** Process one metadata token.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processMetadataToken(final ParseToken token) {
        inMetadata();
        try {
            return token.getName() != null &&
                   MetadataKey.valueOf(token.getName()).process(token, context, metadata);
        } catch (IllegalArgumentException iaeM) {
            try {
                return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
            } catch (IllegalArgumentException iaeD) {
                try {
                    return CommonMetadataKey.valueOf(token.getName()).process(token, context, metadata);
                } catch (IllegalArgumentException iaeC) {
                    try {
                        return OemMetadataKey.valueOf(token.getName()).process(token, context, metadata);
                    } catch (IllegalArgumentException iaeE) {
                        // token has not been recognized
                        return false;
                    }
                }
            }
        }
    }

    /** Process one XML data substructure token.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processXmlSubStructureToken(final ParseToken token) {
        if (COMMENT.equals(token.getName())) {
            return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
        } else {
            try {
                return token.getName() != null &&
                                OemDataSubStructureKey.valueOf(token.getName()).process(token, this);
            } catch (IllegalArgumentException iae) {
                // token has not been recognized
                return false;
            }
        }
    }

    /** Process one data token in a KVN message.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processKvnDataToken(final ParseToken token) {
        if (currentBlock == null) {
            // OEM KVN file lack a DATA_START keyword, hence we can't call prepareData()
            // automatically before the first data token arrives
            prepareData();
        }
        inData();
        if (COMMENT.equals(token.getName())) {
            return token.getType() == TokenType.ENTRY ? currentBlock.addComment(token.getContentAsNormalizedString()) : true;
        } else if (token.getType() == TokenType.RAW_LINE) {
            try {
                final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
                if (fields.length != 7 && fields.length != 10) {
                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                              token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
                }
                stateVectorBlock = new StateVector();
                stateVectorBlock.setEpoch(context.getTimeSystem().getConverter(context).parse(fields[0]));
                stateVectorBlock.setP(0, Unit.KILOMETRE.toSI(Double.parseDouble(fields[1])));
                stateVectorBlock.setP(1, Unit.KILOMETRE.toSI(Double.parseDouble(fields[2])));
                stateVectorBlock.setP(2, Unit.KILOMETRE.toSI(Double.parseDouble(fields[3])));
                stateVectorBlock.setV(0, Units.KM_PER_S.toSI(Double.parseDouble(fields[4])));
                stateVectorBlock.setV(1, Units.KM_PER_S.toSI(Double.parseDouble(fields[5])));
                stateVectorBlock.setV(2, Units.KM_PER_S.toSI(Double.parseDouble(fields[6])));
                if (fields.length == 10) {
                    stateVectorBlock.setA(0, Units.KM_PER_S2.toSI(Double.parseDouble(fields[7])));
                    stateVectorBlock.setA(1, Units.KM_PER_S2.toSI(Double.parseDouble(fields[8])));
                    stateVectorBlock.setA(2, Units.KM_PER_S2.toSI(Double.parseDouble(fields[9])));
                }
                return currentBlock.addData(stateVectorBlock.toTimeStampedPVCoordinates(),
                                            stateVectorBlock.hasAcceleration());
            } catch (NumberFormatException nfe) {
                throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                          token.getLineNumber(), token.getFileName(), token.getRawContent());
            }
        } else {
            // not a raw line, it is most probably either the end of the data section or a covariance section
            return false;
        }
    }

    /** Process one state vector data token in a XML message.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processXmlStateVectorToken(final ParseToken token) {
        anticipateNext(this::processXmlSubStructureToken);
        try {
            return token.getName() != null &&
                   StateVectorKey.valueOf(token.getName()).process(token, context, stateVectorBlock);
        } catch (IllegalArgumentException iae) {
            // token has not been recognized
            return false;
        }
    }

    /** Process one covariance token in a KVN message.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processKvnCovarianceToken(final ParseToken token) {
        anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
        if (token.getName() != null) {
            if (OemDataSubStructureKey.COVARIANCE.name().equals(token.getName()) ||
                OemDataSubStructureKey.covarianceMatrix.name().equals(token.getName())) {
                // we are entering/leaving covariance section
                inCovariance = token.getType() == TokenType.START;
                return true;
            } else if (!inCovariance) {
                // this is not a covariance token
                return false;
            } else {
                // named tokens in covariance section must be at the start, before the raw lines
                if (currentRow > 0) {
                    // the previous covariance matrix was not completed
                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_ELEMENT_IN_FILE,
                                              token.getName(), token.getLineNumber(), token.getFileName());
                }

                if (currentCovariance == null) {
                    // save the current metadata for later retrieval of reference frame
                    final OdmCommonMetadata savedMetadata = metadata;
                    currentCovariance = new CartesianCovariance(() -> savedMetadata.getReferenceFrame());
                    currentRow        = 0;
                }

                // parse the token
                try {
                    return CartesianCovarianceKey.valueOf(token.getName()).
                           process(token, context, currentCovariance);
                } catch (IllegalArgumentException iae) {
                    // token not recognized
                    return false;
                }

            }
        } else {
            // this is a raw line
            try {
                final String[] fields = SPLIT_AT_BLANKS.split(token.getContentAsNormalizedString().trim());
                if (fields.length != currentRow + 1) {
                    throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                              token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
                }
                for (int j = 0; j < fields.length; ++j) {
                    currentCovariance.setCovarianceMatrixEntry(currentRow, j, 1.0e6 * Double.parseDouble(fields[j]));
                }
                if (++currentRow == 6) {
                    // this was the last row
                    currentBlock.addCovarianceMatrix(currentCovariance);
                    currentCovariance = null;
                    currentRow        = -1;
                }
                return true;
            } catch (NumberFormatException nfe) {
                throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                          token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
            }
        }
    }

    /** Process one covariance matrix data token in a XML message.
     * @param token token to process
     * @return true if token was processed, false otherwise
     */
    private boolean processXmlCovarianceToken(final ParseToken token) {
        anticipateNext(this::processXmlSubStructureToken);
        try {
            return token.getName() != null &&
                   CartesianCovarianceKey.valueOf(token.getName()).process(token, context, currentCovariance);
        } catch (IllegalArgumentException iae) {
            // token has not been recognized
            return false;
        }
    }

}