blob: 91209c356cf4afb2b449c9e043e15cca8861ccb0 [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.OperationMethod;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.measure.Latitude;
import org.apache.sis.parameter.Parameters;
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.LambertConformal1SP;
import org.apache.sis.internal.referencing.provider.LambertConformal2SP;
import org.apache.sis.internal.referencing.provider.LambertConformalWest;
import org.apache.sis.internal.referencing.provider.LambertConformalBelgium;
import org.apache.sis.internal.referencing.provider.LambertConformalMichigan;
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.util.Workaround;
import static java.lang.Math.*;
import static java.lang.Double.*;
import static org.apache.sis.math.MathFunctions.isPositive;
/**
* <cite>Lambert Conic Conformal</cite> projection (EPSG codes 9801, 9802, 9803, 9826, 1051).
* See the following references for an overview:
* <ul>
* <li><a href="https://en.wikipedia.org/wiki/Lambert_conformal_conic_projection">Lambert Conformal Conic projection on Wikipedia</a></li>
* <li><a href="http://mathworld.wolfram.com/LambertConformalConicProjection.html">Lambert Conformal Conic projection on MathWorld</a></li>
* </ul>
*
* <h2>Description</h2>
* Areas and shapes are deformed as one moves away from standard parallels.
* The angles are true in a limited area.
* This projection is used for the charts of North America and some European countries.
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @author André Gosselin (MPO)
* @author Rueben Schulz (UBC)
* @author Rémi Maréchal (Geomatys)
* @version 1.1
* @since 0.6
* @module
*/
public class LambertConicConformal extends ConformalProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -8971522854919443706L;
/**
* Codes for variants of Lambert Conical Conformal projection. Those variants modify the way the projections are
* constructed (e.g. in the way parameters are interpreted), but formulas are basically the same after construction.
* Those variants are not exactly the same than variants 1SP and 2SP used by EPSG, but they are closely related.
*
* <p>We do not provide such codes in public API because they duplicate the functionality of
* {@link OperationMethod} instances. We use them only for constructors convenience.</p>
*
* <p><b>CONVENTION:</b> Codes for SP1 case must be odd, and codes for SP2 case must be even.
*
* @see #getVariant(OperationMethod)
*/
private static final byte SP1 = 1, WEST = 3, // Must be odd
SP2 = 2, BELGIUM = 4, MICHIGAN = 6; // Must be even
/**
* Returns the type of the projection based on the name and identifier of the given operation method.
* If this method can not identify the type, then the parameters should be considered as a 2SP case.
*/
private static byte getVariant(final OperationMethod method) {
if (identMatch(method, "(?i).*\\bBelgium\\b.*", LambertConformalBelgium .IDENTIFIER)) return BELGIUM;
if (identMatch(method, "(?i).*\\bMichigan\\b.*", LambertConformalMichigan.IDENTIFIER)) return MICHIGAN;
if (identMatch(method, "(?i).*\\bWest\\b.*", LambertConformalWest .IDENTIFIER)) return WEST;
if (identMatch(method, "(?i).*\\b2SP\\b.*", LambertConformal2SP .IDENTIFIER)) return SP2;
if (identMatch(method, "(?i).*\\b1SP\\b.*", LambertConformal1SP .IDENTIFIER)) return SP1;
return 0; // Unidentified case, to be considered as 2SP.
}
/**
* Constant for the Belgium 2SP case. Defined as 29.2985 seconds, given here in radians.
* Use double-double arithmetic not for map projection accuracy, but for consistency with
* the normalization matrix which use that precision for "degrees to radians" conversion.
* The goal is to have cleaner results after matrix inversions and multiplications.
*
* <div class="note"><b>Tip:</b> how to verify the value:
* {@preformat java
* BigDecimal a = new BigDecimal(BELGE_A.value);
* a = a.add (new BigDecimal(BELGE_A.error));
* a = a.multiply(new BigDecimal("57.29577951308232087679815481410517"));
* a = a.multiply(new BigDecimal(60 * 60));
* System.out.println(a);
* }
* </div>
*/
static Number belgeA() {
return new DoubleDouble(-1.420431363598774E-4, -1.1777378450498224E-20);
}
/**
* Internal coefficients for computation, depending only on eccentricity and values of standards parallels.
* This is defined as {@literal n = (ln m₁ – ln m₂) / (ln t₁ – ln t₂)} in §1.3.1.1 of
* IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015.
*
* <p><b>Note:</b></p>
* <ul>
* <li>If φ₁ = -φ₂, then the cone become a cylinder and this {@code n} value become 0.
* This limiting case is the Mercator projection, but we can not use this class because
* {@code n=0} causes indetermination like 0 × ∞ in the equations of this class.</li>
* <li>If φ₁ = φ₂ = ±90°, then this {@code n} value become ±1. The formulas in the transform and
* inverse transform methods become basically the same than the ones in {@link PolarStereographic},
* but (de)normalization matrices contain NaN values.</li>
* <li>Depending on how the formulas are written, <var>n</var> may be positive in the South hemisphere and
* negative in the North hemisphere (or conversely). However Apache SIS adjusts the coefficients of the
* (de)normalization matrices in order to keep <var>n</var> positive, because the formulas are slightly
* more accurate for positive <var>n</var> values. However this adjustment is optional and can be disabled
* in the constructor.</li>
* </ul>
*/
final double n;
/**
* 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 a Lambert projection from the given parameters.
* The {@code method} argument can be the description of one of the following:
*
* <ul>
* <li><cite>"Lambert Conic Conformal (1SP)"</cite>.</li>
* <li><cite>"Lambert Conic Conformal (West Orientated)"</cite>.</li>
* <li><cite>"Lambert Conic Conformal (2SP)"</cite>.</li>
* <li><cite>"Lambert Conic Conformal (2SP Belgium)"</cite>.</li>
* <li><cite>"Lambert Conic Conformal (2SP Michigan)"</cite>.</li>
* </ul>
*
* @param method description of the projection parameters.
* @param parameters the parameter values of the projection to create.
*/
public LambertConicConformal(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 byte variant = getVariant(method);
final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class);
/*
* "Scale factor" is not formally a "Lambert Conformal (2SP)" argument, but we accept it
* anyway for all Lambert projections since it may be used in some Well Known Text (WKT).
*/
ParameterDescriptor<Double> scaleFactor = LambertConformal1SP.SCALE_FACTOR;
ParameterRole eastingDirection = ParameterRole.FALSE_EASTING;
switch (variant) {
case WEST: {
/*
* For "Lambert Conic Conformal (West Orientated)" projection, the "false easting" parameter is
* effectively a "false westing" (Geomatics Guidance Note number 7, part 2 – April 2015, §1.3.1.3)
*/
eastingDirection = ParameterRole.FALSE_WESTING;
// Fallthrough
}
case SP1: {
roles.put(eastingDirection, LambertConformal1SP.FALSE_EASTING);
roles.put(ParameterRole.FALSE_NORTHING, LambertConformal1SP.FALSE_NORTHING);
roles.put(ParameterRole.CENTRAL_MERIDIAN, LambertConformal1SP.LONGITUDE_OF_ORIGIN);
break;
}
case MICHIGAN: {
scaleFactor = LambertConformalMichigan.SCALE_FACTOR; // Ellipsoid scaling factor (EPSG:1038)
// Fallthrough
}
case BELGIUM:
case SP2: {
roles.put(eastingDirection, LambertConformal2SP.EASTING_AT_FALSE_ORIGIN);
roles.put(ParameterRole.FALSE_NORTHING, LambertConformal2SP.NORTHING_AT_FALSE_ORIGIN);
roles.put(ParameterRole.CENTRAL_MERIDIAN, LambertConformal2SP.LONGITUDE_OF_FALSE_ORIGIN);
break;
}
default: throw new AssertionError(variant);
}
roles.put(ParameterRole.SCALE_FACTOR, scaleFactor);
return new Initializer(method, parameters, roles, variant);
}
/**
* 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 LambertConicConformal(final Initializer initializer) {
super(initializer);
double φ0 = initializer.getAndStore(((initializer.variant & 1) != 0) ? // Odd 'type' are SP1, even 'type' are SP2.
LambertConformal1SP.LATITUDE_OF_ORIGIN : LambertConformal2SP.LATITUDE_OF_FALSE_ORIGIN);
/*
* Standard parallels (SP) are defined only for the 2SP case, but we look for them unconditionally
* in case the user gave us non-standard parameters. For the 1SP case, or for the 2SP case left to
* their default values, EPSG says that we shall use the latitude of origin as the SP.
*/
double φ1 = initializer.getAndStore(LambertConformal2SP.STANDARD_PARALLEL_1, φ0);
double φ2 = initializer.getAndStore(LambertConformal2SP.STANDARD_PARALLEL_2, φ1);
if (abs1 + φ2) < Formulas.ANGULAR_TOLERANCE) {
/*
* We can not allow that because if φ1 = -φ2, then n = 0 and the equations
* in this class break down with indetermination like 0 × ∞.
* The caller should use the Mercator projection instead for such cases.
*/
throw new IllegalArgumentException(Resources.format(Resources.Keys.LatitudesAreOpposite_2,
new Latitude1), new Latitude2)));
}
/*
* Whether to favorize precision in the North hemisphere (true) or in the South hemisphere (false).
* The IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015 uses the
* following formula:
*
* t = tan(π/4 – φ/2) / [(1 – ℯ⋅sinφ)/(1 + ℯ⋅sinφ)] ^ (ℯ/2)
*
* while our 'expΨ' function is defined like above, but with tan(π/4 + φ/2) instead of tan(π/4 - φ/2).
* Those two expressions are the reciprocal of each other if we reverse the sign of φ (see 'expΨ' for
* trigonometric identities), but their accuracies are not equivalent: the hemisphere having values
* closer to zero is favorized. The EPSG formulas favorize the North hemisphere.
*
* Since Apache SIS's formula uses the + operator instead of -, we need to reverse the sign of φ
* values in order to match the EPSG formulas, but we will do that only if the map projection is
* for the North hemisphere.
*
* TEST: whether 'isNorth' is true of false does not change the formulas "correctness": it is only
* a small accuracy improvement. One can safely force this boolean value to 'true' or 'false' for
* testing purpose.
*/
final boolean isNorth = isPositive0);
if (isNorth) {
φ0 = 0;
φ1 = 1;
φ2 = 2;
}
φ0 = toRadians0);
φ1 = toRadians1);
φ2 = toRadians2);
/*
* Compute constants. We do not need to use special formulas for the spherical case below,
* since rν(sinφ) = 1 and expΨ(φ) = tan(π/4 + φ/2) when the eccentricity is zero.
* However we need special formulas for φ1 ≈ φ2 in the calculation of n, otherwise we got
* a 0/0 indetermination.
*/
final double sinφ1 = sin1);
final double m1 = initializer.scaleAtφ(sinφ1, cos1));
final double t1 = expΨ(φ1, eccentricity*sinφ1);
/*
* Compute n = (ln m₁ – ln m₂) / (ln t₁ – ln t₂), which we rewrite as ln(m₁/m₂) / ln(t₁/t₂)
* for reducing the amount of calls to the logarithmic function. Note that this equation
* tends toward 0/0 if φ₁ ≈ φ₂, which force us to do a special check for the SP1 case.
*/
if (abs1 - φ2) >= ANGULAR_TOLERANCE) { // Should be 'true' for 2SP case.
final double sinφ2 = sin2);
final double m2 = initializer.scaleAtφ(sinφ2, cos2));
final double t2 = expΨ(φ2, eccentricity*sinφ2);
n = log(m1/m2) / log(t1/t2); // Tend toward 0/0 if φ1 ≈ φ2.
} else {
n = -sinφ1;
}
/*
* Compute F = m₁/(n⋅t₁ⁿ) from Geomatics Guidance Note number 7.
* Following constants will be stored in the denormalization matrix, to be applied
* after the non-linear formulas implemented by this LambertConicConformal class.
* Opportunistically use double-double arithmetic since the matrix coefficients will
* be stored in that format anyway. This makes a change in the 2 or 3 last digits.
*/
final DoubleDouble F = new DoubleDouble(n);
F.multiply(pow(t1, n));
F.inverseDivideGuessError(m1);
if (!isNorth) {
F.negate();
}
/*
* Compute the radius of the parallel of latitude of the false origin.
* This is related to the "ρ₀" term in Snyder. From EPG guide:
*
* r = a⋅F⋅tⁿ where (in our case) a=1 and t is our 'expΨ' function.
*
* EPSG uses this term in the computation of y = FN + rF – r⋅cos(θ).
*/
DoubleDouble rF = null;
if 0 != copySign(PI/2, -n)) { // For reducing the rounding error documented in expΨ(+π/2).
rF = new DoubleDouble(F);
rF.multiply(pow(expΨ(φ0, eccentricity*sin0)), n));
}
/*
* 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)
* - Multiply longitude by 'n'.
* - In the Belgium case only, subtract BELGE_A to the scaled longitude.
*
* Denormalization
* - Revert the sign of y (by negating the factor F).
* - Scale x and y by F.
* - Translate y by ρ0.
* - Multiply by the scale factor (done by the super-class constructor).
* - Add false easting and false northing (done by the super-class constructor).
*/
DoubleDouble sλ = new DoubleDouble(n);
DoubleDouble sφ = null;
if (isNorth) {
// Reverse the sign of either longitude or latitude values before map projection.
sφ = new DoubleDouble(-1d);
} else {
sλ.negate();
}
final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
normalize .convertAfter(0, sλ, (initializer.variant == BELGIUM) ? belgeA() : null);
normalize .convertAfter(1, sφ, null);
denormalize.convertBefore(0, F, null); F.negate();
denormalize.convertBefore(1, F, rF);
θ_bound = initializer.boundOfScaledLongitude(sλ);
}
/**
* Creates a new projection initialized to the same parameters than the given one.
*/
LambertConicConformal(final LambertConicConformal other) {
super(other);
n = other.n;
θ_bound = other_bound;
}
/**
* Returns the names of additional internal parameters which need to be taken in account when
* comparing two {@code LambertConicConformal} projections or formatting them in debug mode.
*/
@Override
final String[] getInternalParameterNames() {
return new String[] {"n"};
}
/**
* Returns the values of additional internal parameters which need to be taken in account when
* comparing two {@code LambertConicConformal} projections or formatting them in debug mode.
*/
@Override
final double[] getInternalParameterValues() {
return new double[] {n};
}
/**
* 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 {
LambertConicConformal 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
{
/*
* NOTE: If some equation terms seem missing, this is because the linear operations applied before
* the first non-linear one moved to the "normalize" affine transform, and the linear operations
* applied after the last non-linear one moved to the "denormalize" affine transform.
*/
final double θ = wraparoundScaledLongitude(srcPts[srcOff], θ_bound); // θ = Δλ⋅n
final double φ = srcPts[srcOff + 1]; // Sign may be reversed
final double absφ = abs(φ);
final double sinθ = sin(θ);
final double cosθ = cos(θ);
final double sinφ;
final double ρ; // EPSG guide uses "r", but we keep the symbol from Snyder p. 108 for consistency with PolarStereographic.
if (absφ < PI/2) {
sinφ = sin(φ);
ρ = pow(expΨ(φ, eccentricity*sinφ), n);
} else if (absφ < PI/2 + ANGULAR_TOLERANCE) {
sinφ = 1;
ρ = (φ*n >= 0) ? POSITIVE_INFINITY : 0;
} else {
ρ = sinφ = NaN;
}
final double x = ρ * sinθ;
final double y = ρ * cosθ;
if (dstPts != null) {
dstPts[dstOff ] = x;
dstPts[dstOff+1] = y;
}
if (!derivate) {
return null;
}
/*
* End of map projection. Now compute the derivative.
*/
final double dρ;
if (sinφ != 1) {
dρ = n * dy_dφ(sinφ, cos(φ)) * ρ;
} else {
dρ = ρ;
}
return new Matrix2(y, dρ*sinθ, // ∂x/∂λ , ∂x/∂φ
-x, 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: If some equation terms seem missing (e.g. "y = ρ0 - y"), this is because the linear operations
* applied before the first non-linear one moved to the inverse of the "denormalize" transform, and the
* linear operations applied after the last non-linear one moved to the inverse of the "normalize" transform.
*/
dstPts[dstOff ] = atan2(x, y); // Really (x,y), not (y,x)
dstPts[dstOff+1] = -φ(pow(hypot(x, y), 1/n)); // Equivalent to φ(pow(hypot(x,y), -1/n)) but more accurate for n>0.
}
/**
* Provides the transform equations for the spherical case of the Lambert Conformal projection.
*
* <div class="note"><b>Implementation note:</b>
* this class contains explicit checks for latitude values at poles.
* See the discussion in the {@link Mercator.Spherical} javadoc for an explanation.
* The following is specific to the Lambert Conformal projection.
*
* <p>Comparison of observed behavior at poles between the spherical and ellipsoidal cases,
* if no special checks are applied:</p>
*
* {@preformat text
* ┌───────┬──────────────────────────┬────────────────────────┐
* │ │ Spherical │ Ellipsoidal │
* ├───────┼──────────────────────────┼────────────────────────┤
* │ North │ Approximate (y = small) │ Exact answer (y = 0.0) │
* │ South │ Exact answer (y = +∞) │ Approximate (y = big) │
* └───────┴──────────────────────────┴────────────────────────┘
* }
* </div>
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @author André Gosselin (MPO)
* @author Rueben Schulz (UBC)
* @version 1.1
* @since 0.6
* @module
*/
static final class Spherical extends LambertConicConformal {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -8077690516096472987L;
/**
* 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 LambertConicConformal other) {
super(other);
}
/**
* {@inheritDoc}
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate)
{
final double θ = wraparoundScaledLongitude(srcPts[srcOff], θ_bound); // θ = Δλ⋅n
final double φ = srcPts[srcOff + 1]; // Sign may be reversed
final double absφ = abs(φ);
final double sinθ = sin(θ);
final double cosθ = cos(θ);
final double ρ; // EPSG guide uses "r", but we keep the symbol from Snyder p. 108 for consistency with PolarStereographic.
if (absφ < PI/2) {
ρ = pow(tan(PI/4 + 0.5*φ), n);
} else if (absφ < PI/2 + ANGULAR_TOLERANCE) {
ρ = (φ*n >= 0) ? POSITIVE_INFINITY : 0;
} else {
ρ = NaN;
}
final double x = ρ * sinθ;
final double y = ρ * cosθ;
if (dstPts != null) {
dstPts[dstOff ] = x;
dstPts[dstOff+1] = y;
}
if (!derivate) {
return null;
}
final double dρ;
if (absφ < PI/2) {
dρ = n / cos(φ);
} else {
dρ = NaN;
}
return new Matrix2(y, dρ*sinθ, // ∂x/∂λ , ∂x/∂φ
-x, dρ*cosθ); // ∂y/∂λ , ∂y/∂φ
}
/**
* {@inheritDoc}
*/
@Override
protected void inverseTransform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff)
{
double x = srcPts[srcOff ];
double y = srcPts[srcOff+1];
final double ρ = hypot(x, y);
dstPts[dstOff ] = atan2(x, y); // Really (x,y), not (y,x);
dstPts[dstOff+1] = PI/2 - 2*atan(pow(1/ρ, 1/n));
}
}
}