Merge the addition of AlbersEqualArea projection from JDK7 branch.


git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK6@1754380 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
index 972b077..7e7f3ab 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
@@ -32,6 +32,7 @@
 import org.opengis.util.GenericName;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.io.TabularFormat;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
@@ -47,7 +48,6 @@
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.feature.Operation;
-import org.apache.sis.util.CharSequences;
 
 
 /**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
index 15aa566..6a9ba54 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
@@ -18,6 +18,8 @@
 
 import org.opengis.util.GenericName;
 import org.apache.sis.feature.DefaultAssociationRole;
+
+// Branch-dependent imports
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
 import org.opengis.feature.FeatureAssociationRole;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
index 891dcf0..e5d1abf 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
@@ -32,7 +32,6 @@
 // Branch-dependent imports
 import org.apache.sis.internal.jdk7.Objects;
 import org.opengis.feature.AttributeType;
-import org.opengis.feature.Feature;
 import org.opengis.feature.PropertyType;
 
 
@@ -254,9 +253,6 @@
      * @see #characteristics()
      */
     public <C> CharacteristicTypeBuilder<C> addCharacteristic(final Class<C> type) {
-        if (valueClass == Feature.class) {
-            throw new UnsupportedOperationException(errors().getString(Errors.Keys.IllegalOperationForValueClass_1, valueClass));
-        }
         ensureNonNull("type", type);
         final CharacteristicTypeBuilder<C> characteristic = new CharacteristicTypeBuilder<C>(this, type);
         characteristics.add(characteristic);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
index da3eed1..acd4eb4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/PropertyTypeBuilder.java
@@ -18,6 +18,8 @@
 
 import org.opengis.util.GenericName;
 import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
diff --git a/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
index 3dd944b..16e1d5a 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/feature/builder/FeatureTypeBuilderTest.java
@@ -19,8 +19,11 @@
 import java.util.Iterator;
 import com.esri.core.geometry.Geometry;
 import com.esri.core.geometry.Point;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.feature.DefaultFeatureTypeTest;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -30,9 +33,6 @@
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
-import org.apache.sis.feature.DefaultFeatureTypeTest;
-import org.apache.sis.internal.feature.AttributeConvention;
-import org.apache.sis.test.TestUtilities;
 
 
 /**
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/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 7258591..f7323e1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -39,6 +39,7 @@
 import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.EngineeringCRS;
+import org.opengis.referencing.operation.OperationNotFoundException;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
@@ -282,11 +283,17 @@
      *       for checking if the operation has sufficient accuracy for caller's purpose.</li>
      * </ul>
      *
+     * If the source and target CRS are equivalent, then this method returns an operation backed by an
+     * {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform#isIdentity() identity}
+     * transform. If there is no known operation between the given pair of CRS, then this method throws an
+     * {@link OperationNotFoundException}.
+     *
      * @param  sourceCRS      the CRS of source coordinates.
      * @param  targetCRS      the CRS of target coordinates.
      * @param  areaOfInterest the area of interest, or {@code null} if none.
      * @return the mathematical operation from {@code sourceCRS} to {@code targetCRS}.
-     * @throws FactoryException if the operation can not be created.
+     * @throws OperationNotFoundException if no operation was found between the given pair of CRS.
+     * @throws FactoryException if the operation can not be created for another reason.
      *
      * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index 2fe6697..60d88e5 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -356,8 +356,12 @@
         final CoordinateOperation step1;
         try {
             step1 = inverse(sourceCRS.getConversionFromBase());
+        } catch (OperationNotFoundException exception) {
+            throw exception;
+        } catch (FactoryException exception) {
+            throw new OperationNotFoundException(canNotInvert(sourceCRS), exception);
         } catch (NoninvertibleTransformException exception) {
-            throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
+            throw new OperationNotFoundException(canNotInvert(sourceCRS), exception);
         }
         return concatenate(step1, step2);
     }
@@ -387,8 +391,12 @@
         final CoordinateOperation step1;
         try {
             step1 = inverse(sourceCRS.getConversionFromBase());
+        } catch (OperationNotFoundException exception) {
+            throw exception;
+        } catch (FactoryException exception) {
+            throw new OperationNotFoundException(canNotInvert(sourceCRS), exception);
         } catch (NoninvertibleTransformException exception) {
-            throw new OperationNotFoundException(notFoundMessage(sourceCRS, targetCRS), exception);
+            throw new OperationNotFoundException(canNotInvert(sourceCRS), exception);
         }
         return concatenate(step1, step2, step3);
     }
@@ -1064,4 +1072,15 @@
     private static String notFoundMessage(final IdentifiedObject source, final IdentifiedObject target) {
         return Errors.format(Errors.Keys.CoordinateOperationNotFound_2, CRSPair.label(source), CRSPair.label(target));
     }
+
+    /**
+     * Returns an error message for "Can not invert operation XYZ.".
+     * This is used for the construction of {@link OperationNotFoundException}.
+     *
+     * @param  crs  the CRS having a conversion that can not be inverted.
+     * @return A default error message.
+     */
+    private static String canNotInvert(final GeneralDerivedCRS crs) {
+        return Errors.format(Errors.Keys.NonInvertibleOperation_1, crs.getConversionFromBase().getName().getCode());
+    }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 965b25a..0276e37 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -500,24 +500,52 @@
 
     /**
      * Creates the inverse of the given single operation.
+     * If this operation succeed, then the returned coordinate operations has the following properties:
+     *
+     * <ul>
+     *   <li>Its {@code sourceCRS} is the {@code targetCRS} of the given operation.</li>
+     *   <li>Its {@code targetCRS} is the {@code sourceCRS} of the given operation.</li>
+     *   <li>Its {@code interpolationCRS} is {@code null}.</li>
+     *   <li>Its {@code MathTransform} is the
+     *       {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform#inverse() inverse}
+     *       of the {@code MathTransform} of this operation.</li>
+     *   <li>Its domain of validity and accuracy is the same.</li>
+     * </ul>
+     *
+     * <div class="note"><b>Note:</b>
+     * in many cases, the inverse operation is numerically less accurate than the direct operation because it
+     * uses approximations like series expansions or iterative methods. However the numerical errors caused by
+     * those approximations are not of interest here, because they are usually much smaller than the inaccuracy
+     * due to the stochastic nature of coordinate transformations (not to be confused with coordinate conversions;
+     * see ISO 19111 for more information).</div>
      */
     final CoordinateOperation inverse(final SingleOperation op) throws NoninvertibleTransformException, FactoryException {
         final CoordinateReferenceSystem sourceCRS = op.getSourceCRS();
         final CoordinateReferenceSystem targetCRS = op.getTargetCRS();
         final MathTransform transform = op.getMathTransform().inverse();
+        final OperationMethod method = InverseOperationMethod.create(op.getMethod());
+        final Map<String,Object> properties = properties(INVERSE_OPERATION);
+        InverseOperationMethod.properties(op, properties);
+        /*
+         * Find a hint about whether the coordinate operation is a transformation or a conversion,
+         * but do not set any conversion subtype. In particular, do not specify a Projection type,
+         * because the inverse of a Projection does not implement the Projection interface.
+         */
         Class<? extends CoordinateOperation> type = null;
         if (op instanceof Transformation)  type = Transformation.class;
         else if (op instanceof Conversion) type = Conversion.class;
-        final Map<String,Object> properties = properties(INVERSE_OPERATION);
-        InverseOperationMethod.putMetadata(op, properties);
-        InverseOperationMethod.putParameters(op, properties);
-        return createFromMathTransform(properties, targetCRS, sourceCRS,
-                transform, InverseOperationMethod.create(op.getMethod()), null, type);
+        return createFromMathTransform(properties, targetCRS, sourceCRS, transform, method, null, type);
     }
 
     /**
      * Creates the inverse of the given operation, which may be single or compound.
      *
+     * <p><b>Design note:</b>
+     * we do not provide a {@code AbstractCoordinateOperation.inverse()} method. If the user wants an inverse method,
+     * he should invoke {@code CRS.findOperation(targetCRS, sourceCRS, null)} or something equivalent. This is because
+     * a new query of EPSG database may be necessary, and if no explicit definition is found there is too many arbitrary
+     * values to set in a default inverse operation for making that API public.</p>
+     *
      * @param  operation The operation to invert, or {@code null}.
      * @return The inverse of {@code operation}, or {@code null} if none.
      * @throws NoninvertibleTransformException if the operation is not invertible.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/InverseOperationMethod.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/InverseOperationMethod.java
index 21af1bd..09b865e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/InverseOperationMethod.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/InverseOperationMethod.java
@@ -45,7 +45,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @XmlTransient
@@ -107,7 +107,15 @@
     }
 
     /**
-     * Copies accuracy and domain of validity metadata from the given operation into the given properties map.
+     * Infers the properties to give to an inverse coordinate operation.
+     * The returned map will contains three kind of information:
+     *
+     * <ul>
+     *   <li>Metadata (domain of validity, accuracy)</li>
+     *   <li>Parameter values, if possible</li>
+     * </ul>
+     *
+     * This method copies accuracy and domain of validity metadata from the given operation.
      * We presume that the inverse operation has the same accuracy than the direct operation.
      *
      * <div class="note"><b>Note:</b>
@@ -116,25 +124,25 @@
      * those approximations are not of interest here, because they are usually much smaller than the inaccuracy
      * due to the stochastic nature of coordinate transformations (not to be confused with coordinate conversions;
      * see ISO 19111 for more information).</div>
+     *
+     * If the inverse of the given operation can be represented by inverting the sign of all numerical
+     * parameter values, then this method copies also those parameters in a {@code "parameters"} entry.
+     *
+     * @param source  the operation for which to get the inverse parameters.
+     * @param target  where to store the inverse parameters.
      */
-    static void putMetadata(final SingleOperation source, final Map<String,Object> target) {
+    static void properties(final SingleOperation source, final Map<String,Object> target) {
         target.put(SingleOperation.DOMAIN_OF_VALIDITY_KEY, source.getDomainOfValidity());
         final Collection<PositionalAccuracy> accuracy = source.getCoordinateOperationAccuracy();
         if (!Containers.isNullOrEmpty(accuracy)) {
             target.put(SingleOperation.COORDINATE_OPERATION_ACCURACY_KEY,
                     accuracy.toArray(new PositionalAccuracy[accuracy.size()]));
         }
-    }
-
-    /**
-     * If the inverse of the given operation can be represented by inverting the sign of all numerical
-     * parameter values, copies those parameters in a {@code "parameters"} entry in the given map.
-     * Otherwise does nothing.
-     *
-     * @param source  the operation for which to get the inverse parameters.
-     * @param target  where to store the inverse parameters.
-     */
-    static void putParameters(final SingleOperation source, final Map<String,Object> target) {
+        /*
+         * If the inverse of the given operation can be represented by inverting the sign of all numerical
+         * parameter values, copies those parameters in a "parameters" entry in the properties map.
+         * Otherwise does nothing.
+         */
         final boolean isInvertible = isInvertible(source.getMethod());
         final ParameterValueGroup parameters = source.getParameterValues();
         final ParameterValueGroup copy = parameters.getDescriptor().createValue();
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..16fb4e3
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/AlbersEqualArea.java
@@ -0,0 +1,349 @@
+/*
+ * 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, ParameterDescriptor<Double>>(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];
+        /*
+         * Note: Synder suggests to reverse the sign of x, y and ρ₀ if n is negative. It should not done in Apache SIS
+         * implementation because (x,y) are premultiplied by n (by the normalization affine transform) before to enter
+         * in this method, so if n was negative those values have already their sign reverted.
+         */
+        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 ef5c624..a582e93 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.
@@ -177,16 +170,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);
     }
 
     /**
@@ -195,7 +179,6 @@
     CylindricalEqualArea(final CylindricalEqualArea other) {
         super(other);
         variant = other.variant;
-        qmPolar = other.qmPolar;
     }
 
     /**
@@ -236,8 +219,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.
@@ -266,7 +249,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)
             }
         }
     }
@@ -284,7 +267,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..71ffb1c 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;
@@ -50,6 +51,14 @@
      *
      * Note that since this boolean is static final, the compiler should exclude the code in the branch that is never
      * executed (no need to comment-out that code).
+     *
+     *
+     * <p><b>BENCHMARK AND ANALYSIS:</b>
+     * as of July 2016, benchmarking shows no benefit in using trigonometric identities for {@code EqualAreaProjection}
+     * (contrarily to {@link ConformalProjection} where we did measured a benefit). This may be because in this class,
+     * the series expansion is unconditionally followed by iterative method in order to reach the centimetric precision.
+     * We observe that the original series expansion allows convergence in only one iteration, while the formulas using
+     * trigonometric identifies often requires two iterations. Consequently we disallow those modifications for now.</p>
      */
     private static final boolean ALLOW_TRIGONOMETRIC_IDENTITIES = false;
 
@@ -70,6 +79,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 +100,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 +116,7 @@
             ci4 *=  8;
             ci8 *= 64;
         }
