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 List<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            mjd;
179             private DateComponents dc;
180             private AbsoluteDate   date;
181             private double         dtu1;
182             private double         lod;
183             private double         x;
184             private double         y;
185             private double         xRate;
186             private double         yRate;
187             private double         dpsi;
188             private double         deps;
189             private double         dx;
190             private double         dy;
191 
192             // CHECKSTYLE: resume JavadocVariable check
193 
194             /** File name. */
195             private final String name;
196 
197             /** Buffer for read characters. */
198             private final StringBuilder buffer;
199 
200             /** Indicator for daily data XML format or final data XML format. */
201             private DataFileContent content;
202 
203             /** ITRF version configuration. */
204             private ITRFVersionLoader.ITRFVersionConfiguration configuration;
205 
206             /** Simple constructor.
207              * @param name file name
208              */
209             EOPContentHandler(final String name) {
210                 this.name   = name;
211                 this.buffer = new StringBuilder();
212             }
213 
214             /** {@inheritDoc} */
215             @Override
216             public void startDocument() {
217                 content       = DataFileContent.UNKNOWN;
218                 configuration = null;
219             }
220 
221             /** {@inheritDoc} */
222             @Override
223             public void characters(final char[] ch, final int start, final int length) {
224                 buffer.append(ch, start, length);
225             }
226 
227             /** {@inheritDoc} */
228             @Override
229             public void startElement(final String uri, final String localName,
230                                      final String qName, final Attributes atts) {
231 
232                 // reset the buffer to empty
233                 buffer.delete(0, buffer.length());
234 
235                 if (content == DataFileContent.UNKNOWN) {
236                     // try to identify file content
237                     switch (qName) {
238                         case TIME_SERIES_ELT ->
239                             // the file contains final data
240                             content = DataFileContent.DAILY;
241                         case FINALS_ELT ->
242                             // the file contains final data
243                             content = DataFileContent.FINAL;
244                         case DATA_ELT -> {
245                             final String product = atts.getValue(PRODUCT_ATTR);
246                             if (product != null) {
247                                 if (product.startsWith(BULLETIN_A_PROD)) {
248                                     // the file contains bulletinA
249                                     content = DataFileContent.BULLETIN_A;
250                                     inBulletinA = true;
251                                 } else if (product.startsWith(BULLETIN_B_PROD)) {
252                                     // the file contains bulletinB
253                                     content = DataFileContent.BULLETIN_B;
254                                 } else if (product.startsWith(EOP_C04_PROD_PREFIX) && product.endsWith(
255                                     EOP_C04_PROD_SUFFIX)) {
256                                     // the file contains EOP C04
257                                     content = DataFileContent.EOP_C04;
258                                 }
259                             }
260                         }
261                         default ->
262                             content = DataFileContent.UNKNOWN;
263                     }
264                 }
265 
266                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
267                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
268                     startDailyElement(qName, atts);
269                 } else if (content == DataFileContent.FINAL) {
270                     startFinalElement(qName);
271                 }
272 
273             }
274 
275             /** Handle end of an element in a daily data file.
276              * @param qName name of the element
277              * @param atts element attributes
278              */
279             private void startDailyElement(final String qName, final Attributes atts) {
280                 if (qName.equals(TIME_SERIES_ELT)) {
281                     // reset EOP data
282                     resetEOPData();
283                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
284                     final String source = atts.getValue(SOURCE_ATTR);
285                     if (source != null) {
286                         inBulletinA = source.equals(BULLETIN_A_PROD);
287                     }
288                 }
289             }
290 
291             /** Handle end of an element in a final data file.
292              * @param qName name of the element
293              */
294             private void startFinalElement(final String qName) {
295                 if (qName.equals(EOP_SET_ELT)) {
296                     // reset EOP data
297                     resetEOPData();
298                 } else if (qName.equals(BULLETIN_A_ELT)) {
299                     inBulletinA = true;
300                 }
301             }
302 
303             /** Reset EOP data.
304              */
305             private void resetEOPData() {
306                 inBulletinA = false;
307                 dc          = null;
308                 mjd         = -1;
309                 date        = null;
310                 dtu1        = Double.NaN;
311                 lod         = Double.NaN;
312                 x           = Double.NaN;
313                 y           = Double.NaN;
314                 xRate       = Double.NaN;
315                 yRate       = Double.NaN;
316                 dpsi        = Double.NaN;
317                 deps        = Double.NaN;
318                 dx          = Double.NaN;
319                 dy          = Double.NaN;
320             }
321 
322             /** {@inheritDoc} */
323             @Override
324             public void endElement(final String uri, final String localName, final String qName) {
325                 if (content == DataFileContent.DAILY      || content == DataFileContent.BULLETIN_A ||
326                     content == DataFileContent.BULLETIN_B || content == DataFileContent.EOP_C04) {
327                     endDailyElement(qName);
328                 } else if (content == DataFileContent.FINAL) {
329                     endFinalElement(qName);
330                 }
331             }
332 
333             /** Handle end of an element in a daily data file.
334              * @param qName name of the element
335              */
336             private void endDailyElement(final String qName) {
337                 if (qName.equals(DATE_YEAR_ELT) && !buffer.isEmpty()) {
338                     dc = new DateComponents(Integer.parseInt(buffer.toString()), 1, 1);
339                 } else if (qName.equals(DATE_MONTH_ELT) && !buffer.isEmpty()) {
340                     dc = new DateComponents(dc.getYear(), Integer.parseInt(buffer.toString()), 1);
341                 } else if (qName.equals(DATE_DAY_ELT) && !buffer.isEmpty()) {
342                     dc = new DateComponents(dc.getYear(), dc.getMonth(), Integer.parseInt(buffer.toString()));
343                 } else if (qName.equals(MJD_ELT) && !buffer.isEmpty()) {
344                     mjd  = Integer.parseInt(buffer.toString());
345                     date = new AbsoluteDate(dc, getUtc());
346                 } else if (qName.equals(UT1_M_UTC_ELT)) {
347                     dtu1 = overwrite(dtu1, Unit.SECOND);
348                 } else if (qName.equals(LOD_ELT)) {
349                     lod = overwrite(lod, MILLI_SECOND);
350                 } else if (qName.equals(X_ELT)) {
351                     x = overwrite(x, Unit.ARC_SECOND);
352                 } else if (qName.equals(Y_ELT)) {
353                     y = overwrite(y, Unit.ARC_SECOND);
354                 } else if (qName.equals(X_RATE_ELT)) {
355                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
356                 } else if (qName.equals(Y_RATE_ELT)) {
357                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
358                 } else if (qName.equals(DPSI_ELT)) {
359                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
360                 } else if (qName.equals(DEPSILON_ELT)) {
361                     deps = overwrite(deps, MILLI_ARC_SECOND);
362                 } else if (qName.equals(DX_ELT)) {
363                     dx   = overwrite(dx, MILLI_ARC_SECOND);
364                 } else if (qName.equals(DY_ELT)) {
365                     dy   = overwrite(dy, MILLI_ARC_SECOND);
366                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
367                     inBulletinA = false;
368                 } else if (qName.equals(DATA_EOP_ELT)) {
369                     checkDates();
370                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
371                         final double[] equinox;
372                         final double[] nro;
373                         if (Double.isNaN(dpsi)) {
374                             nro = new double[] {
375                                 dx, dy
376                             };
377                             equinox = getConverter().toEquinox(date, nro[0], nro[1]);
378                         } else {
379                             equinox = new double[] {
380                                 dpsi, deps
381                             };
382                             nro = getConverter().toNonRotating(date, equinox[0], equinox[1]);
383                         }
384                         if (configuration == null || !configuration.isValid(mjd)) {
385                             // get a configuration for current name and date range
386                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
387                         }
388                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, Double.NaN, Double.NaN,
389                                                  equinox[0], equinox[1], nro[0], nro[1],
390                                                  configuration.getVersion(), date, EopDataType.UNKNOWN,
391                                                  new EOPOrigin(dc, name), null, null));
392                     }
393                 }
394             }
395 
396             /** Handle end of an element in a final data file.
397              * @param qName name of the element
398              */
399             private void endFinalElement(final String qName) {
400                 if (qName.equals(DATE_ELT) && !buffer.isEmpty()) {
401                     final String[] fields = buffer.toString().split("-");
402                     if (fields.length == 3) {
403                         dc    = new DateComponents(Integer.parseInt(fields[0]),
404                                                    Integer.parseInt(fields[1]),
405                                                    Integer.parseInt(fields[2]));
406                     }
407                 } else if (qName.equals(MJD_ELT) && !buffer.isEmpty()) {
408                     mjd  = Integer.parseInt(buffer.toString());
409                     date = new AbsoluteDate(dc, getUtc());
410                 } else if (qName.equals(UT1_U_UTC_ELT)) {
411                     dtu1 = overwrite(dtu1, Unit.SECOND);
412                 } else if (qName.equals(LOD_ELT)) {
413                     lod = overwrite(lod, MILLI_SECOND);
414                 } else if (qName.equals(X_ELT)) {
415                     x = overwrite(x, Unit.ARC_SECOND);
416                 } else if (qName.equals(Y_ELT)) {
417                     y = overwrite(y, Unit.ARC_SECOND);
418                 } else if (qName.equals(X_RATE_ELT)) {
419                     xRate = overwrite(xRate, ARC_SECOND_PER_DAY);
420                 } else if (qName.equals(Y_RATE_ELT)) {
421                     yRate = overwrite(yRate, ARC_SECOND_PER_DAY);
422                 } else if (qName.equals(DPSI_ELT)) {
423                     dpsi = overwrite(dpsi, MILLI_ARC_SECOND);
424                 } else if (qName.equals(DEPSILON_ELT)) {
425                     deps = overwrite(deps, MILLI_ARC_SECOND);
426                 } else if (qName.equals(DX_ELT)) {
427                     dx   = overwrite(dx, MILLI_ARC_SECOND);
428                 } else if (qName.equals(DY_ELT)) {
429                     dy   = overwrite(dy, MILLI_ARC_SECOND);
430                 } else if (qName.equals(BULLETIN_A_ELT)) {
431                     inBulletinA = false;
432                 } else if (qName.equals(EOP_SET_ELT)) {
433                     checkDates();
434                     if (!Double.isNaN(dtu1) && !Double.isNaN(x) && !Double.isNaN(y)) {
435                         final double[] equinox;
436                         final double[] nro;
437                         if (Double.isNaN(dpsi)) {
438                             nro = new double[] {
439                                 dx, dy
440                             };
441                             equinox = getConverter().toEquinox(date, nro[0], nro[1]);
442                         } else {
443                             equinox = new double[] {
444                                 dpsi, deps
445                             };
446                             nro = getConverter().toNonRotating(date, equinox[0], equinox[1]);
447                         }
448                         if (configuration == null || !configuration.isValid(mjd)) {
449                             // get a configuration for current name and date range
450                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
451                         }
452                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, xRate, yRate,
453                                                  equinox[0], equinox[1], nro[0], nro[1],
454                                                  configuration.getVersion(), date, EopDataType.UNKNOWN,
455                                                  new EOPOrigin(dc, name), null, null));
456                     }
457                 }
458             }
459 
460             /** Overwrite a value if it is not set or if we are in a bulletinB.
461              * @param oldValue old value to overwrite (may be NaN)
462              * @param units units of raw data
463              * @return a new value
464              */
465             private double overwrite(final double oldValue, final Unit units) {
466                 if (buffer.isEmpty()) {
467                     // there is nothing to overwrite with
468                     return oldValue;
469                 } else if (inBulletinA && !Double.isNaN(oldValue)) {
470                     // the value is already set and bulletin A values have a low priority
471                     return oldValue;
472                 } else {
473                     // either the value is not set or it is a high priority bulletin B value
474                     return units.toSI(Double.parseDouble(buffer.toString()));
475                 }
476             }
477 
478             /** Check if the year, month, day date and MJD date are consistent.
479              */
480             private void checkDates() {
481                 if (dc.getMJD() != mjd) {
482                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
483                                               name, dc.getYear(), dc.getMonth(), dc.getDay(), mjd);
484                 }
485             }
486 
487             /** {@inheritDoc} */
488             @Override
489             public InputSource resolveEntity(final String publicId, final String systemId) {
490                 // disable external entities
491                 return new InputSource();
492             }
493 
494         }
495 
496     }
497 
498     /** Enumerate for data file content. */
499     private enum DataFileContent {
500 
501         /** Unknown content. */
502         UNKNOWN,
503 
504         /** Bulletin A data.
505          * @since 12.0
506          */
507         BULLETIN_A,
508 
509         /** Bulletin B data.
510          * @since 12.0
511          */
512         BULLETIN_B,
513 
514         /** EOP_C04 data.
515          * @since 12.0
516          */
517         EOP_C04,
518 
519         /** Daily data. */
520         DAILY,
521 
522         /** Final data. */
523         FINAL
524 
525     }
526 
527 }