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.propagation.analytical.tle;
18  
19  import java.util.Arrays;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.orekit.errors.OrekitException;
25  import org.orekit.errors.OrekitMessages;
26  
27  /** Utility class for TLE parsing, including alpha-5 TLE satellites IDs handling.
28   * <p>
29   * Alpha-5 extends the range of existing 5 digits TLE satellite numbers
30   * by allowing the first digit to be an upper case letter, ignoring 'I'
31   * and 'O' to avoid confusion with numbers '1' and '0'.
32   * </p>
33   * @see <a href="https://www.space-track.org/documentation#tle-alpha5>TLE-alpha5</a>
34   * @author Mark rutten
35   */
36  class ParseUtils  {
37  
38      /** Letter-number map for satellite number. */
39      private static final int MAX_NUMERIC_SATNUM = 99999;
40  
41      /** Letter-number map for satellite number. */
42      private static final Map<Character, Integer> ALPHA5_NUMBERS;
43  
44      /** Number-letter map for satellite number. */
45      private static final Map<Integer, Character> ALPHA5_LETTERS;
46  
47      /** Scaling factor for alpha5 numbers. */
48      private static final int ALPHA5_SCALING = 10000;
49  
50      static {
51          // Generate maps between TLE satellite alphabetic characters and integers.
52          final List<Character> alpha5Letters =
53                          Arrays.asList('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
54                                        'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
55                                        'U', 'V', 'W', 'X', 'Y', 'Z');
56          ALPHA5_NUMBERS = new HashMap<>(alpha5Letters.size());
57          ALPHA5_LETTERS = new HashMap<>(alpha5Letters.size());
58          for (int i = 0; i < alpha5Letters.size(); ++i) {
59              ALPHA5_NUMBERS.put(alpha5Letters.get(i), i + 10);
60              ALPHA5_LETTERS.put(i + 10, alpha5Letters.get(i));
61          }
62      }
63  
64      /** Private constructor for a utility class. */
65      private ParseUtils() {
66          // nothing to do
67      }
68  
69      /** Build an alpha5 satellite number.
70       * @param satelliteNumber satellite number, that may exceed the 99999 limit
71       * @param name parameter name
72       * @return satellite number in alpha5 representation
73       */
74      public static String buildSatelliteNumber(final int satelliteNumber, final String name) {
75          if (satelliteNumber > MAX_NUMERIC_SATNUM) {
76              final int highDigits = satelliteNumber / ALPHA5_SCALING;
77              final int lowDigits = satelliteNumber - highDigits * ALPHA5_SCALING;
78  
79              final Character alpha = ALPHA5_LETTERS.get(highDigits);
80              if (alpha == null) {
81                  throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
82                                            satelliteNumber, name, "null");
83              }
84              return alpha + addPadding(name, lowDigits, '0', 4, true, satelliteNumber);
85          } else {
86              return addPadding(name, satelliteNumber, '0', 5, true, satelliteNumber);
87          }
88      }
89  
90      /** Add padding characters before an integer.
91       * @param name parameter name
92       * @param k integer to pad
93       * @param c padding character
94       * @param size desired size
95       * @param rightJustified if true, the resulting string is
96       * right justified (i.e. space are added to the left)
97       * @param satelliteNumber satellite number
98       * @return padded string
99       */
100     public static String addPadding(final String name, final int k, final char c,
101                                     final int size, final boolean rightJustified,
102                                     final int satelliteNumber) {
103         return addPadding(name, Integer.toString(k), c, size, rightJustified, satelliteNumber);
104     }
105 
106     /** Add padding characters to a string.
107      * @param name parameter name
108      * @param string string to pad
109      * @param c padding character
110      * @param size desired size
111      * @param rightJustified if true, the resulting string is
112      * right justified (i.e. space are added to the left)
113      * @param satelliteNumber satellite number
114      * @return padded string
115      */
116     public static String addPadding(final String name, final String string, final char c,
117                                     final int size, final boolean rightJustified,
118                                     final int satelliteNumber) {
119 
120         if (string.length() > size) {
121             throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
122                                       satelliteNumber, name, string);
123         }
124 
125         final StringBuilder padding = new StringBuilder();
126         for (int i = 0; i < size; ++i) {
127             padding.append(c);
128         }
129 
130         if (rightJustified) {
131             final String concatenated = padding + string;
132             final int l = concatenated.length();
133             return concatenated.substring(l - size, l);
134         }
135 
136         return (string + padding).substring(0, size);
137 
138     }
139 
140     /** Parse a double.
141      * @param line line to parse
142      * @param start start index of the first character
143      * @param length length of the string
144      * @return value of the double
145      */
146     public static double parseDouble(final String line, final int start, final int length) {
147         final String string = line.substring(start, start + length).trim();
148         return string.length() > 0 ? Double.parseDouble(string.replace(' ', '0')) : 0;
149     }
150 
151     /** Parse an integer.
152      * @param line line to parse
153      * @param start start index of the first character
154      * @param length length of the string
155      * @return value of the integer
156      */
157     public static int parseInteger(final String line, final int start, final int length) {
158         final String field = line.substring(start, start + length).trim();
159         return field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
160     }
161 
162     /** Parse a satellite number.
163      * @param line line to parse
164      * @param start start index of the first character
165      * @param length length of the string
166      * @return value of the integer
167      */
168     public static int parseSatelliteNumber(final String line, final int start, final int length) {
169         String field = line.substring(start, start + length);
170         int satelliteNumber;
171 
172         final Integer alpha = ALPHA5_NUMBERS.get(field.charAt(0));
173         if (alpha != null) {
174             satelliteNumber = Integer.parseInt(field.substring(1));
175             satelliteNumber += alpha * ALPHA5_SCALING;
176         } else {
177             field = field.trim();
178             satelliteNumber = field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
179         }
180         return satelliteNumber;
181     }
182 
183     /** Parse a year written on 2 digits.
184      * @param line line to parse
185      * @param start start index of the first character
186      * @return value of the year
187      */
188     public static int parseYear(final String line, final int start) {
189         final int year = 2000 + parseInteger(line, start, 2);
190         return (year > 2056) ? (year - 100) : year;
191     }
192 
193 }