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.models.earth.troposphere;
18
19 import java.io.BufferedReader;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.nio.charset.StandardCharsets;
24 import java.text.ParseException;
25 import java.util.ArrayList;
26 import java.util.Locale;
27 import java.util.regex.Pattern;
28
29 import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
30 import org.hipparchus.util.FastMath;
31 import org.hipparchus.util.MathUtils;
32 import org.orekit.annotation.DefaultDataContext;
33 import org.orekit.data.AbstractSelfFeedingLoader;
34 import org.orekit.data.DataContext;
35 import org.orekit.data.DataLoader;
36 import org.orekit.data.DataProvidersManager;
37 import org.orekit.errors.OrekitException;
38 import org.orekit.errors.OrekitMessages;
39 import org.orekit.time.DateTimeComponents;
40
41 /** Loads Vienna tropospheric coefficients a given input stream.
42 * A stream contains, for a given day and a given hour, the hydrostatic and wet zenith delays
43 * and the ah and aw coefficients used for the computation of the mapping function.
44 * The coefficients are given with a time interval of 6 hours.
45 * <p>
46 * A bilinear interpolation is performed the case of the user initialize the latitude and the
47 * longitude with values that are not contained in the stream.
48 * </p>
49 * <p>
50 * The coefficients are obtained from <a href="http://vmf.geo.tuwien.ac.at/trop_products/GRID/">Vienna Mapping Functions Open Access Data</a>.
51 * Find more on the files at the <a href="http://vmf.geo.tuwien.ac.at/readme.txt">VMF Model Documentation</a>.
52 * <p>
53 * The files have to be extracted to UTF-8 text files before being read by this loader.
54 * <p>
55 * After extraction, it is assumed they are named VMFG_YYYYMMDD.Hhh for {@link ViennaOne} and VMF3_YYYYMMDD.Hhh {@link ViennaThree}.
56 * Where YYYY is the 4-digits year, MM the month, DD the day and hh the 2-digits hour.
57 *
58 * <p>
59 * The format is always the same, with and example shown below for VMF1 model.
60 * <p>
61 * Example:
62 * </p>
63 * <pre>
64 * ! Version: 1.0
65 * ! Source: J. Boehm, TU Vienna (created: 2018-11-20)
66 * ! Data_types: VMF1 (lat lon ah aw zhd zwd)
67 * ! Epoch: 2018 11 19 18 00 0.0
68 * ! Scale_factor: 1.e+00
69 * ! Range/resolution: -90 90 0 360 2 2.5
70 * ! Comment: http://vmf.geo.tuwien.ac.at/trop_products/GRID/2.5x2/VMF1/VMF1_OP/
71 * 90.0 0.0 0.00116059 0.00055318 2.3043 0.0096
72 * 90.0 2.5 0.00116059 0.00055318 2.3043 0.0096
73 * 90.0 5.0 0.00116059 0.00055318 2.3043 0.0096
74 * 90.0 7.5 0.00116059 0.00055318 2.3043 0.0096
75 * 90.0 10.0 0.00116059 0.00055318 2.3043 0.0096
76 * 90.0 12.5 0.00116059 0.00055318 2.3043 0.0096
77 * 90.0 15.0 0.00116059 0.00055318 2.3043 0.0096
78 * 90.0 17.5 0.00116059 0.00055318 2.3043 0.0096
79 * 90.0 20.0 0.00116059 0.00055318 2.3043 0.0096
80 * 90.0 22.5 0.00116059 0.00055318 2.3043 0.0096
81 * 90.0 25.0 0.00116059 0.00055318 2.3043 0.0096
82 * 90.0 27.5 0.00116059 0.00055318 2.3043 0.0096
83 * </pre>
84 *
85 * <p>It is not safe for multiple threads to share a single instance of this class.
86 *
87 * @author Bryan Cazabonne
88 */
89 public class ViennaModelCoefficientsLoader extends AbstractSelfFeedingLoader
90 implements DataLoader {
91
92 /** Default supported files name pattern. */
93 public static final String DEFAULT_SUPPORTED_NAMES = "VMF*_\\\\*\\*\\.*H$";
94
95 /** Pattern for delimiting regular expressions. */
96 private static final Pattern SEPARATOR = Pattern.compile("\\s+");
97
98 /** The hydrostatic and wet a coefficients loaded. */
99 private double[] coefficientsA;
100
101 /** The hydrostatic and wet zenith delays loaded. */
102 private double[] zenithDelay;
103
104 /** Geodetic site latitude, radians.*/
105 private final double latitude;
106
107 /** Geodetic site longitude, radians.*/
108 private final double longitude;
109
110 /** Vienna tropospheric model type.*/
111 private final ViennaModelType type;
112
113 /** Constructor with supported names given by user. This constructor uses the
114 * {@link DataContext#getDefault() default data context}.
115 *
116 * @param supportedNames Supported names
117 * @param latitude geodetic latitude of the station, in radians
118 * @param longitude geodetic latitude of the station, in radians
119 * @param type the type of Vienna tropospheric model (one or three)
120 * @see #ViennaModelCoefficientsLoader(String, double, double, ViennaModelType, DataProvidersManager)
121 */
122 @DefaultDataContext
123 public ViennaModelCoefficientsLoader(final String supportedNames, final double latitude,
124 final double longitude, final ViennaModelType type) {
125 this(supportedNames, latitude, longitude, type, DataContext.getDefault().getDataProvidersManager());
126 }
127
128 /**
129 * Constructor with supported names and source of mapping function files given by the
130 * user.
131 *
132 * @param supportedNames Supported names
133 * @param latitude geodetic latitude of the station, in radians
134 * @param longitude geodetic latitude of the station, in radians
135 * @param type the type of Vienna tropospheric model (one or three)
136 * @param dataProvidersManager provides access to auxiliary files.
137 * @since 10.1
138 */
139 public ViennaModelCoefficientsLoader(final String supportedNames,
140 final double latitude,
141 final double longitude,
142 final ViennaModelType type,
143 final DataProvidersManager dataProvidersManager) {
144 super(supportedNames, dataProvidersManager);
145 this.coefficientsA = null;
146 this.zenithDelay = null;
147 this.type = type;
148 this.latitude = latitude;
149
150 // Normalize longitude between 0 and 2π
151 this.longitude = MathUtils.normalizeAngle(longitude, FastMath.PI);
152 }
153
154 /** Constructor with default supported names. This constructor uses the
155 * {@link DataContext#getDefault() default data context}.
156 *
157 * @param latitude geodetic latitude of the station, in radians
158 * @param longitude geodetic latitude of the station, in radians
159 * @param type the type of Vienna tropospheric model (one or three)
160 * @see #ViennaModelCoefficientsLoader(String, double, double, ViennaModelType, DataProvidersManager)
161 */
162 @DefaultDataContext
163 public ViennaModelCoefficientsLoader(final double latitude, final double longitude,
164 final ViennaModelType type) {
165 this(DEFAULT_SUPPORTED_NAMES, latitude, longitude, type);
166 }
167
168 /** Returns the a coefficients array.
169 * <ul>
170 * <li>double[0] = a<sub>h</sub>
171 * <li>double[1] = a<sub>w</sub>
172 * </ul>
173 * @return the a coefficients array
174 */
175 public double[] getA() {
176 return coefficientsA.clone();
177 }
178
179 /** Returns the zenith delay array.
180 * <ul>
181 * <li>double[0] = D<sub>hz</sub> → zenith hydrostatic delay
182 * <li>double[1] = D<sub>wz</sub> → zenith wet delay
183 * </ul>
184 * @return the zenith delay array
185 */
186 public double[] getZenithDelay() {
187 return zenithDelay.clone();
188 }
189
190 @Override
191 public String getSupportedNames() {
192 return super.getSupportedNames();
193 }
194
195 /** Load the data using supported names .
196 */
197 public void loadViennaCoefficients() {
198 feed(this);
199
200 // Throw an exception if ah, ah, zh or zw were not loaded properly
201 if (coefficientsA == null || zenithDelay == null) {
202 throw new OrekitException(OrekitMessages.VIENNA_ACOEF_OR_ZENITH_DELAY_NOT_LOADED,
203 getSupportedNames());
204 }
205 }
206
207 /** Load the data for a given day.
208 * @param dateTimeComponents date and time component.
209 */
210 public void loadViennaCoefficients(final DateTimeComponents dateTimeComponents) {
211
212 // The files are named VMFG_YYYYMMDD.Hhh for Vienna-1 model and VMF3_YYYYMMDD.Hhh for Vienna-3 model.
213 // Where YYYY is the 4-digits year, MM the month, DD the day of the month and hh the 2-digits hour.
214 // Coefficients are only available for hh = 00 or 06 or 12 or 18.
215 final int year = dateTimeComponents.getDate().getYear();
216 final int month = dateTimeComponents.getDate().getMonth();
217 final int day = dateTimeComponents.getDate().getDay();
218 final int hour = dateTimeComponents.getTime().getHour();
219
220 // Correct month format is with 2-digits.
221 final String monthString;
222 if (month < 10) {
223 monthString = "0" + month;
224 } else {
225 monthString = String.valueOf(month);
226 }
227
228 // Correct day format is with 2-digits.
229 final String dayString;
230 if (day < 10) {
231 dayString = "0" + day;
232 } else {
233 dayString = String.valueOf(day);
234 }
235
236 // Correct hour format is with 2-digits.
237 final String hourString;
238 if (hour < 10) {
239 hourString = "0" + hour;
240 } else {
241 hourString = String.valueOf(hour);
242 }
243
244 // Name of the file is different between VMF1 and VMF3.
245 // For VMF1 it starts with "VMFG" whereas with VMF3 it starts with "VMF3"
246 switch (type) {
247 case VIENNA_ONE:
248 setSupportedNames(String.format(Locale.US, "VMFG_%04d%2s%2s.H%2s",
249 year, monthString, dayString, hourString));
250 break;
251 case VIENNA_THREE:
252 setSupportedNames(String.format(Locale.US, "VMF3_%04d%2s%2s.H%2s",
253 year, monthString, dayString, hourString));
254 break;
255 default:
256 break;
257 }
258
259 try {
260 this.loadViennaCoefficients();
261 } catch (OrekitException oe) {
262 throw new OrekitException(oe,
263 OrekitMessages.VIENNA_ACOEF_OR_ZENITH_DELAY_NOT_AVAILABLE_FOR_DATE,
264 dateTimeComponents.toString());
265 }
266 }
267
268 @Override
269 public boolean stillAcceptsData() {
270 return true;
271 }
272
273 @Override
274 public void loadData(final InputStream input, final String name)
275 throws IOException, ParseException {
276
277 int lineNumber = 0;
278 String line = null;
279
280 // Initialize Lists
281 final ArrayList<Double> latitudes = new ArrayList<>();
282 final ArrayList<Double> longitudes = new ArrayList<>();
283 final ArrayList<Double> ah = new ArrayList<>();
284 final ArrayList<Double> aw = new ArrayList<>();
285 final ArrayList<Double> zhd = new ArrayList<>();
286 final ArrayList<Double> zwd = new ArrayList<>();
287
288 // Open stream and parse data
289 try (BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
290
291 for (line = br.readLine(); line != null; line = br.readLine()) {
292 ++lineNumber;
293 line = line.trim();
294
295 // Fill latitudes and longitudes lists
296 if (line.startsWith("! Range/resolution:")) {
297 final String[] range_line = SEPARATOR.split(line);
298
299 // Latitudes list
300 for (double lat = Double.parseDouble(range_line[2]); lat <= Double.parseDouble(range_line[3]); lat = lat + Double.parseDouble(range_line[6])) {
301 latitudes.add(FastMath.toRadians(lat));
302 }
303
304 // Longitude list
305 for (double lon = Double.parseDouble(range_line[4]); lon <= Double.parseDouble(range_line[5]); lon = lon + Double.parseDouble(range_line[7])) {
306 longitudes.add(FastMath.toRadians(lon));
307 // For VFM1 files, header specify that longitudes end at 360°
308 // In reality they end at 357.5°. That is why we stop the loop when the longitude
309 // reaches 357.5°.
310 if (type == ViennaModelType.VIENNA_ONE && lon >= 357.5) {
311 break;
312 }
313 }
314 }
315
316 // Fill ah, aw, zhd and zwd lists
317 if (!line.startsWith("!")) {
318 final String[] values_line = SEPARATOR.split(line);
319 ah.add(Double.parseDouble(values_line[2]));
320 aw.add(Double.parseDouble(values_line[3]));
321 zhd.add(Double.parseDouble(values_line[4]));
322 zwd.add(Double.parseDouble(values_line[5]));
323 }
324 }
325
326 } catch (NumberFormatException nfe) {
327 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
328 lineNumber, name, line);
329 }
330
331 // Check that ah, aw, zh and zw were found (only one check is enough)
332 if (ah.isEmpty()) {
333 throw new OrekitException(OrekitMessages.NO_VIENNA_ACOEF_OR_ZENITH_DELAY_IN_FILE, name);
334 }
335
336 final int dimLat = latitudes.size();
337 final int dimLon = longitudes.size();
338
339 // Change Lists to Arrays
340 final double[] xVal = new double[dimLat];
341 for (int i = 0; i < dimLat; i++) {
342 xVal[i] = latitudes.get(i);
343 }
344
345 final double[] yVal = new double[dimLon];
346 for (int j = 0; j < dimLon; j++) {
347 yVal[j] = longitudes.get(j);
348 }
349
350 final double[][] fvalAH = new double[dimLat][dimLon];
351 final double[][] fvalAW = new double[dimLat][dimLon];
352 final double[][] fvalZH = new double[dimLat][dimLon];
353 final double[][] fvalZW = new double[dimLat][dimLon];
354
355 int index = dimLon * dimLat;
356 for (int x = 0; x < dimLat; x++) {
357 for (int y = dimLon - 1; y >= 0; y--) {
358 index = index - 1;
359 fvalAH[x][y] = ah.get(index);
360 fvalAW[x][y] = aw.get(index);
361 fvalZH[x][y] = zhd.get(index);
362 fvalZW[x][y] = zwd.get(index);
363 }
364 }
365
366 // Build Bilinear Interpolation Functions
367 final BilinearInterpolatingFunction functionAH = new BilinearInterpolatingFunction(xVal, yVal, fvalAH);
368 final BilinearInterpolatingFunction functionAW = new BilinearInterpolatingFunction(xVal, yVal, fvalAW);
369 final BilinearInterpolatingFunction functionZH = new BilinearInterpolatingFunction(xVal, yVal, fvalZH);
370 final BilinearInterpolatingFunction functionZW = new BilinearInterpolatingFunction(xVal, yVal, fvalZW);
371
372 coefficientsA = new double[2];
373 zenithDelay = new double[2];
374
375 // Get the values for the given latitude and longitude
376 coefficientsA[0] = functionAH.value(latitude, longitude);
377 coefficientsA[1] = functionAW.value(latitude, longitude);
378 zenithDelay[0] = functionZH.value(latitude, longitude);
379 zenithDelay[1] = functionZW.value(latitude, longitude);
380
381 }
382
383 }