1   /* Copyright 2002-2020 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.SAXParserFactory;
31  
32  import org.hipparchus.exception.LocalizedCoreFormats;
33  import org.orekit.data.DataProvidersManager;
34  import org.orekit.errors.OrekitException;
35  import org.orekit.errors.OrekitMessages;
36  import org.orekit.time.AbsoluteDate;
37  import org.orekit.time.DateComponents;
38  import org.orekit.time.TimeScale;
39  import org.orekit.utils.Constants;
40  import org.orekit.utils.IERSConventions;
41  import org.xml.sax.Attributes;
42  import org.xml.sax.InputSource;
43  import org.xml.sax.SAXException;
44  import org.xml.sax.XMLReader;
45  import org.xml.sax.helpers.DefaultHandler;
46  
47  /** Loader for IERS rapid data and prediction file in XML format (finals file).
48   * <p>Rapid data and prediction file contain {@link EOPEntry
49   * Earth Orientation Parameters} for several years periods, in one file
50   * only that is updated regularly.</p>
51   * <p>The XML EOP files are recognized thanks to their base names, which
52   * must match one of the the patterns <code>finals.2000A.*.xml</code> or
53   * <code>finals.*.xml</code> (or the same ending with <code>.gz</code> for
54   * gzip-compressed files) where * stands for a word like "all", "daily",
55   * or "data".</p>
56   * <p>Files containing data (back to 1973) are available at IERS web site: <a
57   * href="http://www.iers.org/IERS/EN/DataProducts/EarthOrientationData/eop.html">Earth orientation data</a>.</p>
58   * <p>
59   * This class is immutable and hence thread-safe
60   * </p>
61   * @author Luc Maisonobe
62   */
63  class RapidDataAndPredictionXMLLoader extends AbstractEopLoader
64          implements EOPHistoryLoader {
65  
66      /** Conversion factor for milli-arc seconds entries. */
67      private static final double MILLI_ARC_SECONDS_TO_RADIANS = Constants.ARC_SECONDS_TO_RADIANS / 1000.0;
68  
69      /** Conversion factor for milli seconds entries. */
70      private static final double MILLI_SECONDS_TO_SECONDS = 1.0 / 1000.0;
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      RapidDataAndPredictionXMLLoader(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 EopParserLoaderopParserLoader">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 reader for line-oriented bulletin B files
123                 final XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
124                 reader.setContentHandler(new EOPContentHandler(name));
125                 // disable external entities
126                 reader.setEntityResolver((publicId, systemId) -> new InputSource());
127 
128                 // read all file, ignoring header
129                 reader.parse(new InputSource(new InputStreamReader(input, StandardCharsets.UTF_8)));
130 
131                 return history;
132 
133             } catch (SAXException se) {
134                 if ((se.getCause() != null) && (se.getCause() instanceof OrekitException)) {
135                     throw (OrekitException) se.getCause();
136                 }
137                 throw new OrekitException(se, LocalizedCoreFormats.SIMPLE_MESSAGE, se.getMessage());
138             } catch (ParserConfigurationException pce) {
139                 throw new OrekitException(pce, LocalizedCoreFormats.SIMPLE_MESSAGE, pce.getMessage());
140             }
141         }
142 
143         /** Local content handler for XML EOP files. */
144         private class EOPContentHandler extends DefaultHandler {
145 
146             // CHECKSTYLE: stop JavadocVariable check
147 
148             // elements and attributes used in both daily and finals data files
149             private static final String MJD_ELT           = "MJD";
150             private static final String LOD_ELT           = "LOD";
151             private static final String X_ELT             = "X";
152             private static final String Y_ELT             = "Y";
153             private static final String DPSI_ELT          = "dPsi";
154             private static final String DEPSILON_ELT      = "dEpsilon";
155             private static final String DX_ELT            = "dX";
156             private static final String DY_ELT            = "dY";
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             private static final String BULLETIN_A_SOURCE = "BulletinA";
170 
171             // elements and attributes specific to finals data files
172             private static final String FINALS_ELT        = "Finals";
173             private static final String DATE_ELT          = "date";
174             private static final String EOP_SET_ELT       = "EOPSet";
175             private static final String BULLETIN_A_ELT    = "bulletinA";
176             private static final String UT1_M_UTC_ELT     = "UT1-UTC";
177 
178             private boolean inBulletinA;
179             private int     year;
180             private int     month;
181             private int     day;
182             private int     mjd;
183             private AbsoluteDate mjdDate;
184             private double  dtu1;
185             private double  lod;
186             private double  x;
187             private double  y;
188             private double  dpsi;
189             private double  deps;
190             private double  dx;
191             private double  dy;
192 
193             // CHECKSTYLE: resume JavadocVariable check
194 
195             /** File name. */
196             private final String name;
197 
198             /** Buffer for read characters. */
199             private final StringBuffer buffer;
200 
201             /** Indicator for daily data XML format or final data XML format. */
202             private DataFileContent content;
203 
204             /** ITRF version configuration. */
205             private ITRFVersionLoader.ITRFVersionConfiguration configuration;
206 
207             /** Simple constructor.
208              * @param name file name
209              */
210             EOPContentHandler(final String name) {
211                 this.name   = name;
212                 this.buffer = new StringBuffer();
213             }
214 
215             /** {@inheritDoc} */
216             @Override
217             public void startDocument() {
218                 content       = DataFileContent.UNKNOWN;
219                 configuration = null;
220             }
221 
222             /** {@inheritDoc} */
223             @Override
224             public void characters(final char[] ch, final int start, final int length) {
225                 buffer.append(ch, start, length);
226             }
227 
228             /** {@inheritDoc} */
229             @Override
230             public void startElement(final String uri, final String localName,
231                                      final String qName, final Attributes atts) {
232 
233                 // reset the buffer to empty
234                 buffer.delete(0, buffer.length());
235 
236                 if (content == DataFileContent.UNKNOWN) {
237                     // try to identify file content
238                     if (qName.equals(TIME_SERIES_ELT)) {
239                         // the file contains final data
240                         content = DataFileContent.DAILY;
241                     } else if (qName.equals(FINALS_ELT)) {
242                         // the file contains final data
243                         content = DataFileContent.FINAL;
244                     }
245                 }
246 
247                 if (content == DataFileContent.DAILY) {
248                     startDailyElement(qName, atts);
249                 } else if (content == DataFileContent.FINAL) {
250                     startFinalElement(qName);
251                 }
252 
253             }
254 
255             /** Handle end of an element in a daily data file.
256              * @param qName name of the element
257              * @param atts element attributes
258              */
259             private void startDailyElement(final String qName, final Attributes atts) {
260                 if (qName.equals(TIME_SERIES_ELT)) {
261                     // reset EOP data
262                     resetEOPData();
263                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
264                     final String source = atts.getValue(SOURCE_ATTR);
265                     if (source != null) {
266                         inBulletinA = source.equals(BULLETIN_A_SOURCE);
267                     }
268                 }
269             }
270 
271             /** Handle end of an element in a final data file.
272              * @param qName name of the element
273              */
274             private void startFinalElement(final String qName) {
275                 if (qName.equals(EOP_SET_ELT)) {
276                     // reset EOP data
277                     resetEOPData();
278                 } else if (qName.equals(BULLETIN_A_ELT)) {
279                     inBulletinA = true;
280                 }
281             }
282 
283             /** Reset EOP data.
284              */
285             private void resetEOPData() {
286                 inBulletinA = false;
287                 year        = -1;
288                 month       = -1;
289                 day         = -1;
290                 mjd         = -1;
291                 mjdDate     = null;
292                 dtu1        = Double.NaN;
293                 lod         = Double.NaN;
294                 x           = Double.NaN;
295                 y           = Double.NaN;
296                 dpsi        = Double.NaN;
297                 deps        = Double.NaN;
298                 dx          = Double.NaN;
299                 dy          = Double.NaN;
300             }
301 
302             /** {@inheritDoc} */
303             @Override
304             public void endElement(final String uri, final String localName, final String qName)
305                 throws SAXException {
306                 try {
307                     if (content == DataFileContent.DAILY) {
308                         endDailyElement(qName);
309                     } else if (content == DataFileContent.FINAL) {
310                         endFinalElement(qName);
311                     }
312                 } catch (OrekitException oe) {
313                     throw new SAXException(oe);
314                 }
315             }
316 
317             /** Handle end of an element in a daily data file.
318              * @param qName name of the element
319              */
320             private void endDailyElement(final String qName) {
321                 if (qName.equals(DATE_YEAR_ELT) && (buffer.length() > 0)) {
322                     year = Integer.parseInt(buffer.toString());
323                 } else if (qName.equals(DATE_MONTH_ELT) && (buffer.length() > 0)) {
324                     month = Integer.parseInt(buffer.toString());
325                 } else if (qName.equals(DATE_DAY_ELT) && (buffer.length() > 0)) {
326                     day = Integer.parseInt(buffer.toString());
327                 } else if (qName.equals(MJD_ELT) && (buffer.length() > 0)) {
328                     mjd     = Integer.parseInt(buffer.toString());
329                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
330                                                getUtc());
331                 } else if (qName.equals(UT1_M_UTC_ELT)) {
332                     dtu1 = overwrite(dtu1, 1.0);
333                 } else if (qName.equals(LOD_ELT)) {
334                     lod = overwrite(lod, MILLI_SECONDS_TO_SECONDS);
335                 } else if (qName.equals(X_ELT)) {
336                     x = overwrite(x, Constants.ARC_SECONDS_TO_RADIANS);
337                 } else if (qName.equals(Y_ELT)) {
338                     y = overwrite(y, Constants.ARC_SECONDS_TO_RADIANS);
339                 } else if (qName.equals(DPSI_ELT)) {
340                     dpsi = overwrite(dpsi, MILLI_ARC_SECONDS_TO_RADIANS);
341                 } else if (qName.equals(DEPSILON_ELT)) {
342                     deps = overwrite(deps, MILLI_ARC_SECONDS_TO_RADIANS);
343                 } else if (qName.equals(DX_ELT)) {
344                     dx   = overwrite(dx, MILLI_ARC_SECONDS_TO_RADIANS);
345                 } else if (qName.equals(DY_ELT)) {
346                     dy   = overwrite(dy, MILLI_ARC_SECONDS_TO_RADIANS);
347                 } else if (qName.equals(POLE_ELT) || qName.equals(UT_ELT) || qName.equals(NUTATION_ELT)) {
348                     inBulletinA = false;
349                 } else if (qName.equals(DATA_EOP_ELT)) {
350                     checkDates();
351                     if ((!Double.isNaN(dtu1)) && (!Double.isNaN(lod)) && (!Double.isNaN(x)) && (!Double.isNaN(y))) {
352                         final double[] equinox;
353                         final double[] nro;
354                         if (Double.isNaN(dpsi)) {
355                             nro = new double[] {
356                                 dx, dy
357                             };
358                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
359                         } else {
360                             equinox = new double[] {
361                                 dpsi, deps
362                             };
363                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
364                         }
365                         if (configuration == null || !configuration.isValid(mjd)) {
366                             // get a configuration for current name and date range
367                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
368                         }
369                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, equinox[0], equinox[1], nro[0], nro[1],
370                                                  configuration.getVersion(), mjdDate));
371                     }
372                 }
373             }
374 
375             /** Handle end of an element in a final data file.
376              * @param qName name of the element
377              */
378             private void endFinalElement(final String qName) {
379                 if (qName.equals(DATE_ELT) && (buffer.length() > 0)) {
380                     final String[] fields = buffer.toString().split("-");
381                     if (fields.length == 3) {
382                         year  = Integer.parseInt(fields[0]);
383                         month = Integer.parseInt(fields[1]);
384                         day   = Integer.parseInt(fields[2]);
385                     }
386                 } else if (qName.equals(MJD_ELT) && (buffer.length() > 0)) {
387                     mjd     = Integer.parseInt(buffer.toString());
388                     mjdDate = new AbsoluteDate(new DateComponents(DateComponents.MODIFIED_JULIAN_EPOCH, mjd),
389                                                getUtc());
390                 } else if (qName.equals(UT1_U_UTC_ELT)) {
391                     dtu1 = overwrite(dtu1, 1.0);
392                 } else if (qName.equals(LOD_ELT)) {
393                     lod = overwrite(lod, MILLI_SECONDS_TO_SECONDS);
394                 } else if (qName.equals(X_ELT)) {
395                     x = overwrite(x, Constants.ARC_SECONDS_TO_RADIANS);
396                 } else if (qName.equals(Y_ELT)) {
397                     y = overwrite(y, Constants.ARC_SECONDS_TO_RADIANS);
398                 } else if (qName.equals(DPSI_ELT)) {
399                     dpsi = overwrite(dpsi, MILLI_ARC_SECONDS_TO_RADIANS);
400                 } else if (qName.equals(DEPSILON_ELT)) {
401                     deps = overwrite(deps, MILLI_ARC_SECONDS_TO_RADIANS);
402                 } else if (qName.equals(DX_ELT)) {
403                     dx   = overwrite(dx, MILLI_ARC_SECONDS_TO_RADIANS);
404                 } else if (qName.equals(DY_ELT)) {
405                     dy   = overwrite(dy, MILLI_ARC_SECONDS_TO_RADIANS);
406                 } else if (qName.equals(BULLETIN_A_ELT)) {
407                     inBulletinA = false;
408                 } else if (qName.equals(EOP_SET_ELT)) {
409                     checkDates();
410                     if ((!Double.isNaN(dtu1)) && (!Double.isNaN(lod)) && (!Double.isNaN(x)) && (!Double.isNaN(y))) {
411                         final double[] equinox;
412                         final double[] nro;
413                         if (Double.isNaN(dpsi)) {
414                             nro = new double[] {
415                                 dx, dy
416                             };
417                             equinox = getConverter().toEquinox(mjdDate, nro[0], nro[1]);
418                         } else {
419                             equinox = new double[] {
420                                 dpsi, deps
421                             };
422                             nro = getConverter().toNonRotating(mjdDate, equinox[0], equinox[1]);
423                         }
424                         if (configuration == null || !configuration.isValid(mjd)) {
425                             // get a configuration for current name and date range
426                             configuration = getItrfVersionProvider().getConfiguration(name, mjd);
427                         }
428                         history.add(new EOPEntry(mjd, dtu1, lod, x, y, equinox[0], equinox[1], nro[0], nro[1],
429                                                  configuration.getVersion(), mjdDate));
430                     }
431                 }
432             }
433 
434             /** Overwrite a value if it is not set or if we are in a bulletinB.
435              * @param oldValue old value to overwrite (may be NaN)
436              * @param factor multiplicative factor to apply to raw read data
437              * @return a new value
438              */
439             private double overwrite(final double oldValue, final double factor) {
440                 if (buffer.length() == 0) {
441                     // there is nothing to overwrite with
442                     return oldValue;
443                 } else if (inBulletinA && (!Double.isNaN(oldValue))) {
444                     // the value is already set and bulletin A values have a low priority
445                     return oldValue;
446                 } else {
447                     // either the value is not set or it is a high priority bulletin B value
448                     return Double.parseDouble(buffer.toString()) * factor;
449                 }
450             }
451 
452             /** Check if the year, month, day date and MJD date are consistent.
453              */
454             private void checkDates() {
455                 if (new DateComponents(year, month, day).getMJD() != mjd) {
456                     throw new OrekitException(OrekitMessages.INCONSISTENT_DATES_IN_IERS_FILE,
457                                               name, year, month, day, mjd);
458                 }
459             }
460 
461         }
462 
463     }
464 
465     /** Enumerate for data file content. */
466     private enum DataFileContent {
467 
468         /** Unknown content. */
469         UNKNOWN,
470 
471         /** Daily data. */
472         DAILY,
473 
474         /** Final data. */
475         FINAL
476 
477     }
478 
479 }