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 }