1   /* Copyright 2002-2022 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.files.sinex;
18  
19  import java.io.BufferedInputStream;
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.nio.charset.StandardCharsets;
25  import java.text.ParseException;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.regex.Pattern;
30  
31  import org.hipparchus.exception.DummyLocalizable;
32  import org.hipparchus.geometry.euclidean.threed.Vector3D;
33  import org.hipparchus.util.FastMath;
34  import org.orekit.annotation.DefaultDataContext;
35  import org.orekit.data.DataContext;
36  import org.orekit.data.DataLoader;
37  import org.orekit.data.DataProvidersManager;
38  import org.orekit.data.DataSource;
39  import org.orekit.errors.OrekitException;
40  import org.orekit.errors.OrekitMessages;
41  import org.orekit.files.sinex.Station.ReferenceSystem;
42  import org.orekit.time.AbsoluteDate;
43  import org.orekit.time.DateComponents;
44  import org.orekit.time.TimeScale;
45  import org.orekit.utils.Constants;
46  
47  /**
48   * Loader for Solution INdependent EXchange (SINEX) files.
49   * <p>
50   * For now only few keys are supported: SITE/ID, SITE/ECCENTRICITY, SOLUTION/EPOCHS and SOLUTION/ESTIMATE.
51   * They represent the minimum set of parameters that are interesting to consider in a SINEX file.
52   * </p>
53   * @author Bryan Cazabonne
54   * @since 10.3
55   */
56  public class SinexLoader {
57  
58      /** 00:000:00000 epoch. */
59      private static final String DEFAULT_EPOCH = "00:000:00000";
60  
61      /** Pattern for delimiting regular expressions. */
62      private static final Pattern SEPARATOR = Pattern.compile(":");
63  
64      /** Station data.
65       * Key: Site code
66       */
67      private final Map<String, Station> stations;
68  
69      /** UTC time scale. */
70      private final TimeScale utc;
71  
72      /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
73       * default data context}.
74       * @param supportedNames regular expression for supported files names
75       * @see #SinexLoader(String, DataProvidersManager, TimeScale)
76       */
77      @DefaultDataContext
78      public SinexLoader(final String supportedNames) {
79          this(supportedNames,
80               DataContext.getDefault().getDataProvidersManager(),
81               DataContext.getDefault().getTimeScales().getUTC());
82      }
83  
84      /**
85       * Construct a loader by specifying the source of SINEX auxiliary data files.
86       * @param supportedNames regular expression for supported files names
87       * @param dataProvidersManager provides access to auxiliary data.
88       * @param utc UTC time scale
89       */
90      public SinexLoader(final String supportedNames,
91                         final DataProvidersManager dataProvidersManager,
92                         final TimeScale utc) {
93          this.utc = utc;
94          stations = new HashMap<>();
95          dataProvidersManager.feed(supportedNames, new Parser());
96      }
97  
98      /** Simple constructor. This constructor uses the {@link DataContext#getDefault()
99       * default data context}.
100      * @param source source for the RINEX data
101      * @see #SinexLoader(String, DataProvidersManager, TimeScale)
102      */
103     @DefaultDataContext
104     public SinexLoader(final DataSource source) {
105         this(source, DataContext.getDefault().getTimeScales().getUTC());
106     }
107 
108     /**
109      * Loads SINEX from the given input stream using the specified auxiliary data.
110      * @param source source for the RINEX data
111      * @param utc UTC time scale
112      */
113     public SinexLoader(final DataSource source, final TimeScale utc) {
114         try {
115             this.utc = utc;
116             stations = new HashMap<>();
117             try (InputStream         is  = source.getOpener().openStreamOnce();
118                  BufferedInputStream bis = new BufferedInputStream(is)) {
119                 new Parser().loadData(bis, source.getName());
120             }
121         } catch (IOException | ParseException ioe) {
122             throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
123         }
124     }
125 
126     /**
127      * Get the parsed station data.
128      * @return unmodifiable view of parsed station data
129      */
130     public Map<String, Station> getStations() {
131         return Collections.unmodifiableMap(stations);
132     }
133 
134     /**
135      * Get the station corresponding to the given site code.
136      * @param siteCode site code
137      * @return the corresponding station
138      */
139     public Station getStation(final String siteCode) {
140         return stations.get(siteCode);
141     }
142 
143     /**
144      * Add a new entry to the map of stations.
145      * @param station station entry to add
146      */
147     private void addStation(final Station station) {
148         // Check if station already exists
149         if (stations.get(station.getSiteCode()) == null) {
150             stations.put(station.getSiteCode(), station);
151         }
152     }
153 
154     /** Parser for SINEX files. */
155     private class Parser implements DataLoader {
156 
157         /** Start character of a comment line. */
158         private static final String COMMENT = "*";
159 
160         /** {@inheritDoc} */
161         @Override
162         public boolean stillAcceptsData() {
163             // We load all SINEX files we can find
164             return true;
165         }
166 
167         /** {@inheritDoc} */
168         @Override
169         public void loadData(final InputStream input, final String name)
170             throws IOException, ParseException {
171 
172             // Useful parameters
173             int lineNumber     = 0;
174             String line        = null;
175             boolean inId       = false;
176             boolean inEcc      = false;
177             boolean inEpoch    = false;
178             boolean inEstimate = false;
179             boolean firstEcc   = true;
180             Vector3D position  = Vector3D.ZERO;
181             Vector3D velocity  = Vector3D.ZERO;
182 
183             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
184 
185                 // Loop on lines
186                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
187                     ++lineNumber;
188                     // For now, only few keys are supported
189                     // They represent the minimum set of parameters that are interesting to consider in a SINEX file
190                     // Other keys can be added depending user needs
191                     switch (line.trim()) {
192                         case "+SITE/ID" :
193                             // Start of site id. data
194                             inId = true;
195                             break;
196                         case "-SITE/ID" :
197                             // End of site id. data
198                             inId = false;
199                             break;
200                         case "+SITE/ECCENTRICITY" :
201                             // Start of antenna eccentricities data
202                             inEcc = true;
203                             break;
204                         case "-SITE/ECCENTRICITY" :
205                             // End of antenna eccentricities data
206                             inEcc = false;
207                             break;
208                         case "+SOLUTION/EPOCHS" :
209                             // Start of epoch data
210                             inEpoch = true;
211                             break;
212                         case "-SOLUTION/EPOCHS" :
213                             // End of epoch data
214                             inEpoch = false;
215                             break;
216                         case "+SOLUTION/ESTIMATE" :
217                             // Start of coordinates data
218                             inEstimate = true;
219                             break;
220                         case "-SOLUTION/ESTIMATE" :
221                             // Start of coordinates data
222                             inEstimate = false;
223                             break;
224                         default:
225                             if (line.startsWith(COMMENT)) {
226                                 // ignore that line
227                             } else {
228                                 // parsing data
229                                 if (inId) {
230                                     // read site id. data
231                                     final Station station = new Station();
232                                     station.setSiteCode(parseString(line, 1, 4));
233                                     station.setDomes(parseString(line, 9, 9));
234                                     // add the station to the map
235                                     addStation(station);
236                                 } else if (inEcc) {
237 
238                                     // read antenna eccentricities data
239                                     final Station station = getStation(parseString(line, 1, 4));
240 
241                                     // check if it is the first eccentricity entry for this station
242                                     if (station.getEccentricitiesTimeSpanMap().getSpansNumber() == 1) {
243                                         // we are parsing eccentricity data for a new station
244                                         firstEcc = true;
245                                     }
246 
247                                     // start and end of validity for the current entry
248                                     final AbsoluteDate start = stringEpochToAbsoluteDate(parseString(line, 16, 12));
249                                     final AbsoluteDate end   = stringEpochToAbsoluteDate(parseString(line, 29, 12));
250 
251                                     // reference system UNE or XYZ
252                                     station.setEccRefSystem(ReferenceSystem.getEccRefSystem(parseString(line, 42, 3)));
253 
254                                     // eccentricity vector
255                                     final Vector3D eccStation = new Vector3D(parseDouble(line, 46, 8),
256                                                                              parseDouble(line, 55, 8),
257                                                                              parseDouble(line, 64, 8));
258 
259                                     // special implementation for the first entry
260                                     if (firstEcc) {
261                                         // we want null values outside validity limits of the station
262                                         station.addStationEccentricitiesValidBefore(eccStation, end);
263                                         station.addStationEccentricitiesValidBefore(null,       start);
264                                         // we parsed the first entry, set the flag to false
265                                         firstEcc = false;
266                                     } else {
267                                         station.addStationEccentricitiesValidBefore(eccStation, end);
268                                     }
269 
270                                     // update the last known eccentricities entry
271                                     station.setEccentricities(eccStation);
272 
273                                 } else if (inEpoch) {
274                                     // read epoch data
275                                     final Station station = getStation(parseString(line, 1, 4));
276                                     station.setValidFrom(stringEpochToAbsoluteDate(parseString(line, 16, 12)));
277                                     station.setValidUntil(stringEpochToAbsoluteDate(parseString(line, 29, 12)));
278                                 } else if (inEstimate) {
279                                     final Station station = getStation(parseString(line, 14, 4));
280                                     // check if this station exists
281                                     if (station != null) {
282                                         // switch on coordinates data
283                                         switch (parseString(line, 7, 6)) {
284                                             case "STAX":
285                                                 // station X coordinate
286                                                 final double x = parseDouble(line, 47, 22);
287                                                 position = new Vector3D(x, position.getY(), position.getZ());
288                                                 station.setPosition(position);
289                                                 break;
290                                             case "STAY":
291                                                 // station Y coordinate
292                                                 final double y = parseDouble(line, 47, 22);
293                                                 position = new Vector3D(position.getX(), y, position.getZ());
294                                                 station.setPosition(position);
295                                                 break;
296                                             case "STAZ":
297                                                 // station Z coordinate
298                                                 final double z = parseDouble(line, 47, 22);
299                                                 position = new Vector3D(position.getX(), position.getY(), z);
300                                                 station.setPosition(position);
301                                                 // set the reference epoch (identical for all coordinates)
302                                                 station.setEpoch(stringEpochToAbsoluteDate(parseString(line, 27, 12)));
303                                                 // reset position vector
304                                                 position = Vector3D.ZERO;
305                                                 break;
306                                             case "VELX":
307                                                 // station X velocity (value is in m/y)
308                                                 final double vx = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
309                                                 velocity = new Vector3D(vx, velocity.getY(), velocity.getZ());
310                                                 station.setVelocity(velocity);
311                                                 break;
312                                             case "VELY":
313                                                 // station Y velocity (value is in m/y)
314                                                 final double vy = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
315                                                 velocity = new Vector3D(velocity.getX(), vy, velocity.getZ());
316                                                 station.setVelocity(velocity);
317                                                 break;
318                                             case "VELZ":
319                                                 // station Z velocity (value is in m/y)
320                                                 final double vz = parseDouble(line, 47, 22) / Constants.JULIAN_YEAR;
321                                                 velocity = new Vector3D(velocity.getX(), velocity.getY(), vz);
322                                                 station.setVelocity(velocity);
323                                                 // reset position vector
324                                                 velocity = Vector3D.ZERO;
325                                                 break;
326                                             default:
327                                                 // ignore that field
328                                                 break;
329                                         }
330                                     }
331 
332                                 } else {
333                                     // not supported line, ignore it
334                                 }
335                             }
336                             break;
337                     }
338                 }
339 
340             } catch (NumberFormatException nfe) {
341                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
342                                           lineNumber, name, line);
343             }
344 
345         }
346 
347         /** Extract a string from a line.
348          * @param line to parse
349          * @param start start index of the string
350          * @param length length of the string
351          * @return parsed string
352          */
353         private String parseString(final String line, final int start, final int length) {
354             return line.substring(start, FastMath.min(line.length(), start + length)).trim();
355         }
356 
357         /** Extract a double from a line.
358          * @param line to parse
359          * @param start start index of the real
360          * @param length length of the real
361          * @return parsed real
362          */
363         private double parseDouble(final String line, final int start, final int length) {
364             return Double.parseDouble(parseString(line, start, length));
365         }
366 
367     }
368 
369     /**
370      * Transform a String epoch to an AbsoluteDate.
371      * @param stringDate string epoch
372      * @return the corresponding AbsoluteDate
373      */
374     private AbsoluteDate stringEpochToAbsoluteDate(final String stringDate) {
375 
376         // Deal with 00:000:00000 epochs
377         if (DEFAULT_EPOCH.equals(stringDate)) {
378             // Data is still available, return a dummy date at infinity in the future direction
379             return AbsoluteDate.FUTURE_INFINITY;
380         }
381 
382         // Date components
383         final String[] fields = SEPARATOR.split(stringDate);
384 
385         // Read fields
386         final int twoDigitsYear = Integer.parseInt(fields[0]);
387         final int day           = Integer.parseInt(fields[1]);
388         final int secInDay      = Integer.parseInt(fields[2]);
389 
390         // Data year
391         final int year;
392         if (twoDigitsYear > 50) {
393             year = 1900 + twoDigitsYear;
394         } else {
395             year = 2000 + twoDigitsYear;
396         }
397 
398         // Return an absolute date.
399         // Initialize to 1st January of the given year because
400         // sometimes day in equal to 0 in the file.
401         return new AbsoluteDate(new DateComponents(year, 1, 1), utc).
402                         shiftedBy(Constants.JULIAN_DAY * (day - 1)).
403                         shiftedBy(secInDay);
404 
405     }
406 
407 }