AbstractGenerator.java

  1. /* Copyright 2002-2025 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.ccsds.utils.generation;

  18. import java.io.IOException;
  19. import java.util.ArrayDeque;
  20. import java.util.Deque;
  21. import java.util.HashMap;
  22. import java.util.List;
  23. import java.util.Map;

  24. import org.hipparchus.fraction.Fraction;
  25. import org.hipparchus.util.FastMath;
  26. import org.orekit.errors.OrekitException;
  27. import org.orekit.errors.OrekitInternalError;
  28. import org.orekit.errors.OrekitMessages;
  29. import org.orekit.files.ccsds.definitions.TimeConverter;
  30. import org.orekit.time.AbsoluteDate;
  31. import org.orekit.time.DateTimeComponents;
  32. import org.orekit.utils.AccurateFormatter;
  33. import org.orekit.utils.Formatter;
  34. import org.orekit.utils.units.Parser;
  35. import org.orekit.utils.units.PowerTerm;
  36. import org.orekit.utils.units.Unit;

  37. /** Base class for both Key-Value Notation and eXtended Markup Language generators for CCSDS messages.
  38.  * @author Luc Maisonobe
  39.  * @since 11.0
  40.  */
  41. public abstract class AbstractGenerator implements Generator {

  42.     /** New line separator for output file. */
  43.     private static final char NEW_LINE = '\n';

  44.     /** Destination of generated output. */
  45.     private final Appendable output;

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

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

  52.     /** Flag for writing units. */
  53.     private final boolean writeUnits;

  54.     /** Sections stack. */
  55.     private final Deque<String> sections;

  56.     /** Map from SI Units name to CCSDS unit names. */
  57.     private final Map<String, String> siToCcsds;

  58.     /** Used to format dates and doubles to string. */
  59.     private final Formatter formatter;

  60.     /** Simple constructor.
  61.      * @param output destination of generated output
  62.      * @param outputName output name for error messages
  63.      * @param maxRelativeOffset maximum offset in seconds to use relative dates
  64.      * (if a date is too far from reference, it will be displayed as calendar elements)
  65.      * @param formatter how to format date and double to string.
  66.      * @param writeUnits if true, units must be written
  67.      */
  68.     public AbstractGenerator(final Appendable output, final String outputName,
  69.                              final double maxRelativeOffset, final boolean writeUnits,
  70.                              final Formatter formatter) {
  71.         this.output            = output;
  72.         this.outputName        = outputName;
  73.         this.maxRelativeOffset = maxRelativeOffset;
  74.         this.writeUnits        = writeUnits;
  75.         this.sections          = new ArrayDeque<>();
  76.         this.siToCcsds         = new HashMap<>();
  77.         this.formatter = formatter;
  78.     }

  79.     /** Simple constructor.
  80.      * @param output destination of generated output
  81.      * @param outputName output name for error messages
  82.      * @param maxRelativeOffset maximum offset in seconds to use relative dates
  83.      * (if a date is too far from reference, it will be displayed as calendar elements)
  84.      * @param writeUnits if true, units must be written
  85.      * @deprecated since 13.0, since does not allow user to specify formatter. This defaults to {@link AccurateFormatter}
  86.      * Use {@link AbstractGenerator#AbstractGenerator(Appendable, String, double, boolean, Formatter)} instead.
  87.      */
  88.     @Deprecated
  89.     public AbstractGenerator(final Appendable output, final String outputName,
  90.                              final double maxRelativeOffset, final boolean writeUnits) {
  91.         this(output, outputName, maxRelativeOffset, writeUnits, new AccurateFormatter());
  92.     }

  93.     /** {@inheritDoc} */
  94.     @Override
  95.     public String getOutputName() {
  96.         return outputName;
  97.     }

  98.     /** {@inheritDoc} */
  99.     @Override
  100.     public Formatter getFormatter() { return formatter; }

  101.     /** Check if unit must be written.
  102.      * @param unit entry unit
  103.      * @return true if units must be written
  104.      */
  105.     public boolean writeUnits(final Unit unit) {
  106.         return writeUnits &&
  107.                unit != null &&
  108.                !unit.getName().equals(Unit.NONE.getName()) &&
  109.                !unit.getName().equals(Unit.ONE.getName());
  110.     }

  111.     /** {@inheritDoc} */
  112.     @Override
  113.     public void close() throws IOException {

  114.         // get out from all sections properly
  115.         while (!sections.isEmpty()) {
  116.             exitSection();
  117.         }

  118.     }

  119.     /** {@inheritDoc} */
  120.     @Override
  121.     public void newLine() throws IOException {
  122.         output.append(NEW_LINE);
  123.     }

  124.     /** {@inheritDoc} */
  125.     @Override
  126.     public void writeEntry(final String key, final List<String> value, final boolean mandatory) throws IOException {
  127.         if (value == null || value.isEmpty()) {
  128.             complain(key, mandatory);
  129.         } else {
  130.             final StringBuilder builder = new StringBuilder();
  131.             boolean first = true;
  132.             for (final String v : value) {
  133.                 if (!first) {
  134.                     builder.append(',');
  135.                 }
  136.                 builder.append(v);
  137.                 first = false;
  138.             }
  139.             writeEntry(key, builder.toString(), null, mandatory);
  140.         }
  141.     }

  142.     /** {@inheritDoc} */
  143.     @Override
  144.     public void writeEntry(final String key, final Enum<?> value, final boolean mandatory) throws IOException {
  145.         writeEntry(key, value == null ? null : value.name(), null, mandatory);
  146.     }

  147.     /** {@inheritDoc} */
  148.     @Override
  149.     public void writeEntry(final String key, final TimeConverter converter, final AbsoluteDate date,
  150.                            final boolean forceCalendar, final boolean mandatory)
  151.         throws IOException {
  152.         if (date == null) {
  153.             writeEntry(key, (String) null, null, mandatory);
  154.         } else {
  155.             writeEntry(key,
  156.                        forceCalendar ? dateToCalendarString(converter, date) : dateToString(converter, date),
  157.                        null,
  158.                        mandatory);
  159.         }
  160.     }

  161.     /** {@inheritDoc} */
  162.     @Override
  163.     public void writeEntry(final String key, final double value, final Unit unit, final boolean mandatory) throws IOException {
  164.         writeEntry(key, doubleToString(unit.fromSI(value)), unit, mandatory);
  165.     }

  166.     /** {@inheritDoc} */
  167.     @Override
  168.     public void writeEntry(final String key, final Double value, final Unit unit, final boolean mandatory) throws IOException {
  169.         writeEntry(key, value == null ? (String) null : doubleToString(unit.fromSI(value.doubleValue())), unit, mandatory);
  170.     }

  171.     /** {@inheritDoc} */
  172.     @Override
  173.     public void writeEntry(final String key, final char value, final boolean mandatory) throws IOException {
  174.         writeEntry(key, Character.toString(value), null, mandatory);
  175.     }

  176.     /** {@inheritDoc} */
  177.     @Override
  178.     public void writeEntry(final String key, final int value, final boolean mandatory) throws IOException {
  179.         writeEntry(key, Integer.toString(value), null, mandatory);
  180.     }

  181.     /** {@inheritDoc} */
  182.     @Override
  183.     public void writeRawData(final char data) throws IOException {
  184.         output.append(data);
  185.     }

  186.     /** {@inheritDoc} */
  187.     @Override
  188.     public void writeRawData(final CharSequence data) throws IOException {
  189.         output.append(data);
  190.     }

  191.     /** {@inheritDoc} */
  192.     @Override
  193.     public void enterSection(final String name) throws IOException {
  194.         sections.offerLast(name);
  195.     }

  196.     /** {@inheritDoc} */
  197.     @Override
  198.     public String exitSection() throws IOException {
  199.         return sections.pollLast();
  200.     }

  201.     /** Complain about a missing value.
  202.      * @param key the keyword to write
  203.      * @param mandatory if true, triggers en exception, otherwise do nothing
  204.      */
  205.     protected void complain(final String key, final boolean mandatory) {
  206.         if (mandatory) {
  207.             throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD, key, outputName);
  208.         }
  209.     }

  210.     /** {@inheritDoc} */
  211.     @Override
  212.     public String doubleToString(final double value) {
  213.         return Double.isNaN(value) ? null : formatter.toString(value);
  214.     }

  215.     /** {@inheritDoc} */
  216.     @Override
  217.     public String dateToString(final TimeConverter converter, final AbsoluteDate date) {

  218.         if (converter.getReferenceDate() != null) {
  219.             final double relative = date.durationFrom(converter.getReferenceDate());
  220.             if (FastMath.abs(relative) <= maxRelativeOffset) {
  221.                 // we can use a relative date
  222.                 return formatter.toString(relative);
  223.             }
  224.         }

  225.         // display the date as calendar elements
  226.         return dateToCalendarString(converter, date);

  227.     }

  228.     /** {@inheritDoc} */
  229.     @Override
  230.     public String dateToCalendarString(final TimeConverter converter, final AbsoluteDate date) {
  231.         final DateTimeComponents dt = converter.components(date);
  232.         return dateToString(dt.getDate().getYear(), dt.getDate().getMonth(), dt.getDate().getDay(),
  233.                             dt.getTime().getHour(), dt.getTime().getMinute(), dt.getTime().getSecond());
  234.     }

  235.     /** {@inheritDoc} */
  236.     @Override
  237.     public String dateToString(final int year, final int month, final int day,
  238.                                final int hour, final int minute, final double seconds) {
  239.         return formatter.toString(year, month, day, hour, minute, seconds);
  240.     }

  241.     /** {@inheritDoc} */
  242.     @Override
  243.     public String unitsListToString(final List<Unit> units) {

  244.         if (units == null || units.isEmpty()) {
  245.             // nothing to output
  246.             return null;
  247.         }

  248.         final StringBuilder builder = new StringBuilder();
  249.         builder.append('[');
  250.         boolean first = true;
  251.         for (final Unit unit : units) {
  252.             if (!first) {
  253.                 builder.append(',');
  254.             }
  255.             builder.append(siToCcsdsName(unit.getName()));
  256.             first = false;
  257.         }
  258.         builder.append(']');
  259.         return builder.toString();

  260.     }

  261.     /** {@inheritDoc} */
  262.     @Override
  263.     public String siToCcsdsName(final String siName) {

  264.         if (!siToCcsds.containsKey(siName)) {

  265.             // build a name using only CCSDS syntax
  266.             final StringBuilder builder = new StringBuilder();

  267.             // parse the SI name that may contain fancy features like unicode superscripts, square roots sign…
  268.             final List<PowerTerm> terms = Parser.buildTermsList(siName);

  269.             if (terms == null) {
  270.                 builder.append("n/a");
  271.             } else {

  272.                 // put the positive exponent first
  273.                 boolean first = true;
  274.                 for (final PowerTerm term : terms) {
  275.                     if (term.getExponent().getNumerator() >= 0) {
  276.                         if (!first) {
  277.                             builder.append('*');
  278.                         }
  279.                         appendScale(builder, term.getScale());
  280.                         appendBase(builder, term.getBase());
  281.                         appendExponent(builder, term.getExponent());
  282.                         first = false;
  283.                     }
  284.                 }

  285.                 if (first) {
  286.                     // no positive exponents at all, we add "1" to get something like "1/s"
  287.                     builder.append('1');
  288.                 }

  289.                 // put the negative exponents last
  290.                 for (final PowerTerm term : terms) {
  291.                     if (term.getExponent().getNumerator() < 0) {
  292.                         builder.append('/');
  293.                         appendScale(builder, term.getScale());
  294.                         appendBase(builder, term.getBase());
  295.                         appendExponent(builder, term.getExponent().negate());
  296.                     }
  297.                 }

  298.             }

  299.             // put the converted name in the map for reuse
  300.             siToCcsds.put(siName, builder.toString());

  301.         }

  302.         return siToCcsds.get(siName);

  303.     }

  304.     /** Append a scaling factor.
  305.      * @param builder builder to which term must be added
  306.      * @param scale scaling factor
  307.      */
  308.     private void appendScale(final StringBuilder builder, final double scale) {
  309.         final int factor = (int) FastMath.rint(scale);
  310.         if (FastMath.abs(scale - factor) > 1.0e-12) {
  311.             // this should never happen with CCSDS units
  312.             throw new OrekitInternalError(null);
  313.         }
  314.         if (factor != 1) {
  315.             builder.append(factor);
  316.         }
  317.     }

  318.     /** Append a base term.
  319.      * @param builder builder to which term must be added
  320.      * @param base base term
  321.      */
  322.     private void appendBase(final StringBuilder builder, final CharSequence base) {
  323.         if ("°".equals(base) || "◦".equals(base)) {
  324.             builder.append("deg");
  325.         } else {
  326.             builder.append(base);
  327.         }
  328.     }

  329.     /** Append an exponent.
  330.      * @param builder builder to which term must be added
  331.      * @param exponent exponent to add
  332.      */
  333.     private void appendExponent(final StringBuilder builder, final Fraction exponent) {
  334.         if (!exponent.equals(Fraction.ONE)) {
  335.             builder.append("**");
  336.             if (exponent.equals(Fraction.ONE_HALF)) {
  337.                 builder.append("0.5");
  338.             } else if (exponent.getNumerator() == 3 && exponent.getDenominator() == 2) {
  339.                 builder.append("1.5");
  340.             } else {
  341.                 builder.append(exponent);
  342.             }
  343.         }
  344.     }

  345. }