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