+        qmPolar = qm(1);
     }
 
     /**
@@ -113,6 +130,7 @@
         ci2 = other.ci2;
         ci4 = other.ci4;
         ci8 = other.ci8;
+        qmPolar = other.qmPolar;
     }
 
     /**
@@ -120,6 +138,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 +150,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 +186,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
@@ -179,24 +220,62 @@
             final double t4β = 0.5 - sin2_β;                                        // = sin(4β) / ( 4⋅sin(2β))
             final double t8β = (cos2_β - sin2_β)*(cos2_β*cos2_β - cos2_β + 1./8);   // = sin(8β) / (32⋅sin(2β))
 
-            assert identityEquals(t2β, sin(2*β) / ( 2      ));
-            assert identityEquals(t4β, sin(4*β) / ( 8 * t2β));
-            assert identityEquals(t8β, sin(8*β) / (64 * t2β));
+            assert ConformalProjection.identityEquals(t2β, sin(2*β) / ( 2      ));
+            assert ConformalProjection.identityEquals(t4β, sin(4*β) / ( 8 * t2β));
+            assert ConformalProjection.identityEquals(t8β, sin(8*β) / (64 * t2β));
 
-            return (ci8*t8β  +  ci4*t4β  +  ci2) * t2β  +  β;
+            φ = (ci8*t8β  +  ci4*t4β  +  ci2) * t2β  +  β;
         }
-    }
-
-    /**
-     * Verifies if a trigonometric identity produced the expected value. This method is used in assertions only.
-     * The tolerance threshold is approximatively 1.5E-12 (note that it still about 7000 time greater than
-     * {@code Math.ulp(1.0)}).
-     *
-     * @see #ALLOW_TRIGONOMETRIC_IDENTITIES
-     */
-    private static boolean identityEquals(final double actual, final double expected) {
-        // Use !(a > b) instead of (a <= b) in order to tolerate NaN.
-        return !(abs(actual - expected) > (ANGULAR_TOLERANCE / 1000));
+        /*
+         * 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 φ;
+            }
+        }
+        /*
+         * In the Albers Equal Area discussion, Synder said that above algorithm does not converge if
+         *
+         *   q = ±(1 - (1-ℯ²)/(2ℯ) ⋅ ln((1-ℯ)/(1+ℯ)))
+         *
+         * which we rewrite as
+         *
+         *   q = ±(1 + (1-ℯ²)⋅atanh(ℯ)/ℯ)
+         *
+         * Given that y = q/(1-ℯ²)  (see above comment), we rewrite as
+         *
+         *   y  =  ±(1/(1-ℯ²) + atanh(ℯ)/ℯ)  =  ±qmPolar
+         *
+         * which implies  sinβ = ±1. This is consistent with Synder discussion of Cylndrical Equal Area
+         * projection, where he said exactly that about the same formula (that it does not converge for
+         * β = ±90°). In both case, Synder said that the result is φ = β, with the same sign.
+         */
+        final double as = abs(sinβ);
+        if (abs(as - 1) < ANGULAR_TOLERANCE) {
+            return copySign(PI/2, y);               // Value is at a pole.
+        }
+        if (as >= 1 || Double.isNaN(y)) {
+            return Double.NaN;                      // Value "after" the pole.
+        }
+        // Value should have converged but did not.
+        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 9b2d962..a4447ae 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));
@@ -275,11 +275,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;
         }
