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.atmosphere.data;
18
19 import org.orekit.annotation.DefaultDataContext;
20 import org.orekit.data.DataContext;
21 import org.orekit.errors.OrekitException;
22 import org.orekit.errors.OrekitMessages;
23 import org.orekit.models.earth.atmosphere.data.MarshallSolarActivityFutureEstimation.StrengthLevel;
24 import org.orekit.time.AbsoluteDate;
25 import org.orekit.time.ChronologicalComparator;
26 import org.orekit.time.DateComponents;
27 import org.orekit.time.Month;
28 import org.orekit.time.TimeScale;
29 import org.orekit.time.TimeStamped;
30
31 import java.io.BufferedReader;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.InputStreamReader;
35 import java.nio.charset.StandardCharsets;
36 import java.text.ParseException;
37 import java.util.Iterator;
38 import java.util.SortedSet;
39 import java.util.TreeSet;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 import java.util.stream.Collectors;
43
44 /**
45 * This class reads solar activity data needed by atmospheric models: F107 solar flux, Ap and Kp indexes.
46 * <p>
47 * The data are retrieved through the NASA Marshall Solar Activity Future Estimation (MSAFE) as estimates of monthly F10.7
48 * Mean solar flux and Ap geomagnetic parameter. The data can be retrieved at the NASA <a
49 * href="https://www.nasa.gov/msfcsolar/archivedforecast"> Marshall Solar Activity website</a>. Here Kp indices are deduced
50 * from Ap indexes, which in turn are tabulated equivalent of retrieved Ap values.
51 * </p>
52 * <p>
53 * If several MSAFE files are available, some dates may appear in several files (for example August 2007 is in all files from
54 * the first one published in March 1999 to the February 2008 file). In this case, the data from the most recent file is used
55 * and the older ones are discarded. The date of the file is assumed to be 6 months after its first entry (which explains why
56 * the file having August 2007 as its first entry is the February 2008 file). This implies that MSAFE files must <em>not</em>
57 * be edited to change their time span, otherwise this would break the old entries overriding mechanism.
58 * </p>
59 *
60 * <h2>References</h2>
61 *
62 * <ol> <li> Jacchia, L. G. "CIRA 1972, recent atmospheric models, and improvements in
63 * progress." COSPAR, 21st Plenary Meeting. Vol. 1. 1978. </li> </ol>
64 *
65 * @author Bruno Revelin
66 * @author Luc Maisonobe
67 * @author Evan Ward
68 * @author Pascal Parraud
69 * @author Vincent Cucchietti
70 */
71 public class MarshallSolarActivityFutureEstimationLoader
72 extends AbstractSolarActivityDataLoader<MarshallSolarActivityFutureEstimationLoader.LineParameters> {
73
74 /** Pattern for the data fields of MSAFE data. */
75 private final Pattern dataPattern;
76
77 /** Data set. */
78 private final SortedSet<TimeStamped> data;
79
80 /** Selected strength level of activity. */
81 private final StrengthLevel strengthLevel;
82
83 /**
84 * Simple constructor. This constructor uses the {@link DataContext#getDefault() default data context}.
85 *
86 * @param strengthLevel selected strength level of activity
87 */
88 @DefaultDataContext
89 public MarshallSolarActivityFutureEstimationLoader(final StrengthLevel strengthLevel) {
90 this(strengthLevel, DataContext.getDefault().getTimeScales().getUTC());
91 }
92
93 /**
94 * Constructor that allows specifying the source of the MSAFE auxiliary data files.
95 *
96 * @param strengthLevel selected strength level of activity
97 * @param utc UTC time scale.
98 *
99 * @since 10.1
100 */
101 public MarshallSolarActivityFutureEstimationLoader(final StrengthLevel strengthLevel, final TimeScale utc) {
102 super(utc);
103
104 this.data = new TreeSet<>(new ChronologicalComparator());
105 this.strengthLevel = strengthLevel;
106
107 // the data lines have the following form:
108 // 2010.5003 JUL 83.4 81.3 78.7 6.4 5.9 5.2
109 // 2010.5837 AUG 87.3 83.4 78.5 7.0 6.1 4.9
110 // 2010.6670 SEP 90.8 85.5 79.4 7.8 6.2 4.7
111 // 2010.7503 OCT 94.2 87.6 80.4 9.1 6.4 4.9
112 final StringBuilder builder = new StringBuilder("^");
113
114 // first group: year
115 builder.append("\\p{Blank}*(\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})");
116
117 // month as fraction of year, not stored in a group
118 builder.append("\\.\\p{Digit}+");
119
120 // second group: month as a three upper case letters abbreviation
121 builder.append("\\p{Blank}+(");
122 for (final Month month : Month.values()) {
123 builder.append(month.getUpperCaseAbbreviation());
124 builder.append('|');
125 }
126 builder.delete(builder.length() - 1, builder.length());
127 builder.append(")");
128
129 // third to eighth group: data fields
130 for (int i = 0; i < 6; ++i) {
131 builder.append("\\p{Blank}+([-+]?[0-9]+\\.[0-9]+)");
132 }
133
134 // end of line
135 builder.append("\\p{Blank}*$");
136
137 // compile the pattern
138 this.dataPattern = Pattern.compile(builder.toString());
139
140 }
141
142 /** {@inheritDoc} */
143 public void loadData(final InputStream input, final String name)
144 throws IOException, ParseException, OrekitException {
145
146 // select the groups we want to store
147 final int f107Group;
148 final int apGroup;
149 switch (strengthLevel) {
150 case STRONG:
151 f107Group = 3;
152 apGroup = 6;
153 break;
154 case AVERAGE:
155 f107Group = 4;
156 apGroup = 7;
157 break;
158 default:
159 f107Group = 5;
160 apGroup = 8;
161 break;
162 }
163
164 boolean inData = false;
165 DateComponents fileDate = null;
166
167 // try to read the data
168 try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
169
170 // Go through each line
171 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
172 line = line.trim();
173 if (!line.isEmpty()) {
174 final Matcher matcher = dataPattern.matcher(line);
175 if (matcher.matches()) {
176
177 // We are in the data section
178 inData = true;
179
180 // Extract the data from the line
181 final int year = Integer.parseInt(matcher.group(1));
182 final Month month = Month.parseMonth(matcher.group(2));
183 final AbsoluteDate date = new AbsoluteDate(year, month, 1, getUTC());
184 if (fileDate == null) {
185 /* The first entry of each file correspond exactly to 6 months before file publication,
186 so we compute the file date by adding 6 months to its first entry */
187 if (month.getNumber() > 6) {
188 fileDate = new DateComponents(year + 1, month.getNumber() - 6, 1);
189 } else {
190 fileDate = new DateComponents(year, month.getNumber() + 6, 1);
191 }
192 }
193
194 // check if there is already an entry for this date or not
195 boolean addEntry = false;
196 final Iterator<TimeStamped> iterator = data.tailSet(date).iterator();
197 if (iterator.hasNext()) {
198 final LineParameters existingEntry = (LineParameters) iterator.next();
199 if (existingEntry.getDate().equals(date)) {
200 // there is an entry for this date
201 if (existingEntry.getFileDate().compareTo(fileDate) < 0) {
202 // the entry was read from an earlier file
203 // we replace it with the new entry as it is fresher
204 iterator.remove();
205 addEntry = true;
206 }
207 } else {
208 // it is the first entry we get for this date
209 addEntry = true;
210 }
211 } else {
212 // it is the first entry we get for this date
213 addEntry = true;
214 }
215 if (addEntry) {
216 // we must add the new entry
217 data.add(new LineParameters(fileDate, date,
218 Double.parseDouble(matcher.group(f107Group)),
219 Double.parseDouble(matcher.group(apGroup))));
220 }
221
222 } else {
223 if (inData) {
224 /* We have already read some data, so we are not in the header anymore
225 however, we don't recognize this non-empty line, we consider the file is corrupted */
226 throw new OrekitException(OrekitMessages.NOT_A_MARSHALL_SOLAR_ACTIVITY_FUTURE_ESTIMATION_FILE,
227 name);
228 }
229 }
230 }
231 }
232
233 }
234
235 if (data.isEmpty()) {
236 throw new OrekitException(OrekitMessages.NOT_A_MARSHALL_SOLAR_ACTIVITY_FUTURE_ESTIMATION_FILE, name);
237 }
238 setMinDate(data.first().getDate());
239 setMaxDate(data.last().getDate());
240
241 }
242
243 /** @return the data set */
244 @Override
245 public SortedSet<LineParameters> getDataSet() {
246 return data.stream().map(value -> (LineParameters) value).collect(Collectors.toCollection(TreeSet::new));
247 }
248
249 /** Container class for Solar activity indexes. */
250 public static class LineParameters extends AbstractSolarActivityDataLoader.LineParameters {
251
252 /** Serializable UID. */
253 private static final long serialVersionUID = 6607862001953526475L;
254
255 /** File date. */
256 private final DateComponents fileDate;
257
258 /** F10.7 flux at date. */
259 private final double f107;
260
261 /** Ap index at date. */
262 private final double ap;
263
264 /**
265 * Simple constructor.
266 *
267 * @param fileDate file date
268 * @param date entry date
269 * @param f107 F10.7 flux at date
270 * @param ap Ap index at date
271 */
272 private LineParameters(final DateComponents fileDate, final AbsoluteDate date, final double f107, final double ap) {
273 super(date);
274 this.fileDate = fileDate;
275 this.f107 = f107;
276 this.ap = ap;
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 otherInstance) {
288 if (this == otherInstance) {
289 return true;
290 }
291 if (otherInstance == null || getClass() != otherInstance.getClass()) {
292 return false;
293 }
294
295 final LineParameters msafeParams = (LineParameters) otherInstance;
296
297 if (Double.compare(getF107(), msafeParams.getF107()) != 0) {
298 return false;
299 }
300 if (Double.compare(getAp(), msafeParams.getAp()) != 0) {
301 return false;
302 }
303 return getFileDate().equals(msafeParams.getFileDate());
304 }
305
306 /** {@inheritDoc} */
307 @Override
308 public int hashCode() {
309 int result;
310 result = getFileDate().hashCode();
311 result = 31 * result + Double.hashCode(getF107());
312 result = 31 * result + Double.hashCode(getAp());
313 return result;
314 }
315
316 /**
317 * Get the file date.
318 *
319 * @return file date
320 */
321 public DateComponents getFileDate() {
322 return fileDate;
323 }
324
325 /**
326 * Get the F10.0 flux.
327 *
328 * @return f10.7 flux
329 */
330 public double getF107() {
331 return f107;
332 }
333
334 /**
335 * Get the Ap index.
336 *
337 * @return Ap index
338 */
339 public double getAp() {
340 return ap;
341 }
342
343 }
344
345 }