All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.geotoolkit.referencing.operation.projection.UnitaryProjection Maven / Gradle / Ivy

/*
 *    Geotoolkit.org - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 1999-2012, Open Source Geospatial Foundation (OSGeo)
 *    (C) 2009-2012, Geomatys
 *
 *    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;
 *    version 2.1 of the License.
 *
 *    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.
 *
 *    This package contains formulas from the PROJ package of USGS.
 *    USGS's work is fully acknowledged here. This derived work has
 *    been relicensed under LGPL with Frank Warmerdam's permission.
 */
package org.geotoolkit.referencing.operation.projection;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Level;
import java.awt.geom.AffineTransform;
import net.jcip.annotations.Immutable;

import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.util.GenericName;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;

import org.geotoolkit.resources.Loggings;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.measure.Latitude;
import org.geotoolkit.measure.Longitude;
import org.geotoolkit.util.XArrays;
import org.geotoolkit.util.Utilities;
import org.geotoolkit.util.Deprecable;
import org.geotoolkit.util.ComparisonMode;
import org.geotoolkit.util.logging.Logging;
import org.geotoolkit.util.collection.WeakHashSet;
import org.geotoolkit.referencing.operation.provider.UniversalParameters;
import org.geotoolkit.metadata.iso.citation.Citations;
import org.geotoolkit.referencing.IdentifiedObjects;
import org.geotoolkit.referencing.operation.matrix.Matrices;
import org.geotoolkit.referencing.operation.provider.MapProjection;
import org.geotoolkit.referencing.operation.transform.AbstractMathTransform2D;

import static java.lang.Math.*;
import static java.lang.Double.*;
import static org.geotoolkit.math.XMath.atanh;
import static org.geotoolkit.math.XMath.xorSign;
import static org.geotoolkit.util.Utilities.hash;
import static org.geotoolkit.internal.InternalUtilities.epsilonEqual;
import static org.geotoolkit.parameter.Parameters.getOrCreate;
import static org.geotoolkit.referencing.operation.provider.UniversalParameters.*;
import static org.geotoolkit.referencing.operation.provider.MapProjection.XY_PLANE_ROTATION;


