CCIRLoader.java

/* Copyright 2002-2025 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.models.earth.ionosphere.nequick;

import org.hipparchus.util.FastMath;
import org.orekit.data.DataSource;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.DateComponents;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Locale;
import java.util.regex.Pattern;

/**
 * Parser for CCIR files.
 * <p>
 * Numerical grid maps which describe the regular variation of the ionosphere. They are used to derive other variables
 * such as critical frequencies and transmission factors.
 * </p> <p>
 * The coefficients correspond to low and high solar activity conditions.
 * </p> <p>
 * The CCIR file naming convention is ccirXX.asc where each XX means month + 10.
 * </p> <p>
 * Coefficients are store into tow arrays, F2 and Fm3. F2 coefficients are used for the computation of the F2 layer
 * critical frequency. Fm3 for the computation of the F2 layer maximum usable frequency factor. The size of these two
 * arrays is fixed and discussed into the section 2.5.3.2 of the reference document.
 * </p>
 * @author Bryan Cazabonne
 * @since 13.0
 */
class CCIRLoader {

    /** Total number of F2 coefficients contained in the file. */
    private static final int NUMBER_F2_COEFFICIENTS = 1976;

    /** Pattern for delimiting regular expressions. */
    private static final Pattern SEPARATOR = Pattern.compile("\\s+");

    /** Rows number for F2 and Fm3 arrays. */
    private static final int ROWS = 2;

    /** Columns number for F2 array. */
    private static final int TOTAL_COLUMNS_F2 = 76;

    /** Columns number for Fm3 array. */
    private static final int TOTAL_COLUMNS_FM3 = 49;

    /** Depth of F2 array. */
    private static final int DEPTH_F2 = 13;

    /** Depth of Fm3 array. */
    private static final int DEPTH_FM3 = 9;

    /** F2 coefficients used for the computation of the F2 layer critical frequency. */
    private double[][][] parsedF2;

    /** Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor. */
    private double[][][] parsedFm3;

    /**
     * Build a new instance.
     */
    CCIRLoader() {
        this.parsedF2  = new double[ROWS][TOTAL_COLUMNS_F2][DEPTH_F2];
        this.parsedFm3 = new double[ROWS][TOTAL_COLUMNS_FM3][DEPTH_FM3];
    }

    /**
     * Get the F2 coefficients used for the computation of the F2 layer critical frequency.
     *
     * @return the F2 coefficients
     */
    public double[][][] getF2() {
        return parsedF2.clone();
    }

    /**
     * Get the Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor.
     *
     * @return the F2 coefficients
     */
    public double[][][] getFm3() {
        return parsedFm3.clone();
    }

    /**
     * Load the data for a given month.
     *
     * @param dateComponents month given but its DateComponents
     */
    public void loadCCIRCoefficients(final DateComponents dateComponents) {

        // The files are named ccirXX.asc where XX substitute the month of the year + 10
        final int currentMonth = dateComponents.getMonth();
        final String fileName = String.format(Locale.US, "/assets/org/orekit/nequick/ccir%02d.asc",
                                              currentMonth + 10);
        loadData(new DataSource(fileName, () -> CCIRLoader.class.getResourceAsStream(fileName)));

    }

    /** Load data.
     * @param dataSource data source
     */
    public void loadData(final DataSource dataSource) {

        // Placeholders for parsed data
        int    lineNumber       = 0;
        int    index            = 0;
        int    currentRowF2     = 0;
        int    currentColumnF2  = 0;
        int    currentDepthF2   = 0;
        int    currentRowFm3    = 0;
        int    currentColumnFm3 = 0;
        int    currentDepthFm3  = 0;
        String line             = null;

        try (Reader         r  = dataSource.getOpener().openReaderOnce();
             BufferedReader br = new BufferedReader(r)) {

            for (line = br.readLine(); line != null; line = br.readLine()) {
                ++lineNumber;
                line = line.trim();

                // Read grid data
                if (!line.isEmpty()) {
                    final String[] ccir_line = SEPARATOR.split(line);
                    for (final String field : ccir_line) {

                        if (index < NUMBER_F2_COEFFICIENTS) {
                            // Parse F2 coefficients
                            if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 < (TOTAL_COLUMNS_F2 - 1)) {
                                currentDepthF2 = 0;
                                currentColumnF2++;
                            } else if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 >= (TOTAL_COLUMNS_F2 - 1)) {
                                currentDepthF2 = 0;
                                currentColumnF2 = 0;
                                currentRowF2++;
                            }
                            parsedF2[currentRowF2][currentColumnF2][currentDepthF2++] = Double.parseDouble(field);
                            index++;
                        } else {
                            // Parse Fm3 coefficients
                            if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 < (TOTAL_COLUMNS_FM3 - 1)) {
                                currentDepthFm3 = 0;
                                currentColumnFm3++;
                            } else if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 >= (TOTAL_COLUMNS_FM3 - 1)) {
                                currentDepthFm3 = 0;
                                currentColumnFm3 = 0;
                                currentRowFm3++;
                            }
                            parsedFm3[currentRowFm3][currentColumnFm3][currentDepthFm3++] = Double.parseDouble(field);
                            index++;
                        }

                    }
                }

            }

        } catch (IOException ioe) {
            throw new OrekitException(ioe, OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, dataSource.getName());
        } catch (NumberFormatException nfe) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                      lineNumber, dataSource.getName(), line);
        }

        checkDimensions(currentRowF2,  currentColumnF2,  currentDepthF2,  parsedF2,  dataSource.getName());
        checkDimensions(currentRowFm3, currentColumnFm3, currentDepthFm3, parsedFm3, dataSource.getName());

    }

    /** Check dimensions.
     * @param currentRow current row index
     * @param currentColumn current column index
     * @param currentDepth current depth index
     * @param array storage array
     * @param name data source name
     */
    private void checkDimensions(final int currentRow, final int currentColumn, final int currentDepth,
                                 final double[][][] array, final String name) {
        // just three equality tests
        // written in a way test coverage doesn't complain about missing cases…
        if (FastMath.max(FastMath.max(FastMath.abs(currentRow - (array.length - 1)),
                                      FastMath.abs(currentColumn - (array[0].length - 1))),
                         FastMath.abs(currentDepth - array[0][0].length)) != 0) {
            throw new OrekitException(OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, name);
        }
    }

}