AttitudeWriter.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.adm.aem;

import java.io.IOException;
import java.util.List;

import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.definitions.FrameFacade;
import org.orekit.files.ccsds.ndm.adm.AdmHeader;
import org.orekit.files.ccsds.utils.FileFormat;
import org.orekit.files.ccsds.utils.generation.Generator;
import org.orekit.files.ccsds.utils.generation.KvnGenerator;
import org.orekit.files.ccsds.utils.generation.XmlGenerator;
import org.orekit.files.general.AttitudeEphemerisFile;
import org.orekit.files.general.AttitudeEphemerisFile.SatelliteAttitudeEphemeris;
import org.orekit.files.general.AttitudeEphemerisFileWriter;
import org.orekit.utils.TimeStampedAngularCoordinates;

/** An {@link AttitudeEphemerisFileWriter} generating {@link Aem AEM} files.
 * @author Bryan Cazabonne
 * @since 11.0
 */
public class AttitudeWriter implements AttitudeEphemerisFileWriter {

    /** Underlying writer. */
    private final AemWriter writer;

    /** Header. */
    private final AdmHeader header;

    /** Current metadata. */
    private final AemMetadata metadata;

    /** File format to use. */
    private final FileFormat fileFormat;

    /** Output name for error messages. */
    private final String outputName;

    /** Maximum offset for relative dates.
     * @since 12.0
     */
    private final double maxRelativeOffset;

    /** Column number for aligning units. */
    private final int unitsColumn;

    /**
     * Constructor used to create a new AEM writer configured with the necessary parameters
     * to successfully fill in all required fields that aren't part of a standard object.
     * <p>
     * If the mandatory header entries are not present (or if header is null),
     * built-in defaults will be used
     * </p>
     * <p>
     * The writer is built from the complete header and partial metadata. The template
     * metadata is used to initialize and independent local copy, that will be updated
     * as new segments are written (with at least the segment start and stop will change,
     * but some other parts may change too). The {@code template} argument itself is not
     * changed.
     * </p>
     * <p>
     * Calling this constructor directly is not recommended. Users should rather use
     * {@link org.orekit.files.ccsds.ndm.WriterBuilder#buildAemWriter()}.
     * </p>
     * @param writer underlying writer
     * @param header file header (may be null)
     * @param template template for metadata
     * @param fileFormat file format to use
     * @param outputName output name for error messages
     * @param maxRelativeOffset maximum offset in seconds to use relative dates
     * (if a date is too far from reference, it will be displayed as calendar elements)
     * @param unitsColumn columns number for aligning units (if negative or zero, units are not output)
     * @since 12.0
     */
    public AttitudeWriter(final AemWriter writer,
                          final AdmHeader header, final AemMetadata template,
                          final FileFormat fileFormat, final String outputName,
                          final double maxRelativeOffset, final int unitsColumn) {
        this.writer            = writer;
        this.header            = header;
        this.metadata          = template.copy(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
        this.fileFormat        = fileFormat;
        this.outputName        = outputName;
        this.maxRelativeOffset = maxRelativeOffset;
        this.unitsColumn       = unitsColumn;
    }

    /** {@inheritDoc}
     * <p>
     * As {@code AttitudeEphemerisFile.SatelliteAttitudeEphemeris} does not have all the entries
     * from {@link AemMetadata}, the only values that will be extracted from the
     * {@code ephemerisFile} will be the start time, stop time, reference frame, interpolation
     * method and interpolation degree. The missing values (like object name, local spacecraft
     * body frame, attitude type...) will be inherited from the template  metadata set at writer
     * {@link #AttitudeWriter(AemWriter, AdmHeader, AemMetadata, FileFormat, String, double, int) construction}.
     * </p>
     */
    @Override
    public <C extends TimeStampedAngularCoordinates, S extends AttitudeEphemerisFile.AttitudeEphemerisSegment<C>>
        void write(final Appendable appendable, final AttitudeEphemerisFile<C, S> ephemerisFile)
        throws IOException {

        if (appendable == null) {
            throw new OrekitIllegalArgumentException(OrekitMessages.NULL_ARGUMENT, "writer");
        }

        if (ephemerisFile == null) {
            return;
        }

        final SatelliteAttitudeEphemeris<C, S> satEphem =
                        ephemerisFile.getSatellites().get(metadata.getObjectID());
        if (satEphem == null) {
            throw new OrekitIllegalArgumentException(OrekitMessages.VALUE_NOT_FOUND,
                                                     metadata.getObjectID(), "ephemerisFile");
        }

        // Get attitude ephemeris segments to output.
        final List<S> segments = satEphem.getSegments();
        if (segments.isEmpty()) {
            // No data -> No output
            return;
        }

        try (Generator generator = fileFormat == FileFormat.KVN ?
             new KvnGenerator(appendable, AemWriter.KVN_PADDING_WIDTH, outputName,
                              maxRelativeOffset, unitsColumn) :
             new XmlGenerator(appendable, XmlGenerator.DEFAULT_INDENT, outputName,
                              maxRelativeOffset, unitsColumn > 0, null)) {

            writer.writeHeader(generator, header);

            // Loop on segments
            for (final S segment : segments) {
                writeSegment(generator, segment);
            }

            writer.writeFooter(generator);

        }

    }

    /** Write one segment.
     * @param generator generator to use for producing output
     * @param segment segment to write
     * @param <C> type of the angular coordinates
     * @param <S> type of the segment
     * @throws IOException if any buffer writing operations fails
     */
    private <C extends TimeStampedAngularCoordinates, S extends AttitudeEphemerisFile.AttitudeEphemerisSegment<C>>
        void writeSegment(final Generator generator, final S segment) throws IOException {

        // override template metadata with segment values
        metadata.setStartTime(segment.getStart());
        metadata.setStopTime(segment.getStop());
        if (metadata.getEndpoints().getFrameA() == null ||
            metadata.getEndpoints().getFrameA().asSpacecraftBodyFrame() == null) {
            // the external frame must be frame A
            metadata.getEndpoints().setFrameA(FrameFacade.map(segment.getReferenceFrame()));
        } else {
            // the external frame must be frame B
            metadata.getEndpoints().setFrameB(FrameFacade.map(segment.getReferenceFrame()));
        }
        metadata.setInterpolationMethod(segment.getInterpolationMethod());
        metadata.setInterpolationDegree(segment.getInterpolationSamples() - 1);
        metadata.validate(header == null ? writer.getDefaultVersion() : header.getFormatVersion());
        writer.writeMetadata(generator,
                             header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
                             metadata);

        // Loop on attitude data
        writer.startAttitudeBlock(generator);
        if (segment instanceof AemSegment) {
            generator.writeComments(((AemSegment) segment).getData().getComments());
        }
        for (final TimeStampedAngularCoordinates coordinates : segment.getAngularCoordinates()) {
            writer.writeAttitudeEphemerisLine(generator,
                                              header == null ? writer.getDefaultVersion() : header.getFormatVersion(),
                                              metadata, coordinates);
        }
        writer.endAttitudeBlock(generator);

    }

}