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 }