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.weather;
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.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.SortedSet;
30  import java.util.TreeSet;
31  import java.util.regex.Pattern;
32  
33  import org.hipparchus.util.FastMath;
34  import org.orekit.data.DataLoader;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.errors.OrekitMessages;
37  
38  /** Base parser for Global Pressure and Temperature 2, 2w and 3 models.
39   * <p>
40   * The format for all models is always the same, with an example shown below
41   * for the pressure and the temperature. The "GPT2w" model (w stands for wet)
42   * also provides humidity parameters and the "GPT3" model also provides horizontal
43   * gradient, so the number of columns vary depending on the model.
44   * <p>
45   * Example:
46   * </p>
47   * <pre>
48   * %  lat    lon   p:a0    A1   B1   A2   B2  T:a0    A1   B1   A2   B2
49   *   87.5    2.5 101421    21  409 -217 -122 259.2 -13.2 -6.1  2.6  0.3
50   *   87.5    7.5 101416    21  411 -213 -120 259.3 -13.1 -6.1  2.6  0.3
51   *   87.5   12.5 101411    22  413 -209 -118 259.3 -13.1 -6.1  2.6  0.3
52   *   87.5   17.5 101407    23  415 -205 -116 259.4 -13.0 -6.1  2.6  0.3
53   *   ...
54   * </pre>
55   *
56   * @see "K. Lagler, M. Schindelegger, J. Böhm, H. Krasna, T. Nilsson (2013),
57   * GPT2: empirical slant delay model for radio space geodetic techniques. Geophys
58   * Res Lett 40(6):1069–1073. doi:10.1002/grl.50288"
59   *
60   * @author Bryan Cazabonne
61   * @author Luc Maisonobe
62   * @since 12.1
63   */
64  class GptNParser implements DataLoader {
65  
66      /** Comment prefix. */
67      private static final String COMMENT = "%";
68  
69      /** Pattern for delimiting regular expressions. */
70      private static final Pattern SEPARATOR = Pattern.compile("\\s+");
71  
72      /** Label for latitude field. */
73      private static final String LATITUDE_LABEL = "lat";
74  
75      /** Label for longitude field. */
76      private static final String LONGITUDE_LABEL = "lon";
77  
78      /** Label for undulation field. */
79      private static final String UNDULATION_LABEL = "undu";
80  
81      /** Label for height correction field. */
82      private static final String HEIGHT_CORRECTION_LABEL = "Hs";
83  
84      /** Label for annual cosine amplitude field. */
85      private static final String A1 = "A1";
86  
87      /** Label for annual sine amplitude field. */
88      private static final String B1 = "B1";
89  
90      /** Label for semi-annual cosine amplitude field. */
91      private static final String A2 = "A2";
92  
93      /** Label for semi-annual sine amplitude field. */
94      private static final String B2 = "B2";
95  
96      /** Expected seasonal models types. */
97      private final SeasonalModelType[] expected;
98  
99      /** Index for latitude field. */
100     private int latitudeIndex;
101 
102     /** Index for longitude field. */
103     private int longitudeIndex;
104 
105     /** Index for undulation field. */
106     private int undulationIndex;
107 
108     /** Index for height correction field. */
109     private int heightCorrectionIndex;
110 
111     /** Maximum index. */
112     private int maxIndex;
113 
114     /** Indices for expected seasonal models types field. */
115     private final int[] expectedIndices;
116 
117     /** Grid entries. */
118     private Grid grid;
119 
120     /** Simple constructor.
121      * @param expected expected seasonal models types
122      */
123     GptNParser(final SeasonalModelType... expected) {
124         this.expected        = expected.clone();
125         this.expectedIndices = new int[expected.length];
126     }
127 
128     @Override
129     public boolean stillAcceptsData() {
130         return grid == null;
131     }
132 
133     @Override
134     public void loadData(final InputStream input, final String name) throws IOException {
135 
136         final SortedSet<Integer> latSample = new TreeSet<>();
137         final SortedSet<Integer> lonSample = new TreeSet<>();
138         final List<GridEntry>    entries   = new ArrayList<>();
139 
140         // Open stream and parse data
141         try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
142              BufferedReader    br  = new BufferedReader(isr)) {
143             int     lineNumber = 0;
144             String  line;
145             for (line = br.readLine(); line != null; line = br.readLine()) {
146                 ++lineNumber;
147                 line = line.trim();
148                 if (lineNumber == 1) {
149                     // read header and store columns numbers
150                     parseHeader(line, lineNumber, name);
151                 } else if (!line.isEmpty()) {
152                     // read grid data
153                     final GridEntry entry = parseEntry(line, lineNumber, name);
154                     latSample.add(entry.getLatKey());
155                     lonSample.add(entry.getLonKey());
156                     entries.add(entry);
157                 }
158 
159             }
160         }
161 
162         // organize entries in a grid that wraps around Earth in longitude
163         grid = new Grid(latSample, lonSample, entries, name);
164 
165     }
166 
167     /** Parse header line in the grid file.
168      * @param line grid line
169      * @param lineNumber line number
170      * @param name file name
171      */
172     private void parseHeader(final String line, final int lineNumber, final String name) {
173 
174         // reset indices
175         latitudeIndex         = -1;
176         longitudeIndex        = -1;
177         undulationIndex       = -1;
178         heightCorrectionIndex = -1;
179         maxIndex              = -1;
180         Arrays.fill(expectedIndices, -1);
181 
182         final String[] fields = SEPARATOR.split(line.substring(COMMENT.length()).trim());
183         String lookingFor = LATITUDE_LABEL;
184         for (int i = 0; i < fields.length; ++i) {
185             maxIndex = FastMath.max(maxIndex, i);
186             checkLabel(fields[i], lookingFor, line, lineNumber, name);
187             switch (fields[i]) {
188                 case LATITUDE_LABEL :
189                     latitudeIndex = i;
190                     lookingFor = LONGITUDE_LABEL;
191                     break;
192                 case LONGITUDE_LABEL :
193                     lookingFor = null;
194                     longitudeIndex = i;
195                     break;
196                 case UNDULATION_LABEL :
197                     lookingFor = HEIGHT_CORRECTION_LABEL;
198                     undulationIndex = i;
199                     break;
200                 case HEIGHT_CORRECTION_LABEL :
201                     lookingFor = null;
202                     heightCorrectionIndex = i;
203                     break;
204                 case A1 :
205                     lookingFor = B1;
206                     break;
207                 case B1 :
208                     lookingFor = A2;
209                     break;
210                 case A2 :
211                     lookingFor = B2;
212                     break;
213                 case B2 :
214                     lookingFor = null;
215                     break;
216                 default : {
217                     final SeasonalModelType type = SeasonalModelType.parseType(fields[i]);
218                     for (int j = 0; j < expected.length; ++j) {
219                         if (type == expected[j]) {
220                             expectedIndices[j] = i;
221                             lookingFor = A1;
222                             break;
223                         }
224                     }
225                 }
226             }
227         }
228 
229         // check all indices have been set
230         int minIndex = FastMath.min(latitudeIndex,
231                                     FastMath.min(longitudeIndex,
232                                                  FastMath.min(undulationIndex,
233                                                               heightCorrectionIndex)));
234         for (int index : expectedIndices) {
235             minIndex = FastMath.min(minIndex, index);
236         }
237         if (minIndex < 0) {
238             // some indices in the header are missing
239             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
240                                       lineNumber, name, line);
241         }
242 
243     }
244 
245     /** Check if header label is what we are looking for.
246      * @param label label to check
247      * @param lookingFor label we are looking for, or null if we don't known what to expect
248      * @param line grid line
249      * @param lineNumber line number
250      * @param name file name
251      */
252     private void checkLabel(final String label, final String lookingFor,
253                             final String line, final int lineNumber, final String name) {
254         if (lookingFor != null && !lookingFor.equals(label)) {
255             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
256                                       lineNumber, name, line);
257         }
258     }
259 
260     /** Parse one entry in the grid file.
261      * @param line grid line
262      * @param lineNumber line number
263      * @param name file name
264      * @return parsed entry
265      */
266     private GridEntry parseEntry(final String line, final int lineNumber, final String name) {
267         try {
268 
269             final String[] fields = SEPARATOR.split(line);
270             if (fields.length != maxIndex + 1) {
271                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
272                                           lineNumber, name, line);
273             }
274 
275             final double latDegree = Double.parseDouble(fields[latitudeIndex]);
276             final double lonDegree = Double.parseDouble(fields[longitudeIndex]);
277 
278             final Map<SeasonalModelType, SeasonalModel> models = new HashMap<>(expected.length);
279             for (int i = 0; i < expected.length; ++i) {
280                 final int first = expectedIndices[i];
281                 models.put(expected[i], new SeasonalModel(Double.parseDouble(fields[first    ]),
282                                                           Double.parseDouble(fields[first + 1]),
283                                                           Double.parseDouble(fields[first + 2]),
284                                                           Double.parseDouble(fields[first + 3]),
285                                                           Double.parseDouble(fields[first + 4])));
286             }
287 
288             return new GridEntry(FastMath.toRadians(latDegree),
289                                  (int) FastMath.rint(latDegree * GridEntry.DEG_TO_MAS),
290                                  FastMath.toRadians(lonDegree),
291                                  (int) FastMath.rint(lonDegree * GridEntry.DEG_TO_MAS),
292                                  Double.parseDouble(fields[undulationIndex]),
293                                  Double.parseDouble(fields[heightCorrectionIndex]),
294                                  models);
295 
296         } catch (NumberFormatException nfe) {
297             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
298                                       lineNumber, name, line);
299         }
300     }
301 
302     /** Get the parsed grid.
303      * @return parsed grid
304      */
305     public Grid getGrid() {
306         return grid;
307     }
308 
309 }