1   /* Copyright 2002-2026 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  
19  import java.io.IOException;
20  import java.util.ArrayDeque;
21  import java.util.Deque;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import org.hipparchus.fraction.Fraction;
27  import org.hipparchus.util.FastMath;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitInternalError;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.files.ccsds.definitions.TimeConverter;
32  import org.orekit.time.AbsoluteDate;
33  import org.orekit.time.DateTimeComponents;
34  import org.orekit.utils.Formatter;
35  import org.orekit.utils.units.Parser;
36  import org.orekit.utils.units.PowerTerm;
37  import org.orekit.utils.units.Unit;
38  
39  /** Base class for both Key-Value Notation and eXtended Markup Language generators for CCSDS messages.
40   * @author Luc Maisonobe
41   * @since 11.0
42   */
43  public abstract class AbstractGenerator implements Generator {
44  
45      /** New line separator for output file. */
46      private static final char NEW_LINE = '\n';
47  
48      /** Destination of generated output. */
49      private final Appendable output;
50  
51      /** Output name for error messages. */
52      private final String outputName;
53  
54      /** Maximum offset for relative dates.
55       * @since 12.0
56       */
57      private final double maxRelativeOffset;
58  
59      /** Flag for writing units. */
60      private final boolean writeUnits;
61  
62      /** Sections stack. */
63      private final Deque<String> sections;
64  
65      /** Map from SI Units name to CCSDS unit names. */
66      private final Map<String, String> siToCcsds;
67  
68      /** Used to format dates and doubles to string. */
69      private final Formatter formatter;
70  
71      /** Simple constructor.
72       * @param output destination of generated output
73       * @param outputName output name for error messages
74       * @param maxRelativeOffset maximum offset in seconds to use relative dates
75       * (if a date is too far from reference, it will be displayed as calendar elements)
76       * @param formatter how to format date and double to string.
77       * @param writeUnits if true, units must be written
78       */
79      protected AbstractGenerator(final Appendable output, final String outputName,
80                                  final double maxRelativeOffset, final boolean writeUnits,
81                                  final Formatter formatter) {
82          this.output            = output;
83          this.outputName        = outputName;
84          this.maxRelativeOffset = maxRelativeOffset;
85          this.writeUnits        = writeUnits;
86          this.sections          = new ArrayDeque<>();
87          this.siToCcsds         = new HashMap<>();
88          this.formatter = formatter;
89      }
90  
91      /** {@inheritDoc} */
92      @Override
93      public String getOutputName() {
94          return outputName;
95      }
96  
97      /** {@inheritDoc} */
98      @Override
99      public Formatter getFormatter() { return formatter; }
100 
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 
112     /** {@inheritDoc} */
113     @Override
114     public void close() throws IOException {
115 
116         // get out from all sections properly
117         while (!sections.isEmpty()) {
118             exitSection();
119         }
120 
121     }
122 
123     /** {@inheritDoc} */
124     @Override
125     public void newLine() throws IOException {
126         output.append(NEW_LINE);
127     }
128 
129     /** {@inheritDoc} */
130     @Override
131     public void writeEntry(final String key, final List<String> value, final boolean mandatory) throws IOException {
132         if (value == null || value.isEmpty()) {
133             complain(key, mandatory);
134         } else {
135             final StringBuilder builder = new StringBuilder();
136             boolean first = true;
137             for (final String v : value) {
138                 if (!first) {
139                     builder.append(',');
140                 }
141                 builder.append(v);
142                 first = false;
143             }
144             writeEntry(key, builder.toString(), null, mandatory);
145         }
146     }
147 
148     /** {@inheritDoc} */
149     @Override
150     public void writeEntry(final String key, final Enum<?> value, final boolean mandatory) throws IOException {
151         writeEntry(key, value == null ? null : value.name(), null, mandatory);
152     }
153 
154     /** {@inheritDoc} */
155     @Override
156     public void writeEntry(final String key, final TimeConverter converter, final AbsoluteDate date,
157                            final boolean forceCalendar, final boolean mandatory)
158         throws IOException {
159         if (date == null) {
160             writeEntry(key, (String) null, null, mandatory);
161         } else {
162             writeEntry(key,
163                        forceCalendar ? dateToCalendarString(converter, date) : dateToString(converter, date),
164                        null,
165                        mandatory);
166         }
167     }
168 
169     /** {@inheritDoc} */
170     @Override
171     public void writeEntry(final String key, final double value, final Unit unit, final boolean mandatory) throws IOException {
172         writeEntry(key, doubleToString(unit.fromSI(value)), unit, mandatory);
173     }
174 
175     /** {@inheritDoc} */
176     @Override
177     public void writeEntry(final String key, final Double value, final Unit unit, final boolean mandatory) throws IOException {
178         writeEntry(key, value == null ? null : doubleToString(unit.fromSI(value.doubleValue())), unit, mandatory);
179     }
180 
181     /** {@inheritDoc} */
182     @Override
183     public void writeEntry(final String key, final char value, final boolean mandatory) throws IOException {
184         writeEntry(key, Character.toString(value), null, mandatory);
185     }
186 
187     /** {@inheritDoc} */
188     @Override
189     public void writeEntry(final String key, final int value, final boolean mandatory) throws IOException {
190         writeEntry(key, Integer.toString(value), null, mandatory);
191     }
192 
193     /** {@inheritDoc} */
194     @Override
195     public void writeRawData(final char data) throws IOException {
196         output.append(data);
197     }
198 
199     /** {@inheritDoc} */
200     @Override
201     public void writeRawData(final CharSequence data) throws IOException {
202         output.append(data);
203     }
204 
205     /** {@inheritDoc} */
206     @Override
207     public void enterSection(final String name) throws IOException {
208         sections.offerLast(name);
209     }
210 
211     /** {@inheritDoc} */
212     @Override
213     public String exitSection() throws IOException {
214         return sections.pollLast();
215     }
216 
217     /** Complain about a missing value.
218      * @param key the keyword to write
219      * @param mandatory if true, triggers en exception, otherwise do nothing
220      */
221     protected void complain(final String key, final boolean mandatory) {
222         if (mandatory) {
223             throw new OrekitException(OrekitMessages.CCSDS_MISSING_KEYWORD, key, outputName);
224         }
225     }
226 
227     /** {@inheritDoc} */
228     @Override
229     public String doubleToString(final double value) {
230         return Double.isNaN(value) ? null : formatter.toString(value);
231     }
232 
233     /** {@inheritDoc} */
234     @Override
235     public String dateToString(final TimeConverter converter, final AbsoluteDate date) {
236 
237         if (converter.getReferenceDate() != null) {
238             final double relative = date.durationFrom(converter.getReferenceDate());
239             if (FastMath.abs(relative) <= maxRelativeOffset) {
240                 // we can use a relative date
241                 return formatter.toString(relative);
242             }
243         }
244 
245         // display the date as calendar elements
246         return dateToCalendarString(converter, date);
247 
248     }
249 
250     /** {@inheritDoc} */
251     @Override
252     public String dateToCalendarString(final TimeConverter converter, final AbsoluteDate date) {
253         final DateTimeComponents dt = converter.components(date);
254         return dateToString(dt.getDate().getYear(), dt.getDate().getMonth(), dt.getDate().getDay(),
255                             dt.getTime().getHour(), dt.getTime().getMinute(), dt.getTime().getSecond());
256     }
257 
258     /** {@inheritDoc} */
259     @Override
260     public String dateToString(final int year, final int month, final int day,
261                                final int hour, final int minute, final double seconds) {
262         return formatter.toString(year, month, day, hour, minute, seconds);
263     }
264 
265     /** {@inheritDoc} */
266     @Override
267     public String unitsListToString(final List<Unit> units) {
268 
269         if (units == null || units.isEmpty()) {
270             // nothing to output
271             return null;
272         }
273 
274         final StringBuilder builder = new StringBuilder();
275         builder.append('[');
276         boolean first = true;
277         for (final Unit unit : units) {
278             if (!first) {
279                 builder.append(',');
280             }
281             builder.append(siToCcsdsName(unit.getName()));
282             first = false;
283         }
284         builder.append(']');
285         return builder.toString();
286 
287     }
288 
289     /** {@inheritDoc} */
290     @Override
291     public String siToCcsdsName(final String siName) {
292 
293         if (!siToCcsds.containsKey(siName)) {
294 
295             // build a name using only CCSDS syntax
296             final StringBuilder builder = new StringBuilder();
297 
298             // parse the SI name that may contain fancy features like unicode superscripts, square roots sign…
299             final List<PowerTerm> terms = Parser.buildTermsList(siName);
300 
301             if (terms == null) {
302                 builder.append("n/a");
303             } else {
304 
305                 // put the positive exponent first
306                 boolean first = true;
307                 for (final PowerTerm term : terms) {
308                     if (term.getExponent().getNumerator() >= 0) {
309                         if (!first) {
310                             builder.append('*');
311                         }
312                         appendScale(builder, term.getScale());
313                         appendBase(builder, term.getBase());
314                         appendExponent(builder, term.getExponent());
315                         first = false;
316                     }
317                 }
318 
319                 if (first) {
320                     // no positive exponents at all, we add "1" to get something like "1/s"
321                     builder.append('1');
322                 }
323 
324                 // put the negative exponents last
325                 for (final PowerTerm term : terms) {
326                     if (term.getExponent().getNumerator() < 0) {
327                         builder.append('/');
328                         appendScale(builder, term.getScale());
329                         appendBase(builder, term.getBase());
330                         appendExponent(builder, term.getExponent().negate());
331                     }
332                 }
333 
334             }
335 
336             // put the converted name in the map for reuse
337             siToCcsds.put(siName, builder.toString());
338 
339         }
340 
341         return siToCcsds.get(siName);
342 
343     }
344 
345     /** Append a scaling factor.
346      * @param builder builder to which term must be added
347      * @param scale scaling factor
348      */
349     private void appendScale(final StringBuilder builder, final double scale) {
350         final int factor = (int) FastMath.rint(scale);
351         if (FastMath.abs(scale - factor) > 1.0e-12) {
352             // this should never happen with CCSDS units
353             throw new OrekitInternalError(null);
354         }
355         if (factor != 1) {
356             builder.append(factor);
357         }
358     }
359 
360     /** Append a base term.
361      * @param builder builder to which term must be added
362      * @param base base term
363      */
364     private void appendBase(final StringBuilder builder, final CharSequence base) {
365         if ("°".equals(base) || "◦".equals(base)) {
366             builder.append("deg");
367         } else {
368             builder.append(base);
369         }
370     }
371 
372     /** Append an exponent.
373      * @param builder builder to which term must be added
374      * @param exponent exponent to add
375      */
376     private void appendExponent(final StringBuilder builder, final Fraction exponent) {
377         if (!exponent.equals(Fraction.ONE)) {
378             builder.append("**");
379             if (exponent.equals(Fraction.ONE_HALF)) {
380                 builder.append("0.5");
381             } else if (exponent.getNumerator() == 3 && exponent.getDenominator() == 2) {
382                 builder.append("1.5");
383             } else {
384                 builder.append(exponent);
385             }
386         }
387     }
388 
389 }
390