blob: cb6e4148364e8f24f0e8640e2ac7fabe297e607c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.sis.referencing.operation.projection;
import java.util.EnumMap;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.OperationMethod;
import org.apache.sis.referencing.operation.matrix.Matrix2;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.ContextualParameters;
import org.apache.sis.internal.referencing.provider.TransverseMercatorSouth;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.Workaround;
import static java.lang.Math.*;
import static org.apache.sis.math.MathFunctions.asinh;
import static org.apache.sis.math.MathFunctions.atanh;
import static org.apache.sis.internal.referencing.provider.TransverseMercator.*;
/**
* <cite>Transverse Mercator</cite> projection (EPSG codes 9807).
* This class implements the "JHS formulas" reproduced in
* IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015.
*
* <div class="section">Description</div>
* This is a cylindrical projection, in which the cylinder has been rotated 90°.
* Instead of being tangent to the equator (or to an other standard latitude), it is tangent to a central meridian.
* Deformation are more important as we are going further from the central meridian.
* The Transverse Mercator projection is appropriate for region which have a greater extent north-south than east-west.
*
* <p>There are a number of versions of the Transverse Mercator projection including the Universal (UTM)
* and Modified (MTM) Transverses Mercator projections. In these cases the earth is divided into zones.
* For the UTM the zones are 6 degrees wide, numbered from 1 to 60 proceeding east from 180 degrees longitude,
* and between latitude 84 degrees North and 80 degrees South. The central meridian is taken as the center of the zone
* and the latitude of origin is the equator. A scale factor of 0.9996 and false easting of 500000 metres is used for
* all zones and a false northing of 10000000 metres is used for zones in the southern hemisphere.
*
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @version 1.0
*
* @see Mercator
* @see ObliqueMercator
*
* @since 0.6
* @module
*/
public class TransverseMercator extends NormalizedProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -627685138188387835L;
/**
* Distance from central meridian, in degrees, at which errors are considered too important.
* This threshold is determined by comparisons of computed values against values provided by
* <cite>Karney (2009) Test data for the transverse Mercator projection</cite> data file.
* On the WGS84 ellipsoid we observed:
*
* <ul>
* <li>For ∆λ below 60°, errors below 1 centimetre.</li>
* <li>For ∆λ between 60° and 66°, errors up to 0.1 metre.</li>
* <li>For ∆λ between 66° and 70°, errors up to 1 metre.</li>
* <li>For ∆λ between 70° and 72°, errors up to 2 metres.</li>
* <li>For ∆λ between 72° and 74°, errors up to 10 metres.</li>
* <li>For ∆λ between 74° and 76°, errors up to 30 metres.</li>
* <li>For ∆λ between 76° and 78°, errors up to 1 kilometre.</li>
* <li>For ∆λ greater than 85°, results are chaotic.</li>
* </ul>
*/
static final double DOMAIN_OF_VALIDITY = 70;
/**
* {@code false} for using the original formulas as published by EPSG, or {@code true} for using formulas
* modified using trigonometric identities. The use of trigonometric identities is for reducing the amount
* of calls to the {@link Math#sin(double)} and similar methods. Some identities used are:
*
* <ul>
* <li>sin(2θ) = 2⋅sinθ⋅cosθ</li>
* <li>cos(2θ) = cos²θ - sin²θ</li>
* <li>sin(3θ) = (3 - 4⋅sin²θ)⋅sinθ</li>
* <li>cos(3θ) = (4⋅cos³θ) - 3⋅cosθ</li>
* <li>sin(4θ) = (4 - 8⋅sin²θ)⋅sinθ⋅cosθ</li>
* <li>cos(4θ) = (8⋅cos⁴θ) - (8⋅cos²θ) + 1</li>
* </ul>
*
* Hyperbolic formulas:
*
* <ul>
* <li>sinh(2θ) = 2⋅sinhθ⋅coshθ</li>
* <li>cosh(2θ) = cosh²θ + sinh²θ = 2⋅cosh²θ - 1 = 1 + 2⋅sinh²θ</li>
* <li>sinh(3θ) = (3 + 4⋅sinh²θ)⋅sinhθ</li>
* <li>cosh(3θ) = ((4⋅cosh²θ) - 3)⋅coshθ</li>
* <li>sinh(4θ) = (1 + 2⋅sinh²θ)⋅4.sinhθ⋅coshθ
* = 4.cosh(2θ).sinhθ⋅coshθ</li>
* <li>cosh(4θ) = (8⋅cosh⁴θ) - (8⋅cosh²θ) + 1
* = 8⋅cosh²(θ) ⋅ (cosh²θ - 1) + 1
* = 8⋅cosh²(θ) ⋅ sinh²(θ) + 1
* = 2⋅sinh²(2θ) + 1</li>
* </ul>
*
* Note that since this boolean is static final, the compiler should exclude the code in the branch that is never
* executed (no need to comment-out that code).
*
* @see #identityEquals(double, double)
*/
private static final boolean ALLOW_TRIGONOMETRIC_IDENTITIES = true;
/**
* Verifies if a trigonometric identity produced the expected value. This method is used in assertions only,
* for values close to the [-1 … +1] range. The tolerance threshold is approximately 1.5E-12 (note that it
* still about 7000 time greater than {@code Math.ulp(1.0)}).
*
* @see #ALLOW_TRIGONOMETRIC_IDENTITIES
*/
private static boolean identityEquals(final double actual, final double expected) {
return !(abs(actual - expected) > // Use !(a > b) instead of (a <= b) in order to tolerate NaN.
(ANGULAR_TOLERANCE / 1000) * max(1, abs(expected))); // Increase tolerance for values outside the [-1 … +1] range.
}
/**
* Coefficients in the series expansion of the forward projection,
* depending only on {@linkplain #eccentricity eccentricity} value.
* The series expansion is of the following form:
*
* <blockquote>cf₂⋅f(2θ) + cf₄⋅f(4θ) + cf₆⋅f(6θ) + cf₈⋅f(8θ)</blockquote>
*
* Those coefficients are named h₁, h₂, h₃ and h₄ in §1.3.5.1 of
* IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015.
*
* <div class="note"><b>Serialization note:</b>
* we do not strictly need to serialize those fields since they could be computed after deserialization.
* Bu we serialize them anyway in order to simplify a little bit this class (it allows us to keep those
* fields final) and because values computed after deserialization could be slightly different than the
* ones computed after construction since the constructor uses the double-double values provided by
* {@link Initializer}.</div>
*/
private final double cf2, cf4, cf6, cf8;
/**
* Coefficients in the series expansion of the inverse projection,
* depending only on {@linkplain #eccentricity eccentricity} value.
*/
private final double ci2, ci4, ci6, ci8;
/**
* Creates a Transverse Mercator projection from the given parameters.
* The {@code method} argument can be the description of one of the following:
*
* <ul>
* <li><cite>"Transverse Mercator"</cite>.</li>
* <li><cite>"Transverse Mercator (South Orientated)"</cite>.</li>
* </ul>
*
* @param method description of the projection parameters.
* @param parameters the parameter values of the projection to create.
*/
public TransverseMercator(final OperationMethod method, final Parameters parameters) {
this(initializer(method, parameters));
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
@Workaround(library="JDK", version="1.7")
private static Initializer initializer(final OperationMethod method, final Parameters parameters) {
final boolean isSouth = identMatch(method, "(?i).*\\bSouth\\b.*", TransverseMercatorSouth.IDENTIFIER);
final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class);
ParameterRole xOffset = ParameterRole.FALSE_EASTING;
ParameterRole yOffset = ParameterRole.FALSE_NORTHING;
if (isSouth) {
xOffset = ParameterRole.FALSE_WESTING;
yOffset = ParameterRole.FALSE_SOUTHING;
}
roles.put(ParameterRole.CENTRAL_MERIDIAN, LONGITUDE_OF_ORIGIN);
roles.put(ParameterRole.SCALE_FACTOR, SCALE_FACTOR);
roles.put(xOffset, FALSE_EASTING);
roles.put(yOffset, FALSE_NORTHING);
return new Initializer(method, parameters, roles, isSouth ? (byte) 1 : (byte) 0);
}
/**
* Creates a new Transverse Mercator projection from the given initializer.
* This constructor is used also by {@link ZonedGridSystem}.
*/
TransverseMercator(final Initializer initializer) {
super(initializer);
final double φ0 = toRadians(initializer.getAndStore(LATITUDE_OF_ORIGIN));
/*
* Opportunistically use double-double arithmetic for computation of B since we will store
* it in the denormalization matrix, and there is no sine/cosine functions involved here.
*/
double cf4, cf6, cf8, ci4, ci6, ci8;
final DoubleDouble B;
{ // For keeping the `t` and `n` variables locale.
/*
* The n parameter is defined by the EPSG guide as:
*
* n = f / (2-f)
*
* Where f is the flattening factor (1 - b/a). This equation can be rewritten as:
*
* n = (1 - b/a) / (1 + b/a)
*
* As much as possible, b/a should be computed from the map projection parameters.
* However if those parameters are not available anymore, then they can be computed
* from the eccentricity as:
*
* b/a = √(1 - ℯ²)
*/
final DoubleDouble t = initializer.axisLengthRatio(); // t = b/a
t.ratio_1m_1p(); // t = (1 - t) / (1 + t)
final double n = t.doubleValue(); // n = f / (2-f)
final double n2 = n * n;
final double n3 = n2 * n;
final double n4 = n2 * n2;
/*
* Coefficients for the forward projections.
* Add the smallest values first in order to reduce rounding errors.
*/
cf2 = ( 41. / 180)*n4 + ( 5. / 16)*n3 + (-2. / 3)*n2 + n/2;
cf4 = ( 557. / 1440)*n4 + (-3. / 5)*n3 + (13. / 48)*n2;
cf6 = ( -103. / 140)*n4 + (61. / 240)*n3;
cf8 = (49561. / 161280)*n4;
/*
* Coefficients for the inverse projections.
* Add the smallest values first in order to reduce rounding errors.
*/
ci2 = ( -1. / 360)*n4 + (37. / 96)*n3 + (-2. / 3)*n2 + n/2;
ci4 = (-437. / 1440)*n4 + ( 1. / 15)*n3 + ( 1. / 48)*n2;
ci6 = ( -37. / 840)*n4 + (17. / 480)*n3;
ci8 = (4397. / 161280)*n4;
/*
* Compute B = (1 + n²/4 + n⁴/64) / (1 + n)
*/
B = new DoubleDouble(t); // B = n
B.square();
B.series(1, 0.25, 1./64); // B = (1 + n²/4 + n⁴/64)
t.add(1);
B.divide(t); // B = (1 + n²/4 + n⁴/64) / (1 + n)
}
/*
* Compute M₀ = B⋅(ξ₁ + ξ₂ + ξ₃ + ξ₄) and negate in anticipation for what will be needed
* in the denormalization matrix. We opportunistically use double-double arithmetic but
* only for the final multiplication by B, for consistency with the translation term to
* be stored in the denormalization matrix. It is not worth to use double-double in the
* sum of sine functions because the extra digits would be meaningless.
*
* NOTE: the EPSG documentation makes special cases for φ₀ = 0 or ±π/2. This is not
* needed here; we verified that the code below produces naturally the expected values.
*/
final double Q = asinh(tan0)) - eccentricity * atanh(eccentricity * sin0));
final double β = atan(sinh(Q));
final DoubleDouble M0 = new DoubleDouble();
M0.value = cf8 * sin(8*β)
+ cf6 * sin(6*β)
+ cf4 * sin(4*β)
+ cf2 * sin(2*β)
+ β;
M0.multiply(B);
M0.negate();
/*
* At this point, all parameters have been processed. Now store
* the linear operations in the (de)normalize affine transforms:
*
* Normalization:
* - Subtract central meridian to longitudes (done by the super-class constructor).
* - Convert longitudes and latitudes from degrees to radians (done by the super-class constructor)
*
* Denormalization
* - Scale x and y by B.
* - Subtract M₀ to the northing.
* - Multiply by the scale factor (done by the super-class constructor).
* - Add false easting and false northing (done by the super-class constructor).
*/
final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
denormalize.convertBefore(0, B, null);
denormalize.convertBefore(1, B, M0);
/*
* When rewriting equations using trigonometric identities, some constants appear.
* For example sin(2θ) = 2⋅sinθ⋅cosθ, so we can factor out the 2 constant into the
* corresponding 'c' field. Note: this factorization can only be performed after
* the constructor finished to compute other constants.
*/
if (ALLOW_TRIGONOMETRIC_IDENTITIES) {
// Multiplication by powers of 2 does not bring any additional rounding error.
cf4 *= 4; ci4 *= 4;
cf6 *= 16; ci6 *= 16;
cf8 *= 64; ci8 *= 64;
}
this.cf4 = cf4;
this.cf6 = cf6;
this.cf8 = cf8;
this.ci4 = ci4;
this.ci6 = ci6;
this.ci8 = ci8;
}
/**
* Creates a new projection initialized to the same parameters than the given one.
*/
TransverseMercator(final TransverseMercator other) {
super(other);
cf2 = other.cf2;
cf4 = other.cf4;
cf6 = other.cf6;
cf8 = other.cf8;
ci2 = other.ci2;
ci4 = other.ci4;
ci6 = other.ci6;
ci8 = other.ci8;
}
/**
* Returns the sequence of <cite>normalization</cite> → {@code this} → <cite>denormalization</cite> transforms
* as a whole. The transform returned by this method expects (<var>longitude</var>, <var>latitude</var>)
* coordinates in <em>degrees</em> and returns (<var>x</var>,<var>y</var>) coordinates in <em>metres</em>.
*
* <p>The non-linear part of the returned transform will be {@code this} transform, except if the ellipsoid
* is spherical. In the later case, {@code this} transform will be replaced by a simplified implementation.</p>
*
* @param factory the factory to use for creating the transform.
* @return the map projection from (λ,φ) to (<var>x</var>,<var>y</var>) coordinates.
* @throws FactoryException if an error occurred while creating a transform.
*/
@Override
public MathTransform createMapProjection(final MathTransformFactory factory) throws FactoryException {
TransverseMercator kernel = this;
if (eccentricity == 0) {
kernel = new Spherical(this);
}
return context.completeTransform(factory, kernel);
}
/**
* Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
* In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
*
* @return the matrix of the projection derivative at the given source position,
* or {@code null} if the {@code derivate} argument is {@code false}.
* @throws ProjectionException if the coordinate can not be converted.
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate) throws ProjectionException
{
final double λ = srcPts[srcOff];
if (abs(λ) >= DOMAIN_OF_VALIDITY * (PI/180)) {
/*
* The Transverse Mercator projection is conceptually a Mercator projection rotated by 90°.
* In Mercator projection, y values tend toward infinity for latitudes close to ±90°.
* In Transverse Mercator, x values tend toward infinity for longitudes close to ±90°
* at equator and after subtraction of central meridian. After we pass the 90° limit,
* the Transverse Mercator results at (90° + Δ) are the same as for (90° - Δ).
*
* Problem is that 90° is an ordinary longitude value, not even close to the limit of longitude
* values range (±180°). So having f(π/2+Δ, φ) = f(π/2-Δ, φ) results in wrong behavior in some
* algorithms like the one used by Envelopes.transform(CoordinateOperation, Envelope).
* Since a distance of 90° from central meridian is far outside the Transverse Mercator
* domain of validity anyway, we do not let the user go further.
*
* In the particular case of ellipsoidal formulas, we put a limit of 81° instead of 90°
* because experience shows that results close to equator become chaotic after 85° when
* using WGS84 ellipsoid. We do not need to reduce the limit for the spherical formulas,
* because the mathematic are simpler and the function still smooth until 90°.
*/
throw new ProjectionException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
final double φ = srcPts[srcOff+1];
final double sinλ = sin(λ);
final double sinφ = sin(φ) * eccentricity;
final double Q = asinh(tan(φ)) - atanh(ℯsinφ) * eccentricity;
final double coshQ = cosh(Q); // Can not be smaller than 1.
final double η0 = atanh(sinλ / coshQ); // Tends toward ±∞ if λ → ±90°.
/*
* Original formula: η0 = atanh(sin(λ) * cos(β)) where
* cos(β) = cos(atan(sinh(Q)))
* = 1 / sqrt(1 + sinh²(Q))
* = 1 / (sqrt(cosh²(Q) - sinh²(Q) + sinh²(Q)))
* = 1 / sqrt(cosh²(Q))
* = 1 / cosh(Q)
*
* So η0 = atanh(sin(λ) / cosh(Q))
*/
final double coshη0 = cosh0);
final double ξ0 = asin(tanh(Q) * coshη0);
/*
* Compute sin(2⋅ξ₀), sin(4⋅ξ₀), sin(6⋅ξ₀), sin(8⋅ξ₀) and same for cos, but using the following
* trigonometric identities in order to reduce the number of calls to Math.sin and cos methods.
*/
final double sin_2ξ0 = sin(20);
final double cos_2ξ0 = cos(20);
final double sin_4ξ0, sin_6ξ0, sin_8ξ0,
cos_4ξ0, cos_6ξ0, cos_8ξ0;
if (!ALLOW_TRIGONOMETRIC_IDENTITIES) {
sin_4ξ0 = sin(40);
cos_4ξ0 = cos(40);
sin_6ξ0 = sin(60);
cos_6ξ0 = cos(60);
sin_8ξ0 = sin(80);
cos_8ξ0 = cos(80);
} else {
final double sin2 = sin_2ξ0 * sin_2ξ0;
final double cos2 = cos_2ξ0 * cos_2ξ0;
sin_4ξ0 = sin_2ξ0 * cos_2ξ0; assert identityEquals(sin_4ξ0, sin(40) / 2) : ξ0;
cos_4ξ0 = (cos2 - sin2) * 0.5; assert identityEquals(cos_4ξ0, cos(40) / 2) : ξ0;
sin_6ξ0 = (0.75 - sin2) * sin_2ξ0; assert identityEquals(sin_6ξ0, sin(60) / 4) : ξ0;
cos_6ξ0 = (cos2 - 0.75) * cos_2ξ0; assert identityEquals(cos_6ξ0, cos(60) / 4) : ξ0;
sin_8ξ0 = sin_4ξ0 * cos_4ξ0; assert identityEquals(sin_8ξ0, sin(80) / 8) : ξ0;
cos_8ξ0 = 0.125 - sin_4ξ0 * sin_4ξ0; assert identityEquals(cos_8ξ0, cos(80) / 8) : ξ0;
}
/*
* Compute sinh(2⋅ξ₀), sinh(4⋅ξ₀), sinh(6⋅ξ₀), sinh(8⋅ξ₀) and same for cosh, but using the following
* hyperbolic identities in order to reduce the number of calls to Math.sinh and cosh methods.
* Note that the formulas are very similar to the above ones, with only some signs reversed.
*/
final double sinh_2η0 = sinh(20);
final double cosh_2η0 = cosh(20);
final double sinh_4η0, sinh_6η0, sinh_8η0,
cosh_4η0, cosh_6η0, cosh_8η0;
if (!ALLOW_TRIGONOMETRIC_IDENTITIES) {
sinh_4η0 = sinh(40);
cosh_4η0 = cosh(40);
sinh_6η0 = sinh(60);
cosh_6η0 = cosh(60);
sinh_8η0 = sinh(80);
cosh_8η0 = cosh(80);
} else {
final double sinh2 = sinh_2η0 * sinh_2η0;
final double cosh2 = cosh_2η0 * cosh_2η0;
cosh_4η0 = (cosh2 + sinh2) * 0.5; assert identityEquals(cosh_4η0, cosh(40) / 2) : η0;
sinh_4η0 = cosh_2η0 * sinh_2η0; assert identityEquals(sinh_4η0, sinh(40) / 2) : η0;
cosh_6η0 = cosh_2η0 * (cosh2 - 0.75); assert identityEquals(cosh_6η0, cosh(60) / 4) : η0;
sinh_6η0 = sinh_2η0 * (sinh2 + 0.75); assert identityEquals(sinh_6η0, sinh(60) / 4) : η0;
cosh_8η0 = sinh_4η0 * sinh_4η0 + 0.125; assert identityEquals(cosh_8η0, cosh(80) / 8) : η0;
sinh_8η0 = sinh_4η0 * cosh_4η0; assert identityEquals(sinh_8η0, sinh(80) / 8) : η0;
}
/*
* The projection of (λ,φ) is given by (η⋅B, ξ⋅B+M₀) — ignoring scale factors and false easting/northing.
* But the B and M₀ parameters have been merged by the constructor with other linear operations in the
* "denormalization" matrix. Consequently we only need to compute (η,ξ) below.
*/
if (dstPts != null) {
// η(λ,φ)
dstPts[dstOff ] = cf8 * cos_8ξ0 * sinh_8η0
+ cf6 * cos_6ξ0 * sinh_6η0
+ cf4 * cos_4ξ0 * sinh_4η0
+ cf2 * cos_2ξ0 * sinh_2η0
+ η0;
// ξ(λ,φ)
dstPts[dstOff+1] = cf8 * sin_8ξ0 * cosh_8η0
+ cf6 * sin_6ξ0 * cosh_6η0
+ cf4 * sin_4ξ0 * cosh_4η0
+ cf2 * sin_2ξ0 * cosh_2η0
+ ξ0;
}
if (!derivate) {
return null;
}
/*
* Now compute the derivative, if the user asked for it.
*/
final double cosλ = cos(λ); //-- λ
final double cosφ = cos(φ); //-- φ
final double cosh2Q = coshQ * coshQ; //-- Q
final double sinhQ = sinh(Q);
final double tanhQ = tanh(Q);
final double cosh2Q_sin2λ = cosh2Q - (sinλ * sinλ); //-- Qλ
final double sinhη0 = sinh0); //-- η0
final double sqrt1_thQchη0 = sqrt(1 - (tanhQ * tanhQ) * (coshη0 * coshη0)); //-- Qη0
//-- dQ_dλ = 0;
final double dQ_dφ = 1 / cosφ - eccentricitySquared * cosφ / (1 - sinφ * sinφ);
final double dη0_dλ = cosλ * coshQ / cosh2Q_sin2λ;
final double dη0_dφ = -dQ_dφ * sinλ * sinhQ / cosh2Q_sin2λ;
final double dξ0_dλ = sinhQ * sinhη0 * cosλ / (cosh2Q_sin2λ * sqrt1_thQchη0);
final double dξ0_dφ = (dQ_dφ * coshη0 / cosh2Q + dη0_dφ * sinhη0 * tanhQ) / sqrt1_thQchη0;
/*
* Jac(Proj(λ,φ)) is the Jacobian matrix of Proj(λ,φ) function.
* So the derivative of Proj(λ,φ) is defined by:
*
* ┌ ┐
* │ ∂η(λ,φ)/∂λ, ∂η(λ,φ)/∂φ │
* Jac = │ │
* (Proj(λ,φ)) │ ∂ξ(λ,φ)/∂λ, ∂ξ(λ,φ)/∂φ │
* └ ┘
*/
//-- dξ(λ, φ) / dλ
final double dξ_dλ = dξ0_dλ
+ 2 * (cf2 * (dξ0_dλ * cos_2ξ0 * cosh_2η0 + dη0_dλ * sinh_2η0 * sin_2ξ0)
+ 3 * cf6 * (dξ0_dλ * cos_6ξ0 * cosh_6η0 + dη0_dλ * sinh_6η0 * sin_6ξ0)
+ 2 * (cf4 * (dξ0_dλ * cos_4ξ0 * cosh_4η0 + dη0_dλ * sinh_4η0 * sin_4ξ0)
+ 2 * cf8 * (dξ0_dλ * cos_8ξ0 * cosh_8η0 + dη0_dλ * sinh_8η0 * sin_8ξ0)));
//-- dξ(λ, φ) / dφ
final double dξ_dφ = dξ0_dφ
+ 2 * (cf2 * (dξ0_dφ * cos_2ξ0 * cosh_2η0 + dη0_dφ * sinh_2η0 * sin_2ξ0)
+ 3 * cf6 * (dξ0_dφ * cos_6ξ0 * cosh_6η0 + dη0_dφ * sinh_6η0 * sin_6ξ0)
+ 2 * (cf4 * (dξ0_dφ * cos_4ξ0 * cosh_4η0 + dη0_dφ * sinh_4η0 * sin_4ξ0)
+ 2 * cf8 * (dξ0_dφ * cos_8ξ0 * cosh_8η0 + dη0_dφ * sinh_8η0 * sin_8ξ0)));
//-- dη(λ, φ) / dλ
final double dη_dλ = dη0_dλ
+ 2 * (cf2 * (dη0_dλ * cosh_2η0 * cos_2ξ0 - dξ0_dλ * sin_2ξ0 * sinh_2η0)
+ 3 * cf6 * (dη0_dλ * cosh_6η0 * cos_6ξ0 - dξ0_dλ * sin_6ξ0 * sinh_6η0)
+ 2 * (cf4 * (dη0_dλ * cosh_4η0 * cos_4ξ0 - dξ0_dλ * sin_4ξ0 * sinh_4η0)
+ 2 * cf8 * (dη0_dλ * cosh_8η0 * cos_8ξ0 - dξ0_dλ * sin_8ξ0 * sinh_8η0)));
//-- dη(λ, φ) / dφ
final double dη_dφ = dη0_dφ
+ 2 * (cf2 * (dη0_dφ * cosh_2η0 * cos_2ξ0 - dξ0_dφ * sin_2ξ0 * sinh_2η0)
+ 3 * cf6 * (dη0_dφ * cosh_6η0 * cos_6ξ0 - dξ0_dφ * sin_6ξ0 * sinh_6η0)
+ 2 * (cf4 * (dη0_dφ * cosh_4η0 * cos_4ξ0 - dξ0_dφ * sin_4ξ0 * sinh_4η0)
+ 2 * cf8 * (dη0_dφ * cosh_8η0 * cos_8ξ0 - dξ0_dφ * sin_8ξ0 * sinh_8η0)));
return new Matrix2(dη_dλ, dη_dφ,
dξ_dλ, dξ_dφ);
}
/**
* Transforms the specified (η, ξ) coordinates and stores the result in {@code dstPts} (angles in radians).
*
* @throws ProjectionException if the point can not be converted.
*/
@Override
protected void inverseTransform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff)
throws ProjectionException
{
final double η = srcPts[srcOff ];
final double ξ = srcPts[srcOff+1];
/*
* Following calculation of sin_2ξ, sin_4ξ, etc. is basically a copy-and-paste of the code in transform(…).
* Its purpose is the same than for transform(…): reduce the amount of calls to Math.sin(double) and other
* methods.
*/
final double sin_2ξ = sin (2*ξ);
final double cos_2ξ = cos (2*ξ);
final double sinh_2η = sinh(2*η);
final double cosh_2η = cosh(2*η);
final double sin_4ξ, sin_6ξ, sin_8ξ,
cos_4ξ, cos_6ξ, cos_8ξ,
sinh_4η, sinh_6η, sinh_8η,
cosh_4η, cosh_6η, cosh_8η;
if (!ALLOW_TRIGONOMETRIC_IDENTITIES) {
sin_4ξ = sin(4*ξ);
cos_4ξ = cos(4*ξ);
sin_6ξ = sin(6*ξ);
cos_6ξ = cos(6*ξ);
sin_8ξ = sin(8*ξ);
cos_8ξ = cos(8*ξ);
sinh_4η = sinh(4*η);
cosh_4η = cosh(4*η);
sinh_6η = sinh(6*η);
cosh_6η = cosh(6*η);
sinh_8η = sinh(8*η);
cosh_8η = cosh(8*η);
} else {
final double sin2 = sin_2ξ * sin_2ξ;
final double cos2 = cos_2ξ * cos_2ξ;
sin_4ξ = sin_2ξ * cos_2ξ; assert identityEquals(sin_4ξ, sin(4*ξ) / 2) : ξ;
cos_4ξ = (cos2 - sin2) * 0.5; assert identityEquals(cos_4ξ, cos(4*ξ) / 2) : ξ;
sin_6ξ = (0.75 - sin2) * sin_2ξ; assert identityEquals(sin_6ξ, sin(6*ξ) / 4) : ξ;
cos_6ξ = (cos2 - 0.75) * cos_2ξ; assert identityEquals(cos_6ξ, cos(6*ξ) / 4) : ξ;
sin_8ξ = sin_4ξ * cos_4ξ; assert identityEquals(sin_8ξ, sin(8*ξ) / 8) : ξ;
cos_8ξ = 0.125 - sin_4ξ * sin_4ξ; assert identityEquals(cos_8ξ, cos(8*ξ) / 8) : ξ;
final double sinh2 = sinh_2η * sinh_2η;
final double cosh2 = cosh_2η * cosh_2η;
cosh_4η = (cosh2 + sinh2) * 0.5; assert identityEquals(cosh_4η, cosh(4*η) / 2) : η;
sinh_4η = cosh_2η * sinh_2η; assert identityEquals(sinh_4η, sinh(4*η) / 2) : η;
cosh_6η = cosh_2η * (cosh2 - 0.75); assert identityEquals(cosh_6η, cosh(6*η) / 4) : η;
sinh_6η = sinh_2η * (sinh2 + 0.75); assert identityEquals(sinh_6η, sinh(6*η) / 4) : η;
cosh_8η = sinh_4η * sinh_4η + 0.125; assert identityEquals(cosh_8η, cosh(8*η) / 8) : η;
sinh_8η = sinh_4η * cosh_4η; assert identityEquals(sinh_8η, sinh(8*η) / 8) : η;
}
/*
* The actual inverse transform.
*/
final double ξ0 = ξ - (ci8 * sin_8ξ * cosh_8η
+ ci6 * sin_6ξ * cosh_6η
+ ci4 * sin_4ξ * cosh_4η
+ ci2 * sin_2ξ * cosh_2η);
final double η0 = η - (ci8 * cos_8ξ * sinh_8η
+ ci6 * cos_6ξ * sinh_6η
+ ci4 * cos_4ξ * sinh_4η
+ ci2 * cos_2ξ * sinh_2η);
final double β = asin(sin0) / cosh0));
final double Q = asinh(tan(β));
/*
* Following usually converges in 4 iterations.
* The first iteration is unrolled.
*/
double p = eccentricity * atanh(eccentricity * tanh(Q));
double Qp = Q + p;
for (int it=0; it<MAXIMUM_ITERATIONS; it++) {
final double c = eccentricity * atanh(eccentricity * tanh(Qp));
Qp = Q + c;
if (abs(c - p) <= ITERATION_TOLERANCE) {
dstPts[dstOff ] = asin(tanh0) / cos(β));
dstPts[dstOff+1] = atan(sinh(Qp));
return;
}
p = c;
}
throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
}
/**
* Provides the transform equations for the spherical case of the Transverse Mercator projection.
*
* @author André Gosselin (MPO)
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Rueben Schulz (UBC)
* @version 0.6
* @since 0.6
* @module
*/
private static final class Spherical extends TransverseMercator {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 8903592710452235162L;
/**
* Constructs a new map projection from the parameters of the given projection.
*
* @param other the other projection (usually ellipsoidal) from which to copy the parameters.
*/
protected Spherical(final TransverseMercator other) {
super(other);
}
/**
* {@inheritDoc}
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate) throws ProjectionException
{
final double λ = srcPts[srcOff];
/*
* The Transverse Mercator projection is conceptually a Mercator projection rotated by 90°.
* In Mercator projection, y values tend toward infinity for latitudes close to ±90° while
* in Transverse Mercator, x values tend toward infinity for longitudes close to ±90° from
* central meridian (and at equator). After we pass the 90° limit, the Transverse Mercator
* results at (90° + Δ) are the same as for (90° - Δ).
*
* Problem is that 90° is an ordinary longitude value, not even close to the limit of longitude
* values range (±180°). So having f(π/2+Δ, φ) = f(π/2-Δ, φ) results in wrong behavior in some
* algorithms like the one used by Envelopes.transform(CoordinateOperation, Envelope). Since a
* distance of 90° from central meridian is outside projection domain of validity anyway, we do
* not let the user go further.
*
* Note that in the case of ellipsoidal formulas (implemented by parent class), we put the
* limit to a lower value (DOMAIN_OF_VALIDITY) because experience shows that results close
* to equator become chaotic after 85° when using WGS84 ellipsoid. We do not need to reduce
* the limit for the spherical formulas, because the mathematic are simpler and the function
* still smooth until 90°.
*/
if (abs(λ) > PI/2) {
throw new ProjectionException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
final double φ = srcPts[srcOff+1];
final double sinλ = sin(λ);
final double cosλ = cos(λ);
final double sinφ = sin(φ);
final double cosφ = cos(φ);
final double tanφ = sinφ / cosφ;
final double B = cosφ * sinλ;
/*
* Using Snyder's equation for calculating y, instead of the one used in Proj.4.
* Potential problems when y and x = 90 degrees, but behaves ok in tests.
*/
if (dstPts != null) {
dstPts[dstOff ] = atanh(B); // Snyder 8-1;
dstPts[dstOff+1] = atan2(tanφ, cosλ); // Snyder 8-3;
}
if (!derivate) {
return null;
}
final double Bm = B*B - 1;
final double sct = cosλ*cosλ + tanφ*tanφ;
return new Matrix2(-(cosφ * cosλ) / Bm, // ∂x/∂λ
(sinφ * sinλ) / Bm, // ∂x/∂φ
(tanφ * sinλ) / sct, // ∂y/∂λ
cosλ / (cosφ * cosφ * sct)); // ∂y/∂φ
}
/**
* {@inheritDoc}
*/
@Override
protected void inverseTransform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff)
{
final double x = srcPts[srcOff ];
final double y = srcPts[srcOff+1];
final double sinhx = sinh(x);
final double cosy = cos(y);
// 'copySign' corrects for the fact that we made everything positive using sqrt(…)
dstPts[dstOff ] = atan2(sinhx, cosy);
dstPts[dstOff+1] = copySign(asin(sqrt((1 - cosy*cosy) / (1 + sinhx*sinhx))), y);
}
}
}