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