@@ -376,8 +376,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
@@ -561,7 +561,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 762e2ac..bc4e963 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..54535d0
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/AlbersEqualAreaTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.TransformException;
+import org.apache.sis.internal.referencing.Formulas;
+
+import static java.lang.StrictMath.*;
+import static java.lang.Double.NaN;
+
+// 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 {
+    /**
+     * 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 {
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                6370997,    // Semi-major axis from Synder table 15
+                6370997,    // Semi-minor axis
+                0,          // Central meridian
+                0,          // Latitude of origin
+                29.5,       // Standard parallel 1 (from Synder table 15)
+                45.5,       // Standard parallel 2 (from Synder table 15)
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
+
+        final double delta = toRadians(100.0 / 60) / 1852;                  // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        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},          // Minimal input ordinate values
+                       new double[] {+20, 50},          // Maximal input ordinate values
+                       new int[]    {  5,  5},          // Number of points to test
+                       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 {
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                6378206.4,  // Semi-major axis from Synder table 15
+                6356583.8,  // Semi-minor axis
+                0,          // Central meridian
+                0,          // Latitude of origin
+                29.5,       // Standard parallel 1 (from Synder table 15)
+                45.5,       // Standard parallel 2 (from Synder table 15)
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
+
+        final double delta = toRadians(100.0 / 60) / 1852;                  // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        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},          // Minimal input ordinate values
+                       new double[] {+20, 50},          // Maximal input ordinate values
+                       new int[]    {  5,  5},          // Number of points to test
+                       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
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                6400000,    // Semi-major axis
+                6400000,    // Semi-minor axis
+                0,          // Central meridian
+                0,          // Latitude of origin
+                0,          // Standard parallel 1
+                2,          // Standard parallel 2
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
+
+        verifyTransform(new double[] {2, 1}, new double[] {223334.085, 111780.432});
+
+        // Ellipsoidal case
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                6378137,            // Semi-major axis
+                6356752.314140347,  // Semi-minor axis
+                0,                  // Central meridian
+                0,                  // Latitude of origin
+                0,                  // Standard parallel 1
+                2,                  // Standard parallel 2
+                NaN,                // Scale factor (none)
+                0,                  // False easting
+                0);                 // False northing
+        verifyTransform(new double[] {2, 1}, new double[] {222571.609, 110653.327});
+    }
+
+    /**
+     * Tests a few "special" points which need special care in inverse projection algorithm.
+     *
+     * @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 testSingularity() throws FactoryException, TransformException {
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0,          // Central meridian
+                0,          // Latitude of origin
+                0,          // Standard parallel 1
+                2,          // Standard parallel 2
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
+
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        verifyTransform(new double[] {0,        0,
+                                      0,      +90,
+                                      0,      -90},
+                        new double[] {0,        0,
+                                      0, +6420271.594575703,    // Computed empirically with SIS (not from an external source).
+                                      0, -6309429.217});
+    }
+
+    /**
+     * Tests conversion of random points with non-zero central meridian, standard parallel
+     * and false easting/northing.
+     *
+     * @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 testRandomPoints() throws FactoryException, TransformException {
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.AlbersEqualArea(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                12,         // Central meridian
+                NaN,        // Latitude of origin (none)
+                24,         // Standard parallel 1
+                40,         // Standard parallel 2
+                NaN,        // Scale factor (none)
+                300,        // False easting
+                200);       // False northing
+
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        derivativeDeltas = new double[] {100, 100};
+        final double delta = toRadians(100.0 / 60) / 1852;      // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        verifyInDomain(new double[] {-40, 10},                  // Minimal input ordinate values
+                       new double[] {+40, 60},                  // Maximal input ordinate values
+                       new int[]    {  5,  5},                  // Number of points to test
+                       TestUtilities.createRandomNumberGenerator());
+    }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java
new file mode 100644
index 0000000..4dc1dbd
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/Benchmark.java
@@ -0,0 +1,281 @@
+/*
+ * 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.List;
+import java.util.Random;
+import java.io.IOException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.apache.sis.math.Statistics;
+import org.apache.sis.math.StatisticsFormat;
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.referencing.provider.AbstractProvider;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.MathTransformFactoryMock;
+
+
+/**
+ * Measures the performance of a given map projection implementation.
+ * This class can be used for comparing different implementation alternatives,
+ * for example with {@code ALLOW_TRIGONOMETRIC_IDENTITIES} flag on or off.
+ *
+ * <p><b>Usage:</b> modify the provider created in the {@code main} method if needed, and run. Change map projection
+ * implementation (for example by changing a {@code ALLOW_TRIGONOMETRIC_IDENTITIES} flag value) and run again.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+@SuppressWarnings("UseOfSystemOutOrSystemErr")
+public final class Benchmark {
+    /**
+     * Runs the benchmark and prints the time result to the standard output.
+     * Edit this method for measuring the performance of a different map projection implementation.
+     *
+     * @param  args ignored.
+     * @throws Exception if an error occurred while creating the map projection, projecting a point, <i>etc.</i>
+     */
+    public static void main(String[] args) throws Exception {
+        final Benchmark benchmark = new Benchmark(  // Put on next line the provider of the projection to benchmark.
+                new org.apache.sis.internal.referencing.provider.LambertConformal2SP(),
+                8,      // Central meridian
+                25,     // Standard parallel 1
+                40);    // Standard parallel 2
+
+        for (int i=0; i<10; i++) {
+            benchmark.run(true);
+            System.gc();
+            Thread.sleep(1000);
+        }
+        benchmark.printStatistics();
+    }
+
+    /**
+     * Number of points to project. Can be modified freely.
+     */
+    private static final int NUM_POINTS = 1000000;
+
+    /**
+     * Number of dimension (not modifiable).
+     */
+    private static final int DIMENSION = 2;
+
+    /**
+     * The math transforms to use for the forward projection.
+     */
+    private final Transforms forward;
+
+    /**
+     * The math transforms to use for the inverse projection.
+     */
+    private final Transforms inverse;
+
+    /**
+     * The coordinates to project, filled with random points.
+     */
+    private final double[] coordinates;
+
+    /**
+     * The result of map projections.
+     */
+    private final double[] result;
+
+    /**
+     * Difference between inverse projections and the original coordinates.
+     */
+    private final Statistics errors;
+
+    /**
+     * Random number generator for the points to project.
+     */
+    private final Random random;
+
+    /**
+     * Prepares benchmarking for the map projection created by the given provider.
+     */
+    private Benchmark(final AbstractProvider provider,
+                      final double centralMeridian,
+                      final double standardParallel1,
+                      final double standardParallel2) throws FactoryException, NoninvertibleTransformException
+    {
+        random = new Random();
+        final Parameters values = Parameters.castOrWrap(provider.getParameters().createValue());
+        values.parameter(Constants.SEMI_MAJOR)         .setValue(MapProjectionTestCase.WGS84_A);
+        values.parameter(Constants.SEMI_MINOR)         .setValue(MapProjectionTestCase.WGS84_B);
+        values.parameter(Constants.CENTRAL_MERIDIAN)   .setValue(centralMeridian);
+        values.parameter(Constants.STANDARD_PARALLEL_1).setValue(standardParallel1);
+        values.parameter(Constants.STANDARD_PARALLEL_2).setValue(standardParallel2);
+        forward = new Transforms("Forward", new MathTransformFactoryMock(provider).createParameterizedTransform(values));
+        inverse = new Transforms("Inverse", forward.projection.inverse());
+        coordinates = new double[NUM_POINTS * DIMENSION];
+        final double λmin = centralMeridian - 40;
+        final double Δλ   = 80;
+        final double φmin = standardParallel1 * 0.75;
+        final double Δφ   = Math.min(standardParallel2 * 1.25, Latitude.MAX_VALUE) - φmin;
+        for (int i=0; i<coordinates.length;) {
+            coordinates[i++] = random.nextDouble() * Δλ + λmin;     // Longitude
+            coordinates[i++] = random.nextDouble() * Δφ + φmin;     // Latitude
+        }
+        result = new double[coordinates.length];
+        errors = new Statistics("Errors (cm)");
+    }
+
+    /**
+     * Decomposition of the {@code MathTransform} that perform map projections.
+     */
+    private static final class Transforms {
+        /**
+         * The original (full) map projection.
+         */
+        final MathTransform projection;
+
+        /**
+         * The transform that performs the non-linear part of the map projection.
+         * This is the transform that we want to benchmark.
+         */
+        private final MathTransform kernel;
+
+        /**
+         * The normalization performed before the map projection.
+         */
+        private final LinearTransform normalize;
+
+        /**
+         * The denormalization performed after the map projection.
+         */
+        private final LinearTransform denormalize;
+
+        /**
+         * Statistics about the time needed for performing the map projection.
+         */
+        private final Statistics performance;
+
+        /**
+         * Creates a decomposition of the given map projection.
+         *
+         * @param label       a label for display purpose.
+         * @param projection  the map projection to benchmark.
+         */
+        private Transforms(final String label, final MathTransform projection) {
+            this.projection = projection;
+            performance = new Statistics(label);
+            final List<MathTransform> steps = MathTransforms.getSteps(projection);
+            int kernelIndex = -1;
+            for (int i=steps.size(); --i >= 0;) {
+                if (!(steps.get(i) instanceof LinearTransform)) {
+                    if (kernelIndex >= 0) {
+                        throw new IllegalArgumentException("Found more than one non-linear kernel.");
+                    }
+                    kernelIndex = i;
+                }
+            }
+            if (kernelIndex < 0) {
+                throw new IllegalArgumentException("Non-linear kernel not found.");
+            }
+            kernel      = steps.get(kernelIndex);
+            normalize   = singleton(steps.subList(0, kernelIndex));
+            denormalize = singleton(steps.subList(kernelIndex + 1, steps.size()));
+        }
+
+        /**
+         * Returns the single elements of the given list, or an identity transform if none.
+         */
+        private static LinearTransform singleton(final List<MathTransform> components) {
+            switch (components.size()) {
+                case 0: return MathTransforms.identity(DIMENSION);
+                case 1: return (LinearTransform) components.get(0);
+                default: throw new IllegalArgumentException("Unexpected number of components.");
+            }
+        }
+
+        /**
+         * Runs the benchmark on the complete map projection, including the linear parts.
+         */
+        final void runComplete(final double[] sources, final double[] targets) throws TransformException {
+            long time = System.nanoTime();
+            projection.transform(sources, 0, targets, 0, NUM_POINTS);
+            time = System.nanoTime() - time;
+            final double seconds = time * NANOS_TO_SECONDS;
+            System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
+            performance.accept(seconds);
+        }
+
+        /**
+         * Runs the benchmark only on the non-linear part of the map projection,
+         * ignoring the linear parts.
+         */
+        final void runKernel(final double[] sources, final double[] targets) throws TransformException {
+            normalize.transform(sources, 0, targets, 0, NUM_POINTS);
+            long time = System.nanoTime();
+            kernel.transform(targets, 0, targets, 0, NUM_POINTS);
+            time = System.nanoTime() - time;
+            denormalize.transform(targets, 0, targets, 0, NUM_POINTS);
+            final double seconds = time * NANOS_TO_SECONDS;
+            System.out.printf("%s time: %1.4f%n", performance.name(), seconds);
+            performance.accept(seconds);
+        }
+
+        /**
+         * For reporting the time measured by {@link System#nanoTime()}.
+         */
+        private static final double NANOS_TO_SECONDS = 1E-9;
+    }
+
+    /**
+     * Runs the benchmark.
+     *
+     * @param kernelOnly {@code true} for measuring the performance of only the non-linear part,
+     *                   or {@code false} for measuring the performance of the whole projection.
+     */
+    private void run(final boolean kernelOnly) throws TransformException {
+        if (kernelOnly) {
+            forward.runKernel(coordinates, result);
+            inverse.runKernel(result, result);
+        } else {
+            forward.runComplete(coordinates, result);
+            inverse.runComplete(result, result);
+        }
+        for (int i=0; i<NUM_POINTS;) {
+            final double dx = result[i] - coordinates[i]; coordinates[i++] += random.nextDouble() - 0.5;
+            final double dy = result[i] - coordinates[i]; coordinates[i++] += random.nextDouble() - 0.5;
+            errors.accept(Math.hypot(dx*DEGREES_TO_CENTIMETRES, dy*DEGREES_TO_CENTIMETRES));
+        }
+    }
+
+    /**
+     * For reporting the errors in more convenient units.
+     * This is an approximative conversion based on the nautical mile length.
+     */
+    private static final double DEGREES_TO_CENTIMETRES = 60*1852*100;
+
+    /**
+     * Prints statistics about measured time.
+     */
+    private void printStatistics() throws IOException {
+        System.out.println();
+        StatisticsFormat.getInstance().format(new Statistics[] {forward.performance, inverse.performance}, System.out);
+        System.out.printf("%nAverage error is %1.2E cm (standard deviation %1.1E).%n", errors.mean(), errors.standardDeviation(false));
+        System.out.flush();
+    }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/ConformalProjectionTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/ConformalProjectionTest.java
index e9498ae..98246fa 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/ConformalProjectionTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/ConformalProjectionTest.java
@@ -178,7 +178,7 @@
     /**
      * Tests the {@link NormalizedProjection#dy_dφ(double, double)} method.
      *
-     * @throws TransformException Should never happen.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     @DependsOnMethod("testExpOfNorthing")
@@ -235,7 +235,7 @@
      * In theory only the [-90° … +90°] range needs to be tested. However the function is still
      * consistent in the [-90° … +270°] range so we test that range for tracking this fact.
      *
-     * @throws ProjectionException Should never happen.
+     * @throws ProjectionException if an error occurred while projecting a point.
      */
     @Test
     @DependsOnMethod("testExpOfNorthing")
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/CylindricalEqualAreaTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/CylindricalEqualAreaTest.java
index f71db67..3e9267b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/CylindricalEqualAreaTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/CylindricalEqualAreaTest.java
@@ -22,10 +22,12 @@
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea;
 import org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical;
