/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * This file is derived from NGA/NASA software available for unlimited distribution. * See http://earth-info.nima.mil/GandG/wgs84/gravitymod/. */ package org.geotools.referencing.operation.transform; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.FileNotFoundException; import java.util.Collections; import java.util.StringTokenizer; import org.opengis.parameter.ParameterValue; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptor; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterNotFoundException; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.Transformation; import org.opengis.referencing.operation.TransformException; import org.geotools.parameter.Parameter; import org.geotools.parameter.ParameterGroup; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.operation.MathTransformProvider; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.metadata.iso.citation.Citations; /** * Transforms vertical coordinates using coefficients from the * <A HREF="http://earth-info.nima.mil/GandG/wgs84/gravitymod/wgs84_180/wgs84_180.html">Earth * Gravitational Model</A>. * <p> * <strong>Aknowledgement</strong><br> * This class is an adaption of Fortran code * <code><a href="http://earth-info.nga.mil/GandG/wgs84/gravitymod/wgs84_180/clenqt.for">clenqt.for</a></code> * from the <cite>National Geospatial-Intelligence Agency</cite> and available in public domain. The * <cite>normalized geopotential coefficients</cite> file bundled in this module is an adaptation of * <code><a href="http://earth-info.nima.mil/GandG/wgs84/gravitymod/wgs84_180/egm180.nor">egm180.nor</a></code> * file, with some spaces trimmed. * * @since 2.3 * * @source $URL$ * @version $Id$ * @author Pierre Cardinal * @author Martin Desruisseaux */ public final class EarthGravitationalModel extends VerticalTransform { /** * Pre-computed values of some square roots. */ private static final double SQRT_03 = 1.7320508075688772935274463415059, SQRT_05 = 2.2360679774997896964091736687313, SQRT_13 = 3.6055512754639892931192212674705, SQRT_17 = 4.1231056256176605498214098559741, SQRT_21 = 4.5825756949558400065880471937280; /** The default value for {@link #nmax}. */ static final int DEFAULT_ORDER = 180; /** {@code true} for WGS84 model, or {@code false} for WGS72 model. */ private final boolean wgs84; /** Maximum degree and order attained. */ private final int nmax; /** WGS 84 semi-major axis. */ private final double semiMajor; /** The first Eccentricity Squared (e²) for WGS 84 ellipsoid. */ private final double esq; /** Even zonal coefficient. */ private final double c2; /** WGS 84 Earth's Gravitational Constant w/ atmosphere. */ private final double rkm; /** Theoretical (Normal) Gravity at the Equator (on the Ellipsoid). */ private final double grava; /** Theoretical (Normal) Gravity Formula Constant. */ private final double star; /** * The geopotential coefficients read from the ASCII file. * Those arrays are filled by the {@link #load} method. */ private final double[] cnmGeopCoef, snmGeopCoef; /** * Cleanshaw coefficients needed for the selected gravimetric quantities that are computed. * Those arrays are computed by the {@link #initialize} method. */ private final double[] aClenshaw, bClenshaw, as; /** * Temporary buffer for use by {@link #heightOffset} only. Allocated once for ever * for avoiding too many objects creation / destruction. */ private final double[] cr, sr, s11, s12; /** * Creates a model with the default maximum degree and order. */ EarthGravitationalModel() { this(DEFAULT_ORDER, true); } /** * Creates a model with the specified maximum degree and order. */ EarthGravitationalModel(final int nmax, final boolean wgs84) { this.nmax = nmax; this.wgs84 = wgs84; if (wgs84) { /* * WGS84 model values. * NOTE: The Fortran program gives 3.9860015e+14 for 'rkm' constant. This value has been * modified in later programs. From http://cddis.gsfc.nasa.gov/926/egm96/doc/S11.HTML : * * "We next need to consider the determination of GM, GM0, W0, U0. The value of GM0 * will be that adopted for the updated GM of the WGS84 ellipsoid. This value is * 3.986004418e+14 m³/s², which is identical to that given in the IERS Numerical * Standards [McCarthy, 1996, Table 4.1]. The best estimate of GM can be taken as * the same value based on the recommendations of the IAG Special Commission SC3, * Fundamental Constants [Bursa, 1995b, p. 381]." */ semiMajor = 6378137.0; esq = 0.00669437999013; c2 = 108262.9989050e-8; rkm = 3.986004418e+14; grava = 9.7803267714; star = 0.001931851386; } else { /* * WGS72 model values. */ semiMajor = 6378135.0; esq = 0.006694317778; c2 = 108263.0e-8; rkm = 3.986005e+14; grava = 9.7803327; star = 0.005278994; } final int cleanshawLength = locatingArray(nmax + 3); final int geopCoefLength = locatingArray(nmax + 1); aClenshaw = new double[cleanshawLength]; bClenshaw = new double[cleanshawLength]; cnmGeopCoef = new double[geopCoefLength]; snmGeopCoef = new double[geopCoefLength]; as = new double[nmax + 1]; cr = new double[nmax + 1]; sr = new double[nmax + 1]; s11 = new double[nmax + 3]; s12 = new double[nmax + 3]; } /** * Computes the index as it would be returned by the locating array {@code iv} * (from the Fortran code). * <p> * Tip (used in some place in this class): * {@code locatingArray(n+1)} == {@code locatingArray(n) + n + 1}. */ private static int locatingArray(final int n) { return ((n+1) * n) >> 1; } /** * Loads the coefficients from the specified ASCII file and initialize the internal * <cite>clenshaw arrays</cite>. * <p> * <strong>Note:</strong> ASCII may looks like an unefficient format for binary distribution. * A binary file with coefficient values read by {@link java.io.DataInput#readDouble} would * be more compact than an <u>uncompressed</u> ASCII file. However, binary files are hard to * compress by the ZIP algorithm. Our experience show that a 675 kb uncompressed ASCII file * is only 222 kb after ZIP or JAR compression. The same data as a binary file is 257 kb * uncompressed and 248 kb compressed. So surprisingly, the ASCII file is more compact than * the binary file after compression. Since it is the primary format provided by the * Earth-Info web site, we use it directly in order to avoid a multiplication of formats. * * @param filename The filename (e.g. {@code "WGS84.cof"}, relative to this class directory. * @throws IOException if the file can't be read or has an invalid content. */ protected void load(final String filename) throws IOException { final InputStream stream = EarthGravitationalModel.class.getResourceAsStream(filename); if (stream == null) { throw new FileNotFoundException(filename); } final LineNumberReader in; in = new LineNumberReader(new InputStreamReader(stream, "ISO-8859-1")); String line; while ((line = in.readLine()) != null) { final StringTokenizer tokens = new StringTokenizer(line); try { /* * Note: we use 'parseShort' instead of 'parseInt' as an easy way to ensure that * the values are in some reasonable range. The range is typically [0..180]. * We don't check that, but at least 'parseShort' disallows values greater * than 32767. Additional note: we real all lines in all cases even if we * discard some of them, in order to check the file format. */ final int n = Short .parseShort (tokens.nextToken()); final int m = Short .parseShort (tokens.nextToken()); final double cbar = Double.parseDouble(tokens.nextToken()); final double sbar = Double.parseDouble(tokens.nextToken()); if (n <= nmax) { final int ll = locatingArray(n) + m; cnmGeopCoef[ll] = cbar; snmGeopCoef[ll] = sbar; } } catch (RuntimeException cause) { /* * Catch the following exceptions: * - NoSuchElementException if a line has too few numbers. * - NumberFormatException if a number can't be parsed. * - IndexOutOfBoundsException if 'n' or 'm' values are illegal. */ final IOException exception = new IOException(Errors.format( ErrorKeys.BAD_LINE_IN_FILE_$2, filename, in.getLineNumber())); exception.initCause(cause); // TODO: Inline when we will be allowed to target Java 6. throw exception; } } in.close(); initialize(); } /** * Computes the <cite>clenshaw arrays</cite> after all coefficients have been read. * We performs this step in a separated method than {@link #from} in case we wish * to read the coefficient from an other source than an ASCII file in some future * version. */ private final void initialize() { /* * MODIFY CNM EVEN ZONAL COEFFICIENTS. */ if (wgs84) { final double[] c2n = new double[6]; c2n[1] = c2; int sign = 1; double esqi = esq; for (int i=2; i<c2n.length; i++) { sign *= -1; esqi *= esq; c2n[i] = sign * (3*esqi) / ((2*i + 1) * (2*i + 3)) * (1-i + (5*i*c2 / esq)); } /* all nmax */ cnmGeopCoef[ 3] += c2n[1] / SQRT_05; /* all nmax */ cnmGeopCoef[10] += c2n[2] / 3; /* all nmax */ cnmGeopCoef[21] += c2n[3] / SQRT_13; if (nmax > 6) cnmGeopCoef[36] += c2n[4] / SQRT_17; if (nmax > 9) cnmGeopCoef[55] += c2n[5] / SQRT_21; } else { /* all nmax */ cnmGeopCoef[ 3] += 4.841732e-04; /* all nmax */ cnmGeopCoef[10] += -7.8305e-07; } /* * BUILD ALL CLENSHAW COEFFICIENT ARRAYS. */ for (int i=0; i<=nmax; i++) { as[i] = -Math.sqrt(1.0 + 1.0/(2*(i+1))); } for (int i=0; i<=nmax; i++) { for (int j=i+1; j<=nmax; j++) { final int ll = locatingArray(j) + i; final int n = 2*j + 1; final int ji = (j-i) * (j+i); aClenshaw[ll] = Math.sqrt(n*(2*j - 1) / (double) (ji)); bClenshaw[ll] = Math.sqrt(n*(j+i - 1)*(j-i - 1) / (double) (ji*(2*j - 3))); } } } /** * {@inheritDoc} */ public double heightOffset(final double longitude, final double latitude, final double height) throws TransformException { /* * Note: no need to ensure that longitude is in [-180..+180°] range, because its value * is used only in trigonometric functions (sin / cos), which roll it as we would expect. * Latitude is used only in trigonometric functions as well. */ final double phi = Math.toRadians(latitude); final double sin_phi = Math.sin(phi); final double sin2_phi = sin_phi * sin_phi; final double rni = Math.sqrt(1.0 - esq*sin2_phi); final double rn = semiMajor / rni; final double t22 = (rn + height) * Math.cos(phi); final double x2y2 = t22 * t22; final double z1 = ((rn * (1 - esq)) + height) * sin_phi; final double th = (Math.PI / 2.0) - Math.atan(z1 / Math.sqrt(x2y2)); final double y = Math.sin(th); final double t = Math.cos(th); final double f1 = semiMajor / Math.sqrt(x2y2 + z1*z1); final double f2 = f1*f1; final double rlam = Math.toRadians(longitude); final double gravn; if (wgs84) { gravn = grava * (1.0 + star * sin2_phi) / rni; } else { gravn = grava * (1.0 + star * sin2_phi) + 0.000023461 * (sin2_phi * sin2_phi); } sr[0]=0; sr[1]=Math.sin(rlam); cr[0]=1; cr[1]=Math.cos(rlam); for (int j=2; j<=nmax; j++) { sr[j] = (2.0 * cr[1] * sr[j-1]) - sr[j-2]; cr[j] = (2.0 * cr[1] * cr[j-1]) - cr[j-2]; } double sht=0, previousSht=0; for (int i=nmax; i>=0; i--) { for (int j=nmax; j>=i; j--) { final int ll = locatingArray(j) + i; final int ll2 = ll + j + 1; final int ll3 = ll2 + j + 2; final double ta = aClenshaw[ll2] * f1 * t; final double tb = bClenshaw[ll3] * f2; s11[j] = (ta * s11[j + 1]) - (tb * s11[j + 2]) + cnmGeopCoef[ll]; s12[j] = (ta * s12[j + 1]) - (tb * s12[j + 2]) + snmGeopCoef[ll]; } previousSht = sht; sht = (-as[i] * y * f1 * sht) + (s11[i] * cr[i]) + (s12[i] * sr[i]); } return ((s11[0] + s12[0]) * f1 + (previousSht * SQRT_03 * y * f2)) * rkm / (semiMajor * (gravn - (height * 0.3086e-5))); } /** * Returns the parameter descriptors for this math transform. */ @Override public ParameterDescriptorGroup getParameterDescriptors() { return Provider.PARAMETERS; } /** * Returns the parameters for this math transform. */ @Override public ParameterValueGroup getParameterValues() { return new ParameterGroup(getParameterDescriptors(), new ParameterValue[] {new Parameter(Provider.ORDER, nmax)}); } /** * The provider for {@link EarthGravitationalModel}. * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux */ public static class Provider extends MathTransformProvider { /** * The operation parameter descriptor for the maximum degree and order. * The default value is 180. */ public static final ParameterDescriptor<Integer> ORDER = DefaultParameterDescriptor.create( Collections.singletonMap(NAME_KEY, new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational( VocabularyKeys.ORDER))), DEFAULT_ORDER, 2, 180, false); /** * The parameters group. */ static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] { new NamedIdentifier(Citations.GEOTOOLS, Vocabulary.formatInternational( VocabularyKeys.EARTH_GRAVITATIONAL_MODEL)) }, new ParameterDescriptor[] { ORDER }); /** * Constructs a math transform provider. */ public Provider() { super(3, 3, PARAMETERS); } /** * Returns the operation type for this transform. */ @Override public Class<? extends Transformation> getOperationType() { return Transformation.class; } /** * Creates a math transform from the specified group of parameter values. * * @param values The group of parameter values. * @return The created math transform. * @throws ParameterNotFoundException if a required parameter was not found. * @throws FactoryException if this method failed to load the coefficient file. */ protected MathTransform createMathTransform(final ParameterValueGroup values) throws ParameterNotFoundException, FactoryException { int nmax = intValue(ORDER, values); if (nmax == 0) { nmax = DEFAULT_ORDER; } final EarthGravitationalModel mt = new EarthGravitationalModel(nmax, true); final String filename = "EGM180.nor"; try { mt.load(filename); } catch (IOException e) { throw new FactoryException(Errors.format(ErrorKeys.CANT_READ_$1, filename), e); } return mt; } } }