AlbersEqualArea changes for revisions 9213:9214

This projection is rewritten from scratch using the formulas published in EPSG guidance notes.

Command line:

svn diff --extensions "--unified --ignore-space-change --ignore-all-space --ignore-eol-style" -r9213:9214 https://svn.osgeo.org/geotools/trunk/modules/library/referencing/src/main/java/org/geotools/referencing/operation/projection/AlbersEqualArea.java
Revision 9214
/*
 * Geotools - OpenSource mapping toolkit
 * (C) 2003, 2004 Geotools Project Managment Committee (PMC)
 *
 *    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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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.
 *
 *    You should have received a copy of the GNU Lesser General Public
 *    License along with this library; if not, write to the Free Software
 *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *
 *    This package contains formulas from the PROJ package of USGS.
 *    USGS's work is fully acknowledged here.
 */
/*
 *****************************************************************************
 * Some parts Copyright (c) 1995, Gerald Evenden
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ******************************************************************************
 */
package org.geotools.referencing.operation.projection;

// J2SE dependencies and extensions
import java.util.Collection;
import java.awt.geom.Point2D;
import javax.units.NonSI;

// OpenGIS dependencies
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.referencing.operation.MathTransform;

// Geotools dependencies
import org.geotools.measure.Latitude;
import org.geotools.referencing.Identifier;
import org.geotools.metadata.citation.Citation;
import org.geotools.resources.cts.ResourceKeys;
import org.geotools.resources.cts.Resources;

/**
 * Albers Equal Area Projection (EPSG code 9822). This is a conic projection
 * with parallels being unequally spaced arcs of concentric circles, more
 * closely spaced at north and south edges of the map. Merideans
 * are equally spaced radii of the same circles and intersect parallels at right
 * angles. As the name implies, this projection minimizes distortion in areas.
 * <br><br>
 *
 * NOTE: formulae used below are from a port, to java, of the
 *       'proj4' package of the USGS survey. USGS work is acknowledged here.
 * <br><br>
 *
 * <strong>References:</strong><ul>
 *   <li> Proj-4.4.7 available at <A HREF="http://www.remotesensing.org/proj">www.remotesensing.org/proj</A><br>
 *        Relevent files are: PJ_aea.c, pj_fwd.c and pj_inv.c </li>
 *   <li> John P. Snyder (Map Projections - A Working Manual,
 *        U.S. Geological Survey Professional Paper 1395, 1987)</li>
 *   <li> "Coordinate Conversions and Transformations including Formulas",
 *        EPSG Guidence Note Number 7, Version 19.</li>
 * </ul>
 *
 * @see <A HREF="http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html/">Albers Equal-Area Conic Projection on MathWorld</A>
 * @see <A HREF="http://www.remotesensing.org/geotiff/proj_list/albers_equal_area_conic.html"> "Albers_Conic_Equal_Area" on www.remotesensing.org</A>
 * @see <A HREF="http://srmwww.gov.bc.ca/gis/bceprojection.html">British Columbia Albers Standard Projection</A>
 *
 * @version $Id$
 * @author Rueben Schulz
 */
public class AlbersEqualArea extends MapProjection {
    /**
     * Constants used by the spherical and elliptical Albers projection.
     */
    private final double n, c, rho0;

    /**
     * An error condition indicating itteration will not converge for the
     * inverse ellipse. See Snyder (14-20)
     */
    private final double ec;

    /**
     * Standards parallels in radians, for {@link #getParameterValues} implementation.
     */
    private final double phi1, phi2;