+import org.apache.sis.referencing.operation.transform.CoordinateDomain;
 import org.apache.sis.test.DependsOnMethod;
 import org.junit.Test;
 
 import static java.lang.StrictMath.*;
+import static java.lang.Double.NaN;
 
 
 /**
@@ -38,16 +40,6 @@
  */
 public final strictfp class CylindricalEqualAreaTest extends MapProjectionTestCase {
     /**
-     * Creates a map projection.
-     */
-    private void createCompleteProjection(final boolean ellipse,
-            final double centralMeridian, final double standardParallel) throws FactoryException
-    {
-        createCompleteProjection(new LambertCylindricalEqualArea(),
-                ellipse, centralMeridian, 0, standardParallel, 1, 0, 0);
-    }
-
-    /**
      * Tests the derivatives at a few points. This method compares the derivatives computed by
      * the projection with an estimation of derivatives computed by the finite differences method.
      *
@@ -69,7 +61,16 @@
      */
     @Test
     public void testEllipsoidal() throws FactoryException, TransformException {
-        createCompleteProjection(true, 0, 0);
+        createCompleteProjection(new LambertCylindricalEqualArea(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0,          // Central meridian
+                NaN,        // Latitude of origin
+                0,          // Standard parallel 1
+                NaN,        // Standard parallel 2
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         toleranceModifier = ToleranceModifier.PROJECTION;
         final double λ = 2;
@@ -90,7 +91,16 @@
     @Test
     @DependsOnMethod("testEllipsoidal")
     public void testSpherical() throws FactoryException, TransformException {
-        createCompleteProjection(false, 0, 0);
+        createCompleteProjection(new LambertCylindricalEqualArea(),
+                6371007,    // Semi-major axis length
+                6371007,    // Semi-minor axis length
+                0,          // Central meridian
+                NaN,        // Latitude of origin
+                0,          // Standard parallel 1
+                NaN,        // Standard parallel 2
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         toleranceModifier = ToleranceModifier.PROJECTION;
         final double λ = 2;
@@ -114,7 +124,16 @@
     @Test
     @DependsOnMethod("testSpherical")
     public void testSphericalWithConformalSphereRadius() throws FactoryException, TransformException {
-        createCompleteProjection(new LambertCylindricalEqualAreaSpherical(), true, 0, 0, 0, 1, 0, 0);
+        createCompleteProjection(new LambertCylindricalEqualAreaSpherical(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0,          // Central meridian
+                NaN,        // Latitude of origin
+                0,          // Standard parallel 1
+                NaN,        // Standard parallel 2
+                NaN,        // Scale factor (none)
+                0,          // False easting
+                0);         // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         toleranceModifier = ToleranceModifier.PROJECTION;
         final double λ = 2;
@@ -125,4 +144,32 @@
                         new double[] {x, y,  -x, y,  x, -y,  -x, -y});
         testDerivative();
     }
+
+    /**
+     * Tests conversion of random points with non-zero central meridian, standard parallel
+     * and false easting/northing.
+     *
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
+     */
+    @Test
+    public void testRandomPoints() throws FactoryException, TransformException {
+        createCompleteProjection(new LambertCylindricalEqualArea(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                12,         // Central meridian
+                NaN,        // Latitude of origin (none)
+                24,         // Standard parallel 1
+                NaN,        // Standard parallel 2
+                NaN,        // Scale factor (none)
+                300,        // False easting
+                200);       // False northing
+
+        tolerance = Formulas.LINEAR_TOLERANCE;
+        toleranceModifier = ToleranceModifier.PROJECTION;
+        derivativeDeltas = new double[] {100, 100};
+        final double delta = toRadians(100.0 / 60) / 1852;      // Approximatively 100 metres.
+        derivativeDeltas = new double[] {delta, delta};
+        verifyInDomain(CoordinateDomain.GEOGRAPHIC_SAFE, 0);
+    }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/EquirectangularTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/EquirectangularTest.java
index 3b03f5e..3a0da83 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/EquirectangularTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/EquirectangularTest.java
@@ -27,6 +27,7 @@
 import org.apache.sis.test.ReferencingAssert;
 import org.junit.Test;
 
+import static java.lang.Double.NaN;
 import static java.lang.StrictMath.toRadians;
 import static org.apache.sis.internal.metadata.ReferencingServices.AUTHALIC_RADIUS;
 
@@ -38,7 +39,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 public final strictfp class EquirectangularTest extends MapProjectionTestCase {
@@ -61,7 +62,7 @@
      * an affine transform, the WKT formatter should handle this projection in a special way and shows the
      * projection parameters instead than the affine transform parameters (except in "show internal" mode).
      *
-     * @throws FactoryException should never happen.
+     * @throws FactoryException if an error occurred while creating the map projection.
      */
     @Test
     public void testWKT() throws FactoryException {
@@ -90,19 +91,19 @@
     /**
      * Tests a simple transform on a sphere.
      *
-     * @throws FactoryException should never happen.
-     * @throws TransformException should never happen.
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     public void testSimpleTransform() throws FactoryException, TransformException {
         createCompleteProjection();
         verifyTransform(
-                new double[] {  // (λ,φ) coordinates in degrees to project.
+                new double[] {          // (λ,φ) coordinates in degrees to project.
                     0, 0,
                     2, 0,
                     0, 3
                 },
-                new double[] {  // Expected (x,y) results in metres.
+                new double[] {          // Expected (x,y) results in metres.
                     0,                            0,
                     AUTHALIC_RADIUS*toRadians(2), 0,
                     0, AUTHALIC_RADIUS*toRadians(3)
@@ -113,18 +114,21 @@
      * Tests conversion of random points. This test is actually of limited interest since the Equirectangular
      * projection is implemented by an affine transform, which has been tested elsewhere.
      *
-     * @throws FactoryException should never happen.
-     * @throws TransformException should never happen.
+     * @throws FactoryException if an error occurred while creating the map projection.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     public void testRandomPoints() throws FactoryException, TransformException {
-        createCompleteProjection(new Equirectangular(), true,
-                  0.5,  // Central meridian
-                  0,    // Latitude of origin (none)
-                 20,    // Standard parallel
-                  1,    // Scale factor (none)
-                200,    // False easting
-                100);   // False northing
+        createCompleteProjection(new Equirectangular(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0.5,        // Central meridian
+                0,          // Latitude of origin (none)
+                20,         // Standard parallel 1
+                NaN,        // Standard parallel 2
+                NaN,        // Scale factor (none)
+                200,        // False easting
+                100);       // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;  // Not NORMALIZED_TOLERANCE since this is not a NormalizedProjection.
         derivativeDeltas = new double[] {100, 100};
         verifyInDomain(CoordinateDomain.GEOGRAPHIC, 0);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
index 51918c4..67e69a8 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertConicConformalTest.java
@@ -49,7 +49,7 @@
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @DependsOn(ConformalProjectionTest.class)
@@ -112,12 +112,12 @@
     /**
      * Tests the projection at some special latitudes (0, ±π/2, NaN and others).
      *
-     * @throws ProjectionException Should never happen.
+     * @throws ProjectionException if an error occurred while projecting a point.
      */
     @Test
     public void testSpecialLatitudes() throws ProjectionException {
-        if (transform == null) {    // May have been initialized by 'testSphericalCase'.
-            createNormalizedProjection(true, 40);  // Elliptical case
+        if (transform == null) {                        // May have been initialized by 'testSphericalCase'.
+            createNormalizedProjection(true, 40);       // Elliptical case
         }
         final double INF = POSITIVE_INFINITY;
         assertEquals ("Not a number",     NaN, transform(NaN),            NORMALIZED_TOLERANCE);
@@ -161,7 +161,7 @@
      * Tests the derivatives at a few points. This method compares the derivatives computed by
      * the projection with an estimation of derivatives computed by the finite differences method.
      *
-     * @throws TransformException Should never happen.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     @DependsOnMethod("testSpecialLatitudes")
@@ -246,28 +246,34 @@
     @Test
     @DependsOnMethod("testLambertConicConformal1SP")
     public void testLambertConicConformalWestOrientated() throws FactoryException, TransformException {
-        createCompleteProjection(new LambertConformal1SP(), false,
-                  0.5,    // Central meridian
-                 40,      // Latitude of origin
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new LambertConformal1SP(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0.5,        // Central meridian
+                40,         // Latitude of origin
+                NaN,        // Standard parallel 1
+                NaN,        // Standard parallel 2
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
         final MathTransform reference = transform;
 
-        createCompleteProjection(new LambertConformalWest(), false,
-                  0.5,    // Central meridian
-                 40,      // Latitude of origin
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new LambertConformalWest(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0.5,        // Central meridian
+                40,         // Latitude of origin
+                NaN,        // Standard parallel 1
+                NaN,        // Standard parallel 2
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
 
         final Random random = TestUtilities.createRandomNumberGenerator();
         final double[] sources = new double[20];
         for (int i=0; i<sources.length;) {
-            sources[i++] = 20 * random.nextDouble();      // Longitude
-            sources[i++] = 10 * random.nextDouble() + 35; // Latitude
+            sources[i++] = 20 * random.nextDouble();            // Longitude
+            sources[i++] = 10 * random.nextDouble() + 35;       // Latitude
         }
         final double[] expected = new double[sources.length];
         reference.transform(sources, 0, expected, 0, sources.length/2);
@@ -326,13 +332,16 @@
     @Test
     @DependsOnMethod("testSphericalCase")
     public void compareEllipticalWithSpherical() throws FactoryException, TransformException {
-        createCompleteProjection(new LambertConformal1SP(), false,
-                  0.5,    // Central meridian
-                 40,      // Latitude of origin
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new LambertConformal1SP(),
+                6371007,    // Semi-major axis length
+                6371007,    // Semi-minor axis length
+                0.5,        // Central meridian
+                40,         // Latitude of origin
+                NaN,        // Standard parallel 1
+                NaN,        // Standard parallel 2
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         compareEllipticalWithSpherical(CoordinateDomain.GEOGRAPHIC_SAFE, 0);
     }
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..619a8e8 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
@@ -16,9 +16,9 @@
  */
 package org.apache.sis.referencing.operation.projection;
 
-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,8 +28,10 @@
 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.Double.isNaN;
 import static java.lang.StrictMath.*;
 import static org.junit.Assert.*;
 
@@ -39,10 +41,20 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
-strictfp class MapProjectionTestCase extends MathTransformTestCase {
+abstract strictfp class MapProjectionTestCase extends MathTransformTestCase {
+    /**
+     * Semi-major axis length of WGS84 ellipsoid.
+     */
+    static final double WGS84_A = 6378137;
+
+    /**
+     * Semi-minor axis length of WGS84 ellipsoid.
+     */
+    static final double WGS84_B = 6356752.314245179;
+
     /**
      * Tolerance level for comparing formulas on the unitary sphere or ellipsoid.
      */
@@ -55,11 +67,22 @@
     }
 
     /**
-     * Returns the parameters to use for instantiating the projection to test.
+     * Instantiates the object to use for running GeoAPI 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.
+     * @return the GeoAPI test class using the given provider.
+     */
+    static ParameterizedTransformTest createGeoApiTest(final MapProjection provider) {
+        return new ParameterizedTransformTest(new MathTransformFactoryMock(provider));
+    }
+
+    /**
+     * Returns the parameters to use for instantiating the projection to test.
+     * The parameters are initialized with the ellipse semi-axis lengths.
+     *
+     * @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());
@@ -73,45 +96,57 @@
     }
 
     /**
-     * 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.
-     */
-    static ParameterizedTransformTest createGeoApiTest(final MapProjection provider) {
-        return new ParameterizedTransformTest(new MathTransformFactoryMock(provider));
-    }
-
-    /**
      * Initializes a complete projection (including conversion from degrees to radians) for the given provider.
-     * This method uses arbitrary central meridian, scale factor, false easting and false northing for increasing
-     * the chances to detect a mismatch. The result is stored in the {@link #transform} field.
      */
-    final void createCompleteProjection(final DefaultOperationMethod provider, final boolean ellipse,
+    final void createCompleteProjection(final DefaultOperationMethod provider,
+            final double semiMajor,
+            final double semiMinor,
             final double centralMeridian,
             final double latitudeOfOrigin,
-            final double standardParallel,
+            final double standardParallel1,
+            final double standardParallel2,
             final double scaleFactor,
             final double falseEasting,
             final double falseNorthing) throws FactoryException
     {
-        final Parameters parameters = parameters(provider, ellipse);
-        if (centralMeridian  != 0) parameters.parameter(Constants.CENTRAL_MERIDIAN)   .setValue(centralMeridian, NonSI.DEGREE_ANGLE);
-        if (latitudeOfOrigin != 0) parameters.parameter(Constants.LATITUDE_OF_ORIGIN) .setValue(latitudeOfOrigin);
-        if (standardParallel != 0) parameters.parameter(Constants.STANDARD_PARALLEL_1).setValue(standardParallel);
-        if (scaleFactor      != 1) parameters.parameter(Constants.SCALE_FACTOR)       .setValue(scaleFactor);
-        if (falseEasting     != 0) parameters.parameter(Constants.FALSE_EASTING)      .setValue(falseEasting);
-        if (falseNorthing    != 0) parameters.parameter(Constants.FALSE_NORTHING)     .setValue(falseNorthing);
-        transform = new MathTransformFactoryMock(provider).createParameterizedTransform(parameters);
+        final Parameters values = Parameters.castOrWrap(provider.getParameters().createValue());
+        values.parameter(Constants.SEMI_MAJOR).setValue(semiMajor);
+        values.parameter(Constants.SEMI_MINOR).setValue(semiMinor);
+        if (semiMajor == WGS84_A && semiMinor == WGS84_B) {
+            values.parameter(Constants.INVERSE_FLATTENING).setValue(298.257223563);
+        }
+        if (!isNaN(centralMeridian))   values.parameter(Constants.CENTRAL_MERIDIAN)   .setValue(centralMeridian);
+        if (!isNaN(latitudeOfOrigin))  values.parameter(Constants.LATITUDE_OF_ORIGIN) .setValue(latitudeOfOrigin);
+        if (!isNaN(standardParallel1)) values.parameter(Constants.STANDARD_PARALLEL_1).setValue(standardParallel1);
+        if (!isNaN(standardParallel2)) values.parameter(Constants.STANDARD_PARALLEL_2).setValue(standardParallel2);
+        if (!isNaN(scaleFactor))       values.parameter(Constants.SCALE_FACTOR)       .setValue(scaleFactor);
+        if (!isNaN(falseEasting))      values.parameter(Constants.FALSE_EASTING)      .setValue(falseEasting);
+        if (!isNaN(falseNorthing))     values.parameter(Constants.FALSE_NORTHING)     .setValue(falseNorthing);
+        transform = new MathTransformFactoryMock(provider).createParameterizedTransform(values);
         validate();
     }
 
     /**
+     * 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 {
@@ -119,7 +154,7 @@
         coordinate[1] = φ;
         ((NormalizedProjection) transform).transform(coordinate, 0, coordinate, 0, false);
         final double y = coordinate[1];
-        if (!Double.isNaN(y) && !Double.isInfinite(y)) {
+        if (!isNaN(y) && !Double.isInfinite(y)) {
             assertEquals(0, coordinate[0], tolerance);
         }
         return y;
@@ -129,8 +164,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 {
@@ -138,7 +173,7 @@
         coordinate[1] = y;
         ((NormalizedProjection) transform).inverseTransform(coordinate, 0, coordinate, 0);
         final double φ = coordinate[1];
-        if (!Double.isNaN(φ)) {
+        if (!isNaN(φ)) {
             /*
              * Opportunistically verify that the longitude is still zero. However the longitude value is meaningless
              * at poles. We can not always use coordinate[0] for testing if we are at a pole because its calculation
@@ -159,9 +194,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/referencing/operation/projection/MercatorTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
index 002a8e7..0f15fe5 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/MercatorTest.java
@@ -44,7 +44,7 @@
  * @author  Simon Reynard (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
  * @since   0.6
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @DependsOn(ConformalProjectionTest.class)
@@ -70,7 +70,7 @@
      * the ellipsoid eccentricity. We expect nothing else because all other parameters are used
      * by the (de)normalization affine transforms instead than the {@link Mercator} class itself.
      *
-     * @throws NoninvertibleTransformException should never happen.
+     * @throws NoninvertibleTransformException if the transform can not be inverted.
      *
      * @see LambertConicConformalTest#testNormalizedWKT()
      */
@@ -90,18 +90,21 @@
      * Tests WKT of a complete map projection.
      *
      * @throws FactoryException if an error occurred while creating the map projection.
-     * @throws NoninvertibleTransformException should never happen.
+     * @throws NoninvertibleTransformException if the transform can not be inverted.
      */
     @Test
     @DependsOnMethod("testNormalizedWKT")
     public void testCompleteWKT() throws FactoryException, NoninvertibleTransformException {
-        createCompleteProjection(new Mercator1SP(), true,
-                  0.5,    // Central meridian
-                  0,      // Latitude of origin (none)
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new Mercator1SP(),
+                WGS84_A,    // Semi-major axis length
+                WGS84_B,    // Semi-minor axis length
+                0.5,        // Central meridian
+                NaN,        // Latitude of origin (none)
+                NaN,        // Standard parallel 1 (none)
+                NaN,        // Standard parallel 2 (none)
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
 
         assertWktEquals("PARAM_MT[“Mercator_1SP”,\n" +
                         "  PARAMETER[“semi_major”, 6378137.0],\n" +
@@ -289,13 +292,16 @@
     @Test
     @DependsOnMethod("testSphericalCase")
     public void compareEllipticalWithSpherical() throws FactoryException, TransformException {
-        createCompleteProjection(new Mercator1SP(), false,
-                  0.5,    // Central meridian
-                  0,      // Latitude of origin (none)
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new Mercator1SP(),
+                6371007,    // Semi-major axis length
+                6371007,    // Semi-minor axis length
+                0.5,        // Central meridian
+                NaN,        // Latitude of origin (none)
+                NaN,        // Standard parallel 1 (none)
+                NaN,        // Standard parallel 2 (none)
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         compareEllipticalWithSpherical(CoordinateDomain.GEOGRAPHIC_SAFE, 0);
     }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
index 66ea07a..d415658 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/PolarStereographicTest.java
@@ -32,6 +32,7 @@
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
+import static java.lang.Double.NaN;
 import static java.lang.StrictMath.*;
 
 
@@ -40,7 +41,7 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.6
- * @version 0.6
+ * @version 0.8
  * @module
  */
 @DependsOn(NormalizedProjectionTest.class)
@@ -169,11 +170,14 @@
     private void compareEllipticalWithSpherical(final CoordinateDomain domain, final double latitudeOfOrigin,
             final long randomSeed) throws FactoryException, TransformException
     {
-        createCompleteProjection(new PolarStereographicA(), false,
-                  0.5,              // Central meridian
-                 latitudeOfOrigin,  // Latitude of origin
-                  0,                // Standard parallel (none)
-                  0.994,            // Scale factor
+        createCompleteProjection(new PolarStereographicA(),
+                6371007,            // Semi-major axis length
+                6371007,            // Semi-minor axis length
+                0.5,                // Central meridian
+                latitudeOfOrigin,   // Latitude of origin
+                NaN,                // Standard parallel 1 (none)
+                NaN,                // Standard parallel 2 (none)
+                0.994,              // Scale factor
                 200,                // False easting
                 100);               // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
index bf09139..5952a8a 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/TransverseMercatorTest.java
@@ -27,6 +27,7 @@
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
+import static java.lang.Double.NaN;
 import static java.lang.StrictMath.toRadians;
 import static org.apache.sis.test.Assert.*;
 
@@ -96,13 +97,16 @@
     @Test
     @DependsOnMethod("testTransverseMercator")
     public void compareEllipticalWithSpherical() throws FactoryException, TransformException {
-        createCompleteProjection(new org.apache.sis.internal.referencing.provider.TransverseMercator(), false,
-                  0.5,    // Central meridian
-                  2.5,    // Latitude of origin
-                  0,      // Standard parallel (none)
-                  0.997,  // Scale factor
-                200,      // False easting
-                100);     // False northing
+        createCompleteProjection(new org.apache.sis.internal.referencing.provider.TransverseMercator(),
+                6371007,    // Semi-major axis length
+                6371007,    // Semi-minor axis length
+                0.5,        // Central meridian
+                2.5,        // Latitude of origin
+                NaN,        // Standard parallel 1 (none)
+                NaN,        // Standard parallel 1 (none)
+                0.997,      // Scale factor
+                200,        // False easting
+                100);       // False northing
         tolerance = Formulas.LINEAR_TOLERANCE;
         compareEllipticalWithSpherical(CoordinateDomain.RANGE_10, 0);
     }
@@ -110,7 +114,7 @@
     /**
      * Creates a projection and derivates a few points.
      *
-     * @throws TransformException Should never happen.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     public void testSphericalDerivative() throws TransformException {
@@ -127,7 +131,7 @@
     /**
      * Creates a projection and derivates a few points.
      *
-     * @throws TransformException Should never happen.
+     * @throws TransformException if an error occurred while projecting a point.
      */
     @Test
     public void testEllipsoidalDerivative() 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,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 7dc9a57..efd82d0 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -807,6 +807,11 @@
         public static final short NonInvertibleMatrix_2 = 81;
 
         /**
+         * Can not invert the “{0}” operation.
+         */
+        public static final short NonInvertibleOperation_1 = 232;
+
+        /**
          * Transform is not invertible.
          */
         public static final short NonInvertibleTransform = 82;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index 222180f..714ccb5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -166,6 +166,7 @@
 NonEquilibratedParenthesis_2      = Missing a \u2018{1}\u2019 parenthesis in \u201c{0}\u201d.
 NonInvertibleConversion           = Conversion is not invertible.
 NonInvertibleMatrix_2             = Non invertible {0}\u00d7{1} matrix.
+NonInvertibleOperation_1          = Can not invert the \u201c{0}\u201d operation.
 NonInvertibleTransform            = Transform is not invertible.
 NonAngularUnit_1                  = \u201c{0}\u201d is not an angular unit.
 NonLinearUnit_1                   = \u201c{0}\u201d is not a linear unit.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index 4c43ae0..a3145ad 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -163,6 +163,7 @@
 NonEquilibratedParenthesis_2      = Il manque une parenth\u00e8se \u2018{1}\u2019 dans \u00ab\u202f{0}\u202f\u00bb.
 NonInvertibleConversion           = La conversion n\u2019est pas inversible.
 NonInvertibleMatrix_2             = Matrice {0}\u00d7{1} non inversible.
+NonInvertibleOperation_1          = Ne peut pas inverser l\u2019op\u00e9ration \u00ab\u202f{0}\u202f\u00bb.
 NonInvertibleTransform            = La transformation n\u2019est pas inversible.
 NonAngularUnit_1                  = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas une unit\u00e9 d\u2019angles.
 NonLinearUnit_1                   = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas une unit\u00e9 de longueurs.