/* 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.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" + NEW_LINE; /** 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}. * @throws OrekitException If the UTC time scale could not be used. */ public StreamingOemWriter(final Appendable writer, final TimeScale timeScale, final Map<Keyword, String> metadata) throws OrekitException { 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 (name.contains("CIO") && name.contains(itrf)) { return "ITRF2008"; } 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.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) throws OrekitException { 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) throws OrekitException { 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); } }