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