AbstractGenerator.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.utils.generation;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hipparchus.fraction.Fraction;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitInternalError;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.ccsds.definitions.TimeConverter;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.DateTimeComponents;
import org.orekit.utils.AccurateFormatter;
import org.orekit.utils.units.Parser;
import org.orekit.utils.units.PowerTerm;
import org.orekit.utils.units.Unit;
/** Base class for both Key-Value Notation and eXtended Markup Language generators for CCSDS messages.
* @author Luc Maisonobe
* @since 11.0
*/
public abstract class AbstractGenerator implements Generator {
/** New line separator for output file. */
private static final char NEW_LINE = '\n';
/** Destination of generated output. */
private final Appendable output;
/** Output name for error messages. */
private final String outputName;
/** Maximum offset for relative dates.
* @since 12.0
*/
private final double maxRelativeOffset;
/** Flag for writing units. */
private final boolean writeUnits;
/** Sections stack. */
private final Deque<String> sections;
/** Map from SI Units name to CCSDS unit names. */
private final Map<String, String> siToCcsds;
/** Simple constructor.
* @param output destination of generated output
* @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 writeUnits if true, units must be written
*/
public AbstractGenerator(final Appendable output, final String outputName,
final double maxRelativeOffset, final boolean writeUnits) {
this.output = output;
this.outputName = outputName;
this.maxRelativeOffset = maxRelativeOffset;
this.writeUnits = writeUnits;
this.sections = new ArrayDeque<>();
this.siToCcsds = new HashMap<>();
}
/** {@inheritDoc} */
@Override
public String getOutputName() {
return outputName;
}
/** Check if unit must be written.
* @param unit entry unit
* @return true if units must be written
*/
public boolean writeUnits(final Unit unit) {
return writeUnits &&
unit != null &&
!unit.getName().equals(Unit.NONE.getName()) &&
!unit.getName().equals(Unit.ONE.getName());
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
// get out from all sections properly
while (!sections.isEmpty()) {
exitSection();
}
}
/** {@inheritDoc} */
@Override
public void newLine() throws IOException {
output.append(NEW_LINE);
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final List<String> value, final boolean mandatory) throws IOException {
if (value == null || value.isEmpty()) {
complain(key, mandatory);
} else {
final StringBuilder builder = new StringBuilder();
boolean first = true;
for (final String v : value) {
if (!first) {
builder.append(',');
}
builder.append(v);
first = false;
}
writeEntry(key, builder.toString(), null, mandatory);
}
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final Enum<?> value, final boolean mandatory) throws IOException {
writeEntry(key, value == null ? null : value.name(), null, mandatory);
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final TimeConverter converter, final AbsoluteDate date,
final boolean forceCalendar, final boolean mandatory)
throws IOException {
if (date == null) {
writeEntry(key, (String) null, null, mandatory);
} else {
writeEntry(key,
forceCalendar ? dateToCalendarString(converter, date) : dateToString(converter, date),
null,
mandatory);
}
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final double value, final Unit unit, final boolean mandatory) throws IOException {
writeEntry(key, doubleToString(unit.fromSI(value)), unit, mandatory);
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final Double value, final Unit unit, final boolean mandatory) throws IOException {
writeEntry(key, value == null ? (String) null : doubleToString(unit.fromSI(value.doubleValue())), unit, mandatory);
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final char value, final boolean mandatory) throws IOException {
writeEntry(key, Character.toString(value), null, mandatory);
}
/** {@inheritDoc} */
@Override
public void writeEntry(final String key, final int value, final boolean mandatory) throws IOException {
writeEntry(key, Integer.toString(value), null, mandatory);
}
/** {@inheritDoc} */
@Override
public void writeRawData(final char data) throws IOException {
output.append(data);
}
/** {@inheritDoc} */
@Override
public void writeRawData(final CharSequence data) throws IOException {
output.append(data);
}
/** {@inheritDoc} */
@Override
public void enterSection(final String name) throws IOException {
sections.offerLast(name);
}
/** {@inheritDoc} */
@Override
public String exitSection() throws IOException {
return sections.pollLast();
}
/** Complain about a missing value.
* @param key the keyword to write
* @param mandatory if true, triggers en exception, otherwise do nothing
*/
protected void complain(final String key, final boolean mandatory) {
if (mandatory) {
throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD, key, outputName);
}
}
/** {@inheritDoc} */
@Override
public String doubleToString(final double value) {
return Double.isNaN(value) ? null : AccurateFormatter.format(value);
}
/** {@inheritDoc} */
@Override
public String dateToString(final TimeConverter converter, final AbsoluteDate date) {
if (converter.getReferenceDate() != null) {
final double relative = date.durationFrom(converter.getReferenceDate());
if (FastMath.abs(relative) <= maxRelativeOffset) {
// we can use a relative date
return AccurateFormatter.format(relative);
}
}
// display the date as calendar elements
return dateToCalendarString(converter, date);
}
/** {@inheritDoc} */
@Override
public String dateToCalendarString(final TimeConverter converter, final AbsoluteDate date) {
final DateTimeComponents dt = converter.components(date);
return dateToString(dt.getDate().getYear(), dt.getDate().getMonth(), dt.getDate().getDay(),
dt.getTime().getHour(), dt.getTime().getMinute(), dt.getTime().getSecond());
}
/** {@inheritDoc} */
@Override
public String dateToString(final int year, final int month, final int day,
final int hour, final int minute, final double seconds) {
return AccurateFormatter.format(year, month, day, hour, minute, seconds);
}
/** {@inheritDoc} */
@Override
public String unitsListToString(final List<Unit> units) {
if (units == null || units.isEmpty()) {
// nothing to output
return null;
}
final StringBuilder builder = new StringBuilder();
builder.append('[');
boolean first = true;
for (final Unit unit : units) {
if (!first) {
builder.append(',');
}
builder.append(siToCcsdsName(unit.getName()));
first = false;
}
builder.append(']');
return builder.toString();
}
/** {@inheritDoc} */
@Override
public String siToCcsdsName(final String siName) {
if (!siToCcsds.containsKey(siName)) {
// build a name using only CCSDS syntax
final StringBuilder builder = new StringBuilder();
// parse the SI name that may contain fancy features like unicode superscripts, square roots sign…
final List<PowerTerm> terms = Parser.buildTermsList(siName);
if (terms == null) {
builder.append("n/a");
} else {
// put the positive exponent first
boolean first = true;
for (final PowerTerm term : terms) {
if (term.getExponent().getNumerator() >= 0) {
if (!first) {
builder.append('*');
}
appendScale(builder, term.getScale());
appendBase(builder, term.getBase());
appendExponent(builder, term.getExponent());
first = false;
}
}
if (first) {
// no positive exponents at all, we add "1" to get something like "1/s"
builder.append('1');
}
// put the negative exponents last
for (final PowerTerm term : terms) {
if (term.getExponent().getNumerator() < 0) {
builder.append('/');
appendScale(builder, term.getScale());
appendBase(builder, term.getBase());
appendExponent(builder, term.getExponent().negate());
}
}
}
// put the converted name in the map for reuse
siToCcsds.put(siName, builder.toString());
}
return siToCcsds.get(siName);
}
/** Append a scaling factor.
* @param builder builder to which term must be added
* @param scale scaling factor
*/
private void appendScale(final StringBuilder builder, final double scale) {
final int factor = (int) FastMath.rint(scale);
if (FastMath.abs(scale - factor) > 1.0e-12) {
// this should never happen with CCSDS units
throw new OrekitInternalError(null);
}
if (factor != 1) {
builder.append(factor);
}
}
/** Append a base term.
* @param builder builder to which term must be added
* @param base base term
*/
private void appendBase(final StringBuilder builder, final CharSequence base) {
if ("°".equals(base) || "◦".equals(base)) {
builder.append("deg");
} else {
builder.append(base);
}
}
/** Append an exponent.
* @param builder builder to which term must be added
* @param exponent exponent to add
*/
private void appendExponent(final StringBuilder builder, final Fraction exponent) {
if (!exponent.equals(Fraction.ONE)) {
builder.append("**");
if (exponent.equals(Fraction.ONE_HALF)) {
builder.append("0.5");
} else if (exponent.getNumerator() == 3 && exponent.getDenominator() == 2) {
builder.append("1.5");
} else {
builder.append(exponent);
}
}
}
}