1   /* Copyright 2020-2025 Clément Jonglez
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    * Clément Jonglez 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  
18  package org.orekit.models.earth.atmosphere.data;
19  
20  import org.orekit.errors.OrekitException;
21  import org.orekit.errors.OrekitMessages;
22  import org.orekit.time.AbsoluteDate;
23  import org.orekit.time.ChronologicalComparator;
24  import org.orekit.time.TimeScale;
25  
26  import java.io.BufferedReader;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.nio.charset.StandardCharsets;
31  import java.text.ParseException;
32  import java.util.Arrays;
33  import java.util.HashSet;
34  import java.util.NoSuchElementException;
35  import java.util.Set;
36  import java.util.SortedSet;
37  import java.util.TreeSet;
38  
39  /**
40   * This class reads solar activity data from CSSI Space Weather files for the class {@link CssiSpaceWeatherData}.
41   * <p>
42   * The data are retrieved through space weather files offered by CSSI/AGI. The data can be retrieved on the AGI
43   * <a href="ftp://ftp.agi.com/pub/DynamicEarthData/SpaceWeather-All-v1.2.txt">
44   * FTP</a>. This file is updated several times a day by using several sources mentioned in the <a
45   * href="http://celestrak.com/SpaceData/SpaceWx-format.php"> Celestrak space weather data documentation</a>.
46   * </p>
47   *
48   * @author Clément Jonglez
49   * @since 10.2
50   */
51  public class CssiSpaceWeatherDataLoader extends AbstractSolarActivityDataLoader<CssiSpaceWeatherDataLoader.LineParameters> {
52  
53      /** Date of last data before the prediction starts. */
54      private AbsoluteDate lastObservedDate;
55  
56      /** Date of last daily prediction before the monthly prediction starts. */
57      private AbsoluteDate lastDailyPredictedDate;
58  
59      /** Data set. */
60      private final SortedSet<LineParameters> set;
61  
62      /**
63       * Constructor.
64       *
65       * @param utc UTC time scale
66       */
67      public CssiSpaceWeatherDataLoader(final TimeScale utc) {
68          super(utc);
69          this.lastDailyPredictedDate = null;
70          this.lastObservedDate       = null;
71          this.set                    = new TreeSet<>(new ChronologicalComparator());
72      }
73  
74      /**
75       * Checks if the string contains a floating point number.
76       *
77       * @param strNum string to check
78       *
79       * @return true if string contains a valid floating point number, else false
80       */
81      private static boolean isNumeric(final String strNum) {
82          if (strNum == null) {
83              return false;
84          }
85          try {
86              Double.parseDouble(strNum);
87          }
88          catch (NumberFormatException nfe) {
89              return false;
90          }
91          return true;
92      }
93  
94      /** {@inheritDoc} */
95      public void loadData(final InputStream input, final String name) throws IOException, ParseException, OrekitException {
96  
97          int                     lineNumber   = 0;
98          String                  line         = null;
99          final Set<AbsoluteDate> parsedEpochs = new HashSet<>();
100 
101         try (BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
102 
103             final CommonLineReader reader = new CommonLineReader(br);
104 
105             for (line = reader.readLine(); line != null; line = reader.readLine()) {
106                 lineNumber++;
107 
108                 line = line.trim();
109                 if (!line.isEmpty()) {
110 
111                     if (line.equals("BEGIN DAILY_PREDICTED")) {
112                         lastObservedDate = set.last().getDate();
113                     }
114 
115                     if (line.equals("BEGIN MONTHLY_FIT")) {
116                         lastDailyPredictedDate = set.last().getDate();
117                     }
118 
119                     if (line.length() == 130 && isNumeric(line.substring(0, 4))) {
120                         // extract the data from the line
121                         final int          year  = Integer.parseInt(line.substring(0, 4));
122                         final int          month = Integer.parseInt(line.substring(5, 7));
123                         final int          day   = Integer.parseInt(line.substring(8, 10));
124                         final AbsoluteDate date  = new AbsoluteDate(year, month, day, getUTC());
125 
126                         if (parsedEpochs.add(date)) { // Checking if entry doesn't exist yet
127                             final double[] threeHourlyKp = new double[8];
128                             /* Kp is written as an integer where a unit equals 0.1, the conversion is
129                              * Kp_double = 0.1 * double(Kp_integer) */
130                             for (int i = 0; i < 8; i++) {
131                                 threeHourlyKp[i] = 0.1 * Double.parseDouble(line.substring(19 + 3 * i, 21 + 3 * i));
132                             }
133                             final double kpSum = 0.1 * Double.parseDouble(line.substring(43, 46));
134 
135                             final double[] threeHourlyAp = new double[8];
136                             for (int i = 0; i < 8; i++) {
137                                 threeHourlyAp[i] = Double.parseDouble(line.substring(47 + 4 * i, 50 + 4 * i));
138                             }
139                             final double apAvg = Double.parseDouble(line.substring(79, 82));
140 
141                             final double f107Adj = Double.parseDouble(line.substring(93, 98));
142 
143                             final int fluxQualifier = Integer.parseInt(line.substring(99, 100));
144 
145                             final double ctr81Adj = Double.parseDouble(line.substring(101, 106));
146 
147                             final double lst81Adj = Double.parseDouble(line.substring(107, 112));
148 
149                             final double f107Obs = Double.parseDouble(line.substring(113, 118));
150 
151                             final double ctr81Obs = Double.parseDouble(line.substring(119, 124));
152 
153                             final double lst81Obs = Double.parseDouble(line.substring(125, 130));
154 
155                             set.add(new LineParameters(date, threeHourlyKp, kpSum, threeHourlyAp, apAvg, f107Adj,
156                                                        fluxQualifier, ctr81Adj, lst81Adj, f107Obs, ctr81Obs, lst81Obs));
157                         }
158                     }
159                 }
160             }
161         }
162         catch (NumberFormatException nfe) {
163             throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, name, line);
164         }
165 
166         try {
167             setMinDate(set.first().getDate());
168             setMaxDate(set.last().getDate());
169         }
170         catch (NoSuchElementException nse) {
171             throw new OrekitException(nse, OrekitMessages.NO_DATA_IN_FILE, name);
172         }
173 
174     }
175 
176     /**
177      * Getter for the data set.
178      *
179      * @return the data set
180      */
181     @Override
182     public SortedSet<LineParameters> getDataSet() {
183         return set;
184     }
185 
186     /**
187      * Gets the day (at data start) of the last daily data entry.
188      *
189      * @return the last daily predicted date
190      */
191     public AbsoluteDate getLastDailyPredictedDate() {
192         return lastDailyPredictedDate;
193     }
194 
195     /**
196      * Gets the day (at data start) of the last observed data entry.
197      *
198      * @return the last observed date
199      */
200     public AbsoluteDate getLastObservedDate() {
201         return lastObservedDate;
202     }
203 
204     /** Container class for Solar activity indexes. */
205     public static class LineParameters extends AbstractSolarActivityDataLoader.LineParameters {
206 
207         /** Serializable UID. */
208         private static final long serialVersionUID = 8151260459653484163L;
209 
210         /** Array of 8 three-hourly Kp indices for this entry. */
211         private final double[] threeHourlyKp;
212 
213         /**
214          * Sum of the 8 Kp indices for the day expressed to the nearest third of a unit.
215          */
216         private final double kpSum;
217 
218         /** Array of 8 three-hourly Ap indices for this entry. */
219         private final double[] threeHourlyAp;
220 
221         /** Arithmetic average of the 8 Ap indices for the day. */
222         private final double apAvg;
223 
224         /** 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU. */
225         private final double f107Adj;
226 
227         /** Flux Qualifier. */
228         private final int fluxQualifier;
229 
230         /** Centered 81-day arithmetic average of F10.7 (adjusted). */
231         private final double ctr81Adj;
232 
233         /** Last 81-day arithmetic average of F10.7 (adjusted). */
234         private final double lst81Adj;
235 
236         /** Observed (unadjusted) value of F10.7. */
237         private final double f107Obs;
238 
239         /** Centered 81-day arithmetic average of F10.7 (observed). */
240         private final double ctr81Obs;
241 
242         /** Last 81-day arithmetic average of F10.7 (observed). */
243         private final double lst81Obs;
244 
245         /**
246          * Constructor.
247          *
248          * @param date entry date
249          * @param threeHourlyKp array of 8 three-hourly Kp indices for this entry
250          * @param kpSum sum of the 8 Kp indices for the day expressed to the nearest third of a unit
251          * @param threeHourlyAp array of 8 three-hourly Ap indices for this entry
252          * @param apAvg arithmetic average of the 8 Ap indices for the day
253          * @param f107Adj 10.7-cm Solar Radio Flux (F10.7)
254          * @param fluxQualifier flux Qualifier
255          * @param ctr81Adj centered 81-day arithmetic average of F10.7
256          * @param lst81Adj last 81-day arithmetic average of F10.7
257          * @param f107Obs observed (unadjusted) value of F10.7
258          * @param ctr81Obs centered 81-day arithmetic average of F10.7 (observed)
259          * @param lst81Obs last 81-day arithmetic average of F10.7 (observed)
260          */
261         public LineParameters(final AbsoluteDate date, final double[] threeHourlyKp, final double kpSum,
262                               final double[] threeHourlyAp, final double apAvg, final double f107Adj,
263                               final int fluxQualifier, final double ctr81Adj, final double lst81Adj,
264                               final double f107Obs, final double ctr81Obs, final double lst81Obs) {
265             super(date);
266             this.threeHourlyKp = threeHourlyKp.clone();
267             this.kpSum         = kpSum;
268             this.threeHourlyAp = threeHourlyAp.clone();
269             this.apAvg         = apAvg;
270             this.f107Adj       = f107Adj;
271             this.fluxQualifier = fluxQualifier;
272             this.ctr81Adj      = ctr81Adj;
273             this.lst81Adj      = lst81Adj;
274             this.f107Obs       = f107Obs;
275             this.ctr81Obs      = ctr81Obs;
276             this.lst81Obs      = lst81Obs;
277         }
278 
279         /** {@inheritDoc} */
280         @Override
281         public int compareTo(final AbstractSolarActivityDataLoader.LineParameters lineParameters) {
282             return getDate().compareTo(lineParameters.getDate());
283         }
284 
285         /** {@inheritDoc} */
286         @Override
287         public boolean equals(final Object o) {
288             if (this == o) {
289                 return true;
290             }
291             if (o == null || getClass() != o.getClass()) {
292                 return false;
293             }
294 
295             final LineParameters that = (LineParameters) o;
296 
297             if (Double.compare(getKpSum(), that.getKpSum()) != 0) {
298                 return false;
299             }
300             if (Double.compare(getApAvg(), that.getApAvg()) != 0) {
301                 return false;
302             }
303             if (Double.compare(getF107Adj(), that.getF107Adj()) != 0) {
304                 return false;
305             }
306             if (getFluxQualifier() != that.getFluxQualifier()) {
307                 return false;
308             }
309             if (Double.compare(getCtr81Adj(), that.getCtr81Adj()) != 0) {
310                 return false;
311             }
312             if (Double.compare(getLst81Adj(), that.getLst81Adj()) != 0) {
313                 return false;
314             }
315             if (Double.compare(getF107Obs(), that.getF107Obs()) != 0) {
316                 return false;
317             }
318             if (Double.compare(getCtr81Obs(), that.getCtr81Obs()) != 0) {
319                 return false;
320             }
321             if (Double.compare(getLst81Obs(), that.getLst81Obs()) != 0) {
322                 return false;
323             }
324             if (!Arrays.equals(getThreeHourlyKp(), that.getThreeHourlyKp())) {
325                 return false;
326             }
327             return Arrays.equals(getThreeHourlyAp(), that.getThreeHourlyAp());
328         }
329 
330         /** {@inheritDoc} */
331         @Override
332         public int hashCode() {
333             int  result;
334             result = Arrays.hashCode(getThreeHourlyKp());
335             result = 31 * result + Double.hashCode(getKpSum());
336             result = 31 * result + Arrays.hashCode(getThreeHourlyAp());
337             result = 31 * result + Double.hashCode(getApAvg());
338             result = 31 * result + Double.hashCode(getF107Adj());
339             result = 31 * result + getFluxQualifier();
340             result = 31 * result + Double.hashCode(getCtr81Adj());
341             result = 31 * result + Double.hashCode(getLst81Adj());
342             result = 31 * result + Double.hashCode(getF107Obs());
343             result = 31 * result + Double.hashCode(getCtr81Obs());
344             result = 31 * result + Double.hashCode(getLst81Obs());
345             return result;
346         }
347 
348         /**
349          * Gets the three-hourly Kp index at index i from the threeHourlyKp array.
350          *
351          * @param i index of the Kp index to retrieve [0-7]
352          *
353          * @return the three hourly Kp index at index i
354          */
355         public double getThreeHourlyKp(final int i) {
356             return threeHourlyKp[i];
357         }
358 
359         /**
360          * Gets the three-hourly Ap index at index i from the threeHourlyAp array.
361          *
362          * @param i index of the Ap to retrieve [0-7]
363          *
364          * @return the three hourly Ap index at index i
365          */
366         public double getThreeHourlyAp(final int i) {
367             return threeHourlyAp[i];
368         }
369 
370         /**
371          * Gets the array of the eight three-hourly Kp indices for the current entry.
372          *
373          * @return the array of eight three-hourly Kp indices
374          */
375         public double[] getThreeHourlyKp() {
376             return threeHourlyKp.clone();
377         }
378 
379         /**
380          * Gets the sum of all eight Kp indices for the current entry.
381          *
382          * @return the sum of all eight Kp indices
383          */
384         public double getKpSum() {
385             return kpSum;
386         }
387 
388         /**
389          * Gets the array of the eight three-hourly Ap indices for the current entry.
390          *
391          * @return the array of eight three-hourly Ap indices
392          */
393         public double[] getThreeHourlyAp() {
394             return threeHourlyAp.clone();
395         }
396 
397         /**
398          * Gets the arithmetic average of all eight Ap indices for the current entry.
399          *
400          * @return the average of all eight Ap indices
401          */
402         public double getApAvg() {
403             return apAvg;
404         }
405 
406         /**
407          * Gets the last 81-day arithmetic average of F10.7 (observed).
408          *
409          * @return the last 81-day arithmetic average of F10.7 (observed)
410          */
411         public double getLst81Obs() {
412             return lst81Obs;
413         }
414 
415         /**
416          * Gets the centered 81-day arithmetic average of F10.7 (observed).
417          *
418          * @return the centered 81-day arithmetic average of F10.7 (observed)
419          */
420         public double getCtr81Obs() {
421             return ctr81Obs;
422         }
423 
424         /**
425          * Gets the observed (unadjusted) value of F10.7.
426          *
427          * @return the observed (unadjusted) value of F10.7
428          */
429         public double getF107Obs() {
430             return f107Obs;
431         }
432 
433         /**
434          * Gets the last 81-day arithmetic average of F10.7 (adjusted).
435          *
436          * @return the last 81-day arithmetic average of F10.7 (adjusted)
437          */
438         public double getLst81Adj() {
439             return lst81Adj;
440         }
441 
442         /**
443          * Gets the centered 81-day arithmetic average of F10.7 (adjusted).
444          *
445          * @return the centered 81-day arithmetic average of F10.7 (adjusted)
446          */
447         public double getCtr81Adj() {
448             return ctr81Adj;
449         }
450 
451         /**
452          * Gets the Flux Qualifier.
453          *
454          * @return the Flux Qualifier
455          */
456         public int getFluxQualifier() {
457             return fluxQualifier;
458         }
459 
460         /**
461          * Gets the 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU.
462          *
463          * @return the 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU
464          */
465         public double getF107Adj() {
466             return f107Adj;
467         }
468     }
469 
470 }