Initial implementation of AlbersEqualArea. Share some more code with CylindricalEqualArea when applicable.


git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1753540 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java
new file mode 100644
index 0000000..d09c984
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/AlbersEqualArea.java
@@ -0,0 +1,197 @@
+/*
+ * 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.internal.referencing.provider;
+
+import javax.xml.bind.annotation.XmlTransient;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.referencing.operation.ConicProjection;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.referencing.operation.projection.NormalizedProjection;
+
+
+/**
+ * The provider for "<cite>Albers Equal Area</cite>" projection (EPSG:9822).
+ *
+ * @author  Rueben Schulz (UBC)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ *
+ * @see <a href="http://www.remotesensing.org/geotiff/proj_list/albers_equal_area_conic.html">Albers Equal-Area Conic on RemoteSensing.org</a>
+ */
+@XmlTransient
+public final class AlbersEqualArea extends MapProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -7489679528438418778L;
+
+    /**
+     * The operation parameter descriptor for the <cite>Latitude of false origin</cite> (φf) parameter value.
+     * Valid values range is [-90 … 90]° and default value is 0°.
+     */
+    public static final ParameterDescriptor<Double> LATITUDE_OF_FALSE_ORIGIN;
+
+    /**
+     * The operation parameter descriptor for the <cite>Longitude of false origin</cite> (λf) parameter value.
+     * Valid values range is [-180 … 180]° and default value is 0°.
+     */
+    public static final ParameterDescriptor<Double> LONGITUDE_OF_FALSE_ORIGIN;
+
+    /**
+     * The operation parameter descriptor for the <cite>Latitude of 1st standard parallel</cite> parameter value.
+     * Valid values range is [-90 … 90]° and default value is the value given to the {@link #LATITUDE_OF_FALSE_ORIGIN}
+     * parameter.
+     */
+    public static final ParameterDescriptor<Double> STANDARD_PARALLEL_1;
+
+    /**
+     * The operation parameter descriptor for the <cite>Latitude of 2nd standard parallel</cite> parameter value.
+     * Valid values range is [-90 … 90]° and default value is the value given to the {@link #STANDARD_PARALLEL_2}
+     * parameter.
+     */
+    public static final ParameterDescriptor<Double> STANDARD_PARALLEL_2;
+
+    /**
+     * The operation parameter descriptor for the <cite>Easting at false origin</cite> (Ef) parameter value.
+     * Valid values range is unrestricted and default value is 0 metre.
+     */
+    public static final ParameterDescriptor<Double> EASTING_AT_FALSE_ORIGIN;
+
+    /**
+     * The operation parameter descriptor for the <cite>Northing at false origin</cite> (Nf) parameter value.
+     * Valid values range is unrestricted and default value is 0 metre.
+     */
+    public static final ParameterDescriptor<Double> NORTHING_AT_FALSE_ORIGIN;
+
+    /**
+     * The group of all parameters expected by this coordinate operation.
+     */
+    static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = builder();
+        /*
+         * EPSG:    Latitude of false origin
+         * OGC:     latitude_of_center
+         * ESRI:    Latitude_Of_Origin
+         * NetCDF:  latitude_of_projection_origin
+         * GeoTIFF: NatOriginLat
+         */
+        LATITUDE_OF_FALSE_ORIGIN = createLatitude(
+                renameAlias(LambertConformal2SP.LATITUDE_OF_FALSE_ORIGIN, Citations.GEOTIFF, Mercator1SP.LATITUDE_OF_ORIGIN, builder)
+                .rename(Citations.OGC, "latitude_of_center"), true);
+        /*
+         * EPSG:    Longitude of false origin
+         * OGC:     longitude_of_center
+         * ESRI:    Central_Meridian
+         * NetCDF:  longitude_of_central_meridian
+         * GeoTIFF: NatOriginLong
+         */
+        LONGITUDE_OF_FALSE_ORIGIN = createLongitude(
+                renameAlias(LambertConformal2SP.LONGITUDE_OF_FALSE_ORIGIN, Citations.GEOTIFF, Mercator1SP.LONGITUDE_OF_ORIGIN, builder)
+                .rename(Citations.OGC, "longitude_of_center"));
+        /*
+         * EPSG:    Latitude of 1st standard parallel
+         * OGC:     standard_parallel_1
+         * ESRI:    Standard_Parallel_1
+         * NetCDF:  standard_parallel
+         * GeoTIFF: StdParallel1
+         *
+         * Special case: default value shall be the value of LATITUDE_OF_FALSE_ORIGIN.
+         */
+        STANDARD_PARALLEL_1 = LambertConformal2SP.STANDARD_PARALLEL_1;
+        /*
+         * EPSG:    Latitude of 2nd standard parallel
+         * OGC:     standard_parallel_2
+         * ESRI:    Standard_Parallel_2
+         * NetCDF:  standard_parallel
+         * GeoTIFF: StdParallel2
+         *
+         * Special case: default value shall be the value of STANDARD_PARALLEL_1.
+         */
+        STANDARD_PARALLEL_2 = LambertConformal2SP.STANDARD_PARALLEL_2;
+        /*
+         * EPSG:    Easting at false origin
+         * OGC:     false_easting
+         * ESRI:    False_Easting
+         * NetCDF:  false_easting
+         * GeoTIFF: FalseEasting
+         */
+        EASTING_AT_FALSE_ORIGIN = createShift(
+                renameAlias(LambertConformal2SP.EASTING_AT_FALSE_ORIGIN, Citations.GEOTIFF, LambertConformal1SP.FALSE_EASTING, builder));
+        /*
+         * EPSG:    Northing at false origin
+         * OGC:     false_northing
+         * ESRI:    False_Northing
+         * NetCDF:  false_northing
+         * GeoTIFF: FalseNorthing
+         */
+        NORTHING_AT_FALSE_ORIGIN = createShift(
+                renameAlias(LambertConformal2SP.NORTHING_AT_FALSE_ORIGIN, Citations.GEOTIFF, LambertConformal1SP.FALSE_NORTHING, builder));
+
+        PARAMETERS = builder
+                .addIdentifier(             "9822")
+                .addName(                   "Albers Equal Area")
+                .addName(Citations.OGC,     "Albers_Conic_Equal_Area")
+                .addName(Citations.ESRI,    "Albers_Equal_Area_Conic")
+                .addName(Citations.ESRI,    "Albers")
+                .addName(Citations.NETCDF,  "AlbersEqualArea")
+                .addName(Citations.GEOTIFF, "CT_AlbersEqualArea")
+                .addName(Citations.PROJ4,   "aea")
+                .addIdentifier(Citations.GEOTIFF, "11")
+                .addIdentifier(Citations.MAP_INFO, "9")
+                .addIdentifier(Citations.S57,      "1")
+                .createGroupForMapProjection(
+                        LATITUDE_OF_FALSE_ORIGIN,
+                        LONGITUDE_OF_FALSE_ORIGIN,
+                        STANDARD_PARALLEL_1,
+                        STANDARD_PARALLEL_2,
+                        EASTING_AT_FALSE_ORIGIN,
+                        NORTHING_AT_FALSE_ORIGIN);
+    }
+
+    /**
+     * Constructs a new provider.
+     */
+    public AlbersEqualArea() {
+        super(PARAMETERS);
+    }
+
+    /**
+     * Returns the operation type for this map projection.
+     *
+     * @return {@code ConicProjection.class}
+     */
+    @Override
+    public final Class<ConicProjection> getOperationType() {
+        return ConicProjection.class;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return The map projection created from the given parameter values.
+     */
+    @Override
+    protected final NormalizedProjection createProjection(final Parameters parameters) {
+        return new org.apache.sis.referencing.operation.projection.AlbersEqualArea(this, parameters);
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
new file mode 100644
index 0000000..b2e601a
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.operation.projection;
+
+import java.util.EnumMap;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.OperationMethod;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.util.Workaround;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.operation.matrix.Matrix2;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.ContextualParameters;
+
+import static java.lang.Math.*;
+import static org.apache.sis.internal.referencing.provider.AlbersEqualArea.*;
+
+
+/**
+ * <cite>Albers Equal Area</cite> projection (EPSG code 9822).
+ * See the <a href="http://mathworld.wolfram.com/AlbersEqual-AreaConicProjection.html">Albers Equal-Area
+ * Conic projection on MathWorld</a> for an overview.
+ *
+ * <p>The {@code "standard_parallel_2"} parameter is optional and will be given the same value as
+ * {@code "standard_parallel_1"} if not set.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @author  Rémi Maréchal (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public class AlbersEqualArea extends EqualAreaProjection {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -3024658742514888646L;
+
+    /**
+     * Internal coefficients for computation, depending only on eccentricity and values of standards parallels.
+     * This is defined as {@literal n = (m₁² – m₂²) / (α₂ – α₁)} in §1.3.13 of IOGP Publication 373-7-2 (april 2015).
+     *
+     * <p>In Apache SIS implementation, we use modified formulas in which the (1 - ℯ²) factor is omitted in
+     * {@link #qm(double)} calculation. Consequently what we get is a modified value <var>nm</var> which is
+     * related to Synder's <var>n</var> value by {@literal n = nm / (1 - ℯ²)}.  The omitted (1 - ℯ²) factor
+     * is either taken in account by the (de)normalization matrix, or cancels with other (1 - ℯ²) factors
+     * when we develop the formulas.</p>
+     *
+     * <p>Note that in the spherical case, <var>nm</var> = Synder's <var>n</var>.</p>
+     */
+    final double nm;
+
+    /**
+     * Internal coefficients for computation, depending only on values of standards parallels.
+     * This is defined as {@literal C = m₁² + (n⋅α₁)} in §1.3.13 of IOGP Publication 373-7-2 –
+     * Geomatics Guidance Note number 7, part 2 – April 2015.
+     */
+    final double C;
+
+    /**
+     * Creates an Albers Equal Area projection from the given parameters.
+     *
+     * @param method     Description of the projection parameters.
+     * @param parameters The parameter values of the projection to create.
+     */
+    public AlbersEqualArea(final OperationMethod method, final Parameters parameters) {
+        this(initializer(method, parameters));
+    }
+
+    /**
+     * Work around for RFE #4093999 in Sun's bug database
+     * ("Relax constraint on placement of this()/super() call in constructors").
+     */
+    @SuppressWarnings("fallthrough")
+    @Workaround(library="JDK", version="1.7")
+    private static Initializer initializer(final OperationMethod method, final Parameters parameters) {
+        final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class);
+        roles.put(ParameterRole.FALSE_EASTING,    EASTING_AT_FALSE_ORIGIN);
+        roles.put(ParameterRole.FALSE_NORTHING,   NORTHING_AT_FALSE_ORIGIN);
+        roles.put(ParameterRole.CENTRAL_MERIDIAN, LONGITUDE_OF_FALSE_ORIGIN);
+        return new Initializer(method, parameters, roles, (byte) 0);
+    }
+
+    /**
+     * Work around for RFE #4093999 in Sun's bug database
+     * ("Relax constraint on placement of this()/super() call in constructors").
+     */
+    @Workaround(library="JDK", version="1.7")
+    private AlbersEqualArea(final Initializer initializer) {
+        super(initializer);
+        double φ0 = initializer.getAndStore(LATITUDE_OF_FALSE_ORIGIN);
+        double φ1 = initializer.getAndStore(STANDARD_PARALLEL_1, φ0);
+        double φ2 = initializer.getAndStore(STANDARD_PARALLEL_2, φ1);
+        if (abs(φ1 + φ2) < Formulas.ANGULAR_TOLERANCE) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.LatitudesAreOpposite_2,
+                    new Latitude(φ1), new Latitude(φ2)));
+        }
+        final boolean secant = (abs(φ1 - φ2) >= Formulas.ANGULAR_TOLERANCE);
+        φ0 = toRadians(φ0);
+        φ1 = toRadians(φ1);
+        φ2 = toRadians(φ2);
+        final double sinφ0 = sin(φ0);
+        final double sinφ1 = sin(φ1);
+        final double cosφ1 = cos(φ1);
+        final double sinφ2 = sin(φ2);
+        final double cosφ2 = cos(φ2);
+        final double m1 = initializer.scaleAtφ(sinφ1, cosφ1);           // = cos(φ₁) / √(1 – ℯ²sin²φ₁)
+        final double α1 = qm(sinφ1);                                    // Omitted ×(1-ℯ²)
+        if (secant) {
+            final double m2 = initializer.scaleAtφ(sinφ2, cosφ2);       // = cos(φ₂) / √(1 – ℯ²sin²φ₂)
+            final double α2 = qm(sinφ2);                                // Omitted ×(1-ℯ²)
+            nm = (m1*m1 - m2*m2) / (α2 - α1);                           // n = nm / (1-ℯ²)
+        } else {
+            nm = sinφ1;
+        }
+        C = m1*m1 + nm*α1;                  // Omitted (1-ℯ²) term in nm cancels with omitted (1-ℯ²) term in α₁.
+        /*
+         * Compute rn = (1-ℯ²)/nm, which is the reciprocal of the "real" n used in Synder and EPSG guidance note.
+         * We opportunistically use double-double arithmetic since the MatrixSIS operations use them anyway, but
+         * we do not really have that accuracy because of the limited precision of 'nm'. The intend is rather to
+         * increase the chances term cancellations happen during concatenation of coordinate operations.
+         */
+        final DoubleDouble rn = DoubleDouble.verbatim(1);
+        rn.subtract(initializer.eccentricitySquared);
+        rn.divide(nm, 0);
+        /*
+         * Compute  ρ₀ = √(C - n⋅q(sinφ₀))/n  with multiplication by a omitted because already taken in account
+         * by the denormalization matrix. Omitted (1-ℯ²) term in nm cancels with omitted (1-ℯ²) term in qm(…).
+         * See above note about double-double arithmetic usage.
+         */
+        final DoubleDouble ρ0 = DoubleDouble.verbatim(C - nm*qm(sinφ0));
+        ρ0.sqrt();
+        ρ0.multiply(rn);
+        /*
+         * At this point, all parameters have been processed. Now process to their
+         * validation and the initialization of (de)normalize affine transforms.
+         */
+        final MatrixSIS normalize   = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
+        final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
+        denormalize.convertBefore(0, rn, null); rn.negate();
+        denormalize.convertBefore(1, rn, ρ0);   rn.inverseDivide(-1, 0);
+        normalize.convertAfter(0, rn, null);
+        super.computeCoefficients();
+    }
+
+    /**
+     * Creates a new projection initialized to the same parameters than the given one.
+     */
+    AlbersEqualArea(final AlbersEqualArea other) {
+        super(other);
+        nm = other.nm;
+        C  = other.C;
+    }
+
+    /**
+     * Returns the names of additional internal parameters which need to be taken in account when
+     * comparing two {@code AlbersEqualArea} projections or formatting them in debug mode.
+     */
+    @Override
+    final String[] getInternalParameterNames() {
+        return new String[] {"n", "C"};
+    }
+
+    /**
+     * Returns the values of additional internal parameters which need to be taken in account when
+     * comparing two {@code AlbersEqualArea} projections or formatting them in debug mode.
+     */
+    @Override
+    final double[] getInternalParameterValues() {
+        return new double[] {nm / (1 - eccentricitySquared), C};
+    }
+
+    /**
+     * Returns the sequence of <cite>normalization</cite> → {@code this} → <cite>denormalization</cite> transforms
+     * as a whole. The transform returned by this method expects (<var>longitude</var>, <var>latitude</var>)
+     * coordinates in <em>degrees</em> and returns (<var>x</var>,<var>y</var>) coordinates in <em>metres</em>.
+     *
+     * <p>The non-linear part of the returned transform will be {@code this} transform, except if the ellipsoid
+     * is spherical. In the later case, {@code this} transform will be replaced by a simplified implementation.</p>
+     *
+     * @param  factory The factory to use for creating the transform.
+     * @return The map projection from (λ,φ) to (<var>x</var>,<var>y</var>) coordinates.
+     * @throws FactoryException if an error occurred while creating a transform.
+     */
+    @Override
+    public MathTransform createMapProjection(final MathTransformFactory factory) throws FactoryException {
+        AlbersEqualArea kernel = this;
+        if (eccentricity == 0) {
+            kernel = new Spherical(this);
+        }
+        return context.completeTransform(factory, kernel);
+    }
+
+    /**
+     * Converts the specified (θ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
+     * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
+     *
+     * @return The matrix of the projection derivative at the given source position,
+     *         or {@code null} if the {@code derivate} argument is {@code false}.
+     * @throws ProjectionException if the coordinate can not be converted.
+     */
+    @Override
+    public Matrix transform(final double[] srcPts, final int srcOff,
+                            final double[] dstPts, final int dstOff,
+                            final boolean derivate) throws ProjectionException
+    {
+        final double θ = srcPts[srcOff  ];      // θ = n⋅λ
+        final double φ = srcPts[srcOff+1];
+        final double cosθ = cos(θ);
+        final double sinθ = sin(θ);
+        final double sinφ = sin(φ);
+        final double ρ = sqrt(C - nm*qm_ellipsoid(sinφ));
+        if (dstPts != null) {
+            dstPts[dstOff  ] = ρ * sinθ;
+            dstPts[dstOff+1] = ρ * cosθ;
+        }
+        if (!derivate) {
+            return null;
+        }
+        /*
+         * End of map projection. Now compute the derivative.
+         */
+        final double me = 1 - eccentricitySquared;
+        final double dρ_dφ = -0.5 * nm*dqm_dφ(sinφ, cos(φ)*me) / (me*ρ);
+        return new Matrix2(cosθ*ρ, dρ_dφ*sinθ,          // ∂x/∂λ, ∂x/∂φ
+                          -sinθ*ρ, dρ_dφ*cosθ);         // ∂y/∂λ, ∂y/∂φ
+    }
+
+    /**
+     * Converts the specified (<var>x</var>,<var>y</var>) coordinates and stores the (θ,φ) result in {@code dstPts}.
+     *
+     * @throws ProjectionException if the point can not be converted.
+     */
+    @Override
+    protected void inverseTransform(final double[] srcPts, final int srcOff,
+                                    final double[] dstPts, final int dstOff)
+            throws ProjectionException
+    {
+        final double x = srcPts[srcOff  ];
+        final double y = srcPts[srcOff+1];
+        dstPts[dstOff  ] = atan2(x, y);
+        dstPts[dstOff+1] = φ((C - (x*x + y*y)) / nm);
+        /*
+         * Note: Synder 14-19 gives  q = (C - ρ²n²/a²)/n  where  ρ = √(x² + (ρ₀ - y)²).
+         * But in Apache SIS implementation, ρ₀ has already been subtracted by the matrix before we reach this point.
+         * So we can simplify by ρ² = x² + y². Furthermore the matrix also divided x and y by a (the semi-major axis
+         * length) before this method, and multiplied by n. so what we have is actually (ρ⋅n/a)² = x² + y².
+         * So the formula become:
+         *
+         *      q  =  (C - (x² + y²)) / n
+         *
+         * We divide by nm instead of n, so a (1-ℯ²) term is missing. But that missing term will be cancelled with
+         * the missing (1-ℯ²) term in qmPolar (the divisor applied by the φ(double) method that we invoke).
+         */
+    }
+
+
+    /**
+     * Provides the transform equations for the spherical case of the Albers Equal Area projection.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @author  Rémi Maréchal (Geomatys)
+     * @since   0.8
+     * @version 0.8
+     * @module
+     */
+    static final class Spherical extends AlbersEqualArea {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = 9090765015127854096L;
+
+        /**
+         * Constructs a new map projection from the parameters of the given projection.
+         *
+         * @param other the other projection (usually ellipsoidal) from which to copy the parameters.
+         */
+        protected Spherical(final AlbersEqualArea other) {
+            super(other);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public Matrix transform(final double[] srcPts, final int srcOff,
+                                final double[] dstPts, final int dstOff,
+                                final boolean derivate) throws ProjectionException
+        {
+            final double θ = srcPts[srcOff];                // θ = n⋅λ
+            final double φ = srcPts[srcOff+1];
+            final double cosθ = cos(θ);
+            final double sinθ = sin(θ);
+            final double sinφ = sin(φ);
+            final double ρ = sqrt(C - 2*nm*sinφ);           // Synder 14-3 with radius and division by n omitted.
+            if (dstPts != null) {
+                dstPts[dstOff  ] = ρ * sinθ;                // Synder 14-1
+                dstPts[dstOff+1] = ρ * cosθ;                // Synder 14-2
+            }
+            if (!derivate) {
+                return null;
+            }
+            final double dρ_dφ = -nm*cos(φ) / ρ;
+            return new Matrix2(cosθ*ρ, dρ_dφ*sinθ,          // ∂x/∂λ, ∂x/∂φ
+                              -sinθ*ρ, dρ_dφ*cosθ);         // ∂y/∂λ, ∂y/∂φ
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void inverseTransform(final double[] srcPts, final int srcOff,
+                                        final double[] dstPts, final int dstOff)
+                throws ProjectionException
+        {
+            final double x = srcPts[srcOff];
+            final double y = srcPts[srcOff + 1];
+            dstPts[dstOff  ] = atan2(x, y);                         // Part of Synder 14-11
+            dstPts[dstOff+1] = asin((C - (x*x + y*y)) / (nm*2));    // Synder 14-8 modified
+        }
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
index 9c2d16b..ae74e73 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CylindricalEqualArea.java
@@ -97,13 +97,6 @@
     private final byte variant;
 
     /**
-     * Value of {@link #qm(double)} function (part of Snyder equation (3-12)) at pole (sinφ = 1).
-     *
-     * @see #computeCoefficients()
-     */
-    private transient double qmPolar;
-
-    /**
      * Creates a Cylindrical Equal Area projection from the given parameters.
      *
      * @param method     Description of the projection parameters.
@@ -176,16 +169,7 @@
         ik.divide(k0);
         denormalize.convertAfter(0, k0, null);
         denormalize.convertAfter(1, ik, null);
-        computeCoefficients();
-    }
-
-    /**
-     * Invoked at construction time or on deserialization for computing the transient fields.
-     */
-    @Override
-    final void computeCoefficients() {
         super.computeCoefficients();
-        qmPolar = qm(1);
     }
 
     /**
@@ -194,7 +178,6 @@
     CylindricalEqualArea(final CylindricalEqualArea other) {
         super(other);
         variant = other.variant;
-        qmPolar = other.qmPolar;
     }
 
     /**
@@ -235,8 +218,8 @@
         final double φ    = srcPts[srcOff+1];
         final double sinφ = sin(φ);
         if (dstPts != null) {
-            dstPts[dstOff  ] = srcPts[srcOff];  // Multiplication by k₀ will be applied by the denormalization matrix.
-            dstPts[dstOff+1] = qm(sinφ);        // Multiplication by (1-ℯ²)/(2k₀) will be applied by the denormalization matrix.
+            dstPts[dstOff  ] = srcPts[srcOff];      // Multiplication by k₀ will be applied by the denormalization matrix.
+            dstPts[dstOff+1] = qm_ellipsoid(sinφ);  // Multiplication by (1-ℯ²)/(2k₀) will be applied by the denormalization matrix.
         }
         /*
          * End of map projection. Now compute the derivative, if requested.
@@ -265,7 +248,7 @@
             dstOff--;
             while (--numPts >= 0) {
                 final double φ = dstPts[dstOff += 2];                   // Same as srcPts[srcOff + 1].
-                dstPts[dstOff] = qm(sin(φ));                            // Part of Synder equation (10-15)
+                dstPts[dstOff] = qm_ellipsoid(sin(φ));                  // Part of Synder equation (10-15)
             }
         }
     }
@@ -283,7 +266,7 @@
     {
         final double y   = srcPts[srcOff+1];            // Must be before writing x.
         dstPts[dstOff  ] = srcPts[srcOff  ];            // Must be before writing y.
-        dstPts[dstOff+1] = φ(y / qmPolar);
+        dstPts[dstOff+1] = φ(y);
         /*
          * Equation 10-26 of Synder gives β = asin(2y⋅k₀/(a⋅qPolar)).
          * In our case it simplifies to sinβ = (y/qmPolar) because:
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
index d6a101c..0c17bf6 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java
@@ -18,6 +18,7 @@
 
 import java.io.IOException;
 import java.io.ObjectInputStream;
+import org.apache.sis.util.resources.Errors;
 
 import static java.lang.Math.*;
 import static org.apache.sis.math.MathFunctions.atanh;
@@ -70,6 +71,13 @@
     private transient double ci2, ci4, ci8;
 
     /**
+     * Value of {@link #qm(double)} function (part of Snyder equation (3-12)) at pole (sinφ = 1).
+     *
+     * @see #computeCoefficients()
+     */
+    private transient double qmPolar;
+
+    /**
      * Creates a new normalized projection from the parameters computed by the given initializer.
      *
      * @param initializer The initializer for computing map projection internal parameters.
@@ -84,8 +92,8 @@
      */
     void computeCoefficients() {
         final double e2 = eccentricitySquared;
-        final double e4 = e2 * e2;
-        final double e6 = e2 * e4;
+        final double e4  = e2 * e2;
+        final double e6  = e2 * e4;
         ci2  =  517/5040.  * e6  +  31/180. * e4  +  1/3. * e2;
         ci4  =  251/3780.  * e6  +  23/360. * e4;
         ci8  =  761/45360. * e6;
@@ -100,6 +108,7 @@
             ci4 *=  8;
             ci8 *= 64;
         }
+        qmPolar = qm(1);
     }
 
     /**
@@ -113,6 +122,7 @@
         ci2 = other.ci2;
         ci4 = other.ci4;
         ci8 = other.ci8;
+        qmPolar = other.qmPolar;
     }
 
     /**
@@ -120,6 +130,8 @@
      * In order to get the <var>q</var> function, this method output must be multiplied
      * by <code>(1 - {@linkplain #eccentricitySquared})</code>.
      *
+     * <p>The <var>q</var> variable is named <var>α</var> in EPSG guidance notes.</p>
+     *
      * <p>This equation has the following properties:</p>
      *
      * <ul>
@@ -130,13 +142,28 @@
      *   <li>q(0) = 0</li>
      * </ul>
      *
-     * In the spherical case, <var>q</var> = 2⋅sinφ. It is caller responsibility to ensure that this
-     * method is not invoked in the spherical case, since this implementation does not work in such case.
+     * In the spherical case, <var>q</var> = 2⋅sinφ.
      *
      * @param  sinφ sine of the latitude <var>q</var> is calculated for.
      * @return <var>q</var> from Snyder equation (3-12).
      */
     final double qm(final double sinφ) {
+        /*
+         * Check for zero eccentricity is required because qm_ellipsoid(sinφ) would
+         * simplify to sinφ + atanh(0) / 0 == sinφ + 0/0, thus producing NaN.
+         */
+        return (eccentricity == 0) ? 2*sinφ : qm_ellipsoid(sinφ);
+    }
+
+    /**
+     * Same as {@link #qm(double)} but without check about whether the map projection is a spherical case.
+     * It is caller responsibility to ensure that this method is not invoked in the spherical case, since
+     * this implementation does not work in such case.
+     *
+     * @param  sinφ sine of the latitude <var>q</var> is calculated for.
+     * @return <var>q</var> from Snyder equation (3-12).
+     */
+    final double qm_ellipsoid(final double sinφ) {
         final double ℯsinφ = eccentricity * sinφ;
         return sinφ / (1 - ℯsinφ*ℯsinφ) + atanh(ℯsinφ) / eccentricity;
     }
@@ -151,23 +178,29 @@
      * @return the {@code qm} derivative at the specified latitude.
      */
     final double dqm_dφ(final double sinφ, final double cosφ) {
-        final double ℯsinφ2 = eccentricitySquared * (sinφ*sinφ);
-        return (cosφ / (1 - ℯsinφ2)) * (1 + ((1 + ℯsinφ2) / (1 - ℯsinφ2)));
+        final double t = 1 - eccentricitySquared*(sinφ*sinφ);
+        return 2*cosφ / (t*t);
     }
 
     /**
-     * Computes the latitude using equation 3-18 from Synder.
+     * Computes the latitude using equation 3-18 from Synder, followed by iterative resolution of Synder 3-16.
+     * If theory, the series expansion given by equation 3-18 (φ ≈ c₂⋅sin(2β) + c₄⋅sin(4β) + c₈⋅sin(8β)) should
+     * be used in replacement of the iterative method. However in practice the series expansion seems to not
+     * have a sufficient amount of terms for achieving the centimetric precision, so we "finish" it by the
+     * iterative method. The series expansion is nevertheless useful for reducing the number of iterations.
      *
-     * @param  sinβ see Synder equation 10-26.
+     * @param  y in the cylindrical case, this is northing on the normalized ellipsoid.
      * @return the latitude in radians.
      */
-    final double φ(final double sinβ) {
+    final double φ(final double y) throws ProjectionException {
+        final double sinβ = y / qmPolar;
         final double β = asin(sinβ);
+        double φ;
         if (!ALLOW_TRIGONOMETRIC_IDENTITIES) {
-            return ci8 * sin(8*β)
-                 + ci4 * sin(4*β)
-                 + ci2 * sin(2*β)
-                 + β;                                                               // Synder 3-18
+            φ = ci8 * sin(8*β)
+              + ci4 * sin(4*β)
+              + ci2 * sin(2*β)
+              + β;                                                                  // Synder 3-18
         } else {
             /*
              * Same formula than above, but rewriten using trigonometric identities in order to avoid
@@ -183,8 +216,33 @@
             assert identityEquals(t4β, sin(4*β) / ( 8 * t2β));
             assert identityEquals(t8β, sin(8*β) / (64 * t2β));
 
-            return (ci8*t8β  +  ci4*t4β  +  ci2) * t2β  +  β;
+            φ = (ci8*t8β  +  ci4*t4β  +  ci2) * t2β  +  β;
         }
+        /*
+         * At this point φ is close to the desired value, but may have an error of a few centimetres.
+         * Use the iterative method for reaching the last part of missing accuracy. Usually this loop
+         * will perform exactly one iteration, no more, because φ is already quite close to the result.
+         *
+         * Mathematical note: Synder 3-16 gives q/(1-ℯ²) instead of y in the calculation of Δφ below.
+         * For Cylindrical Equal Area projection, Synder 10-17 gives  q = (qPolar⋅sinβ), which simplifies
+         * as y.
+         *
+         * For Albers Equal Area projection, Synder 14-19 gives  q = (C - ρ²n²/a²)/n,  which we rewrite
+         * as  q = (C - ρ²)/n  (see comment in AlbersEqualArea.inverseTransform(…) for the mathematic).
+         * The y value given to this method is y = (C - ρ²) / (n⋅(1-ℯ²)) = q/(1-ℯ²), the desired value.
+         */
+        for (int i=0; i<MAXIMUM_ITERATIONS; i++) {
+            final double sinφ  = sin(φ);
+            final double cosφ  = cos(φ);
+            final double ℯsinφ = eccentricity * sinφ;
+            final double ome   = 1 - ℯsinφ*ℯsinφ;
+            final double Δφ    = ome*ome/(2*cosφ) * (y - sinφ/ome - atanh(ℯsinφ)/eccentricity);
+            φ += Δφ;
+            if (abs(Δφ) <= ITERATION_TOLERANCE) {
+                return φ;
+            }
+        }
+        throw new ProjectionException(Errors.format(Errors.Keys.NoConvergence));
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
index 237afba..ec356e1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Initializer.java
@@ -295,7 +295,7 @@
      *
      * <blockquote>ν = 1 / √(1 - ℯ²⋅sin²φ)</blockquote>
      *
-     * This method returns 1/ν².
+     * This method returns 1/ν², which is the (1 - ℯ²⋅sin²φ) part of above equation.
      *
      * <div class="section">Relationship with Snyder</div>
      * This is related to functions (14-15) from Snyder (used for computation of scale factors
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
index a289459..4d6b8af 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertConicConformal.java
@@ -117,7 +117,7 @@
     }
 
     /**
-     * Internal coefficients for computation, depending only on values of standards parallels.
+     * 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.
      *
@@ -150,8 +150,8 @@
      *   <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.
+     * @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));
@@ -274,11 +274,11 @@
          * 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 (abs(φ1 - φ2) >= ANGULAR_TOLERANCE) {  // Should be 'true' for 2SP case.
+        if (abs(φ1 - φ2) >= ANGULAR_TOLERANCE) {                    // Should be 'true' for 2SP case.
             final double sinφ2 = sin(φ2);
             final double m2 = initializer.scaleAtφ(sinφ2, cos(φ2));
             final double t2 = expOfNorthing(φ2, eccentricity*sinφ2);
-            n = log(m1/m2) / log(t1/t2);    // Tend toward 0/0 if φ1 ≈ φ2.
+            n = log(m1/m2) / log(t1/t2);                            // Tend toward 0/0 if φ1 ≈ φ2.
         } else {
             n = -sinφ1;
         }
@@ -375,8 +375,8 @@
      * <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.
+     * @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
@@ -560,7 +560,7 @@
             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  ] = atan2(x, y);                     // Really (x,y), not (y,x);
             dstPts[dstOff+1] = PI/2 - 2*atan(pow(1/ρ, 1/n));
         }
     }
diff --git a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
index e93d3d7..3a0f820 100644
--- a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
+++ b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod
@@ -27,13 +27,14 @@
 org.apache.sis.internal.referencing.provider.PseudoMercator
 org.apache.sis.internal.referencing.provider.RegionalMercator
 org.apache.sis.internal.referencing.provider.MillerCylindrical
-org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea
-org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical
 org.apache.sis.internal.referencing.provider.LambertConformal1SP
 org.apache.sis.internal.referencing.provider.LambertConformal2SP
 org.apache.sis.internal.referencing.provider.LambertConformalWest
 org.apache.sis.internal.referencing.provider.LambertConformalBelgium
 org.apache.sis.internal.referencing.provider.LambertConformalMichigan
+org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea
+org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical
+org.apache.sis.internal.referencing.provider.AlbersEqualArea
 org.apache.sis.internal.referencing.provider.TransverseMercator
 org.apache.sis.internal.referencing.provider.TransverseMercatorSouth
 org.apache.sis.internal.referencing.provider.PolarStereographicA
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
index cbe2c51..31e571b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java
@@ -79,13 +79,14 @@
             PseudoMercator.class,
             RegionalMercator.class,
             MillerCylindrical.class,
-            LambertCylindricalEqualArea.class,
-            LambertCylindricalEqualAreaSpherical.class,
             LambertConformal1SP.class,
             LambertConformal2SP.class,
             LambertConformalWest.class,
             LambertConformalBelgium.class,
             LambertConformalMichigan.class,
+            LambertCylindricalEqualArea.class,
+            LambertCylindricalEqualAreaSpherical.class,
+            AlbersEqualArea.class,
             TransverseMercator.class,
             TransverseMercatorSouth.class,
             PolarStereographicA.class,
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/AlbersEqualAreaTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/AlbersEqualAreaTest.java
new file mode 100644
index 0000000..75265e2
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/AlbersEqualAreaTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.referencing.provider.MapProjection;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.parameter.Parameters;
+
+import static java.lang.StrictMath.*;
+
+// Test dependencies
+import org.opengis.test.ToleranceModifier;
+import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestUtilities;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link AlbersEqualArea} class. We test using various values of standard parallels.
+ * We do not test with various values of the latitude of origin, because its only effect is to
+ * modify the translation term on the <var>y</var> axis.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @author  Rémi Maréchal (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+@DependsOn(CylindricalEqualAreaTest.class)
+public final strictfp class AlbersEqualAreaTest extends MapProjectionTestCase {
+    /**
+     * Creates a new map projection. See the class javadoc for an explanation about
+     * why we ask only for the standard parallels and not the latitude of origin.
+     *
+     * @param  a   semi-major axis length.
+     * @param  b   semi-minor axis length.
+     * @param  φ1  first standard parallel.
+     * @param  φ2  second standard parallel.
+     * @return newly created projection.
+     * @throws FactoryException if an error occurred while creating the map projection.
+     */
+    private static MathTransform create(final double a, final double b, double φ1, double φ2) throws FactoryException {
+        final MapProjection method = new org.apache.sis.internal.referencing.provider.AlbersEqualArea();
+        final Parameters values = Parameters.castOrWrap(method.getParameters().createValue());
+        values.parameter("semi_major").setValue(a);
+        values.parameter("semi_minor").setValue(b);
+        values.parameter("standard_parallel_1").setValue(φ1);
+        values.parameter("standard_parallel_2").setValue(φ2);
+        return method.createMathTransform(DefaultFactories.forBuildin(MathTransformFactory.class), values);
+    }
+
+    /**
+     * Returns whether the given projection is the spherical implementation.
+     */
+    private static boolean isSpherical(final AlbersEqualArea transform) {
+        return transform instanceof AlbersEqualArea.Spherical;
+    }
+
+    /**
+     * Tests the unitary projection on a sphere.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    public void testSphere() throws FactoryException, TransformException {
+        final double delta = toRadians(100.0 / 60) / 1852;                  // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        transform = create(6370997, 6370997, 29.5, 45.5);                   // Standard parallels from Synder table 15.
+        final AlbersEqualArea kernel = (AlbersEqualArea) getKernel();
+        assertTrue("isSpherical", isSpherical(kernel));
+        assertEquals("n", 0.6028370, kernel.nm, 0.5E-7);                    // Expected 'n' value from Synder table 15.
+        /*
+         * When stepping into the AlbersEqualArea.Sphere.transform(…) method with a debugger, the
+         * expected value of 6370997*ρ/n is 6910941 (value taken from ρ column in Synder table 15).
+         */
+        verifyTransform(new double[] {0, 50}, new double[] {0, 5373933.180});
+        /*
+         * Expect 6370997*ρ/n  ≈  8022413   (can be verified only with the debugger)
+         */
+        verifyTransform(new double[] {0, 40}, new double[] {0, 4262461.266});
+        /*
+         * Expect 6370997*ρ/n  ≈  9695749   (can be verified only with the debugger)
+         */
+        verifyTransform(new double[] {0, 25}, new double[] {0, 2589125.654});
+        /*
+         * Verify consistency with random points.
+         */
+        verifyInDomain(new double[] {-20, 20}, new double[] {20, 50}, new int[] {5, 5},
+                TestUtilities.createRandomNumberGenerator());
+    }
+
+    /**
+     * Tests the unitary projection on an ellipse.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    @DependsOnMethod("testSphere")
+    public void testEllipse() throws FactoryException, TransformException {
+        final double delta = toRadians(100.0 / 60) / 1852;                  // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        transform = create(6378206.4, 6356583.8, 29.5, 45.5);               // Standard parallels from Synder table 15.
+        final AlbersEqualArea kernel = (AlbersEqualArea) getKernel();
+        assertFalse("isSpherical", isSpherical(kernel));
+        /*
+         * Expected 'n' value from Synder table 15. The division by (1-ℯ²) is because Apache SIS omits this factor
+         * in its calculation of n (we rather take it in account in (de)normalization matrices and elsewhere).
+         */
+        assertEquals("n", 0.6029035, kernel.nm / (1 - kernel.eccentricitySquared), 0.5E-7);
+        /*
+         * When stepping into the AlbersEqualArea.Sphere.transform(…) method with a debugger, the expected
+         * value of 6378206.4*ρ/(nm/(1-ℯ²)) is 6931335 (value taken from ρ column in Synder table 15).
+         */
+        verifyTransform(new double[] {0, 50}, new double[] {0, 5356698.435});
+        /*
+         * Expect 6378206.4*ρ/(nm/(1-ℯ²))  ≈  8042164   (can be verified only with the debugger)
+         */
+        verifyTransform(new double[] {0, 40}, new double[] {0, 4245869.390});
+        /*
+         * Expect 6378206.4*ρ/(nm/(1-ℯ²))  ≈  9710969   (can be verified only with the debugger)
+         */
+        verifyTransform(new double[] {0, 25}, new double[] {0, 2577064.350});
+        /*
+         * Verify consistency with random points.
+         */
+        verifyInDomain(new double[] {-20, 20}, new double[] {20, 50}, new int[] {5, 5},
+                TestUtilities.createRandomNumberGenerator());
+    }
+
+    /**
+     * Uses Proj.4 test point has a reference.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    @DependsOnMethod("testEllipse")
+    public void compareWithProj4() throws FactoryException, TransformException {
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        tolerance = Formulas.LINEAR_TOLERANCE;
+
+        // Spherical case
+        transform = create(6400000, 6400000, 0, 2);
+        verifyTransform(new double[] {2, 1}, new double[] {223334.085, 111780.432});
+
+        // Ellipsoidal case
+        transform = create(6378137, 6356752.314140347, 0, 2);
+        verifyTransform(new double[] {2, 1}, new double[] {222571.609, 110653.327});
+    }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MapProjectionTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MapProjectionTestCase.java
index ed95188..8ee3fbb 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MapProjectionTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MapProjectionTestCase.java
@@ -19,6 +19,7 @@
 import javax.measure.unit.NonSI;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.test.referencing.ParameterizedTransformTest;
 import org.apache.sis.parameter.Parameters;
@@ -28,6 +29,7 @@
 import org.apache.sis.referencing.operation.transform.CoordinateDomain;
 import org.apache.sis.referencing.operation.transform.MathTransformTestCase;
 import org.apache.sis.referencing.operation.transform.MathTransformFactoryMock;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.datum.GeodeticDatumMock;
 
 import static java.lang.StrictMath.*;
@@ -39,7 +41,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 strictfp class MapProjectionTestCase extends MathTransformTestCase {
@@ -57,9 +59,9 @@
     /**
      * Returns the parameters to use for instantiating the projection to test.
      *
-     * @param  provider The provider of the projection to test.
-     * @param  ellipse {@code false} for a sphere, or {@code true} for WGS84 ellipsoid.
-     * @return The parameters to use for instantiating the projection.
+     * @param  provider  the provider of the projection to test.
+     * @param  ellipse   {@code false} for a sphere, or {@code true} for WGS84 ellipsoid.
+     * @return the parameters to use for instantiating the projection.
      */
     static Parameters parameters(final DefaultOperationMethod provider, final boolean ellipse) {
         final Parameters parameters = Parameters.castOrWrap(provider.getParameters().createValue());
@@ -75,8 +77,8 @@
     /**
      * Instantiates the object to use for running GeoAPI test.
      *
-     * @param  provider The provider of the projection to test.
-     * @return The GeoAPI test class using the given provider.
+     * @param  provider  the provider of the projection to test.
+     * @return the GeoAPI test class using the given provider.
      */
     static ParameterizedTransformTest createGeoApiTest(final MapProjection provider) {
         return new ParameterizedTransformTest(new MathTransformFactoryMock(provider));
@@ -107,11 +109,26 @@
     }
 
     /**
+     * Returns the {@code NormalizedProjection} component of the current transform.
+     */
+    final NormalizedProjection getKernel() {
+        NormalizedProjection kernel = null;
+        for (final MathTransform component : MathTransforms.getSteps(transform)) {
+            if (component instanceof NormalizedProjection) {
+                assertNull("Found more than one kernel.", kernel);
+                kernel = (NormalizedProjection) component;
+            }
+        }
+        assertNotNull("Kernel not found.", kernel);
+        return kernel;
+    }
+
+    /**
      * Projects the given latitude value. The longitude is fixed to zero.
      * This method is useful for testing the behavior close to poles in a simple case.
      *
-     * @param  φ The latitude.
-     * @return The northing.
+     * @param  φ  the latitude.
+     * @return the northing.
      * @throws ProjectionException if the projection failed.
      */
     final double transform(final double φ) throws ProjectionException {
@@ -129,8 +146,8 @@
      * Inverse projects the given northing value. The easting is fixed to zero.
      * This method is useful for testing the behavior close to poles in a simple case.
      *
-     * @param  y The northing.
-     * @return The latitude.
+     * @param  y  the northing.
+     * @return the latitude.
      * @throws ProjectionException if the projection failed.
      */
     final double inverseTransform(final double y) throws ProjectionException {
@@ -159,9 +176,9 @@
      * The spherical formulas are arbitrarily selected as the reference implementation because they are simpler,
      * so less bug-prone, than the elliptical formulas.
      *
-     * @param  domain The domain of the numbers to be generated.
-     * @param  randomSeed The seed for the random number generator, or 0 for choosing a random seed.
-     * @throws TransformException If a conversion, transformation or derivative failed.
+     * @param  domain      the domain of the numbers to be generated.
+     * @param  randomSeed  the seed for the random number generator, or 0 for choosing a random seed.
+     * @throws TransformException if a conversion, transformation or derivative failed.
      */
     final void compareEllipticalWithSpherical(final CoordinateDomain domain, final long randomSeed)
             throws TransformException
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index 3a5fdc9..081e003 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -165,6 +165,7 @@
     org.apache.sis.referencing.operation.projection.PolarStereographicTest.class,
     org.apache.sis.referencing.operation.projection.ObliqueStereographicTest.class,
     org.apache.sis.referencing.operation.projection.CylindricalEqualAreaTest.class,
+    org.apache.sis.referencing.operation.projection.AlbersEqualAreaTest.class,
 
     // Coordinate operation and derived Coordinate Reference Systems (cyclic dependency).
     org.apache.sis.referencing.operation.DefaultTransformationTest.class,