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 }