/**
 * Base class for conversion services between ellipsoidal and cartographic projections.
 * This conversion works on a normalized spaces, where angles are express in radians and
 * computations are performed for a sphere having a semi-major axis of 1. More specifically:
 *
 * 
    *
  • On input, the {@link #transform(double[],int,double[],int,boolean) transform} method expects * (longitude, latitude) angles in radians. Longitudes * have the {@linkplain Parameters#centralMeridian central meridian} removed before the transform * method is invoked. The conversion from degrees to radians and the longitude rotation are applied * by the {@linkplain Parameters#normalize(boolean) normalize} affine transform.

  • * *
  • On output, the {@link #transform(double[],int,double[],int,boolean) transform} method returns * (easting, northing) values on a sphere or ellipse having a semi-major * axis length of 1. The multiplication by the scale factor and the false easting/northing offsets * are applied by the {@link Parameters#normalize(boolean) denormalize} affine transform.

  • *
* * {@code UnitaryProjection} does not expose publicly the above cited parameters (central meridian, * scale factor, etc.) on intend, in order to make clear that those parameters are not used * by subclasses. This separation removes ambiguity when testing for {@linkplain #equals(Object, * ComparisonMode) equivalence}. The ability to recognize two {@code UnitaryProjection}s as * equivalent without consideration for the scale factor (among other) allow more efficient * concatenation in some cases (typically some combinations of inverse projection followed * by a direct projection). *

* All angles (either fields, method parameters or return values) in this class and subclasses are * in radians. This is the opposite of {@link Parameters} where all angles are in decimal degrees. * * {@note Serialization of this class is appropriate for short-term storage or RMI use, but may * not be compatible with future versions. For long term storage, WKT (Well Know Text) or * XML are more appropriate.} * * @author Martin Desruisseaux (MPO, IRD, Geomatys) * @author André Gosselin (MPO) * @author Rueben Schulz (UBC) * @author Rémi Maréchal (Geomatys) * @version 3.20 * * @see Map projections on MathWorld * @see Map projections on the atlas of Canada * * @since 3.18 * @module */ @Immutable public abstract class UnitaryProjection extends AbstractMathTransform2D implements Serializable { /** * For cross-version compatibility. */ private static final long serialVersionUID = 1969740225939106310L; /** * Tolerance in the correctness of argument values provided to the mathematical functions * defined in this class. * * @since 3.18 */ private static final double ARGUMENT_TOLERANCE = 1E-15; /** * Maximum difference allowed when comparing longitudes or latitudes in radians. * A tolerance of 1E-6 is about 0.2 second of arcs, which is about 6 kilometers * (computed from the standard length of nautical mile). *

* Some formulas use this tolerance value for testing sinus or cosinus of an angle. * In the sinus case, this is justified because sin(θ) ≅ θ * when θ is small. Similar reasoning applies to cosinus with * cos(θ) ≅ θ + π/2 when θ is small. */ static final double ANGLE_TOLERANCE = 1E-6; /** * Difference allowed in iterative computations. A value of 1E-10 causes the * {@link #cphi2} function to compute the latitude at a precision of 1E-10 radians, * which is slightly smaller than one millimetre. */ static final double ITERATION_TOLERANCE = 1E-10; /** * Maximum number of iterations for iterative computations. */ static final int MAXIMUM_ITERATIONS = 15; /** * Maximum difference allowed when comparing real numbers (other cases). The value defined * here is consistent with the one that was used in {@link LambertAzimuthalEqualArea} for * the same purpose (not to be confused with the current {@code EPSILON} constant defined * in the above-mentioned class, which has been renamed), and the modified value used in * {@link AlbersEqualArea}. */ static final double EPSILON = 1E-7; /** * The pool of unitary projections created in this running JVM. */ private static final WeakHashSet POOL = WeakHashSet.newInstance(UnitaryProjection.class); /** * The parameters used for creating this projection. They are used for formatting Well * Known Text (WKT) and error messages. Subclasses shall not use the values defined in * this object for computation purpose, except at construction time. */ final Parameters parameters; /** * Ellipsoid excentricity, equal to sqrt({@linkplain #excentricitySquared}). * Value 0 means that the ellipsoid is spherical. */ protected final double excentricity; /** * The square of excentricity: e² = (a²-b²)/a² where * e is the {@linkplain #excentricity excentricity}, * a is the {@linkplain Parameters#semiMajor semi major} axis length and * b is the {@linkplain Parameters#semiMinor semi minor} axis length. */ protected final double excentricitySquared; /** * The inverse of this map projection. */ private final MathTransform2D inverse; /** * The absolute value of the minimal and maximal longitude value. This is usually either * infinity (no bounds check) or {@code PI}, but could also be a different value if a scale * has been applied on the normalize affine transform. */ private double longitudeBound = POSITIVE_INFINITY; /** * The value to subtract from the longitude before to apply a forward projection. This is * usually equal to the central meridian in radians, except if some scale has been applied * on the normalize affine transform. */ private double longitudeRotation = 0; /** * Constructs a new map projection from the supplied parameters. Subclass constructors * must invoke {@link #finish} when they have finished their work. * * @param parameters The parameters of the projection to be created. */ protected UnitaryProjection(final Parameters parameters) { this.parameters = parameters; ensureNonNull("parameters", parameters); final double a = parameters.semiMajor; final double b = parameters.semiMinor; excentricitySquared = 1.0 - (b*b) / (a*a); excentricity = sqrt(excentricitySquared); inverse = new Inverse(); } /** * Must be invoked by subclass constructors after they finished their work. Current * implementation computes the longitude bounds to be used by {@link #rollLongitude()}. */ protected final void finish() { double rotation = 0; double bound = POSITIVE_INFINITY; final Boolean p = parameters.rollLongitude; if ((p == null) ? (parameters.centralMeridian != 0) : p.booleanValue()) { final AffineTransform normalize = parameters.normalize(true); if (normalize.getShearX() == 0 && normalize.getShearY() == 0) { rotation = -normalize.getTranslateX(); bound = abs(normalize.getScaleX()) * 180; if (abs(bound - PI) <= ANGLE_TOLERANCE) { bound = PI; } } else { /* * Should not happen with most projections. If it happen anyway, just log a * warning. We do not consider this limitation as a fatal error since the * projection will still work fine in many cases. We set the source to the * subclass constructor since this is the place where the issue originate. */ Logging.log(UnitaryProjection.class, "finish", Loggings.format(Level.WARNING, Loggings.Keys.CANT_ROLL_LONGITUDE_$1, getClass())); } } longitudeBound = bound; longitudeRotation = rotation; } /** * Creates a chain of concatenated transforms from the normalize transform, * this unitary projection and the denormalize transform. This method tries * to recycle existing instances of {@code UnitaryProjection} if possible, so subclasses * should be careful to implement their {@link #hashCode()} and {@link #equals} methods. *

* This method is not public as a safety against user-defined subclasses which may not * implement the above methods correctly. User-defined implementations can use the following * code in their {@code create} method, which does the same without the recycling of existing * instances: * * {@preformat java * public static MyProjection create(...) { * Parameters parameters = new Parameters(...); * MyProjection projection = new MyProjection(parameters); * return parameters.createConcatenatedTransform(projection); * } * } * * Note that we do not cache the {@code ConcatenatedTransform} instance since it will be * {@code DefaultMathTransformFactory}'s job to do so. We cache only the internal parts * because the factory will not see them. * * @return The concatenation of (normalize – this unitary projection * – denormalize) transforms. */ final MathTransform2D createConcatenatedTransform() { return parameters.createConcatenatedTransform(POOL.unique(this)); } /** * Convenience method for throwing an exception in case of unknown parameter. * This is used by subclass constructors. */ static IllegalArgumentException unknownParameter(final Object parameter) { final String name; if (parameter instanceof IdentifiedObject) { name = ((IdentifiedObject) parameter).getName().getCode(); } else { name = String.valueOf(parameter); } return new IllegalArgumentException(Errors.format(Errors.Keys.UNKNOWN_PARAMETER_$1, name)); } /** * Returns {@code true} if {@link #rollLongitude(double)} needs to be invoked. * This is used for optimizations only. */ final boolean rollLongitude() { return longitudeBound != POSITIVE_INFINITY; } /** * Ensures that the specified longitude stay within its valid range. The longitude bounds * are typically, but not always, ±π radians. This method returns x * unchanged if no longitude rolling should be applied. *

* This method should be invoked before the {@linkplain #transform(double[],int,double[],int,boolean) * forward transform} begin its calculation. * * @param x The longitude to roll. * @return The rolled longitude. * * @see MapProjection#ROLL_LONGITUDE */ protected final double rollLongitude(double x) { final double mx = longitudeBound; if (mx != POSITIVE_INFINITY) { x = rollLongitude(x, mx); } return x; } /** * Ensures that the given longitude added to the {@linkplain Parameters#centralMeridian * central meridian} will stay in the [-180 … 180]° range. This method returns * x unchanged if no longitude rolling should be applied. *

* This method should be invoked after the {@linkplain #inverseTransform inverse transform} * finished its calculation. * * @param x The longitude to unroll. * @return The unrolled longitude. * * @see #rollLongitude(double) */ protected final double unrollLongitude(double x) { final double r = longitudeRotation; if (r != 0) { final double mx = longitudeBound; if (xorSign(x + r, r) > mx) { x -= 2*copySign(mx, r); } } return x; } /** * Converts the coordinate in {@code srcPts} at the given offset and stores the result * in {@code dstPts} at the given offset. In addition, opportunistically computes the * transform derivative if requested. *

* The input ordinates are (λ,φ) (the variable names for * longitude and latitude respectively) angles in radians, usually * (but not always) in the range [-π..π] and [-π/2..π/2] respectively. However * values outside those ranges are accepted on the assumption that most implementations use * those values only in trigonometric functions like {@linkplain Math#sin sin} and * {@linkplain Math#cos cos}. If this assumption is not applicable to a particular subclass, * then it is implementor's responsibility to check the range. *

* Input coordinate shall have the {@linkplain Parameters#centralMeridian central meridian} * removed from the longitude before this method is invoked. After this method is invoked, * the output coordinate shall be multiplied by the global scale factor and the ({@linkplain * Parameters#falseEasting false easting}, {@linkplain Parameters#falseNorthing false northing}) * offset shall be applied. This means that projections that implement this method are performed * on a sphere or ellipse having a semi-major axis length of 1. *

* In PROJ.4, the same standardization, * described above, is handled by {@code pj_fwd.c}. Therefore when porting projections * from PROJ.4, the forward transform equations can be used directly here with minimal * change. * * @param srcPts The array containing the source point coordinate, as (longitude, * latitude) angles in radians. * @param srcOff The offset of the point to be converted in the source array. * @param dstPts The array into which the converted point coordinate is returned (may be * the same than {@code srcPts}). Ordinates will be expressed in a dimensionless * unit, as a linear distance on a unit sphere or ellipse. * @param dstOff The offset of the location of the converted point that is * stored in the destination array. * @param derivate {@code true} for computing the derivative, or {@code false} if not needed. * @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 point can't be converted. * * @since 3.20 (derived from 3.00) */ @Override public abstract Matrix transform(double[] srcPts, int srcOff, double[] dstPts, int dstOff, boolean derivate) throws ProjectionException; /** * Converts the coordinate in {@code srcPts} at the given offset and stores the result * in {@code ptDst} at the given offset. The output ordinates are (longitude, * latitude) angles in radians, usually in the range [-π..π] and * [-π/2..π/2] respectively. *

* Input coordinate shall have the ({@linkplain Parameters#falseEasting false easting}, * {@linkplain Parameters#falseNorthing false northing}) removed and the result divided * by the global scale factor before this method is invoked. After this method is invoked, * the output coordinate shall have the {@linkplain Parameters#centralMeridian central meridian} * added to the longitude in {@code ptDst}. This means that projections that implement this * method are performed on a sphere or ellipse having a semi-major axis of 1. *

* In PROJ.4, the same standardization, * described above, is handled by {@code pj_inv.c}. Therefore when porting projections * from PROJ.4, the inverse transform equations can be used directly here with minimal * change. * * @param srcPts The array containing the source point coordinate, as linear distance * on a unit sphere or ellipse. * @param srcOff The offset of the point to be converted in the source array. * @param dstPts the array into which the converted point coordinate is returned (may be * the same than {@code srcPts}). Ordinates will be (longitude, * latitude) angles in radians. * @param dstOff The offset of the location of the converted point that is * stored in the destination array. * @throws ProjectionException if the point can't be converted. */ protected abstract void inverseTransform(double[] srcPts, int srcOff, double[] dstPts, int dstOff) throws ProjectionException; /* * Note for subclasses: do NOT invoke Assertions.checkReciprocal in implementations of this * method. Doing so would introduce a never-ending loop. This assertion is already performed * by the Inverse inner class. */ /** * Returns the inverse of this map projection. */ @Override public MathTransform2D inverse() { return inverse; } /** * Inverse of a normalized map projection. * * @author Martin Desruisseaux (MPO, IRD) * @version 3.00 * * @since 3.00 * @module */ private final class Inverse extends AbstractMathTransform2D.Inverse { /** * For cross-version compatibility. */ private static final long serialVersionUID = -9138242780765956870L; /** * Default constructor. */ public Inverse() { UnitaryProjection.this.super(); } /** * Inverse transforms the specified {@code srcPts} and stores the result in {@code dstPts}. * If the derivative has been requested, then this method will delegate the derivative * calculation to the enclosing class and inverts the resulting matrix. * * @since 3.20 (derived from 3.00) */ @Override public Matrix transform(final double[] srcPts, final int srcOff, final double[] dstPts, final int dstOff, final boolean derivate) throws TransformException { inverseTransform(srcPts, srcOff, dstPts, dstOff); if (derivate) { return Matrices.invert(UnitaryProjection.this.transform(dstPts, dstOff, null, 0, true)); } return null; } } /** * @deprecated Not supported anymore. * * @since 2.5 */ @Deprecated public static synchronized void resetWarnings() { } ////////////////////////////////////////////////////////////////////////////////////////// //////// //////// //////// FORMULAS FROM SNYDER //////// //////// //////// ////////////////////////////////////////////////////////////////////////////////////////// /** * Computes function f(s,c,e²) = c/sqrt(1 - s²×e²) needed for the true scale * latitude (Snyder 14-15), where s and c are the sine and cosine of * the true scale latitude, and is the {@linkplain #excentricitySquared * eccentricity squared}. *

* Special cases: *

    *
  • If φ is 0°, then this method returns 1.
  • *
  • If φ is ±90°, then this method returns 0 provided that we are * not in the spherical case (otherwise we get {@link Double#NaN}).
  • *
* * @param sinφ The sine of the φ latitude in radians. * @param cosφ The cosine of the φ latitude in radians. */ final double msfn(final double sinφ, final double cosφ) { assert !(abs(sinφ*sinφ + cosφ*cosφ - 1) > ARGUMENT_TOLERANCE); return cosφ / sqrt(1.0 - (sinφ*sinφ) * excentricitySquared); } /** * Computes the derivative of this {@link #msfn(double, double)} method divided by {@code msfn}. * Callers must multiply the return value by {@code msfn} in order to get the actual value. * * @param sinφ The sinus of latitude. * @param cosφ The cosine of latitude. * @param msfn The value of {@code msfn(sinφ, cosφ)}. * @return The {@code msfn} derivative at the specified latitude. * * @since 3.19 */ final double dmsfn_dφ(final double sinφ, final double cosφ, double msfn) { msfn *= excentricity; return (sinφ/cosφ) * (msfn - 1) * (msfn + 1); } /** * Computes part of function (3-1) from Snyder. This is numerically equivalent to * {@linkplain #tsfn tsfn}(-φ, sinφ), but is defined as a separated * function for clarity and because the function properties are not the same. * * @param φ The latitude in radians. * @param sinφ The sine of the φ argument. This is provided explicitly * because in many cases, the caller has already computed this value. */ final double ssfn(double φ, double sinφ) { assert !(abs(sinφ - sin(φ)) > ARGUMENT_TOLERANCE) : φ; sinφ *= excentricity; return tan(PI/4 + 0.5*φ) * pow((1-sinφ) / (1+sinφ), 0.5*excentricity); } /** * Computes the derivative of the {@link #ssfn(double, double)} method divided by {@code ssfn}. * Callers must multiply the return value by {@code ssfn} in order to get the actual value. * * @param φ The latitude. * @param sinφ the sine of latitude. * @param cosφ The cosine of latitude. * @return The {@code dssfn} derivative at the specified latitude. * * @since 3.18 */ final double dssfn_dφ(final double φ, final double sinφ, final double cosφ) { assert !(abs(sinφ - sin(φ)) > ARGUMENT_TOLERANCE) : φ; assert !(abs(cosφ - cos(φ)) > ARGUMENT_TOLERANCE) : φ; return (1/cosφ) - (excentricitySquared*cosφ)/(1-excentricitySquared*sinφ*sinφ); } /** * Computes functions (15-9) and (9-13) from Snyder. This is equivalent to * the negative of function (7-7) and is the converse of {@link #cphi2}. *

* This function has a periodicity of 2π. The result is always a positive value when * φ is valid (more on it below). More specifically its behavior at some * particular points is: *

*

    *
  • If φ is NaN or infinite, then the result is NaN.
  • *
  • If φ is π/2, then the result is close to 0.
  • *
  • If φ is 0, then the result is close to 1.
  • *
  • If φ is -π/2, then the result tends toward positive infinity. * The actual result is not infinity however, but some large value like 1E+10.
  • *
  • If φ, after removal of any 2π periodicity, still outside the [-π/2 ... π/2] * range, then the result is a negative number. If the caller is going to compute the * logarithm of the returned value as in the Mercator projection, he will get NaN.
  • *
* * {@note ssfn(φ, sinφ) which is part of function (3-1) * from Snyder, is equivalent to tsfn(-φ, sinφ).} * * @param φ The latitude in radians. * @param sinφ The sine of the φ argument. This is provided explicitly * because in many cases, the caller has already computed this value. * * @return The negative of function 7-7 from Snyder. In the case of Mercator projection, * this is {@code exp(-y)} where y is the northing on the unit ellipse. */ final double tsfn(final double φ, double sinφ) { assert !(abs(sinφ - sin(φ)) > ARGUMENT_TOLERANCE) : φ; sinφ *= excentricity; return tan(PI/4 - 0.5*φ) / pow((1-sinφ) / (1+sinφ), 0.5*excentricity); } /** * Gets the derivative of the {@link #tsfn(double, double)} method divided by {@code tsfn}. * Callers must multiply the return value by {@code tsfn} in order to get the actual value. * * @param φ The latitude. * @param sinφ the sine of latitude. * @param cosφ The cosine of latitude. * @return The {@code tsfn} derivative at the specified latitude. * * @since 3.18 */ final double dtsfn_dφ(final double φ, final double sinφ, final double cosφ) { assert !(abs(sinφ - sin(φ)) > ARGUMENT_TOLERANCE) : φ; assert !(abs(cosφ - cos(φ)) > ARGUMENT_TOLERANCE) : φ; final double t = (1 - sinφ) / cosφ; return (excentricitySquared*cosφ / (1 - excentricitySquared*sinφ*sinφ) - 0.5*(t + 1/t)); } /** * Iteratively solve equation (7-9) from Snyder. This is the converse of {@link #tsfn}. * 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: *

*

    *
  • If {@code ts} is zero, then the result is close to π/2.
  • *
  • If {@code ts} is 1, then the result is close to zero.
  • *
  • If {@code ts} is positive infinity, then the result is close to -π/2.
  • *
* * @param ts The value returned by {@link #tsfn}. * @return The latitude in radians. * @throws ProjectionException if the iteration does not converge. */ final double cphi2(final double ts) throws ProjectionException { final double he = 0.5 * excentricity; double φ = (PI/2) - 2.0 * atan(ts); for (int i=0; iq, Snyder equation (3-12). * This equation has the following properties: *
    *
  • Input in the [-1 ... +1] range.
  • *
  • Output in the [-2 ... +2] range.
  • *
  • Output is 0 when input is 0.
  • *
  • Output of the same sign than input.
  • *
  • {@code qsfn(-sinφ) == -qsfn(sinφ)}.
  • *
* * @param sinφ Sine of the latitude q is calculated for. * @return q from Snyder equation (3-12). */ final double qsfn(final double sinφ) { if (excentricity < EPSILON) { return 2 * sinφ; } /* * Above check was required because the expression below would simplify to * sinφ - 0.5/0*log(1) where the right terms are infinity multiplied by * zero, thus producing NaN. */ final double esinφ = excentricity * sinφ; return (1 - excentricitySquared) * (sinφ / (1 - esinφ*esinφ) + atanh(esinφ)/excentricity); } /** * Gets the derivative of the {@link #qsfn(double)} method. * * @param sinφ The sine of latitude. * @param cosφ The cosines of latitude. * @return The {@code qsfn} derivative at the specified latitude. * * @since 3.18 */ final double dqsfn_dφ(final double sinφ, final double cosφ) { assert !(abs(sinφ*sinφ + cosφ*cosφ - 1) > ARGUMENT_TOLERANCE); double esinφ2 = excentricity * sinφ; esinφ2 *= esinφ2; return (1 - excentricitySquared) * (cosφ / (1 - esinφ2)) * (1 + ((1 + esinφ2) / (1 - esinφ2))); } ////////////////////////////////////////////////////////////////////////////////////////// //////// //////// //////// PARAMETERS //////// //////// //////// ////////////////////////////////////////////////////////////////////////////////////////// /** * Parameters that determine the affine transforms to be applied before and after the * {@link UnitaryProjection}. Most of those parameters are not used by the unitary * projection itself, but a few ones like the {@linkplain #standardParallels standard * parallels} may be on a case-by-case basis. *

* The lifecycle of this object is as below: * *

    *
  1. {@linkplain MapProjection#createMathTransform MapProjection.createMathTransform} * invokes the static {@code create} method of some appropriate {@link UnitaryProjection} * subclass. For example both {@link org.geotoolkit.referencing.operation.provider.Mercator1SP} * and {@link org.geotoolkit.referencing.operation.provider.Mercator2SP} invokes the same * {@link Mercator#create Mercator.create} method, but with different descriptor.

  2. * *
  3. The static factory method creates a new instance of this {@code Parameters} class * with the user-supplied {@link ParameterValueGroup}. This have the effect of decoding the * parameters in the given group and store their values in the corresponding {@code Parameters} * fields. The parameters are then given to the class constructor.

  4. * *
  5. The constructor at step 2 is free to modify the value of any field contained in * the {@code Parameters} instance it got in argument. The most typical use cases are to * multiply the {@linkplain #scaleFactor scale factor} by some value inferred from the * {@linkplain #standardParallels standard parallels}, and to restrict the {@linkplain * #centralMeridian central meridian} to the middle of a UTM or MTM zone.

  6. * *
  7. When every fields have their final values, the constructor at steps 2-3 must invoke * {@link #validate()}. At this point, the scalar values in {@code Parameters} should not be * modified anymore. However the constructor is free to apply additional operations on the * two affine transforms ({@linkplain #normalize(boolean) normalize/denormalize}) after * {@code validate()} has been invoked.

  8. * *
  9. Once the execution point returned to {@link MapProjection}, the affine transforms * are marked as immutable and concatenated in that order: * {@linkplain #normalize(boolean) normalize(true)}, * <the transform created at step 2>, * {@linkplain #normalize(boolean) normalize(false)}. * The {@code Parameters} instance is saved for Well Known Text formatting, * but is not used in the transformation chain.

  10. *
* * All angles in this class (either fields, method parameters or return values) are in decimal * degrees. This is the opposite of {@link UnitaryProjection} where all angles are in radians. * * {@note Serialization of this class is appropriate for short-term storage or RMI use, but may * not be compatible with future versions. For long term storage, WKT (Well Know Text) or * XML are more appropriate.} * * @author Martin Desruisseaux (Geomatys) * @version 3.00 * * @since 3.00 * @module */ protected static class Parameters extends AbstractMathTransform2D.Parameters { /** * For cross-version compatibility. */ private static final long serialVersionUID = -4952134260969915530L; /** * Namespaces of identifiers to ignore because they are ambiguous. For example the * Proj.4 {@code "lcc"} projection name could be both Lambert Conformal 1SP * or Lambert Conformal 2SP, so we can not use the Proj.4 code as a criterion. * * @since 3.20 */ private static final Citation[] AMBIGUOUS = new Citation[] { Citations.GEOTOOLKIT, Citations.PROJ4 }; /** * Length of semi-major axis, in metres. This is named a or R * (Radius in spherical cases) in Snyder. * * @see MapProjection#SEMI_MAJOR * @see UnitaryProjection#excentricity */ public final double semiMajor; /** * Length of semi-minor axis, in metres. This is named b in Snyder. * * @see MapProjection#SEMI_MINOR * @see UnitaryProjection#excentricity */ public final double semiMinor; /** * Whatever the projection should roll longitude. If {@code true}, then the value of * (longitude - {@linkplain #centralMeridian central meridian}) will be * rolled to the [-180 … 180]° range before the projection is applied. *

* This parameter may be {@code null} if the user didn't set it explicitly. */ final Boolean rollLongitude; /** * Central longitude in degrees. Default value is 0, the Greenwich meridian. * This is named λ0 in Snyder. */ public double centralMeridian; /** * Latitude of origin in degrees. Default value is 0, the equator. * This is named phi0 in Snyder. */ public double latitudeOfOrigin; /** * The standard parallels, or an empty array if there is none. * There is typically no more than 2 standard parallels. */ public final double[] standardParallels; /** * The azimuth of the central line passing through the centre of the projection, * in degrees. This is 0° for most projections. */ public double azimuth; /** * The scale factor. Default value is 1. This is named k in Snyder. */ public double scaleFactor; /** * False easting, in metres. Default value is 0. */ public double falseEasting; /** * False northing, in metres. Default value is 0. */ public double falseNorthing; /** * The ESRI-specific parameters, or {@code null} if none. Those parameters * are {@code "X_Scale"}, {@code "Y_Scale"} and {@code "XY_Plane_Rotation"}. * We stores those values as an array for saving space in the common case * where no value is given. */ private final double[] xyScaleAndRotation; /** * Creates parameters initialized to values extracted from the given parameter group. * The following parameters are recognized: *

*

    *
  • {@code "semi_major"} (mandatory)
  • *
  • {@code "semi_minor"} (mandatory)
  • *
  • {@code "central_meridian"} (default to 0°)
  • *
  • {@code "latitude_of_origin"} (default to 0°)
  • *
  • {@code "standard_parallel_1"} (default to none)
  • *
  • {@code "standard_parallel_2"} (default to none)
  • *
  • {@code "azimuth"} (default to 0°)
  • *
  • {@code "scale_factor"} (default to 1)
  • *
  • {@code "false_easting"} (default to 0)
  • *
  • {@code "false_northing"} (default to 0)
  • *
*

* Constructors of {@link UnitaryProjection} subclasses must invoke the {@link #validate()} * method when all parameters (especially the {@linkplain #centralMeridian central meridian} * and {@linkplain #scaleFactor scale factor}) are assigned their final value. * * @param descriptor The descriptor of parameters that are legal for the projection being * constructed. In theory it should be the same than {@code values.descriptors()}, * but projection providers should give explicitly the expected descriptors for safety. * @param values The parameter values in standard units. * @throws ParameterNotFoundException if a mandatory parameter is missing. */ public Parameters(final ParameterDescriptorGroup descriptor, final ParameterValueGroup values) throws ParameterNotFoundException { super(descriptor); ensureNonNull("values", values); final Collection expected = descriptor.descriptors(); final double standardParallel1, standardParallel2, xScale, yScale, xyPlaneRotation; semiMajor = org.geotoolkit.parameter.Parameters.doubleValue(SEMI_MAJOR, values); semiMinor = org.geotoolkit.parameter.Parameters.doubleValue(SEMI_MINOR, values); rollLongitude = org.geotoolkit.parameter.Parameters.value(ROLL_LONGITUDE, values); centralMeridian = doubleValue(expected, CENTRAL_MERIDIAN, values); latitudeOfOrigin = doubleValue(expected, LATITUDE_OF_ORIGIN, values); standardParallel1 = doubleValue(expected, STANDARD_PARALLEL_1, values); standardParallel2 = doubleValue(expected, STANDARD_PARALLEL_2, values); azimuth = doubleValue(expected, AZIMUTH, values); scaleFactor = doubleValue(expected, SCALE_FACTOR, values); falseEasting = doubleValue(expected, FALSE_EASTING, values); falseNorthing = doubleValue(expected, FALSE_NORTHING, values); xScale = doubleValue(expected, X_SCALE, values); yScale = doubleValue(expected, Y_SCALE, values); xyPlaneRotation = doubleValue(expected, XY_PLANE_ROTATION, values); switch ((isNaN(standardParallel1) ? 0 : 1) | (isNaN(standardParallel2) ? 0 : 2)) { case 0: standardParallels = XArrays.EMPTY_DOUBLE; break; case 1: standardParallels = new double[] {standardParallel1}; break; case 2: standardParallels = new double[] {standardParallel2}; break; case 3: standardParallels = new double[] {standardParallel1, standardParallel2}; break; default: throw new AssertionError(); } if (xScale != 1 || yScale != 1 || xyPlaneRotation != 0) { xyScaleAndRotation = new double[] {xScale, yScale, xyPlaneRotation}; } else { xyScaleAndRotation = null; } } /** * Returns {@code true} if at least one identifier of the {@linkplain #descriptor} * matches the name of the given descriptor. The {@code reference} arguments must * be the {@code PARAMETERS} constant of one of Geotk provider implementations. * This method is not public because it make assumptions about the way those * {@code PARAMETERS} constants are constructed. */ final boolean nameMatches(final ParameterDescriptorGroup reference) { final ParameterDescriptorGroup descriptor = getParameterDescriptors(); for (final GenericName name : reference.getAlias()) { if (name instanceof Identifier) { final Identifier identifier = (Identifier) name; if (!XArrays.containsIdentity(AMBIGUOUS, identifier.getAuthority()) && IdentifiedObjects.nameMatches(descriptor, identifier.getCode())) { if (identifier instanceof Deprecable && ((Deprecable) identifier).isDeprecated()) { /* * The name matches, but is a deprecated. This case occurs with: * * - Equidistant Cylindrical (Spherical) * - Lambert Azimuthal Equal Area (Spherical) * * which are defined twice by EPSG with different parameter names. * One of the definition is deprecated and needs to be ignored, in * order to allow the referencing module to pickup the correct one. */ continue; } return true; } } } return false; } /** * Returns {@code true} if the parameters have equal semi-major and semi-minor axis. * * @return {@code true} if the parameters have equal semi-major and semi-minor axis. */ final boolean isSpherical() { return semiMajor == semiMinor; } /** * Ensures that the parameters have equal semi-major and semi-minor axis. This method * is invoked by constructors of classes implementing only spherical formulas. * * @throws IllegalArgumentException If the {@linkplain #semiMajor semi-major} axis length * is not equal to the {@linkplain #semiMinor semi-minor} axis length. */ final void ensureSpherical() throws IllegalArgumentException { if (!isSpherical()) { throw new IllegalArgumentException(Errors.format(Errors.Keys.ELLIPTICAL_NOT_SUPPORTED)); } } /** * Sets the {@linkplain #normalize(boolean) normalize/denormalize} affine transforms to * the values contained in this object. They are the transforms to be applied before and * after {@link UnitaryProjection} respectively. *

* The default implementation defines those affine transforms as if they were performing * the following steps, in that order: *

    *
  • Normalize:

      *
    1. Remove the {@linkplain #centralMeridian central meridian} value from the longitude.
    2. *
    3. Convert the ordinates from degrees to radians.
    4. *

  • *
  • Denormalize:

      *
    1. Multiply the ordinates by the {@linkplain #semiMajor semi-major} axis length.
    2. *
    3. Multiply again the ordinates by the {@linkplain #scaleFactor scale factor}.
    4. *
    5. Add the ({@linkplain #falseEasting false easting}, * {@linkplain #falseNorthing false northing}) offsets.
    6. *

  • *
*

* The normalize/denormalize affine are usually identity transforms before this method is * invoked. If they were not, the above operations will be concatenated to their current * state. * * @throws IllegalArgumentException if a field has an illegal value. */ public void validate() throws IllegalArgumentException { ensureLongitudeInRange(CENTRAL_MERIDIAN, centralMeridian, true); ensureLatitudeInRange (LATITUDE_OF_ORIGIN, latitudeOfOrigin, true); final AffineTransform normalize = normalize(true); normalize.scale(PI/180, PI/180); if (centralMeridian != 0) { /* * In theory the above test is useless because -0 == 0. However Java has a notion * of negative zero, and in this case we want to avoid this negative zero because * we don't want it to be appears in WKT formatting of matrix elements (negative * zero is considered different than the default value because the comparison is * done bitwise). */ normalize.translate(-centralMeridian, 0); } final AffineTransform denormalize = normalize(false); final double globalScale = scaleFactor * semiMajor; denormalize.translate(falseEasting, falseNorthing); denormalize.scale(globalScale, globalScale); /* * If there is a rotation, apply it before the false (easting,northing) translation. * If we applied the rotation after the translation, the later would not be toward * (east,north) anymore. In addition, this is consistent with the order of operation * performed by ObliqueMercator, which has an ESRI "XY_Plane_Rotation" parameter * similar to the one involved here. */ if (xyScaleAndRotation != null) { final double r = xyScaleAndRotation[2]; final double qr = r / 90; final int qri = (int) qr; if (qr == qri) { denormalize.quadrantRotate(qri); } else { denormalize.rotate(toRadians(r)); } denormalize.scale(xyScaleAndRotation[0], xyScaleAndRotation[1]); } } /** * Returns a group of parameters initialized to the values contained in this object. * Changes to the returned parameters will not affect this object. */ @Override @SuppressWarnings("fallthrough") public ParameterValueGroup getParameterValues() { final ParameterDescriptorGroup descriptor = getParameterDescriptors(); final ParameterValueGroup values = descriptor.createValue(); final Collection expected = descriptor.descriptors(); getOrCreate(SEMI_MAJOR, values).setValue(semiMajor); getOrCreate(SEMI_MINOR, values).setValue(semiMinor); if (rollLongitude != null) { getOrCreate(ROLL_LONGITUDE, values).setValue(rollLongitude); } set(expected, AZIMUTH, values, azimuth ); set(expected, CENTRAL_MERIDIAN, values, centralMeridian ); set(expected, LATITUDE_OF_ORIGIN, values, latitudeOfOrigin); set(expected, SCALE_FACTOR, values, scaleFactor ); set(expected, FALSE_EASTING, values, falseEasting ); set(expected, FALSE_NORTHING, values, falseNorthing ); switch (standardParallels.length) { default: // Fall through in all cases case 2: set(expected, STANDARD_PARALLEL_2, values, standardParallels[1]); case 1: set(expected, STANDARD_PARALLEL_1, values, standardParallels[0]); case 0: break; } if (xyScaleAndRotation != null) { set(expected, X_SCALE, values, xyScaleAndRotation[0]); set(expected, Y_SCALE, values, xyScaleAndRotation[1]); set(expected, XY_PLANE_ROTATION, values, xyScaleAndRotation[2]); } return values; } /** * Returns a hash code value for this object. This value is * implementation-dependent and may change in any future version. */ @Override public int hashCode() { return Utilities.hash(semiMajor, Utilities.hash(semiMinor, Utilities.hash(centralMeridian, Utilities.hash(latitudeOfOrigin, Utilities.hash(scaleFactor, Utilities.hash(falseEasting, Utilities.hash(falseNorthing, Arrays.hashCode(standardParallels)))))))); } /** * Compares the given object with the parameters for equality. * * @param object The object to compare with the parameters. * @return {@code true} if the given object is equal to this one. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (super.equals(object)) { final Parameters that = (Parameters) object; return Utilities.equals(semiMajor, that.semiMajor) && Utilities.equals(semiMinor, that.semiMinor) && Utilities.equals(rollLongitude, that.rollLongitude) && Utilities.equals(centralMeridian, that.centralMeridian) && Utilities.equals(latitudeOfOrigin, that.latitudeOfOrigin) && Arrays.equals(standardParallels, that.standardParallels) && Utilities.equals(azimuth, that.azimuth) && Utilities.equals(scaleFactor, that.scaleFactor) && Utilities.equals(falseEasting, that.falseEasting) && Utilities.equals(falseNorthing, that.falseNorthing) && Arrays.equals(xyScaleAndRotation, that.xyScaleAndRotation); } return false; } /** * Returns the parameter value for the specified operation parameter. Values are automatically * converted into the standard units specified by the supplied {@code param} argument. * * @param expected The value returned by {@code descriptor.descriptors()}. * @param param The parameter to look for. * @param group The parameter value group to search into. * @return The requested parameter value, or {@code NaN} if {@code param} is * {@linkplain MathTransformProvider#createOptionalDescriptor optional} * and the user didn't provided any value. * @throws ParameterNotFoundException if the parameter is not found. */ static double doubleValue(final Collection expected, final ParameterDescriptor param, final ParameterValueGroup group) throws ParameterNotFoundException { if (param instanceof UniversalParameters) { final ParameterDescriptor descriptor = ((UniversalParameters) param).find(expected); if (descriptor != null) { final double value = org.geotoolkit.parameter.Parameters.doubleValue(descriptor, group); if (!isNaN(value)) { return value; } } } else if (expected.contains(param)) { /* * The line above search for exactly the given descriptor, not a descriptor having * the same name. This strictness is needed in order to avoid confusing a specific * descriptor like XY_PLANE_ROTATION (which is not a standard parameter) with some * "real" projection parameter having the same alias, like RECTIFIED_GRID_ANGLE. */ final double value = org.geotoolkit.parameter.Parameters.doubleValue(param, group); if (!isNaN(value)) { return value; } } /* * The constructor asked for a parameter value that do not apply to the type of the * projection to be created. Returns a default value common to all projection types, * but this value should not be used in projection computations. */ final Object value = param.getDefaultValue(); return (value instanceof Number) ? ((Number) value).doubleValue() : Double.NaN; } /** * Ensures that the given longitude is within allowed limits (±180°). This method * is used for checking the validity of projection parameters like {@link #centralMeridian}. * * @param x Longitude to verify, in degrees. * @param edge {@code true} for accepting longitudes of exactly ±180°. * @throws IllegalArgumentException if the longitude is out of range. */ static void ensureLongitudeInRange(final ParameterDescriptor name, final double x, final boolean edge) throws IllegalArgumentException { if (edge ? (x >= Longitude.MIN_VALUE && x <= Longitude.MAX_VALUE) : (x > Longitude.MIN_VALUE && x < Longitude.MAX_VALUE)) { return; } final String code = name.getName().getCode(); throw new InvalidParameterValueException(Errors.format( Errors.Keys.LONGITUDE_OUT_OF_RANGE_$1, new Longitude(x)), code, x); } /** * Ensures that the latitude is within allowed limits (±90°). This method is useful * for checking the validity of projection parameters like {@link #latitudeOfOrigin}. * * @param y Latitude to check, in degrees. * @param edge {@code true} to accept latitudes of exactly ±90°. * @throws IllegalArgumentException if the latitude is out of range. */ static void ensureLatitudeInRange(final ParameterDescriptor name, final double y, final boolean edge) throws IllegalArgumentException { if (edge ? (y >= Latitude.MIN_VALUE && y <= Latitude.MAX_VALUE) : (y > Latitude.MIN_VALUE && y < Latitude.MAX_VALUE)) { return; } final String code = name.getName().getCode(); throw new InvalidParameterValueException(Errors.format( Errors.Keys.LATITUDE_OUT_OF_RANGE_$1, new Latitude(y)), code, y); } /** * Ensures that the absolute value of a latitude is equal to the specified value, up to the * tolerance value. The expected value is usually either 0 or 90° (the equator or a pole). * * @param y Latitude to check, in degrees. * @param expected The expected value, in degrees. * @throws IllegalArgumentException if the latitude is not the expected one. */ static void ensureLatitudeEquals(final ParameterDescriptor name, final double y, final double expected) throws IllegalArgumentException { if (!(abs(abs(y) - expected) < ANGLE_TOLERANCE * (180/PI))) { final String code = name.getName().getCode(); throw new InvalidParameterValueException(Errors.format( Errors.Keys.ILLEGAL_ARGUMENT_$2, code, new Latitude(y)), code, y); } } /** * Sets the value in a parameter group. * * @param expected The value returned by {@code descriptor.descriptors()}. * @param param One of the {@link MapProjection} provider constants. * @param group The group in which to set the value. * @param value The value to set. */ static void set(final Collection expected, ParameterDescriptor descriptor, final ParameterValueGroup group, double value) { if (descriptor instanceof UniversalParameters) { descriptor = ((UniversalParameters) descriptor).find(expected); if (descriptor == null) { return; } } else if (!expected.contains(descriptor)) { /* * The line above search for exactly the given descriptor, not a descriptor having * the same name. This strictness is needed in order to avoid confusing a specific * descriptor like XY_PLANE_ROTATION (which is not a standard parameter) with some * "real" projection parameter having the same alias, like RECTIFIED_GRID_ANGLE. */ return; } if (descriptor.getMinimumOccurs() == 0) { /* * Parameter is optional. Checks if its value is equals * to the default one. If it is, then we will omit it. */ final Object df = descriptor.getDefaultValue(); if (df instanceof Number) { if (Utilities.equals(((Number) df).doubleValue(), value)) { return; } } } getOrCreate(descriptor, group).setValue(value); } } ////////////////////////////////////////////////////////////////////////////////////////// //////// //////// //////// METHODS FROM SUPER-CLASS //////// //////// //////// ////////////////////////////////////////////////////////////////////////////////////////// /** * The parameters used for creating this projection. They are used for formatting Well * Known Text (WKT) and error messages. Subclasses shall not use the values defined in * this object for computation purpose, except at construction time. * * @level advanced */ @Override protected final Parameters getUnmarshalledParameters() { return parameters; } /** * Returns a copy of the parameter values for this projection. * The default implementation returns a group of parameters with the * {@linkplain MapProjection#SEMI_MAJOR semi-major} axis length set to 1 and the * {@linkplain MapProjection#SEMI_MINOR semi-minor} axis length set to * sqrt(1 - {@linkplain #excentricitySquared e²}), which is * consistent with the definition of {@link UnitaryProjection}. * * @return A copy of the parameter values for this unitary projection. */ @Override public ParameterValueGroup getParameterValues() { final ParameterDescriptorGroup descriptor = getParameterDescriptors(); final ParameterValueGroup values = descriptor.createValue(); getOrCreate(SEMI_MAJOR, values).setValue(1.0); getOrCreate(SEMI_MINOR, values).setValue(sqrt(1 - excentricitySquared)); return values; } /** * Returns {@code true} if this class is a {@code Spherical} nested class. This method is not * public because the usage of those nested classes is specific to Geotk implementation. * This information is used sometime for selecting formulas, and for testing purpose. */ boolean isSpherical() { return false; } /** * Computes a hash code value for this unitary projection. The default implementation * computes a value from the parameters given at construction time. */ @Override protected int computeHashCode() { return hash(parameters, super.computeHashCode()); } /** * Compares the given object with this transform for equivalence. The default implementation * checks if {@code object} is an instance of the same class than {@code this}, then compares * the excentricity. *

* If this method returns {@code true}, then for any given identical source position, the * two compared unitary projections shall compute the same target position. Many of the * {@linkplain Parameters projection parameters} used for creating the unitary projections * are irrelevant and don't need to be known. Those projection parameters will be compared * only if the comparison mode is {@link ComparisonMode#STRICT STRICT} or * {@link ComparisonMode#BY_CONTRACT BY_CONTRACT}. * *

Example: a {@linkplain Mercator Mercator} projection can * be created in the 2SP case with a {@linkplain Parameters#standardParallels standard parallel} * value of 60°. The same projection can also be created in the 1SP case with a {@linkplain * Parameters#scaleFactor scale factor} of 0.5. Nevertheless those two unitary projections * applied on a sphere gives identical results. Considering them as equivalent allows the * referencing module to transform coordinates between those two projections more * efficiently.
* * @param object The object to compare with this unitary projection for equivalence. * @param mode The strictness level of the comparison. Default to {@link ComparisonMode#STRICT STRICT}. * @return {@code true} if the given object is equivalent to this unitary projection. */ @Override public boolean equals(final Object object, final ComparisonMode mode) { if (object == this) { return true; } if (super.equals(object, mode)) { final double e1, e2; final UnitaryProjection that = (UnitaryProjection) object; if (mode.ordinal() < ComparisonMode.IGNORE_METADATA.ordinal()) { if (!Utilities.equals(parameters, that.parameters)) { return false; } e1 = this.excentricitySquared; e2 = that.excentricitySquared; } else { e1 = this.excentricity; e2 = that.excentricity; } /* * There is no need to compare both 'excentricity' and 'excentricitySquared' since * the former is computed from the later. In strict comparison mode, we are better * to compare the 'excentricitySquared' since it is the original value from which * the other value is derived. However in approximative comparison mode, we need * to use the 'excentricity', otherwise we would need to take the square of the * tolerance factor before comparing 'excentricitySquared'. */ return epsilonEqual(e1, e2, mode) && epsilonEqual(longitudeRotation, that.longitudeRotation, mode) && epsilonEqual(longitudeBound, that.longitudeBound, mode); } return false; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy