GptNParser.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.models.earth.weather;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.hipparchus.util.FastMath;
import org.orekit.data.DataLoader;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @Override
    public boolean stillAcceptsData() {
        return grid == null;
    }

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

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

        // Open stream and parse data
        try (InputStreamReader isr = new InputStreamReader(input, StandardCharsets.UTF_8);
             BufferedReader    br  = new BufferedReader(isr)) {
            int     lineNumber = 0;
            String  line;
            for (line = br.readLine(); line != null; line = br.readLine()) {
                ++lineNumber;
                line = line.trim();
                if (lineNumber == 1) {
                    // read header and store columns numbers
                    parseHeader(line, lineNumber, name);
                } else if (!line.isEmpty()) {
                    // read grid data
                    final GridEntry entry = parseEntry(line, lineNumber, name);
                    latSample.add(entry.getLatKey());
                    lonSample.add(entry.getLonKey());
                    entries.add(entry);
                }

            }
        }

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

    }

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

        // reset indices
        latitudeIndex         = -1;
        longitudeIndex        = -1;
        undulationIndex       = -1;
        heightCorrectionIndex = -1;
        maxIndex              = -1;
        Arrays.fill(expectedIndices, -1);

        final String[] fields = SEPARATOR.split(line.substring(COMMENT.length()).trim());
        String lookingFor = LATITUDE_LABEL;
        for (int i = 0; i < fields.length; ++i) {
            maxIndex = FastMath.max(maxIndex, i);
            checkLabel(fields[i], lookingFor, line, lineNumber, name);
            switch (fields[i]) {
                case LATITUDE_LABEL :
                    latitudeIndex = i;
                    lookingFor = LONGITUDE_LABEL;
                    break;
                case LONGITUDE_LABEL :
                    lookingFor = null;
                    longitudeIndex = i;
                    break;
                case UNDULATION_LABEL :
                    lookingFor = HEIGHT_CORRECTION_LABEL;
                    undulationIndex = i;
                    break;
                case HEIGHT_CORRECTION_LABEL :
                    lookingFor = null;
                    heightCorrectionIndex = i;
                    break;
                case A1 :
                    lookingFor = B1;
                    break;
                case B1 :
                    lookingFor = A2;
                    break;
                case A2 :
                    lookingFor = B2;
                    break;
                case B2 :
                    lookingFor = null;
                    break;
                default : {
                    final SeasonalModelType type = SeasonalModelType.parseType(fields[i]);
                    for (int j = 0; j < expected.length; ++j) {
                        if (type == expected[j]) {
                            expectedIndices[j] = i;
                            lookingFor = A1;
                            break;
                        }
                    }
                }
            }
        }

        // check all indices have been set
        int minIndex = FastMath.min(latitudeIndex,
                                    FastMath.min(longitudeIndex,
                                                 FastMath.min(undulationIndex,
                                                              heightCorrectionIndex)));
        for (int index : expectedIndices) {
            minIndex = FastMath.min(minIndex, index);
        }
        if (minIndex < 0) {
            // some indices in the header are missing
            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                      lineNumber, name, line);
        }

    }

    /** Check if header label is what we are looking for.
     * @param label label to check
     * @param lookingFor label we are looking for, or null if we don't known what to expect
     * @param line grid line
     * @param lineNumber line number
     * @param name file name
     */
    private void checkLabel(final String label, final String lookingFor,
                            final String line, final int lineNumber, final String name) {
        if (lookingFor != null && !lookingFor.equals(label)) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                      lineNumber, name, line);
        }
    }

    /** Parse one entry in the grid file.
     * @param line grid line
     * @param lineNumber line number
     * @param name file name
     * @return parsed entry
     */
    private GridEntry parseEntry(final String line, final int lineNumber, final String name) {
        try {

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

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

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

            return new GridEntry(FastMath.toRadians(latDegree),
                                 (int) FastMath.rint(latDegree * GridEntry.DEG_TO_MAS),
                                 FastMath.toRadians(lonDegree),
                                 (int) FastMath.rint(lonDegree * GridEntry.DEG_TO_MAS),
                                 Double.parseDouble(fields[undulationIndex]),
                                 Double.parseDouble(fields[heightCorrectionIndex]),
                                 models);

        } catch (NumberFormatException nfe) {
            throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
                                      lineNumber, name, line);
        }
    }

    /** Get the parsed grid.
     * @return parsed grid
     */
    public Grid getGrid() {
        return grid;
    }

}