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