ViennaModelCoefficientsLoader.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.troposphere;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.regex.Pattern;
import org.hipparchus.analysis.interpolation.BilinearInterpolatingFunction;
import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.AbstractSelfFeedingLoader;
import org.orekit.data.DataContext;
import org.orekit.data.DataLoader;
import org.orekit.data.DataProvidersManager;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
import org.orekit.time.DateTimeComponents;
/** Loads Vienna tropospheric coefficients a given input stream.
* A stream contains, for a given day and a given hour, the hydrostatic and wet zenith delays
* and the ah and aw coefficients used for the computation of the mapping function.
* The coefficients are given with a time interval of 6 hours.
* <p>
* A bilinear interpolation is performed the case of the user initialize the latitude and the
* longitude with values that are not contained in the stream.
* </p>
* <p>
* The coefficients are obtained from <a href="http://vmf.geo.tuwien.ac.at/trop_products/GRID/">Vienna Mapping Functions Open Access Data</a>.
* Find more on the files at the <a href="http://vmf.geo.tuwien.ac.at/readme.txt">VMF Model Documentation</a>.
* <p>
* The files have to be extracted to UTF-8 text files before being read by this loader.
* <p>
* After extraction, it is assumed they are named VMFG_YYYYMMDD.Hhh for {@link ViennaOneModel} and VMF3_YYYYMMDD.Hhh {@link ViennaThreeModel}.
* Where YYYY is the 4-digits year, MM the month, DD the day and hh the 2-digits hour.
*
* <p>
* The format is always the same, with and example shown below for VMF1 model.
* <p>
* Example:
* </p>
* <pre>
* ! Version: 1.0
* ! Source: J. Boehm, TU Vienna (created: 2018-11-20)
* ! Data_types: VMF1 (lat lon ah aw zhd zwd)
* ! Epoch: 2018 11 19 18 00 0.0
* ! Scale_factor: 1.e+00
* ! Range/resolution: -90 90 0 360 2 2.5
* ! Comment: http://vmf.geo.tuwien.ac.at/trop_products/GRID/2.5x2/VMF1/VMF1_OP/
* 90.0 0.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 2.5 0.00116059 0.00055318 2.3043 0.0096
* 90.0 5.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 7.5 0.00116059 0.00055318 2.3043 0.0096
* 90.0 10.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 12.5 0.00116059 0.00055318 2.3043 0.0096
* 90.0 15.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 17.5 0.00116059 0.00055318 2.3043 0.0096
* 90.0 20.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 22.5 0.00116059 0.00055318 2.3043 0.0096
* 90.0 25.0 0.00116059 0.00055318 2.3043 0.0096
* 90.0 27.5 0.00116059 0.00055318 2.3043 0.0096
* </pre>
*
* <p>It is not safe for multiple threads to share a single instance of this class.
*
* @author Bryan Cazabonne
*/
public class ViennaModelCoefficientsLoader extends AbstractSelfFeedingLoader
implements DataLoader {
/** Default supported files name pattern. */
public static final String DEFAULT_SUPPORTED_NAMES = "VMF*_\\\\*\\*\\.*H$";
/** Pattern for delimiting regular expressions. */
private static final Pattern SEPARATOR = Pattern.compile("\\s+");
/** The hydrostatic and wet a coefficients loaded. */
private double[] coefficientsA;
/** The hydrostatic and wet zenith delays loaded. */
private double[] zenithDelay;
/** Geodetic site latitude, radians.*/
private double latitude;
/** Geodetic site longitude, radians.*/
private double longitude;
/** Vienna tropospheric model type.*/
private ViennaModelType type;
/** Constructor with supported names given by user. This constructor uses the
* {@link DataContext#getDefault() default data context}.
*
* @param supportedNames Supported names
* @param latitude geodetic latitude of the station, in radians
* @param longitude geodetic latitude of the station, in radians
* @param type the type of Vienna tropospheric model (one or three)
* @see #ViennaModelCoefficientsLoader(String, double, double, ViennaModelType, DataProvidersManager)
*/
@DefaultDataContext
public ViennaModelCoefficientsLoader(final String supportedNames, final double latitude,
final double longitude, final ViennaModelType type) {
this(supportedNames, latitude, longitude, type, DataContext.getDefault().getDataProvidersManager());
}
/**
* Constructor with supported names and source of mapping function files given by the
* user.
*
* @param supportedNames Supported names
* @param latitude geodetic latitude of the station, in radians
* @param longitude geodetic latitude of the station, in radians
* @param type the type of Vienna tropospheric model (one or three)
* @param dataProvidersManager provides access to auxiliary files.
* @since 10.1
*/
public ViennaModelCoefficientsLoader(final String supportedNames,
final double latitude,
final double longitude,
final ViennaModelType type,
final DataProvidersManager dataProvidersManager) {
super(supportedNames, dataProvidersManager);
this.coefficientsA = null;
this.zenithDelay = null;
this.type = type;
this.latitude = latitude;
// Normalize longitude between 0 and 2π
this.longitude = MathUtils.normalizeAngle(longitude, FastMath.PI);
}
/** Constructor with default supported names. This constructor uses the
* {@link DataContext#getDefault() default data context}.
*
* @param latitude geodetic latitude of the station, in radians
* @param longitude geodetic latitude of the station, in radians
* @param type the type of Vienna tropospheric model (one or three)
* @see #ViennaModelCoefficientsLoader(String, double, double, ViennaModelType, DataProvidersManager)
*/
@DefaultDataContext
public ViennaModelCoefficientsLoader(final double latitude, final double longitude,
final ViennaModelType type) {
this(DEFAULT_SUPPORTED_NAMES, latitude, longitude, type);
}
/** Returns the a coefficients array.
* <ul>
* <li>double[0] = a<sub>h</sub>
* <li>double[1] = a<sub>w</sub>
* </ul>
* @return the a coefficients array
*/
public double[] getA() {
return coefficientsA.clone();
}
/** Returns the zenith delay array.
* <ul>
* <li>double[0] = D<sub>hz</sub> → zenith hydrostatic delay
* <li>double[1] = D<sub>wz</sub> → zenith wet delay
* </ul>
* @return the zenith delay array
*/
public double[] getZenithDelay() {
return zenithDelay.clone();
}
@Override
public String getSupportedNames() {
return super.getSupportedNames();
}
/** Load the data using supported names .
*/
public void loadViennaCoefficients() {
feed(this);
// Throw an exception if ah, ah, zh or zw were not loaded properly
if (coefficientsA == null || zenithDelay == null) {
throw new OrekitException(OrekitMessages.VIENNA_ACOEF_OR_ZENITH_DELAY_NOT_LOADED,
getSupportedNames());
}
}
/** Load the data for a given day.
* @param dateTimeComponents date and time component.
*/
public void loadViennaCoefficients(final DateTimeComponents dateTimeComponents) {
// The files are named VMFG_YYYYMMDD.Hhh for Vienna-1 model and VMF3_YYYYMMDD.Hhh for Vienna-3 model.
// Where YYYY is the 4-digits year, MM the month, DD the day of the month and hh the 2-digits hour.
// Coefficients are only available for hh = 00 or 06 or 12 or 18.
final int year = dateTimeComponents.getDate().getYear();
final int month = dateTimeComponents.getDate().getMonth();
final int day = dateTimeComponents.getDate().getDay();
final int hour = dateTimeComponents.getTime().getHour();
// Correct month format is with 2-digits.
final String monthString;
if (month < 10) {
monthString = "0" + month;
} else {
monthString = String.valueOf(month);
}
// Correct day format is with 2-digits.
final String dayString;
if (day < 10) {
dayString = "0" + day;
} else {
dayString = String.valueOf(day);
}
// Correct hour format is with 2-digits.
final String hourString;
if (hour < 10) {
hourString = "0" + hour;
} else {
hourString = String.valueOf(hour);
}
// Name of the file is different between VMF1 and VMF3.
// For VMF1 it starts with "VMFG" whereas with VMF3 it starts with "VMF3"
switch (type) {
case VIENNA_ONE:
setSupportedNames(String.format("VMFG_%04d%2s%2s.H%2s", year, monthString, dayString, hourString));
break;
case VIENNA_THREE:
setSupportedNames(String.format("VMF3_%04d%2s%2s.H%2s", year, monthString, dayString, hourString));
break;
default:
break;
}
try {
this.loadViennaCoefficients();
} catch (OrekitException oe) {
throw new OrekitException(oe,
OrekitMessages.VIENNA_ACOEF_OR_ZENITH_DELAY_NOT_AVAILABLE_FOR_DATE,
dateTimeComponents.toString());
}
}
@Override
public boolean stillAcceptsData() {
return true;
}
@Override
public void loadData(final InputStream input, final String name)
throws IOException, ParseException {
int lineNumber = 0;
String line = null;
// Initialize Lists
final ArrayList<Double> latitudes = new ArrayList<>();
final ArrayList<Double> longitudes = new ArrayList<>();
final ArrayList<Double> ah = new ArrayList<>();
final ArrayList<Double> aw = new ArrayList<>();
final ArrayList<Double> zhd = new ArrayList<>();
final ArrayList<Double> zwd = new ArrayList<>();
// Open stream and parse data
try (BufferedReader br = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
for (line = br.readLine(); line != null; line = br.readLine()) {
++lineNumber;
line = line.trim();
// Fill latitudes and longitudes lists
if (line.length() > 0 && line.startsWith("! Range/resolution:")) {
final String[] range_line = SEPARATOR.split(line);
// Latitudes list
for (double lat = Double.parseDouble(range_line[2]); lat <= Double.parseDouble(range_line[3]); lat = lat + Double.parseDouble(range_line[6])) {
latitudes.add(FastMath.toRadians(lat));
}
// Longitude list
for (double lon = Double.parseDouble(range_line[4]); lon <= Double.parseDouble(range_line[5]); lon = lon + Double.parseDouble(range_line[7])) {
longitudes.add(FastMath.toRadians(lon));
// For VFM1 files, header specify that longitudes end at 360°
// In reality they end at 357.5°. That is why we stop the loop when the longitude
// reaches 357.5°.
if (type == ViennaModelType.VIENNA_ONE && lon >= 357.5) {
break;
}
}
}
// Fill ah, aw, zhd and zwd lists
if (line.length() > 0 && !line.startsWith("!")) {
final String[] values_line = SEPARATOR.split(line);
ah.add(Double.parseDouble(values_line[2]));
aw.add(Double.parseDouble(values_line[3]));
zhd.add(Double.parseDouble(values_line[4]));
zwd.add(Double.parseDouble(values_line[5]));
}
}
} catch (NumberFormatException nfe) {
throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
lineNumber, name, line);
}
// Check that ah, aw, zh and zw were found (only one check is enough)
if (ah.isEmpty()) {
throw new OrekitException(OrekitMessages.NO_VIENNA_ACOEF_OR_ZENITH_DELAY_IN_FILE, name);
}
final int dimLat = latitudes.size();
final int dimLon = longitudes.size();
// Change Lists to Arrays
final double[] xVal = new double[dimLat];
for (int i = 0; i < dimLat; i++) {
xVal[i] = latitudes.get(i);
}
final double[] yVal = new double[dimLon];
for (int j = 0; j < dimLon; j++) {
yVal[j] = longitudes.get(j);
}
final double[][] fvalAH = new double[dimLat][dimLon];
final double[][] fvalAW = new double[dimLat][dimLon];
final double[][] fvalZH = new double[dimLat][dimLon];
final double[][] fvalZW = new double[dimLat][dimLon];
int index = dimLon * dimLat;
for (int x = 0; x < dimLat; x++) {
for (int y = dimLon - 1; y >= 0; y--) {
index = index - 1;
fvalAH[x][y] = ah.get(index);
fvalAW[x][y] = aw.get(index);
fvalZH[x][y] = zhd.get(index);
fvalZW[x][y] = zwd.get(index);
}
}
// Build Bilinear Interpolation Functions
final BilinearInterpolatingFunction functionAH = new BilinearInterpolatingFunction(xVal, yVal, fvalAH);
final BilinearInterpolatingFunction functionAW = new BilinearInterpolatingFunction(xVal, yVal, fvalAW);
final BilinearInterpolatingFunction functionZH = new BilinearInterpolatingFunction(xVal, yVal, fvalZH);
final BilinearInterpolatingFunction functionZW = new BilinearInterpolatingFunction(xVal, yVal, fvalZW);
coefficientsA = new double[2];
zenithDelay = new double[2];
// Get the values for the given latitude and longitude
coefficientsA[0] = functionAH.value(latitude, longitude);
coefficientsA[1] = functionAW.value(latitude, longitude);
zenithDelay[0] = functionZH.value(latitude, longitude);
zenithDelay[1] = functionZW.value(latitude, longitude);
}
}