1   /* Copyright 2002-2026 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.IOException;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.List;
23  import java.util.function.Supplier;
24  
25  import javax.xml.parsers.ParserConfigurationException;
26  import javax.xml.parsers.SAXParser;
27  import javax.xml.parsers.SAXParserFactory;
28  
29  import org.hipparchus.exception.LocalizedCoreFormats;
30  import org.orekit.data.DataProvidersManager;
31  import org.orekit.data.DataSource;
32  import org.orekit.errors.OrekitException;
33  import org.orekit.errors.OrekitMessages;
34  import org.orekit.time.AbsoluteDate;
35  import org.orekit.time.DateComponents;
36  import org.orekit.time.TimeScale;
37  import org.orekit.utils.IERSConventions;
38  import org.orekit.utils.units.Unit;
39  import org.xml.sax.Attributes;
40  import org.xml.sax.InputSource;
41  import org.xml.sax.SAXException;
42  import org.xml.sax.helpers.DefaultHandler;
43  
44  /** Loader for IERS EOP data in XML format (finals and EOPC04 files).
45   * <p>The XML EOP files are recognized thanks to their base names, which
46   * must match one of the the patterns {@code finals.2000A.*.xml} or
47   * {@code finals.*.xml} or {@code eopc04_*.xml} (or the same ending with
48   * {@code .gz} for gzip-compressed files) where * stands for any string of characters.</p>
49   * <p>Files containing data (back to 1962) are available at IERS web site: <a
50   * href="https://datacenter.iers.org/products/eop/">IERS https data download</a>.</p>
51   * <p>
52   * This class is immutable and hence thread-safe
53   * </p>
54   * @author Luc Maisonobe
55   */
56  class EopXmlLoader extends AbstractEopLoader implements EopHistoryLoader {
57  
58      /** Millisecond unit. */
59      private static final Unit MILLI_SECOND = Unit.parse("ms");
60  
61      /** Milli arcsecond unit. */
62      private static final Unit MILLI_ARC_SECOND = Unit.parse("mas");
63  
64      /**Arcsecond per day unit.
65       * @since 12.0
66       */
67      private static final Unit ARC_SECOND_PER_DAY = Unit.parse("as/day");
68  
69      /**
70       * Build a loader for IERS XML EOP files.
71       *
72       * @param supportedNames regular expression for supported files names
73       * @param manager        provides access to the XML EOP files.
74       * @param utcSupplier    UTC time scale.
75       */
76      EopXmlLoader(final String supportedNames,
77                   final DataProvidersManager manager,
78                   final Supplier<TimeScale> utcSupplier) {
79          super(supportedNames, manager, utcSupplier);
80      }
81  
82      /** {@inheritDoc} */
83      public void fillHistory(final IERSConventions.NutationCorrectionConverter converter,
84                              final Collection<EOPEntry> history) {
85          final ItrfVersionProvider itrfVersionProvider = new ITRFVersionLoader(
86                  ITRFVersionLoader.SUPPORTED_NAMES,
87                  getDataProvidersManager());
88          final Parser parser = new Parser(converter, itrfVersionProvider, getUtc());
89          final EopParserLoader loader = new EopParserLoader(parser);
90          this.feed(loader);
91          history.addAll(loader.getEop());
92      }
93  
94      /** Internal class performing the parsing. */
95      static class Parser extends AbstractEopParser {
96  
97          /** History entries. */
98          private List<EOPEntry> history;
99  
100         /**
101          * Simple constructor.
102          *
103          * @param converter           converter to use
104          * @param itrfVersionProvider to use for determining the ITRF version of the EOP.
105          * @param utc                 time scale for parsing dates.
106          */
107         Parser(final IERSConventions.NutationCorrectionConverter converter,
108                final ItrfVersionProvider itrfVersionProvider,
109                final TimeScale utc) {
110             super(converter, itrfVersionProvider, utc);
111         }
112 
113         /** {@inheritDoc} */
114         @Override
115         public Collection<EOPEntry> parse(final DataSource source)
116             throws IOException, OrekitException {
117             try {
118                 this.history = new ArrayList<>();
119                 // set up a parser for line-oriented bulletin B files
120                 final SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
121 
122                 // read all file, ignoring header
123                 parser.parse(new InputSource(source.getOpener().openStreamOnce()),
124                              new EOPContentHandler(source.getName()));
125 
126                 return history;
127 
128             } catch (SAXException | ParserConfigurationException e) {
129                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE, e.getMessage());
130             }
131         }
132 
133         /** Local content handler for XML EOP files. */
134         private class EOPContentHandler extends DefaultHandler {
135 
136             // CHECKSTYLE: stop JavadocVariable check
137 
138             // elements and attributes used in both daily and finals data files
139             private static final String MJD_ELT           = "MJD";
140             private static final String LOD_ELT           = "LOD";
141             private static final String X_ELT             = "X";
142             private static final String Y_ELT             = "Y";
143             private static final String X_RATE_ELT        = "x_rate";
144             private static final String Y_RATE_ELT        = "y_rate";
145             private static final String DPSI_ELT          = "dPsi";
146             private static final String DEPSILON_ELT      = "dEpsilon";
147             private static final String DX_ELT            = "dX";
148             private static final String DY_ELT            = "dY";
149 
150             // elements and attributes specific to bulletinA, bulletinB and EOP C04 files
151             private static final String DATA_ELT            = "data";
152             private static final String PRODUCT_ATTR        = "product";
153             private static final String BULLETIN_A_PROD     = "BulletinA";
154             private static final String BULLETIN_B_PROD     = "BulletinB";
155             private static final String EOP_C04_PROD_PREFIX = "EOP";
156             private static final String EOP_C04_PROD_SUFFIX = "C04";
157 
158             // elements and attributes specific to daily data files
159             private static final String DATA_EOP_ELT      = "dataEOP";
160             private static final String TIME_SERIES_ELT   = "timeSeries";
161             private static final String DATE_YEAR_ELT     = "dateYear";
162             private static final String DATE_MONTH_ELT    = "dateMonth";
163             private static final String DATE_DAY_ELT      = "dateDay";
164             private static final String POLE_ELT          = "pole";
165             private static final String UT_ELT            = "UT";
166             private static final String UT1_U_UTC_ELT     = "UT1_UTC";
167             private static final String NUTATION_ELT      = "nutation";
168             private static final String SOURCE_ATTR       = "source";
169 
170             // elements and attributes specific to finals data files
171             private static final String FINALS_ELT        = "Finals";
172             private static final String DATE_ELT          = "date";
173             private static final String EOP_SET_ELT       = "EOPSet";
174             private static final String BULLETIN_A_ELT    = "bulletinA";
175             private static final String UT1_M_UTC_ELT     = "UT1-UTC";
176 
177             private boolean inBulletinA;
178             private int     year;
179             private int     month;
180             private int     day;
181             private int     mjd;
182             private AbsoluteDate mjdDate;
183             private double  dtu1;
184             private double  lod;
185             private double  x;
186             private double  y;
187             private double  xRate;
188             private double  yRate;
189             private double  dpsi;
190             private double  deps;
191             private double  dx;
192             private double  dy;
193 
194             // CHECKSTYLE: resume JavadocVariable check
195 
196             /** File name. */
197             private final String name;
198 
199             /** Buffer for read characters. */
200             private final StringBuilder buffer;
201 
202             /** Indicator for daily data XML format or final data XML format. */
203             private DataFileContent content;
204 
205             /** ITRF version configuration. */
206             private ITRFVersionLoader.ITRFVersionConfiguration configuration;
207 
208             /** Simple constructor.
209              * @param name file name
210              */
211             EOPContentHandler(final String name) {
212                 this.name   = name;
213                 this.buffer = new StringBuilder();
214             }
215 
216             /** {@inheritDoc} */
217             @Override
218             public void startDocument() {
219                 content       = DataFileContent.UNKNOWN;
220                 configuration = null;
221             }
222 
223             /** {@inheritDoc} */
224             @Override
225             public void characters(final char[] ch, final int start, final int length) {
226                 buffer.append(ch, start, length);
227             }
228 
229             /** {@inheritDoc} */
230             @Override
231             public void startElement(final String uri, final String localName,
232                                      final String qName, final Attributes atts) {
233 
234                 // reset the buffer to empty
235                 buffer.delete(0, buffer.length());
236 
237                 if (content == DataFileContent.UNKNOWN) {
238                     // try to identify file content
239                     switch (qName) {
240                         case TIME_SERIES_ELT ->
241                             // the file contains final data
242                             content = DataFileContent.DAILY;
243                         case FINALS_ELT ->
244                             // the file contains final data
245                             content = DataFileContent.FINAL;
246                         case DATA_ELT -> {
247                             final String product = atts.getValue(PRODUCT_ATTR);
248                             if (product != null) {
249                                 if (product.startsWith(BULLETIN_A_PROD)) {
250                                     // the file contains bulletinA
251                                     content = DataFileContent.BULLETIN_A;
252                                     inBulletinA = true;
253                                 } else if (product.startsWith(BULLETIN_B_PROD)) {
254                                     // the file contains bulletinB
255                                     content = DataFileContent.BULLETIN_B;
256                                 } else if (product.startsWith(EOP_C04_PROD_PREFIX) && product.endsWith(
257                                     EOP_C04_PROD_SUFFIX)) {
258                                     // the file contains EOP C04
259                                     content = DataFileContent.EOP_C04;
260                                 }
261                             }
262                         }
263                         default ->
264                             content = DataFileContent.UNKNOWN;
265                     }
266                 }
267 
268                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
269                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
270                     startDailyElement(qName, atts);
271                 } else if (content == DataFileContent.FINAL) {
272                     startFinalElement(qName);
273                 }
274 
275             }
276 
277             /** Handle end of an element in a daily data file.
278              * @param qName name of the element
279              * @param atts element attributes
280              */
281             private void startDailyElement(final String qName, final Attributes atts) {
282                 if (qName.equals(TIME_SERIES_ELT)) {
283                     // reset EOP data
284                     resetEOPData();
285                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
286                     final String source = atts.getValue(SOURCE_ATTR);
287                     if (source != null) {
288                         inBulletinA = source.equals(BULLETIN_A_PROD);
289                     }
290                 }
291             }
292 
293             /** Handle end of an element in a final data file.
294              * @param qName name of the element
295              */
296             private void startFinalElement(final String qName) {
297                 if (qName.equals(EOP_SET_ELT)) {
298                     // reset EOP data
299                     resetEOPData();
300                 } else if (qName.equals(BULLETIN_A_ELT)) {
301                     inBulletinA = true;
302                 }
303             }
304 
305             /** Reset EOP data.
306              */
307             private void resetEOPData() {
308                 inBulletinA = false;
309                 year        = -1;
310                 month       = -1;
311                 day         = -1;
312                 mjd         = -1;
313                 mjdDate     = null;
314                 dtu1        = Double.NaN;
315                 lod         = Double.NaN;
316                 x           = Double.NaN;
317                 y           = Double.NaN;
318                 xRate       = Double.NaN;
319                 yRate       = Double.NaN;
320                 dpsi        = Double.NaN;
321                 deps        = Double.NaN;
322                 dx          = Double.NaN;
323                 dy          = Double.NaN;
324             }
325 
326             /** {@inheritDoc} */
327             @Override
328             public void endElement(final String uri, final String localName, final String qName) {
329                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
330                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
331                     endDailyElement(qName);
332                 } else if (content == DataFileContent.FINAL) {
333                     endFinalElement(qName);
334                 }
335             }
336 
337             /** Handle end of an element in a daily data file.
338              * @param qName name of the element
339              */
340             private void endDailyElement(final String qName) {
341                 if (qName.equals(DATE_YEAR_ELT) && !buffer.isEmpty()) {
342                     year = Integer.parseInt(buffer.toString());
343                 } else if (qName.equals(DATE_MONTH_ELT) && !buffer.isEmpty()) {
344                     month = Integer.parseInt(buffer.toString());
345                 } else if (qName.equals(DATE_DAY_ELT) && !buffer.isEmpty()) {
346                     day = Integer.parseInt(buffer.toString());
347                 } else if (qName.equals(MJD_ELT) && !buffer.isEmpty()) {
348                     mjd     = Integer.parseInt(buffer.toString());
349                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
350                                                getUtc());
351                 } else if (qName.equals(UT1_M_UTC_ELT)) {
352                     dtu1 = overwrite(dtu1, Unit.SECOND);
353                 } else if (qName.equals(LOD_ELT)) {
354                     lod = overwrite(lod, MILLI_SECOND);
355                 } else if (qName.equals(X_ELT)) {
356                     x = overwrite(x, Unit.ARC_SECOND);
357                 } else if (qName.equals(Y_ELT)) {
358                     y = overwrite(y, Unit.ARC_SECOND);
359                 } else if (qName.equals(X_RATE_ELT)) {
360                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
361                 } else if (qName.equals(Y_RATE_ELT)) {
362                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
363                 } else if (qName.equals(DPSI_ELT)) {
364                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
365                 } else if (qName.equals(DEPSILON_ELT)) {
366                     deps = overwrite(deps, MILLI_ARC_SECOND);
367                 } else if (qName.equals(DX_ELT)) {
368                     dx   = overwrite(dx, MILLI_ARC_SECOND);
369                 } else if (qName.equals(DY_ELT)) {
370                     dy   = overwrite(dy, MILLI_ARC_SECOND);
371                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
372                     inBulletinA = false;
373                 } else if (qName.equals(DATA_EOP_ELT)) {
374                     checkDates();
375                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
376                         final double[] equinox;
377                         final double[] nro;
378                         if (Double.isNaN(dpsi)) {
379                             nro = new double[] {
380                                 dx, dy
381                             };
382                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
383                         } else {
384                             equinox = new double[] {
385                                 dpsi, deps
386                             };
387                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
388                         }
389                         if (configuration == null || !configuration.isValid(mjd)) {
390                             // get a configuration for current name and date range
391                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
392                         }
393                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
394                                                  equinox[0], equinox[1], nro[0], nro[1],
395                                                  configuration.getVersion(), mjdDate, EopDataType.UNKNOWN));
396                     }
397                 }
398             }
399 
400             /** Handle end of an element in a final data file.
401              * @param qName name of the element
402              */
403             private void endFinalElement(final String qName) {
404                 if (qName.equals(DATE_ELT) && !buffer.isEmpty()) {
405                     final String[] fields = buffer.toString().split("-");
406                     if (fields.length == 3) {
407                         year  = Integer.parseInt(fields[0]);
408                         month = Integer.parseInt(fields[1]);
409                         day   = Integer.parseInt(fields[2]);
410                     }
411                 } else if (qName.equals(MJD_ELT) && !buffer.isEmpty()) {
412                     mjd     = Integer.parseInt(buffer.toString());
413                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
414                                                getUtc());
415                 } else if (qName.equals(UT1_U_UTC_ELT)) {
416                     dtu1 = overwrite(dtu1, Unit.SECOND);
417                 } else if (qName.equals(LOD_ELT)) {
418                     lod = overwrite(lod, MILLI_SECOND);
419                 } else if (qName.equals(X_ELT)) {
420                     x = overwrite(x, Unit.ARC_SECOND);
421                 } else if (qName.equals(Y_ELT)) {
422                     y = overwrite(y, Unit.ARC_SECOND);
423                 } else if (qName.equals(X_RATE_ELT)) {
424                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
425                 } else if (qName.equals(Y_RATE_ELT)) {
426                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
427                 } else if (qName.equals(DPSI_ELT)) {
428                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
429                 } else if (qName.equals(DEPSILON_ELT)) {
430                     deps = overwrite(deps, MILLI_ARC_SECOND);
431                 } else if (qName.equals(DX_ELT)) {
432                     dx   = overwrite(dx, MILLI_ARC_SECOND);
433                 } else if (qName.equals(DY_ELT)) {
434                     dy   = overwrite(dy, MILLI_ARC_SECOND);
435                 } else if (qName.equals(BULLETIN_A_ELT)) {
436                     inBulletinA = false;
437                 } else if (qName.equals(EOP_SET_ELT)) {
438                     checkDates();
439                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
440                         final double[] equinox;
441                         final double[] nro;
442                         if (Double.isNaN(dpsi)) {
443                             nro = new double[] {
444                                 dx, dy
445                             };
446                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
447                         } else {
448                             equinox = new double[] {
449                                 dpsi, deps
450                             };
451                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
452                         }
453                         if (configuration == null || !configuration.isValid(mjd)) {
454                             // get a configuration for current name and date range
455                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
456                         }
457                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, xRate, yRate,
458                                                  equinox[0], equinox[1], nro[0], nro[1],
459                                                  configuration.getVersion(), mjdDate,
460                                                  EopDataType.UNKNOWN));
461                     }
462                 }
463             }
464 
465             /** Overwrite a value if it is not set or if we are in a bulletinB.
466              * @param oldValue old value to overwrite (may be NaN)
467              * @param units units of raw data
468              * @return a new value
469              */
470             private double overwrite(final double oldValue, final Unit units) {
471                 if (buffer.isEmpty()) {
472                     // there is nothing to overwrite with
473                     return oldValue;
474                 } else if (inBulletinA && !Double.isNaN(oldValue)) {
475                     // the value is already set and bulletin A values have a low priority
476                     return oldValue;
477                 } else {
478                     // either the value is not set or it is a high priority bulletin B value
479                     return units.toSI(Double.parseDouble(buffer.toString()));
480                 }
481             }
482 
483             /** Check if the year, month, day date and MJD date are consistent.
484              */
485             private void checkDates() {
486                 if (new DateComponents(year, month, day).getMJD() != mjd) {
487                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
488                                               name, year, month, day, mjd);
489                 }
490             }
491 
492             /** {@inheritDoc} */
493             @Override
494             public InputSource resolveEntity(final String publicId, final String systemId) {
495                 // disable external entities
496                 return new InputSource();
497             }
498 
499         }
500 
501     }
502 
503     /** Enumerate for data file content. */
504     private enum DataFileContent {
505 
506         /** Unknown content. */
507         UNKNOWN,
508 
509         /** Bulletin A data.
510          * @since 12.0
511          */
512         BULLETIN_A,
513 
514         /** Bulletin B data.
515          * @since 12.0
516          */
517         BULLETIN_B,
518 
519         /** EOP_C04 data.
520          * @since 12.0
521          */
522         EOP_C04,
523 
524         /** Daily data. */
525         DAILY,
526 
527         /** Final data. */
528         FINAL
529 
530     }
531 
532 }