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 }