CssiSpaceWeatherDataLoader.java
/* Copyright 2020 Clément Jonglez
* 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.
* Clément Jonglez 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.atmosphere.data;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.NoSuchElementException;
import java.util.SortedSet;
import java.util.TreeSet;
import org.hipparchus.exception.Localizable;
import org.orekit.data.DataLoader;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.ChronologicalComparator;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeStamped;
/**
* This class reads solar activity data from CSSI Space Weather files for the
* class {@link CssiSpaceWeatherData}.
* <p>
* The data are retrieved through space weather files offered by CSSI/AGI. The
* data can be retrieved on the AGI
* <a href="ftp://ftp.agi.com/pub/DynamicEarthData/SpaceWeather-All-v1.2.txt">
* FTP</a>. This file is updated several times a day by using several sources
* mentioned in the <a href="http://celestrak.com/SpaceData/SpaceWx-format.php">
* Celestrak space weather data documentation</a>.
* </p>
*
* @author Clément Jonglez
* @since 10.2
*/
public class CssiSpaceWeatherDataLoader implements DataLoader {
/** Helper class to parse line data and to raise exceptions if needed. */
public static class LineReader {
/** Name of the file. Used in error messages. */
private final String name;
/** The input stream. */
private final BufferedReader in;
/** The last line read from the file. */
private String line;
/** The number of the last line read from the file. */
private long lineNo;
/**
* Create a line reader.
*
* @param name of the data source for error messages.
* @param in the input data stream.
*/
public LineReader(final String name, final BufferedReader in) {
this.name = name;
this.in = in;
this.line = null;
this.lineNo = 0;
}
/**
* Read a line from the input data stream.
*
* @return the next line without the line termination character, or {@code null}
* if the end of the stream has been reached.
* @throws IOException if an I/O error occurs.
* @see BufferedReader#readLine()
*/
public String readLine() throws IOException {
line = in.readLine();
lineNo++;
return line;
}
/**
* Read a line from the input data stream, or if the end of the stream has been
* reached throw an exception.
*
* @param message for the exception if the end of the stream is reached.
* @param args for the exception if the end of stream is reached.
* @return the next line without the line termination character, or {@code null}
* if the end of the stream has been reached.
* @throws IOException if an I/O error occurs.
* @throws OrekitException if a line could not be read because the end of the
* stream has been reached.
* @see #readLine()
*/
public String readLineOrThrow(final Localizable message, final Object... args)
throws IOException, OrekitException {
final String text = readLine();
if (text == null) {
throw new OrekitException(message, args);
}
return text;
}
/**
* Annotate an exception with the file context.
*
* @param cause the reason why the line could not be parsed.
* @return an exception with the cause, file name, line number, and line text.
*/
public OrekitException unableToParseLine(final Throwable cause) {
return new OrekitException(cause, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNo, name, line);
}
/**
* Get the last line read from the stream.
*
* @return May be {@code null} if no lines have been read or the end of stream
* has been reached.
*/
public String getLine() {
return line;
}
/**
* Get the line number of the last line read from the file.
*
* @return the line number.
*/
public long getLineNumber() {
return lineNo;
}
}
/** Container class for Solar activity indexes. */
public static class LineParameters implements TimeStamped, Serializable {
/** Serializable UID. */
private static final long serialVersionUID = 8151260459653484163L;
/** Entry date. */
private final AbsoluteDate date;
/** Array of 8 three-hourly Kp indices for this entry. */
private final double[] threeHourlyKp;
/**
* Sum of the 8 Kp indices for the day expressed to the nearest third of a unit.
*/
private final double kpSum;
/** Array of 8 three-hourly Ap indices for this entry. */
private final double[] threeHourlyAp;
/** Arithmetic average of the 8 Ap indices for the day. */
private final double apAvg;
/** 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU. */
private final double f107Adj;
/** Flux Qualifier. */
private final int fluxQualifier;
/** Centered 81-day arithmetic average of F10.7 (adjusted). */
private final double ctr81Adj;
/** Last 81-day arithmetic average of F10.7 (adjusted). */
private final double lst81Adj;
/** Observed (unadjusted) value of F10.7. */
private final double f107Obs;
/** Centered 81-day arithmetic average of F10.7 (observed). */
private final double ctr81Obs;
/** Last 81-day arithmetic average of F10.7 (observed). */
private final double lst81Obs;
/**
* Constructor.
* @param date entry date
* @param threeHourlyKp array of 8 three-hourly Kp indices for this entry
* @param kpSum sum of the 8 Kp indices for the day expressed to the nearest third of a unit
* @param threeHourlyAp array of 8 three-hourly Ap indices for this entry
* @param apAvg arithmetic average of the 8 Ap indices for the day
* @param f107Adj 10.7-cm Solar Radio Flux (F10.7)
* @param fluxQualifier flux Qualifier
* @param ctr81Adj centered 81-day arithmetic average of F10.7
* @param lst81Adj last 81-day arithmetic average of F10.7
* @param f107Obs observed (unadjusted) value of F10.7
* @param ctr81Obs centered 81-day arithmetic average of F10.7 (observed)
* @param lst81Obs last 81-day arithmetic average of F10.7 (observed)
*/
public LineParameters(final AbsoluteDate date, final double[] threeHourlyKp, final double kpSum,
final double[] threeHourlyAp, final double apAvg, final double f107Adj, final int fluxQualifier,
final double ctr81Adj, final double lst81Adj, final double f107Obs, final double ctr81Obs,
final double lst81Obs) {
this.date = date;
this.threeHourlyKp = threeHourlyKp.clone();
this.kpSum = kpSum;
this.threeHourlyAp = threeHourlyAp.clone();
this.apAvg = apAvg;
this.f107Adj = f107Adj;
this.fluxQualifier = fluxQualifier;
this.ctr81Adj = ctr81Adj;
this.lst81Adj = lst81Adj;
this.f107Obs = f107Obs;
this.ctr81Obs = ctr81Obs;
this.lst81Obs = lst81Obs;
}
@Override
public AbsoluteDate getDate() {
return date;
}
/**
* Gets the array of the eight three-hourly Kp indices for the current entry.
* @return the array of eight three-hourly Kp indices
*/
public double[] getThreeHourlyKp() {
return threeHourlyKp.clone();
}
/**
* Gets the three-hourly Kp index at index i from the threeHourlyKp array.
* @param i index of the Kp index to retrieve [0-7]
* @return the three hourly Kp index at index i
*/
public double getThreeHourlyKp(final int i) {
return threeHourlyKp[i];
}
/**
* Gets the sum of all eight Kp indices for the current entry.
* @return the sum of all eight Kp indices
*/
public double getKpSum() {
return kpSum;
}
/**
* Gets the array of the eight three-hourly Ap indices for the current entry.
* @return the array of eight three-hourly Ap indices
*/
public double[] getThreeHourlyAp() {
return threeHourlyAp.clone();
}
/**
* Gets the three-hourly Ap index at index i from the threeHourlyAp array.
* @param i index of the Ap to retrieve [0-7]
* @return the three hourly Ap index at index i
*/
public double getThreeHourlyAp(final int i) {
return threeHourlyAp[i];
}
/**
* Gets the arithmetic average of all eight Ap indices for the current entry.
* @return the average of all eight Ap indices
*/
public double getApAvg() {
return apAvg;
}
/**
* Gets the last 81-day arithmetic average of F10.7 (observed).
* @return the last 81-day arithmetic average of F10.7 (observed)
*/
public double getLst81Obs() {
return lst81Obs;
}
/**
* Gets the centered 81-day arithmetic average of F10.7 (observed).
* @return the centered 81-day arithmetic average of F10.7 (observed)
*/
public double getCtr81Obs() {
return ctr81Obs;
}
/**
* Gets the observed (unadjusted) value of F10.7.
* @return the observed (unadjusted) value of F10.7
*/
public double getF107Obs() {
return f107Obs;
}
/**
* Gets the last 81-day arithmetic average of F10.7 (adjusted).
* @return the last 81-day arithmetic average of F10.7 (adjusted)
*/
public double getLst81Adj() {
return lst81Adj;
}
/**
* Gets the centered 81-day arithmetic average of F10.7 (adjusted).
* @return the centered 81-day arithmetic average of F10.7 (adjusted)
*/
public double getCtr81Adj() {
return ctr81Adj;
}
/**
* Gets the Flux Qualifier.
* @return the Flux Qualifier
*/
public int getFluxQualifier() {
return fluxQualifier;
}
/**
* Gets the 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU.
* @return the 10.7-cm Solar Radio Flux (F10.7) Adjusted to 1 AU
*/
public double getF107Adj() {
return f107Adj;
}
}
/** UTC time scale. */
private final TimeScale utc;
/** First available date. */
private AbsoluteDate firstDate;
/** Date of last data before the prediction starts. */
private AbsoluteDate lastObservedDate;
/** Date of last daily prediction before the monthly prediction starts. */
private AbsoluteDate lastDailyPredictedDate;
/** Last available date. */
private AbsoluteDate lastDate;
/** Data set. */
private SortedSet<TimeStamped> set;
/**
* Constructor.
* @param utc UTC time scale
*/
public CssiSpaceWeatherDataLoader(final TimeScale utc) {
this.utc = utc;
firstDate = null;
lastDailyPredictedDate = null;
lastDate = null;
lastObservedDate = null;
set = new TreeSet<>(new ChronologicalComparator());
}
/**
* Getter for the data set.
* @return the data set
*/
public SortedSet<TimeStamped> getDataSet() {
return set;
}
/**
* Gets the available data range minimum date.
* @return the minimum date.
*/
public AbsoluteDate getMinDate() {
return firstDate;
}
/**
* Gets the available data range maximum date.
* @return the maximum date.
*/
public AbsoluteDate getMaxDate() {
return lastDate;
}
/**
* Gets the day (at data start) of the last daily data entry.
* @return the last daily predicted date
*/
public AbsoluteDate getLastDailyPredictedDate() {
return lastDailyPredictedDate;
}
/**
* Gets the day (at data start) of the last observed data entry.
* @return the last observed date
*/
public AbsoluteDate getLastObservedDate() {
return lastObservedDate;
}
/**
* Checks if the string contains a floating point number.
*
* @param strNum string to check
* @return true if string contains a valid floating point number, else false
*/
private static boolean isNumeric(final String strNum) {
if (strNum == null) {
return false;
}
try {
Double.parseDouble(strNum);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
/** {@inheritDoc} */
public void loadData(final InputStream input, final String name)
throws IOException, ParseException, OrekitException {
// read the data
int lineNumber = 0;
String line = null;
try (BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
final LineReader reader = new LineReader(name, br);
for (line = reader.readLine(); line != null; line = reader.readLine()) {
lineNumber++;
line = line.trim();
if (line.length() > 0) {
if (line.equals("BEGIN DAILY_PREDICTED")) {
lastObservedDate = set.last().getDate();
}
if (line.equals("BEGIN MONTHLY_FIT")) {
lastDailyPredictedDate = set.last().getDate();
}
if (line.length() == 130 && isNumeric(line.substring(0, 4))) {
// extract the data from the line
final int year = Integer.parseInt(line.substring(0, 4));
final int month = Integer.parseInt(line.substring(5, 7));
final int day = Integer.parseInt(line.substring(8, 10));
final AbsoluteDate date = new AbsoluteDate(year, month, day, this.utc);
if (!set.contains(date)) { // Checking if entry doesn't exist yet
final double[] threeHourlyKp = new double[8];
/**
* Kp is written as an integer where a unit equals 0.1, the conversion is
* Kp_double = 0.1 * double(Kp_integer)
*/
for (int i = 0; i < 8; i++) {
threeHourlyKp[i] = 0.1 * Double.parseDouble(line.substring(19 + 3 * i, 21 + 3 * i));
}
final double kpSum = 0.1 * Double.parseDouble(line.substring(43, 46));
final double[] threeHourlyAp = new double[8];
for (int i = 0; i < 8; i++) {
threeHourlyAp[i] = Double.parseDouble(line.substring(47 + 4 * i, 50 + 4 * i));
}
final double apAvg = Double.parseDouble(line.substring(79, 82));
final double f107Adj = Double.parseDouble(line.substring(93, 98));
final int fluxQualifier = Integer.parseInt(line.substring(99, 100));
final double ctr81Adj = Double.parseDouble(line.substring(101, 106));
final double lst81Adj = Double.parseDouble(line.substring(107, 112));
final double f107Obs = Double.parseDouble(line.substring(113, 118));
final double ctr81Obs = Double.parseDouble(line.substring(119, 124));
final double lst81Obs = Double.parseDouble(line.substring(125, 130));
set.add(new LineParameters(date, threeHourlyKp, kpSum, threeHourlyAp, apAvg, f107Adj,
fluxQualifier, ctr81Adj, lst81Adj, f107Obs, ctr81Obs, lst81Obs));
}
}
}
}
} catch (NumberFormatException nfe) {
throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE, lineNumber, name, line);
}
try {
firstDate = set.first().getDate();
lastDate = set.last().getDate();
} catch (NoSuchElementException nse) {
throw new OrekitException(nse, OrekitMessages.NO_DATA_IN_FILE, name);
}
}
/** {@inheritDoc} */
public boolean stillAcceptsData() {
return true;
}
}