StreamingOemWriter.java
- /* Contributed in the public domain.
- * 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.IOException;
- import java.text.DecimalFormat;
- import java.text.DecimalFormatSymbols;
- import java.util.Arrays;
- import java.util.Date;
- import java.util.LinkedHashMap;
- import java.util.Locale;
- import java.util.Map;
- import org.hipparchus.exception.LocalizedCoreFormats;
- import org.orekit.bodies.CelestialBodyFactory;
- import org.orekit.errors.OrekitException;
- import org.orekit.frames.FactoryManagedFrame;
- import org.orekit.frames.Frame;
- import org.orekit.frames.Predefined;
- import org.orekit.frames.VersionedITRF;
- 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.TimeComponents;
- import org.orekit.time.TimeScale;
- import org.orekit.time.TimeScalesFactory;
- import org.orekit.utils.TimeStampedPVCoordinates;
- /**
- * A writer for OEM files.
- *
- * <p> Each instance corresponds to a single OEM file. A new OEM ephemeris segment is
- * started by calling {@link #newSegment(Frame, Map)}.
- *
- * <h3> Metadata </h3>
- *
- * <p> The OEM metadata used by this writer is described in the following table. Many
- * metadata items are optional or have default values so they do not need to be specified.
- * At a minimum the user must supply those values that are required and for which no
- * default exits: {@link Keyword#OBJECT_NAME}, and {@link Keyword#OBJECT_ID}. The usage
- * column in the table indicates where the metadata item is used, either in the OEM header
- * or in the metadata section at the start of an OEM ephemeris segment.
- *
- * <p> The OEM metadata for the whole OEM file is set in the {@link
- * #StreamingOemWriter(Appendable, TimeScale, Map) constructor}. Any of the metadata may
- * be overridden for a particular segment using the {@code metadata} argument to {@link
- * #newSegment(Frame, Map)}.
- *
- * <table>
- * <thead>
- * <tr>
- * <th>Keyword
- * <th>Usage
- * <th>Obligatory
- * <th>Default
- * <th>Reference
- * </thead>
- * <tbody>
- * <tr>
- * <td>{@link Keyword#CCSDS_OEM_VERS}
- * <td>Header
- * <td>Yes
- * <td>{@link #CCSDS_OEM_VERS}
- * <td>Table 5-2
- * <tr>
- * <td>{@link Keyword#COMMENT}
- * <td>Header
- * <td>No
- * <td>
- * <td>Table 5-2
- * <tr>
- * <td>{@link Keyword#CREATION_DATE}
- * <td>Header
- * <td>Yes
- * <td>{@link Date#Date() Now}
- * <td>Table 5.2, 6.5.9
- * <tr>
- * <td>{@link Keyword#ORIGINATOR}
- * <td>Header
- * <td>Yes
- * <td>{@link #DEFAULT_ORIGINATOR}
- * <td>Table 5-2
- * <tr>
- * <td>{@link Keyword#OBJECT_NAME}
- * <td>Segment
- * <td>Yes
- * <td>
- * <td>Table 5-3
- * <tr>
- * <td>{@link Keyword#OBJECT_ID}
- * <td>Segment
- * <td>Yes
- * <td>
- * <td>Table 5-3
- * <tr>
- * <td>{@link Keyword#CENTER_NAME}
- * <td>Segment
- * <td>Yes
- * <td>Guessed from the {@link #newSegment(Frame, Map) segment}'s {@code frame}
- * <td>Table 5-3
- * <tr>
- * <td>{@link Keyword#REF_FRAME}
- * <td>Segment
- * <td>Yes
- * <td>Guessed from the {@link #newSegment(Frame, Map) segment}'s {@code frame}
- * <td>Table 5-3, Annex A
- * <tr>
- * <td>{@link Keyword#REF_FRAME_EPOCH}
- * <td>Segment
- * <td>No
- * <td>
- * <td>Table 5-3, 6.5.9
- * <tr>
- * <td>{@link Keyword#TIME_SYSTEM}
- * <td>Segment
- * <td>Yes
- * <td>Guessed from {@code timeScale} set in the
- * {@link #StreamingOemWriter(Appendable, TimeScale, Map) constructor}.
- * <td>Table 5-3, Annex A
- * <tr>
- * <td>{@link Keyword#START_TIME}
- * <td>Segment
- * <td>Yes
- * <td>Date of initial state in {@link Segment#init(SpacecraftState,
- * AbsoluteDate, double) Segment.init(...)}
- * <td>Table 5-3, 6.5.9
- * <tr>
- * <td>{@link Keyword#USEABLE_START_TIME}
- * <td>Segment
- * <td>No
- * <td>
- * <td>Table 5-3, 6.5.9
- * <tr>
- * <td>{@link Keyword#STOP_TIME}
- * <td>Segment
- * <td>Yes
- * <td>Target date in {@link Segment#init(SpacecraftState,
- * AbsoluteDate, double) Segment.init(...)}
- * <td>Table 5-3, 6.5.9
- * <tr>
- * <td>{@link Keyword#USEABLE_STOP_TIME}
- * <td>Segment
- * <td>No
- * <td>
- * <td>Table 5-3, 6.5.9
- * <tr>
- * <td>{@link Keyword#INTERPOLATION}
- * <td>Segment
- * <td>No
- * <td>
- * <td>Table 5-3
- * <tr>
- * <td>{@link Keyword#INTERPOLATION_DEGREE}
- * <td>Segment
- * <td>No
- * <td>
- * <td>Table 5-3
- * </tbody>
- *</table>
- *
- * <p> The {@link Keyword#TIME_SYSTEM} must be constant for the whole file and is used
- * to interpret all dates except {@link Keyword#CREATION_DATE}. The guessing algorithm
- * is not guaranteed to work so it is recommended to provide values for {@link
- * Keyword#CENTER_NAME}, {@link Keyword#REF_FRAME}, and {@link Keyword#TIME_SYSTEM} to
- * avoid any bugs associated with incorrect guesses.
- *
- * <p> Standardized values for {@link Keyword#TIME_SYSTEM} are GMST, GPS, ME, MRT, SCLK,
- * TAI, TCB, TDB, TCG, TT, UT1, and UTC. Standardized values for {@link Keyword#REF_FRAME}
- * are EME2000, GCRF, GRC, ICRF, ITRF2000, ITRF-93, ITRF-97, MCI, TDR, TEME, and TOD.
- * Additionally ITRF followed by a four digit year may be used.
- *
- * <h3> Examples </h3>
- *
- * <p> This class can be used as a step handler for a {@link Propagator}, or on its own.
- * Either way the object name and ID must be specified. The following example shows its
- * use as a step handler.
- *
- * <pre>{@code
- * Propagator propagator = ...; // pre-configured propagator
- * Appendable out = ...; // set-up output stream
- * Map<Keyword, String> metadata = new LinkedHashMap<>();
- * metadata.put(Keyword.OBJECT_NAME, "Vanguard");
- * metadata.put(Keyword.OBJECT_ID, "1958-002B");
- * StreamingOemWriter writer = new StreamingOemWriter(out, utc, metadata);
- * writer.writeHeader();
- * Segment segment = writer.newSegment(frame, Collections.emptyMap());
- * propagator.setMasterMode(step, segment);
- * propagator.propagate(startDate, stopDate);
- * }</pre>
- *
- * Alternatively a collection of state vectors can be written without the use of a
- * Propagator. In this case the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME}
- * need to be specified as part of the metadata.
- *
- * <pre>{@code
- * List<TimeStampedPVCoordinates> states = ...; // pre-generated states
- * Appendable out = ...; // set-up output stream
- * Map<Keyword, String> metadata = new LinkedHashMap<>();
- * metadata.put(Keyword.OBJECT_NAME, "Vanguard");
- * metadata.put(Keyword.OBJECT_ID, "1958-002B");
- * StreamingOemWriter writer = new StreamingOemWriter(out, utc, metadata);
- * writer.writeHeader();
- * // manually set start and stop times for this segment
- * Map<Keyword, String> segmentData = new LinkedHashMap<>();
- * segmentData.put(Keyword.START_TIME, start.toString());
- * segmentData.put(Keyword.STOP_TIME, stop.toString());
- * Segment segment = writer.newSegment(frame, segmentData);
- * segment.writeMetadata(); // output metadata block
- * for (TimeStampedPVCoordinates state : states) {
- * segment.writeEphemerisLine(state);
- * }
- * }</pre>
- *
- * @author Evan Ward
- * @see <a href="https://public.ccsds.org/Pubs/502x0b2c1.pdf">CCSDS 502.0-B-2 Orbit Data
- * Messages</a>
- * @see <a href="https://public.ccsds.org/Pubs/500x0g3.pdf">CCSDS 500.0-G-3 Navigation
- * Data Definitions and Conventions</a>
- * @see OEMWriter
- */
- public class StreamingOemWriter {
- /** Version number implemented. **/
- public static final String CCSDS_OEM_VERS = "2.0";
- /** Default value for {@link Keyword#ORIGINATOR}. */
- public static final String DEFAULT_ORIGINATOR = "OREKIT";
- /** New line separator for output file. See 6.3.6. */
- private static final String NEW_LINE = "\n";
- /**
- * Standardized locale to use, to ensure files can be exchanged without
- * internationalization issues.
- */
- private static final Locale STANDARDIZED_LOCALE = Locale.US;
- /** String format used for all key/value pair lines. **/
- private static final String KV_FORMAT = "%s = %s%n";
- /** Factor for converting meters to km. */
- private static final double M_TO_KM = 1e-3;
- /** Suffix of the name of the inertial frame attached to a planet. */
- private static final String INERTIAL_FRAME_SUFFIX = "/inertial";
- /** Output stream. */
- private final Appendable writer;
- /** Metadata for this OEM file. */
- private final Map<Keyword, String> metadata;
- /** Time scale for all dates except {@link Keyword#CREATION_DATE}. */
- private final TimeScale timeScale;
- /**
- * Create an OEM writer than streams data to the given output stream.
- *
- * @param writer The output stream for the OEM file. Most methods will append data
- * to this {@code writer}.
- * @param timeScale for all times in the OEM except {@link Keyword#CREATION_DATE}. See
- * Section 5.2.4.5 and Annex A.
- * @param metadata for the satellite. Can be overridden in {@link #newSegment(Frame,
- * Map)} for a specific segment. See {@link StreamingOemWriter}.
- */
- public StreamingOemWriter(final Appendable writer,
- final TimeScale timeScale,
- final Map<Keyword, String> metadata) {
- this.writer = writer;
- this.timeScale = timeScale;
- this.metadata = new LinkedHashMap<>(metadata);
- // set default metadata
- this.metadata.putIfAbsent(Keyword.CCSDS_OEM_VERS, CCSDS_OEM_VERS);
- this.metadata.putIfAbsent(Keyword.CREATION_DATE,
- new AbsoluteDate(new Date(), TimeScalesFactory.getUTC()).toString());
- this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
- this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
- }
- /**
- * Guesses names from Table 5-3 and Annex A.
- *
- * <p> The goal of this method is to perform the opposite mapping of {@link
- * CCSDSFrame}.
- *
- * @param frame a reference frame for ephemeris output.
- * @return the string to use in the OEM file to identify {@code frame}.
- */
- static String guessFrame(final Frame frame) {
- // define some constant strings to make checkstyle happy
- final String tod = "TOD";
- final String itrf = "ITRF";
- // Try to determine the CCSDS name from Annex A by examining the Orekit name.
- final String name = frame.getName();
- if (Arrays.stream(CCSDSFrame.values())
- .map(CCSDSFrame::name)
- .anyMatch(name::equals)) {
- // should handle J2000, GCRF, TEME, and some frames created by OEMParser.
- return name;
- } else if (frame instanceof CcsdsModifiedFrame) {
- return ((CcsdsModifiedFrame) frame).getRefFrame();
- } else if ((CelestialBodyFactory.MARS + INERTIAL_FRAME_SUFFIX).equals(name)) {
- return "MCI";
- } else if ((CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER + INERTIAL_FRAME_SUFFIX)
- .equals(name)) {
- return "ICRF";
- } else if (name.contains("GTOD")) {
- return "TDR";
- } else if (name.contains(tod)) { // check after GTOD
- return tod;
- } else if (name.contains("Equinox") && name.contains(itrf)) {
- return "GRC";
- } else if (frame instanceof VersionedITRF) {
- return ((VersionedITRF) frame).getITRFVersion().getName().replace("-", "");
- } else if (name.contains("CIO") && name.contains(itrf)) {
- return "ITRF2014";
- } else {
- // don't know how to map it to a CCSDS reference frame
- return name;
- }
- }
- /**
- * Guess the name of the center of the reference frame.
- *
- * @param frame a reference frame for ephemeris output.
- * @return the string to use in the OEM file to describe the origin of {@code frame}.
- */
- static String guessCenter(final Frame frame) {
- final String name = frame.getName();
- if (name.endsWith(INERTIAL_FRAME_SUFFIX) || name.endsWith("/rotating")) {
- return name.substring(0, name.length() - 9).toUpperCase(STANDARDIZED_LOCALE);
- } else if (frame instanceof CcsdsModifiedFrame) {
- return ((CcsdsModifiedFrame) frame).getCenterName();
- } else if (frame.getName().equals(Predefined.ICRF.getName())) {
- return CelestialBodyFactory.SOLAR_SYSTEM_BARYCENTER.toUpperCase(STANDARDIZED_LOCALE);
- } else if (frame.getDepth() == 0 || frame instanceof FactoryManagedFrame) {
- return "EARTH";
- } else {
- return "UNKNOWN";
- }
- }
- /**
- * Write a single key and value to the stream using Key Value Notation (KVN).
- *
- * @param key the keyword to write
- * @param value the value to write
- * @throws IOException if an I/O error occurs.
- */
- private void writeKeyValue(final Keyword key, final String value) throws IOException {
- writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
- }
- /**
- * Writes the standard OEM header for the file.
- *
- * @throws IOException if the stream cannot write to stream
- */
- public void writeHeader() throws IOException {
- writeKeyValue(Keyword.CCSDS_OEM_VERS, this.metadata.get(Keyword.CCSDS_OEM_VERS));
- final String comment = this.metadata.get(Keyword.COMMENT);
- if (comment != null) {
- writeKeyValue(Keyword.COMMENT, comment);
- }
- writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
- writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
- writer.append(NEW_LINE);
- }
- /**
- * Create a writer for a new OEM ephemeris segment.
- *
- * <p> The returned writer can only write a single ephemeris segment in an OEM. This
- * method must be called to create a writer for each ephemeris segment.
- *
- * @param frame the reference frame to use for the segment. If this value is
- * {@code null} then {@link Segment#handleStep(SpacecraftState,
- * boolean)} will throw a {@link NullPointerException} and the
- * metadata item {@link Keyword#REF_FRAME} must be specified in
- * the metadata.
- * @param segmentMetadata the metadata to use for the segment. Overrides for this
- * segment any other source of meta data values. See {@link
- * #StreamingOemWriter} for a description of which metadata are
- * required and how they are determined.
- * @return a new OEM segment, ready for writing.
- */
- public Segment newSegment(final Frame frame,
- final Map<Keyword, String> segmentMetadata) {
- final Map<Keyword, String> meta = new LinkedHashMap<>(this.metadata);
- meta.putAll(segmentMetadata);
- if (!meta.containsKey(Keyword.REF_FRAME)) {
- meta.put(Keyword.REF_FRAME, guessFrame(frame));
- }
- if (!meta.containsKey(Keyword.CENTER_NAME)) {
- meta.put(Keyword.CENTER_NAME, guessCenter(frame));
- }
- return new Segment(frame, meta);
- }
- /** A writer for a segment of an OEM. */
- public class Segment implements OrekitFixedStepHandler {
- /** Reference frame of the output states. */
- private final Frame frame;
- /** Metadata for this OEM Segment. */
- private final Map<Keyword, String> metadata;
- /**
- * Create a new segment writer.
- *
- * @param frame for the output states. Used by {@link #handleStep(SpacecraftState,
- * boolean)}.
- * @param metadata to use when writing this segment.
- */
- private Segment(final Frame frame, final Map<Keyword, String> metadata) {
- this.frame = frame;
- this.metadata = metadata;
- }
- /**
- * Write the ephemeris segment metadata.
- *
- * <p> See {@link StreamingOemWriter} for a description of how the metadata is
- * set.
- *
- * @throws IOException if the output stream throws one while writing.
- */
- public void writeMetadata() throws IOException {
- writer.append("META_START").append(NEW_LINE);
- if (this.frame != null) {
- writer.append("COMMENT ").append("Orekit frame: ")
- .append(this.frame.toString()).append(NEW_LINE);
- }
- // Table 5.3
- writeKeyValue(Keyword.OBJECT_NAME, this.metadata.get(Keyword.OBJECT_NAME));
- writeKeyValue(Keyword.OBJECT_ID, this.metadata.get(Keyword.OBJECT_ID));
- writeKeyValue(Keyword.CENTER_NAME, this.metadata.get(Keyword.CENTER_NAME));
- writeKeyValue(Keyword.REF_FRAME, this.metadata.get(Keyword.REF_FRAME));
- final String refFrameEpoch = this.metadata.get(Keyword.REF_FRAME_EPOCH);
- if (refFrameEpoch != null) {
- writeKeyValue(Keyword.REF_FRAME_EPOCH, refFrameEpoch);
- }
- writeKeyValue(Keyword.TIME_SYSTEM, this.metadata.get(Keyword.TIME_SYSTEM));
- writeKeyValue(Keyword.START_TIME, this.metadata.get(Keyword.START_TIME));
- final String usableStartTime = this.metadata.get(Keyword.USEABLE_START_TIME);
- if (usableStartTime != null) {
- writeKeyValue(Keyword.USEABLE_START_TIME, usableStartTime);
- }
- writeKeyValue(Keyword.STOP_TIME, this.metadata.get(Keyword.STOP_TIME));
- final String usableStopTime = this.metadata.get(Keyword.USEABLE_STOP_TIME);
- if (usableStopTime != null) {
- writeKeyValue(Keyword.USEABLE_STOP_TIME, usableStopTime);
- }
- final String interpolation = this.metadata.get(Keyword.INTERPOLATION);
- if (interpolation != null) {
- writeKeyValue(Keyword.INTERPOLATION, interpolation);
- }
- final String interpolationDegree =
- this.metadata.get(Keyword.INTERPOLATION_DEGREE);
- if (interpolationDegree != null) {
- writeKeyValue(Keyword.INTERPOLATION_DEGREE, interpolationDegree);
- }
- writer.append("META_STOP").append(NEW_LINE).append(NEW_LINE);
- }
- /**
- * Write a single ephemeris line according to section 5.2.4. This method does not
- * write the optional acceleration terms.
- *
- * @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 {
- final String epoch = dateToString(pv.getDate().getComponents(timeScale));
- writer.append(epoch).append(" ");
- // output in km, see Section 6.6.2.1
- writer.append(Double.toString(pv.getPosition().getX() * M_TO_KM)).append(" ");
- writer.append(Double.toString(pv.getPosition().getY() * M_TO_KM)).append(" ");
- writer.append(Double.toString(pv.getPosition().getZ() * M_TO_KM)).append(" ");
- writer.append(Double.toString(pv.getVelocity().getX() * M_TO_KM)).append(" ");
- writer.append(Double.toString(pv.getVelocity().getY() * M_TO_KM)).append(" ");
- writer.append(Double.toString(pv.getVelocity().getZ() * M_TO_KM));
- writer.append(NEW_LINE);
- }
- /**
- * {@inheritDoc}
- *
- * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
- * segment's metadata if not already set by the user. Then calls {@link
- * #writeMetadata()} to start the segment.
- */
- @Override
- public void init(final SpacecraftState s0,
- final AbsoluteDate t,
- final double step) {
- try {
- final String start = dateToString(s0.getDate().getComponents(timeScale));
- final String stop = dateToString(t.getComponents(timeScale));
- this.metadata.putIfAbsent(Keyword.START_TIME, start);
- this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
- this.writeMetadata();
- } catch (IOException e) {
- throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
- e.getLocalizedMessage());
- }
- }
- @Override
- public void handleStep(final SpacecraftState s,
- final boolean isLast) {
- try {
- writeEphemerisLine(s.getPVCoordinates(this.frame));
- } catch (IOException e) {
- throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
- e.getLocalizedMessage());
- }
- }
- }
- /**
- * Convert a date to a string with more precision.
- *
- * @param components to convert to a String.
- * @return the String form of {@code date} with at least 9 digits of precision.
- */
- static String dateToString(final DateTimeComponents components) {
- final TimeComponents time = components.getTime();
- final int hour = time.getHour();
- final int minute = time.getMinute();
- final double second = time.getSecond();
- // Decimal formatting classes could be static final if they were thread safe.
- final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
- final DecimalFormat twoDigits = new DecimalFormat("00", locale);
- final DecimalFormat precise = new DecimalFormat("00.0########", locale);
- return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
- twoDigits.format(minute) + ":" + precise.format(second);
- }
- }