blob: 92f153891d6bfab9d3af4e30a08cdd6924287dae [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.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.measure.Latitude;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.util.Workaround;
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 static java.lang.Math.*;
import static org.apache.sis.internal.referencing.provider.AlbersEqualArea.*;
/**
* <cite>Albers Equal Area</cite> projection (EPSG code 9822).
* See the following references for an overview:
* <ul>
* <li><a href="https://en.wikipedia.org/wiki/Albers_projection">Albers projection on Wikipedia</a></li>
* <li><a href="http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html">Albers Equal-Area Conic projection on MathWorld</a></li>
* </ul>
*
* <p>The {@code "standard_parallel_2"} parameter is optional and will be given the same value as
* {@code "standard_parallel_1"} if not set.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @version 1.1
* @since 0.8
* @module
*/
public class AlbersEqualArea extends EqualAreaProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -3466040922402982480L;
/**
* Internal coefficients for computation, depending only on eccentricity and values of standards parallels.
* This is defined as {@literal n = (m₁² – m₂²) / (α₂ – α₁)} in §1.3.13 of IOGP Publication 373-7-2 (april 2015).
*
* <p>In Apache SIS implementation, we use modified formulas in which the (1 - ℯ²) factor is omitted in
* {@link #qm(double)} calculation. Consequently what we get is a modified value <var>nm</var> which is
* related to Snyder's <var>n</var> value by {@literal n = nm / (1 - ℯ²)}. The omitted (1 - ℯ²) factor
* is either taken in account by the (de)normalization matrix, or cancels with other (1 - ℯ²) factors
* when we develop the formulas.</p>
*
* <p>Note that in the spherical case, <var>nm</var> = Snyder's <var>n</var>.</p>
*/
final double nm;
/**
* Internal coefficients for computation, depending only on values of standards parallels.
* This is defined as {@literal C = m₁² + (n⋅α₁)} in §1.3.13 of IOGP Publication 373-7-2 –
* Geomatics Guidance Note number 7, part 2 – April 2015.
*/
final double C;
/**
* A bound of the [−n⋅π … n⋅π] range, which is the valid range of θ = n⋅λ values.
* Some (not all) θ values need to be shifted inside that range before to use them
* in trigonometric functions.
*
* @see Initializer#boundOfScaledLongitude(DoubleDouble)
*/
final double θ_bound;
/**
* Creates an Albers Equal Area projection from the given parameters.
*
* @param method description of the projection parameters.
* @param parameters the parameter values of the projection to create.
*/
public AlbersEqualArea(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").
*/
@SuppressWarnings("fallthrough")
@Workaround(library="JDK", version="1.7")
private static Initializer initializer(final OperationMethod method, final Parameters parameters) {
final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class);
roles.put(ParameterRole.FALSE_EASTING, EASTING_AT_FALSE_ORIGIN);
roles.put(ParameterRole.FALSE_NORTHING, NORTHING_AT_FALSE_ORIGIN);
roles.put(ParameterRole.CENTRAL_MERIDIAN, LONGITUDE_OF_FALSE_ORIGIN);
return new Initializer(method, parameters, roles, (byte) 0);
}
/**
* 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 AlbersEqualArea(final Initializer initializer) {
super(initializer);
double φ0 = initializer.getAndStore(LATITUDE_OF_FALSE_ORIGIN);
double φ1 = initializer.getAndStore(STANDARD_PARALLEL_1, φ0);
double φ2 = initializer.getAndStore(STANDARD_PARALLEL_2, φ1);
if (abs1 + φ2) < Formulas.ANGULAR_TOLERANCE) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.LatitudesAreOpposite_2,
new Latitude1), new Latitude2)));
}
final boolean secant = (abs1 - φ2) >= Formulas.ANGULAR_TOLERANCE);
φ0 = toRadians0);
φ1 = toRadians1);
φ2 = toRadians2);
final double sinφ0 = sin0);
final double sinφ1 = sin1);
final double cosφ1 = cos1);
final double sinφ2 = sin2);
final double cosφ2 = cos2);
final double m1 = initializer.scaleAtφ(sinφ1, cosφ1); // = cos(φ₁) / √(1 – ℯ²sin²φ₁)
final double α1 = qm(sinφ1); // Omitted ×(1-ℯ²)
if (secant) {
final double m2 = initializer.scaleAtφ(sinφ2, cosφ2); // = cos(φ₂) / √(1 – ℯ²sin²φ₂)
final double α2 = qm(sinφ2); // Omitted ×(1-ℯ²)
nm = (m1*m1 - m2*m2) / 2 - α1); // n = nm / (1-ℯ²)
} else {
nm = sinφ1;
}
C = m1*m1 + nm1; // Omitted (1-ℯ²) term in nm cancels with omitted (1-ℯ²) term in α₁.
/*
* Compute rn = (1-ℯ²)/nm, which is the reciprocal of the "real" n used in Snyder and EPSG guidance note.
* We opportunistically use double-double arithmetic since the MatrixSIS operations use them anyway, but
* we do not really have that accuracy because of the limited precision of 'nm'. The intent is rather to
* increase the chances term cancellations happen during concatenation of coordinate operations.
*/
final DoubleDouble rn = new DoubleDouble(1d);
rn.subtract(initializer.eccentricitySquared);
rn.divide(nm);
/*
* Compute ρ₀ = √(C - n⋅q(sinφ₀))/n with multiplication by a omitted because already taken in account
* by the denormalization matrix. Omitted (1-ℯ²) term in nm cancels with omitted (1-ℯ²) term in qm(…).
* See above note about double-double arithmetic usage.
*/
final DoubleDouble ρ0 = new DoubleDouble(C - nm*qm(sinφ0));
ρ0.sqrt();
ρ0.multiply(rn);
/*
* At this point, all parameters have been processed. Now process to their
* validation and the initialization of (de)normalize affine transforms.
*/
final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
denormalize.convertBefore(0, rn, null); rn.negate();
denormalize.convertBefore(1, rn, ρ0); rn.inverseDivide(-1);
normalize.convertAfter(0, rn, null); // On this line, `rn` became `n`.
θ_bound = initializer.boundOfScaledLongitude(rn);
}
/**
* Creates a new projection initialized to the same parameters than the given one.
*/
AlbersEqualArea(final AlbersEqualArea other) {
super(other);
nm = other.nm;
C = other.C;
θ_bound = other_bound;
}
/**
* Returns the names of additional internal parameters which need to be taken in account when
* comparing two {@code AlbersEqualArea} projections or formatting them in debug mode.
*/
@Override
final String[] getInternalParameterNames() {
return new String[] {"n", "C"};
}
/**
* Returns the values of additional internal parameters which need to be taken in account when
* comparing two {@code AlbersEqualArea} projections or formatting them in debug mode.
*/
@Override
final double[] getInternalParameterValues() {
return new double[] {nm / (1 - eccentricitySquared), C};
}
/**
* 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 {
AlbersEqualArea 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
{
// θ = n⋅λ reduced to [−n⋅π … n⋅π] range.
final double θ = wraparoundScaledLongitude(srcPts[srcOff], θ_bound);
final double φ = srcPts[srcOff + 1];
final double cosθ = cos(θ);
final double sinθ = sin(θ);
final double sinφ = sin(φ);
final double ρ = sqrt(C - nm*qm_ellipsoid(sinφ));
if (dstPts != null) {
dstPts[dstOff ] = ρ * sinθ;
dstPts[dstOff+1] = ρ * cosθ;
}
if (!derivate) {
return null;
}
/*
* End of map projection. Now compute the derivative.
*/
final double me = 1 - eccentricitySquared;
final double dρ_dφ = -0.5 * nm*dqm_dφ(sinφ, cos(φ)*me) / (me*ρ);
return new Matrix2(cosθ*ρ, dρ_dφ*sinθ, // ∂x/∂λ, ∂x/∂φ
-sinθ*ρ, dρ_dφ*cosθ); // ∂y/∂λ, ∂y/∂φ
}
/**
* Converts the specified (<var>x</var>,<var>y</var>) coordinates and stores the (θ,φ) result in {@code dstPts}.
*
* @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 x = srcPts[srcOff ];
final double y = srcPts[srcOff+1];
/*
* Note: Snyder suggests to reverse the sign of x, y and ρ₀ if n is negative. It should not done in Apache SIS
* implementation because (x,y) are premultiplied by n (by the normalization affine transform) before to enter
* in this method, so if n was negative those values have already their sign reverted.
*/
dstPts[dstOff ] = atan2(x, y);
dstPts[dstOff+1] = φ((C - (x*x + y*y)) / nm);
/*
* Note: Snyder 14-19 gives q = (C - ρ²n²/a²)/n where ρ = √(x² + (ρ₀ - y)²).
* But in Apache SIS implementation, ρ₀ has already been subtracted by the matrix before we reach this point.
* So we can simplify by ρ² = x² + y². Furthermore the matrix also divided x and y by a (the semi-major axis
* length) before this method, and multiplied by n. so what we have is actually (ρ⋅n/a)² = x² + y².
* So the formula become:
*
* q = (C - (x² + y²)) / n
*
* We divide by nm instead of n, so a (1-ℯ²) term is missing. But that missing term will be cancelled with
* the missing (1-ℯ²) term in qmPolar (the divisor applied by the φ(double) method that we invoke).
*/
}
/**
* Provides the transform equations for the spherical case of the Albers Equal Area projection.
*
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @version 1.1
* @since 0.8
* @module
*/
static final class Spherical extends AlbersEqualArea {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -7238296545347764989L;
/**
* 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 AlbersEqualArea other) {
super(other);
}
/**
* {@inheritDoc}
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate)
{
// θ = n⋅λ reduced to [−n⋅π … n⋅π] range.
final double θ = wraparoundScaledLongitude(srcPts[srcOff], θ_bound);
final double φ = srcPts[srcOff + 1];
final double cosθ = cos(θ);
final double sinθ = sin(θ);
final double sinφ = sin(φ);
final double ρ = sqrt(C - 2*nm*sinφ); // Snyder 14-3 with radius and division by n omitted.
if (dstPts != null) {
dstPts[dstOff ] = ρ * sinθ; // Snyder 14-1
dstPts[dstOff+1] = ρ * cosθ; // Snyder 14-2
}
if (!derivate) {
return null;
}
final double dρ_dφ = -nm*cos(φ) / ρ;
return new Matrix2(cosθ*ρ, dρ_dφ*sinθ, // ∂x/∂λ, ∂x/∂φ
-sinθ*ρ, dρ_dφ*cosθ); // ∂y/∂λ, ∂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];
dstPts[dstOff ] = atan2(x, y); // Part of Snyder 14-11
dstPts[dstOff+1] = asin((C - (x*x + y*y)) / (nm*2)); // Snyder 14-8 modified
}
}
}