ParseUtils.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.propagation.analytical.tle;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;

/** Utility class for TLE parsing, including alpha-5 TLE satellites IDs handling.
 * <p>
 * Alpha-5 extends the range of existing 5 digits TLE satellite numbers
 * by allowing the first digit to be an upper case letter, ignoring 'I'
 * and 'O' to avoid confusion with numbers '1' and '0'.
 * </p>
 * @see <a href="https://www.space-track.org/documentation#tle-alpha5>TLE-alpha5</a>
 * @author Mark rutten
 */
class ParseUtils  {

    /** Letter-number map for satellite number. */
    private static final int MAX_NUMERIC_SATNUM = 99999;

    /** Letter-number map for satellite number. */
    private static final Map<Character, Integer> ALPHA5_NUMBERS;

    /** Number-letter map for satellite number. */
    private static final Map<Integer, Character> ALPHA5_LETTERS;

    /** Scaling factor for alpha5 numbers. */
    private static final int ALPHA5_SCALING = 10000;

    static {
        // Generate maps between TLE satellite alphabetic characters and integers.
        final List<Character> alpha5Letters =
                        Arrays.asList('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
                                      'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T',
                                      'U', 'V', 'W', 'X', 'Y', 'Z');
        ALPHA5_NUMBERS = new HashMap<>(alpha5Letters.size());
        ALPHA5_LETTERS = new HashMap<>(alpha5Letters.size());
        for (int i = 0; i < alpha5Letters.size(); ++i) {
            ALPHA5_NUMBERS.put(alpha5Letters.get(i), i + 10);
            ALPHA5_LETTERS.put(i + 10, alpha5Letters.get(i));
        }
    }

    /** Private constructor for a utility class. */
    private ParseUtils() {
        // nothing to do
    }

    /** Build an alpha5 satellite number.
     * @param satelliteNumber satellite number, that may exceed the 99999 limit
     * @param name parameter name
     * @return satellite number in alpha5 representation
     */
    public static String buildSatelliteNumber(final int satelliteNumber, final String name) {
        if (satelliteNumber > MAX_NUMERIC_SATNUM) {
            final int highDigits = satelliteNumber / ALPHA5_SCALING;
            final int lowDigits = satelliteNumber - highDigits * ALPHA5_SCALING;

            final Character alpha = ALPHA5_LETTERS.get(highDigits);
            if (alpha == null) {
                throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
                                          satelliteNumber, name, "null");
            }
            return alpha + addPadding(name, lowDigits, '0', 4, true, satelliteNumber);
        } else {
            return addPadding(name, satelliteNumber, '0', 5, true, satelliteNumber);
        }
    }

    /** Add padding characters before an integer.
     * @param name parameter name
     * @param k integer to pad
     * @param c padding character
     * @param size desired size
     * @param rightJustified if true, the resulting string is
     * right justified (i.e. space are added to the left)
     * @param satelliteNumber satellite number
     * @return padded string
     */
    public static String addPadding(final String name, final int k, final char c,
                                    final int size, final boolean rightJustified,
                                    final int satelliteNumber) {
        return addPadding(name, Integer.toString(k), c, size, rightJustified, satelliteNumber);
    }

    /** Add padding characters to a string.
     * @param name parameter name
     * @param string string to pad
     * @param c padding character
     * @param size desired size
     * @param rightJustified if true, the resulting string is
     * right justified (i.e. space are added to the left)
     * @param satelliteNumber satellite number
     * @return padded string
     */
    public static String addPadding(final String name, final String string, final char c,
                                    final int size, final boolean rightJustified,
                                    final int satelliteNumber) {

        if (string.length() > size) {
            throw new OrekitException(OrekitMessages.TLE_INVALID_PARAMETER,
                                      satelliteNumber, name, string);
        }

        final StringBuilder padding = new StringBuilder();
        for (int i = 0; i < size; ++i) {
            padding.append(c);
        }

        if (rightJustified) {
            final String concatenated = padding + string;
            final int l = concatenated.length();
            return concatenated.substring(l - size, l);
        }

        return (string + padding).substring(0, size);

    }

    /** Parse a double.
     * @param line line to parse
     * @param start start index of the first character
     * @param length length of the string
     * @return value of the double
     */
    public static double parseDouble(final String line, final int start, final int length) {
        final String string = line.substring(start, start + length).trim();
        return string.length() > 0 ? Double.parseDouble(string.replace(' ', '0')) : 0;
    }

    /** Parse an integer.
     * @param line line to parse
     * @param start start index of the first character
     * @param length length of the string
     * @return value of the integer
     */
    public static int parseInteger(final String line, final int start, final int length) {
        final String field = line.substring(start, start + length).trim();
        return field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
    }

    /** Parse a satellite number.
     * @param line line to parse
     * @param start start index of the first character
     * @param length length of the string
     * @return value of the integer
     */
    public static int parseSatelliteNumber(final String line, final int start, final int length) {
        String field = line.substring(start, start + length);
        int satelliteNumber;

        final Integer alpha = ALPHA5_NUMBERS.get(field.charAt(0));
        if (alpha != null) {
            satelliteNumber = Integer.parseInt(field.substring(1));
            satelliteNumber += alpha * ALPHA5_SCALING;
        } else {
            field = field.trim();
            satelliteNumber = field.length() > 0 ? Integer.parseInt(field.replace(' ', '0')) : 0;
        }
        return satelliteNumber;
    }

    /** Parse a year written on 2 digits.
     * @param line line to parse
     * @param start start index of the first character
     * @return value of the year
     */
    public static int parseYear(final String line, final int start) {
        final int year = 2000 + parseInteger(line, start, 2);
        return (year > 2056) ? (year - 100) : year;
    }

}