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