    /**
     * The {@link MathTransformProvider} for a {@link AlbersEqualArea} projection.
     *
     * @see MathTransformFactory
     *
     * @version $Id$
     * @author Rueben Schulz
     */
    public static final class Provider
            extends org.geotools.referencing.operation.projection.MapProjection.Provider {

        /**
         * The operation parameter descriptor for the {@link #phi1 standard parallel 1}
         * parameter value. Valid values range is from -90 to 90°. Default value is 0.
         */
        public static final ParameterDescriptor STANDARD_PARALLEL_1 = createDescriptor(
                new Identifier[] {
                    new Identifier(Citation.OPEN_GIS, "standard_parallel_1"),
                    new Identifier(Citation.EPSG,     "Latitude of 1st standard parallel"),
                    new Identifier(Citation.GEOTIFF,  "StdParallel1")
                },
                0, -90, 90, NonSI.DEGREE_ANGLE);

        /**
         * The operation parameter descriptor for the {@link #phi2 standard parallel 2}
         * parameter value. Valid values range is from -90 to 90°. Default value is 0.
         */
        public static final ParameterDescriptor STANDARD_PARALLEL_2 = createDescriptor(
                new Identifier[] {
                    new Identifier(Citation.OPEN_GIS, "standard_parallel_2"),
                    new Identifier(Citation.EPSG,     "Latitude of 2nd standard parallel"),
                    new Identifier(Citation.GEOTIFF,  "StdParallel2")
                },
                0, -90, 90, NonSI.DEGREE_ANGLE);
        /**
         * The parameters group.
         */
        static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new Identifier[] {
                new Identifier(Citation.OPEN_GIS, "Albers_Conic_Equal_Area"),
                new Identifier(Citation.EPSG,     "Albers Equal Area"),
                new Identifier(Citation.EPSG,     "9822"),
                new Identifier(Citation.GEOTIFF,  "CT_AlbersEqualArea"),
                new Identifier(Citation.ESRI,     "Albers"),
                new Identifier(Citation.ESRI,     "Albers Equal Area Conic"),
                new Identifier(Citation.GEOTOOLS, Resources.formatInternational(
                                                  ResourceKeys.ALBERS_EQUAL_AREA_PROJECTION))
            }, new ParameterDescriptor[] {
                SEMI_MAJOR,          SEMI_MINOR,
                CENTRAL_MERIDIAN,    LATITUDE_OF_ORIGIN,
                STANDARD_PARALLEL_1, STANDARD_PARALLEL_2,
                FALSE_EASTING,       FALSE_NORTHING
            });

        /**
         * Construct a new provider.
         */
        public Provider() {
            super(PARAMETERS);
        }

