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 }