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.models.earth.ionosphere.nequick;
18  
19  import org.hipparchus.util.FastMath;
20  import org.orekit.data.DataSource;
21  import org.orekit.errors.OrekitException;
22  import org.orekit.errors.OrekitMessages;
23  import org.orekit.time.DateComponents;
24  
25  import java.io.BufferedReader;
26  import java.io.IOException;
27  import java.io.Reader;
28  import java.util.Locale;
29  import java.util.regex.Pattern;
30  
31  /**
32   * Parser for CCIR files.
33   * <p>
34   * Numerical grid maps which describe the regular variation of the ionosphere. They are used to derive other variables
35   * such as critical frequencies and transmission factors.
36   * </p> <p>
37   * The coefficients correspond to low and high solar activity conditions.
38   * </p> <p>
39   * The CCIR file naming convention is ccirXX.asc where each XX means month + 10.
40   * </p> <p>
41   * Coefficients are store into tow arrays, F2 and Fm3. F2 coefficients are used for the computation of the F2 layer
42   * critical frequency. Fm3 for the computation of the F2 layer maximum usable frequency factor. The size of these two
43   * arrays is fixed and discussed into the section 2.5.3.2 of the reference document.
44   * </p>
45   * @author Bryan Cazabonne
46   * @since 13.0
47   */
48  class CCIRLoader {
49  
50      /** Total number of F2 coefficients contained in the file. */
51      private static final int NUMBER_F2_COEFFICIENTS = 1976;
52  
53      /** Pattern for delimiting regular expressions. */
54      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
55  
56      /** Rows number for F2 and Fm3 arrays. */
57      private static final int ROWS = 2;
58  
59      /** Columns number for F2 array. */
60      private static final int TOTAL_COLUMNS_F2 = 76;
61  
62      /** Columns number for Fm3 array. */
63      private static final int TOTAL_COLUMNS_FM3 = 49;
64  
65      /** Depth of F2 array. */
66      private static final int DEPTH_F2 = 13;
67  
68      /** Depth of Fm3 array. */
69      private static final int DEPTH_FM3 = 9;
70  
71      /** F2 coefficients used for the computation of the F2 layer critical frequency. */
72      private double[][][] parsedF2;
73  
74      /** Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor. */
75      private double[][][] parsedFm3;
76  
77      /**
78       * Build a new instance.
79       */
80      CCIRLoader() {
81          this.parsedF2  = new double[ROWS][TOTAL_COLUMNS_F2][DEPTH_F2];
82          this.parsedFm3 = new double[ROWS][TOTAL_COLUMNS_FM3][DEPTH_FM3];
83      }
84  
85      /**
86       * Get the F2 coefficients used for the computation of the F2 layer critical frequency.
87       *
88       * @return the F2 coefficients
89       */
90      public double[][][] getF2() {
91          return parsedF2.clone();
92      }
93  
94      /**
95       * Get the Fm3 coefficients used for the computation of the F2 layer maximum usable frequency factor.
96       *
97       * @return the F2 coefficients
98       */
99      public double[][][] getFm3() {
100         return parsedFm3.clone();
101     }
102 
103     /**
104      * Load the data for a given month.
105      *
106      * @param dateComponents month given but its DateComponents
107      */
108     public void loadCCIRCoefficients(final DateComponents dateComponents) {
109 
110         // The files are named ccirXX.asc where XX substitute the month of the year + 10
111         final int currentMonth = dateComponents.getMonth();
112         final String fileName = String.format(Locale.US, "/assets/org/orekit/nequick/ccir%02d.asc",
113                                               currentMonth + 10);
114         loadData(new DataSource(fileName, () -> CCIRLoader.class.getResourceAsStream(fileName)));
115 
116     }
117 
118     /** Load data.
119      * @param dataSource data source
120      */
121     public void loadData(final DataSource dataSource) {
122 
123         // Placeholders for parsed data
124         int    lineNumber       = 0;
125         int    index            = 0;
126         int    currentRowF2     = 0;
127         int    currentColumnF2  = 0;
128         int    currentDepthF2   = 0;
129         int    currentRowFm3    = 0;
130         int    currentColumnFm3 = 0;
131         int    currentDepthFm3  = 0;
132         String line             = null;
133 
134         try (Reader         r  = dataSource.getOpener().openReaderOnce();
135              BufferedReader br = new BufferedReader(r)) {
136 
137             for (line = br.readLine(); line != null; line = br.readLine()) {
138                 ++lineNumber;
139                 line = line.trim();
140 
141                 // Read grid data
142                 if (!line.isEmpty()) {
143                     final String[] ccir_line = SEPARATOR.split(line);
144                     for (final String field : ccir_line) {
145 
146                         if (index < NUMBER_F2_COEFFICIENTS) {
147                             // Parse F2 coefficients
148                             if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 < (TOTAL_COLUMNS_F2 - 1)) {
149                                 currentDepthF2 = 0;
150                                 currentColumnF2++;
151                             } else if (currentDepthF2 >= DEPTH_F2 && currentColumnF2 >= (TOTAL_COLUMNS_F2 - 1)) {
152                                 currentDepthF2 = 0;
153                                 currentColumnF2 = 0;
154                                 currentRowF2++;
155                             }
156                             parsedF2[currentRowF2][currentColumnF2][currentDepthF2++] = Double.parseDouble(field);
157                             index++;
158                         } else {
159                             // Parse Fm3 coefficients
160                             if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 < (TOTAL_COLUMNS_FM3 - 1)) {
161                                 currentDepthFm3 = 0;
162                                 currentColumnFm3++;
163                             } else if (currentDepthFm3 >= DEPTH_FM3 && currentColumnFm3 >= (TOTAL_COLUMNS_FM3 - 1)) {
164                                 currentDepthFm3 = 0;
165                                 currentColumnFm3 = 0;
166                                 currentRowFm3++;
167                             }
168                             parsedFm3[currentRowFm3][currentColumnFm3][currentDepthFm3++] = Double.parseDouble(field);
169                             index++;
170                         }
171 
172                     }
173                 }
174 
175             }
176 
177         } catch (IOException ioe) {
178             throw new OrekitException(ioe, OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, dataSource.getName());
179         } catch (NumberFormatException nfe) {
180             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
181                                       lineNumber, dataSource.getName(), line);
182         }
183 
184         checkDimensions(currentRowF2,  currentColumnF2,  currentDepthF2,  parsedF2,  dataSource.getName());
185         checkDimensions(currentRowFm3, currentColumnFm3, currentDepthFm3, parsedFm3, dataSource.getName());
186 
187     }
188 
189     /** Check dimensions.
190      * @param currentRow current row index
191      * @param currentColumn current column index
192      * @param currentDepth current depth index
193      * @param array storage array
194      * @param name data source name
195      */
196     private void checkDimensions(final int currentRow, final int currentColumn, final int currentDepth,
197                                  final double[][][] array, final String name) {
198         // just three equality tests
199         // written in a way test coverage doesn't complain about missing cases…
200         if (FastMath.max(FastMath.max(FastMath.abs(currentRow - (array.length - 1)),
201                                       FastMath.abs(currentColumn - (array[0].length - 1))),
202                          FastMath.abs(currentDepth - array[0][0].length)) != 0) {
203             throw new OrekitException(OrekitMessages.NEQUICK_F2_FM3_NOT_LOADED, name);
204         }
205     }
206 
207 }