AbstractGenerator.java

  1. /* Copyright 2002-2024 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.units.Parser;
  34. import org.orekit.utils.units.PowerTerm;
  35. import org.orekit.utils.units.Unit;

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

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

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

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

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

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

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

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

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

  73.     /** {@inheritDoc} */
  74.     @Override
  75.     public String getOutputName() {
  76.         return outputName;
  77.     }

  78.     /** Check if unit must be written.
  79.      * @param unit entry unit
  80.      * @return true if units must be written
  81.      */
  82.     public boolean writeUnits(final Unit unit) {
  83.         return writeUnits &&
  84.                unit != null &&
  85.                !unit.getName().equals(Unit.NONE.getName()) &&
  86.                !unit.getName().equals(Unit.ONE.getName());
  87.     }

  88.     /** {@inheritDoc} */
  89.     @Override
  90.     public void close() throws IOException {

  91.         // get out from all sections properly
  92.         while (!sections.isEmpty()) {
  93.             exitSection();
  94.         }

  95.     }

  96.     /** {@inheritDoc} */
  97.     @Override
  98.     public void newLine() throws IOException {
  99.         output.append(NEW_LINE);
  100.     }

  101.     /** {@inheritDoc} */
  102.     @Override
  103.     public void writeEntry(final String key, final List<String> value, final boolean mandatory) throws IOException {
  104.         if (value == null || value.isEmpty()) {
  105.             complain(key, mandatory);
  106.         } else {
  107.             final StringBuilder builder = new StringBuilder();
  108.             boolean first = true;
  109.             for (final String v : value) {
  110.                 if (!first) {
  111.                     builder.append(',');
  112.                 }
  113.                 builder.append(v);
  114.                 first = false;
  115.             }
  116.             writeEntry(key, builder.toString(), null, mandatory);
  117.         }
  118.     }

  119.     /** {@inheritDoc} */
  120.     @Override
  121.     public void writeEntry(final String key, final Enum<?> value, final boolean mandatory) throws IOException {
  122.         writeEntry(key, value == null ? null : value.name(), null, mandatory);
  123.     }

  124.     /** {@inheritDoc} */
  125.     @Override
  126.     public void writeEntry(final String key, final TimeConverter converter, final AbsoluteDate date,
  127.                            final boolean forceCalendar, final boolean mandatory)
  128.         throws IOException {
  129.         if (date == null) {
  130.             writeEntry(key, (String) null, null, mandatory);
  131.         } else {
  132.             writeEntry(key,
  133.                        forceCalendar ? dateToCalendarString(converter, date) : dateToString(converter, date),
  134.                        null,
  135.                        mandatory);
  136.         }
  137.     }

  138.     /** {@inheritDoc} */
  139.     @Override
  140.     public void writeEntry(final String key, final double value, final Unit unit, final boolean mandatory) throws IOException {
  141.         writeEntry(key, doubleToString(unit.fromSI(value)), unit, mandatory);
  142.     }

  143.     /** {@inheritDoc} */
  144.     @Override
  145.     public void writeEntry(final String key, final Double value, final Unit unit, final boolean mandatory) throws IOException {
  146.         writeEntry(key, value == null ? (String) null : doubleToString(unit.fromSI(value.doubleValue())), unit, mandatory);
  147.     }

  148.     /** {@inheritDoc} */
  149.     @Override
  150.     public void writeEntry(final String key, final char value, final boolean mandatory) throws IOException {
  151.         writeEntry(key, Character.toString(value), null, mandatory);
  152.     }

  153.     /** {@inheritDoc} */
  154.     @Override
  155.     public void writeEntry(final String key, final int value, final boolean mandatory) throws IOException {
  156.         writeEntry(key, Integer.toString(value), null, mandatory);
  157.     }

  158.     /** {@inheritDoc} */
  159.     @Override
  160.     public void writeRawData(final char data) throws IOException {
  161.         output.append(data);
  162.     }

  163.     /** {@inheritDoc} */
  164.     @Override
  165.     public void writeRawData(final CharSequence data) throws IOException {
  166.         output.append(data);
  167.     }

  168.     /** {@inheritDoc} */
  169.     @Override
  170.     public void enterSection(final String name) throws IOException {
  171.         sections.offerLast(name);
  172.     }

  173.     /** {@inheritDoc} */
  174.     @Override
  175.     public String exitSection() throws IOException {
  176.         return sections.pollLast();
  177.     }

  178.     /** Complain about a missing value.
  179.      * @param key the keyword to write
  180.      * @param mandatory if true, triggers en exception, otherwise do nothing
  181.      */
  182.     protected void complain(final String key, final boolean mandatory) {
  183.         if (mandatory) {
  184.             throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD, key, outputName);
  185.         }
  186.     }

  187.     /** {@inheritDoc} */
  188.     @Override
  189.     public String doubleToString(final double value) {
  190.         return Double.isNaN(value) ? null : AccurateFormatter.format(value);
  191.     }

  192.     /** {@inheritDoc} */
  193.     @Override
  194.     public String dateToString(final TimeConverter converter, final AbsoluteDate date) {

  195.         if (converter.getReferenceDate() != null) {
  196.             final double relative = date.durationFrom(converter.getReferenceDate());
  197.             if (FastMath.abs(relative) <= maxRelativeOffset) {
  198.                 // we can use a relative date
  199.                 return AccurateFormatter.format(relative);
  200.             }
  201.         }

  202.         // display the date as calendar elements
  203.         return dateToCalendarString(converter, date);

  204.     }

  205.     /** {@inheritDoc} */
  206.     @Override
  207.     public String dateToCalendarString(final TimeConverter converter, final AbsoluteDate date) {
  208.         final DateTimeComponents dt = converter.components(date);
  209.         return dateToString(dt.getDate().getYear(), dt.getDate().getMonth(), dt.getDate().getDay(),
  210.                             dt.getTime().getHour(), dt.getTime().getMinute(), dt.getTime().getSecond());
  211.     }

  212.     /** {@inheritDoc} */
  213.     @Override
  214.     public String dateToString(final int year, final int month, final int day,
  215.                                final int hour, final int minute, final double seconds) {
  216.         return AccurateFormatter.format(year, month, day, hour, minute, seconds);
  217.     }

  218.     /** {@inheritDoc} */
  219.     @Override
  220.     public String unitsListToString(final List<Unit> units) {

  221.         if (units == null || units.isEmpty()) {
  222.             // nothing to output
  223.             return null;
  224.         }

  225.         final StringBuilder builder = new StringBuilder();
  226.         builder.append('[');
  227.         boolean first = true;
  228.         for (final Unit unit : units) {
  229.             if (!first) {
  230.                 builder.append(',');
  231.             }
  232.             builder.append(siToCcsdsName(unit.getName()));
  233.             first = false;
  234.         }
  235.         builder.append(']');
  236.         return builder.toString();

  237.     }

  238.     /** {@inheritDoc} */
  239.     @Override
  240.     public String siToCcsdsName(final String siName) {

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

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

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

  246.             if (terms == null) {
  247.                 builder.append("n/a");
  248.             } else {

  249.                 // put the positive exponent first
  250.                 boolean first = true;
  251.                 for (final PowerTerm term : terms) {
  252.                     if (term.getExponent().getNumerator() >= 0) {
  253.                         if (!first) {
  254.                             builder.append('*');
  255.                         }
  256.                         appendScale(builder, term.getScale());
  257.                         appendBase(builder, term.getBase());
  258.                         appendExponent(builder, term.getExponent());
  259.                         first = false;
  260.                     }
  261.                 }

  262.                 if (first) {
  263.                     // no positive exponents at all, we add "1" to get something like "1/s"
  264.                     builder.append('1');
  265.                 }

  266.                 // put the negative exponents last
  267.                 for (final PowerTerm term : terms) {
  268.                     if (term.getExponent().getNumerator() < 0) {
  269.                         builder.append('/');
  270.                         appendScale(builder, term.getScale());
  271.                         appendBase(builder, term.getBase());
  272.                         appendExponent(builder, term.getExponent().negate());
  273.                     }
  274.                 }

  275.             }

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

  278.         }

  279.         return siToCcsds.get(siName);

  280.     }

  281.     /** Append a scaling factor.
  282.      * @param builder builder to which term must be added
  283.      * @param scale scaling factor
  284.      */
  285.     private void appendScale(final StringBuilder builder, final double scale) {
  286.         final int factor = (int) FastMath.rint(scale);
  287.         if (FastMath.abs(scale - factor) > 1.0e-12) {
  288.             // this should never happen with CCSDS units
  289.             throw new OrekitInternalError(null);
  290.         }
  291.         if (factor != 1) {
  292.             builder.append(factor);
  293.         }
  294.     }

  295.     /** Append a base term.
  296.      * @param builder builder to which term must be added
  297.      * @param base base term
  298.      */
  299.     private void appendBase(final StringBuilder builder, final CharSequence base) {
  300.         if ("°".equals(base) || "◦".equals(base)) {
  301.             builder.append("deg");
  302.         } else {
  303.             builder.append(base);
  304.         }
  305.     }

  306.     /** Append an exponent.
  307.      * @param builder builder to which term must be added
  308.      * @param exponent exponent to add
  309.      */
  310.     private void appendExponent(final StringBuilder builder, final Fraction exponent) {
  311.         if (!exponent.equals(Fraction.ONE)) {
  312.             builder.append("**");
  313.             if (exponent.equals(Fraction.ONE_HALF)) {
  314.                 builder.append("0.5");
  315.             } else if (exponent.getNumerator() == 3 && exponent.getDenominator() == 2) {
  316.                 builder.append("1.5");
  317.             } else {
  318.                 builder.append(exponent);
  319.             }
  320.         }
  321.     }

  322. }