        /**
         * Creates a transform from the specified group of parameter values.
         *
         * @param  parameters The group of parameter values.
         * @return The created math transform.
         * @throws ParameterNotFoundException if a required parameter was not found.
         */
        public MathTransform createMathTransform(final ParameterValueGroup parameters)
                throws ParameterNotFoundException
        {
            final Collection descriptors = PARAMETERS.descriptors();
            return new AlbersEqualArea(parameters, descriptors);
        }
    }


    /**
     * Construct a new map projection from the supplied parameters.
     *
     * @param  parameters The parameter values in standard units.
     * @param  The expected parameter descriptors.
     * @throws ParameterNotFoundException if a mandatory parameter is missing.
     *
     * @task REVISIT: set phi2 = phi1 if no SP2 param is given by user (a 1sp projection)
     */
    public AlbersEqualArea(final ParameterValueGroup parameters, final Collection expected) {
        //Fetch parameters
        super(parameters, expected);

        phi1 = doubleValue(expected, Provider.STANDARD_PARALLEL_1, parameters);
        phi2 = doubleValue(expected, Provider.STANDARD_PARALLEL_2, parameters);

    //Compute Constants
        if (Math.abs(phi1 + phi2) < EPS)
            throw new IllegalArgumentException(Resources.format(
                    ResourceKeys.ERROR_ANTIPODE_LATITUDES_$2,
                    new Latitude(Math.toDegrees(phi1)),
                    new Latitude(Math.toDegrees(phi2))));

        double  sinphi = Math.sin(phi1);
        double  cosphi = Math.cos(phi1);
        double  n      = sinphi;
        boolean secant = (Math.abs(phi1 - phi2) >= EPS);
        if (isSpherical) {
            if (secant) {
                n = 0.5 * (n + Math.sin(phi2));
            }
            c    = cosphi * cosphi + n*2 * sinphi;
            rho0 = Math.sqrt(c - n*2 * Math.sin(latitudeOfOrigin)) /n;
            ec   = Double.NaN;
        } else {
            double m1 = msfn(sinphi, cosphi);
            double q1 = qsfn(sinphi);
            if (secant) { /* secant cone */
                sinphi    = Math.sin(phi2);
                cosphi    = Math.cos(phi2);
                double m2 = msfn(sinphi, cosphi);
                double q2 = qsfn(sinphi);
                n = (m1 * m1 - m2 * m2) / (q2 - q1);
            }
            c = m1 * m1 + n * q1;
            rho0 = Math.sqrt(c - n * qsfn(Math.sin(latitudeOfOrigin))) /n;
            ec = 1.0 - .5 * (1.0-excentricitySquared) *
                 Math.log((1.0 - excentricity) / (1.0 + excentricity)) / excentricity;
        }
        this.n = n;
    }

    /**
     * {@inheritDoc}
     */
    protected ParameterDescriptorGroup getParameterDescriptors() {
        return Provider.PARAMETERS;
    }

    /**
     * {@inheritDoc}
     */
    public ParameterValueGroup getParameterValues() {
        final ParameterValueGroup values = super.getParameterValues();
        final Collection expected = getParameterDescriptors().descriptors();
        set(expected, Provider.STANDARD_PARALLEL_1, values, phi1);
        set(expected, Provider.STANDARD_PARALLEL_2, values, phi2);
        return values;
    }

    /**
     * Transforms the specified (<var>x</var>,<var>y</var>) coordinate (units in radians)
     * and stores the result in <code>ptDst</code> (linear distance on a unit sphere).
     */
    protected Point2D transformNormalized(double x, double y, Point2D ptDst)
            throws ProjectionException
    {
        x *= n;
        double rho;
        if (isSpherical) {
            rho = c - n*2 * Math.sin(y);
        } else {
            rho = c - n * qsfn(Math.sin(y));
        }

        if (rho < 0.0) {
            // TODO: fix message (and check when this condition will occur)
            // this just checks for a tolerence error that may cause rho to be
            // close to -0.0, may be better to just round up in these cases
            throw new ProjectionException("Tolerance condition error");
        }
        rho = Math.sqrt(rho) / n;
        y   = rho0 - rho * Math.cos(x);
        x   =        rho * Math.sin(x);

        if (ptDst != null) {
            ptDst.setLocation(x,y);
            return ptDst;
        }
        return new Point2D.Double(x,y);
    }

    /**
     * Transforms the specified (<var>x</var>,<var>y</var>) coordinate
     * and stores the result in <code>ptDst</code>.
     */
    protected Point2D inverseTransformNormalized(double x, double y, Point2D ptDst)
            throws ProjectionException
    {
        y = rho0 - y;
        double rho = Math.sqrt(x*x + y*y);
        if (rho > EPS) {
            if (n < 0.0) {
                rho = -rho;
                x   = -x;
                y   = -y;
            }
            x = Math.atan2(x, y) / n;
            y =  rho*n;
            if (isSpherical) {
                y = (c - y * y) / (n*2);
                if (Math.abs(y) <= 1.0){
                    y = Math.asin(y);
                }
                else {
                    y = (y < 0.0) ? -Math.PI/2.0 : Math.PI/2.0;
                }
            } else {
                y = (c - y*y) / n;
                if (Math.abs(ec - Math.abs(y)) > EPS) {
                    y = phi1(y);
                } else {
                    y = (y < 0.0) ? -Math.PI/2.0 : Math.PI/2.0;
                }
            }
        } else {
            x = 0.0;
            y = n > 0.0 ? Math.PI/2.0 : - Math.PI/2.0;
        }

        if (ptDst != null) {
            ptDst.setLocation(x,y);
            return ptDst;
        }
        return new Point2D.Double(x,y);
    }

    /**
     * Iteratively solves equation (3-16) from Snyder.
     *
     * @param qs arcsin(q/2), used in the first step of itteration
     * @return the latitude
     */
    private double phi1(double qs) throws ProjectionException {
        final double tone_es = 1 - excentricitySquared;
        double phi = Math.asin(0.5 * qs);
        if (excentricity < EPS) {
            return phi;
        }
        for (int i=0; i<MAX_ITER; i++) {
            final double sinpi = Math.sin(phi);
            final double cospi = Math.cos(phi);
            final double con   = excentricity * sinpi;
            final double com   = 1.0 - con*con;
            final double dphi  = 0.5 * com*com / cospi *
                                 (qs/tone_es - sinpi / com + 0.5/excentricity *
                                 Math.log((1. - con) / (1. + con)));
            phi += dphi;
            if (Math.abs(dphi) <= TOL) {
                return phi;
            }
        }
        throw new ProjectionException(Resources.format(ResourceKeys.ERROR_NO_CONVERGENCE));
    }

    /**
     * Calculates q, Snyder equation (3-12)
     *
     * @param sinphi sin of the latitude q is calculated for
     * @return q from Snyder equation (3-12)
     */
    private double qsfn(double sinphi) {
        final double one_es = 1 - excentricitySquared;
        if (excentricity >= EPS) {
            final double con = excentricity * sinphi;
            return (one_es * (sinphi / (1. - con*con) -
                   (0.5/excentricity) * Math.log((1.-con) / (1.+con))));
        } else {
            return sinphi + sinphi;
        }
    }

    /**
     * Returns a hash value for this projection.
     */
    public int hashCode() {
        final long code = Double.doubleToLongBits(c);
        return ((int)code ^ (int)(code >>> 32)) + 37*super.hashCode();
    }

    /**
     * Compares the specified object with this map projection for equality.
     */
    public boolean equals(final Object object) {
        if (object == this) {
            // Slight optimization
            return true;
        }
        if (super.equals(object)) {
            final AlbersEqualArea that = (AlbersEqualArea) object;
            return equals(this.n    , that.n   ) &&
                   equals(this.c    , that.c   ) &&
                   equals(this.rho0 , that.rho0) &&
                   equals(this.phi1 , that.phi1) &&
                   equals(this.phi2 , that.phi2);
        }
        return false;
    }

}