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