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