StreamingCpfWriter.java
/* Copyright 2002-2023 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.ilrs;
import java.io.IOException;
import java.util.Locale;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.orekit.errors.OrekitException;
import org.orekit.frames.Frame;
import org.orekit.propagation.Propagator;
import org.orekit.propagation.SpacecraftState;
import org.orekit.propagation.sampling.OrekitFixedStepHandler;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.utils.TimeStampedPVCoordinates;
/**
* A writer for CPF files.
*
* <p> Each instance corresponds to a single CPF file.
*
* <p> This class can be used as a step handler for a {@link Propagator}.
* The following example shows its use as a step handler.
*
* <p>
* <b>Note:</b> By default, only required header keys are wrote (H1 and H2). Furthermore, only position data can be written.
* Other keys (optionals) are simply ignored.
* Contributions are welcome to support more fields in the format.
*
* @author Bryan Cazabonne
* @since 10.3
*/
public class StreamingCpfWriter {
/** New line separator for output file. */
private static final String NEW_LINE = "\n";
/** String A2 Format. */
private static final String A1 = "%1s";
/** String A2 Format. */
private static final String A2 = "%2s";
/** String A3 Format. */
private static final String A3 = "%3s";
/** String A4 Format. */
private static final String A4 = "%4s";
/** String A8 Format. */
private static final String A8 = "%8s";
/** String A10 Format. */
private static final String A10 = "%10s";
/** Integer I1 Format. */
private static final String I1 = "%1d";
/** Integer I2 Format. */
private static final String I2 = "%2d";
/** Integer I3 Format. */
private static final String I3 = "%3d";
/** Integer I4 Format. */
private static final String I4 = "%4d";
/** Integer I5 Format. */
private static final String I5 = "%5d";
/** Real 13.6 Format. */
private static final String F13_6 = "%13.6f";
/** Real 17.3 Format. */
private static final String F17_3 = "%17.3f";
/** Real 19.6 Format. */
private static final String F19_6 = "%19.6f";
/** Space. */
private static final String SPACE = " ";
/** Empty string. */
private static final String EMPTY_STRING = "";
/** File format. */
private static final String FORMAT = "CPF";
/** Default locale. */
private static final Locale STANDARDIZED_LOCALE = Locale.US;
/** Default value for direction flag in position record. */
private static final int DEFAULT_DIRECTION_FLAG = 0;
/** Output stream. */
private final Appendable writer;
/** Time scale for all dates. */
private final TimeScale timeScale;
/** Container for header data. */
private final CPFHeader header;
/** Flag for optional velocity record. */
private final boolean velocityFlag;
/**
* Create a CPF writer than streams data to the given output stream.
* <p>
* Using this constructor, velocity data are not written.
* </p>
* @param writer the output stream for the CPF file.
* @param timeScale for all times in the CPF
* @param header container for header data
* @see #StreamingCpfWriter(Appendable, TimeScale, CPFHeader, boolean)
*/
public StreamingCpfWriter(final Appendable writer,
final TimeScale timeScale,
final CPFHeader header) {
this(writer, timeScale, header, false);
}
/**
* Create a CPF writer than streams data to the given output stream.
*
* @param writer the output stream for the CPF file.
* @param timeScale for all times in the CPF
* @param header container for header data
* @param velocityFlag true if velocity must be written
* @since 11.2
*/
public StreamingCpfWriter(final Appendable writer,
final TimeScale timeScale,
final CPFHeader header,
final boolean velocityFlag) {
this.writer = writer;
this.timeScale = timeScale;
this.header = header;
this.velocityFlag = velocityFlag;
}
/**
* Writes the CPF header for the file.
* @throws IOException if the stream cannot write to stream
*/
public void writeHeader() throws IOException {
// Write H1
HeaderLineWriter.H1.write(header, writer, timeScale);
writer.append(NEW_LINE);
// Write H2
HeaderLineWriter.H2.write(header, writer, timeScale);
writer.append(NEW_LINE);
// End of header
writer.append("H9");
writer.append(NEW_LINE);
}
/**
* Write end of file.
* @throws IOException if the stream cannot write to stream
*/
public void writeEndOfFile() throws IOException {
writer.append("99");
}
/**
* Create a writer for a new CPF ephemeris segment.
* <p>
* The returned writer can only write a single ephemeris segment in a CPF.
* </p>
* @param frame the reference frame to use for the segment.
* @return a new CPF segment, ready for writing.
*/
public Segment newSegment(final Frame frame) {
return new Segment(frame);
}
/**
* Write a String value in the file.
* @param cpfWriter writer
* @param format format
* @param value value
* @param withSpace true if a space must be added
* @throws IOException if value cannot be written
*/
private static void writeValue(final Appendable cpfWriter, final String format,
final String value, final boolean withSpace)
throws IOException {
cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
}
/**
* Write a integer value in the file.
* @param cpfWriter writer
* @param format format
* @param value value
* @param withSpace true if a space must be added
* @throws IOException if value cannot be written
*/
private static void writeValue(final Appendable cpfWriter, final String format,
final int value, final boolean withSpace)
throws IOException {
cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
}
/**
* Write a real value in the file.
* @param cpfWriter writer
* @param format format
* @param value value
* @param withSpace true if a space must be added
* @throws IOException if value cannot be written
*/
private static void writeValue(final Appendable cpfWriter, final String format,
final double value, final boolean withSpace)
throws IOException {
cpfWriter.append(String.format(STANDARDIZED_LOCALE, format, value)).append(withSpace ? SPACE : EMPTY_STRING);
}
/**
* Write a String value in the file.
* @param cpfWriter writer
* @param format format
* @param value value
* @param withSpace true if a space must be added
* @throws IOException if value cannot be written
*/
private static void writeValue(final Appendable cpfWriter, final String format,
final boolean value, final boolean withSpace)
throws IOException {
// Change to an integer value
final int intValue = value ? 1 : 0;
writeValue(cpfWriter, format, intValue, withSpace);
}
/** A writer for a segment of a CPF. */
public class Segment implements OrekitFixedStepHandler {
/** Reference frame of the output states. */
private final Frame frame;
/**
* Create a new segment writer.
*
* @param frame for the output states. Used by {@link #handleStep(SpacecraftState,
* boolean)}.
*/
private Segment(final Frame frame) {
this.frame = frame;
}
/** {@inheritDoc}. */
@Override
public void handleStep(final SpacecraftState currentState) {
try {
// Write ephemeris line
writeEphemerisLine(currentState.getPVCoordinates(frame));
} catch (IOException e) {
throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
e.getLocalizedMessage());
}
}
/** {@inheritDoc}. */
@Override
public void finish(final SpacecraftState finalState) {
try {
// Write ephemeris line
writeEphemerisLine(finalState.getPVCoordinates(frame));
// Write end of file
writeEndOfFile();
} catch (IOException e) {
throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
e.getLocalizedMessage());
}
}
/**
* Write ephemeris lines.
* <p>
* If <code>velocityFlag</code> is equals to true, both
* position and velocity records are written. Otherwise,
* only the position data are used.
* </p>
* @param pv the time, position, and velocity to write.
* @throws IOException if the output stream throws one while writing.
*/
public void writeEphemerisLine(final TimeStampedPVCoordinates pv)
throws IOException {
// Record type and direction flag
writeValue(writer, A2, "10", true);
writeValue(writer, I1, DEFAULT_DIRECTION_FLAG, true);
// Epoch
final AbsoluteDate epoch = pv.getDate();
final DateTimeComponents dtc = epoch.getComponents(timeScale);
writeValue(writer, I5, dtc.getDate().getMJD(), true);
writeValue(writer, F13_6, dtc.getTime().getSecondsInLocalDay(), true);
// Leap second flag (default 0)
writeValue(writer, I2, 0, true);
// Position
final Vector3D position = pv.getPosition();
writeValue(writer, F17_3, position.getX(), true);
writeValue(writer, F17_3, position.getY(), true);
writeValue(writer, F17_3, position.getZ(), false);
// New line
writer.append(NEW_LINE);
// Write the velocity record
if (velocityFlag) {
// Record type and direction flag
writeValue(writer, A2, "20", true);
writeValue(writer, I1, DEFAULT_DIRECTION_FLAG, true);
// Velocity
final Vector3D velocity = pv.getVelocity();
writeValue(writer, F19_6, velocity.getX(), true);
writeValue(writer, F19_6, velocity.getY(), true);
writeValue(writer, F19_6, velocity.getZ(), false);
// New line
writer.append(NEW_LINE);
}
}
}
/** Writer for specific header lines. */
public enum HeaderLineWriter {
/** Header first line. */
H1("H1") {
/** {@inheritDoc} */
@Override
public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
throws IOException {
// write first keys
writeValue(cpfWriter, A2, getIdentifier(), true);
writeValue(cpfWriter, A3, FORMAT, true);
writeValue(cpfWriter, I2, cpfHeader.getVersion(), true);
writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
writeValue(cpfWriter, A3, cpfHeader.getSource(), true);
writeValue(cpfWriter, I4, cpfHeader.getProductionEpoch().getYear(), true);
writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getMonth(), true);
writeValue(cpfWriter, I2, cpfHeader.getProductionEpoch().getDay(), true);
writeValue(cpfWriter, I2, cpfHeader.getProductionHour(), true);
writeValue(cpfWriter, A1, SPACE, false); // One additional column, see CPF v1 format
writeValue(cpfWriter, I3, cpfHeader.getSequenceNumber(), true);
// check file version
if (cpfHeader.getVersion() == 2) {
writeValue(cpfWriter, I2, cpfHeader.getSubDailySequenceNumber(), true);
}
// write target name from official list
writeValue(cpfWriter, A10, cpfHeader.getName(), true);
// write notes (not supported yet)
writeValue(cpfWriter, A10, SPACE, false);
}
},
/** Header second line. */
H2("H2") {
/** {@inheritDoc} */
@Override
public void write(final CPFHeader cpfHeader, final Appendable cpfWriter, final TimeScale timescale)
throws IOException {
// write identifiers
writeValue(cpfWriter, A2, getIdentifier(), true);
writeValue(cpfWriter, A8, cpfHeader.getIlrsSatelliteId(), true);
writeValue(cpfWriter, A4, cpfHeader.getSic(), true);
writeValue(cpfWriter, A8, cpfHeader.getNoradId(), true);
// write starting epoch
final AbsoluteDate starting = cpfHeader.getStartEpoch();
final DateTimeComponents dtcStart = starting.getComponents(timescale);
writeValue(cpfWriter, I4, dtcStart.getDate().getYear(), true);
writeValue(cpfWriter, I2, dtcStart.getDate().getMonth(), true);
writeValue(cpfWriter, I2, dtcStart.getDate().getDay(), true);
writeValue(cpfWriter, I2, dtcStart.getTime().getHour(), true);
writeValue(cpfWriter, I2, dtcStart.getTime().getMinute(), true);
writeValue(cpfWriter, I2, (int) dtcStart.getTime().getSecond(), true);
// write ending epoch
final AbsoluteDate ending = cpfHeader.getEndEpoch();
final DateTimeComponents dtcEnd = ending.getComponents(timescale);
writeValue(cpfWriter, I4, dtcEnd.getDate().getYear(), true);
writeValue(cpfWriter, I2, dtcEnd.getDate().getMonth(), true);
writeValue(cpfWriter, I2, dtcEnd.getDate().getDay(), true);
writeValue(cpfWriter, I2, dtcEnd.getTime().getHour(), true);
writeValue(cpfWriter, I2, dtcEnd.getTime().getMinute(), true);
writeValue(cpfWriter, I2, (int) dtcEnd.getTime().getSecond(), true);
// write last keys
writeValue(cpfWriter, I5, cpfHeader.getStep(), true);
writeValue(cpfWriter, I1, cpfHeader.isCompatibleWithTIVs(), true);
writeValue(cpfWriter, I1, cpfHeader.getTargetClass(), true);
writeValue(cpfWriter, I2, cpfHeader.getRefFrameId(), true);
writeValue(cpfWriter, I1, cpfHeader.getRotationalAngleType(), true);
if (cpfHeader.getVersion() == 1) {
writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), false);
} else {
writeValue(cpfWriter, I1, cpfHeader.isCenterOfMassCorrectionApplied(), true);
writeValue(cpfWriter, I2, cpfHeader.getTargetLocation(), false);
}
}
};
/** Identifier. */
private final String identifier;
/** Simple constructor.
* @param identifier regular expression for identifying line (i.e. first element)
*/
HeaderLineWriter(final String identifier) {
this.identifier = identifier;
}
/** Write a line.
* @param cpfHeader container for header data
* @param cpfWriter writer
* @param timescale time scale for dates
* @throws IOException
* if any buffer writing operations fail or if the underlying
* format doesn't support a configuration in the file
*/
public abstract void write(CPFHeader cpfHeader, Appendable cpfWriter, TimeScale timescale) throws IOException;
/**
* Get the regular expression for identifying line.
* @return the regular expression for identifying line
*/
public String getIdentifier() {
return identifier;
}
}
}