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.frames;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.nio.charset.StandardCharsets;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.hipparchus.util.FastMath;
30  import org.orekit.annotation.DefaultDataContext;
31  import org.orekit.data.DataContext;
32  import org.orekit.data.DataLoader;
33  import org.orekit.data.DataProvidersManager;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.time.DateComponents;
37  
38  /** Loader for ITRF version configuration file.
39   * <p>
40   * The ITRF version configuration file specifies
41   * the {@link ITRFVersion ITRF versions} that each
42   * type of Earth Orientation Parameter file contains
43   * for each date. This configuration file is used to
44   * interpret {@link EopC04FilesLoader EOP C04} files,
45   * {@link BulletinAFilesLoader Bulletin A} files,
46   * {@link BulletinBFilesLoader Bulletin B} files,
47   * {@link RapidDataAndPredictionColumnsLoader rapid data
48   * and prediction files in columns format} files,
49   * {@link EopXmlLoader rapid data
50   * and prediction files in XML format} files...
51   * </p>
52   * <p>This file is an Orekit-specific configuration file.
53   * </p>
54   * <p>
55   * This class is immutable and hence thread-safe
56   * </p>
57   * @see EopC04FilesLoader
58   * @see BulletinAFilesLoader
59   * @see BulletinBFilesLoader
60   * @see RapidDataAndPredictionColumnsLoader
61   * @see EopXmlLoader
62   * @author Luc Maisonobe
63   * @since 9.2
64   */
65  public class ITRFVersionLoader implements ItrfVersionProvider {
66  
67      /** Regular expression for supported files names. */
68      public static final String SUPPORTED_NAMES = "itrf-versions.conf";
69  
70      /** Default entry to use if no suitable configuration is found. */
71      private static final ITRFVersionConfiguration DEFAULT =
72                      new ITRFVersionConfiguration("", ITRFVersion.ITRF_2014,
73                                                   Integer.MIN_VALUE, Integer.MAX_VALUE);
74  
75      /** Configuration. */
76      private final List<ITRFVersionConfiguration> configurations;
77  
78      /**
79       * Build a loader for ITRF version configuration file. This constructor uses the
80       * {@link DataContext#getDefault() default data context}.
81       *
82       * @param supportedNames regular expression for supported files names
83       * @see #ITRFVersionLoader(String, DataProvidersManager)
84       */
85      @DefaultDataContext
86      public ITRFVersionLoader(final String supportedNames) {
87          this(supportedNames, DataContext.getDefault().getDataProvidersManager());
88      }
89  
90      /**
91       * Build a loader for ITRF version configuration file.
92       *
93       * @param supportedNames       regular expression for supported files names
94       * @param dataProvidersManager provides access to the {@code itrf-versions.conf}
95       *                             file.
96       */
97      public ITRFVersionLoader(final String supportedNames,
98                               final DataProvidersManager dataProvidersManager) {
99          this.configurations = new ArrayList<>();
100         dataProvidersManager.feed(supportedNames, new Parser());
101     }
102 
103     /**
104      * Build a loader for ITRF version configuration file using the default name. This
105      * constructor uses the {@link DataContext#getDefault() default data context}.
106      *
107      * <p>This constructor uses the {@link DataContext#getDefault() default data context}.
108      *
109      * @see #ITRFVersionLoader(String)
110      * @see #ITRFVersionLoader(String, DataProvidersManager)
111      * @see #SUPPORTED_NAMES
112      */
113     @DefaultDataContext
114     public ITRFVersionLoader() {
115         this(SUPPORTED_NAMES);
116     }
117 
118     @Override
119     public ITRFVersionConfiguration getConfiguration(final String name, final int mjd) {
120 
121         for (final ITRFVersionConfiguration configuration : configurations) {
122             if (configuration.appliesTo(name) && configuration.isValid(mjd)) {
123                 // we have found a matching configuration
124                 return configuration;
125             }
126         }
127 
128         // no suitable configuration found, use the default value
129         return DEFAULT;
130 
131     }
132 
133     /** Internal class performing the parsing. */
134     private class Parser implements DataLoader {
135 
136         /** Regular expression matching start of line. */
137         private static final String START  = "^";
138 
139         /** Regular expression matching a non-blank field (for names regexp). */
140         private static final String NON_BLANK_FIELD = "(\\S+)";
141 
142         /** Regular expression matching a calendar date. */
143         private static final String CALENDAR_DATE  = "\\s+(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)";
144 
145         /** Regular expression matching a date at infinity. */
146         private static final String INFINITY_DATE  = "\\s+-+";
147 
148         /** Regular expression matching an ITRF version. */
149         private static final String ITRF  = "\\s+([Ii][Tt][Rr][Ff][-_ ]?[0-9]{2,4})";
150 
151         /** Regular expression matching end of line. */
152         private static final String END  = "$";
153 
154         /** {@inheritDoc} */
155         public boolean stillAcceptsData() {
156             return configurations.isEmpty();
157         }
158 
159         /** {@inheritDoc} */
160         public void loadData(final InputStream input, final String name)
161             throws IOException {
162 
163             // regular expressions for date lines
164             final Pattern patternII = Pattern.compile(START + NON_BLANK_FIELD + INFINITY_DATE + INFINITY_DATE + ITRF + END);
165             final Pattern patternID = Pattern.compile(START + NON_BLANK_FIELD + INFINITY_DATE + CALENDAR_DATE + ITRF + END);
166             final Pattern patternDI = Pattern.compile(START + NON_BLANK_FIELD + CALENDAR_DATE + INFINITY_DATE + ITRF + END);
167             final Pattern patternDD = Pattern.compile(START + NON_BLANK_FIELD + CALENDAR_DATE + CALENDAR_DATE + ITRF + END);
168 
169 
170             int lineNumber =  0;
171             String line = null;
172             // set up a reader for line-oriented bulletin A files
173             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
174                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
175 
176                     lineNumber++;
177                     line = line.trim();
178                     if (!(line.startsWith("#") || line.isEmpty())) {
179                         String prefix       = null;
180                         ITRFVersion version = null;
181                         int validityStart   = Integer.MIN_VALUE;
182                         int validityEnd     = Integer.MAX_VALUE;
183                         final Matcher matcherII = patternII.matcher(line);
184                         if (matcherII.matches()) {
185                             // both start and end of validity are at infinity
186                             // the ITRF version applies throughout history
187                             prefix  = matcherII.group(1);
188                             version = ITRFVersion.getITRFVersion(matcherII.group(2));
189                         } else {
190                             final Matcher matcherID = patternID.matcher(line);
191                             if (matcherID.matches()) {
192                                 // both start of validity is at infinity
193                                 // the ITRF version applies in the far past
194                                 prefix      = matcherID.group(1);
195                                 validityEnd = new DateComponents(Integer.parseInt(matcherID.group(2)),
196                                                                  Integer.parseInt(matcherID.group(3)),
197                                                                  Integer.parseInt(matcherID.group(4))).getMJD();
198                                 version     = ITRFVersion.getITRFVersion(matcherID.group(5));
199                             } else {
200                                 final Matcher matcherDI = patternDI.matcher(line);
201                                 if (matcherDI.matches()) {
202                                     // both end of validity is at infinity
203                                     // the ITRF version applies to the upcoming future
204                                     prefix        = matcherDI.group(1);
205                                     validityStart = new DateComponents(Integer.parseInt(matcherDI.group(2)),
206                                                                        Integer.parseInt(matcherDI.group(3)),
207                                                                        Integer.parseInt(matcherDI.group(4))).getMJD();
208                                     version       = ITRFVersion.getITRFVersion(matcherDI.group(5));
209                                 } else {
210                                     final Matcher matcherDD = patternDD.matcher(line);
211                                     if (matcherDD.matches()) {
212                                         // the ITRF version applies during a limited range
213                                         prefix        = matcherDD.group(1);
214                                         validityStart = new DateComponents(Integer.parseInt(matcherDD.group(2)),
215                                                                            Integer.parseInt(matcherDD.group(3)),
216                                                                            Integer.parseInt(matcherDD.group(4))).getMJD();
217                                         validityEnd   = new DateComponents(Integer.parseInt(matcherDD.group(5)),
218                                                                            Integer.parseInt(matcherDD.group(6)),
219                                                                            Integer.parseInt(matcherDD.group(7))).getMJD();
220                                         version       = ITRFVersion.getITRFVersion(matcherDD.group(8));
221                                     } else {
222                                         // data line was not recognized
223                                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
224                                                                   lineNumber, name, line);
225                                     }
226                                 }
227                             }
228                         }
229                         // error if prefix contains / or \ since these will never match
230                         // CHECKSTYLE: stop MultipleStringLiterals check
231                         if (prefix.contains("\\") || prefix.contains("/")) {
232                             throw new OrekitException(
233                                     OrekitMessages.ITRF_VERSIONS_PREFIX_ONLY, prefix);
234                         }
235                         // CHECKSTYLE: resume MultipleStringLiterals check
236                         // store the parsed entry
237                         configurations.add(new ITRFVersionConfiguration(prefix, version, validityStart, validityEnd));
238 
239                     }
240 
241                 }
242             } catch (IllegalArgumentException e) {
243                 throw new OrekitException(e,
244                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
245                                           lineNumber, name, line);
246             }
247 
248         }
249 
250     }
251 
252     /** ITRF version configuration entry. */
253     public static class ITRFVersionConfiguration {
254 
255         /** File names to which this configuration applies. */
256         private final String prefix;
257 
258         /** ITRF version. */
259         private final ITRFVersion version;
260 
261         /** Start of validity. */
262         private final int validityStart;
263 
264         /** End of validity. */
265         private final int validityEnd;
266 
267         /** Simple constructor.
268          * @param prefix file names to which this configuration applies
269          * @param version ITRF version
270          * @param validityStart start of validity MJD (included)
271          * @param validityEnd end of validity MJD (excluded)
272          */
273         public ITRFVersionConfiguration(final String prefix,
274                                         final ITRFVersion version,
275                                         final int validityStart,
276                                         final int validityEnd) {
277             this.prefix        = prefix;
278             this.version       = version;
279             this.validityStart = validityStart;
280             this.validityEnd   = validityEnd;
281         }
282 
283         /** Check if this entry applies to a file name.
284          * @param name file name to check
285          * @return true if the configuration applies to the specified file
286          */
287         public boolean appliesTo(final String name) {
288             final int i = FastMath.max(name.lastIndexOf("/"), name.lastIndexOf("\\"));
289             return name.startsWith(prefix, i + 1);
290         }
291 
292         /** Get ITRF version.
293          * @return ITRF version
294          */
295         public ITRFVersion getVersion() {
296             return version;
297         }
298 
299         /** Check if configuration entry is valid for a date.
300          * @param mjd date to check in modified Julian day
301          * @return true if entry is valid for the specified date
302          */
303         public boolean isValid(final int mjd) {
304             return validityStart <= mjd && mjd < validityEnd;
305         }
306 
307     }
308 
309 }