GptNParser.java

  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. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.HashMap;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.SortedSet;
  29. import java.util.TreeSet;
  30. import java.util.regex.Pattern;

  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. /** Base parser for Global Pressure and Temperature 2, 2w and 3 models.
  36.  * <p>
  37.  * The format for all models is always the same, with an example shown below
  38.  * for the pressure and the temperature. The "GPT2w" model (w stands for wet)
  39.  * also provides humidity parameters and the "GPT3" model also provides horizontal
  40.  * gradient, so the number of columns vary depending on the model.
  41.  * <p>
  42.  * Example:
  43.  * </p>
  44.  * <pre>
  45.  * %  lat    lon   p:a0    A1   B1   A2   B2  T:a0    A1   B1   A2   B2
  46.  *   87.5    2.5 101421    21  409 -217 -122 259.2 -13.2 -6.1  2.6  0.3
  47.  *   87.5    7.5 101416    21  411 -213 -120 259.3 -13.1 -6.1  2.6  0.3
  48.  *   87.5   12.5 101411    22  413 -209 -118 259.3 -13.1 -6.1  2.6  0.3
  49.  *   87.5   17.5 101407    23  415 -205 -116 259.4 -13.0 -6.1  2.6  0.3
  50.  *   ...
  51.  * </pre>
  52.  *
  53.  * @see "K. Lagler, M. Schindelegger, J. Böhm, H. Krasna, T. Nilsson (2013),
  54.  * GPT2: empirical slant delay model for radio space geodetic techniques. Geophys
  55.  * Res Lett 40(6):1069–1073. doi:10.1002/grl.50288"
  56.  *
  57.  * @author Bryan Cazabonne
  58.  * @author Luc Maisonobe
  59.  * @since 12.1
  60.  */
  61. class GptNParser implements DataLoader {

  62.     /** Comment prefix. */
  63.     private static final String COMMENT = "%";

  64.     /** Pattern for delimiting regular expressions. */
  65.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  66.     /** Label for latitude field. */
  67.     private static final String LATITUDE_LABEL = "lat";

  68.     /** Label for longitude field. */
  69.     private static final String LONGITUDE_LABEL = "lon";

  70.     /** Label for undulation field. */
  71.     private static final String UNDULATION_LABEL = "undu";

  72.     /** Label for height correction field. */
  73.     private static final String HEIGHT_CORRECTION_LABEL = "Hs";

  74.     /** Label for annual cosine amplitude field. */
  75.     private static final String A1 = "A1";

  76.     /** Label for annual sine amplitude field. */
  77.     private static final String B1 = "B1";

  78.     /** Label for semi-annual cosine amplitude field. */
  79.     private static final String A2 = "A2";

  80.     /** Label for semi-annual sine amplitude field. */
  81.     private static final String B2 = "B2";

  82.     /** Expected seasonal models types. */
  83.     private final SeasonalModelType[] expected;

  84.     /** Index for latitude field. */
  85.     private int latitudeIndex;

  86.     /** Index for longitude field. */
  87.     private int longitudeIndex;

  88.     /** Index for undulation field. */
  89.     private int undulationIndex;

  90.     /** Index for height correction field. */
  91.     private int heightCorrectionIndex;

  92.     /** Maximum index. */
  93.     private int maxIndex;

  94.     /** Indices for expected seasonal models types field. */
  95.     private final int[] expectedIndices;

  96.     /** Grid entries. */
  97.     private Grid grid;

  98.     /** Simple constructor.
  99.      * @param expected expected seasonal models types
  100.      */
  101.     GptNParser(final SeasonalModelType... expected) {
  102.         this.expected        = expected.clone();
  103.         this.expectedIndices = new int[expected.length];
  104.     }

  105.     @Override
  106.     public boolean stillAcceptsData() {
  107.         return grid == null;
  108.     }

  109.     @Override
  110.     public void loadData(final InputStream input, final String name) throws IOException {

  111.         final SortedSet<Integer> latSample = new TreeSet<>();
  112.         final SortedSet<Integer> lonSample = new TreeSet<>();
  113.         final List<GridEntry>    entries   = new ArrayList<>();

  114.         // Open stream and parse data
  115.         try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
  116.              BufferedReader    br  = new BufferedReader(isr)) {
  117.             int     lineNumber = 0;
  118.             String  line;
  119.             for (line = br.readLine(); line != null; line = br.readLine()) {
  120.                 ++lineNumber;
  121.                 line = line.trim();
  122.                 if (lineNumber == 1) {
  123.                     // read header and store columns numbers
  124.                     parseHeader(line, lineNumber, name);
  125.                 } else if (!line.isEmpty()) {
  126.                     // read grid data
  127.                     final GridEntry entry = parseEntry(line, lineNumber, name);
  128.                     latSample.add(entry.getLatKey());
  129.                     lonSample.add(entry.getLonKey());
  130.                     entries.add(entry);
  131.                 }

  132.             }
  133.         }

  134.         // organize entries in a grid that wraps around Earth in longitude
  135.         grid = new Grid(latSample, lonSample, entries, name);

  136.     }

  137.     /** Parse header line in the grid file.
  138.      * @param line grid line
  139.      * @param lineNumber line number
  140.      * @param name file name
  141.      */
  142.     private void parseHeader(final String line, final int lineNumber, final String name) {

  143.         // reset indices
  144.         latitudeIndex         = -1;
  145.         longitudeIndex        = -1;
  146.         undulationIndex       = -1;
  147.         heightCorrectionIndex = -1;
  148.         maxIndex              = -1;
  149.         Arrays.fill(expectedIndices, -1);

  150.         final String[] fields = SEPARATOR.split(line.substring(COMMENT.length()).trim());
  151.         String lookingFor = LATITUDE_LABEL;
  152.         for (int i = 0; i < fields.length; ++i) {
  153.             maxIndex = FastMath.max(maxIndex, i);
  154.             checkLabel(fields[i], lookingFor, line, lineNumber, name);
  155.             switch (fields[i]) {
  156.                 case LATITUDE_LABEL :
  157.                     latitudeIndex = i;
  158.                     lookingFor = LONGITUDE_LABEL;
  159.                     break;
  160.                 case LONGITUDE_LABEL :
  161.                     lookingFor = null;
  162.                     longitudeIndex = i;
  163.                     break;
  164.                 case UNDULATION_LABEL :
  165.                     lookingFor = HEIGHT_CORRECTION_LABEL;
  166.                     undulationIndex = i;
  167.                     break;
  168.                 case HEIGHT_CORRECTION_LABEL :
  169.                     lookingFor = null;
  170.                     heightCorrectionIndex = i;
  171.                     break;
  172.                 case A1 :
  173.                     lookingFor = B1;
  174.                     break;
  175.                 case B1 :
  176.                     lookingFor = A2;
  177.                     break;
  178.                 case A2 :
  179.                     lookingFor = B2;
  180.                     break;
  181.                 case B2 :
  182.                     lookingFor = null;
  183.                     break;
  184.                 default : {
  185.                     final SeasonalModelType type = SeasonalModelType.parseType(fields[i]);
  186.                     for (int j = 0; j < expected.length; ++j) {
  187.                         if (type == expected[j]) {
  188.                             expectedIndices[j] = i;
  189.                             lookingFor = A1;
  190.                             break;
  191.                         }
  192.                     }
  193.                 }
  194.             }
  195.         }

  196.         // check all indices have been set
  197.         int minIndex = FastMath.min(latitudeIndex,
  198.                                     FastMath.min(longitudeIndex,
  199.                                                  FastMath.min(undulationIndex,
  200.                                                               heightCorrectionIndex)));
  201.         for (int index : expectedIndices) {
  202.             minIndex = FastMath.min(minIndex, index);
  203.         }
  204.         if (minIndex < 0) {
  205.             // some indices in the header are missing
  206.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  207.                                       lineNumber, name, line);
  208.         }

  209.     }

  210.     /** Check if header label is what we are looking for.
  211.      * @param label label to check
  212.      * @param lookingFor label we are looking for, or null if we don't known what to expect
  213.      * @param line grid line
  214.      * @param lineNumber line number
  215.      * @param name file name
  216.      */
  217.     private void checkLabel(final String label, final String lookingFor,
  218.                             final String line, final int lineNumber, final String name) {
  219.         if (lookingFor != null && !lookingFor.equals(label)) {
  220.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  221.                                       lineNumber, name, line);
  222.         }
  223.     }

  224.     /** Parse one entry in the grid file.
  225.      * @param line grid line
  226.      * @param lineNumber line number
  227.      * @param name file name
  228.      * @return parsed entry
  229.      */
  230.     private GridEntry parseEntry(final String line, final int lineNumber, final String name) {
  231.         try {

  232.             final String[] fields = SEPARATOR.split(line);
  233.             if (fields.length != maxIndex + 1) {
  234.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  235.                                           lineNumber, name, line);
  236.             }

  237.             final double latDegree = Double.parseDouble(fields[latitudeIndex]);
  238.             final double lonDegree = Double.parseDouble(fields[longitudeIndex]);

  239.             final Map<SeasonalModelType, SeasonalModel> models = new HashMap<>(expected.length);
  240.             for (int i = 0; i < expected.length; ++i) {
  241.                 final int first = expectedIndices[i];
  242.                 models.put(expected[i], new SeasonalModel(Double.parseDouble(fields[first    ]),
  243.                                                           Double.parseDouble(fields[first + 1]),
  244.                                                           Double.parseDouble(fields[first + 2]),
  245.                                                           Double.parseDouble(fields[first + 3]),
  246.                                                           Double.parseDouble(fields[first + 4])));
  247.             }

  248.             return new GridEntry(FastMath.toRadians(latDegree),
  249.                                  (int) FastMath.rint(latDegree * GridEntry.DEG_TO_MAS),
  250.                                  FastMath.toRadians(lonDegree),
  251.                                  (int) FastMath.rint(lonDegree * GridEntry.DEG_TO_MAS),
  252.                                  Double.parseDouble(fields[undulationIndex]),
  253.                                  Double.parseDouble(fields[heightCorrectionIndex]),
  254.                                  models);

  255.         } catch (NumberFormatException nfe) {
  256.             throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  257.                                       lineNumber, name, line);
  258.         }
  259.     }

  260.     /** Get the parsed grid.
  261.      * @return parsed grid
  262.      */
  263.     public Grid getGrid() {
  264.         return grid;
  265.     }

  266. }