/*
* Geotools 2 - OpenSource mapping toolkit
* (C) 2003, Geotools Project Managment Committee (PMC)
* (C) 2001, Institut de Recherche pour le Développement
*
* 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; either
* version 2.1 of the License, or (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* This package contains documentation from OpenGIS specifications.
* OpenGIS consortium's work is fully acknowledged here.
*/
package org.geotools.ct;
// J2SE dependencies
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import javax.media.jai.ParameterList;
import org.geotools.cs.Ellipsoid;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.units.Unit;
import org.opengis.referencing.operation.TransformException;
/**
* Transforms three dimensional geographic points to geocentric
* coordinate points. Input points must be longitudes, latitudes
* and heights above the ellipsoid.
*
* @source $URL$
* @version $Id$
* @author Frank Warmerdam
* @author Martin Desruisseaux
*
* @deprecated Replaced by {@link org.geotools.referencing.operation.GeocentricTransform}
* in the org.geotools.referencing.operation.transform
package.
*/
final class GeocentricTransform extends AbstractMathTransform implements Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = -3352045463953828140L;
/**
* Maximal error tolerance in metres during assertions, in metres. If assertions
* are enabled (JDK 1.4 only), then every coordinates transformed with
* {@link #inverseTransform} will be transformed again with {@link #transform}.
* If the distance between the resulting position and the original position
* is greater than MAX_ERROR
, then a {@link AssertionError} is thrown.
*/
private static final double MAX_ERROR = 0.01;
/**
* Cosine of 67.5 degrees.
*/
private static final double COS_67P5 = 0.38268343236508977;
/**
* Toms region 1 constant.
*/
private static final double AD_C = 1.0026000;
/**
* Semi-major axis of ellipsoid in meters.
*/
private final double a;
/**
* Semi-minor axis of ellipsoid in meters.
*/
private final double b;
/**
* Square of semi-major axis (@link #a}˛).
*/
private final double a2;
/**
* Square of semi-minor axis ({@link #b}˛).
*/
private final double b2;
/**
* Eccentricity squared.
*/
private final double e2;
/**
* 2nd eccentricity squared.
*/
private final double ep2;
/**
* true
if geographic coordinates
* include an ellipsoidal height (i.e. are 3-D),
* or false
if they are strictly 2-D.
*/
private final boolean hasHeight;
/**
* The inverse of this transform.
* Will be created only when needed.
*/
private transient MathTransform inverse;
/**
* Construct a transform.
*
* @param ellipsoid The ellipsoid.
* @param hasHeight true
if geographic coordinates
* include an ellipsoidal height (i.e. are 3-D),
* or false
if they are strictly 2-D.
*/
protected GeocentricTransform(final Ellipsoid ellipsoid, final boolean hasHeight) {
this(ellipsoid.getSemiMajorAxis(),
ellipsoid.getSemiMinorAxis(),
ellipsoid.getAxisUnit(), hasHeight);
}
/**
* Construct a transform.
*
* @param semiMajor The semi-major axis length.
* @param semiMinor The semi-minor axis length.
* @param units The axis units.
* @param hasHeight true
if geographic coordinates
* include an ellipsoidal height (i.e. are 3-D),
* or false
if they are strictly 2-D.
*/
protected GeocentricTransform(final double semiMajor,
final double semiMinor,
final Unit units,
final boolean hasHeight)
{
this.hasHeight = hasHeight;
a = Unit.METRE.convert(semiMajor, units);
b = Unit.METRE.convert(semiMinor, units);
a2 = a*a;
b2 = b*b;
e2 = (a2 - b2) / a2;
ep2 = (a2 - b2) / b2;
checkArgument("a", a, Double.MAX_VALUE);
checkArgument("b", b, a);
}
/**
* Check an argument value. The argument must be greater
* than 0 and finite, otherwise an exception is thrown.
*
* @param name The argument name.
* @param value The argument value.
* @param max The maximal legal argument value.
*/
private static void checkArgument(final String name,
final double value,
final double max) throws IllegalArgumentException
{
if (!(value>=0 && value<=max)) {
// Use '!' in order to trap NaN
throw new IllegalArgumentException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, name, new Double(value)));
}
}
/**
* Converts geodetic coordinates (longitude, latitude, height) to
* geocentric coordinates (x, y, z) according to the current ellipsoid
* parameters.
*/
public void transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts) {
transform(srcPts, srcOff, dstPts, dstOff, numPts, false);
}
/**
* Implementation of geodetic to geocentric conversion. This
* implementation allows the caller to use height in computation.
*/
private void transform(final double[] srcPts, int srcOff,
final double[] dstPts, int dstOff,
int numPts, boolean hasHeight)
{
int step = 0;
final int dimSource = getDimSource();
hasHeight |= (dimSource>=3);
if (srcPts==dstPts && srcOffdstOff) {
step = -dimSource;
srcOff -= (numPts-1)*step;
dstOff -= (numPts-1)*step;
}
while (--numPts >= 0) {
final double L = Math.toRadians(srcPts[srcOff++]); // Longitude
final double P = Math.toRadians(srcPts[srcOff++]); // Latitude
final double h = hasHeight ? srcPts[srcOff++] : 0; // Height above the ellipsoid (m)
final double cosLat = Math.cos(P);
final double sinLat = Math.sin(P);
final double rn = a / Math.sqrt(1 - e2 * (sinLat*sinLat));
dstPts[dstOff++] = (rn + h) * cosLat * Math.cos(L); // X: Toward prime meridian
dstPts[dstOff++] = (rn + h) * cosLat * Math.sin(L); // Y: Toward East
dstPts[dstOff++] = (rn * (1-e2) + h) * sinLat; // Z: Toward North
srcOff += step;
dstOff += step;
}
}
/**
* Converts geodetic coordinates (longitude, latitude, height) to
* geocentric coordinates (x, y, z) according to the current ellipsoid
* parameters.
*/
public void transform(final float[] srcPts, int srcOff,
final float[] dstPts, int dstOff, int numPts)
{
int step = 0;
final int dimSource = getDimSource();
final boolean hasHeight = (dimSource>=3);
if (srcPts==dstPts && srcOffdstOff) {
step = -dimSource;
srcOff -= (numPts-1)*step;
dstOff -= (numPts-1)*step;
}
while (--numPts >= 0) {
final double L = Math.toRadians(srcPts[srcOff++]); // Longitude
final double P = Math.toRadians(srcPts[srcOff++]); // Latitude
final double h = hasHeight ? srcPts[srcOff++] : 0; // Height above the ellipsoid (m)
final double cosLat = Math.cos(P);
final double sinLat = Math.sin(P);
final double rn = a / Math.sqrt(1 - e2 * (sinLat*sinLat));
dstPts[dstOff++] = (float) ((rn + h) * cosLat * Math.cos(L)); // X: Toward prime meridian
dstPts[dstOff++] = (float) ((rn + h) * cosLat * Math.sin(L)); // Y: Toward East
dstPts[dstOff++] = (float) ((rn * (1-e2) + h) * sinLat); // Z: Toward North
srcOff += step;
dstOff += step;
}
}
/**
* Converts geocentric coordinates (x, y, z) to geodetic coordinates
* (longitude, latitude, height), according to the current ellipsoid
* parameters. The method used here is derived from "An Improved
* Algorithm for Geocentric to Geodetic Coordinate Conversion", by
* Ralph Toms, Feb 1996.
*/
protected final void inverseTransform(final double[] srcPts, int srcOff,
final double[] dstPts, int dstOff, int numPts)
{
int step = 0;
final int dimSource = getDimSource();
final boolean hasHeight = (dimSource>=3);
boolean computeHeight = hasHeight;
assert (computeHeight=true) == true; // Intentional side effect FIXME are you insane?
if (srcPts==dstPts && srcOffdstOff) {
step = -dimSource;
srcOff -= (numPts-1)*step;
dstOff -= (numPts-1)*step;
}
while (--numPts >= 0) {
final double x = srcPts[srcOff++]; // Toward prime meridian
final double y = srcPts[srcOff++]; // Toward East
final double z = srcPts[srcOff++]; // Toward North
// Note: The Java version of 'atan2' work correctly for x==0.
// No need for special handling like in the C version.
// No special handling neither for latitude. Formulas
// below are generic enough, considering that 'atan'
// work correctly with infinities (1/0).
// Note: Variable names follow the notation used in Toms, Feb 1996
final double W2 = x*x + y*y; // square of distance from Z axis
final double W = Math.sqrt(W2); // distance from Z axis
final double T0 = z * AD_C; // initial estimate of vertical component
final double S0 = Math.sqrt(T0*T0 + W2); // initial estimate of horizontal component
final double sin_B0 = T0 / S0; // sin(B0), B0 is estimate of Bowring aux variable
final double cos_B0 = W / S0; // cos(B0)
final double sin3_B0 = sin_B0 * sin_B0 * sin_B0; // cube of sin(B0)
final double T1 = z + b * ep2 * sin3_B0; // corrected estimate of vertical component
final double sum = W - a*e2*(cos_B0*cos_B0*cos_B0); // numerator of cos(phi1)
final double S1 = Math.sqrt(T1*T1 + sum * sum); // corrected estimate of horizontal component
final double sin_p1 = T1 / S1; // sin(phi1), phi1 is estimated latitude
final double cos_p1 = sum / S1; // cos(phi1)
final double longitude = Math.toDegrees(Math.atan2(y , x ));
final double latitude = Math.toDegrees(Math.atan(sin_p1 / cos_p1));
final double height;
dstPts[dstOff++] = longitude;
dstPts[dstOff++] = latitude;
if (computeHeight) {
final double rn = a/Math.sqrt(1-e2*(sin_p1*sin_p1)); // Earth radius at location
if (cos_p1 >= +COS_67P5) height = W / +cos_p1 - rn;
else if (cos_p1 <= -COS_67P5) height = W / -cos_p1 - rn;
else height = z / sin_p1 + rn*(e2 - 1.0);
if (hasHeight) {
dstPts[dstOff++] = height;
}
// If assertion are enabled, then transform the
// result and compare it with the input array.
double distance;
assert MAX_ERROR > (distance=checkTransform(new double[]
{x,y,z, longitude, latitude, height})) : distance;
}
srcOff += step;
dstOff += step;
}
}
/**
* Converts geocentric coordinates (x, y, z) to geodetic coordinates
* (longitude, latitude, height), according to the current ellipsoid
* parameters. The method used here is derived from "An Improved
* Algorithm for Geocentric to Geodetic Coordinate Conversion", by
* Ralph Toms, Feb 1996.
*/
protected final void inverseTransform(final float[] srcPts, int srcOff,
final float[] dstPts, int dstOff, int numPts)
{
int step = 0;
final int dimSource = getDimSource();
final boolean hasHeight = (dimSource>=3);
boolean computeHeight = hasHeight;
assert (computeHeight=true) == true; // Intentional side effect // FIXME are you insane?
if (srcPts==dstPts && srcOffdstOff) {
step = -dimSource;
srcOff -= (numPts-1)*step;
dstOff -= (numPts-1)*step;
}
while (--numPts >= 0) {
final double x = srcPts[srcOff++]; // Toward prime meridian
final double y = srcPts[srcOff++]; // Toward East
final double z = srcPts[srcOff++]; // Toward North
// Note: The Java version of 'atan2' work correctly for x==0.
// No need for special handling like in the C version.
// No special handling neither for latitude. Formulas
// below are generic enough, considering that 'atan'
// work correctly with infinities (1/0).
// Note: Variable names follow the notation used in Toms, Feb 1996
final double W2 = x*x + y*y; // square of distance from Z axis
final double W = Math.sqrt(W2); // distance from Z axis
final double T0 = z * AD_C; // initial estimate of vertical component
final double S0 = Math.sqrt(T0*T0 + W2); // initial estimate of horizontal component
final double sin_B0 = T0 / S0; // sin(B0), B0 is estimate of Bowring aux variable
final double cos_B0 = W / S0; // cos(B0)
final double sin3_B0 = sin_B0 * sin_B0 * sin_B0; // cube of sin(B0)
final double T1 = z + b * ep2 * sin3_B0; // corrected estimate of vertical component
final double sum = W - a*e2*(cos_B0*cos_B0*cos_B0); // numerator of cos(phi1)
final double S1 = Math.sqrt(T1*T1 + sum * sum); // corrected estimate of horizontal component
final double sin_p1 = T1 / S1; // sin(phi1), phi1 is estimated latitude
final double cos_p1 = sum / S1; // cos(phi1)
final double longitude = Math.toDegrees(Math.atan2(y , x ));
final double latitude = Math.toDegrees(Math.atan(sin_p1 / cos_p1));
final double height;
dstPts[dstOff++] = (float) longitude;
dstPts[dstOff++] = (float) latitude;
if (computeHeight) {
final double rn = a/Math.sqrt(1-e2*(sin_p1*sin_p1)); // Earth radius at location
if (cos_p1 >= +COS_67P5) height = W / +cos_p1 - rn;
else if (cos_p1 <= -COS_67P5) height = W / -cos_p1 - rn;
else height = z / sin_p1 + rn*(e2 - 1.0);
if (hasHeight) {
dstPts[dstOff++] = (float) height;
}
// If assertion are enabled, then transform the
// result and compare it with the input array.
double distance;
assert MAX_ERROR > (distance=checkTransform(new double[]
{x,y,z, longitude, latitude, height})) : distance;
}
srcOff += step;
dstOff += step;
}
}
/**
* Transform the last half if the specified array and returns
* the distance with the first half. Array points
* must have a length of 6.
*/
private double checkTransform(final double[] points) {
transform(points, 3, points, 3, 1, true);
final double dx = points[0]-points[3];
final double dy = points[1]-points[4];
final double dz = points[2]-points[5];
return Math.sqrt(dx*dx + dy*dy + dz*dz);
}
/**
* Gets the dimension of input points, which is 2 or 3.
*/
public int getDimSource() {
return hasHeight ? 3 : 2;
}
/**
* Gets the dimension of output points, which is 3.
*/
public final int getDimTarget() {
return 3;
}
/**
* Returns the inverse of this transform.
*/
public synchronized MathTransform inverse() {
if (inverse == null) {
inverse = new Inverse();
}
return inverse;
}
/**
* Returns a hash value for this transform.
*/
public final int hashCode() {
final long code = Double.doubleToLongBits( a ) +
37*(Double.doubleToLongBits( b ) +
37*(Double.doubleToLongBits( a2) +
37*(Double.doubleToLongBits( b2) +
37*(Double.doubleToLongBits( e2) +
37*(Double.doubleToLongBits(ep2))))));
return (int) code ^ (int) (code >>> 32);
}
/**
* Compares the specified object with
* this math transform for equality.
*/
public final boolean equals(final Object object) {
if (object==this) {
// Slight optimization
return true;
}
if (super.equals(object)) {
final GeocentricTransform that = (GeocentricTransform) object;
return Double.doubleToLongBits(this. a ) == Double.doubleToLongBits(that. a ) &&
Double.doubleToLongBits(this. b ) == Double.doubleToLongBits(that. b ) &&
Double.doubleToLongBits(this. a2) == Double.doubleToLongBits(that. a2) &&
Double.doubleToLongBits(this. b2) == Double.doubleToLongBits(that. b2) &&
Double.doubleToLongBits(this. e2) == Double.doubleToLongBits(that. e2) &&
Double.doubleToLongBits(this.ep2) == Double.doubleToLongBits(that.ep2) &&
this.hasHeight == that.hasHeight;
}
return false;
}
/**
* Returns the WKT for this math transform.
*/
public final String toString() {
return toString("Ellipsoid_To_Geocentric");
}
/**
* Returns the WKT for this math transform with the
* specified classification name. The classification
* name should be "Ellipsoid_To_Geocentric" or
* "Geocentric_To_Ellipsoid".
*/
final String toString(final String classification) {
final StringBuffer buffer = paramMT(classification);
addParameter(buffer, "semi_major", a);
addParameter(buffer, "semi_minor", b);
buffer.append(']');
return buffer.toString();
}
/**
* Inverse of a geocentric transform.
*
* @version $Id$
* @author Martin Desruisseaux
*/
private final class Inverse extends AbstractMathTransform.Inverse implements Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 6942084702259211803L;
/**
* Default constructor.
*/
public Inverse() {
GeocentricTransform.this.super();
}
/**
* Inverse transform an array of points.
*/
public void transform(final double[] source, final int srcOffset,
final double[] dest, final int dstOffset, final int length)
throws TransformException
{
GeocentricTransform.this.inverseTransform(source, srcOffset, dest, dstOffset, length);
}
/**
* Inverse transform an array of points.
*/
public void transform(final float[] source, final int srcOffset,
final float[] dest, final int dstOffset, final int length)
throws TransformException
{
GeocentricTransform.this.inverseTransform(source, srcOffset, dest, dstOffset, length);
}
/**
* Returns a string representation of this transform.
*/
public final String toString() {
return GeocentricTransform.this.toString("Geocentric_To_Ellipsoid");
}
/**
* Restore reference to this object after deserialization.
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
GeocentricTransform.this.inverse = this;
}
}
/**
* The provider for {@link GeocentricTransform}.
*
* @version $Id$
* @author Martin Desruisseaux
*/
static final class Provider extends MathTransformProvider {
/**
* false
for the direct transform,
* or true
for the inverse transform.
*/
private final boolean inverse;
/**
* Create a provider.
*
* @param inverse false
for the direct transform,
* or true
for the inverse transform.
*/
public Provider(final boolean inverse) {
super(inverse ? "Geocentric_To_Ellipsoid" : "Ellipsoid_To_Geocentric",
VocabularyKeys.GEOCENTRIC_TRANSFORM, null);
put("semi_major", Double.NaN, POSITIVE_RANGE);
put("semi_minor", Double.NaN, POSITIVE_RANGE);
putInt("dim_geoCS", 3, AbridgedMolodenskiTransform.Provider.DIM_RANGE);
// 'dim_geoCS' is a custom parameter: NOT AN OPENGIS SPECIFICATION
this.inverse = inverse;
}
/**
* Returns a transform for the specified parameters.
*
* @param parameters The parameter values in standard units.
* @return A {@link MathTransform} object of this classification.
*/
public MathTransform create(final ParameterList parameters) {
final double semiMajor = parameters.getDoubleParameter("semi_major");
final double semiMinor = parameters.getDoubleParameter("semi_minor");
int dimGeographic = 3;
try {
dimGeographic = parameters.getIntParameter("dim_geoCS");
} catch (IllegalArgumentException exception) {
// the "dim_geoCS" parameter is a custom one required
// by our Geotools implementation. It is NOT an OpenGIS
// one. We can't require clients to know it.
}
GeocentricTransform transform = new GeocentricTransform(
semiMajor, semiMinor, Unit.METRE, dimGeographic!=2);
return (inverse) ? transform.inverse() : transform;
}
}
}