blob: 4093611f57057623d26609578ce1362b9156d121 [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 org.apache.sis.internal.referencing.Resources;
import static java.lang.Math.*;
/**
* Base class of {@link LambertConicConformal}, {@link Mercator} and {@link PolarStereographic} projections.
* Those projections have in common the property of being <cite>conformal</cite>, i.e. they preserve angles locally.
* However we do not put this base class in public API because not all conformal projections extend this base class.
* For example the {@link TransverseMercator} projection, despite being conformal, uses very different formulas.
*
* <p>Note that no projection can be both conformal and equal-area. This restriction is implemented in class
* hierarchy with {@link ConformalProjection} and {@link EqualAreaProjection} being two distinct classes.</p>
*
* <p>This base class can been seen as a generalization of <cite>Lambert Conic Conformal</cite> projection,
* which includes some other projections like Mercator and Polar Stereographic as special cases.
* For this base class, the Mercator projection is considered as <cite>"a special limiting case of the
* Lambert Conic Conformal map projection with the equator as the single standard parallel."</cite>
* (Source: §1.3.3 in IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015).
* Indeed, those two projections have some equation in commons which are provided in this base class.</p>
*
* <p>The Polar Stereographic projection is not documented as a special case of Lambert Conic Conformal,
* but the equations in the {@code PolarStereographic.transform(…)} and {@code inverseTransform(…)} methods
* appear to be the same with the <var>n</var> factor fixed to 1 or -1, so we leverage the code provided by
* this base class. This class hierarchy is only an implementation convenience and not part of public API.</p>
*
* <p>The Transverse Mercator projection is also conformal, but does not use the formulas provided in this class.
* It will instead compute the coefficients itself and use its own, more complex, formulas with those coefficients.
* However the formulas provided in this {@code ConformalProjection} class can be seen as a special case of
* Transverse Mercator formulas for <var>x</var> = 0.</p>
*
* <div class="note"><b>Reference:</b>
* “Lambert developed the regular Conformal Conic as the oblique aspect of a family containing the previously
* known polar Stereographic and regular Mercator projections. (…) If the standard parallels are symmetrical
* about the Equator, the regular Mercator results (although formulas must be revised). If the only standard
* parallel is a pole, the polar Stereographic results.” (Snyder, page 105)</div>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.6
* @module
*/
abstract class ConformalProjection extends NormalizedProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -292755890184138414L;
/**
* The threshold value of {@link #eccentricity} at which we consider the accuracy of the
* series expansion insufficient. This threshold is determined empirically with the help
* of the {@code MercatorMethodComparison} class in the test directory.
* We choose the value where:
*
* <ul>
* <li>the average error of series expansion become greater than {@link NormalizedProjection#ITERATION_TOLERANCE},</li>
* <li>the maximal error of series expansion become greater than {@link NormalizedProjection#ANGULAR_TOLERANCE}.</li>
* </ul>
*/
static final double ECCENTRICITY_THRESHOLD = 0.16;
/**
* {@code true} if the {@link #eccentricity} value is greater than or equals to {@link #ECCENTRICITY_THRESHOLD},
* in which case the {@link #φ(double)} method will need to use an iterative method.
*/
private final boolean useIterations;
/**
* Coefficients of the first terms in the series expansion of the inverse projection.
* Values of those coefficients depend only on {@linkplain #eccentricity eccentricity} value.
* The series expansion is published under the following form, where χ is the <cite>conformal latitude</cite>:
*
* <blockquote>c₂⋅sin(2χ) + c₄⋅sin(4χ) + c₆⋅sin(6χ) + c₈⋅sin(8χ)</blockquote>
*
* However we rewrite above series expansion for taking advantage of trigonometric identities.
* The equation become (with different <var>c</var> values):
*
* <blockquote>sin(2χ)⋅(c₂ + cos(2χ)⋅(c₄ + cos(2χ)⋅(c₆ + cos(2χ)⋅c₈)))</blockquote>
*
* <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 if a future version of the constructor uses the double-double values
* provided by {@link Initializer}.</div>
*/
private final double c2χ, c4χ, c6χ, c8χ;
/**
* Creates a new normalized projection from the parameters computed by the given initializer.
* This constructor computes the coefficients in the series expansion from the {@link #eccentricitySquared} value.
*
* @param initializer the initializer for computing map projection internal parameters.
*/
ConformalProjection(final Initializer initializer) {
super(initializer);
useIterations = (eccentricity >= ECCENTRICITY_THRESHOLD);
final double e2 = eccentricitySquared;
final double e4 = e2 * e2;
final double e6 = e2 * e4;
final double e8 = e4 * e4;
/*
* Published equation is of the following form:
*
* φ = χ + A⋅sin(2χ) + B⋅sin(4χ) + C⋅sin(6χ) + D⋅sin(8χ) (Snyder 3-34)
*
* We rewrite as:
*
* φ = χ + sin(2χ)⋅(A′ + cos(2χ)⋅(B′ + cos(2χ)⋅(C′ + cos(2χ)⋅D′))) (Snyder 3-35)
* A′ = A - C
* B′ = 2B - 4D
* C′ = 4C
* D′ = 8D
*
* Published coefficients are:
*
* A = 13/360 ⋅ℯ⁸ + 1/12 ⋅ℯ⁶ + 5/24⋅ℯ⁴ + ℯ²/2
* B = 811/11520 ⋅ℯ⁸ + 29/240⋅ℯ⁶ + 7/48⋅ℯ⁴
* C = 81/1120 ⋅ℯ⁸ + 7/120⋅ℯ⁶
* D = 4279/161280⋅ℯ⁸
*
* We replace A B C D by A′ B′ C′ D′ using Snyder 3-35. The result are coefficients below.
* For each line, we add the smallest values first in order to reduce rounding errors.
* The smallest values are the ones using the eccentricity raised to the highest power.
*/
c2χ = -73./ 2016 * e8 + 1./40 * e6 + 5./24 * e4 + 1./2 * e2;
c4χ = 233./ 6720 * e8 + 29./120 * e6 + 7./24 * e4;
c6χ = 81./ 280 * e8 + 7./30 * e6;
c8χ = 4279./20160 * e8;
}
/**
* Creates a new projection initialized to the values of the given one. This constructor may be invoked after
* we determined that the default implementation can be replaced by another one, for example using spherical
* formulas instead than the ellipsoidal ones. This constructor allows to transfer all parameters to the new
* instance without recomputing them.
*/
ConformalProjection(final ConformalProjection other) {
super(other);
useIterations = other.useIterations;
c2χ = other.c2χ;
c4χ = other.c4χ;
c6χ = other.c6χ;
c8χ = other.c8χ;
}
/**
* Computes the latitude for a value closely related to the <var>y</var> value of a Mercator projection.
* This formula is also part of other projections, since Mercator can be considered as a special case of
* Lambert Conic Conformal for instance.
*
* <p>This function is <em>almost</em> the converse of the {@link #expΨ(double, double)} function.
* In a Mercator inverse projection, the value of the {@code rexpΨ} argument is {@code exp(-Ψ)}.</p>
*
* <p>The input should be a positive number, otherwise the result will be either outside
* the [-π/2 … π/2] range, or will be NaN. Its behavior at some particular points is:</p>
*
* <ul>
* <li>φ(0) = π/2</li>
* <li>φ(1) = 0</li>
* <li>φ(∞) = -π/2.</li>
* </ul>
*
* <b>Note:</b> §1.3.3 in Geomatics Guidance Note number 7 part 2 (April 2015) uses a series expansion
* while USGS used an iterative method. The series expansion is twice faster than the iterative method
* for the same precision, but this precision is achieved "only" for relatively small eccentricity like
* the Earth's one. See the {@code MercatorMethodComparison} class in the test package for more discussion.
*
* @param rexpΨ the <em>reciprocal</em> of the value returned by {@link #expΨ}.
* @return the latitude in radians.
* @throws ProjectionException if the iteration does not converge.
*
* @see #expΨ(double, double)
* @see #dy_dφ(double, double)
*/
final double φ(final double rexpΨ) throws ProjectionException {
/*
* Get a first approximation of φ from Snyder (7-11). The result below would be exact if the
* ellipsoid was actually a sphere. But if the eccentricity is different than 0, then we will
* need to add a correction.
*
* Note that the φ value computed by the line below is called χ in EPSG guide.
* We name it φ in our code because we will modify that value in-place in order to get φ.
*/
double φ = (PI/2) - 2*atan(rexpΨ); // at this point == χ (conformal latitude).
/*
* Add a correction for the flattened shape of the Earth. The correction can be represented by an
* infinite series. Here, we apply only the first 4 terms. Those terms are given by §1.3.3 in the
* EPSG guidance note and modified by application of Snyder 3-35. We add those terms in reverse order,
* beginning with the smallest values, for reducing rounding errors due to IEEE 754 arithmetic.
*
* For the original formula as published by EPSG, see MercatorMethodComparison.bySeriesExpansion()
* in the test suite.
*/
final double sin_2φ = sin(2*φ);
final double cos_2φ = cos(2*φ);
φ += sin_2φ * (c2χ + cos_2φ * (c4χ + cos_2φ * (c6χ + cos_2φ * c8χ)));
/*
* Note: a previous version checked if the value of the smallest term c8χ⋅sin(8φ) was smaller than
* the iteration tolerance. But this was not reliable enough. We use now a hard coded threshold
* determined empirically by MercatorMethodComparison.
*/
if (!useIterations) {
return φ;
}
/*
* We should never reach this point for map projections on Earth. But if the ellipsoid is for some
* other planet having a high eccentricity, then the above series expansion may not be sufficient.
* Try to improve by iteratively solving equation (7-9) from Snyder. However instead than using
* Snyder (7-11) as the starting point, we take the result of above calculation as the initial φ.
* Assuming that it is closer to the real φ value, this save us some iteration loops and usually
* gives us more accurate results (according MercatorMethodComparison tests).
*/
final double h = 0.5 * eccentricity;
for (int it=0; it<MAXIMUM_ITERATIONS; it++) {
final double sinφ = eccentricity * sin(φ);
final double Δφ = φ - = PI/2 - 2*atan(rexpΨ * pow((1 - sinφ)/(1 + sinφ), hℯ)));
if (!(abs(Δφ) > ITERATION_TOLERANCE)) { // Use '!' for accepting NaN.
return φ;
}
}
throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
}
/**
* Computes <code>{@linkplain Math#exp(double) exp}(Ψ)</code> where Ψ is the isometric latitude.
* This is part of the Mercator projection for the given latitude. This formula is also part of
* Lambert Conic Conformal projection, since Mercator can be considered as a special case of that
* Lambert projection with the equator as the single standard parallel.
*
* <p>The isometric latitude is given by the {@linkplain Math#log(double) natural logarithm} of the value returned
* by this method. The <em>reciprocal</em> of this function {@code 1/expΨ(φ)} is the converse of {@link #φ(double)}.
* Isometric latitude Ψ is related to conformal latitude χ by {@literal χ(φ) = gd(Ψ(φ))} where {@literal gd(x)} is the
* Gudermannian function. There is many representations of that function, e.g. {@literal gd(Ψ) = 2⋅atan(expΨ) - π/2}.</p>
*
* <p>In IOGP Publication 373-7-2 – Geomatics Guidance Note number 7, part 2 – April 2015,
* a function closely related to this method has the letter <var>t</var>.</p>
*
*
* <h4>Properties</h4>
* This function is used with φ values in the [-π/2 … π/2] range and has a periodicity of 2π.
* The result is always a positive number when the φ argument is inside the above-cited range.
* If, after removal of any 2π periodicity, φ is still outside the [-π/2 … π/2] range, then the
* result is a negative number. In a Mercator projection, such negative number will result in NaN.
*
* <p>Some values are:</p>
* <ul>
* <li>expΨ(NaN) = NaN</li>
* <li>expΨ(±∞) = NaN</li>
* <li>expΨ(-π/2) = 0</li>
* <li>expΨ( 0 ) = 1</li>
* <li>expΨ(+π/2) → ∞ (actually some large value like 1.633E+16)</li>
* <li>expΨ(-φ) = 1 / expΨ(φ)</li>
* </ul>
*
*
* <h4>The π/2 special case</h4>
* The value at {@code Math.PI/2} is not exactly infinity because there is no exact representation of π/2.
* However since the conversion of 90° to radians gives {@code Math.PI/2}, we can presume that the user was
* expecting infinity. The caller should check for the PI/2 special case himself if desired, as this method
* does nothing special about it.
*
* <p>Note that the result for the φ value after {@code Math.PI/2} (as given by {@link Math#nextUp(double)})
* is still positive, maybe because {@literal PI/2 < π/2 < nextUp(PI/2)}. Only the {@code nextUp(nextUp(PI/2))}
* value become negative. Callers may need to take this behavior in account: special check for {@code Math.PI/2}
* is not sufficient, the check needs to include at least the {@code nextUp(Math.PI/2)} case.</p>
*
*
* <h4>Relationship with Snyder</h4>
* This function is related to the following functions from Snyder:
*
* <ul>
* <li>(7-7) in the <cite>Mercator projection</cite> chapter.</li>
* <li>Reciprocal of (9-13) in the <cite>Oblique Mercator projection</cite> chapter.</li>
* <li>Reciprocal of (15-9) in the <cite>Lambert Conformal Conic projection</cite> chapter.</li>
* </ul>
*
* @param φ the latitude in radians.
* @param ℯsinφ the sine of the φ argument multiplied by {@link #eccentricity}.
* @return {@code Math.exp(Ψ)} where Ψ is the isometric latitude.
*
* @see #φ(double)
* @see #dy_dφ(double, double)
* @see <a href="https://en.wikipedia.org/wiki/Latitude#Isometric_latitude">Isometric latitude on Wikipedia</a>
*/
final double expΨ(final double φ, final double sinφ) {
/*
* Note: tan(π/4 - φ/2) = 1 / tan(π/4 + φ/2)
*
* A + sign in the equation favorises slightly the accuracy in South hemisphere, while a - sign
* favorises slightly the North hemisphere (but the differences are very small). In Apache SIS,
* we handle that by changing the sign of some terms in the (de)normalisation matrices.
*/
return tan(PI/4 + 0.5*φ) * pow((1 - sinφ) / (1 + sinφ), 0.5*eccentricity);
}
/**
* Computes the partial derivative of a Mercator projection at the given latitude. This formula is also part of
* other projections, since Mercator can be considered as a special case of Lambert Conic Conformal for instance.
*
* <p>In order to get the derivative of the {@link #expΨ(double, double)} function, caller can multiply
* the returned value by {@code expΨ}.</p>
*
* @param sinφ the sine of latitude.
* @param cosφ the cosine of latitude.
* @return the partial derivative of a Mercator projection at the given latitude.
*
* @see #expΨ(double, double)
* @see #φ(double)
*/
final double dy_dφ(final double sinφ, final double cosφ) {
return (1 / cosφ) - eccentricitySquared * cosφ / (1 - eccentricitySquared * (sinφ*sinφ));
}
}